Skip to content

fix(cli): daemon-provisioned seats get the commonly MCP baseline (TASK-048) - #1741

Merged
lilyshen0722 merged 2 commits into
mainfrom
kai/task048-daemon-default-mcp
Sep 18, 2026
Merged

lilyshen0722 merged 2 commits into
mainfrom
kai/task048-daemon-default-mcp

Conversation

@lilyshen0722

@lilyshen0722 lilyshen0722 commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

TASK-048 (C4-2). A daemon-provisioned wrapper seat now gets the commonly MCP baseline whether or not anyone ever ran agent attach on that machine.

The defect

A seat installed server-side — the marketplace / agent-hub / webhook self-serve paths — declares runtime.adapter/model/effort and no ADR-008 environment at all. The daemon minted its runtime token from exactly that shape, so the token record had no mcp[], and the CLI it spawned had no commonly_* tools and could not post. In the C4 run the operator hand-added the @commonlyai/mcp@latest entry to c4-smoke's token file before the seat was usable (69444).

The default already existed — for agent attach and for the COMMONLY_AGENT_TOKEN bootstrap in agent run. Only the daemon path was missing it, and that is the path a self-serve install lands on.

The change

cli/src/lib/default-environment.js is new: one home for the server declaration and the set of adapters that consume mcp[]. buildDefaultEnvironment (attach + bootstrap) and the daemon now read the same module, so they cannot drift apart.

The daemon applies the baseline to every record it writes for an mcp-consuming adapter: fresh mint, the update path a UI model edit reaches, and the adapter-change path.

The predicate is "is there an mcp entry named commonly?", not "is mcp[] absent?" — the daemon also projects live room grants into this array, so a seat can arrive with mcp: [<grant broker>] and still hold no kernel server. A seat in that state has broker tools and no way to post; the append fixes it, and the default is idempotent across ticks.

cli/src/lib/default-environment.js (new, 70) · cli/src/lib/daemon-supervisor.js (+27/-4) · cli/src/commands/agent.js (+8/-22) · tests (+180) · cli/package.json 0.1.45 → 0.1.47

Verified, not asserted

Against the real c4-smoke row shape (wrapper + pi + deepseek-v4-flash/high, no environment) with the real saveToken/loadToken writers and a temp HOME so no seat on this machine was touched: the environment the daemon now writes is byte-identical to the entry the operator added by hand — both for a fresh mint and for an existing record that has no mcp[].

  • cli/__tests__/default-environment.test.mjs (new): the default per adapter, the append-after-grant-broker case, never duplicating or replacing a declared entry, the same-reference dirty-check contract, malformed specs left alone, idempotence.
  • cli/__tests__/daemon-supervisor.test.mjs: the C4-2 row shape mints the baseline; a declared environment with another server gets the baseline appended; a declared commonly entry is never duplicated or replaced; a stub seat still invents no environment.
  • cli/__tests__/daemon-supervisor.test.mjs: a pre-baseline record for a real adapter is healed and restarted when the row declares nothing; a healed record is not rewritten on the next tick (the dirty check — an unconditional write would restart the seat forever); a stub record on that path is still left alone.
  • cli/__tests__/attach.test.mjs: pi added to the default-environment cases (it was in the set but not in the test).

Three merged tests pinned the defect — inverted deliberately

  1. no declared model — no environment key invented asserted that a row with no declared environment produced a record with no environment key at all. That shape is the defect, so the test is now a seat with no declared environment still gets the commonly MCP baseline (TASK-048), with a sibling keeping the old intent for the adapter that genuinely has no consumption path (stub).
  2. a declared adapter change updates the token record and restarts the seat asserted no write on its first tick for a record with no mcp[] — the same defect as (1), on the path this PR heals. It now asserts the heal, clears the spy, and keeps its original subject (the adapter change).
  3. server runtime row wins when local adapter and model disagree asserted no write at all on its first tick. The record now also gains the missing baseline, so it asserts the write adds only the baseline while adapter and model still come from the row.

Deliberate boundaries

  • A record whose row declares neither a model nor an environment is only healed of the missing baseline. A row that declares nothing still never strips a hand-set environment (a row without a model never strips a hand-set environment), but the record is now also checked for the shipped baseline: a seat minted before 0.1.47 carries no mcp[], its row never changes, and nothing else would ever write it — so it would stay tool-less for as long as it runs. Vera's probe caught this gap on the first version of the fix (the repair sat inside the wanted branch only); the second commit applies the same helper there, behind the dirty check so it cannot restart a healthy seat on every tick.
  • A server-declared environment still wins for the facets it names. When the declaration replaces the environment and carries no mcp, the seat gets the shipped default rather than nothing; a hand-set entry in the local record is not preserved in that case. That is the existing server-authoritative rule, unchanged here — before this PR the same path stripped the entry and left the seat tool-less.

Verified on the real writers, for the gap Vera found

With the real saveAgentToken/loadAgentToken and a temp HOME: a record with adapter: pi and no environment, behind a row declaring nothing, gains exactly two keys (environment, savedAt) — every other field byte-identical — and writes nothing on the next tick. A stub record on the same path is untouched.

Not from this PR

The full CLI suite is sometimes red on codex adapter › cleans up the per-spawn temp dir even when spawn rejects (a shared-tmpdir() count racing the previous test's async cleanup). Pre-existing and measured on the base: 3 full runs on unmodified c8078aba gave green/fail/green; my branch gave the same shape. The suite is 10/10 in isolation either way. No file on that path is in this diff.

Docs/docs-only? No — cli/src changed, so the version rose 0.1.45 → 0.1.47 (0.1.46 is already claimed by the older open #1726).

@samxu01
samxu01 force-pushed the kai/task048-daemon-default-mcp branch from 1295103 to 547d3fd Compare September 18, 2026 11:36
@lilyshen0722
lilyshen0722 force-pushed the kai/task048-daemon-default-mcp branch from bf9bc6b to 7e70004 Compare September 18, 2026 11:43
A seat installed server-side — nobody ever runs `agent attach` on the machine —
reaches the daemon as `runtime.adapter/model/effort` and no ADR-008 environment
at all. The daemon wrote that shape straight into the token record, so the CLI
it spawned had no commonly_* tools and could not post; in the C4 run the
operator hand-added the `@commonlyai/mcp@latest` entry to c4-smoke's token
before the seat was usable (TASK-048).

The default already existed for `agent attach` and for the COMMONLY_AGENT_TOKEN
bootstrap in `agent run`. It now lives in lib/default-environment.js, and the
daemon applies it to every record it writes for an adapter that consumes mcp[].

The predicate is "is there an mcp entry named commonly?", not "is mcp[] absent?"
— the daemon also projects live room grants into this array, so a seat can
arrive with `mcp: [<grant broker>]` and still hold no kernel server.

Two things this deliberately does not do: a record the row never touches is not
healed (that is a migration, not this fix), and a server-declared environment
still wins for the facets it names — a hand-set commonly entry in the local
record is replaced by the shipped default when the declaration replaces the
environment, which is the existing server-authoritative rule.

Verified against the real c4-smoke row shape with the real saveToken/loadToken
writers and a temp HOME: the written environment is byte-identical to the entry
the operator added by hand, both for a fresh mint and for an existing record
with no mcp[].

Two merged tests pinned the old shape (no environment key invented; exact
environment equality). Inverted deliberately — that shape was the defect.
…hat declares nothing

Vera's probe on the first version of this fix: `ensureToken` reached
`return 'ready'` without writing whenever the row declared no environment,
model or effort and the adapter had not changed. The repair lived only inside
the `wanted` branch, so a record minted before the default existed — c4-smoke's
exact shape, adapter present and no environment — stayed tool-less after the
upgrade. The record is never touched again on that path, so nothing else could
repair it either.

Apply the same `withDefaultMcpServer(existing.environment, existing.adapter)`
there, guarded by the dirty check: this runs on every tick, so an unconditional
write would restart the seat forever. A record that already declares the
server, or whose adapter has no mcp consumption path, is not written.

Verified against the real saveAgentToken/loadAgentToken writers with a temp
HOME: the heal adds exactly two keys (`environment`, `savedAt`), every other
field byte-identical, no write on the next tick, and a `stub` record untouched.

The adapter-change test pinned the old shape — it asserted no write on the first
tick for a record with no mcp[]. Inverted deliberately, as with the others.
@lilyshen0722
lilyshen0722 force-pushed the kai/task048-daemon-default-mcp branch from 7e70004 to 069a647 Compare September 18, 2026 11:55
@lilyshen0722
lilyshen0722 merged commit d620c25 into main Sep 18, 2026
13 checks passed
samxu01 pushed a commit that referenced this pull request Sep 18, 2026
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.
samxu01 pushed a commit that referenced this pull request Sep 18, 2026
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.
samxu01 pushed a commit that referenced this pull request Sep 18, 2026
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.
samxu01 pushed a commit that referenced this pull request Sep 19, 2026
…foreign env is refused

Sprint-review measured (Sharpen 69526/69534) that a command-only rule
admits three payloads on the shipped `npx -y @commonlyai/mcp@latest`:
`NODE_OPTIONS=--import=data:…` (code execution in the npx child),
`npm_config_registry` pointed at an attacker (the package itself comes
from them), and a literal `COMMONLY_API_URL=https://attacker…` (the MCP
server posts the seat token there). None needs a `${` expansion, so the
origin rule never saw them.

Now a declared stdio entry is admitted only when it IS the shipped server
— its command, env limited to the two canonical placeholders at their
canonical values, no args, no cwd — or when its execution shape (command,
args, cwd, env) equals an entry the operator already installed in the
local token record. The refusal names the offending env keys.

Tests: the three payloads plus a literal token, extra args and a cwd on
the shipped command are refused; an installed entry matches only as a
whole (same command with an added NODE_OPTIONS is refused); the shipped
entry with a subset of its env still passes. Two #1741 fixtures declared
shapes the guard now refuses and are moved to admitted ones — a
same-origin broker URL, and a hand-set command that is present in the
local token record — without changing what those tests pin.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W7WB68pA8L2nkvSqZPx37w
lilyshen0722 added a commit that referenced this pull request Sep 19, 2026
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.
lilyshen0722 added a commit that referenced this pull request Sep 19, 2026
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.
lilyshen0722 added a commit that referenced this pull request Sep 19, 2026
…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.
samxu01 pushed a commit that referenced this pull request Sep 19, 2026
…foreign env is refused

Sprint-review measured (Sharpen 69526/69534) that a command-only rule
admits three payloads on the shipped `npx -y @commonlyai/mcp@latest`:
`NODE_OPTIONS=--import=data:…` (code execution in the npx child),
`npm_config_registry` pointed at an attacker (the package itself comes
from them), and a literal `COMMONLY_API_URL=https://attacker…` (the MCP
server posts the seat token there). None needs a `${` expansion, so the
origin rule never saw them.

Now a declared stdio entry is admitted only when it IS the shipped server
— its command, env limited to the two canonical placeholders at their
canonical values, no args, no cwd — or when its execution shape (command,
args, cwd, env) equals an entry the operator already installed in the
local token record. The refusal names the offending env keys.

Tests: the three payloads plus a literal token, extra args and a cwd on
the shipped command are refused; an installed entry matches only as a
whole (same command with an added NODE_OPTIONS is refused); the shipped
entry with a subset of its env still passes. Two #1741 fixtures declared
shapes the guard now refuses and are moved to admitted ones — a
same-origin broker URL, and a hand-set command that is present in the
local token record — without changing what those tests pin.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W7WB68pA8L2nkvSqZPx37w
samxu01 pushed a commit that referenced this pull request Sep 19, 2026
…foreign env is refused

Sprint-review measured (Sharpen 69526/69534) that a command-only rule
admits three payloads on the shipped `npx -y @commonlyai/mcp@latest`:
`NODE_OPTIONS=--import=data:…` (code execution in the npx child),
`npm_config_registry` pointed at an attacker (the package itself comes
from them), and a literal `COMMONLY_API_URL=https://attacker…` (the MCP
server posts the seat token there). None needs a `${` expansion, so the
origin rule never saw them.

Now a declared stdio entry is admitted only when it IS the shipped server
— its command, env limited to the two canonical placeholders at their
canonical values, no args, no cwd — or when its execution shape (command,
args, cwd, env) equals an entry the operator already installed in the
local token record. The refusal names the offending env keys.

Tests: the three payloads plus a literal token, extra args and a cwd on
the shipped command are refused; an installed entry matches only as a
whole (same command with an added NODE_OPTIONS is refused); the shipped
entry with a subset of its env still passes. Two #1741 fixtures declared
shapes the guard now refuses and are moved to admitted ones — a
same-origin broker URL, and a hand-set command that is present in the
local token record — without changing what those tests pin.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W7WB68pA8L2nkvSqZPx37w
lilyshen0722 added a commit that referenced this pull request Sep 19, 2026
…foreign command or ship the seat token off-instance (#1744)

* fix(cli): the daemon refuses a declared environment that would run a foreign command or ship the seat token off-instance

The daemon projects `AgentInstallation.config.environment` onto the
owner's machine: each declared stdio server is spawned as the operator and
each declared http server receives the seat token wherever the declaration
puts `${COMMONLY_AGENT_TOKEN}`. The registry PATCH that writes that
declaration was pod-member gated (Vera, Connectors 69500), so a plain
member could run a command on the owner's laptop or exfiltrate the token.
The server fix (owner/admin gating) is kai's; this is the daemon's own
layer, so the machine holds even if the server is wrong again.

`declared-mcp-guard.js`, fail-closed:
- stdio: the command must be the shipped commonly MCP server
  (`npx -y @commonlyai/mcp@<tag>`) or a command already present in the
  local token record — the operator installed those by hand.
- http/sse: a server whose url or headers carry the token placeholder must
  resolve (after `${COMMONLY_API_URL}` / `${COMMONLY_INSTANCE_URL}`
  expansion) to the instance's own origin. The grant broker passes;
  anything else keeps the token. A server without the placeholder may
  point anywhere.

The supervisor audits at both adoption points — an existing token being
updated and a first mint — and on refusal keeps the current seat (or
skips the mint), logging one line per refused server. It never adopts a
partial environment.

Tests: 10 unit cases on the audit (shipped/pinned default, arbitrary
command, operator-installed command, foreign-origin token header, token in
the URL, tokenless http anywhere, same-origin literal and alias, malformed
entries) and three supervisor cases (refused at update, refused at mint,
default + broker adopted as before). Two older fixtures used a made-up
`npx commonly-mcp` command and now use the shipped one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoB8EvqzEZEJoKwqFiBvts

* fix(cli): the declared-mcp guard is origin-based; any foreign expansion is refused outright

Vera measured (Connectors 69519) that a placeholder-matching guard is
bypassed: the claude CLI expands `${VAR}` and `${VAR:-default}` in url and
headers from its own environment, so `?t=${COMMONLY_AGENT_TOKEN:-}` is not
the literal placeholder and still becomes the token, and outside the
public sandbox that environment is the operator's — `?k=${GITHUB_TOKEN}`
leaks too. Her listener received the token, the Bearer and a foreign var.

Now: a declared http/sse entry is allowed only when its url, with only the
two instance placeholders resolved and nothing else expanded, parses to
the instance origin — tokenless or not. Any `${` other than the three
known placeholders, anywhere in an entry (url, headers, command, env,
args), is refused before the transport rule runs.

Tests: `:-` in the url (foreign and same-origin), `:-` in a header, a
non-Commonly var in url and header, a `:-` default on the URL placeholder
itself, a foreign expansion in the shipped stdio server's env and args,
and a tokenless off-origin http server (now refused).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoB8EvqzEZEJoKwqFiBvts

* chore(cli): 0.1.49 — daemon declared-environment guard

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoB8EvqzEZEJoKwqFiBvts

* fix(cli): the stdio rule is whole-entry — the shipped command with a foreign env is refused

Sprint-review measured (Sharpen 69526/69534) that a command-only rule
admits three payloads on the shipped `npx -y @commonlyai/mcp@latest`:
`NODE_OPTIONS=--import=data:…` (code execution in the npx child),
`npm_config_registry` pointed at an attacker (the package itself comes
from them), and a literal `COMMONLY_API_URL=https://attacker…` (the MCP
server posts the seat token there). None needs a `${` expansion, so the
origin rule never saw them.

Now a declared stdio entry is admitted only when it IS the shipped server
— its command, env limited to the two canonical placeholders at their
canonical values, no args, no cwd — or when its execution shape (command,
args, cwd, env) equals an entry the operator already installed in the
local token record. The refusal names the offending env keys.

Tests: the three payloads plus a literal token, extra args and a cwd on
the shipped command are refused; an installed entry matches only as a
whole (same command with an added NODE_OPTIONS is refused); the shipped
entry with a subset of its env still passes. Two #1741 fixtures declared
shapes the guard now refuses and are moved to admitted ones — a
same-origin broker URL, and a hand-set command that is present in the
local token record — without changing what those tests pin.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W7WB68pA8L2nkvSqZPx37w

* fix(cli): the guard refuses an entry that declares both a command and a url; cli 0.1.55

The adapters classified an mcp entry by which field was present, so an
entry with `transport: 'http'`, the instance's own url and a `command`
passed the origin check here and ran as stdio in pi/codex with the token
substituted (measured on the PR, closed adapter-side in #1764). One entry
is one transport: both fields, a url on a stdio entry, or a command on an
http/sse entry are refused before any other rule runs.

Version 0.1.55: this PR lands after the fleet's ascending chain (#1754
0.1.51 → #1761 0.1.52 → #1743 0.1.53 → #1764 0.1.54).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W7WB68pA8L2nkvSqZPx37w

* test(cli): TASK-065 fixture declares the shipped commonly server, which the guard admits

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W7WB68pA8L2nkvSqZPx37w

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant