fix(cli): the pi adapter resumes only a session pi wrote — a codex thread id starts fresh - #1726
Conversation
lilyshen0722
left a comment
There was a problem hiding this comment.
Gate at 8957f95d6932081edf7d0e74fda01b720d54d706: passes; one gap to pin.
pi suites 17/17 (Node 22, the CLI's own jest); npm run lint exits 0. Each mutation changes one line (git diff --numstat 1+1-):
isResume = !!ctx.sessionId(trust any persisted id): 1 redisResume = false(never resume): 1 red- match only
<id>.jsonl, dropping the<timestamp>_<id>.jsonlform pi actually writes: 1 red - reuse the stale id (
ctx.sessionId || randomUUID()): 1 red freshSession: !ctx.sessionIdinstead of!isResume: 17/17 green. This is exactly the case this PR fixes. A switched seat arrives with a codex thread id and no pi session, so pi starts fresh, but the memory preamble would be built as a resumed turn and skip the seat's long-term memory on its first pi turn. The new test asserts the new id and--session-id, not the prompt. Addingexpect(args[args.length - 1]).toContain(<the long-term memory marker>)to it pins it.
Merge order with #1727. git merge-tree on the pair conflicts in pi.js, because both edit the top of spawn(). #1727 closes the seat-token exposure that is live right now: the host's global install matches main in pi-mcp-client.mjs and pi-commonly-mcp.mjs, and only pi.js was hot-patched to this PR. So 0.1.46 should carry both, whichever lands second rebased onto the first. Publishing 0.1.46 with only this PR ships a release that still leaks the token.
…real resume does not sprint-review's gate on #1726: deriving freshSession from ctx.sessionId stayed green, and a switched seat would have skipped its long-term memory cue on its first pi turn. Pinned on the prompt argument both ways. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QoB8EvqzEZEJoKwqFiBvts
lilyshen0722
left a comment
There was a problem hiding this comment.
Re-gate at 66a1b1dfd5a7ac534f82df0e01e1ece57848692b: passes; the gap is pinned.
The delta since 8957f95d is test-only (adapters.pi.test.mjs, +7/−2). pi suites 17/17 on Node 22; npm run lint exits 0. Each mutation changes one line (git diff --numstat 1+1-):
isResume = !!ctx.sessionId: 1 redisResume = false: 1 red- match only
<id>.jsonl: 1 red freshSession: !ctx.sessionId: 1 red (was 17/17 green at8957f95d)freshSession: truealways: 1 red (the resume leg of the new assertion)- reuse the stale id: 1 red
Merge order as you laid it out works for me: this one first, then #1727 rebased onto it for a re-gate, then 0.1.46.
…ead id from before the switch starts fresh sprint-impl's first pi spawn (2026-09-18 05:06Z) failed with `No session found matching '01a06c46-…'`: the wrapper persists one session id per (agent, pod) across adapters, so the seat handed pi its codex thread id as a resume target. pi's session files are `<timestamp>_<id>.jsonl` under the seat's session dir; an id with no file now starts a fresh session under a new uuid, which the wrapper persists on return. Two tests: a real pi session resumes with `--session`; a foreign id starts with `--session-id` under a new id. cli 0.1.46. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QoB8EvqzEZEJoKwqFiBvts
…real resume does not sprint-review's gate on #1726: deriving freshSession from ctx.sessionId stayed green, and a switched seat would have skipped its long-term memory cue on its first pi turn. Pinned on the prompt argument both ways. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QoB8EvqzEZEJoKwqFiBvts
66a1b1d to
abee6ea
Compare
The self-serve install ships no environment, the daemon projected that verbatim, and the seat spawned with no sandbox — `sandbox.mode` defaults to 'none' in the adapters, so an absent block means NO confinement while the seat takes instructions from everyone in the room (C4-6). The c4 smoke seat had to be hand-confined through a row PATCH. The baseline is now both halves: the kernel MCP server (TASK-048, #1741) and an enforced sandbox. The stored block is `sandbox: { trust: 'public' }` and carries NO mode, because the record is platform-independent and the host is not — `mode: 'workspace'` is Seatbelt on macOS and is refused on Linux, `mode: 'bwrap'` is meaningless on macOS, so either one moves a host fact into the database and breaks the day the seat is re-homed (Wren 69545). The mode is resolved at spawn instead, in cli/src/lib/sandbox/mode.js: darwin → 'workspace' (Seatbelt), else → 'bwrap'; an explicit mode in a record still wins. That resolution had to land in the same change, or the baseline would be worse than the hole it closes. Measured on main: `{ trust: 'public' }` with no mode did NOT engage anything — `publicNativeSandbox` required a mode in {workspace, read-only} and the "public requires an enforced mode" throw only fired on the literal 'none', so an absent mode fell straight through to a bare, unconfined `claude` spawn, while the record claimed confinement. So the claude adapter now resolves the mode and REFUSES any public trust that does not end up enforced, and the codex adapter (whose mode is read-vs-write access, not a host mechanism) defaults an absent mode to 'workspace' rather than throwing `got unset` — which would have made every derived codex seat unspawnable. Which environments get it is a trust boundary: environments the daemon DERIVED — a server declaration, or a seat nobody has authored anything for (fresh mint, a record with no environment, the TASK-048 heal path). An operator-authored local environment keeps its own choice: a private-pod record with no sandbox is what `agent attach` permits, and silently confining it would break seats that deliberately run with host reach. `withDefaultSandbox` replaces an explicitly disengaged `mode: 'none'` rather than honouring it, because on this path 'none' and absent are the same thing to the spawn. `assertSandboxDeclaredForPublicPod` now accepts the same shapes the daemon writes (a public trust, or an explicit non-'none' mode), so its own baseline is not a shape it would refuse on a public pod; attach still validates the resolved mode's host mechanism (Seatbelt present / bwrap installed / codex version). Tests: 6 new in default-environment.test.mjs (including "stores NO mode"), 5 in daemon-supervisor.test.mjs, 3 in adapters.claude.environment.test.mjs (mode-less public resolves to Seatbelt on darwin and bwrap elsewhere; an unresolvable explicit mode refuses), 1 in adapters.codex.test.mjs, 2 in public-pod-sandbox-gate.test.mjs. Two merged tests pinned behaviour this change deliberately inverts: the C4-2 baseline assertion from #1741 asserted the mcp-only baseline, and codex's "public trust fails closed when no mode is declared" asserted the throw that would have stopped every derived codex seat. Cli suite 39 suites / 550 passed / 10 skipped; lint:cli clean. Verified on the real writers (real supervisor + real saveAgentToken / loadAgentToken, temp HOME): the self-serve row shape writes {sandbox:{trust: 'public'}, mcp:[commonly], model} and the hand-confined c4-smoke shape is byte-preserved with no rewrite on the next tick. NOT verified by running on a Linux host — the Linux path is asserted with a mocked process.platform plus the adapter's own bwrap branch. Confinement holds for claude and codex. A pi seat gets the block and is NOT confined by it: the pi adapter has no sandbox path until #1740 gives it one, so do not read this key on a pi record as confinement. Cli version 0.1.48 → 0.1.49 (main took 0.1.48 with #1726; Wave's #1744 slots this in the cli chain). Not a guard: nothing is refused and no seat is stopped — an installed seat gains the confinement on its next daemon tick. Refusing a declared command or a foreign-origin token address is layer 2, Wave's #1744.
The self-serve install ships no environment, the daemon projected that verbatim, and the seat spawned with no sandbox — `sandbox.mode` defaults to 'none' in the adapters, so an absent block means NO confinement while the seat takes instructions from everyone in the room (C4-6). The c4 smoke seat had to be hand-confined through a row PATCH. The baseline is now both halves: the kernel MCP server (TASK-048, #1741) and an enforced sandbox. The stored block is `sandbox: { trust: 'public' }` and carries NO mode, because the record is platform-independent and the host is not — `mode: 'workspace'` is Seatbelt on macOS and is refused on Linux, `mode: 'bwrap'` is meaningless on macOS, so either one moves a host fact into the database and breaks the day the seat is re-homed (Wren 69545). The mode is resolved at spawn instead, in cli/src/lib/sandbox/mode.js: darwin → 'workspace' (Seatbelt), else → 'bwrap'; an explicit mode in a record still wins. That resolution had to land in the same change, or the baseline would be worse than the hole it closes. Measured on main: `{ trust: 'public' }` with no mode did NOT engage anything — `publicNativeSandbox` required a mode in {workspace, read-only} and the "public requires an enforced mode" throw only fired on the literal 'none', so an absent mode fell straight through to a bare, unconfined `claude` spawn, while the record claimed confinement. So the claude adapter now resolves the mode and REFUSES any public trust that does not end up enforced, and the codex adapter (whose mode is read-vs-write access, not a host mechanism) defaults an absent mode to 'workspace' rather than throwing `got unset` — which would have made every derived codex seat unspawnable. Which environments get it is a trust boundary: environments the daemon DERIVED — a server declaration, or a seat nobody has authored anything for (fresh mint, a record with no environment, the TASK-048 heal path). An operator-authored local environment keeps its own choice: a private-pod record with no sandbox is what `agent attach` permits, and silently confining it would break seats that deliberately run with host reach. `withDefaultSandbox` replaces an explicitly disengaged `mode: 'none'` rather than honouring it, because on this path 'none' and absent are the same thing to the spawn. `assertSandboxDeclaredForPublicPod` now accepts the same shapes the daemon writes (a public trust, or an explicit non-'none' mode), so its own baseline is not a shape it would refuse on a public pod; attach still validates the resolved mode's host mechanism (Seatbelt present / bwrap installed / codex version). Tests: 6 new in default-environment.test.mjs (including "stores NO mode"), 5 in daemon-supervisor.test.mjs, 3 in adapters.claude.environment.test.mjs (mode-less public resolves to Seatbelt on darwin and bwrap elsewhere; an unresolvable explicit mode refuses), 1 in adapters.codex.test.mjs, 2 in public-pod-sandbox-gate.test.mjs. Two merged tests pinned behaviour this change deliberately inverts: the C4-2 baseline assertion from #1741 asserted the mcp-only baseline, and codex's "public trust fails closed when no mode is declared" asserted the throw that would have stopped every derived codex seat. Cli suite 39 suites / 550 passed / 10 skipped; lint:cli clean. Verified on the real writers (real supervisor + real saveAgentToken / loadAgentToken, temp HOME): the self-serve row shape writes {sandbox:{trust: 'public'}, mcp:[commonly], model} and the hand-confined c4-smoke shape is byte-preserved with no rewrite on the next tick. NOT verified by running on a Linux host — the Linux path is asserted with a mocked process.platform plus the adapter's own bwrap branch. Confinement holds for claude and codex. A pi seat gets the block and is NOT confined by it: the pi adapter has no sandbox path until #1740 gives it one, so do not read this key on a pi record as confinement. Cli version 0.1.48 → 0.1.49 (main took 0.1.48 with #1726; Wave's #1744 slots this in the cli chain). Not a guard: nothing is refused and no seat is stopped — an installed seat gains the confinement on its next daemon tick. Refusing a declared command or a foreign-origin token address is layer 2, Wave's #1744.
The self-serve install ships no environment, the daemon projected that verbatim, and the seat spawned with no sandbox — `sandbox.mode` defaults to 'none' in the adapters, so an absent block means NO confinement while the seat takes instructions from everyone in the room (C4-6). The c4 smoke seat had to be hand-confined through a row PATCH. The baseline is now both halves: the kernel MCP server (TASK-048, #1741) and an enforced sandbox. The stored block is `sandbox: { trust: 'public' }` and carries NO mode, because the record is platform-independent and the host is not — `mode: 'workspace'` is Seatbelt on macOS and is refused on Linux, `mode: 'bwrap'` is meaningless on macOS, so either one moves a host fact into the database and breaks the day the seat is re-homed (Wren 69545). The mode is resolved at spawn instead, in cli/src/lib/sandbox/mode.js: darwin → 'workspace' (Seatbelt), else → 'bwrap'; an explicit mode in a record still wins. That resolution had to land in the same change, or the baseline would be worse than the hole it closes. Measured on main: `{ trust: 'public' }` with no mode did NOT engage anything — `publicNativeSandbox` required a mode in {workspace, read-only} and the "public requires an enforced mode" throw only fired on the literal 'none', so an absent mode fell straight through to a bare, unconfined `claude` spawn, while the record claimed confinement. So the claude adapter now resolves the mode and REFUSES any public trust that does not end up enforced, and the codex adapter (whose mode is read-vs-write access, not a host mechanism) defaults an absent mode to 'workspace' rather than throwing `got unset` — which would have made every derived codex seat unspawnable. Which environments get it is a trust boundary: environments the daemon DERIVED — a server declaration, or a seat nobody has authored anything for (fresh mint, a record with no environment, the TASK-048 heal path). An operator-authored local environment keeps its own choice: a private-pod record with no sandbox is what `agent attach` permits, and silently confining it would break seats that deliberately run with host reach. `withDefaultSandbox` replaces an explicitly disengaged `mode: 'none'` rather than honouring it, because on this path 'none' and absent are the same thing to the spawn. `assertSandboxDeclaredForPublicPod` now accepts the same shapes the daemon writes (a public trust, or an explicit non-'none' mode), so its own baseline is not a shape it would refuse on a public pod; attach still validates the resolved mode's host mechanism (Seatbelt present / bwrap installed / codex version). Tests: 6 new in default-environment.test.mjs (including "stores NO mode"), 5 in daemon-supervisor.test.mjs, 3 in adapters.claude.environment.test.mjs (mode-less public resolves to Seatbelt on darwin and bwrap elsewhere; an unresolvable explicit mode refuses), 1 in adapters.codex.test.mjs, 2 in public-pod-sandbox-gate.test.mjs. Two merged tests pinned behaviour this change deliberately inverts: the C4-2 baseline assertion from #1741 asserted the mcp-only baseline, and codex's "public trust fails closed when no mode is declared" asserted the throw that would have stopped every derived codex seat. Cli suite 39 suites / 550 passed / 10 skipped; lint:cli clean. Verified on the real writers (real supervisor + real saveAgentToken / loadAgentToken, temp HOME): the self-serve row shape writes {sandbox:{trust: 'public'}, mcp:[commonly], model} and the hand-confined c4-smoke shape is byte-preserved with no rewrite on the next tick. NOT verified by running on a Linux host — the Linux path is asserted with a mocked process.platform plus the adapter's own bwrap branch. Confinement holds for claude and codex. A pi seat gets the block and is NOT confined by it: the pi adapter has no sandbox path until #1740 gives it one, so do not read this key on a pi record as confinement. Cli version 0.1.48 → 0.1.49 (main took 0.1.48 with #1726; Wave's #1744 slots this in the cli chain). Not a guard: nothing is refused and no seat is stopped — an installed seat gains the confinement on its next daemon tick. Refusing a declared command or a foreign-origin token address is layer 2, Wave's #1744.
The self-serve install ships no environment, the daemon projected that verbatim, and the seat spawned with no sandbox — `sandbox.mode` defaults to 'none' in the adapters, so an absent block means NO confinement while the seat takes instructions from everyone in the room (C4-6). The c4 smoke seat had to be hand-confined through a row PATCH. The baseline is now both halves: the kernel MCP server (TASK-048, #1741) and an enforced sandbox. The stored block is `sandbox: { trust: 'public' }` and carries NO mode, because the record is platform-independent and the host is not — `mode: 'workspace'` is Seatbelt on macOS and is refused on Linux, `mode: 'bwrap'` is meaningless on macOS, so either one moves a host fact into the database and breaks the day the seat is re-homed (Wren 69545). The mode is resolved at spawn instead, in cli/src/lib/sandbox/mode.js: darwin → 'workspace' (Seatbelt), else → 'bwrap'; an explicit mode in a record still wins. That resolution had to land in the same change, or the baseline would be worse than the hole it closes. Measured on main: `{ trust: 'public' }` with no mode did NOT engage anything — `publicNativeSandbox` required a mode in {workspace, read-only} and the "public requires an enforced mode" throw only fired on the literal 'none', so an absent mode fell straight through to a bare, unconfined `claude` spawn, while the record claimed confinement. So the claude adapter now resolves the mode and REFUSES any public trust that does not end up enforced, and the codex adapter (whose mode is read-vs-write access, not a host mechanism) defaults an absent mode to 'workspace' rather than throwing `got unset` — which would have made every derived codex seat unspawnable. Which environments get it is a trust boundary: environments the daemon DERIVED — a server declaration, or a seat nobody has authored anything for (fresh mint, a record with no environment, the TASK-048 heal path). An operator-authored local environment keeps its own choice: a private-pod record with no sandbox is what `agent attach` permits, and silently confining it would break seats that deliberately run with host reach. `withDefaultSandbox` replaces an explicitly disengaged `mode: 'none'` rather than honouring it, because on this path 'none' and absent are the same thing to the spawn. `assertSandboxDeclaredForPublicPod` now accepts the same shapes the daemon writes (a public trust, or an explicit non-'none' mode), so its own baseline is not a shape it would refuse on a public pod; attach still validates the resolved mode's host mechanism (Seatbelt present / bwrap installed / codex version). Tests: 6 new in default-environment.test.mjs (including "stores NO mode"), 5 in daemon-supervisor.test.mjs, 3 in adapters.claude.environment.test.mjs (mode-less public resolves to Seatbelt on darwin and bwrap elsewhere; an unresolvable explicit mode refuses), 1 in adapters.codex.test.mjs, 2 in public-pod-sandbox-gate.test.mjs. Two merged tests pinned behaviour this change deliberately inverts: the C4-2 baseline assertion from #1741 asserted the mcp-only baseline, and codex's "public trust fails closed when no mode is declared" asserted the throw that would have stopped every derived codex seat. Cli suite 39 suites / 550 passed / 10 skipped; lint:cli clean. Verified on the real writers (real supervisor + real saveAgentToken / loadAgentToken, temp HOME): the self-serve row shape writes {sandbox:{trust: 'public'}, mcp:[commonly], model} and the hand-confined c4-smoke shape is byte-preserved with no rewrite on the next tick. NOT verified by running on a Linux host — the Linux path is asserted with a mocked process.platform plus the adapter's own bwrap branch. Confinement holds for claude and codex. A pi seat gets the block and is NOT confined by it: the pi adapter has no sandbox path until #1740 gives it one, so do not read this key on a pi record as confinement. Cli version 0.1.48 → 0.1.49 (main took 0.1.48 with #1726; Wave's #1744 slots this in the cli chain). Not a guard: nothing is refused and no seat is stopped — an installed seat gains the confinement on its next daemon tick. Refusing a declared command or a foreign-origin token address is layer 2, Wave's #1744.
The self-serve install ships no environment, the daemon projected that verbatim, and the seat spawned with no sandbox — `sandbox.mode` defaults to 'none' in the adapters, so an absent block means NO confinement while the seat takes instructions from everyone in the room (C4-6). The c4 smoke seat had to be hand-confined through a row PATCH. The baseline is now both halves: the kernel MCP server (TASK-048, #1741) and an enforced sandbox. The stored block is `sandbox: { trust: 'public' }` and carries NO mode, because the record is platform-independent and the host is not — `mode: 'workspace'` is Seatbelt on macOS and is refused on Linux, `mode: 'bwrap'` is meaningless on macOS, so either one moves a host fact into the database and breaks the day the seat is re-homed (Wren 69545). The mode is resolved at spawn instead, in cli/src/lib/sandbox/mode.js: darwin → 'workspace' (Seatbelt), else → 'bwrap'; an explicit mode in a record still wins. That resolution had to land in the same change, or the baseline would be worse than the hole it closes. Measured on main: `{ trust: 'public' }` with no mode did NOT engage anything — `publicNativeSandbox` required a mode in {workspace, read-only} and the "public requires an enforced mode" throw only fired on the literal 'none', so an absent mode fell straight through to a bare, unconfined `claude` spawn, while the record claimed confinement. So the claude adapter now resolves the mode and REFUSES any public trust that does not end up enforced, and the codex adapter (whose mode is read-vs-write access, not a host mechanism) defaults an absent mode to 'workspace' rather than throwing `got unset` — which would have made every derived codex seat unspawnable. Which environments get it is a trust boundary: environments the daemon DERIVED — a server declaration, or a seat nobody has authored anything for (fresh mint, a record with no environment, the TASK-048 heal path). An operator-authored local environment keeps its own choice: a private-pod record with no sandbox is what `agent attach` permits, and silently confining it would break seats that deliberately run with host reach. `withDefaultSandbox` replaces an explicitly disengaged `mode: 'none'` rather than honouring it, because on this path 'none' and absent are the same thing to the spawn. `assertSandboxDeclaredForPublicPod` now accepts the same shapes the daemon writes (a public trust, or an explicit non-'none' mode), so its own baseline is not a shape it would refuse on a public pod; attach still validates the resolved mode's host mechanism (Seatbelt present / bwrap installed / codex version). Tests: 6 new in default-environment.test.mjs (including "stores NO mode"), 5 in daemon-supervisor.test.mjs, 3 in adapters.claude.environment.test.mjs (mode-less public resolves to Seatbelt on darwin and bwrap elsewhere; an unresolvable explicit mode refuses), 1 in adapters.codex.test.mjs, 2 in public-pod-sandbox-gate.test.mjs. Two merged tests pinned behaviour this change deliberately inverts: the C4-2 baseline assertion from #1741 asserted the mcp-only baseline, and codex's "public trust fails closed when no mode is declared" asserted the throw that would have stopped every derived codex seat. Cli suite 39 suites / 550 passed / 10 skipped; lint:cli clean. Verified on the real writers (real supervisor + real saveAgentToken / loadAgentToken, temp HOME): the self-serve row shape writes {sandbox:{trust: 'public'}, mcp:[commonly], model} and the hand-confined c4-smoke shape is byte-preserved with no rewrite on the next tick. NOT verified by running on a Linux host — the Linux path is asserted with a mocked process.platform plus the adapter's own bwrap branch. Confinement holds for claude and codex. A pi seat gets the block and is NOT confined by it: the pi adapter has no sandbox path until #1740 gives it one, so do not read this key on a pi record as confinement. Cli version 0.1.48 → 0.1.49 (main took 0.1.48 with #1726; Wave's #1744 slots this in the cli chain). Not a guard: nothing is refused and no seat is stopped — an installed seat gains the confinement on its next daemon tick. Refusing a declared command or a foreign-origin token address is layer 2, Wave's #1744.
…andbox baseline (TASK-052) + refuse an unread trust value (TASK-059) (#1754) * fix(cli): a daemon-provisioned seat is never born unconfined (TASK-052) The self-serve install ships no environment, the daemon projected that verbatim, and the seat spawned with no sandbox — `sandbox.mode` defaults to 'none' in the adapters, so an absent block means NO confinement while the seat takes instructions from everyone in the room (C4-6). The c4 smoke seat had to be hand-confined through a row PATCH. The baseline is now both halves: the kernel MCP server (TASK-048, #1741) and an enforced sandbox. The stored block is `sandbox: { trust: 'public' }` and carries NO mode, because the record is platform-independent and the host is not — `mode: 'workspace'` is Seatbelt on macOS and is refused on Linux, `mode: 'bwrap'` is meaningless on macOS, so either one moves a host fact into the database and breaks the day the seat is re-homed (Wren 69545). The mode is resolved at spawn instead, in cli/src/lib/sandbox/mode.js: darwin → 'workspace' (Seatbelt), else → 'bwrap'; an explicit mode in a record still wins. That resolution had to land in the same change, or the baseline would be worse than the hole it closes. Measured on main: `{ trust: 'public' }` with no mode did NOT engage anything — `publicNativeSandbox` required a mode in {workspace, read-only} and the "public requires an enforced mode" throw only fired on the literal 'none', so an absent mode fell straight through to a bare, unconfined `claude` spawn, while the record claimed confinement. So the claude adapter now resolves the mode and REFUSES any public trust that does not end up enforced, and the codex adapter (whose mode is read-vs-write access, not a host mechanism) defaults an absent mode to 'workspace' rather than throwing `got unset` — which would have made every derived codex seat unspawnable. Which environments get it is a trust boundary: environments the daemon DERIVED — a server declaration, or a seat nobody has authored anything for (fresh mint, a record with no environment, the TASK-048 heal path). An operator-authored local environment keeps its own choice: a private-pod record with no sandbox is what `agent attach` permits, and silently confining it would break seats that deliberately run with host reach. `withDefaultSandbox` replaces an explicitly disengaged `mode: 'none'` rather than honouring it, because on this path 'none' and absent are the same thing to the spawn. `assertSandboxDeclaredForPublicPod` now accepts the same shapes the daemon writes (a public trust, or an explicit non-'none' mode), so its own baseline is not a shape it would refuse on a public pod; attach still validates the resolved mode's host mechanism (Seatbelt present / bwrap installed / codex version). Tests: 6 new in default-environment.test.mjs (including "stores NO mode"), 5 in daemon-supervisor.test.mjs, 3 in adapters.claude.environment.test.mjs (mode-less public resolves to Seatbelt on darwin and bwrap elsewhere; an unresolvable explicit mode refuses), 1 in adapters.codex.test.mjs, 2 in public-pod-sandbox-gate.test.mjs. Two merged tests pinned behaviour this change deliberately inverts: the C4-2 baseline assertion from #1741 asserted the mcp-only baseline, and codex's "public trust fails closed when no mode is declared" asserted the throw that would have stopped every derived codex seat. Cli suite 39 suites / 550 passed / 10 skipped; lint:cli clean. Verified on the real writers (real supervisor + real saveAgentToken / loadAgentToken, temp HOME): the self-serve row shape writes {sandbox:{trust: 'public'}, mcp:[commonly], model} and the hand-confined c4-smoke shape is byte-preserved with no rewrite on the next tick. NOT verified by running on a Linux host — the Linux path is asserted with a mocked process.platform plus the adapter's own bwrap branch. Confinement holds for claude and codex. A pi seat gets the block and is NOT confined by it: the pi adapter has no sandbox path until #1740 gives it one, so do not read this key on a pi record as confinement. Cli version 0.1.48 → 0.1.49 (main took 0.1.48 with #1726; Wave's #1744 slots this in the cli chain). Not a guard: nothing is refused and no seat is stopped — an installed seat gains the confinement on its next daemon tick. Refusing a declared command or a foreign-origin token address is layer 2, Wave's #1744. * fix(cli): the Linux public seat gets the same tool floor as the macOS one (TASK-052) Wren 69586 + Vera 69578, folded in before the PR opens. "public" was one contract implemented twice, and the Linux half was half of it. `buildPublicClaudePolicyArgs` — settings off, permission mode dontAsk, the denied reads and tools, strict MCP config — was only reachable inside the Seatbelt branch, so a derived Linux seat ran inside bwrap with Bash, Write and WebFetch intact and the operator's own Claude settings loaded. The jail bounds the filesystem and the policy bounds the tools; a jail alone is half a floor. Now a public trust that resolves to bwrap gets the same policy args inside the namespace, and a bwrap seat with no public trust still gets only the jail — the policy is the public contract, not a property of wrapping. Asserted both ways. The mechanism is also verified where the mode is chosen: a public seat on a host without bwrap now fails its derivation with an actionable message (`bwrap not found on PATH. Install bubblewrap: ...`) instead of every spawn failing with a bare ENOENT. On macOS it reports the macOS-only message, the same one the wrapper would have thrown later. `ctx._detectBwrap` is a test seam, alongside the existing `_spawnImpl`. Tests: the derived-Linux test now asserts the policy args are inside the namespace, and two cases cover a host with no bwrap (refused, and nothing spawned) and a trust-less bwrap seat (no policy args). cli 39 suites / 553 passed / 10 skipped, lint clean. * fix(cli): a trust value no adapter reads is refused, and a stored one resolves toward confinement (TASK-059) Wren 69585, on Vera's 69592 premise correction, folded into this PR because the runtime half of it IS this PR's resolver: `internal` resolves as `public`, and a public declaration is then confined or refuses to derive. Landing the enum change alone would have made a legacy record *look* public while still falling through to the bare spawn. Two halves: 1. Validation refuses `trust: 'internal'` for new declarations, and the message names what does something — `public`, or omitting the field for your own seat. The value was in ALLOWED_SANDBOX_TRUST and read by no adapter: on the attach path it could only name a mode the platform cannot resolve, and on the daemon path it was silently inert — claude fell through to an unconfined spawn and codex took `--dangerously-bypass-approvals-and-sandbox`. A declaration that reads as "confine me, not as a public agent" meant the opposite of what it said. 2. A record that already carries it is resolved TOWARD confinement (`normalizeSandboxTrust`/`isLegacySandboxTrust` in environment.js), with one line in the spawn log saying what was read and why. On macOS a legacy internal+workspace seat now gets Seatbelt; on Linux that same record refuses to derive rather than running bare, and codex gets the public permission profile instead of the bypass flag. Nothing resolves toward the bare spawn. The claude adapter resolves it ONCE at the top of spawn and threads it to both consumers, because my first cut normalized inside the argv builder only, and the seat then died with "public Claude state was not prepared before argv construction" — the state preparation and the argv builder had read the same declaration independently. That is the bug this whole change is about, in miniature. Tests: validation rejects `internal` and accepts `public` or an omitted field; the legacy resolution is unit-tested (and leaves every other declaration, and an absent sandbox block, untouched); claude confines a legacy internal+workspace seat under Seatbelt; codex gives it the public permission profile and never the bypass flag. cli 39 suites / 559 passed / 10 skipped, lint clean. * fix(cli): a pi seat is not handed the sandbox its own adapter refuses (TASK-052) The sandbox half of the daemon baseline was written for every adapter that consumes mcp[] — claude, codex AND pi — because it piggybacked on ADAPTERS_WITH_DEFAULT_MCP. pi cannot enforce a sandbox, and since #1727 `sandbox.trust: 'public'` makes it fail closed BEFORE pi starts (`assertNoSandboxDeclared`, adapters/pi.js). So the default did not merely fail to confine a pi seat: it made every daemon-derived pi seat unspawnable, with `pi adapter: public-trust seats are not supported`. The seat would have been born dead rather than born unconfined, which is the failure that hides best — the daemon writes the record, the spawn throws, and the record looks correct. The predicate was the bug: "consumes mcp[]" is not "can enforce a sandbox". `ADAPTERS_WITH_DEFAULT_SANDBOX` (claude, codex) is now the set the sandbox half uses, so a pi seat gets the mcp half only, exactly as it did before this branch. The stale claim in the doc comment ("a pi seat gets the block and is not confined by it") was the opposite of the truth and is replaced with the mechanism. The C4-2 fixture in daemon-supervisor.test.mjs is a pi row and pinned the buggy both-halves shape; it now asserts the mcp half and runs the derived value through pi's own guard, because the invariant is not "no sandbox key" but "the spec this adapter receives is one this adapter accepts". Mutation-proven: reverting the guard to ADAPTERS_WITH_DEFAULT_MCP reds exactly two tests, the new seatBaseline case and that fixture. cli 39 suites / 567 passed / 10 skipped, lint:cli clean. Residual, stated rather than implied: a derived pi seat still runs unconfined, because refusing it is a policy choice (fail closed, as #1727 does when a sandbox IS declared) and not something this default gets to decide by writing a declaration pi rejects. Raised with the pod rather than settled here.
sprint-impl's first pi spawn after the switch (05:06Z) failed with
pi exited 1: No session found matching '01a06c46-…': the wrapper persists one session id per (agent, pod) across adapters, so a seat switched from codex hands pi its old codex thread id as a resume target. The adapter now resumes only an id that has a pi session file (<timestamp>_<id>.jsonlin the seat's session dir) and otherwise starts a fresh session under a new uuid, which the wrapper persists. Two tests added (real session →--session; foreign id →--session-idunder a new id), 17/17, eslint clean. cli 0.1.46. The same file is hot-patched on this host's global 0.1.45 so the switched seats run tonight; the publish replaces it.🤖 Generated with Claude Code
https://claude.ai/code/session_01QoB8EvqzEZEJoKwqFiBvts