Skip to content

feat(cli): the daemon withholds the grant broker from a seat it cannot confine (TASK-063) - #1798

Merged
lilyshen0722 merged 3 commits into
mainfrom
kai/task063-daemon-refusal
Sep 20, 2026
Merged

lilyshen0722 merged 3 commits into
mainfrom
kai/task063-daemon-refusal

Conversation

@lilyshen0722

Copy link
Copy Markdown
Contributor

feat(cli): the daemon withholds the grant broker from a seat it cannot confine (TASK-063)

A grant arrives as an injected MCP server and confinement is declared in a
different field, so a seat can be handed a granter's authority with nothing
keeping it in. The server refuses at the projection for the rows it can judge
host-independently; this is the other emitter of the same refusal — the layer
that knows this host, resolved the adapter locally, and holds the record the
seat actually runs from, including the records the projection never reaches
(a row naming no adapter, a backend older than the refusal, a hand-written
token file).

Entry-level, per wren 69829: the broker entry is withheld and the seat still
starts, because the seat was never promised confinement. A seat-level refusal
is #1727's case — a declared sandbox the host cannot enforce.

The refusal is applied at the derive itself: seatBaseline is where an
environment becomes the one the seat runs under, and one local helper wraps it
for all four derive sites, so no site can be missed.

Two facts taken from the live fleet rather than assumed. The broker url a
record holds is the UNRESOLVED placeholder
"${COMMONLY_API_URL}/api/mcp/grants/" (c4-smoke, the only record declaring
one), and new URL() throws on that string — so the adapter's path predicate,
which is called after substitution, matches nothing here. The daemon resolves
the known placeholders to its own instance first, and matches only OUR broker:
a foreign server that happens to share the path is left to the adapters' own
origin rules. And because this layer must judge the case the server may not, it
gains one reason the server does not have: sandbox_absent — an absent block is
the normal state of a daemon-provisioned seat (quill carries none) so the
server must allow it, while reaching it here means the baseline supplied none.

Evidence: 25 tests in a new predicate table, 3 in the supervisor's derive
wiring, both including the controls that keep this from touching a working
seat (a confined record's broker rides and the record is not even rewritten —
identity is the dirty check at the derive sites). Full cli suite 41 suites /
671 tests, eslint src clean. Nine mutations, each reddening its own tests: the
guard removed (5 red), the fresh-mint site unguarded (the pi test alone),
placeholder resolution removed (8), name-keyed instead of url-keyed (2, both
controls), the mode set widened (3), absent allowed (4), identity broken (3,
including two pre-existing tests that already rely on it), the adapter check
narrowed to pi (2).

Honest limit, unchanged from the server half: a daemon-side refusal is still
invisible to the grant read, because no daemon-to-server reporting channel
exists (the heartbeat carries four fields and Machine.ts declares no strict).
The grant read says "server-side refusals only" and must not be read as
liveness.

…t confine (TASK-063)

A grant arrives as an injected MCP server and confinement is declared in a
different field, so a seat can be handed a granter's authority with nothing
keeping it in. The server refuses at the projection for the rows it can judge
host-independently; this is the other emitter of the same refusal — the layer
that knows this host, resolved the adapter locally, and holds the record the
seat actually runs from, including the records the projection never reaches
(a row naming no adapter, a backend older than the refusal, a hand-written
token file).

Entry-level, per wren 69829: the broker entry is withheld and the seat still
starts, because the seat was never promised confinement. A seat-level refusal
is #1727's case — a declared sandbox the host cannot enforce.

The refusal is applied at the derive itself: seatBaseline is where an
environment becomes the one the seat runs under, and one local helper wraps it
for all four derive sites, so no site can be missed.

Two facts taken from the live fleet rather than assumed. The broker url a
record holds is the UNRESOLVED placeholder
"${COMMONLY_API_URL}/api/mcp/grants/<id>" (c4-smoke, the only record declaring
one), and new URL() throws on that string — so the adapter's path predicate,
which is called after substitution, matches nothing here. The daemon resolves
the known placeholders to its own instance first, and matches only OUR broker:
a foreign server that happens to share the path is left to the adapters' own
origin rules. And because this layer must judge the case the server may not, it
gains one reason the server does not have: sandbox_absent — an absent block is
the normal state of a daemon-provisioned seat (quill carries none) so the
server must allow it, while reaching it here means the baseline supplied none.

Evidence: 25 tests in a new predicate table, 3 in the supervisor's derive
wiring, both including the controls that keep this from touching a working
seat (a confined record's broker rides and the record is not even rewritten —
identity is the dirty check at the derive sites). Full cli suite 41 suites /
671 tests, eslint src clean. Nine mutations, each reddening its own tests: the
guard removed (5 red), the fresh-mint site unguarded (the pi test alone),
placeholder resolution removed (8), name-keyed instead of url-keyed (2, both
controls), the mode set widened (3), absent allowed (4), identity broken (3,
including two pre-existing tests that already rely on it), the adapter check
narrowed to pi (2).

Honest limit, unchanged from the server half: a daemon-side refusal is still
invisible to the grant read, because no daemon-to-server reporting channel
exists (the heartbeat carries four fields and Machine.ts declares no strict).
The grant read says "server-side refusals only" and must not be read as
liveness.
…t (TASK-063)

vera (70369) measured a fail-open: a bound instance spelled
`https://api.commonly.me/` turns "${COMMONLY_API_URL}/api/mcp/grants/g1" into
`https://api.commonly.me//api/mcp/grants/g1`, whose pathname starts `//api/`, so
the entry is not recognised as ours and the broker RIDES into a seat this module
had just decided it cannot confine. Reachable, not theoretical: `agent.js` takes
instanceUrl from COMMONLY_API_URL with a bare `.trim()`, while config.js is what
strips the slash.

Reproduced against the module before touching it — the same entry matched with
`https://api.commonly.me` and missed with `https://api.commonly.me/`, and three
non-control rows missed, the other two spellings being a doubled slash stored in
the record itself and a protocol-relative one.

The fix is ONE mechanism: duplicate slashes are collapsed before the path
predicate sees them, in both branches (absolute and relative). It covers the
doubled slash whichever side produced it. The obvious alternative — stripping
the instance's trailing slash — covers only the join, and a mutation showed the
two are redundant at this call site: with the collapse in place, removing the
strip reddened nothing. So the strip is not in the diff; a mechanism no test can
distinguish from another is one a reader cannot check.

Two mutations witness the mechanism that remains: dropping the collapse in the
absolute branch, and in the relative branch, each reddens the doubled-slash case
alone. Dropping the origin check reddens the two controls. Collapsing can only
turn a miss into a match, and a match here means WITHHOLD, so the normalization
moves in the safe direction — the origin equality that gates it is unchanged,
which is what keeps a foreign server out however its path is spelled.

Guard suite 31 tests, supervisor 41; full cli 41 suites / 674 tests; eslint src
clean.
…th (TASK-063)

vera (70372) cleared the fix and corrected one claim in the header: it listed a
protocol-relative spelling as covered when `//api.commonly.me/api/mcp/grants/g1`
measures false — collapsing reads it as the path `/api.commonly.me/api/...`,
which is not the broker's path.

Rather than soften the sentence, the gap is closed, because a miss here is
FAIL-OPEN and that spelling is reachable in exactly the case this layer exists
for — a hand-written local record, which no projection audits. URL semantics say
the string names THIS instance, so a schemeless value is now resolved against the
bound instance as well as read as an anchored path, and counts when that reading
lands on our origin and the broker's path. Both readings are origin-checked, so a
foreign host gets neither: `//evil.example/api/mcp/grants/g1` stays false, and so
does any schemeless value when the daemon has no instance to resolve against.

vera's upstream fact is recorded as additional, not as the reason it was safe to
leave: `declared-mcp-guard`'s `originOf` cannot parse the spelling, so a DECLARED
environment is refused before the daemon adopts it — which does not cover the
record path this module was built for.

Mutations: dropping the URL reading reddens that case alone; letting it ignore
whose origin it landed on reddens the foreign-host control; dropping the anchored
path reading reddens the doubled-slash case. Guard suite 32 tests, supervisor 41,
full cli 41 suites / 678 tests, eslint src clean.
@samxu01
samxu01 force-pushed the kai/task063-daemon-refusal branch from 0b9359b to 89aefdf Compare September 20, 2026 01:40
@lilyshen0722
lilyshen0722 merged commit 1b7e8a0 into main Sep 20, 2026
13 checks passed
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