Skip to content

Commit d2fd66a

Browse files
lilyshen0722claudewren
authored
docs(plans): conform §8 and §9 to #2397's MCP session and vendor trail fields (TASK-263) (#2421)
* docs(plans): conform §8 and §9 to #2397's MCP session and vendor trail fields (TASK-263) §9 gains the session-bound 404 row, a note on the per-walk, per-call MCP session, and what the trail keeps of a failed vendor answer, each cited at main be32f10. It says which of its rows are not built yet (401, 403, 429), and moves the parked-call cites to where the code now is. §8's opening sentence says what #2028 and #2397 built. No header paragraph: two open PRs insert one at the same place, so the new paragraphs carry TASK-263 inline instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: wren <WyJ3cmVuIiwiZGVmYXVsdCJd@agents.commonly.invalid> Agent-Model: claude/claude-opus-5-5/max * docs(plans): a session's closing DELETE is best effort, and each moved cite names its ref (TASK-263) Rhea's HOLD on e7bdb61 (review 5613868473): the session paragraph said a DELETE closes the vendor's session and that no session outlives its walk or call. At main be32f10 close() drops the local id, sends one DELETE, does not read the answer and swallows a failure, so a vendor that refuses the DELETE or never receives it may keep its session. The paragraph now says the DELETE is best effort and never changes the result, that ending the vendor's session is not guaranteed, and that nothing about a session is stored on our side. The unasked review on the same head (5613886499) noted that the doc names the ref its cites are read at, and this diff did not. With no header paragraph to carry it, each paragraph whose cites this diff adds or moves now names main be32f10. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: wren <WyJ3cmVuIiwiZGVmYXVsdCJd@agents.commonly.invalid> Agent-Model: claude/claude-opus-5-5/max --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: wren <WyJ3cmVuIiwiZGVmYXVsdCJd@agents.commonly.invalid>
1 parent 851fcda commit d2fd66a

1 file changed

Lines changed: 13 additions & 6 deletions

File tree

‎docs/plans/hosted-mcp-connection-scope.md‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ The refusal must also hold where the tool runs, not only where the entry is offe
332332

333333
## 8. The trail records whose credential ran
334334

335-
`ToolCallRecord` (`models/ToolCall.ts:62–75`) records the grant, the agent, the tool and the outcome, but not whose credential ran. With per-person Connections, that is the first question a trail reader asks: whose Linear was this?
335+
`ToolCallRecord` (`models/ToolCall.ts:62–84`) records the grant, the agent, the tool and the outcome. Until #2028 it did not record whose credential ran, and with per-person Connections that is the first question a trail reader asks: whose Linear was this? #2028 built the paragraph below (`credentialOwnerId`, `:74`), and TASK-263 added a failed vendor answer's status and first line (`vendorStatus` and `vendorError`, `:80–81`; §9). The code this paragraph cites is read at `main` `be32f102`.
336336

337337
**Every record gains `credentialOwnerId`, the Connection's `createdBy` at call time, for every type.** That includes `github-app`, whose owner is the admin who created the row. It is copied onto the record, because once §10.5's last step deletes the row there is nothing left to join to. The copy holds against the row's delete, not the person's: once a Commonly account is deleted (§7), the id resolves to no one. A name beside it would hold against both, but how long a deleted person's name is kept is a retention decision for account deletion as a whole, and this column does not make it. Soft-deleting the row instead was considered and rejected: §10.5 ends with the delete, and a soft-deleted row is one that the sweeps, the catalogue and the mint would all have to learn to skip.
338338

@@ -344,7 +344,8 @@ The refusal must also hold where the tool runs, not only where the entry is offe
344344
| `invalid_grant` on that refresh | fails the call | `refused`, `reconnect_required` | `status: 'error'`, "reconnect"; only the generation holder writes it (§10.3), and later calls are refused as §10.3 says |
345345
| 403 or `insufficient_scope` | fails the call | `refused`, `provider_denied` (§10.4) | recorded, so the page can say what was refused; the grant is not touched |
346346
| 429 | fails the call | `failed`, `provider_rate_limited`, with `Retry-After` when one is sent | untouched |
347-
| 5xx, a timeout, no connection | fails the call | `failed`, `provider_error` or `provider_unreachable` | untouched |
347+
| 5xx, a timeout, no connection | fails the call | `failed`, `provider_error` or `provider_unreachable`; a vendor's answer keeps its status and first line (below) | untouched |
348+
| 404 on a request that carried a session id, the `initialized` notification included | opens a new session once, then sends that request once more (below) | as the retry ends; a second 404 is `failed`, `provider_error` | untouched |
348349
| a result with `isError: true` | preserves the vendor content and marks the tool result as an error | `failed`, `upstream_tool_error` | untouched |
349350
| a pinned tool missing, or drifted (§3) | refuses before calling | `refused`, `tool_unavailable` or `tool_drift` | untouched |
350351
| an argument the pin does not declare (§3) | refuses before calling, parking or spending budget | `refused`, `invalid_tool_args` | untouched |
@@ -354,11 +355,17 @@ The refusal must also hold where the tool runs, not only where the entry is offe
354355

355356
`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.
356357

357-
**A parked call ends once, and its ending is trailed** (TASK-208). The park writes `pending_approval`, `approval_required` (`toolBrokerService.ts:1217`), and the ending writes one more row under the same `approvalId`:
358+
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`).
358359

359-
- approved: the execution's own row, as for any call (`:1322`, or `:1332` 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;
360-
- declined: `refused`, `approval_declined` (`approvalActionService.ts:725`);
361-
- expired: `refused`, `approval_expired`. The decide path writes it (`:627`). The usual path is the scheduler's five-minute sweep, `sweepExpiredToolCallApprovals` (`approvalActionService.ts:766`, scheduled at `schedulerService.ts:491`), 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.
360+
**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.
361+
362+
**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 <code>: <message>` 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`.
363+
364+
**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`:
365+
366+
- 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;
367+
- declined: `refused`, `approval_declined` (`approvalActionService.ts:832`);
368+
- 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.
362369

363370
The seat learns each ending through §5's event, and the owner sees it on the card.
364371

0 commit comments

Comments
 (0)