From 7041d8a2e832bbf42513a7977f7c72b79c8840bd Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sat, 24 Oct 2026 02:13:03 -0700 Subject: [PATCH 1/3] docs(plans): kernel parking for a seat narrowed to one pod (TASK-214) Designs the first of the CTO's two conditions for the outside offer (82411). Proposed, not built: it builds on A, the runtime-token narrowing in #2386, which has not merged. At N = {P}, a post into another pod and a DM that would open a new room park as approval cards in P. The seat's installer in P decides, the card names who will read the result, and Approve re-checks and runs the route's own path. A refuses every other write outside P, and a seat narrowed in two or more pods is refused as today. The 2026-10-14 sketch and the Still open line are marked superseded, and the TASK-213 header records connector-ops adopting dc53ab37 as the build spec (81864). Cites are read at main c732326a. Co-Authored-By: Claude Opus 5.5 Co-authored-by: wren Agent-Model: claude/claude-opus-5-5/max --- docs/plans/hosted-mcp-connection-scope.md | 31 +++++++++++++++++++---- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/docs/plans/hosted-mcp-connection-scope.md b/docs/plans/hosted-mcp-connection-scope.md index 1fbd9c117f..0e32cb1431 100644 --- a/docs/plans/hosted-mcp-connection-scope.md +++ b/docs/plans/hosted-mcp-connection-scope.md @@ -28,10 +28,12 @@ **Amended 2026-10-19 (writes, with the member's approval):** Sam ruled, relayed by connector-ops on the Wave 1 rows (TASK-237 to TASK-246), that "writes are allowed when the member approves them, and apps are chosen by what our users use". That lifts §3's bar that every entry's credential be unable to write (76043). A tool that writes is pinned `write`, so each call parks for the credential's owner with its exact arguments, and the mint still refuses `write` on a hosted grant (§3, §5). The ruling accepts two residuals: a token scoped to write can write whatever the pin says, and a row connected before its entry requested a write scope must reconnect to write (§3). Without the bar, what a read pin may reach rests on the pin set, so §3 now records the rules Wave 1's pin sets are ruled under (Wren 78917). §5 no longer requires the first write entry to keep a credential that can write away from unparked calls. Its other item stands, and §5 now specifies it: a parked call runs only under the pin it parked under, and that is built before the first entry that pins a write merges. Wave 1's ten rows replace the 2026-10-14 hold and every order step 8 gave, and Basecamp, Honeycomb and Stripe are off (§10, step 8). §12 is unchanged: a hosted write parks on a card that shows its canonical arguments, which is the approval gate the CTO's #2168 ruling allows. The code this amendment cites is read at `main` `df8c219f`. -**Amended 2026-10-21 (TASK-213):** connector-ops ruled, for the CTO's security gate, that §12's 2026-10-21 amendment at `b99e23fb` is the build spec for A–D, and that E is built after them as TASK-257 (the TASK-213 row). §12 now carries the ruling's three notes: the route-table test covers every router that accepts a runtime token, an agent upload resolves to a pod or is refused, and a seat narrowed in two or more pods is told why in plain words. The code gate's census of those routers (Vera, the same row) found three files the ruling's list lacked. Reading every router against A's table then found a class it had no cell for: a seat's own event acknowledgements and spawn credentials, which pass. The same reading added two precisions: a write's pod is read from the record it writes, and a per-spawn child meets B's point 4 through the seat token that minted it. The code this amendment cites is read at `main` `cd37b32e`. +**Amended 2026-10-21 (TASK-213):** connector-ops ruled, for the CTO's security gate, that §12's 2026-10-21 amendment at `b99e23fb` is the build spec for A–D, and that E is built after them as TASK-257 (the TASK-213 row). §12 now carries the ruling's three notes: the route-table test covers every router that accepts a runtime token, an agent upload resolves to a pod or is refused, and a seat narrowed in two or more pods is told why in plain words. The code gate's census of those routers (Vera, the same row) found three files the ruling's list lacked. Reading every router against A's table then found a class it had no cell for: a seat's own event acknowledgements and spawn credentials, which pass. The same reading added two precisions: a write's pod is read from the record it writes, and a per-spawn child meets B's point 4 through the seat token that minted it. Connector-ops then adopted this text, `dc53ab37`, as the build spec in place of `b99e23fb` (81864). The code this amendment cites is read at `main` `cd37b32e`. **Amended 2026-10-22 (TASK-258, a read pin from the vendor's method line):** connector-ops asked how a read can be pinned at a vendor that annotates nothing (the TASK-258 row; the Cloudflare entry, TASK-245, waits on it). Cloudflare's per-tool server annotates 2 of its 3,766 tools. Each of the other 3,764 opens its description with the method and route it calls, alone on the first line, such as `GET /accounts` (captured 2026-10-21). **What follows is the proposed TASK-258 contract, and none of it is built.** Until it is, the entry load refuses every `read` pin without the vendor's `readOnlyHint: true` (`integrations/hostedMcp/entries.ts:204–205`), so no entry can use the exemption below. Main has none of its pieces yet: no `admittedWithoutHint` on the pin type or `readOnlyScopes` on the entry type, no load refusal or drift row for either, and no `credential_not_read_only` verdict (`services/hostedMcpEntryService.ts:49`, `:64`, `:114`). Under the contract, a `read` pin still needs the vendor's `readOnlyHint: true`, with one exemption, made per tool and recorded on the pin: the vendor's own `GET` line, stored verbatim, and our review of that tool. The review is the gate, the line is required and never sufficient, and a tool's name is never evidence. Drift compares the stored line, and refuses a live annotation saying the tool writes. For a pin that records no annotation, that line is the only drift guard that reads what the tool does. The entry also requests only scopes it names read-only, and each row is checked against them. That condition is extra, never the gate, because Cloudflare accepts a read permission on 74 operations that are not `GET`. Cloudflare's list is one reply of about 3.5 MiB, so §3 also states the listing budget TASK-243 ruled and builds: 10 MB, shared by a listing's pages. §4's subject tool and §11 are conformed, and step 2 names the tests Vera asked for on the row (§10). **Gate:** CTO. The code this amendment cites is read at `main` `87687037`. +**Amended 2026-10-24 (TASK-214, kernel parking):** the first of the CTO's two conditions for the outside offer is that the kernel parks §12's items 1–3, and the note was assigned to Wren with Vera as its gate (82411). **What follows is the proposed TASK-214 contract, and none of it is built.** It builds on A, the runtime-token narrowing that #2386 builds and that has not merged. Main has none of its pieces yet: no action type for a parked post or DM (`services/approvalActionService.ts:34`), no executor for one (`:1021–1025`), and no parked reply. Under the contract, a seat that A narrows to one pod P parks two calls instead of being refused: a post into another pod, and a DM that would open a new room. Each becomes an approval card in P, decided by the person who installed the seat there, and runs the route's own path on Approve. A still refuses every other write outside P, and a seat narrowed in two or more pods is refused as before (§12, the TASK-214 block). **Gate:** Vera. The code this amendment cites is read at `main` `c732326a`. + **In short, a hosted MCP server is a second tool source for the grants plan's §10, not a new connector.** It adds one new `Integration.type`, `hosted-mcp`, a per-person Connection (§10.1). Its tools come from a catalogue entry this repo pins, not from the hand-written map in `toolBrokerService.ts`. A member connects their own account through the vendor's authorization server. The broker holds the credential, a room grant scopes it, the vendor runs the tools, and the trail records who acted. Grants work as they do today: tiers, attenuation, budget, expiry, the confirmation floor, approval and the trail. What is new is who wrote the tool list, and most of this note is about that. In this note, `§10.x` means that section of [tools-catalogue-room-grants.md](tools-catalogue-room-grants.md), "the grants plan", and "the entry" means one catalogue entry (§3). @@ -621,9 +623,9 @@ The rule does not compute (a)'s list. It bounds it at the pod instead. Condition - TASK-228 (#2225): an owner can make a pod invite-only, so a team can build an eligible pod. Merged as `1ad396dc` and deployed with `41e98b1e`. - #2224: the shared token issuer refuses every in-process agent (above). Merged as `4b21e895` and deployed with `d37a8600`. -**2026-10-21 amendment (TASK-213).** This re-opens pod grants to seats the daemon runs, when the server can see that the seat is confined (the TASK-213 row, 2026-10-21). It narrows the seat's runtime token while a grant reaches its pod (A), widens condition 4 and the join rule (B), adds a daemon-side check for what the server cannot see (C), and names the walks that prove it (D). They are built in that order. B must not ship before A: without A, a confined seat still holds items 1–3 through its token. Under A–D, private (unconfined) seats stay refused, because the server cannot bound item 6 for a process with a shell and its owner's credentials. E, ruled the same day, admits one on its owner's own connection only, and TASK-257 builds it after A–D. Conditions 1–3 stand, and so do the CTO's two conditions for the outside offer: kernel parking (TASK-214) and the rollout list of live grants with each owner's override recorded. The code cited in this amendment is read at `main` `cd37b32e`. +**2026-10-21 amendment (TASK-213).** This re-opens pod grants to seats the daemon runs, when the server can see that the seat is confined (the TASK-213 row, 2026-10-21). It narrows the seat's runtime token while a grant reaches its pod (A), widens condition 4 and the join rule (B), adds a daemon-side check for what the server cannot see (C), and names the walks that prove it (D). They are built in that order. B must not ship before A: without A, a confined seat still holds items 1–3 through its token. Under A–D, private (unconfined) seats stay refused, because the server cannot bound item 6 for a process with a shell and its owner's credentials. E, ruled the same day, admits one on its owner's own connection only, and TASK-257 builds it after A–D. Conditions 1–3 stand, and so do the CTO's two conditions for the outside offer: kernel parking (TASK-214, designed below and not built) and the rollout list of live grants with each owner's override recorded. The code cited in this amendment is read at `main` `cd37b32e`. -> **Scope boundary — read before implementing against this amendment.** ADR-020 D1 sorts the Guide's own actions: outward needs a person's Approve, contained stays autonomous. This amendment uses the same test of what is outward to decide something else: what any seat in a pod reached by an outsider-writable grant may call with its runtime token. Here an outward call is refused, not parked, because no parking exists for items 1–3 (TASK-214, held). A route's class here does not reclassify it under D1, and a propose-action card posted in the pod still runs on a person's Approve, as D1 and the #2168 ruling allow. Who gets woken by what is ADR-018 D8's decision, and this amendment changes none of it: a narrowed seat is still woken in its other pods, and its writes there are refused. Whether it should be woken there at all belongs in ADR-018. +> **Scope boundary — read before implementing against this amendment.** ADR-020 D1 sorts the Guide's own actions: outward needs a person's Approve, contained stays autonomous. This amendment uses the same test of what is outward to decide something else: what any seat in a pod reached by an outsider-writable grant may call with its runtime token. Here an outward call is refused, not parked, because main parks none of items 1–3. The TASK-214 amendment below proposes parking two of those calls for a seat narrowed to one pod. A route's class here does not reclassify it under D1, and a propose-action card posted in the pod still runs on a person's Approve, as D1 and the #2168 ruling allow. Who gets woken by what is ADR-018 D8's decision, and this amendment changes none of it: a narrowed seat is still woken in its other pods, and its writes there are refused. Whether it should be woken there at all belongs in ADR-018. **A. Narrow the runtime token, not the declared tools.** While a live outsider-writable grant reaches a pod an agent is installed in, every runtime call made as that agent is checked against a route table. @@ -679,6 +681,25 @@ How E meets the rest: - C applies only to seats admitted under B. A seat admitted under E already holds a shell, so a server its owner installed adds nothing to what it can reach, and C would stop the seat running at all. The mark C reads therefore names the rule that admitted the seat. - The walk, on a private claude seat and a private codex seat, each in an invite-only pod with a live grant on its owner's own hosted connection: a read lands in the pod, a write parks and runs only on the owner's Approve, and the Tools page shows point 4's statement. Another person's grant minted into the pod is refused, and so is another person's private seat joining it. Once a second person installs the seat's identity, the seat's next request in the pod is refused. +**2026-10-24 amendment (TASK-214, kernel parking).** This is the parking that the first of the CTO's two conditions for the outside offer waits on (the TASK-213 amendment above). It is proposed, and none of it is built. It builds on A, which #2386 builds and which has not merged, and it applies only to a seat whose runtime token A narrows to exactly one pod P. When N is empty nothing changes. When N holds two or more pods, A's refusal stands: a card shows its text in the pod it sits in, so a card in either grant pod would be the write A refuses there. At N = {P}, two calls park instead of being refused, and A refuses everything else it refuses today. A refusal meets the #2168 ruling as a park does, because a refused write is not an effect the seat can cause. The code this amendment cites is read at `main` `c732326a`. + +- **What parks.** These two are how a narrowed seat reaches anyone outside P. + - **A post into another pod.** `POST /pods/:podId/messages` (agentsRuntime.ts:2165) names a pod Q other than P. The route's own checks run first and are unchanged: the token must reach Q (`ensurePodMatch`, :2174), and the send-freshness hold answers before anything parks (:2180–2189). Then, in place of the post (:2194), the route parks the request's `podId`, `content`, `replyToMessageId` and `threadRootId`. A request carrying `metadata`, or a `messageType` other than text, is refused, because the card shows only what it stores. Content that `sanitizeAgentContent` would suppress (:2180–2181) makes no card, since it posts nothing today either. + - **A DM that opens a new room.** `/agent-dm` (:1058) resolves its target and checks the shared pod as it does today (`sharePod`, :1197; dmService.ts:451). Then, in place of opening the room (:1228–1230), it parks the resolved target and its label. At N = {P} the route refuses an `originPodId` other than P, because it posts a notice into the origin pod (:1237–1248). When the pair already has a room, the route returns it with no card, because opening an existing room creates nothing (dmService.ts:667–670), and each post into it parks as a post into another pod. +- **What stays refused.** Three of A's refusals gain a pointer to the path that works: `/room`'s agent path (:851) points to `/agent-dm`, `POST /pods` (:3179) to the `create_pod` card the propose-action route already makes (:2299), and self-install (:3413) to a person's install, which runs the join rule (registry/install.ts:540). Every other write outside P is refused with no pointer: reactions, the task board, decision cards and asks in another pod, the writes with no pod, publishing, GitHub issue writes, and a memory write that is not private. Each either has a person's path already or can be asked for in a parked post. +- **Who decides.** The card is proposed in P as the seat, through `proposeAction`'s non-broker branch (approvalActionService.ts:444–453). So its decider is the person who installed the seat in P, or else P's creator, and must resolve to a human. It is not the grant's connection owner, whom the broker names for a parked tool call (toolBrokerService.ts:1266). A parked post is the seat's own write, and the person who answers for the seat in P is the one who installed it there. There is one P, so there is one decider, even when several grants with different owners reach P. +- **What the card shows.** The card sits in P, and a shared card shows the params of every action but a tool call (approvalActionService.ts:94): the target pod or agent, and for a post its text and the ids it replies to or continues. It also names who will read the result: Q's members; anyone, without signing in, when Q is `publicRead` (Pod.ts:191); any signed-in user when Q is directly joinable (podListing.ts:93); the chat at the other end when Q is bridged (outsiderGrantPodEligibility.ts:26); and for a DM, every person who shares a pod with either agent (ADR-001 §3.7). Showing the text in P opens no new path, because A already lets the seat post anything in P. +- **Approve.** Only the decider can decide, and only as a human (approvalActionService.ts:788, :797). Approve first re-checks what the park assumed: N is still {P}, the target's readers are the ones the card named, and the route's own checks still pass, so the token still reaches Q, or the pair still shares a pod. A failed re-check ends the card `refused`, and nothing runs. Otherwise the executor runs the route's own path: for a post, from the post through its wakes (agentsRuntime.ts:2194–2233), and for a DM, from the open through the origin notice (:1228–1248). Each path becomes one function with two callers, the route and the executor, so the two cannot drift. A run that the route's own path rejects ends `failed`, with the route's code. A DM opens with the seat as its creator, as the route opens it today (:1228–1230). That is an exception to the executor contract, under which the person who approved owns what an executor creates (approvalActionService.ts:9–11). A DM's creator becomes the installer of both agents in it (dmService.ts:703), and the decider must not become the installer of an agent that is not theirs. +- **Endings.** A parked post or DM ends the way a parked tool call does, not the way a legacy card does. It expires closed, both when a decision arrives late (approvalActionService.ts:822–852) and in the sweep (:982–1016), where a legacy card stays decidable past its age (:862). It records one of the tool call's five endings (models/ApprovalAction.ts:27), and each ending wakes the seat with an `approval.resolved` event shaped like the tool call's (approvalActionService.ts:710–748). The wrapper hands the seat that event's `payload.content` (cli/src/commands/agent.js:1132–1133). A card lives 24 hours (models/ApprovalAction.ts:86). +- **The reply.** The parked call answers 202 with `{parked: true, approvalId}`, and the seat learns the ending from the event. The MCP server passes any 2xx body through (commonly-mcp/src/client.js:169–174), so the reply needs no MCP release. +- **What needs no hook.** The native runtime needs none. TASK-224 keeps a narrowed native turn to reading and posting (nativeRuntimeService.ts:541, :720), the post lands in the run's own pod (:793), and #2224 keeps runtime tokens from every in-process agent. The override needs none either: §12(d) is superseded and none is built, so no override can keep a parked pair accepted. +- **What main lacks.** An action type for each call (approvalActionService.ts:34; models/ApprovalAction.ts:33), an executor (approvalActionService.ts:1021–1025), the 202 reply, and the shared post path. A's table also needs a cell that lets the two calls reach their handlers at N = {P} with P recorded on the request, where A otherwise refuses them at agentRuntimeAuth. The model's comment says executors live in `services/approvalExecutors.ts` (models/ApprovalAction.ts:29–32). No such file exists, and the executors live in approvalActionService.ts. +- **Walks.** Run them on a claude seat and a codex seat, each narrowed to one pod P: + - A post into another pod parks. The card in P names that pod's readers. Approve posts it with its wakes, and Decline and expiry end it with no post. The seat is woken with each ending. + - A DM to an agent with no room parks, and on Approve it opens with the seat as its creator. A DM to an agent with a room returns the room with no card, and a post into it parks. + - The target pod made public between the park and the Approve ends the card `refused`. + - A reaction, a task claim and a pod creation outside P are refused with A's code, the last with its pointer. A seat narrowed in two pods is refused as today. + The CTO ruled on #2168 on 2026-10-14. A grant from a source that outsiders can write is refused while any agent in its pod can cause an outward effect without a person. It is allowed with no override when every such effect is approval-gated: a parked call that waits for a person's Approve, on a card that shows the canonical arguments. Anything else needs the owner's override for that source and those tools, recorded and shown on the Tools page, and revoking either side ends it. Part 1's untrusted marker applies either way, and vendor blocks stay byte-identical. This section is the design note the ruling asks for before any code, and the build is its own row. The code it cites is read at `main` `8a3a93ac`. **The reach.** A grant's reach is the set of pods its results can be posted into without leaving the people the grant was made for. For a pod grant it is the target pod. For a seat grant it is every pod the seat is a member of or installed in, because the seat carries the grant into each of them. An attenuated child keeps its parent's target (`attenuateGrant` is passed none, grants.ts:426), so it has its parent's reach. @@ -716,7 +737,7 @@ The ruling's five names map onto these items: **As ruled, the default refuses every grant today.** Every agent that calls the runtime API holds item 1, so every pod with such an agent fails rule 1, and rule 2 has no case because no DM parks. Vera's census on the TASK-212 row reached the same result from the tool side. Until the kernel parks its own outward calls, every grant from an outsider-writable source needs an override. -**Superseded 2026-10-15** by the TASK-224 amendment above: parking is no longer the next row. The ruling keeps it as the fallback if the call-time check proves too expensive (TASK-214, held). +**Superseded 2026-10-15** by the TASK-224 amendment above: parking is no longer the next row, and the ruling kept it as the fallback if the call-time check proves too expensive. **Superseded again 2026-10-24** by the TASK-214 amendment above, which designs parking for a seat A narrows to one pod: a post into another pod and a DM that opens a new room park, and A refuses the rest of items 1–3. The paragraph below is the 2026-10-14 sketch it replaces. To give rule 2 a case, the kernel would park items 1–3 for agents in the reach of such a grant, as a card with the canonical arguments. The propose-action route (agentsRuntime.ts:2159) already turns `create_pod` and broker `tool_call`s into such cards (approvalActionService.ts:29). That is a kernel build with its own row, and its cost is real: an agent in such a reach would wait for a person on every DM and every post outside it. Even after it lands, three cases stay on the override path: @@ -764,7 +785,7 @@ An override never touches part 1: the marker wraps every result either way. **Still open:** -- **Kernel parking for items 1–3** is no longer the next row: the TASK-224 ruling keeps it as the fallback (TASK-214, held). +- **Kernel parking for items 1–3** is designed in the TASK-214 amendment above and not built. Its build waits on A (#2386) and on that amendment's gate. - **Discord** was not measured. Until it is, a pod with a Discord integration counts as bridged (item 4). - **Agent memory.** A section marked public or pod is read by other agents (agentsRuntime.ts:3713 at `cd37b32e`), so the TASK-213 amendment counts writing one as outward. A private section's reader is the agent. Whether an owner's view of a seat's memory is a way out is unmeasured. - **The disclosure line** for the grant dialog and the privacy page is a separate copy task (the ruling's point 5). From 6b70f692d9bbc20c1965e52c4943b2d4f046ae24 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sat, 24 Oct 2026 02:26:35 -0700 Subject: [PATCH 2/3] docs(plans): bind a parked card to the seat it records, not to a grant (TASK-214) CodeRabbit on #2604 at 7041d8a2: the parked post and DM named no source for the identity the executor runs as, and the card's 24-hour life was not tied to the grants. A card now records the seat's agentUserId beside agentName and instanceId; Approve derives the seat's pods and authoring installation from those, never from the decider, and a DM keeps the target the card names. A parked card is not capped at a grant's expiry, because its premise is N = {P}, which Approve re-checks. Co-Authored-By: Claude Opus 5.5 Co-authored-by: wren Agent-Model: claude/claude-opus-5-5/max --- docs/plans/hosted-mcp-connection-scope.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/plans/hosted-mcp-connection-scope.md b/docs/plans/hosted-mcp-connection-scope.md index 0e32cb1431..717fef7a50 100644 --- a/docs/plans/hosted-mcp-connection-scope.md +++ b/docs/plans/hosted-mcp-connection-scope.md @@ -684,20 +684,21 @@ How E meets the rest: **2026-10-24 amendment (TASK-214, kernel parking).** This is the parking that the first of the CTO's two conditions for the outside offer waits on (the TASK-213 amendment above). It is proposed, and none of it is built. It builds on A, which #2386 builds and which has not merged, and it applies only to a seat whose runtime token A narrows to exactly one pod P. When N is empty nothing changes. When N holds two or more pods, A's refusal stands: a card shows its text in the pod it sits in, so a card in either grant pod would be the write A refuses there. At N = {P}, two calls park instead of being refused, and A refuses everything else it refuses today. A refusal meets the #2168 ruling as a park does, because a refused write is not an effect the seat can cause. The code this amendment cites is read at `main` `c732326a`. - **What parks.** These two are how a narrowed seat reaches anyone outside P. - - **A post into another pod.** `POST /pods/:podId/messages` (agentsRuntime.ts:2165) names a pod Q other than P. The route's own checks run first and are unchanged: the token must reach Q (`ensurePodMatch`, :2174), and the send-freshness hold answers before anything parks (:2180–2189). Then, in place of the post (:2194), the route parks the request's `podId`, `content`, `replyToMessageId` and `threadRootId`. A request carrying `metadata`, or a `messageType` other than text, is refused, because the card shows only what it stores. Content that `sanitizeAgentContent` would suppress (:2180–2181) makes no card, since it posts nothing today either. - - **A DM that opens a new room.** `/agent-dm` (:1058) resolves its target and checks the shared pod as it does today (`sharePod`, :1197; dmService.ts:451). Then, in place of opening the room (:1228–1230), it parks the resolved target and its label. At N = {P} the route refuses an `originPodId` other than P, because it posts a notice into the origin pod (:1237–1248). When the pair already has a room, the route returns it with no card, because opening an existing room creates nothing (dmService.ts:667–670), and each post into it parks as a post into another pod. + - **A post into another pod.** `POST /pods/:podId/messages` (agentsRuntime.ts:2165) names a pod Q other than P. The route's own checks run first and are unchanged: the token must reach Q (`ensurePodMatch`, :2174), and the send-freshness hold answers before anything parks (:2180–2189). Then, in place of the post (:2194), the route parks the request's `podId`, `content`, `replyToMessageId` and `threadRootId`. A request carrying `metadata`, or a `messageType` other than text, is refused, because the card shows only what it stores. An omitted `messageType` is text, as `postMessage` defaults it (agentMessageService.ts:997). Content that `sanitizeAgentContent` would suppress (:2180–2181) makes no card, since it posts nothing today either. + - **A DM that opens a new room.** `/agent-dm` (:1058) resolves its target and checks the shared pod as it does today (`sharePod`, :1197; dmService.ts:451). Then, in place of opening the room (:1228–1230), it parks the resolved target, as its user id, `agentName` and `instanceId`, with its label and the request's origin. At N = {P} the route refuses an `originPodId` other than P, because it posts a notice into the origin pod (:1237–1248). When the pair already has a room, the route returns it with no card, because opening an existing room creates nothing (dmService.ts:667–670), and each post into it parks as a post into another pod. - **What stays refused.** Three of A's refusals gain a pointer to the path that works: `/room`'s agent path (:851) points to `/agent-dm`, `POST /pods` (:3179) to the `create_pod` card the propose-action route already makes (:2299), and self-install (:3413) to a person's install, which runs the join rule (registry/install.ts:540). Every other write outside P is refused with no pointer: reactions, the task board, decision cards and asks in another pod, the writes with no pod, publishing, GitHub issue writes, and a memory write that is not private. Each either has a person's path already or can be asked for in a parked post. - **Who decides.** The card is proposed in P as the seat, through `proposeAction`'s non-broker branch (approvalActionService.ts:444–453). So its decider is the person who installed the seat in P, or else P's creator, and must resolve to a human. It is not the grant's connection owner, whom the broker names for a parked tool call (toolBrokerService.ts:1266). A parked post is the seat's own write, and the person who answers for the seat in P is the one who installed it there. There is one P, so there is one decider, even when several grants with different owners reach P. - **What the card shows.** The card sits in P, and a shared card shows the params of every action but a tool call (approvalActionService.ts:94): the target pod or agent, and for a post its text and the ids it replies to or continues. It also names who will read the result: Q's members; anyone, without signing in, when Q is `publicRead` (Pod.ts:191); any signed-in user when Q is directly joinable (podListing.ts:93); the chat at the other end when Q is bridged (outsiderGrantPodEligibility.ts:26); and for a DM, every person who shares a pod with either agent (ADR-001 §3.7). Showing the text in P opens no new path, because A already lets the seat post anything in P. +- **As whom it runs.** The card records the seat by `agentName` and `instanceId`, as every card does, and also by `agentUserId`, which main records only for a tool call (models/ApprovalAction.ts:59–62; approvalActionService.ts:477). Approve takes the seat only from those, never from the decider. It derives the seat's pods as agentRuntimeAuth derives a token's (agentRuntimeAuth.ts:179–192, :242–252), so the re-check below needs no token. A post is written by the installation the route would pick for Q (agentPodScope.ts:43–52), else the seat's installation in P. That installation supplies `agentName`, `instanceId`, `displayName` and config, as the route passes them (agentsRuntime.ts:2194–2209), and the wakes name the seat (:2221–2222). A DM builds both members from the two recorded users, as the route builds them (:1131–1139, :1146–1154, :1174–1182, :1207–1226). It does not resolve an alias again, so the room opens with the target the decider saw. - **Approve.** Only the decider can decide, and only as a human (approvalActionService.ts:788, :797). Approve first re-checks what the park assumed: N is still {P}, the target's readers are the ones the card named, and the route's own checks still pass, so the token still reaches Q, or the pair still shares a pod. A failed re-check ends the card `refused`, and nothing runs. Otherwise the executor runs the route's own path: for a post, from the post through its wakes (agentsRuntime.ts:2194–2233), and for a DM, from the open through the origin notice (:1228–1248). Each path becomes one function with two callers, the route and the executor, so the two cannot drift. A run that the route's own path rejects ends `failed`, with the route's code. A DM opens with the seat as its creator, as the route opens it today (:1228–1230). That is an exception to the executor contract, under which the person who approved owns what an executor creates (approvalActionService.ts:9–11). A DM's creator becomes the installer of both agents in it (dmService.ts:703), and the decider must not become the installer of an agent that is not theirs. -- **Endings.** A parked post or DM ends the way a parked tool call does, not the way a legacy card does. It expires closed, both when a decision arrives late (approvalActionService.ts:822–852) and in the sweep (:982–1016), where a legacy card stays decidable past its age (:862). It records one of the tool call's five endings (models/ApprovalAction.ts:27), and each ending wakes the seat with an `approval.resolved` event shaped like the tool call's (approvalActionService.ts:710–748). The wrapper hands the seat that event's `payload.content` (cli/src/commands/agent.js:1132–1133). A card lives 24 hours (models/ApprovalAction.ts:86). +- **Endings.** A parked post or DM ends the way a parked tool call does, not the way a legacy card does. It expires closed, both when a decision arrives late (approvalActionService.ts:822–852) and in the sweep (:982–1016), where a legacy card stays decidable past its age (:862). It records one of the tool call's five endings (models/ApprovalAction.ts:27), and each ending wakes the seat with an `approval.resolved` event shaped like the tool call's (approvalActionService.ts:710–748). The wrapper hands the seat that event's `payload.content` (cli/src/commands/agent.js:1132–1133). A card lives 24 hours (models/ApprovalAction.ts:86). A tool call's card is also capped at its grant's expiry (approvalActionService.ts:482–484; toolBrokerService.ts:1269), and this one is not, because a parked post or DM spends no grant. Its premise is N = {P}. A grant's expiry or revocation ends that premise only when no other grant reaches P, and a grant reaching a second pod, or the seat leaving P, ends it with no grant lapsing at all. So Approve re-checks N, and a lapsed premise ends the card `refused` with nothing run. - **The reply.** The parked call answers 202 with `{parked: true, approvalId}`, and the seat learns the ending from the event. The MCP server passes any 2xx body through (commonly-mcp/src/client.js:169–174), so the reply needs no MCP release. - **What needs no hook.** The native runtime needs none. TASK-224 keeps a narrowed native turn to reading and posting (nativeRuntimeService.ts:541, :720), the post lands in the run's own pod (:793), and #2224 keeps runtime tokens from every in-process agent. The override needs none either: §12(d) is superseded and none is built, so no override can keep a parked pair accepted. - **What main lacks.** An action type for each call (approvalActionService.ts:34; models/ApprovalAction.ts:33), an executor (approvalActionService.ts:1021–1025), the 202 reply, and the shared post path. A's table also needs a cell that lets the two calls reach their handlers at N = {P} with P recorded on the request, where A otherwise refuses them at agentRuntimeAuth. The model's comment says executors live in `services/approvalExecutors.ts` (models/ApprovalAction.ts:29–32). No such file exists, and the executors live in approvalActionService.ts. - **Walks.** Run them on a claude seat and a codex seat, each narrowed to one pod P: - A post into another pod parks. The card in P names that pod's readers. Approve posts it with its wakes, and Decline and expiry end it with no post. The seat is woken with each ending. - - A DM to an agent with no room parks, and on Approve it opens with the seat as its creator. A DM to an agent with a room returns the room with no card, and a post into it parks. - - The target pod made public between the park and the Approve ends the card `refused`. + - A DM to an agent with no room parks, and on Approve it opens with the seat as its creator. A DM to an agent with a room returns the room with no card, and a post into it parks. A DM by alias, with the alias rebound after the park, opens with the target on the card. + - The target pod made public, or the only grant reaching P revoked, between the park and the Approve ends the card `refused`. - A reaction, a task claim and a pod creation outside P are refused with A's code, the last with its pointer. A seat narrowed in two pods is refused as today. The CTO ruled on #2168 on 2026-10-14. A grant from a source that outsiders can write is refused while any agent in its pod can cause an outward effect without a person. It is allowed with no override when every such effect is approval-gated: a parked call that waits for a person's Approve, on a card that shows the canonical arguments. Anything else needs the owner's override for that source and those tools, recorded and shown on the Tools page, and revoking either side ends it. Part 1's untrusted marker applies either way, and vendor blocks stay byte-identical. This section is the design note the ruling asks for before any code, and the build is its own row. The code it cites is read at `main` `8a3a93ac`. From dc9e7be279b413c27a45dda219693832dd4a44c7 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sat, 24 Oct 2026 02:45:17 -0700 Subject: [PATCH 3/3] docs(plans): a parked post re-runs the freshness hold, and an ending reaches the seat only in P (TASK-214) Otto's HOLD at 6b70f692 and CodeRabbit's review at the same head. - A parked post stores continueAnyway and the card shows it. The shared post path starts at the send-freshness hold, so Approve runs the hold again with the stored flag: without it, a message that reached Q after the seat last read Q ends the run failed with unseen_messages. The card and the event carry the code, never the messages the hold lists. - The ending event is enqueued in P, and the seat's poller claims only its current pods, so the wake is promised only while the seat is in P. Once it leaves, the ending is on the card and nothing runs. - Two walks: the hold at park and at Approve, and the seat uninstalled from P while its card waits. Co-Authored-By: Claude Opus 5.5 Co-authored-by: wren Agent-Model: claude/claude-opus-5-5/max --- docs/plans/hosted-mcp-connection-scope.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/plans/hosted-mcp-connection-scope.md b/docs/plans/hosted-mcp-connection-scope.md index 717fef7a50..70511c1bee 100644 --- a/docs/plans/hosted-mcp-connection-scope.md +++ b/docs/plans/hosted-mcp-connection-scope.md @@ -684,14 +684,19 @@ How E meets the rest: **2026-10-24 amendment (TASK-214, kernel parking).** This is the parking that the first of the CTO's two conditions for the outside offer waits on (the TASK-213 amendment above). It is proposed, and none of it is built. It builds on A, which #2386 builds and which has not merged, and it applies only to a seat whose runtime token A narrows to exactly one pod P. When N is empty nothing changes. When N holds two or more pods, A's refusal stands: a card shows its text in the pod it sits in, so a card in either grant pod would be the write A refuses there. At N = {P}, two calls park instead of being refused, and A refuses everything else it refuses today. A refusal meets the #2168 ruling as a park does, because a refused write is not an effect the seat can cause. The code this amendment cites is read at `main` `c732326a`. - **What parks.** These two are how a narrowed seat reaches anyone outside P. - - **A post into another pod.** `POST /pods/:podId/messages` (agentsRuntime.ts:2165) names a pod Q other than P. The route's own checks run first and are unchanged: the token must reach Q (`ensurePodMatch`, :2174), and the send-freshness hold answers before anything parks (:2180–2189). Then, in place of the post (:2194), the route parks the request's `podId`, `content`, `replyToMessageId` and `threadRootId`. A request carrying `metadata`, or a `messageType` other than text, is refused, because the card shows only what it stores. An omitted `messageType` is text, as `postMessage` defaults it (agentMessageService.ts:997). Content that `sanitizeAgentContent` would suppress (:2180–2181) makes no card, since it posts nothing today either. + - **A post into another pod.** `POST /pods/:podId/messages` (agentsRuntime.ts:2165) names a pod Q other than P. The route's own checks run first and are unchanged: the token must reach Q (`ensurePodMatch`, :2174), and the send-freshness hold answers before anything parks, so its 409 makes no card, and `continueAnyway` passes it as it does today (:2180–2189). Then, in place of the post (:2194), the route parks the request's `podId`, `content`, `replyToMessageId`, `threadRootId` and `continueAnyway`. A request carrying `metadata`, or a `messageType` other than text, is refused, because the card shows only what it stores. An omitted `messageType` is text, as `postMessage` defaults it (agentMessageService.ts:997). Content that `sanitizeAgentContent` would suppress (:2180–2181) makes no card, since it posts nothing today either. - **A DM that opens a new room.** `/agent-dm` (:1058) resolves its target and checks the shared pod as it does today (`sharePod`, :1197; dmService.ts:451). Then, in place of opening the room (:1228–1230), it parks the resolved target, as its user id, `agentName` and `instanceId`, with its label and the request's origin. At N = {P} the route refuses an `originPodId` other than P, because it posts a notice into the origin pod (:1237–1248). When the pair already has a room, the route returns it with no card, because opening an existing room creates nothing (dmService.ts:667–670), and each post into it parks as a post into another pod. - **What stays refused.** Three of A's refusals gain a pointer to the path that works: `/room`'s agent path (:851) points to `/agent-dm`, `POST /pods` (:3179) to the `create_pod` card the propose-action route already makes (:2299), and self-install (:3413) to a person's install, which runs the join rule (registry/install.ts:540). Every other write outside P is refused with no pointer: reactions, the task board, decision cards and asks in another pod, the writes with no pod, publishing, GitHub issue writes, and a memory write that is not private. Each either has a person's path already or can be asked for in a parked post. - **Who decides.** The card is proposed in P as the seat, through `proposeAction`'s non-broker branch (approvalActionService.ts:444–453). So its decider is the person who installed the seat in P, or else P's creator, and must resolve to a human. It is not the grant's connection owner, whom the broker names for a parked tool call (toolBrokerService.ts:1266). A parked post is the seat's own write, and the person who answers for the seat in P is the one who installed it there. There is one P, so there is one decider, even when several grants with different owners reach P. -- **What the card shows.** The card sits in P, and a shared card shows the params of every action but a tool call (approvalActionService.ts:94): the target pod or agent, and for a post its text and the ids it replies to or continues. It also names who will read the result: Q's members; anyone, without signing in, when Q is `publicRead` (Pod.ts:191); any signed-in user when Q is directly joinable (podListing.ts:93); the chat at the other end when Q is bridged (outsiderGrantPodEligibility.ts:26); and for a DM, every person who shares a pod with either agent (ADR-001 §3.7). Showing the text in P opens no new path, because A already lets the seat post anything in P. +- **What the card shows.** The card sits in P, and a shared card shows the params of every action but a tool call (approvalActionService.ts:94): the target pod or agent, and for a post its text, the ids it replies to or continues, and `continueAnyway` when the seat set it. It also names who will read the result: Q's members; anyone, without signing in, when Q is `publicRead` (Pod.ts:191); any signed-in user when Q is directly joinable (podListing.ts:93); the chat at the other end when Q is bridged (outsiderGrantPodEligibility.ts:26); and for a DM, every person who shares a pod with either agent (ADR-001 §3.7). Showing the text in P opens no new path, because A already lets the seat post anything in P. - **As whom it runs.** The card records the seat by `agentName` and `instanceId`, as every card does, and also by `agentUserId`, which main records only for a tool call (models/ApprovalAction.ts:59–62; approvalActionService.ts:477). Approve takes the seat only from those, never from the decider. It derives the seat's pods as agentRuntimeAuth derives a token's (agentRuntimeAuth.ts:179–192, :242–252), so the re-check below needs no token. A post is written by the installation the route would pick for Q (agentPodScope.ts:43–52), else the seat's installation in P. That installation supplies `agentName`, `instanceId`, `displayName` and config, as the route passes them (agentsRuntime.ts:2194–2209), and the wakes name the seat (:2221–2222). A DM builds both members from the two recorded users, as the route builds them (:1131–1139, :1146–1154, :1174–1182, :1207–1226). It does not resolve an alias again, so the room opens with the target the decider saw. -- **Approve.** Only the decider can decide, and only as a human (approvalActionService.ts:788, :797). Approve first re-checks what the park assumed: N is still {P}, the target's readers are the ones the card named, and the route's own checks still pass, so the token still reaches Q, or the pair still shares a pod. A failed re-check ends the card `refused`, and nothing runs. Otherwise the executor runs the route's own path: for a post, from the post through its wakes (agentsRuntime.ts:2194–2233), and for a DM, from the open through the origin notice (:1228–1248). Each path becomes one function with two callers, the route and the executor, so the two cannot drift. A run that the route's own path rejects ends `failed`, with the route's code. A DM opens with the seat as its creator, as the route opens it today (:1228–1230). That is an exception to the executor contract, under which the person who approved owns what an executor creates (approvalActionService.ts:9–11). A DM's creator becomes the installer of both agents in it (dmService.ts:703), and the decider must not become the installer of an agent that is not theirs. -- **Endings.** A parked post or DM ends the way a parked tool call does, not the way a legacy card does. It expires closed, both when a decision arrives late (approvalActionService.ts:822–852) and in the sweep (:982–1016), where a legacy card stays decidable past its age (:862). It records one of the tool call's five endings (models/ApprovalAction.ts:27), and each ending wakes the seat with an `approval.resolved` event shaped like the tool call's (approvalActionService.ts:710–748). The wrapper hands the seat that event's `payload.content` (cli/src/commands/agent.js:1132–1133). A card lives 24 hours (models/ApprovalAction.ts:86). A tool call's card is also capped at its grant's expiry (approvalActionService.ts:482–484; toolBrokerService.ts:1269), and this one is not, because a parked post or DM spends no grant. Its premise is N = {P}. A grant's expiry or revocation ends that premise only when no other grant reaches P, and a grant reaching a second pod, or the seat leaving P, ends it with no grant lapsing at all. So Approve re-checks N, and a lapsed premise ends the card `refused` with nothing run. +- **Approve.** Only the decider can decide, and only as a human (approvalActionService.ts:788, :797). Approve first re-checks what the park assumed: N is still {P}, the target's readers are the ones the card named, and the route's authority checks still pass, so the token still reaches Q, or the pair still shares a pod. A failed re-check ends the card `refused`, and nothing runs. Otherwise the executor runs the route's own path: for a post, from the send-freshness hold through its wakes (agentsRuntime.ts:2180–2233), and for a DM, from the open through the origin notice (:1228–1248). Each path becomes one function with two callers, the route and the executor, so the two cannot drift. A run that the route's own path rejects ends `failed`, with the route's code. A DM opens with the seat as its creator, as the route opens it today (:1228–1230). That is an exception to the executor contract, under which the person who approved owns what an executor creates (approvalActionService.ts:9–11). A DM's creator becomes the installer of both agents in it (dmService.ts:703), and the decider must not become the installer of an agent that is not theirs. + - Because a post's path starts at the send-freshness hold, the hold runs again when the post is made, with the stored `continueAnyway`. Without the flag, a message from anyone else that reached Q after the seat last read Q ends the run `failed` with `unseen_messages`, and nothing posts. The hold keeps a seat from posting over a conversation it has not read, and this post is made at Approve, not at the park. The decider sits in P and may have no way to read Q, so they cannot answer for what Q said in between. + - The card and the event name the code alone, never the messages the hold lists, which are Q's (agentSendFreshness.ts:99–137). A seat woken with that ending can read Q, because A passes reads at N = {P}, and then park again. +- **Endings.** A parked post or DM ends the way a parked tool call does, not the way a legacy card does. It expires closed, both when a decision arrives late (approvalActionService.ts:822–852) and in the sweep (:982–1016), where a legacy card stays decidable past its age (:862). It records one of the tool call's five endings (models/ApprovalAction.ts:27), and each ending is enqueued for the seat in P as an `approval.resolved` event shaped like the tool call's (approvalActionService.ts:710–748). The wrapper hands the seat that event's `payload.content` (cli/src/commands/agent.js:1132–1133). A card lives 24 hours (models/ApprovalAction.ts:86). A tool call's card is also capped at its grant's expiry (approvalActionService.ts:482–484; toolBrokerService.ts:1269), and this one is not, because a parked post or DM spends no grant. Its premise is N = {P}. A grant's expiry or revocation ends that premise only when no other grant reaches P, and a grant reaching a second pod, or the seat leaving P, ends it with no grant lapsing at all. So Approve re-checks N, and a lapsed premise ends the card `refused` with nothing run. + - That event wakes the seat only while the seat is still in P. Its poller claims events only in the pods it is installed in and the agent-admin pods it belongs to (agentsRuntime.ts:594–630; agentEventService.ts:1573–1577), and a token left with no installation gets a 401 (agentRuntimeAuth.ts:176–178). + - So an ending after the seat leaves P is recorded on the card, where the decider sees it, and is not delivered. Its event stays pending in P until the seat is back in P, or until the retention sweep deletes it, at 168 hours by default (agentEventService.ts:724, :883). A parked tool call's ending works the same way today, because it too is enqueued in its card's pod (approvalActionService.ts:729). + - The event is not redirected to another of the seat's pods. The seat has no turn left in P to continue, and nothing it asked for runs, because Approve's re-check of N ends the card `refused`. - **The reply.** The parked call answers 202 with `{parked: true, approvalId}`, and the seat learns the ending from the event. The MCP server passes any 2xx body through (commonly-mcp/src/client.js:169–174), so the reply needs no MCP release. - **What needs no hook.** The native runtime needs none. TASK-224 keeps a narrowed native turn to reading and posting (nativeRuntimeService.ts:541, :720), the post lands in the run's own pod (:793), and #2224 keeps runtime tokens from every in-process agent. The override needs none either: §12(d) is superseded and none is built, so no override can keep a parked pair accepted. - **What main lacks.** An action type for each call (approvalActionService.ts:34; models/ApprovalAction.ts:33), an executor (approvalActionService.ts:1021–1025), the 202 reply, and the shared post path. A's table also needs a cell that lets the two calls reach their handlers at N = {P} with P recorded on the request, where A otherwise refuses them at agentRuntimeAuth. The model's comment says executors live in `services/approvalExecutors.ts` (models/ApprovalAction.ts:29–32). No such file exists, and the executors live in approvalActionService.ts. @@ -699,6 +704,8 @@ How E meets the rest: - A post into another pod parks. The card in P names that pod's readers. Approve posts it with its wakes, and Decline and expiry end it with no post. The seat is woken with each ending. - A DM to an agent with no room parks, and on Approve it opens with the seat as its creator. A DM to an agent with a room returns the room with no card, and a post into it parks. A DM by alias, with the alias rebound after the park, opens with the target on the card. - The target pod made public, or the only grant reaching P revoked, between the park and the Approve ends the card `refused`. + - A post into Q while the seat has an unread message there gets the route's 409 and makes no card. A post parked with nothing unread in Q, after another member then posts in Q, ends `failed` with `unseen_messages` on Approve, and nothing posts. The seat reads Q, parks again, and that card's Approve posts. The same post parked with `continueAnyway`, which its card shows, posts on Approve. + - The seat uninstalled from P while its card waits, and still installed in Q: Approve ends the card `refused`, nothing posts, the card shows the ending, and the seat's poll claims no event in Q. - A reaction, a task claim and a pod creation outside P are refused with A's code, the last with its pointer. A seat narrowed in two pods is refused as today. The CTO ruled on #2168 on 2026-10-14. A grant from a source that outsiders can write is refused while any agent in its pod can cause an outward effect without a person. It is allowed with no override when every such effect is approval-gated: a parked call that waits for a person's Approve, on a card that shows the canonical arguments. Anything else needs the owner's override for that source and those tools, recorded and shown on the Tools page, and revoking either side ends it. Part 1's untrusted marker applies either way, and vendor blocks stay byte-identical. This section is the design note the ruling asks for before any code, and the build is its own row. The code it cites is read at `main` `8a3a93ac`.