diff --git a/contracts/gen/go/mecatl/v1/harness_grpc.pb.go b/contracts/gen/go/mecatl/v1/harness_grpc.pb.go index bc8d8143ae..558780485b 100644 --- a/contracts/gen/go/mecatl/v1/harness_grpc.pb.go +++ b/contracts/gen/go/mecatl/v1/harness_grpc.pb.go @@ -183,9 +183,10 @@ type HarnessServiceClient interface { ForkSession(ctx context.Context, in *ForkSessionRequest, opts ...grpc.CallOption) (*ForkSessionResponse, error) // Converse drives one run. The first frame MUST be `prompt` or `retry`; later // frames may carry controls. A received second `prompt` or `retry` is rejected - // with INVALID_ARGUMENT. The server streams `Event` envelopes until the terminal - // `result` event, then closes the stream; controls still in transit may instead - // observe normal stream completion. A context cancel from the client aborts the run. + // with INVALID_ARGUMENT. The server streams `Event` envelopes until either a + // terminal `result` or a pending `authorization.required` park, then closes the + // stream; controls still in transit may instead observe normal stream completion. + // A context cancel from the client aborts the run. Converse(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ConverseRequest, ConverseResponse], error) // ResolveRunAsk resolves one ordinary permission ask on the exact addressed // run without opening or owning its event stream. @@ -1428,9 +1429,10 @@ type HarnessServiceServer interface { ForkSession(context.Context, *ForkSessionRequest) (*ForkSessionResponse, error) // Converse drives one run. The first frame MUST be `prompt` or `retry`; later // frames may carry controls. A received second `prompt` or `retry` is rejected - // with INVALID_ARGUMENT. The server streams `Event` envelopes until the terminal - // `result` event, then closes the stream; controls still in transit may instead - // observe normal stream completion. A context cancel from the client aborts the run. + // with INVALID_ARGUMENT. The server streams `Event` envelopes until either a + // terminal `result` or a pending `authorization.required` park, then closes the + // stream; controls still in transit may instead observe normal stream completion. + // A context cancel from the client aborts the run. Converse(grpc.BidiStreamingServer[ConverseRequest, ConverseResponse]) error // ResolveRunAsk resolves one ordinary permission ask on the exact addressed // run without opening or owning its event stream. diff --git a/contracts/proto/mecatl/v1/harness.proto b/contracts/proto/mecatl/v1/harness.proto index 77529542cd..1bde1b1618 100644 --- a/contracts/proto/mecatl/v1/harness.proto +++ b/contracts/proto/mecatl/v1/harness.proto @@ -97,9 +97,10 @@ service HarnessService { // Converse drives one run. The first frame MUST be `prompt` or `retry`; later // frames may carry controls. A received second `prompt` or `retry` is rejected - // with INVALID_ARGUMENT. The server streams `Event` envelopes until the terminal - // `result` event, then closes the stream; controls still in transit may instead - // observe normal stream completion. A context cancel from the client aborts the run. + // with INVALID_ARGUMENT. The server streams `Event` envelopes until either a + // terminal `result` or a pending `authorization.required` park, then closes the + // stream; controls still in transit may instead observe normal stream completion. + // A context cancel from the client aborts the run. rpc Converse(stream ConverseRequest) returns (stream ConverseResponse); // ResolveRunAsk resolves one ordinary permission ask on the exact addressed diff --git a/docs/acceptance/README.md b/docs/acceptance/README.md index cf869106ff..d918c46efb 100644 --- a/docs/acceptance/README.md +++ b/docs/acceptance/README.md @@ -335,6 +335,10 @@ PR after verification. There is no cleanup or status-only PR. ordinary ask resolution, cancellation, strict multimodal steering, and pending-steer retraction over gRPC and HTTP through `session.controls(runId)`, with request options, bounded correlated acknowledgements, and mandatory stale-run guards. Status: proposed. +- [TypeScript SDK MCP authorization lifecycle](sdk-mcp-authorization-lifecycle.md) — a + session-bound authorization handle with live URL presentation, explicit typed recheck/cancel + flows, exact-run permission controls, single-consumption correlation, and honest + disconnect/recovery semantics over gRPC and HTTP. Status: proposed. - [TypeScript SDK HTTP well-known-type JSON compatibility](sdk-http-wkt-json-compatibility.md) — descriptor-guided Timestamp and Duration decoding for daemon stdlib-JSON responses across unary HTTP and SSE, preserving ProtoJSON strings, typed failures, and the registered raw response. diff --git a/docs/acceptance/sdk-mcp-authorization-lifecycle.md b/docs/acceptance/sdk-mcp-authorization-lifecycle.md new file mode 100644 index 0000000000..0c000a2266 --- /dev/null +++ b/docs/acceptance/sdk-mcp-authorization-lifecycle.md @@ -0,0 +1,303 @@ +# TypeScript SDK MCP authorization lifecycle — acceptance plan + +**Contract:** human-reviewed/v2 +**Work classification:** Architectural — this adds durable public TypeScript SDK resource, stream, status, result, and control contracts for a stateful session-bound authorization workflow. +**Decision record:** [ADR 0348](../adr/0348-typescript-sdk-mcp-authorization-lifecycle.md) +**Phase:** ergonomic TypeScript SDK MCP authorization lifecycle +**Status:** proposed, 2026-09-18. The directing user approved the session-bound lifecycle, Run handoff, lazy dispatch, bounded recovery contract, and the attachment, termination, and correlation clarifications requested during Plan / Interface review. +**Delivery:** Split. The public SDK object model, Run parking contract, single-consumption stream grammar, control routing, and disconnect semantics require Plan / Interface review before implementation. +**Expected tasks:** deferred to orchestration +**Issue:** [stacklok/mecatl#1469](https://github.com/stacklok/mecatl/issues/1469) +**Plan PR:** [stacklok/mecatl#1687](https://github.com/stacklok/mecatl/pull/1687) +**Approved baseline:** absent until the Plan / Interface PR merges + +An SDK consumer can receive a normal authorization-parked outcome from an ordinary +`Run`, bind that event's authorization ID to the existing `Session`, obtain the live +presentation URL, and perform one explicit recheck or cancellation through a typed +single-consumption flow. A continuation that parks again returns another typed +authorization handoff instead of being misclassified as a truncated run. The server +continues to own authorization, permissions, expiry, and all terminal outcomes. + +The SDK does not adopt mecatui's phase machine. It supplies correlation, decoding, +exact-run continuation controls, request options, and protocol checks; applications +own rendering, browser launch, polling cadence, persistence, and recovery policy. This +specialized lifecycle supersedes only the ergonomic-resource limit in +[ADR 0304](../adr/0304-typescript-sdk-public-surface-and-release.md), as recorded by +[ADR 0348](../adr/0348-typescript-sdk-mcp-authorization-lifecycle.md). + +## Human decisions + +- [x] Public object model — approve `Session.mcpAuthorization(authorizationId)` returning a reusable correlation handle, with each `recheck()` or `cancel()` producing a distinct single-consumption `McpAuthorizationFlow` and a discriminated pending, settled, completed, or chained-authorization result, instead of adding raw-shaped methods to `client.mcp`. — Decision: approved as proposed. +- [x] Initial Run handoff — approve additive `Run.outcome()` as the normal completed-or-authorization-required drain, clean iterator EOF after an authorization park, and a typed `RunAuthorizationRequiredError` from the existing completed-only `Run.result()` method so valid parking is never a `ProtocolError` or a stuck `Session`. — Decision: approved as proposed. +- [x] Dispatch and automatic-control options — approve lazy recheck/cancel dispatch on the first iterator `next()` or `result()`, with the operation's `RequestOptions` scoped only to that stream and a separate `permissionRequestOptions` value for automatic permission replies. — Decision: approved as proposed. +- [x] Recovery contract — approve explicit one-shot controls with no SDK polling, mutation retry, or transparent reconnect; a lost response is unrecoverable through this lifecycle alone when the server committed and cleared the original pending authorization before the application learned the continuation ID, while retained session activity and an already-observed run ID may permit explicit inspection or attachment. — Decision: approved as proposed. +- [x] Attachment recovery terminal — decide whether an explicit `Session.attach(continuationRunId)` must recognize a replayed chained authorization park or require application-owned watch closure. — Decision: a valid pending `authorization.required` for the exact attached run is terminal, just like `result`; attachment yields it, marks `live` false, clears pending asks, checkpoints it, and closes without application cleanup. +- [x] Flow termination contract — decide the observable outcome of caller abort, deadline, iterator return, transport loss, and client close, including responder, control, and registration lifetimes. — Decision: use the Scenario 5 termination matrix; explicit iterator return ends a pending read cleanly, external failures reject once with their normalized errors, flow-owned responders and automatic controls are aborted, late verdicts are suppressed, and later manual controls fail locally. +- [x] Correlation boundary — decide which origins the SDK can prove without widening the wire. — Decision: the server enforces session ownership through the session-affined request; the SDK checks the handle's authorization ID, learns the original non-empty call ID from the authoritative frame, requires the repeated original resolution to retain both IDs, pins the continuation run ID for every continuation frame, and validates a chained authorization as a different authorization with its own non-empty call ID. + +## Interface contract + +- **gRPC / protobuf:** No wire-shape change — the SDK reuses the existing `GetMcpAuthorizationPresentation`, `RecheckMcpAuthorization`, `CancelMcpAuthorization`, `ResolveRunAsk`, and `CancelRun` descriptors and their current messages and field numbers. The `Converse` source comment is corrected to state that the stream closes after either terminal `result` or a pending `authorization.required` park; regenerated bindings carry that documentation-only correction. The authorization-control relay makes the existing gRPC disconnect distinction explicit: control-stream EOF cancels the exact continuation only when the lost stream strands it on an ordinary permission ask, while runnable work remains detached and drains. The TypeScript RPC catalog changes the HTTP recheck/cancel request-body classification from `json` to `none` and declares response field `event`; the HTTP stream decoder then wraps each server-emitted bare `Event` as the descriptor's `{event}` response. +- **Exported Go APIs / interfaces:** None — the current Service and HarnessServer APIs already own presentation, exact authorization transition, continuation relay, ask resolution, and cancellation; this plan changes no Go symbol or engine surface. +- **Tool schemas:** None — no model-facing tool name, input schema, result schema, permission classification, or dispatch behavior changes. +- **CLI / config:** None — no flag, environment variable, settings key, default, precedence rule, browser behavior, or TUI command changes. +- **Events / persistence:** None — the lifecycle consumes the existing `authorization.required`, `authorization.resolved`, `permission.ask`, and `result` events without adding or changing an event, snapshot, event-log record, cursor, or store. The initial Run and a continuation may both close normally on `authorization.required` without a `result`. Presentation URLs remain live-only and unpersisted. The SDK returns detached decoded values and stores no credential or lifecycle truth outside live handles. +- **Security / authority:** `Session.mcpAuthorization()` binds only the existing session-affined operation bag and caller-supplied authorization ID; it grants no authority and makes no state assertion. The server enforces session ownership through that affined request because authorization events carry no independent session ID. The SDK compares the first authoritative frame with the handle's authorization ID, requires a non-empty original call ID, requires the repeated original resolution to retain both original IDs, pins the learned continuation run ID for every continuation frame, and validates a chained authorization as a different authorization with its own non-empty call ID. Manual and automatic controls use only observed continuation-run and ask IDs. The server remains definitive for ownership, pending correlation, status, expiry, authorization grant/denial, permission effects, continuation admission, and post-disconnect outcome. The SDK returns only an HTTP(S) presentation string, never opens it, and routes continuation mutations through exact-run `RunControls` without choosing a verdict. Automatic permission controls use only the separately supplied `permissionRequestOptions`; flow request options are not silently reused as mutation authority. +- **Compatibility / migration:** Additive hand-written TypeScript SDK minor surface: root, Node, and Deno exports gain `McpAuthorization`, `McpAuthorizationFlow`, `McpAuthorizationFlowOptions`, `McpAuthorizationOperation`, `McpAuthorizationResult`, `McpAuthorizationStatus`, `RunOutcome`, `RunCompletedOutcome`, `RunAuthorizationRequiredOutcome`, and `RunAuthorizationRequiredError`. Existing `Run.result()` remains completed-only, returns `RunResult`, and stays source-compatible, but a valid authorization park changes from an erroneous `ProtocolError` to `RunAuthorizationRequiredError`; consumers that want both valid outcomes migrate to additive `Run.outcome()`. Iteration over a parked Run now closes cleanly and releases the session. An exact-run `AttachedRun` also treats a valid pending `authorization.required` for its run as terminal: it yields and checkpoints the event, marks `live` false, clears pending asks, and closes just as it does for `result`. Session-wide activity remains open. Existing `Event`, `RunControls`, `McpInventory`, raw catalog, and `./gen` APIs otherwise remain source-compatible. Status-only flows work against servers that implement the existing authorization RPCs; permission resolution and explicit continuation cancellation additionally require their existing `prompt_free_controls` feature. API reports, generated reference, public guidance, an executable example, and the automated SDK changelog path record the addition. + +The proposed public TypeScript surface is exact: + +```ts +type McpAuthorizationStatus = + | "pending" + | "granted" + | "denied" + | "cancelled" + | "expired" + | "interrupted" + | "failed" + | "closed"; + +type McpAuthorizationOperation = "recheck" | "cancel"; + +interface McpAuthorizationFlowOptions { + onPermissionAsk?: PermissionAskResponder; + permissionRequestOptions?: RequestOptions; +} + +type McpAuthorizationResult = + | { + readonly outcome: "pending"; + readonly status: "pending"; + readonly authorization: EventOf<"authorization.required">; + } + | { + readonly outcome: "settled"; + readonly status: Exclude; + readonly authorization: EventOf<"authorization.resolved">; + } + | { + readonly outcome: "completed"; + readonly status: Exclude; + readonly authorization: EventOf<"authorization.resolved">; + readonly continuationRunId: string; + readonly continuation: RunResult; + } + | { + readonly outcome: "authorization_required"; + readonly status: Exclude; + readonly authorization: EventOf<"authorization.resolved">; + readonly continuationRunId: string; + readonly nextAuthorization: EventOf<"authorization.required">; + }; + +interface RunCompletedOutcome { + readonly outcome: "completed"; + readonly result: RunResult; +} + +interface RunAuthorizationRequiredOutcome { + readonly outcome: "authorization_required"; + readonly sessionId: string; + readonly runId: string; + readonly authorization: EventOf<"authorization.required">; +} + +type RunOutcome = RunCompletedOutcome | RunAuthorizationRequiredOutcome; + +interface Run { + outcome(): Promise; + result(): Promise; +} + +class RunAuthorizationRequiredError extends InvalidStateError { + readonly outcome: RunAuthorizationRequiredOutcome; +} + +interface McpAuthorizationFlow extends AsyncIterable { + readonly sessionId: string; + readonly authorizationId: string; + readonly operation: McpAuthorizationOperation; + readonly continuationRunId: string | undefined; + resolveAsk( + askId: string, + verdict: PermissionVerdict, + requestOptions?: RequestOptions, + ): Promise; + cancelContinuation(requestOptions?: RequestOptions): Promise; + result(): Promise; +} + +interface McpAuthorization { + readonly sessionId: string; + readonly authorizationId: string; + presentation(requestOptions?: RequestOptions): Promise; + recheck( + options?: McpAuthorizationFlowOptions, + requestOptions?: RequestOptions, + ): McpAuthorizationFlow; + cancel( + options?: McpAuthorizationFlowOptions, + requestOptions?: RequestOptions, + ): McpAuthorizationFlow; +} + +interface Session { + mcpAuthorization(authorizationId: string): McpAuthorization; +} +``` + +`Run.outcome()`, `Run.result()`, and event iteration are three mutually exclusive +ways to claim the same Run. `Run.result()` remains a completed-only convenience; +when the consumed run parks, its `RunAuthorizationRequiredError.outcome` carries +the same detached handoff that `Run.outcome()` would have returned. + +## In scope — 6 scenarios, in implementation order + +### Scenario 1 — an ordinary Run hands off a parked authorization normally + +The SDK reflects the engine's documented non-result +`RunOutcomeAuthorizationPending` rather than treating that valid lifecycle as a broken +stream; see [architecture](../architecture.md#typescript-sdk). + +**Acceptance:** +- AC1.1: `Run.outcome()` drains one claimed Run and returns `RunCompletedOutcome` for exactly one terminal `result`, or `RunAuthorizationRequiredOutcome` when the final event is `authorization.required` with status `pending`, a non-empty authorization/call ID, and the Run's exact non-empty session/run correlation. + - verify: vitest:sdk/typescript/test/run.test.ts — `Run outcome discriminates completion from authorization parking` +- AC1.2: Event iteration yields the valid final `authorization.required` and then closes normally; the completed-only `Run.result()` releases the stream and throws `RunAuthorizationRequiredError` carrying the same detached outcome, never `ProtocolError`. + - verify: vitest:sdk/typescript/test/run.test.ts — `authorization parked Run iteration and result use normal handoff semantics` +- AC1.3: Authorization-park EOF, `outcome()`, `result()`, and iterator return after the parked event close the underlying response iterator, unregister the Run, clear `SessionImpl`'s busy state, and do not cancel or resolve the server's pending authorization; the same Session can immediately create its lifecycle handle. + - verify: vitest:sdk/typescript/test/run.test.ts — `authorization parked Run releases SDK ownership without cancelling authorization` +- AC1.4: `outcome()`, `result()`, and iteration are mutually exclusive single-consumption modes; completed runs retain their existing `RunResult`, and EOF without either a terminal `result` or final valid authorization requirement remains `ProtocolError`. + - verify: vitest:sdk/typescript/test/run.test.ts — `Run outcome preserves completed and malformed stream behavior` + +### Scenario 2 — a session-bound handle presents one live authorization + +The handle extends the established [TypeScript SDK session architecture](../architecture.md#typescript-sdk) +without importing mecatui state or widening the thin MCP inventory namespace. + +**Acceptance:** +- AC2.1: `session.mcpAuthorization(authorizationId)` rejects an empty authorization ID locally; otherwise it synchronously returns an `McpAuthorization` with exact readonly session/authorization IDs and performs no compatibility probe, RPC, stream open, durable watch, or state assertion during construction. + - verify: vitest:sdk/typescript/test/mcp-authorization.test.ts — `MCP authorization handle binds exact correlation without I/O` +- AC2.2: `presentation(requestOptions?)` makes one existing presentation RPC with automatic exact session affinity, preserves caller headers, callbacks, signal, and deadline, and returns the server's absolute HTTP(S) URL string without caching, opening, copying, rendering, or persisting it. + - verify: vitest:sdk/typescript/test/mcp-authorization.test.ts — `MCP authorization presentation is live validated and application owned` +- AC2.3: An absent, relative, non-HTTP(S), or otherwise malformed presentation URL raises `ProtocolError`; server ownership, unknown/past authorization, expiry, lease, and availability refusals retain their normalized typed errors without exposing another session or credential. + - verify: vitest:sdk/typescript/test/mcp-authorization.test.ts — `MCP authorization presentation preserves protocol and server failures` + +### Scenario 3 — recheck and cancel are lazy correlated single-consumption flows + +Each operation consumes the existing stateful stream described by +[ADR 0348](../adr/0348-typescript-sdk-mcp-authorization-lifecycle.md), not a generic +namespace response or TUI phase machine. + +**Acceptance:** +- AC3.1: `recheck()` and `cancel()` synchronously return distinct flows with exact immutable session/authorization/operation values but perform no compatibility probe, registration, RPC, mutation, or timer start until the first iterator `next()` or `result()`; merely requesting an iterator claims it but remains transport-lazy. + - verify: vitest:sdk/typescript/test/mcp-authorization.test.ts — `MCP authorization operations start only on first consumption` +- AC3.2: First consumption registers one client-owned stream, starts `timeoutMs`, observes an already-aborted caller signal before transport work, and issues exactly the named existing descriptor with one initial gRPC frame carrying only the handle's session/authorization IDs or the equivalent bodyless HTTP route. Establishment/server errors and header callbacks surface from that consuming `next()` or `result()`, not from flow construction. + - verify: vitest:sdk/typescript/test/mcp-authorization.test.ts — `MCP authorization flow start preserves request timing and exact control` +- AC3.3: The first authoritative frame is a known authorization event with empty `runId`, the exact authorization ID, non-empty call ID, a known status, and required-`pending` versus resolved-terminal pairing; every mismatch, missing payload, unknown status, or malformed frame raises `ProtocolError`. + - verify: vitest:sdk/typescript/test/mcp-authorization.test.ts — `MCP authorization flow validates the authoritative control result` +- AC3.4: Clean EOF after only the authoritative event returns discriminated `pending` or `settled`; denial, cancellation, expiry, interruption, failure, and closure are typed values rather than exceptions, and impossible event/status/result combinations are not representable by `McpAuthorizationResult`. + - verify: vitest:sdk/typescript/test/mcp-authorization.test.ts — `MCP authorization status-only results are discriminated values` +- AC3.5: Iteration and `result()` are mutually exclusive and claim the flow once; a second iterator, a second `result()`, or cross-mode consumption raises `InvalidStateError` without opening or consuming another stream. + - verify: vitest:sdk/typescript/test/mcp-authorization.test.ts — `MCP authorization flow is single consumption` + +### Scenario 4 — a continuation completes or hands off one chained authorization + +Continuation control reuses the exact-run resource approved by +[ADR 0347](../adr/0347-run-id-addressed-prompt-free-controls.md), preserving one +ergonomic behavior over gRPC and HTTP. + +**Acceptance:** +- AC4.1: The first post-status event fixes one non-empty `continuationRunId`; every continuation event retains it, and the continuation contains exactly one repeated copy of the original resolved authorization payload before it closes. A changed run ID, missing/duplicate original resolution, second result, or event after a terminal result is `ProtocolError`. + - verify: vitest:sdk/typescript/test/mcp-authorization.test.ts — `MCP authorization continuation validates run and repeated resolution grammar` +- AC4.2: Clean continuation EOF after exactly one terminal `result` returns `outcome: "completed"` with the ordinary `RunResult`; iteration yields every decoded event in wire order, and the lifecycle fabricates no `Run`, attachment, cursor, or successor. + - verify: vitest:sdk/typescript/test/mcp-authorization.test.ts — `MCP authorization continuation returns one ordinary completed result` +- AC4.3: A continuation may instead end with one later `authorization.required` carrying status `pending`, a non-empty call ID, the same continuation run ID, and an authorization ID different from the control's original ID. Clean EOF then returns `outcome: "authorization_required"` with that exact `nextAuthorization`; EOF with neither a result nor this chained park remains `ProtocolError`. + - verify: vitest:sdk/typescript/test/mcp-authorization.test.ts — `MCP authorization continuation hands off a chained authorization` +- AC4.4: `onPermissionAsk` receives only an observed ordinary ask plus a lifecycle-bound signal. Its explicit verdict uses only `permissionRequestOptions`; omission or abstention leaves the ask pending. Manual `resolveAsk()` uses only its own request options. Both address the exact observed ask/run through `RunControls`, while unknown, resolved, retracted, plan-originated, or mismatched asks are never guessed. + - verify: vitest:sdk/typescript/test/mcp-authorization-controls.test.ts — `MCP authorization permission decisions and request options remain application owned` +- AC4.5: `cancelContinuation()` addresses only the observed continuation run through `RunControls.cancel`; either manual control before its required run/ask is observed fails locally, and absent `prompt_free_controls` fails with the existing typed feature refusal without cancelling or resolving something else. + - verify: vitest:sdk/typescript/test/mcp-authorization-controls.test.ts — `MCP authorization continuation controls are exact run and feature gated` +- AC4.6: Stream, manual-control, and automatic-control request options independently preserve headers, callbacks, signals, per-request deadlines, session affinity, client-close state, normalized errors, and no-retry behavior on both transports; a plan-originated ask is yielded but requires an existing separate plan workflow or explicit continuation cancellation. + - verify: vitest:sdk/typescript/test/mcp-authorization-controls.test.ts — `MCP authorization request options and unsupported plan asks stay separated` + +### Scenario 5 — concurrency, cancellation, and recovery remain explicit + +The SDK preserves the server-owned lifecycle and durable observation boundaries in the +[architecture](../architecture.md#typescript-sdk); it does not invent a client-side +authorization truth store. + +Flow termination has one observable contract: + +| End cause | Pending `next()` or `result()` | Responder and control lifetime | Later manual controls | SDK cleanup | +|---|---|---|---|---| +| Valid status-only or continuation EOF | `result()` resolves to the typed result; a pending or later iterator `next()` resolves with `{ done: true }` | Pending responders and automatic controls are aborted; late verdicts are ignored | Reject locally with `InvalidStateError` | Close the response iterator and release the registration, timer, and caller listener once | +| Explicit iterator `return()` | The pending `next()` and `return()` resolve with `done: true` | Pending responders and automatic controls are aborted; late verdicts are ignored | Reject locally with `InvalidStateError` | Close and release once without a server mutation | +| Caller signal | Rejects once, preserving a caller-supplied `MecatlError` or otherwise using the normalized caller-cancellation `TransportError`; later `next()` is done | Pending responders and automatic controls are aborted; late verdicts are ignored | Reject locally with `InvalidStateError` | Close and release once without replay or cancellation | +| Deadline | Rejects once with `ServerError` whose `status === Code.DeadlineExceeded`; later `next()` is done | Pending responders and automatic controls are aborted; late verdicts are ignored | Reject locally with `InvalidStateError` | Close and release once and clear the timer | +| Transport loss | Rejects once, preserving an incoming `MecatlError` or otherwise using the normalized `TransportError`; later `next()` is done | Pending responders and automatic controls are aborted; late verdicts are ignored | Reject locally with `InvalidStateError` | Close and release once without retry | +| Client close | Rejects once with the client's existing `InvalidStateError`; later `next()` is done | Pending responders and automatic controls are aborted; late verdicts are ignored | Reject locally with `InvalidStateError` | Close and release once with the client | + +A manual control admitted before termination retains its caller-owned request lifetime +through setup and dispatch. Ending the flow neither aborts, replays, nor converts that +already-admitted request into an automatic control; an invocation admitted after +termination fails locally without starting an RPC. + +**Acceptance:** +- AC5.1: Concurrent flows from one or more handles own independent iterators, request controls, pending-ask maps, registrations, and abort lifetimes. Session ownership is server-enforced through each session-affined request. The SDK rejects an authoritative event whose authorization ID differs from the handle, learns its non-empty original call ID from that frame, requires the repeated original resolution to retain both original IDs, pins one non-empty continuation run ID for every continuation frame, and accepts a chained pending authorization only when it has a different authorization ID and its own non-empty call ID. Controls address only observed continuation-run and ask IDs. No flow consumes another flow's iterator or pending ask. + - verify: vitest:sdk/typescript/test/mcp-authorization-recovery.test.ts — `concurrent MCP authorization flows cannot cross consume or correlate` +- AC5.2: Valid EOF, caller signal, deadline, iterator return while `next()` is pending, transport loss, and client close follow the termination matrix exactly. Every path aborts pending responders and flow-owned automatic controls, suppresses late verdicts, rejects post-terminal `resolveAsk()` and `cancelContinuation()` locally without another RPC, preserves the caller-owned lifetime of a manual control admitted before termination, releases registration and transport resources once, and performs no automatic recheck, mutation replay, browser action, credential persistence, or claim about committed server state. + - verify: vitest:sdk/typescript/test/mcp-authorization-recovery.test.ts — `MCP authorization flow cancellation releases only SDK owned resources` +- AC5.3: A fresh recheck after loss is a new one-shot mutation, not replay or guaranteed recovery: it can proceed only while the same authorization remains pending. If the lost control committed and cleared pending state before the caller observed its status or continuation ID, the server's not-found refusal is preserved and this lifecycle alone cannot reconstruct the lost outcome. + - verify: vitest:sdk/typescript/test/mcp-authorization-recovery.test.ts — `MCP authorization recovery never overpromises replay` +- AC5.4: A caller that observed `continuationRunId`, or a deployment retaining suitable session activity, may explicitly inspect activity and attach to a still-observable run. After disconnect, exact-run attachment treats a replayed `authorization.required` as a valid park only when it carries the exact attached run ID, `pending` status, and non-empty authorization and call IDs. Attachment yields and checkpoints that event, marks `live` false, clears pending asks, and closes without waiting for a nonexistent `result`; session-wide activity remains open. The lifecycle itself never opens a durable watch, scans activity, reconnects, or guarantees event-log retention. + - verify: vitest:sdk/typescript/test/mcp-authorization-recovery.test.ts — `disconnect attach replays chained authorization park as terminal` +- AC5.5: Real-server tests pin phase-specific disconnect behavior: gRPC detaches and drains ordinary continuation work but cancels a run stranded on an ordinary permission ask; HTTP requests cancellation for a still-active continuation and drains it; neither path destroys a follow-up authorization after its `authorization.required` park has committed, and terminal races remain server-authoritative. + - verify: vitest:sdk/typescript/e2e/mcp-authorization.e2e.test.ts — `MCP authorization disconnect follows transport and park phase` + +### Scenario 6 — public and real-wire coverage makes the workflow usable + +The API is release surface under [ADR 0304](../adr/0304-typescript-sdk-public-surface-and-release.md) +and must work against the same-checkout daemon, not only injected transports. + +**Acceptance:** +- AC6.1: The HTTP RPC catalog sends no body for recheck/cancel, declares response field `event`, and the generic HTTP SSE decoder wraps each bare server event for both descriptors. Injected plus real-wire gRPC TCP, gRPC UDS, and HTTP/SSE tests cover initial Run parking, presentation, pending recheck, granted/denied/cancelled resolution, completed and chained continuations, permission allow/deny, explicit continuation cancellation, request options, typed errors, and exact affinity with equivalent high-level results where server semantics coincide. + - verify: vitest:sdk/typescript/e2e/mcp-authorization.e2e.test.ts — `MCP authorization works over gRPC TCP UDS and HTTP SSE` +- AC6.2: Root, Node, and Deno declarations export the exact interface contract; API Extractor reports, package tests, the generated SDK reference, and the runtime import matrix prevent an entry point or type from drifting. + - verify: vitest:sdk/typescript/test/package.test.ts — `MCP authorization lifecycle is exported documented and API reviewed` +- AC6.3: A concise package-export-only example shows initial Run handoff, application-owned URL handling, explicit recheck cadence, permission response, all discriminated results, chained authorization, and bounded recovery without opening a browser or copying mecatui policy. + - verify: vitest:sdk/typescript/test/examples.test.ts — `MCP authorization example uses only the public lifecycle` +- AC6.4: TSDoc, TypeScript SDK permissions/sessions guidance, architecture, implementation notes, generated reference, and the automated SDK changelog describe ownership, Run parking, states, single consumption, lazy dispatch, affinity, request cancellation, no-retry polling, recovery limits, phase-specific HTTP/gRPC disconnect behavior, permission authority, and URL secrecy. + - verify: inspection — public documentation and generated API/changelog artifacts are content/build outputs rather than runtime behavior + +## Out of scope + +| Item | Defer-to | Decision | +|---|---|---| +| Browser opening, clipboard, rendering, instructions, or polling cadence | Application UI | The SDK returns the live URL and one-shot controls only. | +| Client credential or authorization-state persistence | Server/application storage | The SDK owns no credential store or authorization truth. | +| TUI phase, timers, automatic retry count, or generation state | `cmd/mecatui` | Do not make one client's presentation policy the SDK contract. | +| New authorization RPCs, events, statuses, error codes, or feature flags | Future server contract if evidence requires it | Reuse the shipped server protocol and open compatibility/error machinery. | +| Automatic continuation attachment or transparent stream reconnect | Explicit application recovery | Callers choose whether and where to persist and resume observation. | +| Changing server HTTP disconnect semantics | Separate server lifecycle decision | Document and test the current transport distinction rather than hiding it in the SDK. | +| A generic authorization namespace or raw descriptor aliases | Existing `./gen` and raw catalog | The ergonomic workflow is session-bound; raw access remains available. | +| A new prompt-free plan-approval control | Separate server/API decision | A lifecycle continuation yields a plan-originated ask but does not reinterpret it as an ordinary permission or add transport-specific authority. | +| Changes to `RunControls` or ordinary permission semantics | Existing SDK contracts | Reuse those exact controls and verdicts without widening their authority. | + +## Definition of done + +1. `task lint`, `task test`, `task docs`, `task site:build`, and `task api:check` pass. +2. `task sdk:lint`, `task sdk:typecheck`, `task sdk:test`, `task sdk:build`, `task sdk:api:check`, `task sdk:docs:check`, `task sdk:examples:typecheck`, `task sdk:deno`, and `task sdk:e2e` pass. +3. `task ac-trace-strict` resolves every named proof when the plan becomes `landed`. +4. `go run ./cmd/mecademo` remains green. +5. API reports, generated SDK reference, package-export example, architecture, implementation notes, user guides, and the automated SDK changelog path are updated together. +6. The implementation PR links the Plan / Interface PR and approved commit and reports interface conformance. +7. `/panel-review` reports no ship blockers or unwaived reviewer failures. + +## Deferred decisions and known risks + +- A control can commit before its response is lost. The SDK cannot safely replay that mutation and may not learn a continuation run ID. A new recheck succeeds only if the original authorization is still pending; otherwise recovery depends on separately retained session activity and may be impossible. +- The current gRPC and HTTP server relays deliberately differ while a continuation is active after client disconnect. Both preserve a follow-up authorization that has already parked. The high-level API is transport-neutral for successful controls and explicit exact-run mutations, but cannot promise identical effects for an abruptly lost response without a separate server decision. +- Authorization status is a closed server state machine carried in a string field. The ordinary event union keeps the raw string open, while the lifecycle fails closed on an unknown status; a future status addition therefore requires an additive SDK update. +- A status-only flow can work without prompt-free controls, but a continuation that asks permission cannot progress through the transport-neutral high-level API on a server that lacks `prompt_free_controls`. +- A plan-originated ask has no prompt-free transport-parity control today. The lifecycle yields it but does not let `onPermissionAsk` or `resolveAsk()` misclassify it; application policy or explicit cancellation must handle the resulting park. diff --git a/docs/adr/0348-typescript-sdk-mcp-authorization-lifecycle.md b/docs/adr/0348-typescript-sdk-mcp-authorization-lifecycle.md new file mode 100644 index 0000000000..781b8169a7 --- /dev/null +++ b/docs/adr/0348-typescript-sdk-mcp-authorization-lifecycle.md @@ -0,0 +1,275 @@ +# ADR 0348 — TypeScript SDK MCP authorization lifecycle + +- Status: Proposed +- Date: 2026-09-18 +- Scope: `sdk/typescript/` MCP authorization ergonomics over the existing HarnessService presentation, recheck, cancel, run-control, and event contracts +- Supersedes: ADR 0304 Decision 3 only for admitting an MCP authorization lifecycle as another justified ergonomic resource; Team and RunControls remain unchanged +- Superseded by: none + +## Context + +The server already owns the complete MCP authorization transition. An agent run can +emit `authorization.required` with safe session-local correlation, while +`GetMcpAuthorizationPresentation` returns the live browser URL and the +`RecheckMcpAuthorization` and `CancelMcpAuthorization` streams report the authoritative +status and any continuation run. The generated TypeScript surface and both raw +transports can reach those operations, but an SDK consumer must currently assemble +descriptors, manufacture the bidirectional first frame, decode the event stream, and +correlate its continuation by hand. + +That choreography is stateful. A recheck that observes `pending` has no continuation. +A granted, denied, cancelled, expired, interrupted, failed, or closed authorization may +start a continuation run that repeats the control result once among its events. +That run may emit ordinary permission asks and either reach one terminal result or park +again on a different `authorization.required`. The first control result itself has no +run ID, so a client learns the optional continuation ID only from the stream. + +The same non-result terminal shape already exists before this lifecycle begins. A normal +`Session.run()` may publish `authorization.required` and close with the engine's durable +`RunOutcomeAuthorizationPending`. The current `RunImpl` instead requires every stream to +end in `result`, raises `ProtocolError` for the valid park, and can retain the Session's +local busy flag when iteration returns after the handoff. An ergonomic authorization API +must first make that existing Run outcome representable and release its transport +resources without cancelling the newly pending authorization. + +The transports also have different control mechanics and response envelopes. gRPC can +send permission and cancel frames on the bidirectional stream. The HTTP mirror is one +bodyless request with an SSE response, emits each `Event` bare rather than inside the +descriptor's `{event}` envelope, and has no reverse channel. The additive prompt-free run +controls from [ADR 0347](./0347-run-id-addressed-prompt-free-controls.md) provide one safe +common control path after the continuation run ID is known. + +ADR 0304 favored thin namespaces and originally admitted only Team as a new ergonomic +resource. MCP authorization now has the same reasons for an exception: durable +correlation, a stateful stream, application-supplied permission decisions, optional +continuation ownership, and single-consumption rules cannot be represented honestly by +a generated response type or an unstructured namespace promise. + +## Decision + +### 1. Bind authorization to the existing Session resource + +Add `Session.mcpAuthorization(authorizationId): McpAuthorization`. Construction is +synchronous and local. The handle stores only the session-affined SDK operations and the +exact non-empty authorization ID; it performs no request, opens no stream, and asserts no +authorization state. + +Do not add a second top-level authorization namespace or put a session ID back into each +method. The existing `Session` is the SDK's durable owner-scoped resource and already +applies the exact affinity hint to every operation. `client.mcp` remains the thin MCP +inventory namespace established by ADR 0304. + +### 2. Make an authorization-parked Run a normal handoff + +Add `Run.outcome(): Promise` as a third mutually exclusive +single-consumption mode beside iteration and the existing completed-only `result()`. +`RunOutcome` discriminates `completed`, which carries the unchanged `RunResult`, from +`authorization_required`, which carries the exact `authorization.required` event plus +its session and run correlation. + +Iteration yields a valid final authorization requirement and then closes normally. +`result()` keeps its source-compatible `Promise` signature, but a parked Run +raises `RunAuthorizationRequiredError` carrying the same typed outcome instead of +misreporting `ProtocolError`. EOF with neither one terminal result nor one final valid +authorization requirement remains a protocol error. + +Every parked completion path closes the underlying response iterator, unregisters the +Run, and releases `SessionImpl`'s busy flag. Iterator return after observing the park does +the same. Releasing this SDK ownership does not send cancellation or alter the server's +pending authorization, so the same Session can immediately create an authorization +handle. + +### 3. Keep URL presentation separate and application-owned + +`McpAuthorization.presentation(requestOptions?)` performs exactly one +`GetMcpAuthorizationPresentation` call and returns the validated absolute HTTP(S) URL as +a string. It does not cache the URL, open a browser, copy to a clipboard, render +instructions, persist anything, or infer success. A malformed or absent URL is a +`ProtocolError`; owner, pending-state, expiry, and availability refusals remain the +server's typed errors. + +### 4. Each control call creates one lazy single-consumption flow + +`McpAuthorization.recheck(flowOptions?, requestOptions?)` and +`McpAuthorization.cancel(flowOptions?, requestOptions?)` each return a distinct +`McpAuthorizationFlow`. The methods do not poll or retry. Repeated rechecks are explicit +new flows, so the application chooses timing and backoff and concurrent calls cannot +share an iterator or control queue accidentally. + +The flow binds immutable `sessionId`, `authorizationId`, and `operation` properties. It +is an `AsyncIterable` and also exposes `result()`. As with `Run`, callers choose +iteration or `result()` exactly once. A second claim, including a second iterator, raises +`InvalidStateError` before consuming another frame. + +Flow construction and iterator acquisition are transport-lazy. The first iterator +`next()` or `result()` starts the compatibility check, client stream registration, +request timeout, and exactly one control RPC. An already-aborted caller signal prevents +transport work. Stream establishment and server errors, plus header callbacks, surface +from that first consuming operation; an unconsumed flow performs no server mutation and +owns no registered transport resource. The operation's `RequestOptions` apply only to +that stream, not to later permission mutations. + +### 5. Validate a closed control result and one optional continuation + +The first decoded frame must be `authorization.required` or +`authorization.resolved`, carry the handle's exact authorization ID, a non-empty call +ID, an empty run ID, and one status from the closed `McpAuthorizationStatus` union: +`pending | granted | denied | cancelled | expired | interrupted | failed | closed`. +`pending` pairs with `authorization.required`; every other value pairs with +`authorization.resolved`. A mismatch is a `ProtocolError`, not a different lifecycle. + +EOF after only that authoritative status is valid and means there was no continuation. +`result()` returns a discriminated `pending` result for `authorization.required`, or a +`settled` result for a terminal `authorization.resolved` without a continuation. +Server-declared denial and other terminal statuses are values, not thrown errors. + +If another frame follows, its first non-empty run ID fixes `continuationRunId`, and every +continuation frame must retain it. The continuation must contain exactly one repeated +copy of the original resolved authorization payload, including the original authorization +and call IDs. Other continuation events are correlated by the pinned run ID rather than +the original call ID. It then ends in exactly one of two +ways: one terminal `result` with no following event, returned as `outcome: "completed"` +with an ordinary `RunResult`; or a different `authorization.required` with pending +status, its own non-empty call ID, and the continuation run ID, returned after clean EOF as +`outcome: "authorization_required"` with `nextAuthorization`. Missing or duplicate +original resolution, a changed run ID, a second result, a same-ID or malformed chained +authorization, an event after a terminal result, or continuation EOF without either +valid ending is `ProtocolError`. + +This four-way discriminated result prevents impossible independent status/event/result +combinations. Iteration still yields the existing decoded `Event` union in wire order. +Transport, protocol, and server failures remain errors. The ordinary exported `Event` +union keeps its open string status for forward-compatible raw observation; only this +lifecycle validates the server's currently closed state machine. + +### 6. Reuse prompt-free exact-run controls for transport parity + +`McpAuthorizationFlowOptions` contains the existing `onPermissionAsk` responder plus +`permissionRequestOptions`, used only for automatic verdict mutations. The flow exposes +`resolveAsk(askId, verdict, requestOptions?)` for manual resolution and +`cancelContinuation(requestOptions?)` for explicit continuation cancellation. Manual +calls use only their own options. Both controls are available only after the flow has +observed the exact continuation run and the relevant ask where applicable. + +Both gRPC and HTTP use `Session.controls(continuationRunId)` for these mutations. The +SDK deliberately does not expose the gRPC-only reverse stream as a second public +semantics. This preserves exact-run stale guards, request options, typed errors, and one +behavior across transports. A server without `prompt_free_controls` may still report a +status-only authorization result, but a continuation that needs a permission decision +or explicit cancellation fails with the existing typed unsupported-feature error. + +Manual and automatic controls address only the observed continuation run and ask IDs. +A manual control admitted before flow termination retains its caller-owned request lifetime +through setup and dispatch. Invocations after termination fail locally without starting an +RPC. + +The responder is application policy. `undefined` leaves an ask pending for manual +resolution; `deny`, `allow_once`, and `allow_always` are sent to the server unchanged. +The SDK never infers a verdict, upgrades a denial, or treats an authorization status as +permission authority. A plan-originated ask is yielded but is never given to this +ordinary responder or accepted by `resolveAsk()`; it requires an existing separate plan +workflow where applicable or explicit continuation cancellation. This ADR adds no new +prompt-free plan-approval authority. + +### 7. Request cancellation is not retry or guaranteed recovery + +The `RequestOptions` supplied to `presentation`, `recheck`, and `cancel` retain headers, +callbacks, deadlines, caller signals, client-close cancellation, and automatic session +affinity. A request abort ends that request. A caller-supplied `MecatlError` is preserved; +other caller aborts and transport losses are normalized to `TransportError`, deadlines +reject with `ServerError` whose `status === Code.DeadlineExceeded`, and client close uses +the client's existing `InvalidStateError`. It does not cause an automatic recheck, cancel, +durable watch, or browser action. + +Valid EOF resolves `result()` to its typed result and resolves a pending or later iterator +`next()` with `{ done: true }`. Explicit iterator `return()` also completes a pending read +cleanly. Every terminal path closes the response iterator, registration, timer, and caller +listener once; aborts responders and flow-owned automatic controls; suppresses late +verdicts; and makes later manual controls fail locally. An already-admitted manual control +keeps only its caller-owned request lifetime. + +The existing server remains authoritative for what happened before a disconnect. The +authorization-control relay makes the gRPC distinction explicit: it detaches and drains +ordinary runnable work after losing its control stream, but cancels the exact continuation +when control-stream EOF strands it on an ordinary permission ask. HTTP +requests cancellation for a still-active continuation when its SSE request is lost and +drains the result. Neither transport destroys a follow-up authorization after that Run +has already committed its authorization park, because cancellation is then inert. The +SDK documents these phase-specific effects rather than claiming one transport-neutral +disconnect outcome. + +Once a continuation run ID was observed, an application can use the ordinary durable +`Session.attach(runId)` and activity APIs where supported. Exact-run attachment treats a +replayed `authorization.required` as a terminal park only when it carries the attached run +ID, `pending` status, and non-empty authorization and call IDs. It yields and checkpoints +that event, marks the attachment not live, clears pending asks, and closes without waiting +for a `result`; session-wide activity remains open. Suitable retained session activity may +also reveal correlation after loss, but this lifecycle neither scans nor guarantees that +log. Before the ID or authoritative status is observed, a lost mutation +is ambiguous and may be unrecoverable: a fresh exact-correlation recheck is a new +one-shot control that succeeds only if the original authorization remains pending. If +the prior control committed and cleared it, the same-ID recheck returns the server's +not-found refusal rather than replaying the committed result. No control is +automatically replayed. + +### 8. Reuse the wire and correct both HTTP classifications + +No protobuf wire-shape, Go API, event, snapshot, persistence, feature, or error-code +changes. The gRPC authorization-control relay implements the disconnect behavior in +Decision 7 without adding a method or message: EOF while the continuation is parked on an +ordinary permission ask cancels that exact run, while EOF during runnable work leaves it +detached to drain. The `Converse` source comment is corrected to document closure after +either a terminal `result` or a pending `authorization.required` park, and regenerated +bindings carry that comment. The SDK continues to invoke the three existing HarnessService descriptors. +The RPC catalog classifies the HTTP recheck and cancel routes as `requestBody: "none"`, +matching the server's correlation-only path contract; the current `"json"` +classification incorrectly serializes `{}` and must be corrected. Those rows also set +`responseField: "event"`, and the generic HTTP stream decoder applies that field to each +bare SSE document before protobuf decoding. Converse's existing special-case wrapper is +therefore no longer the only HTTP stream capable of decoding a bare server `Event`. + +The root, Node, and Deno entry points export the new lifecycle types. API Extractor, +generated reference, executable examples, architecture, implementation notes, public +SDK guidance, and the automated SDK changelog path remain the compatibility gates. + +## Consequences + +Applications receive a discoverable path from an `authorization.required` event to a +presentation URL and a typed recheck/cancel lifecycle without importing generated +descriptors. The session-bound handle removes repeated correlation parameters, while a +new flow per control prevents unrelated operations from sharing consumption state. + +Ordinary Run consumers can treat authorization parking as a normal outcome and release +the Session immediately. The additive `outcome()` path preserves `result()` for +completed-only callers, at the cost of another Run consumption method and a new typed +error for callers that use that narrower convenience on a parked Run. + +The SDK gains two related resource types instead of stretching the MCP inventory +namespace into a state machine. This is a deliberate second exception to ADR 0304's +original teams-only constraint, following ADR 0347's narrower RunControls exception. + +Status-only rechecks stay cheap and policy-free. Lazy dispatch means constructing or +requesting an iterator from a flow has no server effect; applications must call +`next()` or `result()`. Applications that want polling, browser launch, persistence, or +reconnect policy must implement those choices explicitly. + +Using prompt-free run controls avoids a gRPC-only ergonomic branch, but it makes those +controls a requirement only when an authorization continuation asks permission or needs +explicit cancellation. Automatic replies need their own declared request options, and +plan-originated asks remain outside this ordinary-permission surface. Existing raw +consumers may continue using the bidirectional frames directly. + +Disconnects can remain ambiguous because the two server transports differ while a +continuation is active, and terminal controls consume the pending correlation instead +of retaining a replayable outcome. The lifecycle exposes the continuation run ID as soon +as it is observed and documents phase-specific, best-effort durable reconciliation +rather than promising an unsafe replay. Chained authorization parking remains a normal +result and survives either transport's late cancellation behavior. + +## See also + +- [SDK MCP authorization acceptance plan](../acceptance/sdk-mcp-authorization-lifecycle.md) +- [ADR 0304 — TypeScript SDK public surface completeness](./0304-typescript-sdk-public-surface-and-release.md) +- [ADR 0347 — Run-ID-addressed prompt-free controls](./0347-run-id-addressed-prompt-free-controls.md) +- [TypeScript SDK architecture](../architecture.md#typescript-sdk) +- [Implementation notes](../design/IMPLEMENTATION-NOTES.md#typescript-sdk--sdktypescript-m1m4-public-surface-and-post-v010-deno-integration-adrs-0279-0288-0292-0304-0328-0337-0338-and-0339) diff --git a/docs/adr/README.md b/docs/adr/README.md index 953116d58a..d778e6204a 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -80,6 +80,7 @@ Documentation/citation conventions are in [`docs/design/README.md`](../design/RE - [0340 - Deno owns local daemons through Deno.Command](./0340-typescript-sdk-deno-command.md) *(supersedes ADR 0339 for the Deno public entry-point and local-process decisions)* - [0341 - Deno reuses the ConnectRPC gRPC transport](./0341-typescript-sdk-deno-grpc.md) *(supersedes ADRs 0339 and 0340 for the HTTP-only transport and Node-compatibility exclusions)* - [0347 — Run-ID-addressed prompt-free controls](./0347-run-id-addressed-prompt-free-controls.md) *(proposed; supersedes ADR 0288 Decision 6 and ADR 0304 Decision 3 in part)* +- [0348 — TypeScript SDK MCP authorization lifecycle](./0348-typescript-sdk-mcp-authorization-lifecycle.md) *(proposed; supersedes ADR 0304 Decision 3 only for the authorization-lifecycle resource)* - [0342 - Gate runs on unresolved live context windows](./0342-context-window-admission.md) *(supersedes ADR 0016 only for pre-swap run admission)* - [0346 - Prompt-cache breakpoints are protocol-native, never vendor-keyed](./0346-unified-prompt-cache-dialect.md) *(supersedes ADR 0100's prompt_cache_breakpoint deferral, its root cache_control dialect arm, and its OpenRouter TTL deferral; extends ADR 0334 to OpenRouter)* - [0036 — `engine/` is its own Go module (monorepo via `go.work`)](./0036-engine-module.md) diff --git a/sdk/typescript/src/gen/mecatl/v1/harness_pb.ts b/sdk/typescript/src/gen/mecatl/v1/harness_pb.ts index cee0020d33..7a9a996c3b 100644 --- a/sdk/typescript/src/gen/mecatl/v1/harness_pb.ts +++ b/sdk/typescript/src/gen/mecatl/v1/harness_pb.ts @@ -10286,9 +10286,10 @@ export const HarnessService: GenService<{ /** * Converse drives one run. The first frame MUST be `prompt` or `retry`; later * frames may carry controls. A received second `prompt` or `retry` is rejected - * with INVALID_ARGUMENT. The server streams `Event` envelopes until the terminal - * `result` event, then closes the stream; controls still in transit may instead - * observe normal stream completion. A context cancel from the client aborts the run. + * with INVALID_ARGUMENT. The server streams `Event` envelopes until either a + * terminal `result` or a pending `authorization.required` park, then closes the + * stream; controls still in transit may instead observe normal stream completion. + * A context cancel from the client aborts the run. * * @generated from rpc mecatl.v1.HarnessService.Converse */