diff --git a/.changeset/authenticate-app-parent-source.md b/.changeset/authenticate-app-parent-source.md new file mode 100644 index 000000000..0afa376d2 --- /dev/null +++ b/.changeset/authenticate-app-parent-source.md @@ -0,0 +1,5 @@ +--- +"agent-bundle": patch +--- + +Make `createAppClient()` authenticate unconfigured MCP App transports by exact parent source while preserving strict HTTP(S) `targetOrigin` pinning. (#779) diff --git a/docs/entry-conventions.md b/docs/entry-conventions.md index 3e8a4918f..f4d48eb66 100644 --- a/docs/entry-conventions.md +++ b/docs/entry-conventions.md @@ -1726,7 +1726,7 @@ client rather than by hand-written frames: `tests/serve-app.test.ts` connects relay uses, and the Workbench real-App E2E (`packages/workbench/tests/mcp-app-real.e2e.test.ts`) compiles a fixture view on `createAppClient` and reads its `call()` result through the relay. The -client's own contract — envelopes, handshake, pinning, dispatch, cancellation, +client's own contract — envelopes, handshake, transport authentication, dispatch, cancellation, rebind, disposal — is proven in `tests/app-client.test.ts` over injected ports. The client never decides which server a call reaches or which capability needs consent. @@ -1742,8 +1742,8 @@ capability needs consent. | `request(method, params?, options?)` | The typed JSON-RPC escape hatch for `resources/read` and supported `ui/*` methods; resolves the raw result. An empty method rejects with a `TypeError`. | | `onToolInput(routeId, listener)` / `onToolResult(routeId, listener)` / `onToolError(routeId, listener)` | The opening call's `ui/notifications/tool-input` arguments, the decoded `structuredContent` of a successful `ui/notifications/tool-result`, and that notification's failures as an `AppClientError` — `isError: true` is `rpc` with the whole result on `data`; a malformed envelope or one without an object `structuredContent` is `invalid-message`; a failed result never reaches `onToolResult`. The notifications carry no tool name, so dispatch keys on the tool the handshake named: `hostContext.toolInfo.tool.name` from the initialize result, matched against the final segment of each registered route id. Listeners for other tools stay silent; when the initialize result names no tool, `tool-input` and `tool-result` reach no listener. Listeners run on a microtask, exceptions dropped. Each returns its unsubscribe function. | | `onToolCancelled(listener)` | `ui/notifications/tool-cancelled` as `{ reason? }`, unfiltered; returns its unsubscribe function. | -| `rebind({ parent?, targetOrigin?, window? })` | Bumps the connection generation and rejects the previous generation's pending requests with `connection-rebound` — a `connect()` still in flight included; its late response can never become the live connection — clears the pinned origin and the opening tool name, moves the message listener when `window` changes, adopts the new parent, keeps the configured `targetOrigin` unless the call names the key, and runs `connect()` again. | -| `dispose()` | Idempotent. Removes the message listener, rejects pending requests with `disposed`, drops every registration and the pin. A host `ui/resource-teardown` request is answered with `{}` and disposes the client; any other host request is answered `-32601`. | +| `rebind({ parent?, targetOrigin?, window? })` | Bumps the connection generation and rejects the previous generation's pending requests with `connection-rebound` — a `connect()` still in flight included; its late response can never become the live connection — clears the opening tool name, moves the message listener when `window` changes, adopts the new parent, keeps the configured `targetOrigin` unless the call names the key, and runs `connect()` again. | +| `dispose()` | Idempotent. Removes the message listener, rejects pending requests with `disposed`, and drops every registration. A host `ui/resource-teardown` request is answered with `{}` and disposes the client; any other host request is answered `-32601`. | | `connected` / `disposed` | Read-only state. | `CreateAppClientOptions` are `appInfo` (`{ name, version }`, default @@ -1799,25 +1799,24 @@ that cancellation cannot bypass consent or reach a request the App did not start. Hosts outside the framework apply their own policy; the client's behavior is the same either way. -### Dynamic sandbox handshake +### Parent transport authentication The Workbench and `serve-app` render the App as `