Repository navigation
custody: role isolation behind [security].role_isolation (X-1, N-6, N-7, G-5; ADR-2122) - #12
Merged
Merged
Conversation
…licy.json Catalogue only; no product behaviour changes. config/egress-policy.json gains a `.register` key: 31 routes by which a prompt, code, a secret-adjacent string or a key leaves the box, each with what leaves, destination class, gate, accepted-egress record and notes, every claim cited path:line at 0919dc3. The ADR-2026 `paths` contract is untouched (egress-boundary: 25 passed). scripts/ci/render-egress-register.js renders the table into docs/SECURITY-profiles.md between markers; --check (wired into invariants.yml beside check-adr-index) fails on drift, an unmarked gap or a citation outside its file. Gaps are marked, not filled: 5 routes have no gate, 18 have no accepted-egress record. Primary-model egress is recorded as accepted per Q14; provider credentials are listed as devuser-class per Q8. Linked from docs/LAN-door-threat-model.md (G-10) and ADR-2090 (dated cross-reference, Decision unchanged). Co-Authored-By: jjohare <github@thedreamlab.uk>
a10f336 touched governed paths of both records (invariants.yml; the egress-policy.json `register` key) without touching either decision. Dated notes record what changed and what was checked; verified_commit bumped. Status axes unchanged. Co-Authored-By: jjohare <github@thedreamlab.uk>
b6c44c9 changed run-producer.sh (a governed path) without re-verifying the record; the invariants job has failed at the ADR index step since. The decision is unaffected. Co-Authored-By: jjohare <github@thedreamlab.uk>
… (W0) Tests first for custody X-1 step 1, workstream W0. All five fail against origin/main: - RC-X1-01: a probe planted in ~/workspace/.cargo/bin runs on the pre-W0 root PATH (control) and must not after _ab_root_path_sanitise; the sanitiser runs before the stage dispatch; the profile snippet appends the cargo bin for devuser shells only. - RC-X1-02: Stage B is one-shot per container start, guarded by a root 0700 state dir in sticky /tmp (not /run: that tmpfs is owned by uid 1000, so devuser can rename anything directly under it). - RC-X1-03: the baked /etc/group gives root no members (live check gated on a rebuilt image). - RC-X1-04: the docker socket chmod o+rw runs only with role_isolation off; on, the host inode is left alone and a widened socket is reported degraded. - RC-X1-05: allowlist fixtures, a fake daemon and the real docker CLI for the GET-only read proxy (ps/inspect/logs pass, exec/run/archive/export refused). Co-Authored-By: jjohare <github@thedreamlab.uk>
…(G-5) Adds `mirror_key`, the Rust side of the phone-key contract in config/hooks/nostr-live-mirror.cjs:200-221: child_sk = HMAC-SHA256(operator_sk, tag), tag defaulting to "agentbox-mirror-v1". RustCrypto hmac 0.12 over sha2 (hmac 0.12.1 was already locked via nostr-bbs-core, so the lock gains one edge and no crate). The JS is unchanged; the phone key does not move. tests/mirror_key.rs: 7 known-answer tests on synthetic keys, expected values agreed by Node crypto, Python hmac and @noble/curves. The aaaa vector's child pubkey is the existing CHILD_RECIPIENT fixture in tests/sovereign/egress-boundary.test.js. Scalar 6 (odd y) pins the hazard: the HMAC must take the env hex as supplied, not the even-y negated scalar identity.rs persists, or an odd-y house key would hand the phone a different child. Cargo.lock: hand-applied single edge. A local cargo resolve also bumps solid-pod-rs/nostr-bbs path deps to the sibling checkouts' versions; that drift is pre-existing (--locked fails on main too) and is not carried, because lib/nostr-pod-bridge.nix fetches the pinned revs. Co-Authored-By: jjohare <github@thedreamlab.uk>
…law-server (Q15) Q15 by evidence, 2026-10-03, read from the dreamlab relay (every stored event of kinds 31400-31403, 21 in total) and VisionClaw source, without touching any .env: - VisionClaw signs governance with ACSP_PANEL_NOSTR_PRIVKEY, else VISIONCLAW_NOSTR_PRIVKEY (elevation_actor.rs:199-200, decision_elevation_actor.rs:183-184, app_state.rs:1362-1363). - The only VisionClaw-shaped 31402s (acsp/events.rs tag set: priority, category, subject-kind) are signed by 11ed6422…663c (2026-08-11). - b4165401…2f7a signs only 31403 decisions, as the responder; it is [sovereign_mesh.operator].pubkey_hex, the operator's NIP-07 key. So the comments naming b4165401 "visionclaw-server governance publisher" were wrong, and VisionClaw still shares the house key: a K_broker is needed. Comment-only change; goldens are frozen fixtures and stay as captured. Co-Authored-By: jjohare <github@thedreamlab.uk>
…ion, mirror KAT dep) ADR-2012, 2019, 2020, 2023, 2028, 2029, 2033 and 2105 were tripped by comment-only changes to agentbox.toml (and setup/agentbox.default.toml for 2028/2029). ADR-2030 was tripped by nostr-pod-bridge gaining hmac 0.12, which is MIT OR Apache-2.0 and was already in the lock; the crate licence is unchanged and the licence gate passes. ADR-2012's note corrects the label at its :157-160 (b4165401 is the operator's 31403 decision signer) and confirms that the key split has not landed, so its review_trigger has not fired. Index check leaves only ADR-2103's pre-existing staleness, from run-producer.sh. That is also present on origin/main and is not this branch's. Co-Authored-By: jjohare <github@thedreamlab.uk>
…eld on D1-D3 Co-Authored-By: jjohare <github@thedreamlab.uk>
… (custody W0, bypass 2) devuser could make root run its code. Both boot stages run as root, and the entrypoint PREPENDED the devuser-owned ~/workspace/.cargo/bin to PATH (Stage A for PID 1 and every supervised child, Stage B again for [program:bootstrap]). With the supervisor socket at 0766, devuser could plant a binary there named after any tool the boot calls and `supervisorctl start bootstrap`. - _ab_root_path_sanitise runs once, above the stage dispatch, so it covers both stages: only /nix/store/* and the image's fixed system dirs survive; relative, dotted, globbed and devuser-owned entries are dropped, with a fallback when nothing is left. _ab_cargo_bin_on_path is deleted (rune is baked; nothing in the boot used the workspace cargo bin). - Stage B claims a one-shot sentinel by atomic mkdir in a root 0700 state dir that Stage A arms. A replay exits 0 as a no-op; an untrusted dir (wrong owner, loose mode, symlink, missing) makes Stage B refuse and exit 1. The dir is /tmp/.agentbox-root, not /run: /run, /var/run and /var/log are tmpfs mounts owned by uid 1000, so devuser can rename anything directly under them, root-owned or not; /tmp is root 1777 and its sticky bit stops that. Move it under /run/secrets once W1 makes that a root mount point. - The runtime-env and fish snippets now APPEND the cargo bin for devuser shells only (id -u guard), so a planted or stale workspace binary cannot shadow an image tool and a root shell sourcing the file is untouched. Tests: RC-X1-01 14/14, RC-X1-02 16/16 (both red at 495f29c). The second commit's regex fix in RC-X1-02 corrects the test, not the contract. Co-Authored-By: jjohare <github@thedreamlab.uk>
… REBUILD to undo)
The baked /etc/group read `root:x:0:devuser`, so every devuser process ran
with supplementary gid 0 and could read or write anything root left
group-accessible. Custody isolation (design §2.1: "devuser ∉ {root, docker}")
cannot hold while devuser is in group root.
This is the one W0 change that is not flag-reversible: /etc/group is baked
into the read-only image, so applying it and rolling it back both need
`./scripts/launch.sh rebuild dev`. It ships alone so it can be rehearsed and
reverted alone (git revert this commit, rebuild).
R4: a bounded scan of /etc, /opt/agentbox, /var/lib, /run, /var/run,
/var/log, /home/devuser and /tmp in the running container found no file that
devuser reaches only through group 0. The three group-0 sockets
(supervisor, tailscaled, nvidia-ctk hook dir) are all o+rw or o+rwx, so
nothing listed loses access. An unlisted reliance would show as EACCES after
the rebuild.
wheel:x:998:devuser is unchanged: sudo is inert under no-new-privileges.
Tests: RC-X1-03 static 2/2 (red at 495f29c); live `id -G devuser` check
runs with AGENTBOX_RC_LIVE=1 in the rebuilt image.
Co-Authored-By: jjohare <github@thedreamlab.uk>
…n in NIP-07 The container never mints, holds or backs up K_admin; only its public key comes back for the forum admin list. Adds a key table whose K_admin row awaits that public key. D1 awaits the owner's age recipient; D3 open. Co-Authored-By: jjohare <github@thedreamlab.uk>
…ustody W0, bypass 1) devuser could drive the host's Docker daemon, which is root on the host: the entrypoint ran `chmod o+rw /var/run/docker.sock` on every boot. Any in-container custody boundary is void while that holds. - [security].role_isolation (boot-class, default false) is read once in Stage A with the existing manifest reader and exported as AGENTBOX_ROLE_ISOLATION, so supervisord and every program agree. - _ab_docker_socket_access: flag off runs the original `chmod o+rw "$sock" 2>/dev/null || true` and nothing else (byte-identical behaviour). Flag on never chmods the socket in either direction (it is the host inode, R2); if devuser can still connect (owner, group or other bits) it records degraded:docker-socket in /run/secrets/role-isolation.state and logs ROLE-ISOLATION-DEGRADED, fail loud, not fatal. The state write removes and re-creates with O_EXCL, so it never follows a planted symlink. - config/docker-read-proxy.cjs (node:http + node:fs, no new dependency): GET/HEAD only on /_ping, /version, /info, /containers/json, /containers/<id>/json and /containers/<id>/logs, with an optional /v1.N prefix. Everything else gets 403 and never reaches the daemon, including the read-shaped exfiltration routes (archive, export, attach). Encoded paths, dot segments, absolute-form and Upgrade are refused. It binds /run/docker-ro.sock with umask 0111 (no chmod race), then drops to uid/gid 65534 with only the socket's group and refuses group 0. - [program:docker-read-proxy] runs it via `env -i`, so the .env secrets PID 1 inherits never reach it. It exits 0 at once while the flag is off. TODO(custody W1): move it into the role registry. - Under the flag, Phase 8 exports DOCKER_HOST=unix:///run/docker-ro.sock for devuser shells, so `docker ps/logs/inspect` keep working and `docker exec` moves to the host shell (Q1 default). - Compose override comments state the real reach of group_add and R2. - tests/runtime-contract/README.md: RC-X1 rows and the host-half contract for R2, for the W6a rehearsal to pick up. Tests: RC-X1-04 16/16 plus a live check that is skipped until a rebuilt image runs with the flag on; RC-X1-05 6/6, including the real docker CLI against a fake daemon (ps, inspect and logs pass; exec and run fail; no mutating request reaches the daemon). Both were red at 495f29c. Co-Authored-By: jjohare <github@thedreamlab.uk>
The acceptance gate for [security].role_isolation (custody design §4), written before any isolation code so every later workstream has a test to turn green. - scripts/activation/role-isolation-rehearsal.sh: container half, as devuser. (a) role secrets and at-rest copies refused with EACCES, /run/secrets a root 0711 mount point; (b) role programs RUNNING under their role uid, environ unreadable; (c) pods NIP-98 and a dry-run forum event made by the identity port verify with NostrBridge.verifyNip98 / nostr-tools, relay NIP-42 AUTH accepted, dm_unwrap / foreign URL / generic sign refused, one sign receipt per call; (d) producer under its role uid with a block younger than 2 x interval; (e) classified variable names absent from argv, devuser environ, supervisor environment= lines, identity.env and /var/log (names, never values); (f) not in group 0, no docker, no sudo, no role gid on the socket's group. - scripts/activation/role-isolation-rehearsal.host.sh: host half, as root on the host; refuses inside a container and on a dirty scripts/activation. Root probe piped to docker exec with a /nix/store-only PATH. Role-uid reads via setpriv, root-view environ names, host getent 960-979 collisions (§8.5), host socket mode recorded (Q2). - docs/estate-closeout/schema/x1-rehearsal.schema.json: receipt schema; evidence keys naming a value-bearing field are refused. - tests/config/role-isolation-rehearsal.test.sh: scratch root, fake /proc and supervisor; PASS, flag-off STAGED, one FAIL per check, two required-but-skipped FAILs, value-free receipts, host half. 17 cases. Exit 0 PASS, 2 STAGED (flag off), 1 FAIL; required-but-skipped is FAIL. Co-Authored-By: jjohare <github@thedreamlab.uk>
…rows would fail
Container half run as devuser on image sha256:ef292f55… with
[security].role_isolation absent (false): exit 2, every row not_applicable,
35 of 42 would fail. Every check (a)-(f) is red, which is the starting line:
(a) /run/secrets devuser-owned 0700 on /run's filesystem; no per-role dirs;
nostr.key, agentbox-core.json, the producer key and cookie and both
volume directories open to devuser.
(b) nostr-relay, nostr-gateway, nip98-proxy and sidestr-faucet run as uid
1000 with readable environ; [program:sidestr-producer] is STOPPED.
(c) no identity port socket; nothing attempted, all FAIL.
(d) the dreamlab tip is fresh, but its producer is not running under
supervisor (a hand-started process carries the chain).
(e) 942 classified-name hits across devuser environs (AGENTBOX_NSEC,
AGENTBOX_PRIVKEY_HEX, JUNKIEJARVIS_PRIVKEY_HEX, NIP98_PROXY_*,
TAILSCALE_AUTHKEY); argv, supervisor text and logs clean.
(f) devuser in group 0; docker version answers; the socket's gid 965 equals
the §2.2 uid of ab-faucet-dreamlab.
Co-Authored-By: jjohare <github@thedreamlab.uk>
… config config/custody/g5-key-split.json is the single place the owner's new PUBLIC keys land (null until supplied; K_admin deliberately absent, D2). tests/sovereign/g5-key-split.node-test.js reads it and checks the real path: agentbox.toml -> agentbox-manifest nip98-config -> proxy.mjs. - G5-1 shape: public keys only, no secret-shaped fields, known verifiers. - G5-2 each supplied key is a valid BIP-340 x-only point (noble), new, and distinct from the house key. - G5-3 projection keeps 11ed6422 (until withdrawn) and carries each key. - G5-4 the running proxy admits house key + each key, 401s an outsider. - G5-5/6 dual-admit and withdrawal mechanics with a synthetic candidate. The session-cookie path enforces the same allowlist as NIP-98 and needs no identity private key (its MAC uses a pinned test secret), so a public key alone proves admission. 6 pass, 2 skip until K_browser is supplied; mutation-checked against a key missing from the toml (4 fail), a typo (5 fail), a reused house key (1 fail), early house-key removal (3 fail), and a correctly added key (8 pass, 0 skip). Handoff note updated for D2/D3: keys are minted in their holders, so the owner, who holds the age identity, backs them up and tests the restore. VisionClaw sets only VISIONCLAW_NOSTR_PRIVKEY (governance via fallback, NostrBridge, beads), so K_broker replaces that variable; the mint uses nostr-tools inside visionclaw_container and the .env swap reads the secret from a file, never argv. browsercontainer's /tmp profile has no Podkey (Proton Pass is present); the owner confirms where the copy actually lives. Co-Authored-By: jjohare <github@thedreamlab.uk>
…(G-5) Adds `mirror_key`, the Rust side of the phone-key contract in config/hooks/nostr-live-mirror.cjs:200-221: child_sk = HMAC-SHA256(operator_sk, tag), tag defaulting to "agentbox-mirror-v1". RustCrypto hmac 0.12 over sha2 (hmac 0.12.1 was already locked via nostr-bbs-core, so the lock gains one edge and no crate). The JS is unchanged; the phone key does not move. tests/mirror_key.rs: 7 known-answer tests on synthetic keys, expected values agreed by Node crypto, Python hmac and @noble/curves. The aaaa vector's child pubkey is the existing CHILD_RECIPIENT fixture in tests/sovereign/egress-boundary.test.js. Scalar 6 (odd y) pins the hazard: the HMAC must take the env hex as supplied, not the even-y negated scalar identity.rs persists, or an odd-y house key would hand the phone a different child. Cargo.lock: hand-applied single edge. A local cargo resolve also bumps solid-pod-rs/nostr-bbs path deps to the sibling checkouts' versions; that drift is pre-existing (--locked fails on main too) and is not carried, because lib/nostr-pod-bridge.nix fetches the pinned revs. Co-Authored-By: jjohare <github@thedreamlab.uk>
…the isolated supervisor config (custody W1) config/role-accounts.json is the single source for eight role accounts (uid = gid, 960-979, 965 reserved: it is the host docker gid compose group_adds, so a role there would sit in the Docker socket's group). `agentbox-manifest role-accounts` validates it, prints the passwd/group lines flake.nix bakes, and derives /etc/supervisord.isolated.conf and the /run/secrets delivery plan from the rendered /etc/supervisord.conf. The isolated config is a pure function of today's, so the two cannot drift: only the role programs' user= and environment= lines change. /run/secrets becomes its own root-owned tmpfs (mode 711) in both modes. With [security].role_isolation on, the entrypoint keeps it root-owned, delivers each role's secrets into /run/secrets/<role>/ (0500 dir, 0400 files, owned by the role) from the secrets volume and the classified PID-1 variables, unsets those variables before exec, moves W0's Stage-B guard to /run/secrets/.root-guard, and execs the isolated config. With the flag off every statement is today's (tests/config/role-isolation-boot). Tests without Nix: role-isolation-supervisor (19), role-secrets-delivery (26), role-isolation-boot (22), 12 Rust unit tests; W0's RC-X1 suite and the existing entrypoint suites still pass. Nix was not evaluated here. Co-Authored-By: jjohare <github@thedreamlab.uk>
The key the entrypoint already reads (W0) now exists: agentbox.toml and setup/agentbox.default.toml ship it false, the schema types it, and the catalogue carries module role-isolation. Apply class is boot, not rebuild: accounts, both supervisor configs and the /run/secrets mount ship in every image and the entrypoint picks at boot (ADR-039's definitions); the image that first carries W1 needs one rebuild, which the summary says. Validator and catalogue check pass. Co-Authored-By: jjohare <github@thedreamlab.uk>
… (custody W3)
`nostr-pod-bridge serve-identity` holds the sovereign key (and, when its file
is present, JunkieJarvis's) and answers on a unix socket,
/run/agentbox/identity.sock (0660; group from AGENTBOX_IDENTITY_SOCK_GID).
Every connection is authorised by the SO_PEERCRED uid against the checked-in
config/custody/identity-port-acl.json, which maps uid -> named operation ->
keys -> kinds. Socket mode only narrows who can connect.
Closed operation list (ADR-2101 :110-111): pubkey, nip98, sign_event,
forum_event, nip42_auth, mirror_key. Names follow W6a's rehearsal and design
§2.5 (the brief's nip98-header / sign-event / mirror-key are these). Anything
else, including dm_unwrap and a generic `sign`, is refused as
{"refused": {"op", "reason"}}.
- The ACL loader refuses unknown ops, undeclared keys, kinds on kindless ops,
and kinds 27235, 22242, 31400-31405 and 38414 outright.
- nip98: URL must sit under the key's prefix allowlist (origin-exact,
segment-bounded); the u tag is cut at ?/# exactly as buildNip98Header does;
created_at is the port's.
- nip42_auth: relay must be in the key's relay list.
- mirror_key: W8's mirror_key::child_secret (cherry-picked unchanged), tag
from the server env only, so the port is not an HMAC oracle. The child
secret is returned only on a `secret: true` grant (devuser: the live-mirror
hook signs its self-DM with it, nostr-live-mirror.cjs:487).
- One receipt JSONL line per decision under /var/lib/agentbox/events/sign/,
never content, challenge, header or key material. If an admission's receipt
cannot be written, the request is refused and the signature discarded.
- Signing is nostr-bbs-core sign_event only; no new primitive. url, base64
and zeroize were already in the lock graph (three edges, no new crate; lock
hand-applied like W8's, because the sibling path deps drift on a resolve).
- `sign-request <op>`: stdin JSON -> stdout JSON; exit 1 refused, 2 no port.
- AGENTBOX_ROLE_ISOLATION != 1: serve-identity prints one line and exits 0
without reading the ACL or any key.
Tests: 21 unit (policy over fake peers, per-caller kinds, malformed requests,
secret absent from responses and receipts, odd-y key), 8 integration against
the real binary (uid outside the ACL refused through real peer credentials,
the rehearsal's (c) contract, socket 0660 + group, nothing secret on argv or
environ, world-writable socket dir refused, flag off binds nothing).
tests/contract.rs now recurses into src/ module directories.
Co-Authored-By: jjohare <github@thedreamlab.uk>
…ody W3)
pod-signer gains an identity-port source: `nostr-pod-bridge sign-request
nip98` with {key: core, method, url, payload_sha256}. It is selected by
[security].role_isolation (or AGENTBOX_ROLE_ISOLATION=1), or explicitly by
[integrations.solid_pod_rs].sign_source = "identity-port". When selected it is
the only source: no identity file or stack key is read. The payload hash is
computed exactly as NostrBridge.buildNip98Header computes it.
ADR-2064 is unchanged: a refused port (exit 1), an absent one (exit 2) or any
failure to answer throws, and the adapter turns that into SigningUnavailable
before a byte is sent. sign_requests = false still returns null before any of
this runs, and with the flag off and no sign_source nothing new executes.
tests/sovereign/identity-port.node-test.js spawns the real serve-identity with
a throwaway key and verifies with the real NostrBridge.verifyNip98: CLI header
with bound body; every pods op through resolveAdapters (both impls) signed by
the port with an empty identity root; no socket and foreign URL both fail
closed with zero requests; flag off never consults the port. Skips, and says
so, when the binary is not built. Added to test:node.
Co-Authored-By: jjohare <github@thedreamlab.uk>
…custody W3) W1's isolated supervisor config has not landed, so the [program:identity-port] block ships as config/supervisor/identity-port.conf.fragment: Nix-templated text with @IDENTITY_USER@ and @IDENTITY_PORT_GID@ for W1's role registry, plus the directory, key-file and group layout Stage A must provide. `env -i` keeps PID 1's inherited .env out of the process. Nothing reads the file at run time. Co-Authored-By: jjohare <github@thedreamlab.uk>
…(custody W3) ADR-2030 was tripped by nostr-pod-bridge gaining url, base64 and zeroize, all MIT OR Apache-2.0 and already in the lock. The licence gate reports OK for 9 directories. ADR-2078 was tripped by pod-signer's flag-gated identity-port source. Same sovereign key, new custody, and ADR-2064's fail-closed behaviour is preserved. The amendment design §5 asks for is left to ADR-2122. The other stale records in the index were tripped by W0's flake and entrypoint paths, or (ADR-2103) were already stale before this branch. None of them is this branch's. Co-Authored-By: jjohare <github@thedreamlab.uk>
…rehearsal (custody W1) The W6a rehearsal reads /etc/agentbox/role-accounts.json when the image ships it and otherwise derives a table whose faucet uid lands on 965, the host docker gid. Shipping the real table makes the rehearsal check the accounts that were built. Co-Authored-By: jjohare <github@thedreamlab.uk>
… W3 follow-up)
W3's identity port listens at /run/agentbox/identity.sock, the path the W3
brief fixes (root-owned /run/agentbox with the sticky bit; the socket 0660 to
the callers' group). Design §2.5 had /run/secrets/port/identity.sock, which is
what the rehearsal assumed. Check (c) now reads RH_IDENTITY_SOCK, else
AGENTBOX_IDENTITY_SOCK (the variable the client itself reads), else
/run/agentbox/identity.sock.
The contract header now matches the implementation. A refusal exits 1 with
{"refused": {"op", "reason"}}, no port exits 2 with {"unavailable": ...},
and receipts go to sign-<date>.jsonl. The refusal row prints .refused.reason,
falling back to a bare string. The fixture's fake client returns the same
shape and exit code.
tests/config/role-isolation-rehearsal.test.sh: 17 passed, 0 failed. Driven
against the real serve-identity, all nine (c) rows pass (socket, pubkey,
pods-nip98 via NostrBridge.verifyNip98, forum-event, relay-nip42, three
refusals, 7 receipt lines).
Co-Authored-By: jjohare <github@thedreamlab.uk>
…ity port (proposed, partial, inactive) Records the custody design's sections 2-5 as the decision, with the rehearsal (W6a) as acceptance and an honest list of what is owed before the flag may be turned on (W2 at-rest lockdown above all: until it lands devuser can still read the at-rest copies). Deviations from the design are named with reasons: uid 965 reserved (host docker gid), role homes under /run/secrets, no role-exec yet, apply class boot. Re-verifies the 21 records whose governed paths custody W0 and W1 changed (ADR-2103 was stale from b6c44c9 on main and is re-verified on its own evidence). ADR-2029 records that W0 retired D4's global cargo-bin prepend for root; ADR-2020 records the identical-in-effect re-check for the new gate. ADR-2066 gets a dated note on the devuser key path under the flag. Index regenerated. ADR-Ratchet: ADR-2122 documents custody X-1 step 1 (swarm custody-isolation-2026-10-03, W1) Co-Authored-By: jjohare <github@thedreamlab.uk>
…ustody/integration
…s into ADR-2122
Queen's disposition 4 (§13): W3's identity-port section goes into ADR-2122, which W1 created, as
a dated subsection. W3 wrote no prose section, so "The identity port as built (W3) — 2026-10-03"
is written from the merged code: program, closed operation set, ACL callers, key files, socket,
receipts and the pods consumer. "Integration resolutions — 2026-10-03" records the config name,
the single role table, the socket group from the table, and the deviation from a root-owned
/run/agentbox (devuser owns /run), with the reason.
§4 now points at the built set instead of the design's longer operation list. The deviation
bullet that my rename had made self-contradictory ("roles, not roles") is rewritten as resolved.
Status moves the pods cutover to built and leaves W3b owed. The consequence count goes to 9 group
lines. verified_paths gains the port, ACL, pods signer and rehearsal; verified_commit is
re-stamped once at the integration head. W2's env-scrub section and the W9 pointer are folded in
when those branches land.
Co-Authored-By: jjohare <github@thedreamlab.uk>
…tody integration) The subcommand's help said role groups have no members, ever; that still holds, but the command now also prints the table's shared groups (ab-identity-port) with their listed members. Co-Authored-By: jjohare <github@thedreamlab.uk>
…dy integration) shellcheck 0.11.0 -S warning on every shell script the integration changes: six warnings, all in W1's tests. SC1090 on the deliberate sources of config/lib/role-custody.sh gets a source= directive; the stub's AB_RC_FAILURES is read by the eval'd entrypoint block (SC2034 disabled with that reason); an unused read target becomes _. config/entrypoint-unified.sh carries the same 20 warnings as main, none new. Suites unchanged: boot 22/22, supervisor 23/23. Co-Authored-By: jjohare <github@thedreamlab.uk>
The queen's order puts W5 after W1 and before W3; it landed after W3, W7a, W8 and the integration resolutions were already on this branch. The merge was clean (W5 is based on W1 and touches lib/sidestr-upstream.nix, the runner and ADR-2103 only), so the tree is the same as with the intended order; rebuilding the branch would only have re-done every resolution. ADR-2103 now carries three dated 2026-10-03 notes (main, W1, W5); verified_commit is re-stamped once at the integration head. Co-Authored-By: jjohare <github@thedreamlab.uk>
Conflicts, all resolved additively:
- docs/SECURITY-profiles.md: W7a's prompt egress register and W2's environment classes are two
new sections; both kept, register first.
- management-api test:node: the union of the suite lists (W2 adds role-secret.node-test.js).
- tests/config/role-isolation-boot.test.sh: W2's scrub stub and the deliver -> pick -> scrub
order are taken, with W2's supervisord.isolated.conf renamed to supervisord.roles.conf
(integration resolution 1).
- ADR-2122: W2's §3a, its status updates and its dated note are kept beside W3's and the
integration sections; verified_paths is the union of both sides (W2's side had dropped the
identity-port and rehearsal paths).
- ADR-2012, 2013, 2026, 2028, 2029, 2033, 2078: every dated note kept; verified_commit is
re-stamped once at the integration head.
The queen's loader rule ("file wins; the bare variable only flag-off; under the flag a bare
variable is logged ROLE-ISOLATION-LEAK and ignored") is W2's as merged:
management-api/lib/role-secret.js resolveRoleSecret and nostr-pod-bridge::role_secret read
<NAME>_FILE, then $AGENTBOX_SECRETS_DIR/<NAME>, then the bare variable only with the flag off.
nip98-proxy loads that resolver and fails closed without it; the gateway and nostr-send go
through config/hooks/lib/operator-key.cjs. W3 does not touch these loaders, so nothing
collided there.
Co-Authored-By: jjohare <github@thedreamlab.uk>
…lassifies by W2's ROLE class (custody integration) W2's env-secret inventory cross-checks every variable name the code reads. After the merge it failed on six names W3 and the integration introduced, none of them secret: the port's ACL path, key dir, receipt dir, socket path and socket gid, and pod-signer's AGENTBOX_POD_BRIDGE_BIN. They are NON_SECRET, listed exactly (KEY_DIR is secret-shaped, so no pattern may cover it). Inventory PASS: 648 names, ROLE 11, DEVUSER_CLASS 35, NON_SECRET 602; its node test 13/13. The rehearsal's check (e) classified names from W1's from_env alone and, in my earlier commit, dropped AGENTBOX_AGENT_PRIVKEY_HEX on the grounds that management-api reads it. W2 settles that the other way: config/custody/env-classes.json puts it, OPERATOR_NOSTR_PRIVKEY and AGENT_PRIVKEY_HEX in ROLE ab-identity (w2_only, delivered by W2's capture) and TAILSCALE_AUTHKEY in ROLE root. normalise_registry now takes each role's names as W1's from_env plus W2's ROLE entries for that role, and the root list from the ROLE class instead of a hard-coded constant. A missing ROLE class exits 1. The --print-registry case pins both. Rehearsal 21/21. Co-Authored-By: jjohare <github@thedreamlab.uk>
…custody/integration
…r plant one beside it (custody integration) Asked for by the queen when accepting the correction to §13: the property the disposition got wrong is that a directory is only as safe as its parent. Check (a) now probes each socket directory the role table declares (groups -> <secrets_root>/<group>). It records the directory's owner, group and mode (0750, the owner role, the group's gid), then as devuser attempts `mv -T` of the directory and a `mkdir` beside it. Both must be refused. A directory that does move is moved straight back and the row fails. Nothing is read, and nothing survives the probe. Fixture: the scratch /run/secrets is 0555, standing for the root 0711 mount the runner cannot write. Case 17 leaves it writable, the /run/agentbox situation, and (a) fails with "rename succeeded; create beside planted", the socket intact and no probe entry left. The PASS fixture records "rename refused; create beside refused". Rehearsal 23/23. shellcheck clean. ADR-2122 gains two dated notes. The first is the correction itself: /run is uid=1000 tmpfs (docker-compose.yml:109, flake.nix:3273), while /run/secrets is its own uid=0 mode=711 tmpfs (:110, :3278). It also notes that a root-owned /run/agentbox would have broken bootstrap-seal's /ready signal. The second is the W9 pointer: K_browser lives in the browser sidecar's Podkey and is recorded in browsercontainer/README.md; no role account here holds it. Co-Authored-By: jjohare <github@thedreamlab.uk>
…integration) `mkdir -p -m 0700` applies the mode only to the deepest directory, which was the intent, but shellcheck flags it as the only warning the integration adds to config/entrypoint-unified.sh. The capture now creates the secrets root with mkdir -p (as before) and the role directory with mkdir -m 0700, and the chown 0:0 and chmod 0700 that follow are unchanged. Behaviour is the same. RC-X1-06 30/30, role-isolation-boot 22/22, check-secret-not-in-env PASS. Co-Authored-By: jjohare <github@thedreamlab.uk>
) Queen's disposition 5 (§13): keep every dated note and give each record one verified_commit, at the integration head. `adr-index-gen --check` named 25 records stale after all eight merges and the resolutions. Each gets one dated note listing exactly the commits that touched its governed paths since its previous stamp (from `git log <stamp>..3b54129 -- <path>`), says why its decision is untouched, and is stamped 3b54129. ADR-2122's note also records the suite totals at that head; it stays proposed/inactive until the owner's rebuild passes the rehearsal. Earlier same-day notes from main, W1, W2, W3, W5, W7a and W8 are all kept. No record carries two verified_commit lines. adr-index-gen --check and --check-index: 114 valid, README in sync; adr-ratchet OK. Co-Authored-By: jjohare <github@thedreamlab.uk>
The PR's CI failed three jobs; each fix is behaviour-neutral: - role_accounts.rs: DirSpec.purpose was never read (clippy dead_code). The 'check' summary now prints one line per created directory with its purpose, as it already did for roles and shared groups. - role_accounts.rs: inserts.sort_by(b.cmp(a)) -> sort_by_key(Reverse). Both sorts are stable, so the rendered roles config is byte-identical. - role_accounts.rs test: useless format! on a literal. - flake.nix: 'parent = c.parent;' -> 'inherit (c) parent;' (statix W04). This line came from main (f746541) after main's last flake-check run; flake-check runs only on PRs touching the flake, so this PR is the first to trip it. Co-Authored-By: jjohare <github@thedreamlab.uk>
32cedf9 touched flake.nix (one statix inherit) and role_accounts.rs (clippy only). Each record gets a dated note; every prior note is kept and each record carries one verified_commit. Co-Authored-By: jjohare <github@thedreamlab.uk>
This was referenced Oct 3, 2026
…how the owner turns it on The docs pass for swarm custody-isolation-2026-10-03. Everything is described as staged: nothing here is live until the owner's rebuild and a passing pair of rehearsal receipts. - SECURITY-profiles.md: a role-isolation section with the role table, the /run/secrets mount and delivery, the identity port, the environment classes under the flag, the flag (default off, boot-class), and what is still open (at-rest custody, W3b, W4, the AoE share, Q4, the repo .env). Cited at d03defb. - docs/developer/role-isolation-runbook.md (new): rebuild, post-rebuild checks, the flag-off baseline, the host socket prerequisite, flag on, both rehearsal halves, receipt paths, rollback, and the sidecar's Podkey mint and unlock steps. It states that the flag-on rehearsal is expected to fail at this head. - Container reference: the workspace layer (docker exec is no longer a monitoring path from inside under the flag), AGENTS.md runtime gotchas (roles config, serve-identity and docker-read-proxy EXITED while off, podkey-loader), and BASELINE-container 0.5.3 (new programs, the flag qualifier, the baked producer, the sidecar loader). - AGENTS.md rules: one checkout per worker; the two supervisor configs differ only in user= and environment=. - configuration.md: the flag entry now lists the 11 ROLE variables, serve-identity and the Docker change, and links the runbook. - README.md: the producer runs the baked pinned upstream, not a checkout. - sovereign-stack.md: the identity check printed AGENTBOX_NSEC and AGENTBOX_BRIDGE_SK with the flag off; it now greps public names only. No ADR governs the edited files, so no record is re-verified. adr-index-gen --check: 114 valid; adr-ratchet: OK. Co-Authored-By: jjohare <github@thedreamlab.uk>
The workspace instruction layer gained a role_isolation paragraph; tier projection is unchanged. instructions-layers 9/9; adr-index-gen --check clean. Co-Authored-By: jjohare <github@thedreamlab.uk>
docker-compose.override.yml listed AGENTBOX_NSEC, AGENTBOX_PRIVKEY_HEX and TAILSCALE_AUTHKEY under environment:, and docker-compose.hp.yml the first two. Both files already load .env by env_file, which carries the same values; the environment: entries were a second, explicit route into PID 1 beside the one the role_isolation boot scrub handles. Removed, with a comment saying where the values come from. TAILSCALE_AUTHKEY was not in the brief but is ROLE (role root) in config/custody/env-classes.json, and the gate flags it, so it goes too. New gate tests/security/compose-role-env.test.mjs: every docker-compose*.yml, list/map/flow environment: forms, names checked against the ROLE class. Red 14/2 -> green 16/0. Co-Authored-By: jjohare <github@thedreamlab.uk>
…he plan The brief (diagrams pass) said (d) read .chain/.port/.enabled from role-accounts.json roles. That held at 3cd5614, which the committed receipt x1-rehearsal-20261003T090708Z was taken against; 860f58f already derived chain/port/interval in normalise_registry. What was still wrong: `enabled` meant only "the plan delivers this role something", and the manifest's [sidechain].enabled / [sidechain.<name>].enabled was never read. The live /etc/agentbox.toml is a bind of the repo file, so it can drift from the image whose plan was resolved at build time, and (d) would probe (or skip) the wrong chains without saying so. And with no chain on, (d) failed "no enabled producer" instead of having nothing to check. Now each producer role carries enabled (manifest; [sidechain].enabled dominates, a missing key is off) and baked (plan rows). (d) probes when both are true, fails naming the rebuild when they disagree, and records a pass when no chain is enabled and none baked. tests/config/role-isolation-rehearsal.test.sh, new cases 18-21, case 18 against the shipped agentbox.toml [sidechain] tables (dreamlab on :3450, dreamlab-txbt4 off at 3451 and not probed). Red 23/5 -> green 28/0. shellcheck not available in this container; CI's ShellCheck job covers it. Co-Authored-By: jjohare <github@thedreamlab.uk>
Nothing in CI ran RC-X1-01..05, tests/sovereign/role-secret.node-test.js (path-filtered in contract-tests but no step), the nostr-pod-bridge crate (identity port, role isolation) or the rehearsal test. invariants.yml:84 claimed "the loader and consumers run in contract-tests"; they did not. invariants.yml: RC-X1-01..05 beside RC-X1-06 (scratch-root, no Docker, no root), compose-role-env (W10 fix 1); paths add tests/runtime-contract/RC-X1-*, config/docker-read-proxy.cjs, config/custody/**. The :84 label now names a step that exists. contract-tests.yml: step "Role-secret loader and consumers (custody W2)" in the contract job; new job `custody`. nostr-pod-bridge reaches nostr-bbs-core and solid-pod-rs by ../../../../ path deps, so the job rebuilds lib/nostr-pod-bridge.nix's layout (agentbox at project/agentbox, the public siblings beside it) at the revisions read from that file, then cargo test --locked (whole crate), the node identity-port test against the built binary (fails on any skip), and the rehearsal test. Paths cover the crate, its nix pins, config/custody, the role table, agentbox.toml, the rehearsal scripts, test and receipt schema. Verified locally in the same layout at the pinned revs: crate 209/0 (identity_port 8, lib 99), identity-port.node-test 10/0/0 skipped, role-secret 23/0, rehearsal 28/0 with NODE_PATH and no mcp/node_modules, RC-X1-01 14, -02 16, -03 3, -04 17, -05 6, -06 30, all 0 failed. Co-Authored-By: jjohare <github@thedreamlab.uk>
… isolation flake.nix baked wheel:x:998:devuser and sudoers.d/devuser "NOPASSWD: ALL" into every image. no-new-privileges:true neuters setuid today, but that is one compose line from undone, and then sudo defeats every role boundary. /etc is baked into a read-only rootfs and the flag is boot-class, so the gate cannot live in the flake alone: - bake: config/bake-devuser-privilege.sh <etc> <0|1>, called by flake.nix with roleIsolationBaked = securityCfg.role_isolation or false. Off: group, sudoers and the drop-in byte-identical to before (the heredoc keeps wheel:x:998:devuser). On: devuser leaves the wheel and root member lists (others kept), sudoers keeps root + includedir, no drop-in. - boot: _ab_devuser_privilege_check in the entrypoint's W0 phase, after the docker-socket check. Flag on in an image baked off (a flip without the rebuild) records degraded:devuser-sudo in /run/secrets/role-isolation.state and logs ROLE-ISOLATION-DEGRADED devuser-sudo naming the rebuild; fail loud, not fatal, like docker-socket. Flag off: no-op. Conservative: any NOPASSWD grant to devuser, %devuser, %wheel or ALL counts. tests/runtime-contract/RC-X1-07.sh, fake root seeded from the real heredoc, the check's state file and stderr as recorder. Red 0/4 (bake absent) -> green 21/0 (live case skips without AGENTBOX_RC_LIVE). Wired into invariants.yml. RC-X1-01..06, role-isolation-boot 22/0 and the check-* gates still pass. Nix not evaluated here (no nix in this container). Co-Authored-By: jjohare <github@thedreamlab.uk>
…c91e09) ADR-2122: dated section "Gap fixes after the diagrams pass (W10)", one entry per fix with its sha; a new Invariants row (devuser not in wheel, no sudoers grant); the activation consequence (dropping devuser's sudo needs a rebuild with the flag, the boot reports degraded:devuser-sudo otherwise); verified_paths add the bake, RC-X1-07, the compose gate and the two compose files. ADR-2002, 2009, 2012, 2013, 2028, 2029, 2031, 2033, 2082, 2091, 2092, 2093, 2118, 2119, 2120, 2121: tripped only by W10's edits to flake.nix, the entrypoint, invariants.yml or the compose overlays; each note says which edit, and that the record's own decision is untouched. adr-index-gen --check and --check-index: 114 valid, README in sync. adr-ratchet: OK. Co-Authored-By: jjohare <github@thedreamlab.uk>
…e move (custody W2b, W4) Design 3.2-3.4 was never built: with role_isolation on, the at-rest copies on the volumes stayed devuser's and check (a) of the rehearsal failed. - Registry, derived not hand-listed: `role-accounts isolate` now emits `atrest <role> <path> <legacy|->` for every resolved file secret and each role's `at_rest` (agentbox-core.json, zone-keys.json), and `atrestdir <path> <mode>` for `at_rest_dirs` (secrets/ 700, identities/ 711: the pay402/Hitch spend keys there stay devuser-class under Q6). A secret's `legacy` path (the workspace treasury keys) is read from the canonical volume copy under the flag when the program's environment still names it. - config/lib/role-custody.sh: ab_custody_migrate (flag on) copies an absent canonical copy from its legacy twin once (umask 077, fsync, size and in-memory byte compare, rename; legacy never touched), refuses a symlinked, hard-linked, empty, oversized or first-sighting-differs copy (not re-owned, not delivered), hands each copy to its role 0400 and the dirs to root, records pre-flag modes once in a root-only record and paths only in /run/secrets/role-isolation.migrated. ab_custody_revert (flag off) hands them back from a trusted record; never migrated means no change. Both idempotent: unchanged owners and modes are not re-set (ctime stable). - Entrypoint: the custody step runs after the /run/secrets prep and the volume-root chown, before `nostr-pod-bridge bootstrap`; the volume loop leaves secrets/ alone under the flag; the identity file is ab-identity 0400 after the bootstrap under the flag. - W4: producer, mirror and faucet state move to the agentbox-events volume under the flag (`dirs` with `seed` rows, copied once, source kept; producer dirs group devuser 2750, umask 027, so the mirror reads through the group). The role's HOME is tmpfs, so the workspace default restarted the chain and emptied the faucet ledger every boot. Flag off with the custody copy ahead, producer and faucet refuse rather than fork or re-grant. - The rehearsal takes its at-rest set from the registry rows; its fixture plan follows the generator; case 18 runs the real migrate and (a) passes, 18b shows (a) failing without it. - agentbox.toml: the role_isolation comment no longer says the identity port is not built. Tests: role-custody-migrate 31/31 (new), rehearsal 28 -> 30 (cases 22, 22b), delivery 30, boot 22, supervisor 23, manifest cargo 121 (4 new + 1 seed), RC-X1-01..06 green, env-secret-inventory PASS. Co-Authored-By: jjohare <github@thedreamlab.uk>
…ove (f93586b) - ADR-2122: a dated section for W2b/W4 (derived registry, migrate/revert, refusals, the sidechain state move and its flag-off guard), the Status and Deviations bullets it closes (identities/ 0711 for Q6's spend keys; mirror reads through group devuser, no ab-sidestr-read), the remaining owed items named, and the Acceptance sentence corrected to the receipt paths the scripts write (docs/estate-closeout/x1-rehearsal[-host]-<UTC>.json). - 17 records whose governed paths f93586b touched (agentbox.toml comment, the entrypoint's three custody blocks, run-producer.sh): one dated note each, verified_commit bumped. - docs/estate-closeout/README.md: x1-rehearsal-20261003T090708Z.json is superseded (faucet at uid 965); the receipt itself is not edited. adr-index-gen --check and --check-index: 114 valid, index in sync; adr-ratchet OK. Co-Authored-By: jjohare <github@thedreamlab.uk>
…ain state move) Co-Authored-By: jjohare <github@thedreamlab.uk>
Seven SC2034 warnings arrived with W2b-r and W10; none changes behaviour. - role-custody.sh: two genuinely unused 'type' read targets become '_' (one was masked until the other went); AB_RC_REVERTED is a documented output, so it keeps a disable with the reason. - role-custody-migrate.test.sh, role-isolation-rehearsal.test.sh: the variables are seams read by the sourced library or by the eval'd entrypoint block, which shellcheck cannot see across files; disables carry that reason. Suites: migrate 31, rehearsal 30, delivery 30, boot 22, supervisor 23. Co-Authored-By: jjohare <github@thedreamlab.uk>
Co-Authored-By: jjohare <github@thedreamlab.uk>
… runner uid 1001, a VM) The new custody CI job (W10) ran two suites for the first time outside the agentbox container, and both had assumed it: - role-custody-migrate "revert exactness": the fixture meant a devuser-owned flag-off volume but only seeded identities/ (0:0) in the chown ledger, so every other path fell through to its real owner — 1000 in the container, 1001 on the runner. The ledger now records 1000:1000 for the whole fixture before identities/ is set to root, so the pre-flag snapshot is devuser's on any host. ab_custody_revert is unchanged. - rehearsal host half "refuses to run inside the container": the test dropped its seam and relied on real container detection, which a GitHub runner (a VM) does not trip, so the root check fired instead. New seam HR_ASSUME_CONTAINER=1 forces the in-container refusal and cannot suppress it; the test uses it everywhere and, when it really is in a container, also asserts the genuine detection path without the seam (31 cases here, 30 on a runner). Co-Authored-By: jjohare <github@thedreamlab.uk>
…87fe) Co-Authored-By: jjohare <github@thedreamlab.uk>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Integrates custody X-1 step 1. Everything ships with
[security].role_isolation = false, and with the flag off the boot is today's. This PR is the owner's acceptance checklist for the rebuild. Do not turn the flag on in the same rebuild.Merged (no-ff, authorship kept), in this order:
custody/queen-adr2103custody/w0-bypassescustody/w1-role-accountscustody/w3-identity-port(carries W6a's rehearsal)custody/w7a-egress-registercustody/w8-key-splitcustody/w5-producer-bake(landed after W3; the merge was clean, so the tree is the same as with the intended order)custody/w2-env-scrubcustody/w9-sidecar-podkeyIntegration resolutions (each is its own commit with the reason; see ADR-2122, "Integration resolutions" and "Correction to the integration disposition"):
/etc/supervisord.roles.confeverywhere./etc/agentbox/role-accounts.jsonplusrole-secrets.tsvplus W2's ROLE class. There is no derived numbering, and uid 965 stays reserved.[program:serve-identity]becomesuser=ab-identityunder the flag. The socket is/run/secrets/ab-identity-port/identity.sock, in groupab-identity-port(969: devuser, ab-identity, ab-gateway), with the directoryab-identity:969 0750made by W1's delivery. It is not under/run/agentbox, because/runis a devuser-owned tmpfs. Key files keep W1's names.ROLE-ISOLATION-LEAKand ignored.3b5412963; every dated note is kept.Acceptance for the owner's rebuild
1. Rebuild with the flag still off (host shell, tmux tab 6)
./agentbox.sh rebuild, and the container comes up healthy./readyis 200, which meansbootstrap-sealwrote/run/agentbox/bootstrap.done.grep -n role_isolation /etc/agentbox.tomlshowsrole_isolation = false, or the line is absent.supervisorctl statusshows the same set as before the rebuild, plus:serve-identityEXITED (expected; its log saysrole_isolation is off);docker-read-proxyEXITED (expected).getent passwd ab-identitygives uid 960.getent group ab-identity-portgives969:devuser,ab-identity,ab-gateway.id devusershows no group 0 and no 965.findmnt /run/secretsis a tmpfs,uid=0,mode=711on the mount. With the flag off the entrypoint hands it to devuser 0700, andnostr.keyis there as before./etc/supervisord.roles.conf;/etc/agentbox/role-secrets.tsv, which includessockdir ab-identity-port 960 969and adir /var/lib/agentbox/events/signrow;/etc/agentbox/role-accounts.json./opt/agentbox/sidestr/upstreamexists when[sidechain].enabled, and the producer still makes blocks (W5)../agentbox.sh browsercontainer podkey statusanswers, and the Chrome profile is on thebrowsercontainer-profilevolume (W9).2. The rehearsal, flag off: both halves must say STAGED (exit 2)
bash scripts/activation/role-isolation-rehearsal.sh. Expect exit 2,STAGED. Every row isnot_applicable: flag offand records whether it would pass. The receipt goes todocs/estate-closeout/x1-rehearsal-<UTC>.json.scripts/activation/role-isolation-rehearsal.host.sh --container agentbox. Expect exit 2.h-uid-collisionmust PASS (no host account in 960–979).h-socketrecords the host docker socket's mode and group. The receipt isx1-rehearsal-host-<UTC>.json.3. Only after 1 and 2 are clean: the flag-on rehearsal (a separate, owner-scheduled reboot)
Set
[security].role_isolation = true, restart, then run both halves again. Both must exit 0, PASS. Any FAIL means turning the flag off again; flag off is the rollback. What to look for:/run/secrets/<role>/is 0500 and owned by the role, and its files are 0400. devuser getsEACCES./run/secrets/ab-identity-portis960:969 0750. devuser can neither rename it nor create beside it (row… (rename/replace)).nostr-relayandserve-identityas 960,nostr-gateway961,nip98-proxy962,sidestr-producer964,sidestr-faucet966. Each one's/proc/<pid>/environis unreadable to devuser.agentbox-core's x-only key.forum_eventdry-run verifies under JunkieJarvis's key.nip42_authis accepted by127.0.0.1:7777.dm_unwrap, a foreign URL and an unknown op are refused./var/lib/agentbox/events/sign/.ab-sidestr-dreamlab(964).identity.env, the supervisor configs or/var/log. AnyROLE-ISOLATION-LEAKline in the logs is a finding.degraded:docker-socketanddegraded:secrets-*are failures, not warnings.The flag stays off until the owner has the PASS receipts for both halves. ADR-2122 stays
proposed/inactiveuntil then.Gates at the merged head (
d03defbea)Nix cannot be evaluated in the container, so neither the image nor a boot has been exercised. Everything else passed:
agentbox-manifest role-accounts checkagentbox-config-validate.js, both manifestscheck-manifest-catalogue.jsadr-index-gen --check/--check-indexadr-ratchet.sh docs/adr origin/main HEADand its self-testrender-egress-register.js --checkcheck-secret-not-in-env.shenv-secret-inventory.js --checkcargo testagentbox-manifestcargo testnostr-pod-bridgenpm run test:node-S warningon every changed scriptFlake diff for review (nothing evaluated):
lib/sidestr-upstream.nix,/opt/agentbox/sidestr/upstream);--authkey=file:branch under the flag;[program:docker-read-proxy]and[program:serve-identity], both of which exit 0 with the flag off;/run/secretstmpfs in the baseline mounts;role-accounts isolate, which renderssupervisord.roles.confandrole-secrets.tsv, and shipsrole-accounts.json;root:x:0:;ab-identity-portgroup lines.The compose changes are the
/run/secretstmpfs (docker-compose.yml:110) and the docker-socket group handling in the override.lib/agentbox-manifest.nixhas one change, 3 lines.🤖 Generated by Claude Code