From eca36d244250ca792a5473d41e2725d14a7a5892 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Mon, 19 Oct 2026 00:16:51 -0700 Subject: [PATCH] docs(plans): writes park for the member, and Wave 1 replaces the hold (TASK-172) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sam's 2026-10-19 ruling lifts §3's bar that every entry's credential be unable to write. §3 now records the two residuals the ruling accepts and the rules each Wave 1 pin set is ruled under. §5 drops the credential fence the bar required and specifies its other item: a parked call runs only under the pin it parked under. §10 step 8 records Wave 1 and names that binding's tests, and §11 and the sweep note where the old bar stood. Co-Authored-By: Claude Opus 5.5 Co-authored-by: wren Agent-Model: claude/claude-opus-5-5/max --- .../hosted-mcp-catalogue-sweep-2026-10-13.md | 2 ++ docs/plans/hosted-mcp-connection-scope.md | 34 ++++++++++++++----- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/docs/plans/hosted-mcp-catalogue-sweep-2026-10-13.md b/docs/plans/hosted-mcp-catalogue-sweep-2026-10-13.md index 37e3d4d3b7..aa6956d26d 100644 --- a/docs/plans/hosted-mcp-catalogue-sweep-2026-10-13.md +++ b/docs/plans/hosted-mcp-catalogue-sweep-2026-10-13.md @@ -2,6 +2,8 @@ *This is evidence for the next catalogue rulings. It sits next to `hosted-mcp-connection-scope.md`, the plan, and `integration-readiness-matrix.md`, the live state. No vendor account or consent was used. Every verdict comes from public metadata and vendor documentation. Every first-pass "fit" or "conditional" was re-measured by an independent refuting pass; first-pass "no" and "unreachable" verdicts were not.* +*2026-10-19: Sam lifted the bar this sweep measured against (the plan's §3, amended that day). An entry's credential may now write, and each write parks for the member who owns it. So a verdict that rests only on the lack of a read-only scope no longer excludes an app, and the measurements here stand as evidence of each app's registration, scopes and refresh. Wave 1 is chosen by what our users use, not from this table (the plan's §10, step 8).* + ## Method - **Population.** 61 apps were measured on 2026-10-13 (PostHog, Intercom and Salesforce were measured twice and are merged into one row each). The three vendors from the 2026-10-09 order (Canva, Miro, ClickUp) are a separate population: their "fit" came from that earlier order, and they went through the same refuting pass on 2026-10-14. The table has 64 rows. diff --git a/docs/plans/hosted-mcp-connection-scope.md b/docs/plans/hosted-mcp-connection-scope.md index 568ff4e14d..075a7e63eb 100644 --- a/docs/plans/hosted-mcp-connection-scope.md +++ b/docs/plans/hosted-mcp-connection-scope.md @@ -26,6 +26,8 @@ **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`. +**Amended 2026-10-19 (writes, with the member's approval):** Sam ruled, relayed by connector-ops on the Wave 1 rows (TASK-237 to TASK-246), that "writes are allowed when the member approves them, and apps are chosen by what our users use". That lifts §3's bar that every entry's credential be unable to write (76043). A tool that writes is pinned `write`, so each call parks for the credential's owner with its exact arguments, and the mint still refuses `write` on a hosted grant (§3, §5). The ruling accepts two residuals: a token scoped to write can write whatever the pin says, and a row connected before its entry requested a write scope must reconnect to write (§3). Without the bar, what a read pin may reach rests on the pin set, so §3 now records the rules Wave 1's pin sets are ruled under (Wren 78917). §5 no longer requires the first write entry to keep a credential that can write away from unparked calls. Its other item stands, and §5 now specifies it: a parked call runs only under the pin it parked under, and that is built before the first entry that pins a write merges. Wave 1's ten rows replace the 2026-10-14 hold and every order step 8 gave, and Basecamp, Honeycomb and Stripe are off (§10, step 8). §12 is unchanged: a hosted write parks on a card that shows its canonical arguments, which is the approval gate the CTO's #2168 ruling allows. The code this amendment cites is read at `main` `df8c219f`. + **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). @@ -146,7 +148,17 @@ Upstream tools that are not in the entry are never offered and never callable, a **A tool pinned `read` carries the vendor's own `readOnlyHint: true`.** The spec's default for an absent `readOnlyHint` is `false` (schema 2026-07-28, `ToolAnnotations`), so a tool whose vendor says nothing is one the vendor has not called read-only. A pin without the hint would also leave the drift row above dead for that tool, because a pin that never held the hint cannot see it withdrawn (Vera 75423–75424). The entry load refuses such a pin, as it refuses a `read` pin whose annotations say `readOnlyHint: false` or `destructiveHint: true` (#2014). The hint still grants nothing; its absence refuses. **A vendor that sends no `readOnlyHint` gets no entry, not a write-only one** (75440). The class records what a tool does, so a read tool is never pinned `write` to get past a missing hint, and §5's answer on attribution cannot arrive that way. -**Every entry's credential must be unable to write** (76043). A read never parks (§5). With a credential that can write, a tool pinned `read` is kept from writing only by the vendor's word, its annotations, and those can be wrong: Linear's `extract_images` declares `openWorldHint: false` and fetches the URLs in markdown its caller writes (Vera 76034). A scope the vendor enforces holds whatever the pin says, as Linear's `read` does (§4). So an entry requests only scopes its vendor's docs say cannot write. A vendor that offers none gets no entry until it does: Notion, Sentry and Stripe, on the survey of 2026-10-01 (76040), and Granola and Attio, whose only scope is `mcp`, on the metadata of 2026-10-09 (connector-ops 76914, Wren 76915). A server that exposes no write tool today does not meet this bar. Its tool list is the vendor's word and can grow, and one scope already authorises whatever it adds. Any other fence, a vendor's read-only path included, is a new mechanism and a gate item. +**An entry's credential may write, and each write parks for the member who owns it** (Sam, 2026-10-19, relayed by connector-ops on the Wave 1 rows). From 2026-10-01 this paragraph required every entry's credential to be unable to write (76043), and the ruling lifts that bar. A tool that writes is pinned `class: 'write'`, which `writeModeFor` turns into `write-with-confirm` (`services/hostedMcpEntryService.ts:133`). So every call to it parks for the credential's owner, whose card shows its exact arguments, and the mint still refuses `write` on a hosted grant (`routes/grants.ts:337`, §5). A destructive write also carries `irreversible`. Notion and Sentry, which the bar kept out, are Wave 1 entries. Granola and Attio, which it also kept out, are not, and Stripe is off (§10, step 8). The ruling accepts two residuals: + +- A token scoped to write can write whatever the pin says. The pin limits what we call, not what the token can do. +- A row connected before its entry requested a write scope holds a token that cannot write, so its owner reconnects to gain write (§5). + +**The first residual reaches our own reads, because a read never parks** (§5). With a credential that can write, a tool pinned `read` is kept from writing only by the vendor's word, its annotations, and those can be wrong: Linear's `extract_images` declares `openWorldHint: false` and fetches the URLs in markdown its caller writes (Vera 76034). A scope the vendor enforces holds whatever the pin says, as Linear's `read` does (§4), and that is why the bar stood. Without it, the pin set is what narrows the residual, so each entry's pins are ruled in its PR thread under these rules (Wren 78917): + +- A read is pinned only if it is named, product-scoped and carries no credential or foreign payload (Wren 76760, recorded at `integrations/hostedMcp/atlassian.ts:16`). +- A read leaves only the vendor's own record of that read: query history, result records, usage counters, audit entries. A tool that creates anything a person sees as content, sends a notification or runs third-party code is a write whatever its annotations say, so it is pinned `write` or not at all (Wren, on the TASK-172 row, 2026-10-14). +- No tool is pinned whose reach is picked at call time from the vendor's own list: an operation name, code the caller writes for the vendor to run, or another agent. Drift compares a tool's definition with its pin, so it cannot see that list grow, and no pinned schema bounds what the call reaches. +- An entry requests the narrowest scopes its vendor allows for what it pins. One that pins no write keeps its vendor's read scope where one exists. **The Linear entry is pinned at build** from a `tools/list` taken on a consenting test account, with read tools only, each re-described by us. The measurement contains 38 tools; the entry pins 36 and leaves out the two caller-chosen fetch paths above. The earlier unauthenticated research named no Linear tools. Linear's MCP docs never mention annotations, so whether its read tools carry the hint is known only from the authenticated list (§11). @@ -223,7 +235,9 @@ In member mode, whatever an agent writes appears at the vendor as the member. [l - **(c) The owner sees what they approve.** `V2ApprovalCard.tsx` fetches the pending call's canonical args for the owner only (`:99`, "Approval buttons must not allow an owner to approve blind") and renders them (`:203`). - **(d) What runs is what was approved.** `executeApprovedToolCall` refuses `args_digest_mismatch` (409) when the args no longer match the approved digest (`toolBrokerService.ts:842–843`). -Under these four, the member has read and approved each written call, one at a time. Reads carry no words of the agent's, so they need none of the four. v1's Linear entry has no write tool, and its credential has no `write` scope. A write tool added later also needs re-consent with `write`, and `grantedScope` is how the page tells which rows need it. That alone does not meet §3's bar: the same token would serve every read in the entry, and a read never parks. So the first entry with a write tool also needs a mechanism that keeps a credential able to write away from every call that has not parked. That mechanism is a gate item, specified with that tool, not here. So is a parked call that outlives a re-pin of its tool: approval binds the arguments and the tool's name, not the pin, so a call parked before a deploy that re-pins its tool would run under the new pin. +Under these four, the member has read and approved each written call, one at a time. Reads carry no words of the agent's, so they need none of the four. v1's Linear entry has no write tool, and its credential has no `write` scope. A write tool added later also needs re-consent with `write`, and `grantedScope` is how the page tells which rows need it. The same token then serves every read in the entry, and a read never parks. Until 2026-10-19 that failed §3's bar, so the first entry with a write tool needed a mechanism that kept a credential able to write away from every call that had not parked. Sam's ruling that day accepted that residual and left the pin as what limits our calls (§3), so no entry needs that mechanism. + +**A parked call runs only under the pin it parked under.** Approval binds the arguments and the tool's name (`ApprovalToolCall`, `models/ApprovalAction.ts:35–47`), not the pin, so a call parked before a deploy that re-pins its tool would run under the new pin. A write pin is compared exactly (`services/hostedMcpEntryService.ts:300`), so any upstream change to its `inputSchema`, even a property's description, is refused until a PR re-pins it, and a parked call can wait a day (`models/ApprovalAction.ts:78`). Some re-pins will land while a call waits. So the park records a digest of the tool's definition, which for a hosted tool is its pin. An approved call whose tool's definition has changed since is refused `tool_drift` before the budget reservation, one of the endings below (TASK-208), and a seat that proposes it again parks under the new pin. This is broker code, built once, and it lands before the first entry that pins a write merges. No entry on `main` pins one yet. **A parked call is still a call, so the seat that made it learns how it ended** (TASK-208). When a call parks, the seat's `tools/call` answers `pending_approval` with the card's `approvalId` (`routes/mcpGrants.ts:116–121`), and that turn ends. Today nothing reaches the seat after that. #2161 wakes the proposer of an approved `commonly_propose_action` card and leaves a parked call out on purpose, because what a call returns is the broker's to say. @@ -238,9 +252,9 @@ Under these four, the member has read and approved each written call, one at a t Only the transition that moves the card out of `flagged` enqueues the event, so a second click, or a sweep racing a decision, delivers nothing more. A failed enqueue is logged and never undoes the decision, as in #2161. The CLI branch TASK-207 adds returns `payload.content` for every `approval.resolved`, so a tool call needs no branch of its own, provided that branch does not filter on `actionType`. - **A decline and an expiry are delivered too.** This differs on purpose from #2161's rule for a proposal, where a no wakes nothing. A declined proposal leaves nothing outstanding. A parked call has left the seat telling someone it is waiting, and for a seat grant the card sits in the owner's room with the agent (`toolBrokerService.ts:871–885`), where the person who asked may never look. A seat that proposes again parks again, so a repeat always waits on the owner. - **The event says how the call ended, never what the vendor said.** The server composes `payload.content` from our own text, the pin's tool name, the approver's name, the ending and its code. It never carries the vendor's result or error text, the arguments, or a credential. Both runtimes give `payload.content` to the model as the turn's prompt (#2161, "Runtime coverage"), so vendor text placed there would reach a seat as an instruction rather than as a tool result. §3 lets no live vendor text into what a seat is told about a tool, and this applies that rule to the cue. #2161's cue quotes an executor's error message (`describeApprovedOutcome`). For a proposal that message is ours, but for a hosted call it can be the vendor's (`hostedMcpToolDefinitions.ts:323`), so a tool call's cue names the code instead. -- **A seat that needs what the call produced reads it back** with a read tool, where it arrives as a tool result, confined, trailed and budgeted like any read. The one exception is a receipt that the write tool's definition declares: named fields of the result, each with a shape that cannot carry prose, such as an integer or a fixed identifier pattern. Each value is checked on its way into the cue, and one that fails is dropped. A tool that declares no receipt delivers the ending alone. GitHub's write tools may declare their issue or pull request number in the build, and a hosted write tool declares its receipt in the PR that adds it, with the gate item above. +- **A seat that needs what the call produced reads it back** with a read tool, where it arrives as a tool result, confined, trailed and budgeted like any read. The one exception is a receipt that the write tool's definition declares: named fields of the result, each with a shape that cannot carry prose, such as an integer or a fixed identifier pattern. Each value is checked on its way into the cue, and one that fails is dropped. A tool that declares no receipt delivers the ending alone. GitHub's write tools may declare their issue or pull request number in the build, and a hosted write tool declares its receipt in the PR that adds it, where its pin set is ruled (§3). - **The card and the room carry the ending, not the result.** The card keeps a parked call's arguments to the owner (`approvalActionService.ts:73`), but two fields on `main` can still put the vendor's words on it. `executionResult` holds the vendor's result (`:679`, copied to the card at `:83`), and a write's result can repeat the arguments it was given. `executionError` holds the error's message (`:721`). For a `tool_call`, the row keeps the ending, its code and the ending's trail `callId` instead. The record still links to its execution, through the trail, as it already links to the arguments only through their digest (ADR-020 D2). The room receipt says `done` for every call that returns (`:687`), including one the vendor marked `isError` (`toolBrokerService.ts:1059`), so it names the ending as well. -- **The grant is checked when the call runs, not when it parked.** Approval is consent to one call under the grant as it stands at execution, and it never extends or revives a grant. A grant expired or revoked since the park (`roomGrantService.ts:430–431`), a seat that has left the audience, an owner banned since (§7), a pin that has drifted or a tool gone upstream (§3), or a spent budget each refuses the call with its own code, and the seat learns that code. A parked call's card expires closed after 24 hours (`models/ApprovalAction.ts:78`, `approvalActionService.ts:612`), where a proposal's stays decidable. The build caps that deadline at the grant's `expiresAt`, so a card never outlives the grant it would run under, as a grant is capped at its Connection's (§9). +- **The grant is checked when the call runs, not when it parked.** Approval is consent to one call under the grant as it stands at execution, and it never extends or revives a grant. A grant expired or revoked since the park (`roomGrantService.ts:430–431`), a seat that has left the audience, an owner banned since (§7), a pin that has drifted (§3) or changed since the park (above), a tool gone upstream (§3), or a spent budget each refuses the call with its own code, and the seat learns that code. A parked call's card expires closed after 24 hours (`models/ApprovalAction.ts:78`, `approvalActionService.ts:612`), where a proposal's stays decidable. The build caps that deadline at the grant's `expiresAt`, so a card never outlives the grant it would run under, as a grant is capped at its Connection's (§9). - **Budget is spent when the call runs, never when it parks.** The park returns before the reservation (`toolBrokerService.ts:1145–1233`, then `:1236`). Execution reserves one call against the grant's lineage after every other check and just before the provider (`:1317–1321`). So a declined or expired call spends nothing, and neither does one refused before the reservation. Reads spend from the same budget while a write waits, so an approved write can find it spent. It is then refused `budget_exhausted`, because approval does not lift the granter's bound. A reserved call has spent its reservation whether it runs, fails or the vendor refuses it, as an unattended call does. ## 6. Which surface may offer a hosted write tool @@ -455,6 +469,7 @@ 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). + - **Wave 1** (Sam, 2026-10-19, relayed by connector-ops on the TASK-237 to TASK-246 rows): writes are allowed when the member approves them, and apps are chosen by what our users use. That replaces the hold and every order this step gives, which stay as the record, and Basecamp, Honeycomb and Stripe are off. Wave 1 is ten rows, built now: GitHub with writes, writes for the Linear and Atlassian entries, then Notion, Sentry, Supabase, PostHog, Asana, Cloudflare and GitLab. Any other app waits until it is chosen the same way. Each pin comes from a capture taken on a consenting account, never from docs, and Wren rules each entry's pin set in its PR thread under §3's rules. Every entry still meets the rest of this step, and no entry that pins a write merges before §5's binding of a parked call to its pin. - **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. @@ -465,7 +480,7 @@ Each step can be tested without the vendor, except the live measurements in step - `the backfill records Linear's document on a row that holds a pair and no recorded client, and leaves a recorded client alone` - Then, in the PR of whichever entry merges next after Linear, and after the backfill has run: the refusal of a row without a recorded client (§2). Google Calendar's is that PR unless another merges first. A dry run of the backfill on the deployed build that reports no row to write counts as the backfill having run. - `a connected row with no recorded client is refused as a mismatch` - - Each entry requests only scopes its vendor's docs say cannot write (§3). Canva, Miro and ClickUp each advertise read scopes apart from their write scopes (connector-ops 76914). Todoist and Calendly enter only if a consent with their read scope alone works, measured at build. Todoist's resource metadata advertises only `data:read_write` (fetched 2026-10-01). If Todoist answers a `data:read` request with `data:read_write`, the callback refuses the connect (§4), so its capture shows which it grants. Calendly's docs list `mcp:scheduling:read` and `mcp:scheduling:write` and say its server "requires both for full access". ClickUp's AS advertises no refresh grant, so ClickUp enters only if its token response carries `expires_in` (§4), and that lifetime is how long each connection lasts before its person reconnects. DCR's storage is built before any of the six vendors that offer DCR and no CIMD (§4), for a public client and with no entry (TASK-197). Miro's PR brings its confidential registration (§4). ClickUp's AS lists only the `authorization_code` grant, so ClickUp's PR requests `refresh_token` at registration only where an AS lists it. ClickUp's API docs say its access token "currently does not expire" (developer.clickup.com/docs/authentication, fetched 2026-10-14), but they describe its REST API's OAuth at `api.clickup.com`, not the AS its MCP server names, so ClickUp's capture still decides `expires_in`. Canva and Todoist offer CIMD. Canva advertises one URL as both its `token_endpoint` and its `revocation_endpoint` (fetched 2026-10-09), as Linear does, so its walk measures the revoke as step 7 does. + - Until 2026-10-19, each entry requested only scopes its vendor's docs say cannot write (§3), and this bullet's measurements were taken against that bar. Since then an entry requests the narrowest scopes its vendor allows for what it pins (§3). Canva, Miro and ClickUp each advertise read scopes apart from their write scopes (connector-ops 76914). Todoist and Calendly enter only if a consent with their read scope alone works, measured at build. Todoist's resource metadata advertises only `data:read_write` (fetched 2026-10-01). If Todoist answers a `data:read` request with `data:read_write`, the callback refuses the connect (§4), so its capture shows which it grants. Calendly's docs list `mcp:scheduling:read` and `mcp:scheduling:write` and say its server "requires both for full access". ClickUp's AS advertises no refresh grant, so ClickUp enters only if its token response carries `expires_in` (§4), and that lifetime is how long each connection lasts before its person reconnects. DCR's storage is built before any of the six vendors that offer DCR and no CIMD (§4), for a public client and with no entry (TASK-197). Miro's PR brings its confidential registration (§4). ClickUp's AS lists only the `authorization_code` grant, so ClickUp's PR requests `refresh_token` at registration only where an AS lists it. ClickUp's API docs say its access token "currently does not expire" (developer.clickup.com/docs/authentication, fetched 2026-10-14), but they describe its REST API's OAuth at `api.clickup.com`, not the AS its MCP server names, so ClickUp's capture still decides `expires_in`. Canva and Todoist offer CIMD. Canva advertises one URL as both its `token_endpoint` and its `revocation_endpoint` (fetched 2026-10-09), as Linear does, so its walk measures the revoke as step 7 does. - Every entry PR witnesses the catalogue projection, not only the seed and the broker (Vera, the TASK-172 row). The three do not share a source: the seed and the broker read the entry, while the projection reads the hand-written map through `projectTools` (`services/installable/toolInstallables.ts:194`). That is how Linear's grant was correct at the mint and showed as a GitHub row on the page (connector-ops 76149, Vera 76162). - `the entry's catalogue row lists its pinned tools, each named .`, read through `GET /api/installables`, the call the page makes - Before Google: the pre-registered client (§4). @@ -480,6 +495,9 @@ Each step can be tested without the vendor, except the live measurements in step - `invalid_client marks the registration refused and sends its rows to reconnect` - `a removal on a refused registration sends nothing and returns the page as revokeAt` - 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`. + - Before the first entry that pins a write merges: a parked call's binding to its pin (§5). + - `an approved call whose pin changed after it parked is refused tool_drift before the reservation, and the seat learns that ending` + - `an approved call whose pin is unchanged runs once, as approved`, the contrast to the arm above - 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` @@ -495,7 +513,7 @@ Each step can be tested without the vendor, except the live measurements in step - Google starts as Calendar. Google's setup page lists three scopes for its Calendar server, all reads: `calendar.calendarlist.readonly`, `calendar.events.freebusy` and `calendar.events.readonly`. Calendar's scope page gives them no class; the Cloud console groups declared scopes "into sensitive or restricted categories" (Google's sensitive-scope verification page), so the class is read there. Verification starts now (76044), since an unverified app is capped at "100 new users in total" (support.google.com/cloud/answer/7454865). The instance's Google client is published In production, not Testing. Google issues an external app in Testing a refresh token "expiring in 7 days" (developers.google.com/identity/protocols/oauth2, updated 2026-05-26), so a walk on a Testing client would pass and every row would lapse a week later. Calendar's pin comes from an unauthenticated `tools/list`, where all nine tools carry annotations and the five reads carry `readOnlyHint: true`. The walk's first authenticated list is its drift check (connector-ops 76137). - Google's revocation is project-wide for one Google account. It "removes all OAuth 2.0 scopes previously granted to a project, invalidating any issued access or refresh tokens for all clients registered under that project" (developers.google.com/identity/protocols/oauth2/web-server). Google keeps consent per user, and a revoke removes that user's (developers.google.com/identity/oauth2/web/guides/use-token-model), so the reach crosses the project's clients and not Google accounts (Rhea 76177). The instance's Google client sits in a Cloud project of its own, apart from the client behind Google sign-in (`GOOGLE_OAUTH_CLIENT_ID`, `controllers/oauthController.ts:96`): a revoke through either would end that account's grant to both. The same page warns that "it might take some time before the revocation has full effect", so the walk looks once at a reconnect made straight after a removal: its first call and its first refresh succeed. - Docs enters only if its reads work on `documents.readonly` alone, measured at build. The setup page lists four scopes for its Docs server: `documents.readonly`, `documents`, which writes, and Drive's two below. Docs' scope page classes `documents.readonly` Sensitive, so Docs alone needs no security assessment. If Docs is built on Calendar's project, a revoke at Google for one row ends the other whenever both are connected as one Google account (Rhea 76177). §2's withdrawal likewise ends the old account's other row. The plan rules how removal treats that before a second Google entry is built on that project. - - Drive and Gmail wait for Sam's call on restricted scopes. `drive.file` lets an app "create new Drive files, or modify existing files" (Drive's scope page, updated 2026-09-03), so it fails §3's bar. Drive's only other scope on the setup page is `drive.readonly`, which is Restricted, and so are both Gmail scopes, `gmail.readonly` and `gmail.compose` (Gmail's scope page, updated 2026-09-10). A restricted scope needs Google's restricted-scope review and a security assessment repeated "at least every 12 months" (Google's restricted-scope verification page, updated 2026-08-19). + - Drive and Gmail wait for Sam's call on restricted scopes. `drive.file` lets an app "create new Drive files, or modify existing files" (Drive's scope page, updated 2026-09-03), so it failed §3's bar until 2026-10-19. Drive's only other scope on the setup page is `drive.readonly`, which is Restricted, and so are both Gmail scopes, `gmail.readonly` and `gmail.compose` (Gmail's scope page, updated 2026-09-10). A restricted scope needs Google's restricted-scope review and a security assessment repeated "at least every 12 months" (Google's restricted-scope verification page, updated 2026-08-19). - Google's MCP servers are a Developer Preview, and the setup page requires "Membership in the Google Workspace Developer Preview Program". ## 11. What this corrects, and what stays open @@ -508,9 +526,9 @@ Each step can be tested without the vendor, except the live measurements in step The TASK-172 ruling's part 3 wording on `:180` is replaced by §6. -**Other vendors join the catalogue only once their own docs verify** the URL, transport, auth mode, token lifetime and refresh, and revocation (74820; the TASK-172 row), and a scope that cannot write (§3). A `tools/list` must also show `readOnlyHint: true` on each tool the entry pins `read` (§3). Three facts already shape their entries: +**Other vendors join the catalogue only once their own docs verify** the URL, transport, auth mode, token lifetime and refresh, and revocation (74820; the TASK-172 row). Until 2026-10-19 they also needed a scope that cannot write (§3). A `tools/list` must also show `readOnlyHint: true` on each tool the entry pins `read` (§3). Three facts already shape their entries: -- Notion's only advertised scope is `default`, so a Notion entry cannot narrow its credential to reads the way Linear's `read` does, and §3's bar keeps Notion out until it offers a scope that cannot write (76043). +- Notion's only advertised scope is `default`, so a Notion entry cannot narrow its credential to reads the way Linear's `read` does. §3's bar kept Notion out (76043) until Sam's 2026-10-19 ruling lifted it, and Notion is now Wave 1's TASK-240, its writes pinned `write` (§3). - GitHub needs the pre-registered client (§4). - Two servers publish their source, so their hints can be read before a login, in the files that declare what they serve over MCP. Sentry's tool type makes `readOnlyHint`, `destructiveHint` and `openWorldHint` required, and a test enforces it (`getsentry/sentry-mcp` at `0563bde`, `packages/mcp-core/src/tools/types.ts`); its `internal/agents/tools` are not MCP tools and carry none. GitHub's sets `readOnlyHint` on all 121 tools in `pkg/github`, and its tests fail a tool that omits it (`github/github-mcp-server` at `85598ba`, `pkg/toolvalidation/readonlyhint.go`).