diff --git a/docs/architecture/mecatl.modelith.md b/docs/architecture/mecatl.modelith.md
index 0037cca217..2bc52b7c29 100644
--- a/docs/architecture/mecatl.modelith.md
+++ b/docs/architecture/mecatl.modelith.md
@@ -22,6 +22,27 @@ A reusable definition of a specialist agent — its role prompt, tool grants, mo
- **agentdef-origin-is-tier-not-location** — An `AgentDef`'s origin is a trust tier (managed, project, user), never a filesystem location.
+### `BrokerSession`
+
+Proposed in the MCP broker service design; not yet implemented. The broker-owned state through which one `Session` uses OAuth-protected upstream tools: its catalog, bundle authorizations, workspace enrollment, provider credentials, and replay state. The `Session` stores only an opaque reference issued by the broker, which replaces today's process-local broker-incarnation binding.
+
+**Relationships**
+
+- `Session` — 1:1 — referenced — serves — The `Session` snapshot holds the opaque reference; the broker owns the state. Deleting the `Session` deletes its `BrokerSession`, but the `BrokerSession` can also expire on its own while the `Session` remains.
+
+- `MCPServer` — n:n — referenced — holds provider credentials for
+
+**Invariants**
+
+- **broker-session-reference-exact** — A `BrokerSession` reference is opaque to the harness and names exactly one `BrokerSession` lifetime. Reopening or cleaning up with an old reference never creates, adopts, or deletes a later `BrokerSession` for the same `Session` ID.
+
+- **broker-session-not-inherited** — A fork or clear of a `Session` gets a fresh `BrokerSession`; it never inherits the source's reference or provider credentials.
+
+- **broker-session-restore-no-replay** — Restoring access to a `BrokerSession` after a broker restart never re-executes a `ToolCall` that may already have been dispatched; that `ToolCall` gets an error `ToolResult` recording an unknown outcome.
+
+- **broker-session-idle-expiry** — A `BrokerSession` expires after 48 hours without authorized use, and its provider credentials are reclaimed with it. Expiry leaves the `Session` intact and never extends a provider grant.
+
+
### `CommandRunner`
The optional command-execution port bound into an `Environment`. It runs commands in a namespace selected by the host. Programs access files through a filesystem they understand, such as a shared directory or mount; they do not call the Workspace interface. The environment binding supplies that connection. Binding a working directory does not by itself sandbox a command's filesystem access.
@@ -526,6 +547,7 @@ A logical file namespace exposed through a backend-independent port. File `Tools
```mermaid
erDiagram
AgentDef {}
+ BrokerSession {}
CommandRunner {}
Conversation {}
Environment {}
@@ -559,6 +581,8 @@ erDiagram
ToolResult {}
Turn {}
Workspace {}
+ BrokerSession ||--|| Session : "serves"
+ BrokerSession }o--o{ MCPServer : "holds provider credentials for"
Conversation ||--o{ Message : "orders"
Environment }o--|| Workspace : "provides file access through"
Environment ||--|| ReadLedger : "carries read evidence in"
@@ -1069,6 +1093,25 @@ erDiagram
- **mcp-tools-namespaced** — An `MCPServer`'s `Tools` are registered into the catalog under a server-namespaced name.
+### A broker restart between calls restores access without a second login
+
+**Actors:** Principal
+
+**Steps**
+
+1. A `Session` holds a `BrokerSession` reference and a completed workspace enrollment.
+2. The broker restarts while the `Principal` reads an answer.
+3. The next `ToolCall` reopens the exact `BrokerSession` by its saved reference and runs without a browser login.
+4. A `ToolCall` that was in flight during the restart receives an error `ToolResult` and is not dispatched again.
+
+**Invariants touched**
+
+- **broker-session-reference-exact** — A `BrokerSession` reference is opaque to the harness and names exactly one `BrokerSession` lifetime. Reopening or cleaning up with an old reference never creates, adopts, or deletes a later `BrokerSession` for the same `Session` ID.
+
+- **broker-session-restore-no-replay** — Restoring access to a `BrokerSession` after a broker restart never re-executes a `ToolCall` that may already have been dispatched; that `ToolCall` gets an error `ToolResult` recording an unknown outcome.
+
+- **toolcall-exactly-one-result** — A `ToolCall` produces exactly one `ToolResult`.
+
### A hook blocks a tool use
**Actors:** Operator
diff --git a/docs/architecture/mecatl.modelith.yaml b/docs/architecture/mecatl.modelith.yaml
index 37921ca49c..c010fb0b45 100644
--- a/docs/architecture/mecatl.modelith.yaml
+++ b/docs/architecture/mecatl.modelith.yaml
@@ -821,6 +821,49 @@ entities:
- id: mcp-tools-namespaced
statement: "An `MCPServer`'s `Tools` are registered into the catalog under a server-namespaced name."
+ BrokerSession:
+ definition: >
+ Proposed in the MCP broker service design; not yet implemented. The
+ broker-owned state through which one `Session` uses OAuth-protected
+ upstream tools: its catalog, bundle authorizations, workspace enrollment,
+ provider credentials, and replay state. The `Session` stores only an
+ opaque reference issued by the broker, which replaces today's
+ process-local broker-incarnation binding.
+ relationships:
+ - entity: Session
+ cardinality: "1:1"
+ ownership: referenced
+ role: serves
+ note: >
+ The `Session` snapshot holds the opaque reference; the broker owns the
+ state. Deleting the `Session` deletes its `BrokerSession`, but the
+ `BrokerSession` can also expire on its own while the `Session` remains.
+ - entity: MCPServer
+ cardinality: "n:n"
+ ownership: referenced
+ role: holds provider credentials for
+ invariants:
+ - id: broker-session-reference-exact
+ statement: >
+ A `BrokerSession` reference is opaque to the harness and names exactly
+ one `BrokerSession` lifetime. Reopening or cleaning up with an old
+ reference never creates, adopts, or deletes a later `BrokerSession`
+ for the same `Session` ID.
+ - id: broker-session-not-inherited
+ statement: >
+ A fork or clear of a `Session` gets a fresh `BrokerSession`; it never
+ inherits the source's reference or provider credentials.
+ - id: broker-session-restore-no-replay
+ statement: >
+ Restoring access to a `BrokerSession` after a broker restart never
+ re-executes a `ToolCall` that may already have been dispatched; that
+ `ToolCall` gets an error `ToolResult` recording an unknown outcome.
+ - id: broker-session-idle-expiry
+ statement: >
+ A `BrokerSession` expires after 48 hours without authorized use, and
+ its provider credentials are reclaimed with it. Expiry leaves the
+ `Session` intact and never extends a provider grant.
+
EventLog:
definition: >
The durable, append-only record of a `Session`'s `Events`, distinct from
@@ -1132,6 +1175,15 @@ scenarios:
- "The model issues a `ToolCall` against one of them like any other `Tool`."
invariants_touched: [mcp-streaming-http-only, mcp-tools-namespaced]
+ - name: A broker restart between calls restores access without a second login
+ actors: [Principal]
+ steps:
+ - "A `Session` holds a `BrokerSession` reference and a completed workspace enrollment."
+ - "The broker restarts while the `Principal` reads an answer."
+ - "The next `ToolCall` reopens the exact `BrokerSession` by its saved reference and runs without a browser login."
+ - "A `ToolCall` that was in flight during the restart receives an error `ToolResult` and is not dispatched again."
+ invariants_touched: [broker-session-reference-exact, broker-session-restore-no-replay, toolcall-exactly-one-result]
+
- name: A hook blocks a tool use
actors: [Operator]
steps:
diff --git a/docs/design/README.md b/docs/design/README.md
index 44df372781..412fd2e7f3 100644
--- a/docs/design/README.md
+++ b/docs/design/README.md
@@ -9,6 +9,8 @@ This folder no longer holds the per-feature design records — those are now num
- [surface-migration-plan.md](./surface-migration-plan.md) — the issue #555 surface
interface (one modal, dynamic Open, surfaceDeps); `/soul` is the first migrator.
- [contextual-guardrails-capacity.md](./contextual-guardrails-capacity.md) — the point-in-time offline private-capacity evidence for ADR 0350.
+- [mcp-broker-service.md](./mcp-broker-service.md) — draft design: moving the MCP broker
+ out of mecak8s agent pods into a separate `mecabroker` service.
- this file — the **documentation & citation conventions** the `docs/lint` gate enforces.
One source of truth per fact: current behavior in the owning
diff --git a/docs/design/mcp-broker-service.md b/docs/design/mcp-broker-service.md
new file mode 100644
index 0000000000..b702f1d9c4
--- /dev/null
+++ b/docs/design/mcp-broker-service.md
@@ -0,0 +1,449 @@
+# Design: a separate MCP broker service for mecak8s
+
+This proposal defines the broker's responsibilities and the harness workflows
+its interface must support. The session-reference model below is a proposed
+simplification, not a description of the current protobuf contract. Exact RPCs
+follow the [contract work](#contract-work) described below.
+
+## Why
+
+When a mecak8s deployment lets users connect OAuth-protected MCP servers (GitHub,
+Linear, …), ToolHive runs inside every agent pod and holds those users' upstream tokens.
+That causes two problems.
+
+**Tokens live next to the agent.** The mecak8s process also drives the model and reads
+untrusted content, and ToolHive stores every user's tokens in the session-store Redis,
+unencrypted, with credentials every agent pod holds. A compromised pod, or a leaked Redis credential exposes every connected user's tokens.
+The only barrier today is that the image has no shell.
+
+**The broker pins the agent fleet to one pod.** ToolHive's signing keys and mecatl's
+broker session state are per process, so the chart forces one replica and a `Recreate`
+rollout in broker mode: no scaling, and an outage for everyone on every rollout.
+
+## What changes
+
+ToolHive and mecatl's session wrapper move out of the agent process into one separate
+service, `mecabroker`, installed by the mecak8s chart. Agent pods call the broker
+over authenticated gRPC, scale freely, and never hold an upstream token or OAuth
+client secret. The broker owns provider credentials and broker sessions;
+the harness uses tools through opaque references rather than coordinating broker
+process identities. mecated and mecatui keep the embedded broker.
+
+## Who authenticates to whom
+
+This is what implementing the design delivers; later iterations are in
+[Future iterations](#future-iterations).
+
+```mermaid
+flowchart LR
+ U[Client] -- "Principal token (OIDC JWT)" --> A[mecak8s agent pods]
+ A -- "gRPC over TLS; agent verifies broker certificate
per call: service-account token (JWT, aud = mecabroker)" --> B[mecabroker]
+ W[Principal browser] -- "login + consent" --> AS[Upstream auth server]
+ W -- "upstream callback to ToolHive
(/v1/mcp/broker/oauth/callback)" --> B
+ W -- "broker callback to mecatl
(single-use state, PKCE)" --> B
+ B -- "OAuth client credential" --> AS
+ B -- "subject's access token" --> M[Upstream MCP server]
+ B -- "own ACL user over TLS; encrypted values" --> R[(Redis)]
+ B -. "optional: own service-account token
to fetch cluster signing keys" .-> K[Kubernetes API]
+```
+
+- **Principals** authenticate to mecak8s with their OIDC token, as today.
+- **Agent pods and the broker** use one TLS connection authenticated both ways by
+ different means. The agent verifies the broker's server certificate (operator-supplied,
+ checked against an operator-supplied CA and DNS name) before sending anything. The
+ broker verifies the agent's short-lived Kubernetes service-account token, issued for
+ the broker only and sent inside that connection, against the cluster's signing keys.
+- **The broker** authenticates to upstreams with the configured OAuth client and to
+ Redis with its own ACL user.
+- **Browsers** reach the broker only for OAuth callbacks, authorized by single-use state
+ the broker issued.
+
+## Session model and ownership
+
+This section uses the [domain model](../architecture/mecatl.modelith.md)'s terms:
+`Session` is the aggregate, and its `Conversation` is only the message history.
+The design adds one concept, the broker session, described below. Other broker
+terms come from [the architecture guide](../architecture.md), the broker ADRs, and
+the [outbound identity design](../agent-identity-outbound.md).
+
+A *broker session* is the broker-owned state through which one `Session` uses
+upstream tools: its catalog, its bundle authorizations, its workspace enrollment,
+the provider credentials obtained for it, and its replay state. Its *broker
+session reference* is an opaque value issued by the broker and saved in the
+`Session` snapshot. It replaces today's opaque broker-incarnation binding
+(`ExternalBinding`). Reload still requires an exact match, but the reference
+names durable broker state rather than one broker process. The harness returns
+it unchanged; it neither constructs it nor interprets its contents. Possession of
+the reference alone does not authorize a request.
+
+The reference must distinguish separate lifetimes of broker state. Suppose an
+old pod retains reference `R1`, the `Session` is deleted, and a new `Session` with
+the same session ID receives `R2`. Cleanup using `R1` must not delete
+`R2`, and opening `R1` must not silently create or adopt `R2`. The reference
+stays unchanged across broker restarts. The broker durably retains enough
+session identity and credential association to recognize it after replacement;
+the harness does not store a separate credential-recovery reference. This does
+not make pending bundle authorizations or replay state durable.
+
+A *bundle authorization* is the existing expiring browser interaction that a
+first call to a protected tool starts, tied to that exact `ToolCall`. *Workspace
+enrollment* is the existing pre-prompt flow that authorizes every required
+upstream before publishing the complete catalog. Both have opaque correlation
+references and observable status. These are separate from the broker session
+reference: cancelling one browser interaction must not mean deleting the whole
+broker session.
+
+The owner is the `Session`'s `Principal`, accountable for the stored object
+([ADR 0212](../adr/0212-caller-ownership-enforcement.md)). The *subject* is whose
+upstream account a call spends: whoever completed the browser login, matching
+the owner/subject split in the [outbound identity design](../agent-identity-outbound.md#identity-and-token-profiles).
+*Provider credentials* are the encrypted upstream tokens the broker keeps for
+that consent. They have their own expiry and revocation lifecycle. The harness
+keeps the broker session reference and expiry metadata; the broker keeps
+the credential records and their associations with the broker session, the
+install's agent service account, the subject, and the provider configuration.
+The initial API removes a workspace enrollment's provider credentials as a set,
+not per upstream. Removing them does not revoke the OAuth application's grant at
+the provider.
+
+| Responsibility | Owner |
+|---|---|
+| `Session`, its ID and lifecycle, and durable reference storage | Harness |
+| Deciding when to expose tools, enroll, re-enroll, or forget state | Harness |
+| `ToolCall` ID, arguments, and recording an unknown outcome as a `ToolResult` | Harness |
+| Broker sessions, catalogs, bundle authorizations, workspace enrollments, and provider credentials | Broker |
+| Checking current caller identity and stored credential associations | Broker, within the authentication limits below |
+| Process-local access handles, process identities, and access cleanup | Broker gRPC client and broker |
+
+The broker gRPC client is the harness's driven adapter for the broker. Today's
+process-local attachments ([ADR 0323](../adr/0323-target-governed-mcp-query.md),
+[ADR 0358](../adr/0358-durable-workspace-enrollment-broker-authority.md)) become
+private to it: the harness no longer opens or holds them. If two agent pods use
+one broker session, each pod's client manages its own access. Stopping one pod
+releases its local resources without deleting the broker session or
+interrupting the other pod. Process-local handles and instance identifiers can
+still exist inside the protocol implementation; they are not separate values
+the harness assembles or persists.
+
+## Caller identity
+
+Every request carries a Kubernetes service-account token for the install's agent
+service account: a JWT with `sub` =
+`system:serviceaccount::`, `aud` = `mecabroker`, and a 10-minute
+lifetime. The broker gRPC client re-reads it from its projected file for each call
+and sends it as `authorization: Bearer` metadata over TLS.
+
+The broker checks the token against the cluster's signing keys and its
+allowlist. The chart supplies exactly this install's agent service account.
+Audience alone is insufficient because another pod can obtain a token for its
+own service account with the same audience. All agent pods in the install
+share the allowed account, so a different pod can continue the broker session.
+
+This authenticates the workload, not the `Principal` independently. The harness
+authenticates principals and supplies the owner of the `Session`. Comparing that
+owner with the stored owner prevents accidental cross-session recovery, but a
+host-supplied owner digest does not prove that the broker authenticated the
+`Principal`. Independent verification remains part of the future identity work.
+An opaque broker session reference does not narrow a compromised agent pod's authority.
+
+## Harness workflows
+
+The following describes caller intent, not proposed RPC names or signatures.
+The interface should let the harness perform these workflows without knowing
+how the broker indexes state or identifies its current process.
+
+### Open and use tools
+
+For a new `Session`, the harness obtains a broker session reference and a
+catalog. It saves the reference in the `Session` snapshot before making
+those tools usable. On reload, it supplies the saved reference and requires the
+exact corresponding state. Creating new state is a separate intent from
+reopening existing state. A fork gets a fresh broker session and does not inherit
+the source `Session`'s reference or provider credentials.
+
+The harness invokes a named tool with the `ToolCall` ID and arguments. The
+broker returns a result, an authorization requirement, or a classified failure.
+The `ToolCall` ID is bound to the exact broker session lifetime, tool, and
+arguments; reusing it with different arguments is rejected. Bounded broker-owned
+replay state can return a previously recorded result without executing the tool
+again. A broker restart does not provide distributed exactly-once execution.
+
+For example, a GitHub tool creates an issue and the broker dies before returning
+the result. The harness records an unknown outcome as an error `ToolResult`, so
+the `ToolCall` still has exactly one result, as `Interrupt` already does for
+orphaned calls. Opening a replacement broker session must not create the issue a
+second time. Automatic retry is allowed only when there is exact evidence that
+dispatch of this `ToolCall` did not start; a timeout or a missing broker session
+is not that evidence.
+
+### Authorize upstreams and publish their tools
+
+When a tool requires authorization, the broker ties the bundle authorization to
+that exact `ToolCall`. The harness saves the pending correlation before exposing the
+browser URL. A confirmed failure to save requires abandoning the pending
+authorization so it cannot resume an unrecorded `ToolCall`. If the save outcome
+is unknown, the harness withholds the URL and reconciles the saved state before
+destructive cancellation. Cancellation by the owner has
+a different purpose: it settles a recorded authorization and reports whether it
+was cancelled or already resolved. Both outcomes are required, but they do not
+necessarily require different cancellation RPCs.
+
+Workspace enrollment follows the same rule for durable correlation before browser
+presentation. The harness receives an opaque reference for observation and
+cancellation, an ephemeral browser URL for presentation, and the enrollment's expiry.
+The URL is not persisted. The expiry describes the browser interaction, not the
+broker session's 48-hour inactivity timeout.
+
+The broker reports one overall status: pending, completed, cancelled, expired,
+or failed. Workspace enrollment is all-or-nothing: it completes and publishes its
+catalog only after every required upstream is authorized. Per-upstream progress
+and internal service bitsets remain broker-private. Completion supplies an
+authenticated catalog; the descriptors advertised to the model and the
+executable tools must come from the same snapshot. The harness durably adopts
+the result before exposing those tools.
+
+If workspace enrollment fails, expires, is cancelled, or is interrupted by broker
+restart, the broker discards provider credentials newly obtained by that attempt.
+Partial credentials cannot be used by `ToolCalls` or recovered as a completed
+enrollment. Retry starts a fresh workspace enrollment; there is no
+partial-progress recovery. Provider credentials from an earlier completed
+enrollment are unaffected by this cleanup. Cleanup is local and does not revoke
+a provider grant. The broker owns attempt tracking and bounded cleanup, without
+a harness staging or commit operation.
+
+Cancelling or replacing workspace enrollment targets the exact enrollment.
+Re-enrollment must withdraw the old enrollment-derived tools and reset the
+corresponding broker state; merely opening another browser URL is insufficient.
+A stale completion must not republish tools from the replaced enrollment. The
+same rule applies to restoration already in progress: it cannot restore authority
+withdrawn by a later re-enrollment or deletion. Compatible concurrent restorations
+can share current state, but a delayed result cannot undo a newer lifecycle decision.
+
+### Release access, disconnect, and delete state
+
+Closing a `Session` locally or stopping an agent pod releases local access.
+Deleting a `Session` first removes its durable host record, then cleans up
+the exact referenced broker session and its provider credentials. Cleanup for an
+old reference cannot destroy new state that happens to have the same session ID.
+
+Disconnecting tools withdraws the whole workspace enrollment's tools and removes
+its provider credentials. The initial API has no per-upstream disconnect or
+public per-credential references. Disconnect leaves the broker session available
+for a fresh workspace enrollment. It does not revoke the application's grant at the
+upstream provider or affect other broker sessions. The subject can revoke the
+provider grant separately at that provider. Cancelling a pending bundle
+authorization or workspace enrollment is also distinct from disconnecting
+established access.
+
+The harness durably records the withdrawal of tool access so it cannot
+silently restore those credentials while disconnect is pending. The broker must
+stop accepting new `ToolCalls` that would use removed credentials; disconnect
+cannot undo an upstream action already dispatched. A lost response is an
+unresolved disconnect, not proof of completion, and must be safely reconciled
+or retried.
+
+A delayed OAuth completion or recovery request must not revive disconnected
+credentials. Disconnect and its retries target the original workspace
+enrollment's credentials, so a lost response followed by a retry cannot remove
+credentials from a later valid enrollment in the same broker session. The broker
+owns this distinction; a stable broker session reference must not turn an old
+disconnect into deletion of whichever credentials are current. The final contract
+must specify retry and cleanup behavior without exposing credential-storage identifiers.
+
+## Publication across the two stores
+
+The broker owns broker-session creation. The harness does not prepare, commit, or
+abort it. It obtains an opaque reference, saves it in the `Session` snapshot,
+and only then exposes the tools. Creation must be retry-safe:
+a lost response must not cause an unbounded series of duplicate broker sessions.
+The exact request correlation belongs in the API contract.
+
+If the harness crashes before saving the reference, an unused broker record
+can remain. If the save succeeded before the crash, another agent pod can use
+the saved reference. The broker does not infer which case occurred from a
+connection closing, and the harness does not delete broker state as compensation
+for an ambiguous save.
+
+Broker-session records expire after **48 hours of inactivity**, starting at
+creation and renewed by authenticated, authorized broker-session use. Use the storage
+backend's native TTL for reclamation. Explicit deletion removes the broker session
+sooner. Background token refresh, health checks, and process restart do not
+renew activity, so unused records expire without an orphan-detection service or
+a scan of the harness's session store. The expiry survives broker restart;
+access rejects expired records even before physical reclamation finishes.
+
+A saved reference remains usable within that lifecycle even when an agent pod
+disconnects or restarts. After 48 idle hours, the `Session` remains,
+but its broker session expires and enrolling again can require fresh
+authorization, even if an upstream refresh token would still be valid. Expiry
+is a retention policy, not evidence that a record was never saved. Broker-session TTL
+renewal does not extend a provider grant's lifetime or revive revoked credentials.
+An expired access token can still be refreshed under a valid provider grant;
+access-token expiry alone does not end the broker session.
+
+The broker saves encrypted provider credentials against the existing broker session
+when the subject completes a valid OAuth flow. In a workspace enrollment that spans
+several upstreams, credentials obtained along the way belong to the attempt until the
+whole enrollment completes. Only then does the broker durably record the completed
+enrollment and report success. The harness does not stage or commit credential
+custody and receives no separate credential-recovery reference.
+It observes completion and durably adopts the resulting catalog before
+exposing those tools to the model.
+
+If the harness crashes after the broker durably recorded a completed enrollment
+but before adopting the tools, it can observe it again using the same broker session
+reference. It does not repeat the browser login while those credentials remain
+valid. This recovers completed enrollment state, not a pending bundle authorization or
+permission to replay an interrupted `ToolCall`. Broker-side completion must
+reject stale or cancelled enrollments and must not resurrect an expired broker session or
+revoked credentials.
+
+Credentials can therefore exist before the harness adopts their tools. They
+remain subject to the broker's identity checks and credential lifecycle.
+Credential records owned by a broker session must be reclaimed with a bounded lifetime when
+the broker session expires or is deleted; expiring only the broker-session pointer and leaving
+encrypted tokens indefinitely is insufficient. Keep that cleanup broker-local,
+using storage expiry where possible. Local removal is distinct from revoking an
+provider grant.
+
+The harness still owns durable publication of tools and authorization state.
+Broker-owned broker-session and credential creation do not make the two stores atomic:
+they accept bounded unused records instead of exposing transaction steps to the
+harness. Lost responses during explicit disconnect must still be reconciled or
+retried safely; orphan expiry is not a substitute for completing disconnect.
+
+## Restarts
+
+The broker remains one replica. The broker gRPC client automatically restores access
+using the saved broker session reference after a restart, including for a `Session`
+with history. A restart between calls does not require
+`/tools-connect` or another browser login. Once the broker is available, future
+calls proceed if credentials remain valid, current identity checks pass, and
+the recovered catalog is compatible with the `Session`'s adopted tools.
+
+This reverses a current rule. Today a persisted binding from a prior process is
+never treated as live: ordinary rehydration leaves broker tools unavailable until
+the owner explicitly refreshes ([architecture guide](../architecture.md),
+[ADR 0335](../adr/0335-idle-session-broker-workspace-refresh.md)), and snapshot
+restore never resurrects broker runtime
+([ADR 0358](../adr/0358-durable-workspace-enrollment-broker-authority.md)
+decision 4). The ADR for this design must supersede those clauses. ADR 0358's
+broker-key ledger is unaffected: restoration re-adopts the recorded catalog and
+cannot widen the `Session`'s authority.
+
+Restoring access is separate from retrying an execution:
+
+| Restart timing | Required behavior |
+|---|---|
+| Between calls | Automatically restore access and execute the next call. |
+| Before dispatch, with exact proof the `ToolCall` did not dispatch | Restore access and safely dispatch that `ToolCall`. |
+| After dispatch might have happened, before a result is received | Record an unknown outcome as an error `ToolResult`; do not execute it again automatically. Restore access for subsequent calls. |
+
+For example, the broker restarts while the owner reads an answer. Their next
+message asks the agent to list GitHub issues. The broker gRPC client restores access and
+the tool runs without a manual reconnect. If the interrupted call instead
+created an issue before its response was lost, that call remains uncertain;
+restoration must not create a second issue.
+
+Pending bundle authorizations, workspace enrollments, and parked calls are interrupted;
+automatic restoration does not resume them. The broker and its gRPC client must
+prevent stale execution requests from being accepted as fresh calls after
+restoration, even though the saved broker session reference is unchanged. They own
+that distinction without exposing process identities to the harness.
+
+The broker validates each provider credential's associations with the broker session,
+the agent service account, the subject, and the provider configuration before using
+them. This validation has the `Principal` limitation described in
+[Caller identity](#caller-identity).
+Recovery never extends their original expiry. Revoked or expired credentials,
+failed identity checks, or incompatible tool changes require an appropriate
+error or owner action rather than silent adoption. Temporary storage or broker
+unavailability fails closed within bounded request deadlines without erasing
+recoverable state or pretending that the failure proves revocation.
+
+## Contract work
+
+The session model and ownership decisions above guide the replacement API.
+Exact interfaces, acceptance scenarios, and the domain-model source are the
+next step. The broker session entity is already in the
+[domain model](../architecture/mecatl.modelith.md) as proposed. Broker sessions and
+provider credentials outlive a call, so that work also adds them to
+[ADR 0027](../adr/0027-cloud-native.md) Lists 1 and 2 with owner, cleanup, and
+restart-state decisions. The contract must specify retry correlation, stale-call rejection
+after restart, credential cleanup, and disconnect failure semantics without
+exposing broker process or storage bookkeeping. These local amendments do not
+require compatibility adapters for the unsubmitted API.
+
+## Owners and subjects
+
+The owner decides who may use a `Session`. The subject, whoever completed
+the browser login, decides whose upstream account is spent. Today they're the same
+`Principal`, but the design keeps them apart: provider credentials belong to the
+`Principal` who consented, never to a group, and sharing a `Session` in future will
+never share provider credentials. The broker
+records which upstream account each login used and shows it to the owner, so a wrong
+account or a phished consent is visible, though not prevented. Deployments without OIDC
+have no owners, so everyone with API access is one trust domain, as today.
+
+## What it protects against
+
+- **Taking tokens: stopped.** No upstream token or client secret is ever in an agent pod
+ or the session-store Redis.
+- **Using tokens: not yet stopped.** Every agent pod is the same caller to the broker, so
+ a compromised pod can still call broker tools for any session while the compromise
+ lasts, including tools that create lasting access such as deploy keys. The first future
+ iteration below narrows this to principals who are active.
+- **Out of scope:** static-bearer MCP tokens (still in the agent pod), prompt injection,
+ and consent phishing.
+
+## Future iterations
+
+Everything above is what implementing this design delivers. The following are later
+iterations, most of them the identity work. Nothing in this design blocks them.
+
+```mermaid
+flowchart LR
+ U[Client] -- "Principal token, sender-constrained (DW5)" --> A[mecak8s agent pods]
+ A -- "adds: agent-definition identity
the pod can't choose (DW3)" --> B[mecabroker]
+ B -- "refuses calls without a delegated access token" --> B
+ A -- "delegated access token for the Principal, exchanged from
their token or a grant reference" --> B
+ U -- "approves a schedule; broker holds the grant" --> B
+ W[Principal browser] -- "logs into mecatl's identity provider
before the upstream redirect (DW2)" --> IDP[mecatl identity provider]
+ B -- "out-of-band approval for dangerous
operations (DW4)" --> U2[Owner]
+ B -- "token exchange with a Principal subject (DW8)" --> V[External vMCP]
+```
+
+- **Checking the `Principal` on every call.** The pod trades the `Principal`'s token for a
+ short-lived *delegated access token* (RFC 8693 token exchange: subject = the `Principal`,
+ actor = the agent definition, as in the outbound identity design's token profiles;
+ audience = the broker), and broker calls carry it. The actor depends on agent-definition
+ identity below. The token never outlives the `Principal`'s token,
+ so a compromised pod can only act for principals who are active, on their own sessions.
+ Schedules and long runs have no `Principal` token, so this ships together with grants. The
+ broker first only logs what it would refuse, then refuses, through an internal switch.
+- **Grants.** A grant is a `Principal`'s recorded permission for the broker to keep acting for
+ them while they're away: who gave it, what it covers, and when it ends. It's stored in
+ the broker next to the provider credentials, and mecatl keeps only a reference to it. When
+ there's no live `Principal` token, the pod presents that reference to the same exchange and
+ gets the same short-lived delegated access token back. There are two kinds:
+ - a *schedule grant* (as in the outbound identity design), which the owner gives by approving a schedule's exact terms, and
+ which is redeemed at most once per scheduled slot. The broker keeps the terms and the
+ cadence itself, because the scheduler runs inside the agent pods;
+ - a *run grant*, created when the owner starts a `Run`, so a long `Run` keeps its broker
+ tools for a few hours after the `Principal`'s token lapses. This is new; the identity
+ docs would need to adopt it.
+
+ Schedules the model creates through its Schedule tool stay pending until the owner
+ approves them outside the model's reach. Open: an approval step a compromised pod can't
+ fake, and keeping provider credentials alive after the `Session` that created a schedule is
+ gone, or its schedules stop.
+- **Broker-side login.** The broker terminates `Principal` login itself, so the agent pod
+ never handles a `Principal` token at all.
+- **Browser-side consent check.** The browser logs into mecatl's identity provider before
+ the upstream redirect, and the broker proceeds only if that is the `Session` owner. This
+ closes consent phishing.
+- **Agent-definition identity and per-operation policy.** The *agent* half of per-call
+ policy. It has to follow one rule this design exposed: every per-call input must be
+ verifiable by the broker without trusting the agent pod.
+- **Out-of-band approval** for dangerous operations, and **sender-constrained `Principal`
+ tokens**. Together they close misuse of an active `Principal`'s session by a compromised pod.