Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/plans/hosted-mcp-connection-scope.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ 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.

**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:
**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 a `read` grant cannot call it (`write_mode_not_allowed`, `roomGrantService.ts:417`). The park comes from the grant's mode, not the class: the broker parks every non-read call under `write-with-confirm`, while `write` runs a reversible one unattended (`toolBrokerService.ts:1161–1162`). No hosted grant holds `write`, because the mint refuses it (`routes/grants.ts:337`, §5) and attenuation never raises a grant above its parent (`roomGrantService.ts:346`). So every call to a write tool parks for the credential's owner, whose card shows its exact arguments. 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).
Expand Down Expand Up @@ -237,7 +237,7 @@ In member mode, whatever an agent writes appears at the vendor as the member. [l

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 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 `definitionDigest` beside the arguments' digest: a digest of the canonical JSON of the tool's whole pin and its entry's id, which brings in the `upstreamName` that `tools/call` sends (`services/hostedMcpToolDefinitions.ts:292`). An approved call whose digest does not match the deployed pin is refused `definition_digest_mismatch` (TASK-247), checked with the arguments' digest, before the grant, drift and the budget reservation, one of the endings below (TASK-208). A pin the deploy removed counts as changed. The code is not `tool_drift`, because execution also runs the drift check, and there `tool_drift` means the live tool no longer matches the pin, which a PR fixes by re-pinning it. This code means the pin changed while the call waited, which the seat fixes itself: it proposes the call again, and that call 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.

Expand Down Expand Up @@ -496,7 +496,7 @@ Each step can be tested without the vendor, except the live measurements in step
- `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 changed after it parked is refused definition_digest_mismatch 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`
Expand Down
Loading