From 5722e2afd96c762111e77499816748a9c1abd49c Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Thu, 15 Oct 2026 09:26:07 -0700 Subject: [PATCH 1/3] =?UTF-8?q?docs(plans):=20=C2=A712=20and=20the=20matri?= =?UTF-8?q?x=20follow=20the=20TASK-224=20pod=20rule=20(#2211)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §12 of hosted-mcp-connection-scope.md now states the ruled pod rule as #2211 built it: a pod grant only, no `write` mode, a private, closed and unbridged pod, and only native agents in it, refused at mint and attenuation and re-checked by the broker on every list and call. It marks the default's override finding, the parking paragraph, (c) and (d) as superseded, and records the fresh-account Linear walk under step 7. The readiness matrix adds the pod rule to C3 for grant sources, marks C4 refused by design until TASK-213, records the fresh-account Linear walk, and updates the two broker reds that #2211 makes moot. 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 | 39 +++++++++++++--------- docs/plans/integration-readiness-matrix.md | 10 +++--- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/docs/plans/hosted-mcp-connection-scope.md b/docs/plans/hosted-mcp-connection-scope.md index 836dc10363..02780ce420 100644 --- a/docs/plans/hosted-mcp-connection-scope.md +++ b/docs/plans/hosted-mcp-connection-scope.md @@ -18,6 +18,8 @@ **Amended 2026-10-14 (the catalogue hold):** Sam ruled, relayed by connector-ops (77368), that no new catalogue app is built until an outside user connects one, and that "payment apps and new OAuth apps are out". Linear, Google Calendar, Atlassian and Airtable are built, and every later entry in step 8 is held, along with the order the 2026-10-09 note set (§10, step 8). That note's batch rested on measurements the 2026-10-13 sweep re-took for its first three: Canva is now conditional and parked, and Miro and ClickUp are conditional ([hosted-mcp-catalogue-sweep-2026-10-13.md](hosted-mcp-catalogue-sweep-2026-10-13.md), #2133). So when the hold lifts, the next entry is ruled afresh. This supersedes the 2026-10-09 note's sentence that Canva, Miro and ClickUp follow Airtable. Two passages are also conformed to code that has landed since they were written: TASK-199 carries a failed list's reason into the trail (§3), and the server's adapter rule is now an allowlist, as the daemon's is (§6). The code this amendment cites is read at `main` `e4e10571`. +**Amended 2026-10-15 (TASK-224, the pod rule):** the CTO ruled on TASK-224 that a grant from a source outsiders can write is attached, and its tools called, only in an eligible pod (connector-ops 77728), and #2211 built the rule (§12). The pod is private, invite-only and unbridged, every agent in it runs on the native runtime, and the grant is a pod grant, not a seat grant. While a live grant reaches the pod, TASK-224 narrows each native agent in it to reading the pod's context and posting there. The rule reads no source declaration, and every source is outsider-writable today, so it governs every grant. This supersedes the #2168 part 2 note's finding that every such grant needs an override: a grant in an eligible pod needs neither an override nor parking. Of the three options put to the CTO (Kai, 77725), the ruling took this one, kept the second, holding the offer until the kernel parks outward calls, only as its fallback if the call-time check proves too expensive, and did not accept the third, an owner override that accepts the residual. Until TASK-213, no seat the daemon runs can use a grant, so the readiness matrix's C4 is refused by design. The code this amendment cites is read at `main` `17adb067`. + **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). @@ -443,6 +445,7 @@ Each step can be tested without the vendor, except the live measurements in step - `the page's call to start is credentialed`, before the walk (§4) - Measured live: that a removal's revoke took at Linear, shown by something other than its 200 (§3). Linear advertises one URL as both its `token_endpoint` and its `revocation_endpoint` (fetched 2026-09-29), so only a live removal shows which request that URL treats as a revoke. - The entry's `page`: Linear's Security & access settings, whose "Authorized applications" list has "Revoke access" on each app (Linear docs, *Security & Access*, fetched 2026-09-30). Its URL is pinned from the signed-in walk. + - Walked 2026-10-15, before #2211 deployed, by a fresh account with a fresh Linear workspace (connector-ops 77850, and the readiness matrix's Linear row). Connect granted `read openid`. A read grant of the 36 pins, in a pod that passes §12's rule, carried a native agent's call, ok in the trail, with #2206's marker ahead of the data. The walk had no pi seat: under §12 a pod with one in it fails the rule, so a grant there is refused for every member. Still unmeasured: a removal's revoke taking effect at Linear. 8. **The entries after Linear**, in the order connector-ops proposed (76040) and 76044 kept, with the batch of 2026-10-09 after Airtable (connector-ops 76914, Wren 76915): Atlassian, Airtable, Canva, Miro, ClickUp, Todoist, Calendly, then Google through a pre-registered client (§4). Each is its own entry PR, admitted as §11 says and walked as step 7 is. Google Calendar is built in parallel without jumping that order, since Atlassian and Airtable wait only on Sam's accounts (connector-ops 76137). - **Held** (Sam, 2026-10-14, relayed by connector-ops in 77368): no new catalogue app is built until an outside user connects one, and "payment apps and new OAuth apps are out". Linear, Google Calendar, Atlassian and Airtable are built (`integrations/hostedMcp/entries.ts:20`). Every other entry this step names is held, and so is the order above. Its batch after Airtable rests on the 2026-10-09 measurements, and the 2026-10-13 sweep re-measured the batch's first three: Canva is conditional and parked, and Miro and ClickUp are conditional ([hosted-mcp-catalogue-sweep-2026-10-13.md](hosted-mcp-catalogue-sweep-2026-10-13.md)). So when the hold lifts, the next entry is ruled afresh, not taken from this list. The rest of this step stays as the design each entry follows when it is built. - First, while Linear is the only entry: the client record (§2), then its backfill once that build's rollout has finished. @@ -509,20 +512,18 @@ The TASK-172 ruling's part 3 wording on `:180` is replaced by §6. ## 12. Outsider-writable sources and outward effects (#2168 part 2) -**2026-10-15 CTO amendment (TASK-224):** For the native-runtime connector offer, a -pod eligibility rule is a bounded alternative to the override/parking path -below. An outsider-writable grant may be a **pod grant only**, and its pod must -be private, invite-only, unbridged, and contain only native-runtime agent -members under the TASK-224 tool gate (plus any humans). The grant mint and -attenuation routes refuse failing conditions and `write` mode, which lets a -GitHub App tool write without a person's approval; `write-with-confirm` -remains available. The broker checks again -on every list, direct call, and approved call. Adding an external agent, -making the pod public or joinable, or routing a channel to it stops the next -call. Seat grants remain ineligible until TASK-213. The 2026-10-14 default -and override design below remains the broader path; its statement that no -grant can proceed without an override is superseded for this eligible pod -case. The part 1 untrusted-content marker still applies. +**2026-10-15 amendment (TASK-224).** The CTO ruled that a grant from an outsider-writable source is attached, and its tools called, only in an eligible pod (connector-ops 77728). #2211 built the rule (`outsiderGrantPodRefusal`, outsiderGrantPodEligibility.ts:26). It reads no declaration from (b) and applies to every grant, and every source is outsider-writable today. A grant may be minted, attenuated, listed or called only while all of these hold: + +1. It is a pod grant. A seat grant is refused until TASK-213. +2. Its write mode is not `write`, which would let a GitHub App tool write with no person (item 5). `write-with-confirm` remains. +3. Its pod is private and closed: `publicRead` is `false` and `joinPolicy` is `'invite-only'`, so a pod missing either field is refused. No live integration other than a hosted-MCP or GitHub App connection is attached to the pod or has a gate enabled for it. One that is revoked, disconnected, in error, inactive or paused by an admin does not count. +4. Every agent in the pod runs on the native runtime. Each agent member has exactly one active installation in the pod, its `config.runtime.runtimeType` is `native`, and every active installation belongs to an agent member. Humans are fine. + +Mint and attenuation refuse a failing grant with `outsider_grant_` and a message that points to a separate pod (grants.ts:389, :434). The broker checks again, with no cache, on every list, direct call and approved call (toolBrokerService.ts:787, :1133, :1332). So a non-native agent added, the pod made public or joinable, or a bridge enabled refuses the next call. Grants minted before #2211 are not exempt. + +While a live grant reaches a pod, every native agent in it is narrowed (TASK-224, nativeOutsiderGrantPolicy.ts:22). Its turn is offered only `commonly_read_context` and `commonly_post_message` (nativeRuntimeService.ts:267), dispatch refuses any other name with `outsider_writable_grant_tool_denied` (:304), and the post lands only in the run's pod (:353). The check is re-read before each model call and each dispatch, and a failed read narrows (:983, :1090, :1217). + +The rule does not compute (a)'s list. It bounds it at the pod instead. Condition 3 rules out item 4 and condition 2 rules out item 5. Condition 4 leaves only native agents, which hold no seat tools (item 6) and, while narrowed, can read the pod's context and post in it and nothing else (items 1–3). So (a) and (b) stand as the analysis of outward effects and sources, and the default's finding, the parking paragraph, (c) and (d) are superseded where marked below. Until TASK-213 no seat the daemon runs can use a grant: a seat grant fails condition 1, and a pod grant fails condition 4 while a daemon seat is in the pod. What TASK-213 re-opens is for a later ruling. Part 1's marker covers hosted-MCP results only (toolBrokerService.ts:1271), so a GitHub App result carries none. The CTO's conditions for offering GitHub outside name a hosted entry for that reason (TASK-225), and a GitHub App connection is made only through the administrator route (integrations.ts:390). The code this amendment cites is read at `main` `17adb067`. 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`. @@ -557,8 +558,12 @@ The ruling's five names map onto these items: - no kernel or broker tool sends email today, so email is item 6; - a write to an external system is item 5, item 6, or a bridge (item 4). +**Superseded 2026-10-15** by the TASK-224 amendment above: a grant in an eligible pod needs no override. + **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). + 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: - item 4 keeps a public, joinable or bridged pod there; @@ -577,6 +582,8 @@ To give rule 2 a case, the kernel would park items 1–3 for agents in the reach Inbound email has no entry today. An entry that reads mail is outsider-writable by definition. +**Superseded 2026-10-15** by the TASK-224 amendment above: this check was not built. #2211's rule runs at the mint, attenuation, list, direct call and approved call instead, and refuses with `outsider_grant_`. + **(c) Where the check runs.** One function computes (a)'s list for a grant from current state. It runs at the mint and at call time, and not at attenuation. - **At the mint** (`POST /api/grants`, grants.ts:298), after the existing checks and before the row is written. When the source is outsider-writable and the list is not empty, the mint refuses with `outward_effect_unaccepted` and returns the list, so the dialog can show who holds what. The same request, carrying an override (§12(d)) that names exactly that list, mints the grant. @@ -585,6 +592,8 @@ Inbound email has no entry today. An entry that reads mail is outsider-writable A refused call writes its trail row like any refusal. It does not revoke the grant, which runs again once the list is empty or an override names it. Grants minted before the build are not exempt: their first call after it ships is refused until the owner records an override. That includes our own Linear grants. +**Superseded 2026-10-15** by the TASK-224 amendment above: the ruling did not accept an owner override, and none is built. + **(d) The override.** - **Who:** the connection's owner, the only person the mint accepts (grants.ts:306). They record it at the mint, or later from the Tools page for a grant whose calls are refused. @@ -600,7 +609,7 @@ An override never touches part 1: the marker wraps every result either way. **Still open:** -- **Kernel parking for items 1–3**, recommended as the next row. Without it, rule 2 has no case. +- **Kernel parking for items 1–3** is no longer the next row: the TASK-224 ruling keeps it as the fallback (TASK-214, held). - **Discord** was not measured. Until it is, a pod with a Discord integration counts as bridged (item 4). - **Agent memory** is not counted. Its reader is the agent, and item 3 already counts the agent's other pods. 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). diff --git a/docs/plans/integration-readiness-matrix.md b/docs/plans/integration-readiness-matrix.md index 80f2809424..2975e7504f 100644 --- a/docs/plans/integration-readiness-matrix.md +++ b/docs/plans/integration-readiness-matrix.md @@ -32,8 +32,8 @@ The matrix below is the definition of "ready". A row is ready when every cell is | C0 | Offered on commonly.me | The row appears to a stranger on the deployed instance. Offering (the seeded roster) and readiness (the capability) are separate gates the page hides, so a row that is never offered is red here, not in C1 (Kai; TASK-024) | | C1 | New user connects it | A stranger account completes the connect flow without an operator | | C2 | The person sees the state | The Connectors page shows connected or not enabled; a failure lands as an Activity row; revoke is reachable at 390 (ux-lead) | -| C3 | Hosted agent uses it | A hosted agent in the pod completes a real action through it | -| C4 | Local agent uses it | A daemon seat completes the same action, running the build the deploy shipped (a stale daemon seat reads green and isn't; TASK-089). Precondition: a claude or codex seat declares `trust: 'public'`, with the mode optional, since it resolves at spawn to Seatbelt on macOS and bwrap elsewhere. A daemon seat with no sandbox block is derived to `trust: 'public'` by the seat baseline, so it reads confined. Only a hand-authored record run with `commonly agent run` and no block defaults to mode `none`, which is the unconfined case (Wren). A pi seat confines on no host, so it gets no grant broker anywhere (sprint-review) | +| C3 | Hosted agent uses it | A hosted agent in the pod completes a real action through it. For a grant source, the walk's pod must pass §12's pod rule in `hosted-mcp-connection-scope.md` (#2211) | +| C4 | Local agent uses it | A daemon seat completes the same action, running the build the deploy shipped (a stale daemon seat reads green and isn't; TASK-089). Precondition: a claude or codex seat declares `trust: 'public'`, with the mode optional, since it resolves at spawn to Seatbelt on macOS and bwrap elsewhere. A daemon seat with no sandbox block is derived to `trust: 'public'` by the seat baseline, so it reads confined. Only a hand-authored record run with `commonly agent run` and no block defaults to mode `none`, which is the unconfined case (Wren). A pi seat confines on no host, so it gets no grant broker anywhere (sprint-review). **Refused by design for every grant source until TASK-213**, from #2211: §12's pod rule in `hosted-mcp-connection-scope.md` refuses a seat grant, and a pod grant in a pod with a daemon seat in it | | C5 | Both directions, and silence is explained | Messages or events flow in and out with sender identity preserved, and when nothing flows the person can see why. A refused model route read as silence until #1827 and #1831 (Kai) | | C6 | Decision from it | A decision card is answered from the channel or app, and the agent receives the ruling | | C7 | A second open client sees it | An already-open tab converges without a refresh (the TASK-135 shape) (sprint-review) | @@ -54,7 +54,7 @@ The matrix below is the definition of "ready". A row is ready when every cell is | X | yes (admin OAuth callback + feed) | unverified | | GitHub (app) | yes | Walked 2026-09-25 and re-walked 2026-09-26 on `5561a1dd` (both below). Live on our own repository since 09-18. **red** in C1 until per-person GitHub: a team cannot connect its own repository. C2 at 390 is green since #1874, shown on `5561a1dd`. C3 (a hosted agent gets the broker, #1880) shipped and has not been walked. C8 was green on `9a32fca5`. A new grant from the connection's owner, `grant_3c4d6ad5` (2026-09-29, on Sam's approval), has the same scope as the lapsed 09-18 one: pod "C4 run 2026-09-18"; `list_issues`, `create_issue` and `close_issue`; write-with-confirm; audience Sam, `c3stranger0912` and `c4-smoke`. It runs to 2026-10-06, so C3, C4 and C8 are walkable again. None has been walked on the current build | | Hosted MCP (generic type, TASK-172) | yes | **Built and live, with one follow-up open.** Steps 1–6 of `hosted-mcp-connection-scope.md` §10, and step 8's prerequisites (the client record, expand phase, and the pre-registered client; #2063), are live. **Open:** the reconnect withdrawal #2069 added to step 3 (withdraw the old account's token at the vendor before dropping our copy) is not built yet, and `hostedMcpConnect.ts:406–420` does not do it. The bar every entry must meet (Wren 76043): a credential that cannot write. Notion, Sentry and Stripe wait for a vendor read scope. Entries are offered one by one; the rows below walk them | -| Linear (hosted MCP) | yes | Partly walked live 2026-10-01 (Sam's account, Chrome). **C1 open**: Sam's own operator-led connect succeeded on `cf9b6abf`, through CIMD with scope `read openid`, and the row records its client and the Linear subject. That doesn't prove a stranger connects unaided, and no stranger has tried. **C2 partly observed**: on `cf9b6abf` the grant rendered as GitHub, because the catalogue projected 0 Linear tools. #2068 (`207c68d5`) fixed it, and on the deployed `e370fe9b` (its descendant) the row reads Linear, with its mark and the read eye, at 1200. 390, a failure reaching Activity, and revoke reachability are unwalked. **C3 green**: Scout listed the team and three issues through `grant_c6af31b8` in the C4 run pod; the trail shows the calls ok, with Sam as credential owner. **C4 green** (2026-10-09): c4-smoke, a public local claude seat confined by Seatbelt, called `linear.list_teams` through its broker on `grant_e3a1507b` at 06:12:57Z, and the trail shows it ok. Its daemon started at 06:11:59Z, after global cli 0.1.84 (npm latest) was installed at 06:11:41Z, so it runs the current build. An earlier run on 2026-09-27 didn't count, because that daemon predated the cli it should have been running. The same spawn showed that TASK-228's headers helper works under the public Seatbelt profile. A by-name scan of the spawn's claude process found `COMMONLY_TOKEN_FILE` set and `COMMONLY_AGENT_TOKEN` absent. The rest of #2067's consumer proof is unmeasured: the child processes' environments, and a credential rotation followed by a 401 retry succeeding. **C8 open**: in a pod with no grant, Scout is offered no `linear.*` tools. That's confinement by projection. An attempted out-of-pod call, refused and recorded in the trail, hasn't been made. **C9 open**: Linear's revoke is page-only, and every linear.app settings URL redirects to the workspace's unfinished welcome flow, so neither the revoke page (the entry's `https://linear.app/settings/security` lands on welcome) nor a revoke taking effect at Linear has been measured. C5, C6, C7 and C10 are unwalked | +| Linear (hosted MCP) | yes | **Fresh-account walk 2026-10-15 on `cfe91c21`, green** (connector-ops 77850; the build is Deploy Dev's last run before the walk): a new commonly.me account and a new Linear workspace, neither connected before. **C1 green**: connected through the Connectors page. Linear's consent read "Read, Identity", the authorize request asked for `read openid`, and the token response granted `read openid` (`config.grantedScope`). **C3 green in an eligible pod**: a read grant of the 36 pinned tools, made through the grant panel, which offers only Read, in the account's own My Workspace. That pod is private and invite-only, with no bridge and Scout as its only agent, so it passes §12's pod rule in `hosted-mcp-connection-scope.md` (#2211), which deployed after the walk. Scout listed the workspace's one team through the grant, the trail records the call ok, and Scout quoted #2206's untrusted-content marker ahead of the data. The earlier C3 and C4 walks ran in the C4 run pod, which does not pass that rule, so #2211 refuses its grants. Partly walked live 2026-10-01 (Sam's account, Chrome). Before the fresh-account walk, C1 was open: Sam's own operator-led connect succeeded on `cf9b6abf`, through CIMD with scope `read openid`, and the row records its client and the Linear subject. That didn't prove a stranger connects unaided, and no stranger had tried. **C2 partly observed**: on `cf9b6abf` the grant rendered as GitHub, because the catalogue projected 0 Linear tools. #2068 (`207c68d5`) fixed it, and on the deployed `e370fe9b` (its descendant) the row reads Linear, with its mark and the read eye, at 1200. 390, a failure reaching Activity, and revoke reachability are unwalked. Earlier, C3 was green in the C4 run pod: Scout listed the team and three issues through `grant_c6af31b8`; the trail showed the calls ok, with Sam as credential owner. **C4 refused by design** from #2211 (the C4 column). Before that it was green (2026-10-09): c4-smoke, a public local claude seat confined by Seatbelt, called `linear.list_teams` through its broker on `grant_e3a1507b` at 06:12:57Z, and the trail shows it ok. Its daemon started at 06:11:59Z, after global cli 0.1.84 (npm latest) was installed at 06:11:41Z, so it runs the current build. An earlier run on 2026-09-27 didn't count, because that daemon predated the cli it should have been running. The same spawn showed that TASK-228's headers helper works under the public Seatbelt profile. A by-name scan of the spawn's claude process found `COMMONLY_TOKEN_FILE` set and `COMMONLY_AGENT_TOKEN` absent. The rest of #2067's consumer proof is unmeasured: the child processes' environments, and a credential rotation followed by a 401 retry succeeding. **C8 open**: in a pod with no grant, Scout is offered no `linear.*` tools. That's confinement by projection. An attempted out-of-pod call, refused and recorded in the trail, hasn't been made. **C9 open**: Linear's revoke is page-only, and every linear.app settings URL redirects to the workspace's unfinished welcome flow, so neither the revoke page (the entry's `https://linear.app/settings/security` lands on welcome) nor a revoke taking effect at Linear has been measured. C5, C6, C7 and C10 are unwalked | | Google Calendar (hosted MCP) | yes | **Built, live, not offered.** #2066 (`e370fe9b`) pins the 5 read tools from Google's unauthenticated `tools/list`, with a pre-registered client and `openid` for the account subject. "Not offered" is a **config fact, not a code fact**. The client values arrived on 2026-10-09 (#2111), and the tile was offered until 2026-10-14. OAuth worked, but every tool call failed Google's Developer Preview gate, which is **per Cloud project** (Google named the project in its error on the 2026-10-09 gate test). Since 2026-10-14 `hostedMcp.googleCalendar.enabled` is `false` in values-dev (Wren's ruling), so the instance holds no client values, the catalogue reports `available: false`, and the page shows "not enabled on this instance" with no Connect. Flip the flag once the project is enrolled and an enrolled `list_calendars` call succeeds. Waiting on Sam: a Workspace seat on the domain and Developer Preview enrollment for the connectors project. The OAuth client already exists in a project of its own (Wren: a revoke withdraws every scope a person granted the project) | | next app | no | Sam's decision; see below | @@ -137,8 +137,8 @@ The test gate on "Scout (Default)" was switched back off after the walk. On Sam' | red | state | |---|---| | The IP rate-limit tier trusts `cf-connecting-ip` from any peer, so every callback and webhook row's failure column runs on a bucket the caller picks | filed as TASK-110 | -| A seat started with `commonly agent run` that declares no sandbox can receive the grant broker while unconfined (31 claude and codex seats; 33 of 36 token files declare no sandbox, two of which are pi and refused server-side anyway) | latent: the only grant today belongs to a seat that declares a workspace sandbox. TASK-111: the server hands out the broker only through the daemon's assignment route, so the gap is a hand-authored record run with `commonly agent run`; the fix is the same withholding guard at that spawn site, cli-side (Wren, with Vera concurring) | -| Withholding the broker from a seat is a convenience, not a boundary: `POST /api/mcp/grants/:grantId` accepts any agent token in the grant's audience. If confinement is meant to gate use of a granter's authority, that is a wider row than TASK-111 | open; Wren to scope (Vera) | +| A seat started with `commonly agent run` that declares no sandbox can receive the grant broker while unconfined (31 claude and codex seats; 33 of 36 token files declare no sandbox, two of which are pi and refused server-side anyway) | latent: the only grant today belongs to a seat that declares a workspace sandbox. TASK-111: the server hands out the broker only through the daemon's assignment route, so the gap is a hand-authored record run with `commonly agent run`; the fix is the same withholding guard at that spawn site, cli-side (Wren, with Vera concurring). Moot until TASK-213: from #2211 such a seat can use no grant (§12 of `hosted-mcp-connection-scope.md`) | +| Withholding the broker from a seat is a convenience, not a boundary: `POST /api/mcp/grants/:grantId` accepts any agent token in the grant's audience. If confinement is meant to gate use of a granter's authority, that is a wider row than TASK-111 | closed until TASK-213: from #2211 no daemon seat can use any grant (§12 of `hosted-mcp-connection-scope.md`). Since #1971 (TASK-175) the broker list and call also refuse a seat whose declaration cannot confine it (toolBrokerService.ts:786, :1132) | | A Mac seat that declares `bwrap` keeps it and is refused, since bwrap is Linux-only | unverified fix path | | No agent seat can do a logged-in walk on commonly.me (blocked since 09-16 per ux-lead) | Until that lifts, stranger-session walks run from the operator session with a throwaway account | From 1a49e5bf8eff5b22939429dde0bd9218f12ad2a7 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Thu, 15 Oct 2026 09:29:13 -0700 Subject: [PATCH 2/3] docs(plans): the Linear row and step 7 carry the walk's measured evidence The matrix's Linear row records C2 green at 1440, the same C3 call ok again after #2211 deployed, both C4 run pod grants refused by the deployed rule, and C9 narrowed to whether a revoke takes effect at Linear, with the revoke page the fresh-account walk reached. Step 7 of the plan records the same page under the entry's `page` bullet; #2230 pins it. 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 | 4 ++-- docs/plans/integration-readiness-matrix.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/plans/hosted-mcp-connection-scope.md b/docs/plans/hosted-mcp-connection-scope.md index 02780ce420..9364968333 100644 --- a/docs/plans/hosted-mcp-connection-scope.md +++ b/docs/plans/hosted-mcp-connection-scope.md @@ -444,8 +444,8 @@ Each step can be tested without the vendor, except the live measurements in step - `the Linear pin is the measured list minus caller-chosen fetch paths`: retain all 38 raw tools in the capture, pin the other 36, and keep the exact-lookup URL keys while excluding `extract_images` and `get_attachment` (§3). - `the page's call to start is credentialed`, before the walk (§4) - Measured live: that a removal's revoke took at Linear, shown by something other than its 200 (§3). Linear advertises one URL as both its `token_endpoint` and its `revocation_endpoint` (fetched 2026-09-29), so only a live removal shows which request that URL treats as a revoke. - - The entry's `page`: Linear's Security & access settings, whose "Authorized applications" list has "Revoke access" on each app (Linear docs, *Security & Access*, fetched 2026-09-30). Its URL is pinned from the signed-in walk. - - Walked 2026-10-15, before #2211 deployed, by a fresh account with a fresh Linear workspace (connector-ops 77850, and the readiness matrix's Linear row). Connect granted `read openid`. A read grant of the 36 pins, in a pod that passes §12's rule, carried a native agent's call, ok in the trail, with #2206's marker ahead of the data. The walk had no pi seat: under §12 a pod with one in it fails the rule, so a grant there is refused for every member. Still unmeasured: a removal's revoke taking effect at Linear. + - The entry's `page`: Linear's Security & access settings, whose "Authorized applications" list has "Revoke access" on each app (Linear docs, *Security & Access*, fetched 2026-09-30). The signed-in walk of 2026-10-15 measured its URL as `https://linear.app/settings/account/security`, where the list shows the connection as "MCP · Read · Revoke" (connector-ops 77962), and #2230 pins it. Before #2230 the entry named `https://linear.app/settings/security`, which is Linear's workspace admin page. + - Walked 2026-10-15 by a fresh account with a fresh Linear workspace (connector-ops 77850 and 77962, and the readiness matrix's Linear row). Connect granted `read openid`. A read grant of the 36 pins, in a pod that passes §12's rule, carried a native agent's call, ok in the trail, with #2206's marker ahead of the data, and the same call was ok again after #2211 deployed. The walk had no pi seat: under §12 a pod with one in it fails the rule, so a grant there is refused for every member. Still unmeasured: a removal's revoke taking effect at Linear. 8. **The entries after Linear**, in the order connector-ops proposed (76040) and 76044 kept, with the batch of 2026-10-09 after Airtable (connector-ops 76914, Wren 76915): Atlassian, Airtable, Canva, Miro, ClickUp, Todoist, Calendly, then Google through a pre-registered client (§4). Each is its own entry PR, admitted as §11 says and walked as step 7 is. Google Calendar is built in parallel without jumping that order, since Atlassian and Airtable wait only on Sam's accounts (connector-ops 76137). - **Held** (Sam, 2026-10-14, relayed by connector-ops in 77368): no new catalogue app is built until an outside user connects one, and "payment apps and new OAuth apps are out". Linear, Google Calendar, Atlassian and Airtable are built (`integrations/hostedMcp/entries.ts:20`). Every other entry this step names is held, and so is the order above. Its batch after Airtable rests on the 2026-10-09 measurements, and the 2026-10-13 sweep re-measured the batch's first three: Canva is conditional and parked, and Miro and ClickUp are conditional ([hosted-mcp-catalogue-sweep-2026-10-13.md](hosted-mcp-catalogue-sweep-2026-10-13.md)). So when the hold lifts, the next entry is ruled afresh, not taken from this list. The rest of this step stays as the design each entry follows when it is built. - First, while Linear is the only entry: the client record (§2), then its backfill once that build's rollout has finished. diff --git a/docs/plans/integration-readiness-matrix.md b/docs/plans/integration-readiness-matrix.md index 2975e7504f..e816dd8f20 100644 --- a/docs/plans/integration-readiness-matrix.md +++ b/docs/plans/integration-readiness-matrix.md @@ -54,7 +54,7 @@ The matrix below is the definition of "ready". A row is ready when every cell is | X | yes (admin OAuth callback + feed) | unverified | | GitHub (app) | yes | Walked 2026-09-25 and re-walked 2026-09-26 on `5561a1dd` (both below). Live on our own repository since 09-18. **red** in C1 until per-person GitHub: a team cannot connect its own repository. C2 at 390 is green since #1874, shown on `5561a1dd`. C3 (a hosted agent gets the broker, #1880) shipped and has not been walked. C8 was green on `9a32fca5`. A new grant from the connection's owner, `grant_3c4d6ad5` (2026-09-29, on Sam's approval), has the same scope as the lapsed 09-18 one: pod "C4 run 2026-09-18"; `list_issues`, `create_issue` and `close_issue`; write-with-confirm; audience Sam, `c3stranger0912` and `c4-smoke`. It runs to 2026-10-06, so C3, C4 and C8 are walkable again. None has been walked on the current build | | Hosted MCP (generic type, TASK-172) | yes | **Built and live, with one follow-up open.** Steps 1–6 of `hosted-mcp-connection-scope.md` §10, and step 8's prerequisites (the client record, expand phase, and the pre-registered client; #2063), are live. **Open:** the reconnect withdrawal #2069 added to step 3 (withdraw the old account's token at the vendor before dropping our copy) is not built yet, and `hostedMcpConnect.ts:406–420` does not do it. The bar every entry must meet (Wren 76043): a credential that cannot write. Notion, Sentry and Stripe wait for a vendor read scope. Entries are offered one by one; the rows below walk them | -| Linear (hosted MCP) | yes | **Fresh-account walk 2026-10-15 on `cfe91c21`, green** (connector-ops 77850; the build is Deploy Dev's last run before the walk): a new commonly.me account and a new Linear workspace, neither connected before. **C1 green**: connected through the Connectors page. Linear's consent read "Read, Identity", the authorize request asked for `read openid`, and the token response granted `read openid` (`config.grantedScope`). **C3 green in an eligible pod**: a read grant of the 36 pinned tools, made through the grant panel, which offers only Read, in the account's own My Workspace. That pod is private and invite-only, with no bridge and Scout as its only agent, so it passes §12's pod rule in `hosted-mcp-connection-scope.md` (#2211), which deployed after the walk. Scout listed the workspace's one team through the grant, the trail records the call ok, and Scout quoted #2206's untrusted-content marker ahead of the data. The earlier C3 and C4 walks ran in the C4 run pod, which does not pass that rule, so #2211 refuses its grants. Partly walked live 2026-10-01 (Sam's account, Chrome). Before the fresh-account walk, C1 was open: Sam's own operator-led connect succeeded on `cf9b6abf`, through CIMD with scope `read openid`, and the row records its client and the Linear subject. That didn't prove a stranger connects unaided, and no stranger had tried. **C2 partly observed**: on `cf9b6abf` the grant rendered as GitHub, because the catalogue projected 0 Linear tools. #2068 (`207c68d5`) fixed it, and on the deployed `e370fe9b` (its descendant) the row reads Linear, with its mark and the read eye, at 1200. 390, a failure reaching Activity, and revoke reachability are unwalked. Earlier, C3 was green in the C4 run pod: Scout listed the team and three issues through `grant_c6af31b8`; the trail showed the calls ok, with Sam as credential owner. **C4 refused by design** from #2211 (the C4 column). Before that it was green (2026-10-09): c4-smoke, a public local claude seat confined by Seatbelt, called `linear.list_teams` through its broker on `grant_e3a1507b` at 06:12:57Z, and the trail shows it ok. Its daemon started at 06:11:59Z, after global cli 0.1.84 (npm latest) was installed at 06:11:41Z, so it runs the current build. An earlier run on 2026-09-27 didn't count, because that daemon predated the cli it should have been running. The same spawn showed that TASK-228's headers helper works under the public Seatbelt profile. A by-name scan of the spawn's claude process found `COMMONLY_TOKEN_FILE` set and `COMMONLY_AGENT_TOKEN` absent. The rest of #2067's consumer proof is unmeasured: the child processes' environments, and a credential rotation followed by a 401 retry succeeding. **C8 open**: in a pod with no grant, Scout is offered no `linear.*` tools. That's confinement by projection. An attempted out-of-pod call, refused and recorded in the trail, hasn't been made. **C9 open**: Linear's revoke is page-only, and every linear.app settings URL redirects to the workspace's unfinished welcome flow, so neither the revoke page (the entry's `https://linear.app/settings/security` lands on welcome) nor a revoke taking effect at Linear has been measured. C5, C6, C7 and C10 are unwalked | +| Linear (hosted MCP) | yes | **Fresh-account walk 2026-10-15 on `cfe91c21`, green** (connector-ops 77850 and 77962; the build is Deploy Dev's last run before the walk): a new commonly.me account and a new Linear workspace, neither connected before. **C1 green**: connected through the Connectors page. Linear's consent read "Read, Identity", the authorize request asked for `read openid`, and the token response granted `read openid` (`config.grantedScope`). **C3 green in an eligible pod**: a read grant of the 36 pinned tools, made through the grant panel, which offers only Read, in the account's own My Workspace. That pod is private and invite-only, with no bridge and Scout as its only agent, so it passes §12's pod rule in `hosted-mcp-connection-scope.md` (#2211). Scout listed the workspace's one team through the grant, the trail records the call ok, and Scout quoted #2206's untrusted-content marker ahead of the data. #2211 deployed after the walk, and the same call was ok again after it, at 16:22:37Z. The earlier C3 and C4 walks ran in the C4 run pod, which fails that rule: since `ff909fb3` the deployed rule returns `pod_joinable` for both of its grants. Partly walked live 2026-10-01 (Sam's account, Chrome). Before the fresh-account walk, C1 was open: Sam's own operator-led connect succeeded on `cf9b6abf`, through CIMD with scope `read openid`, and the row records its client and the Linear subject. That didn't prove a stranger connects unaided, and no stranger had tried. **C2 green at 1440, the rest unwalked**: in the fresh-account walk the grant panel offered only Read. Earlier, on `cf9b6abf` the grant rendered as GitHub, because the catalogue projected 0 Linear tools. #2068 (`207c68d5`) fixed it, and on the deployed `e370fe9b` (its descendant) the row reads Linear, with its mark and the read eye, at 1200. 390, a failure reaching Activity, and revoke reachability are unwalked. Earlier, C3 was green in the C4 run pod: Scout listed the team and three issues through `grant_c6af31b8`; the trail showed the calls ok, with Sam as credential owner. **C4 refused by design** from #2211 (the C4 column), and c4-smoke is now offered no Linear tool. Before that it was green (2026-10-09): c4-smoke, a public local claude seat confined by Seatbelt, called `linear.list_teams` through its broker on `grant_e3a1507b` at 06:12:57Z, and the trail shows it ok. Its daemon started at 06:11:59Z, after global cli 0.1.84 (npm latest) was installed at 06:11:41Z, so it runs the current build. An earlier run on 2026-09-27 didn't count, because that daemon predated the cli it should have been running. The same spawn showed that TASK-228's headers helper works under the public Seatbelt profile. A by-name scan of the spawn's claude process found `COMMONLY_TOKEN_FILE` set and `COMMONLY_AGENT_TOKEN` absent. The rest of #2067's consumer proof is unmeasured: the child processes' environments, and a credential rotation followed by a 401 retry succeeding. **C8 open**: in a pod with no grant, Scout is offered no `linear.*` tools. That's confinement by projection. An attempted out-of-pod call, refused and recorded in the trail, hasn't been made. **C9 open**, now only for whether a revoke takes effect at Linear. Linear's revoke is page-only. The fresh-account walk reached the page: `https://linear.app/settings/account/security`, whose Authorized applications list shows the connection as "MCP · Read · Revoke". #2230 points the entry's page there. Before #2230 the entry named `https://linear.app/settings/security`, which is Linear's workspace admin page. Before that walk, every linear.app settings URL redirected to the old workspace's unfinished welcome flow, so the page had not been reached. C5, C6, C7 and C10 are unwalked | | Google Calendar (hosted MCP) | yes | **Built, live, not offered.** #2066 (`e370fe9b`) pins the 5 read tools from Google's unauthenticated `tools/list`, with a pre-registered client and `openid` for the account subject. "Not offered" is a **config fact, not a code fact**. The client values arrived on 2026-10-09 (#2111), and the tile was offered until 2026-10-14. OAuth worked, but every tool call failed Google's Developer Preview gate, which is **per Cloud project** (Google named the project in its error on the 2026-10-09 gate test). Since 2026-10-14 `hostedMcp.googleCalendar.enabled` is `false` in values-dev (Wren's ruling), so the instance holds no client values, the catalogue reports `available: false`, and the page shows "not enabled on this instance" with no Connect. Flip the flag once the project is enrolled and an enrolled `list_calendars` call succeeds. Waiting on Sam: a Workspace seat on the domain and Developer Preview enrollment for the connectors project. The OAuth client already exists in a project of its own (Wren: a revoke withdraws every scope a person granted the project) | | next app | no | Sam's decision; see below | From b7f6a921ed5e3cad9a8430f7e17398c9c1393491 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Thu, 15 Oct 2026 09:32:23 -0700 Subject: [PATCH 3/3] =?UTF-8?q?docs(plans):=20=C2=A712=20scopes=20the=20na?= =?UTF-8?q?rrowing=20to=20built-in=20tools,=20names=20the=20token=20channe?= =?UTF-8?q?l,=20and=20lists=20the=20outside=20offer's=20other=20pieces=20(?= =?UTF-8?q?TASK-224)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rhea's HOLD at 62af8784. The narrowing covers the built-in Commonly tools only: a grant's tools are offered beside them and go to the broker before the native dispatcher, under the grant's own checks. And the bound holds only while no in-process agent holds a runtime token, a second channel to the same identity that the narrowing never sees; #2224 makes the shared issuer refuse one. #2224 merged, so condition 4 counts a hosted agent on the native engine, and every cite in the amendment moves to main d37a8600. The eng lead's fidelity comment asked for the four pieces the outside offer also waits on, each with its state. 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 | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/plans/hosted-mcp-connection-scope.md b/docs/plans/hosted-mcp-connection-scope.md index 9364968333..c2857a717c 100644 --- a/docs/plans/hosted-mcp-connection-scope.md +++ b/docs/plans/hosted-mcp-connection-scope.md @@ -18,7 +18,7 @@ **Amended 2026-10-14 (the catalogue hold):** Sam ruled, relayed by connector-ops (77368), that no new catalogue app is built until an outside user connects one, and that "payment apps and new OAuth apps are out". Linear, Google Calendar, Atlassian and Airtable are built, and every later entry in step 8 is held, along with the order the 2026-10-09 note set (§10, step 8). That note's batch rested on measurements the 2026-10-13 sweep re-took for its first three: Canva is now conditional and parked, and Miro and ClickUp are conditional ([hosted-mcp-catalogue-sweep-2026-10-13.md](hosted-mcp-catalogue-sweep-2026-10-13.md), #2133). So when the hold lifts, the next entry is ruled afresh. This supersedes the 2026-10-09 note's sentence that Canva, Miro and ClickUp follow Airtable. Two passages are also conformed to code that has landed since they were written: TASK-199 carries a failed list's reason into the trail (§3), and the server's adapter rule is now an allowlist, as the daemon's is (§6). The code this amendment cites is read at `main` `e4e10571`. -**Amended 2026-10-15 (TASK-224, the pod rule):** the CTO ruled on TASK-224 that a grant from a source outsiders can write is attached, and its tools called, only in an eligible pod (connector-ops 77728), and #2211 built the rule (§12). The pod is private, invite-only and unbridged, every agent in it runs on the native runtime, and the grant is a pod grant, not a seat grant. While a live grant reaches the pod, TASK-224 narrows each native agent in it to reading the pod's context and posting there. The rule reads no source declaration, and every source is outsider-writable today, so it governs every grant. This supersedes the #2168 part 2 note's finding that every such grant needs an override: a grant in an eligible pod needs neither an override nor parking. Of the three options put to the CTO (Kai, 77725), the ruling took this one, kept the second, holding the offer until the kernel parks outward calls, only as its fallback if the call-time check proves too expensive, and did not accept the third, an owner override that accepts the residual. Until TASK-213, no seat the daemon runs can use a grant, so the readiness matrix's C4 is refused by design. The code this amendment cites is read at `main` `17adb067`. +**Amended 2026-10-15 (TASK-224, the pod rule):** the CTO ruled on TASK-224 that a grant from a source outsiders can write is attached, and its tools called, only in an eligible pod (connector-ops 77728), and #2211 built the rule (§12). The pod is private, invite-only and unbridged, every agent in it runs inside Commonly's backend, on the native runtime or hosted on the native engine, and the grant is a pod grant, not a seat grant. While a live grant reaches the pod, TASK-224 narrows the built-in Commonly tools of each such agent to reading the pod's context and posting there, and leaves the grant's own tools under the broker's checks. That narrowing bounds what the backend does for an agent's turn, so the rule also needs each such agent to hold no runtime token, which would let something outside the backend act as it. #2224 makes the token issuer refuse every such agent. The rule reads no source declaration, and every source is outsider-writable today, so it governs every grant. This supersedes the #2168 part 2 note's finding that every such grant needs an override: a grant in an eligible pod needs neither an override nor parking. Of the three options put to the CTO (Kai, 77725), the ruling took this one, kept the second, holding the offer until the kernel parks outward calls, only as its fallback if the call-time check proves too expensive, and did not accept the third, an owner override that accepts the residual. Until TASK-213, no seat the daemon runs can use a grant, so the readiness matrix's C4 is refused by design. The rule is necessary for offering a grant to outside teams but not sufficient, and §12 lists the four pieces the offer also waits on, with their state. The code this amendment cites is read at `main` `d37a8600`. **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. @@ -512,18 +512,25 @@ The TASK-172 ruling's part 3 wording on `:180` is replaced by §6. ## 12. Outsider-writable sources and outward effects (#2168 part 2) -**2026-10-15 amendment (TASK-224).** The CTO ruled that a grant from an outsider-writable source is attached, and its tools called, only in an eligible pod (connector-ops 77728). #2211 built the rule (`outsiderGrantPodRefusal`, outsiderGrantPodEligibility.ts:26). It reads no declaration from (b) and applies to every grant, and every source is outsider-writable today. A grant may be minted, attenuated, listed or called only while all of these hold: +**2026-10-15 amendment (TASK-224).** The CTO ruled that a grant from an outsider-writable source is attached, and its tools called, only in an eligible pod (connector-ops 77728). #2211 built the rule (`outsiderGrantPodRefusal`, outsiderGrantPodEligibility.ts:28). It reads no declaration from (b) and applies to every grant, and every source is outsider-writable today. A grant may be minted, attenuated, listed or called only while all of these hold: 1. It is a pod grant. A seat grant is refused until TASK-213. 2. Its write mode is not `write`, which would let a GitHub App tool write with no person (item 5). `write-with-confirm` remains. 3. Its pod is private and closed: `publicRead` is `false` and `joinPolicy` is `'invite-only'`, so a pod missing either field is refused. No live integration other than a hosted-MCP or GitHub App connection is attached to the pod or has a gate enabled for it. One that is revoked, disconnected, in error, inactive or paused by an admin does not count. -4. Every agent in the pod runs on the native runtime. Each agent member has exactly one active installation in the pod, its `config.runtime.runtimeType` is `native`, and every active installation belongs to an agent member. Humans are fine. +4. Every agent in the pod runs inside Commonly's backend. Each agent member has exactly one active installation in the pod, and every active installation belongs to an agent member and runs in-process: its `config.runtime.runtimeType` is `native`, or it is a hosted install whose `config.hosted.engine` is `native` (#2224). A hosted agent on the Durable Object polls from outside the backend and is refused (outsiderGrantPodEligibility.ts:73–75). Humans are fine. -Mint and attenuation refuse a failing grant with `outsider_grant_` and a message that points to a separate pod (grants.ts:389, :434). The broker checks again, with no cache, on every list, direct call and approved call (toolBrokerService.ts:787, :1133, :1332). So a non-native agent added, the pod made public or joinable, or a bridge enabled refuses the next call. Grants minted before #2211 are not exempt. +Mint and attenuation refuse a failing grant with `outsider_grant_` and a message that points to a separate pod (grants.ts:389, :434). The broker checks again, with no cache, on every list, direct call and approved call (toolBrokerService.ts:787, :1133, :1332). So an agent added that does not run in-process, the pod made public or joinable, or a bridge enabled refuses the next call. Grants minted before #2211 are not exempt. -While a live grant reaches a pod, every native agent in it is narrowed (TASK-224, nativeOutsiderGrantPolicy.ts:22). Its turn is offered only `commonly_read_context` and `commonly_post_message` (nativeRuntimeService.ts:267), dispatch refuses any other name with `outsider_writable_grant_tool_denied` (:304), and the post lands only in the run's pod (:353). The check is re-read before each model call and each dispatch, and a failed read narrows (:983, :1090, :1217). +While a live grant reaches a pod, every agent in it that runs in-process is narrowed (TASK-224, nativeOutsiderGrantPolicy.ts:22), and the narrowing covers its built-in Commonly tools. Of those, its turn is offered only `commonly_read_context` and `commonly_post_message` (nativeRuntimeService.ts:268), the native dispatcher refuses any other name with `outsider_writable_grant_tool_denied` (:305), and the post lands only in the run's pod (:363). The check is re-read before each model call and each native dispatch, and a failed read narrows (:998, :1105, :1232). A grant's own tools are not narrowed. They are offered beside the built-in two (:1120), and a call to one goes to the broker (:1202) before the native dispatcher (:1233), so it answers to the grant's checks above, not to this list. -The rule does not compute (a)'s list. It bounds it at the pod instead. Condition 3 rules out item 4 and condition 2 rules out item 5. Condition 4 leaves only native agents, which hold no seat tools (item 6) and, while narrowed, can read the pod's context and post in it and nothing else (items 1–3). So (a) and (b) stand as the analysis of outward effects and sources, and the default's finding, the parking paragraph, (c) and (d) are superseded where marked below. Until TASK-213 no seat the daemon runs can use a grant: a seat grant fails condition 1, and a pod grant fails condition 4 while a daemon seat is in the pod. What TASK-213 re-opens is for a later ruling. Part 1's marker covers hosted-MCP results only (toolBrokerService.ts:1271), so a GitHub App result carries none. The CTO's conditions for offering GitHub outside name a hosted entry for that reason (TASK-225), and a GitHub App connection is made only through the administrator route (integrations.ts:390). The code this amendment cites is read at `main` `17adb067`. +The rule does not compute (a)'s list. It bounds it at the pod instead. Condition 3 rules out item 4 and condition 2 rules out item 5, for every grant in the pod. Condition 4 leaves only agents that run in-process, which hold no seat tools (item 6). While narrowed, their built-in tools can read the pod's context and post in it and nothing else (items 1–3), and their grant tools cannot write without a person's Approve, since condition 2 refuses `write` (item 5). That bound is on what the backend dispatches for an agent's turn. A runtime token is a second channel to the same identity: whoever holds one calls the runtime API as the agent from outside the backend, where the narrowing does not apply (registry/tokens.ts:194–200). So the bound holds only while no in-process agent holds a runtime token. #2224 makes the shared token issuer refuse to issue one to any of them, with 409 `in_process_agent_no_token` (:203, :242), and the legacy issuer refuses the same way (:328). So (a) and (b) stand as the analysis of outward effects and sources, and the default's finding, the parking paragraph, (c) and (d) are superseded where marked below. Until TASK-213 no seat the daemon runs can use a grant: a seat grant fails condition 1, and a pod grant fails condition 4 while a daemon seat is in the pod. What TASK-213 re-opens is for a later ruling. Part 1's marker covers hosted-MCP results only (toolBrokerService.ts:1271), so a GitHub App result carries none. The CTO's conditions for offering GitHub outside name a hosted entry for that reason (TASK-225), and a GitHub App connection is made only through the administrator route (integrations.ts:390). The code this amendment cites is read at `main` `d37a8600`. + +**Also required for the outside offer.** The rule is necessary for offering a grant to outside teams but not sufficient. The ruling also waits on four pieces, each in its state at this amendment: + +- TASK-226 (#2220): an agent whose runtime type is not `native` cannot join a pod while the pod holds a live outsider-writable grant (outsiderGrantAgentJoinPolicy.ts:24, :40). So a hosted agent on the native engine, whose type is `hosted`, is refused at the join, though condition 4 accepts one already in the pod. TASK-231 aligns the two. Merged as `cb7cb51f` and deployed with `d37a8600`. +- TASK-227 (#2222): v2 chat shows images only from the instance's own uploads. Merged as `61ac1bd2` and deployed with `ff909fb3`. +- TASK-228 (#2225): an owner can make a pod invite-only, so a team can build an eligible pod. Open. +- #2224: the shared token issuer refuses every in-process agent (above). Merged as `4b21e895` and deployed with `d37a8600`. 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`.