From 1440cb9571b7e03ad7066dad7f26f8c45d1e0763 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 18 Oct 2026 12:22:23 -0700 Subject: [PATCH 1/3] docs(plans): a subject GitHub answers as a number counts (TASK-172) GitHub's AS offers no openid, so a GitHub entry's subject can only come from its server, and get_me answers the account's id as a JSON number. Section 4 counted only a string, so GitHub's one durable field could not count, while login, a handle GitHub frees for reuse, could. A positive safe integer now counts, recorded as its decimal string; any other number refuses subject_unavailable. Adds the step 8 test arm, and re-reads section 4's callback cites at main b57966d2. 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, 8 insertions(+), 3 deletions(-) diff --git a/docs/plans/hosted-mcp-connection-scope.md b/docs/plans/hosted-mcp-connection-scope.md index c2857a717c..4f21f25807 100644 --- a/docs/plans/hosted-mcp-connection-scope.md +++ b/docs/plans/hosted-mcp-connection-scope.md @@ -20,6 +20,8 @@ **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`. +**Amended 2026-10-18 (an integer subject):** GitHub's AS offers no `openid`, so the GitHub entry TASK-225 builds can take its subject only from its own server (§4). GitHub's `get_me` answers the account's `id` as a JSON number, and §4 counted only a string. So the one field GitHub calls durable could not count, and `login`, which could, is a name GitHub frees for anyone to claim once its owner changes it. A positive integer no larger than `Number.MAX_SAFE_INTEGER` now counts as well, recorded as its decimal string, and any other number refuses the connect `subject_unavailable` (§4, §10 step 8). Until the subject read is built, a GitHub entry names no subject. §4's cites of the callback are re-read as well. The upstream source this amendment cites is read at `github/github-mcp-server` `796afb74`, GitHub's metadata and docs were fetched 2026-10-18, and the code it cites is read at `main` `b57966d2`. + **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). @@ -170,14 +172,16 @@ Nothing is registered and no client secret is stored anywhere. A self-hosted ins **Every entry requests the identity scope its AS offers** (the TASK-172 row): `openid` where the AS has it, and never `email` or `profile`, as for Linear above. Without a subject, every reconnect is one §2 cannot compare. It revokes every grant on the row even when the account is the same, and it withdraws nothing at the vendor. Google returns an ID token "only … if your request included an identity scope, such as openid, profile, or email" (developers.google.com/identity/protocols/oauth2/native-app), so Calendar requests `openid` beside its three scopes (§10, step 8). -**An entry whose AS offers no `openid` may read the subject from its own server** (connector-ops 77008 (g), Wren 77045). Of the six vendors below that register by DCR, only Miro's AS lists `openid` (fetched 2026-10-14), and without a subject every reconnect is one §2 cannot compare. Such an entry declares `subject: { tool, field }` (§3). `tool` is one of the entry's own `read` pins, so it carries `readOnlyHint: true`, and its pinned `inputSchema` requires no argument; it is called with `{}` at the entry's `resource`. `field` is a dotted path into the result's `structuredContent`, or, when there is none, into the JSON its single text item holds. Only a non-empty string counts. The entry PR cites the vendor's doc that the field is the account's stable id, never an email or a display name, since either can change or pass to another account. The entry load refuses a `subject` whose `tool` is not one of the entry's `read` pins or whose schema requires an argument, and an entry that names a `subject` and requests `openid`, since a row's subject has one source. +**An entry whose AS offers no `openid` may read the subject from its own server** (connector-ops 77008 (g), Wren 77045). Of the six vendors below that register by DCR, only Miro's AS lists `openid` (fetched 2026-10-14), and without a subject every reconnect is one §2 cannot compare. Such an entry declares `subject: { tool, field }` (§3). `tool` is one of the entry's own `read` pins, so it carries `readOnlyHint: true`, and its pinned `inputSchema` requires no argument; it is called with `{}` at the entry's `resource`. `field` is a dotted path into the result's `structuredContent`, or, when there is none, into the JSON its single text item holds. A non-empty string counts, and so does a positive integer no larger than `Number.MAX_SAFE_INTEGER`, which is recorded as its decimal string. Nothing else counts: a larger integer may have lost digits when the result was parsed, so two accounts could read as one. The entry PR cites the vendor's doc that the field is the account's stable id, never an email or a display name, since either can change or pass to another account. The entry load refuses a `subject` whose `tool` is not one of the entry's `read` pins or whose schema requires an argument, and an entry that names a `subject` and requests `openid`, since a row's subject has one source. -The callback reads the subject after TASK-193's scope check and before it stores or revokes anything. With the new token it lists the server's tools, compares them with the pins as drift does (`assessEntryTools`, `services/hostedMcpEntryService.ts:202`), and calls the subject tool once. No grant is involved and no trail row is written, since no agent made the call. Any failure refuses the connect `subject_unavailable`: the tool missing or drifted, the call failing or answering `isError`, or no string at `field`. That holds on a first connect as on a reconnect, since a row connected without a subject makes its person's next reconnect one §2 cannot compare. The new token goes to `revokeRefusedGrant` (`routes/hostedMcpConnect.ts:100`), which revokes it when the entry has an endpoint and otherwise discards it, as TASK-193's refusal does. The row, its credentials and its grants stay as they were. +The callback reads the subject after TASK-193's scope check and before it stores or revokes anything. With the new token it lists the server's tools, compares them with the pins as drift does (`assessEntryTools`, `services/hostedMcpEntryService.ts:277`), and calls the subject tool once. No grant is involved and no trail row is written, since no agent made the call. Any failure refuses the connect `subject_unavailable`: the tool missing or drifted, the call failing or answering `isError`, or nothing at `field` that counts. That holds on a first connect as on a reconnect, since a row connected without a subject makes its person's next reconnect one §2 cannot compare. The new token goes to `revokeRefusedGrant` (`routes/hostedMcpConnect.ts:102`), which revokes it when the entry has an endpoint and otherwise discards it, as TASK-193's refusal does. The row, its credentials and its grants stay as they were. -**Subjects are compared only when they come from the same source.** The callback records `config.providerSubjectSource` with the subject: `id_token` for the ID token's `sub` (`idTokenSubject`, `routes/hostedMcpConnect.ts:130`), and `tool` for a subject read. A vendor's `sub` and its API's account id need not be the same string for one account, so §2's comparison (`subjectsComparable`, `:480`) treats subjects from different sources as ones it cannot compare. A subject recorded before the key existed reads as `id_token`, its only source until now. So an entry that moves from one source to the other makes each row's next reconnect one §2 cannot compare, once. The key is server-owned, like every key this note adds (§2). +**Subjects are compared only when they come from the same source.** The callback records `config.providerSubjectSource` with the subject: `id_token` for the ID token's `sub` (`idTokenSubject`, `routes/hostedMcpConnect.ts:132`), and `tool` for a subject read. A vendor's `sub` and its API's account id need not be the same string for one account, so §2's comparison (`subjectsComparable`, `:501`) treats subjects from different sources as ones it cannot compare. A subject recorded before the key existed reads as `id_token`, its only source until now. So an entry that moves from one source to the other makes each row's next reconnect one §2 cannot compare, once. The key is server-owned, like every key this note adds (§2). **GitHub is the case CIMD cannot serve.** `github.com/login/oauth` advertises neither DCR nor CIMD, so a GitHub entry uses a pre-registered client: the GitHub App's own, through the user authorization of §10.2 path 1. GitHub's server accepts "GitHub Apps that sign in (are authorized by) a user" (the same `docs/policies-and-governance.md` as §1). That is how per-person GitHub is the same §10 layer (74820). +**GitHub's subject comes from its server too** (TASK-225). Its AS offers no `openid`: the metadata at `https://github.com/.well-known/oauth-authorization-server/login/oauth` lists only `offline_access` in `scopes_supported`, and `https://github.com/.well-known/openid-configuration` answers 404 (both fetched 2026-10-18). Its server's `get_me` takes no argument and carries `readOnlyHint: true`, so it can be a GitHub entry's subject tool. It answers the account's `id` as a JSON number, since its result is a `MinimalUser`, whose `ID` is an `int64` (`github/github-mcp-server` at `796afb74`, `pkg/github/context_tools.go:25`, `:125`, `:155–181`, and `pkg/github/minimal_types.go:150–152`). GitHub's docs say to "always use the `id` of the user. This value will never change for the user or be used to point to a different user", and "*Never* use identifiers that can change over time, including user handles, organization slugs, or email addresses" (docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/best-practices-for-creating-a-github-app). A handle can also pass to another account: "After changing your username, your old username becomes available for anyone else to claim" (docs.github.com/en/account-and-profile/concepts/username-changes). Both pages were fetched 2026-10-18. So once the subject read is built (§10, step 8), a GitHub entry names `{ tool: 'get_me', field: 'id' }`, never `login`. Until then it names none, and each of its reconnects is one §2 cannot compare. + **A pre-registered client is instance config, never entry data** (76042). Its id and secret are held by the instance, through ESO on the hosted one, and neither appears in an entry or a repo file. The secret appears on no row either. A row keeps only a snapshot of the id that minted its pair (§2), and instance config stays the id's source: every exchange, refresh and revocation reads the id from there, never from a row. The id already arrives that way, as the entry's own `_CLIENT_ID` (`resolvedClientId`, `services/hostedMcpIntakeService.ts:145`), and the secret arrives the same way. An instance that lacks either does not offer the entry. The entry's readiness answers `not_configured`, as GitHub's does without its App's credentials (`services/installable/toolInstallables.ts:74–76`), so the catalogue projection lists it as unavailable and the page offers no Connect (Vera 76113). A `cimd` entry still has nothing to set, so its readiness stays constant. The instance also treats the entry as one the catalogue no longer has (§3): its rows are refused, and their removal finishes with the row's copied page (§9). The start route's `client_not_configured` stays as the backstop. It is a confidential client: it authenticates with its secret at the token and revocation endpoints, by a method the AS's metadata lists, and it still sends PKCE `S256`. The entry's callback is registered with the vendor as a redirect URI. Google is the first such vendor. Its setup page for its MCP servers says to create a "Web application" client and copy its "Client ID and Client Secret" (developers.google.com/workspace/guides/configure-mcp-servers, updated 2026-09-18). `accounts.google.com`'s metadata lists no registration endpoint and no CIMD support, and names `client_secret_post` and `client_secret_basic` as its token endpoint's auth methods (fetched 2026-10-01). **A `dcr` entry registers one public client per instance, per entry and issuer, at its first start** (connector-ops 77008 (f), Wren 77045, the TASK-197 row). Six vendors offer DCR and no CIMD: Close, Otter, Buildkite, Miro, Calendly and ClickUp (fetched 2026-10-14). The start registers at the AS's `registration_endpoint` with `token_endpoint_auth_method: none`, the `authorization_code` and `refresh_token` grants, the `code` response type, the entry's scopes, and the entry's callback on this instance as the only redirect URI. It sends PKCE `S256`, as every flow does. Before it sends anything, it refuses `client_registration_unsupported` when the metadata names no https `registration_endpoint`, or when its `token_endpoint_auth_methods_supported` lacks `none`. An omitted list lacks it, since RFC 8414 §2 defaults that list to `client_secret_basic`. It refuses the answer with the same code when the answer registers another method, carries a `client_secret`, or does not echo the one redirect URI. An omitted method is another method, since RFC 7591 §2 defaults it to `client_secret_basic`. An AS that refuses the request ends the start `client_registration_refused`. No `registration_access_token` is stored, and nothing is sent to RFC 7592's management endpoint. @@ -473,6 +477,7 @@ Each step can be tested without the vendor, except the live measurements in step - Before the first DCR entry ships, the page's notices name `client_registration_unsupported` and `client_registration_refused`, beside the client codes in `frontend/src/v2/components/hostedMcpErrorNotice.ts`. - With the first entry that names a subject read (§4). - `the callback records a subject read from the entry's declared tool, with its source` + - `a positive safe integer at the subject's field is recorded as its decimal string, and any other number refuses subject_unavailable` - `a failed subject read refuses subject_unavailable and leaves the row unchanged` - `subjects from different sources are not compared` - `a legacy subject with no source reads as id_token` From 3468b05c3f5bbdf55f61168730a79c58141e9d68 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 18 Oct 2026 12:25:03 -0700 Subject: [PATCH 2/3] docs(plans): the order when the catalogue hold lifts (TASK-172) The hold note says the next entry is ruled afresh when the hold lifts. connector-ops asked whether that order was still open, and it was ruled: the CIMD entries first, since they register nothing at the vendor, then the DCR entries, with ClickUp last and Canva parked. Basecamp and Honeycomb (US) are the first two captures, and since neither AS lists openid, each records the tool and field its subject read would name. The hold itself still stands. 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 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/plans/hosted-mcp-connection-scope.md b/docs/plans/hosted-mcp-connection-scope.md index 4f21f25807..e6bf435612 100644 --- a/docs/plans/hosted-mcp-connection-scope.md +++ b/docs/plans/hosted-mcp-connection-scope.md @@ -22,6 +22,8 @@ **Amended 2026-10-18 (an integer subject):** GitHub's AS offers no `openid`, so the GitHub entry TASK-225 builds can take its subject only from its own server (§4). GitHub's `get_me` answers the account's `id` as a JSON number, and §4 counted only a string. So the one field GitHub calls durable could not count, and `login`, which could, is a name GitHub frees for anyone to claim once its owner changes it. A positive integer no larger than `Number.MAX_SAFE_INTEGER` now counts as well, recorded as its decimal string, and any other number refuses the connect `subject_unavailable` (§4, §10 step 8). Until the subject read is built, a GitHub entry names no subject. §4's cites of the callback are re-read as well. The upstream source this amendment cites is read at `github/github-mcp-server` `796afb74`, GitHub's metadata and docs were fetched 2026-10-18, and the code it cites is read at `main` `b57966d2`. +**Amended 2026-10-18 (the order when the hold lifts):** connector-ops asked whether the order after the hold was still open (78798). Wren had ruled it in 77043, before the hold, and kept it (78807): the CIMD entries first, then the DCR entries, with Basecamp and Honeycomb (US) the first two captures (§10, step 8). That is the fresh ruling the hold note asks for, and it lifts nothing: a capture builds no entry, and no entry is built until an outside user connects one. Basecamp's and Honeycomb's metadata were fetched 2026-10-18, and the code this amendment cites is read at `main` `b57966d2`. + **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). @@ -451,7 +453,8 @@ Each step can be tested without the vendor, except the live measurements in step - 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. + - **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, and the next bullet is that ruling. The rest of this step stays as the design each entry follows when it is built. + - **The order when the hold lifts** (Wren 77043, kept in 78807 when connector-ops asked in 78798). The CIMD entries come first, since they register nothing at the vendor: Basecamp, Honeycomb (US), Grafana, Hugging Face, then Todoist. The DCR entries follow: Close, Otter, Buildkite, Miro and Calendly, with ClickUp last. #2138 built their registration store (TASK-197, `services/hostedMcpDcrRegistry.ts:23`), and the page's two registration notices below still wait. Canva stays parked. The 2026-10-13 sweep measured all of these but Todoist and Calendly, and says what each capture still decides ([hosted-mcp-catalogue-sweep-2026-10-13.md](hosted-mcp-catalogue-sweep-2026-10-13.md)); Todoist and Calendly keep the measurements in this step. Each still enters only as this step says. The first two captures are Basecamp's and Honeycomb's. Neither AS lists `openid` (fetched 2026-10-18), so each capture records the tool and field its subject read would name (§4). Basecamp's also checks that a token granted `mcp read` is refused on a write. Honeycomb's checks, once a Team Owner enables Honeycomb Intelligence, that `run_query` leaves only query records. - First, while Linear is the only entry: the client record (§2), then its backfill once that build's rollout has finished. - `the callback records the client that minted the pair` - `a row minted under another client is refused reconnect_required before any call` From 54065b46e981828cf04a3ea916371fc0387cb6da Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 18 Oct 2026 12:33:57 -0700 Subject: [PATCH 3/3] docs(plans): condition 3 is not read before the posts that follow a grant call (TASK-172) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §12's TASK-224 block said condition 3 rules out item 4. Condition 3 keeps a grant's tools from being listed or called in a pod that is public, joinable or bridged, but nothing reads it again before the posts that follow a grant call in the same turn, so a bridge enabled in between can relay one. TASK-234 (#2318, open) re-reads the bridge before each such post, as #2244 does once a turn has read the web. The block's cites are re-read at main b57966d2, and its list of the four pieces the outside offer waits on is brought to 2026-10-18: TASK-228 (#2225) and TASK-231 (#2239) have merged and deployed. 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 | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/plans/hosted-mcp-connection-scope.md b/docs/plans/hosted-mcp-connection-scope.md index e6bf435612..568ff4e14d 100644 --- a/docs/plans/hosted-mcp-connection-scope.md +++ b/docs/plans/hosted-mcp-connection-scope.md @@ -24,6 +24,8 @@ **Amended 2026-10-18 (the order when the hold lifts):** connector-ops asked whether the order after the hold was still open (78798). Wren had ruled it in 77043, before the hold, and kept it (78807): the CIMD entries first, then the DCR entries, with Basecamp and Honeycomb (US) the first two captures (§10, step 8). That is the fresh ruling the hold note asks for, and it lifts nothing: a capture builds no entry, and no entry is built until an outside user connects one. Basecamp's and Honeycomb's metadata were fetched 2026-10-18, and the code this amendment cites is read at `main` `b57966d2`. +**Amended 2026-10-18 (§12 conformed):** §12's TASK-224 block is re-read at `main` `b57966d2`, and its cites move with the code. TASK-228 and TASK-231 have merged, so each of the four pieces the block lists is merged and deployed, and an agent's join and condition 4 now share one test of whether it runs in-process. The block said condition 3 rules out item 4. Condition 3 keeps a grant's tools from being listed or called in a pod that is public, joinable or bridged, but it is not read again before the posts that follow a grant call in the same turn, so a bridge enabled in between can relay one. TASK-234 re-reads the bridge before each such post (#2318, open). Whether the outside offer also waits on it is not this note's to decide. The code this amendment cites is read at `main` `b57966d2`. + **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). @@ -520,24 +522,24 @@ 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: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: +**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:41). 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 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. +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:71–73). 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 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. +Mint and attenuation refuse a failing grant with `outsider_grant_` and a message that points to a separate pod (grants.ts:389, :440). 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 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. +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:426), the native dispatcher refuses any other name with `outsider_writable_grant_tool_denied` (:579), and the post lands only in the run's pod (:648). The check is re-read before each model call and each native dispatch, and a failed read narrows (:1577, :1698, :1841). A grant's own tools are not narrowed. They are offered beside the built-in two (:1711), and a call to one goes to the broker (:1811) before the native dispatcher (:1852), 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, 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`. +The rule does not compute (a)'s list. It bounds it at the pod instead. Condition 3 keeps a grant's tools from being listed or called while its pod is public, joinable or bridged (item 4), and condition 2 rules out item 5, for every grant in the pod. Condition 3 is not read again before the posts that follow a grant call in the same turn, so a bridge enabled in between can relay one. TASK-234 re-reads the bridge before each such post (#2318, open), as #2244 already does once a turn has read the web (nativeRuntimeService.ts:1844–1845). 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:228–234). 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` (:236–239, :269), and the legacy issuer refuses the same way (:366). 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 was first read at `main` `d37a8600` and is re-read at `b57966d2`. -**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: +**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 on 2026-10-18: -- 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-226 (#2220): an agent that does not run in-process cannot join a pod while the pod holds a live outsider-writable grant (outsiderGrantAgentJoinPolicy.ts:27, :43). Merged as `cb7cb51f` and deployed with `d37a8600`. It first refused every runtime type but `native`, so a hosted agent on the native engine was refused at the join though condition 4 accepts one already in the pod. TASK-231 (#2239) made the join and condition 4 share one test, `runsInNarrowedProcess` (hostedEngine.ts:93). Merged as `8f612d7d` and deployed with `f8b43625`. - 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. +- 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`. 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`.