From 3052f6307fa3d2b31acbc12aae7844ed16dbbba9 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Thu, 22 Oct 2026 20:34:16 -0700 Subject: [PATCH] =?UTF-8?q?docs(plans):=20the=20parked=20call=20as=20built?= =?UTF-8?q?,=20and=20Sentry=20in=20=C2=A712=20(TASK-272)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #2178 delivers a parked call's ending, #2378 binds the call to its pin, and #2328 and #2329 pin the first writes, so §5's "nothing reaches the seat" and "no entry on main pins one yet" were false. §5 and §9 now describe the built endings, card, room receipt, receipt fields and trail rows, with every cite re-read at main a3080f84. §10's Held line and §12(b) count Sentry, and §12(b) gives it a row: Sentry documents a project's DSN as safe to keep public, so anyone can write the events its reads return. 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 | 51 ++++++++++++----------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/docs/plans/hosted-mcp-connection-scope.md b/docs/plans/hosted-mcp-connection-scope.md index 9ce1cebb20..db254487ae 100644 --- a/docs/plans/hosted-mcp-connection-scope.md +++ b/docs/plans/hosted-mcp-connection-scope.md @@ -240,32 +240,32 @@ In member mode, whatever an agent writes appears at the vendor as the member. [l - The mint refuses `writeMode: 'write'` on a `hosted-mcp` row, with `write_requires_confirm`. - Attenuation cannot raise a mode (`roomGrantService.ts:346–348`, `grant_not_attenuated`), so every grant descended from one on this row is `read` or `write-with-confirm`. - Under `read`, a non-read tool is refused `write_mode_not_allowed` (`:417–418`). - - Under `write-with-confirm`, every non-read call parks for approval (`toolBrokerService.ts:717–718`). A `write` grant would run non-read, non-irreversible calls without parking, so the mint refusal is what makes the rule hold. -- **(b) Only the owner approves.** `approvalActionService.ts:493` refuses anyone but the pending call's `ownerUserId`, and `:502` refuses a bot. For this type, `ownerUserId` is the Connection's `createdBy`, carried as it is today (`toolBrokerService.ts:519`, `:742`). -- **(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 `write-with-confirm`, every non-read call parks for approval (`toolBrokerService.ts:1242–1243`). A `write` grant would run non-read, non-irreversible calls without parking, so the mint refusal is what makes the rule hold. +- **(b) Only the owner approves.** `approvalActionService.ts:787` refuses anyone but the pending call's `ownerUserId`, and `:796` refuses a bot. For this type, `ownerUserId` is the Connection's `createdBy`, carried as it is today (`toolBrokerService.ts:640`, `:772`). +- **(c) The owner sees what they approve.** `V2ApprovalCard.tsx` fetches the pending call's canonical args for the owner only (`:115`, "Approval buttons must not allow an owner to approve blind") and renders them (`:236`). +- **(d) What runs is what was approved.** `executeApprovedToolCall` refuses `args_digest_mismatch` (409) when the args no longer match the approved digest (`toolBrokerService.ts:1413–1414`). 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 `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 runs only under the pin it parked under.** Approval binds the arguments and the tool's name (`ApprovalToolCall`, `models/ApprovalAction.ts:37–51`), and those alone do not bind the pin: 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:306–308`), 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:86`). Some re-pins will land while a call waits. So the park records `definitionDigest` beside the arguments' digest (`models/ApprovalAction.ts:44`, `toolBrokerService.ts:1276–1277`): a digest of the canonical JSON of the tool's whole pin and its entry's id (`digestPinnedDefinition`, `services/hostedMcpEntryService.ts:158–159`), which brings in the `upstreamName` that `tools/call` sends (`services/hostedMcpToolDefinitions.ts:579`). 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 (`toolBrokerService.ts:1409–1419`), 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 (#2378), and it landed before the first entries that pin a write: Atlassian's five writes (#2328) and Sentry's `update_issue` (#2329) park under it. -**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. +**A parked call is still a call, so the seat that made it learns how it ended** (TASK-208, built in #2178). When a call parks, the seat's `tools/call` answers `pending_approval` with the card's `approvalId` (`routes/mcpGrants.ts:121–127`), and that turn ends. #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. #2178 delivers a parked call's ending as below. -- **Approval runs the parked call once, as approved.** The winning resolver executes the copy of the envelope it took before the stored args were scrubbed (`approvalActionService.ts:601`, `:676`), through `executeApprovedToolCall` (`toolBrokerService.ts:1282`). That path checks the digest, the grant, the audience, the connection, drift and the budget immediately before the provider, and never parks again (§6). +- **Approval runs the parked call once, as approved.** The winning resolver executes the copy of the envelope it took before the stored args were scrubbed (`approvalActionService.ts:814–821`, `:902`), through `executeApprovedToolCall` (`toolBrokerService.ts:1396`). That path checks the digests, the grant and its audience, §12's rule, the connection, drift and the budget immediately before the provider (`:1409–1442`), and never parks again (§6). - **Each ending reaches the seat once, as `approval.resolved`**, the type #2161 adds, with `actionType: 'tool_call'` and the `approvalId` the seat got at the park. A parked call ends in one of five ways: - - approved, and it ran: `ok`; - - approved, and it failed at the vendor: `failed`, with §9's reason; + - approved, and it ran: `ran`; + - approved, and it failed: `failed`, with `upstream_tool_error` when the tool's result marks an error, the broker's code when the call threw, such as §9's `provider_error`, and `broker_error` when the broker trailed nothing (`approvalActionService.ts:907–908`, `:920–926`); - approved, and refused: `refused`, with the code. A refusal can come before the budget reservation, from a check such as `grant_expired`, `tool_drift` or `budget_exhausted`, or after it, from the vendor refusing the call on its own authority, as with a 403 or a refresh refused `invalid_grant` (§9); - - declined; - - expired undecided. + - declined: `declined`, `approval_declined`; + - expired undecided: `expired`, `approval_expired`. - 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, 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 (§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. + Only the transition that moves the card out of `flagged` enqueues the event (`enqueueToolCallResolvedEvent`, `approvalActionService.ts:710`), 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:939–953`), 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`, `approvalActionService.ts:628`). For a proposal that message is ours, but for a hosted call it can be the vendor's (`hostedMcpToolDefinitions.ts:583–585`), so a tool call's cue names the code instead (`approvalActionService.ts:719–725`). A thrown code reaches the cue only when it matches `^[a-z][a-z0-9_]{0,63}$`, and any other reads `broker_error` (`:921–922`). +- **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 (`approvedCallReceipt`, `toolBrokerService.ts:1129–1142`). A tool that declares no receipt delivers the ending alone. GitHub's `create_issue` declares its issue `number` (`:225`); no hosted write declares one yet, so Atlassian's and Sentry's writes deliver the ending alone. 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 (`buildOwnerCardPayload`, `approvalActionService.ts:118–127`). For a `tool_call`, the row keeps the ending, its code and the ending's trail `callId`, never the vendor's result, which can repeat the arguments it was given, or its error text (`:905–926`); the card carries only those three (`:104–110`). A call resolved before #2178 shows an ending derived from its stored fields, `ran`, or `failed` with `legacy_execution_failed`, never the fields themselves (`:77–85`). 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 for an approved call names the ending and its code (`:750–756`, `:932–935`), so a call whose result the vendor marked `isError` reads `failed (upstream_tool_error)`. +- **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:86`, `approvalActionService.ts:825–853`), where a proposal's stays decidable, and the park caps that deadline at the grant's `expiresAt` (`:482–484`, `toolBrokerService.ts:1269`), 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:1244–1336`, then `:1339–1342`). Execution reserves one call against the grant's lineage after every other check and just before the provider (`:1439–1442`). 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 @@ -365,17 +365,17 @@ The refusal must also hold where the tool runs, not only where the entry is offe `refused` is for answers about authority, whether ours or the vendor's; `failed` is for a call that could not complete (`ToolCallOutcome`, `models/ToolCall.ts:21`). Each row reaches the person as a named state, never as a silent empty result. -The 401, 403 and 429 rows are not built yet. At `main` `be32f102` a vendor 401 or 403 fails the call at once, `refused`, `credential_rejected` (`services/hostedMcpToolDefinitions.ts:224–228`), and a 429 fails it `failed`, `provider_error`. A token is refreshed only before a list walk or a call, when its recorded lifetime is missing or ends within a minute (`isFresh`, `services/hostedMcpCredentialService.ts:305–315`). +The 401, 403 and 429 rows are not built yet. At `main` `a3080f84` a vendor 401 or 403 fails the call at once, `refused`, `credential_rejected` (`services/hostedMcpToolDefinitions.ts:306–310`), and a 429 fails it `failed`, `provider_error`, as any other answer outside 2xx does (`:311–315`). A token is refreshed only before a list walk or a call, when its recorded lifetime is missing or ends within a minute (`isFresh`, `services/hostedMcpCredentialService.ts:324–334`). **Each list walk and each call opens its own MCP session** (TASK-263, 2026-10-22). It is one Streamable HTTP session under the row's credential (`openHostedMcpSession`, `services/hostedMcpToolDefinitions.ts:197–318`): `initialize`, then the `notifications/initialized` notification, which must be answered `202`, then the walk or the call. Each request after `initialize` carries the negotiated protocol version, and the vendor's session id when it gave one. A version other than `2025-03-26`, `2025-06-18` or `2025-11-25` fails the walk or call `provider_error`. When the walk or call ends, it sends one `DELETE` for the session id the vendor gave, if it gave one (`:274–291`). The answer is not read and a request that fails is ignored, so the `DELETE` never changes the walk's or call's result. Ending the vendor's session is not guaranteed: a vendor that refuses the `DELETE`, or never receives it, may keep the session open. Nothing about a session is stored on our side. A walk or call holds its session id only while it runs and never shares or reuses it, so a refresh, a reconnect or a removal has no stored session to drop. **The trail keeps a failed vendor answer's status and first line** (TASK-263, 2026-10-22). When a vendor fails a hosted call with a JSON-RPC error, or with an HTTP error other than 401 or 403, at the pin check's list or at the call itself, the call's row keeps the HTTP status (`vendorStatus`) and a line of the error (`vendorError`). The direct path and an approved call's execution both write them (`toolBrokerService.ts:1341`, `:1424`). The line is the first line of a plain error body, or `JSON-RPC : ` when the error comes as JSON (`vendorErrorLine`, `services/hostedMcpToolDefinitions.ts:174–194`). It is sanitized before it is kept: the row's token, `Bearer` values, the values of `access_token`, `refresh_token`, `client_secret` and `api_key` fields, and any run of 24 or more letters, digits, `_` or `-` are redacted, and it is cut to 200 characters (`:165–172`). An HTTP error whose body runs past 4096 characters, or is JSON that does not parse or has no string `error.message`, keeps its status and no line. Nothing else of the answer is kept: not the rest of the body, not its headers, and never the token. The broker strips both fields, and the JSON-RPC code, from the error a seat receives (`stripVendorDiagnostics`, `toolBrokerService.ts:1024–1031`); a JSON-RPC error on the call still names the vendor's message in that error, sanitized the same way. The code this paragraph and the one above cite is read at `main` `be32f102`. -**A parked call ends once, and its ending is trailed** (TASK-208, its cites re-read at `main` `be32f102`). The park writes `pending_approval`, `approval_required` (`toolBrokerService.ts:1293`), and the ending writes one more row under the same `approvalId`: +**A parked call ends once, and its ending is trailed** (TASK-208, its cites re-read at `main` `a3080f84`). The park writes `pending_approval`, `approval_required` (`toolBrokerService.ts:1320`), and the ending writes one more row under the same `approvalId`: -- approved: the execution's own row, as for any call (`:1409`, or `:1424` when it throws): `ok`, `failed`, or `refused` with the code, whether a check of ours stops it before the provider or the vendor refuses it; -- declined: `refused`, `approval_declined` (`approvalActionService.ts:832`); -- expired: `refused`, `approval_expired`. The decide path writes it (`:734`). The usual path is the scheduler's five-minute sweep, `sweepExpiredToolCallApprovals` (`approvalActionService.ts:873`, scheduled at `schedulerService.ts:515`), which writes no row, so a call it expires ends with nothing in the trail after `pending_approval`. The build writes the same row there. +- approved: the execution's own row, as for any call (`:1444`, or `:1463` when it throws): `ok`, `failed`, or `refused` with the code, whether a check of ours stops it before the provider or the vendor refuses it. A throw that left no row is trailed `failed`, `broker_error` (`approvalActionService.ts:924–926`); +- declined: `refused`, `approval_declined` (`approvalActionService.ts:939`); +- expired: `refused`, `approval_expired`, written by the decide path when the deadline has passed (`:848`) and by the scheduler's five-minute sweep, the usual path (`sweepExpiredToolCallApprovals`, `:982`, its row at `:1010`, scheduled at `schedulerService.ts:515`). The seat learns each ending through §5's event, and the owner sees it on the card. @@ -487,7 +487,7 @@ Each step can be tested without the vendor, except the live measurements in step - 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. + - **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 were built when it was ruled, and Sentry has been built since under Wave 1 (`integrations/hostedMcp/entries.ts:21–23`). 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` @@ -686,7 +686,7 @@ To give rule 2 a case, the kernel would park items 1–3 for agents in the reach - item 5 keeps a `write` GitHub grant there, and minting it `write-with-confirm` instead removes it; - item 6 keeps any unconfined seat there. -**(b) Which sources count as outsider-writable.** A source is outsider-writable when someone outside the reach can write text its tools return. Each catalogue entry declares this and defaults to yes. An entry that says no must carry its reason, and the gate rules on it. Today every source is outsider-writable: the entries in `HOSTED_MCP_ENTRIES` (entries.ts:20), plus GitHub. +**(b) Which sources count as outsider-writable.** A source is outsider-writable when someone outside the reach can write text its tools return. Each catalogue entry declares this and defaults to yes. An entry that says no must carry its reason, and the gate rules on it. Today every source is outsider-writable: the entries in `HOSTED_MCP_ENTRIES` (entries.ts:21–23), plus GitHub. | Source | Who outside the reach writes what it returns | |---|---| @@ -694,6 +694,7 @@ To give rule 2 a case, the kernel would park items 1–3 for agents in the reach | `atlassian` | Jira is named in the ruling. Confluence pages and comments come from anyone with access to the site. | | `airtable` | Named in the ruling: a shared form writes records with no Airtable account. | | `google-calendar` | An invitation from outside can put an event on the calendar, with its own title and description. Whether it does depends on an account setting the grant cannot read. | +| `sentry` | Anyone holding a project's DSN, which Sentry documents as safe to keep public, can send the project events "with any information they want" (Sentry docs, *Data Source Name (DSN)*, fetched 2026-10-23). Its reads return the issues and events built from them. | | GitHub (`github-app`) | Named in the ruling. Issues, comments and pull requests come from anyone with a GitHub account on a public repository, and from every collaborator on a private one. | Inbound email has no entry today. An entry that reads mail is outsider-writable by definition.