From 384db1055d846af0e0092cd2efce1c7e25d6c750 Mon Sep 17 00:00:00 2001 From: MK Date: Sun, 19 Jul 2026 00:59:46 -0400 Subject: [PATCH] docs(mcp): platform-integrator runbook for delegated consent (auth gate) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add docs/mcp/delegated-consent.md — the operational guide for a managing platform to switch MCP human-in-the-loop consent from DEFER to the #330 auth-required gate. Covers the DEFER-vs-gate distinction, the type: user forge.yaml deploy config, detecting a parked call (task auth-required + mcp_auth_required audit event), and driving resume via authenticated POST /mcp/consent {subject, server, granted} — including the grant-before-resume ordering and the no-token contract. Complements the existing configuration.md concept section (linked, not duplicated); linked from the MCP index Related docs. --- docs/mcp/delegated-consent.md | 169 ++++++++++++++++++++++++++++++++++ docs/mcp/index.md | 1 + 2 files changed, 170 insertions(+) create mode 100644 docs/mcp/delegated-consent.md diff --git a/docs/mcp/delegated-consent.md b/docs/mcp/delegated-consent.md new file mode 100644 index 00000000..3a94601b --- /dev/null +++ b/docs/mcp/delegated-consent.md @@ -0,0 +1,169 @@ +--- +title: "MCP — Delegated Consent (Platform Integration)" +description: "How a managing platform deploys type: user MCP servers and drives the auth-required gate to obtain and manage per-user consent." +order: 6 +--- + +This is the operational runbook for a **platform** that manages Forge agent +deployments and wants per-user (delegated-identity) MCP access — replacing any +prior use of [DEFER](../security/defer-decisions.md) for MCP human-in-the-loop +consent. For the concept and config-field reference, see +[Configuration → the auth-required gate](./configuration.md#delegated-consent--the-auth-required-gate-330); +for the event fields, see [Audit events](./audit-events.md). + +## First: the auth gate is not DEFER + +DEFER and the auth-required gate solve different problems. Migrate **only** the +MCP-consent case; keep DEFER for per-action approval. + +| | DEFER — `POST /tasks/{id}/decisions` | Auth gate — `POST /mcp/consent` | +|---|---|---| +| Question | "Approve/reject *this write action*?" | "Has the user *authorized* the agent to act as them on this server?" | +| Keyed by | `taskID` (1 decision : 1 task) | `{subject, server}` — fans out; one consent unblocks **all** of that user's parked calls for that server | +| Outcome | approve / reject (approver allowlist) | granted / timeout / canceled | +| Trigger | every matching tool call | only a `type: user` call with **no grant** for the requesting user | +| Token | n/a | Forge never receives it — the resume is a "a grant now exists, re-resolve" signal | + +If a human must still approve a specific MCP *write*, that stays on DEFER. The +auth gate is purely for delegated-identity **consent** (the user hasn't +connected their Atlassian / Linear / etc. account yet). + +## 1. Deploy-time — forge.yaml + +Mark each delegated MCP server `auth.type: user` and point Forge at the +platform token resolver: + +```yaml +platform: + token_endpoint: https://platform.internal/agents/{id}/mcp/token # your resolver + +mcp: + servers: + - name: atlassian + transport: http + url: https://mcp.atlassian.example/mcp + required: false # MUST be false — no user exists at startup + auth: + type: user # delegated per-user identity; activates the gate + ref: atlassian-registry-entry # platform tool-registry key the resolver authorizes against + tools: + allow: ["*"] # or an explicit allowlist (default-deny) +``` + +Invariants the runtime enforces: + +| Rule | Why | +|------|-----| +| `type: user` must not be `required: true` | Delegated identity is lazy — there is no user at boot, so the server can't be a startup gate. | +| No `token_store_path`, no `forge mcp login` for `type: user` | The token is materialized by your `token_endpoint`, keyed `{server, subject}` — never stored on disk by the agent. | +| The gate attaches to every MCP tool automatically | It only trips on an `ErrNoToken` from the per-user resolver, so non-delegated servers are unaffected — nothing to disable. | + +Remove those MCP tools from any DEFER policy that was standing in for consent. + +## 2. Detect a pending consent + +When a `type: user` call has no grant, the runtime surfaces the need three ways +(no polling of internal state required): + +1. **Task status flips to `auth-required`** (A2A `TaskState`). A `GET /tasks/{id}` + reader sees state `auth-required` with the agent message + *"Authorization required: connect \ to continue (as \)"*. +2. **Audit event `mcp_auth_required`** — emitted once per `{subject, server}` + (not per call): + + ```json + {"event":"mcp_auth_required","correlation_id":"…","task_id":"…", + "fields":{"server":"atlassian","subject":"user@corp.com", + "deadline":"2026-07-19T12:34:56Z","timeout_ms":600000}} + ``` + +3. Resolution emits **`mcp_auth_resolved`**; expiry emits **`mcp_auth_timeout`**. + +`subject` is the user's email (falls back to the opaque user ID), `server` is the +MCP server name, `deadline` is the hard park window — **default 10 minutes**, +after which the call fails with an auth-required error and the LLM sees a +`no_token` result. Consume the audit stream and/or watch task status. + +## 3. Deliver consent, then resume + +The platform owns delivery, the OAuth callback, and token custody (managed +mode). The end-to-end sequence: + +``` +type: user call, no grant for subject + → resolver calls token_endpoint {server, subject} → no grant → ErrNoToken + → gate PARKS; task → auth-required; emit mcp_auth_required{subject, server, deadline} + → platform delivers consent to subject (Slack DM "Connect Atlassian", console…) + → subject completes OAuth; platform VAULTS the grant + → platform: POST /mcp/consent {subject, server, granted:true} + → gate resolves → parked call re-resolves → token_endpoint now returns a token → call proceeds +``` + +The resume call: + +```http +POST /mcp/consent # on the agent's A2A server +Authorization: Bearer # NOT auth-exempt — you must authenticate +Content-Type: application/json + +{ "subject": "user@corp.com", "server": "atlassian", "granted": true } +``` + +Response codes: + +| Code | Meaning | +|------|---------| +| `200` | Gate resolved; every parked call for `{subject, server}` re-resolves and re-fetches the token. | +| `404` | No call is currently parked for that `{subject, server}`. | +| `400` | Missing `subject`/`server`, or malformed body. | +| `409` | Race — the gate already resolved (e.g. timed out) between check and resolve. | + +- **`granted: false`** reports a refusal so the parked call **fails fast** + instead of idling to the deadline. Omitting `granted` defaults to `true`. +- **Ordering is load-bearing:** send `granted: true` **only after** your + `token_endpoint` can return a token for that subject. The consent endpoint + merely unblocks re-resolution; if the grant isn't vaulted yet, the re-resolve + hits `ErrNoToken` again and re-parks. Delegation follows authorization. +- **No token crosses the boundary.** Forge fetches the token itself via the + delegated resolver (`POST {token_endpoint}` with `{server, subject}`) on + re-resolution (AARM R10; `design-tool-registry.md` §18.5). + +## 4. Embedding Forge (optional) + +The stock binary is fully drivable via the HTTP signals above. If you link Forge +as a library and prefer push-delivery over consuming the audit stream, set the +deliverer seam so Forge calls you directly when a gate opens: + +```go +runner.SetConsentDeliverer(func(ctx context.Context, subject, server, taskID string, deadline time.Time) error { + return platform.DeliverConsentPrompt(subject, server, taskID, deadline) // Slack DM, etc. +}) +``` + +Delivery is best-effort — an error is logged, never fatal; the park/resume still +works. The standalone loopback callback (`GET /mcp/oauth/callback`, public) is +for the Forge-hosts-its-own-callback case and is **not** used in managed mode. + +## Durable token custody across replicas + +The per-user access-token cache is the `SubjectTokenStore` interface (in-process +by default). A managed broker can substitute a shared/durable implementation so +grants survive restarts and are shared across replicas — the resolver and agent +are unchanged. See +[Configuration → the auth-required gate](./configuration.md#delegated-consent--the-auth-required-gate-330). + +## Migration checklist + +- [ ] `auth.type: user` + `ref` on the delegated servers; `required: false`; `platform.token_endpoint` set. +- [ ] Remove those MCP tools from any DEFER policy that was standing in for consent. +- [ ] Subscribe to `mcp_auth_required` / `mcp_auth_resolved` / `mcp_auth_timeout` (and/or watch task `auth-required`). +- [ ] Wire consent delivery → OAuth → **vault grant** → authenticated `POST /mcp/consent`. +- [ ] Ensure `token_endpoint` returns `{server, subject}` tokens **before** posting `granted: true`. +- [ ] Keep DEFER for genuine per-action approve/reject. + +## Related docs + +- [Configuration reference](./configuration.md) — every `mcp:` field + the gate concept +- [Audit events](./audit-events.md) — `mcp_auth_*` fields +- [DEFER decisions](../security/defer-decisions.md) — per-action approval (the tool you keep) +- [Authentication](../security/authentication.md) — the agent auth that guards `POST /mcp/consent` diff --git a/docs/mcp/index.md b/docs/mcp/index.md index 3543a49e..e0768f84 100644 --- a/docs/mcp/index.md +++ b/docs/mcp/index.md @@ -87,3 +87,4 @@ forge run # tools available as linear__create_issu - [CLI reference](./cli-reference.md) — every flag of every subcommand - [Audit events](./audit-events.md) — the seven event types and their fields - [Troubleshooting](./troubleshooting.md) — reason codes → fixes +- [Delegated consent (platform integration)](./delegated-consent.md) — deploy `type: user` servers and drive the auth-required gate