Layer: Forge — with the managed split. The BearerToken(server, subject, session) seam and its standalone interactive-OAuth default are Forge. In managed mode the OAuth callback, consent delivery, and token custody move to the platform (§18.4 / §19); Forge parks and resumes, and never learns which resolver answered. Authority: design-tool-registry.md §18–§19. Delegated-identity broker work: initializ/aip/mcp-delegated-identity-broker.md (#318).
Problem
Forge's remote-MCP OAuth is a single, process-global, deploy-time grant (storeKey = "mcp_" + name): every end user shares one downstream identity — no per-user authorization, attribution, or isolation. This epic is the delegated (per-requesting-user) path: each user authorizes with their own account, scoped to their session.
⚠️ This issue does NOT unblock a deployed agent's startup. A headless agent still fails with mcp: no stored token — login required. That is the agent-principal path (type: platform — the agent's own identity), which is separate, new, in neither this issue nor #318, and more urgent than delegated. Tracked in #324. Do not schedule #317 expecting it to fix deployment.
Why the delegated path is Forge (§18.1)
Forge owns the seam plus a standalone-capable default. initializ owns anything requiring an enterprise trust relationship.
- Air-gapped, no control plane? Yes for standalone — interactive OAuth needs only the user's browser and the MCP server. → Forge default.
- Mechanism or decision? Park/resume, identity plumbing, calling the seam — mechanism. → Forge. (Consent delivery + callback custody = platform in managed mode.)
- Holds a credential or trust relationship? No in Forge — the token lands in an interface-backed store standalone; the broker holds it in managed mode. → seam is Forge, custody is the broker's.
The seam and the resolver axis (§18.2 / §19)
BearerToken(server, subject, session) → short-lived token. Forge-facing config declares the principal, not the flow — the method (interactive vs ID-JAG) is platform-decided and opaque to Forge (§19 supersedes §18.2's method:-in-forge.yaml):
auth:
type: user # delegated — per requesting user (this epic)
# type: platform # agent-principal — the agent's own identity (#324, separate)
max_scope: [read, write]
- Standalone resolves
type: user via the local interactive-OAuth default (loopback forge mcp login).
- Managed resolves
type: user via the platform token endpoint — the platform picks interactive-consent or ID-JAG; Forge never learns which.
Token-endpoint contract (the wire seam)
POST {token_endpoint} + agent identity → access token only:
delegated: { server, subject, user_assertion } → { access_token }
agent-principal: { server } → { access_token } (#324)
The user_assertion is exactly what a broker-side ID-JAG/token-exchange consumes (#318).
The managed flow (§18.4)
1. Agent wants an MCP tool → BearerToken(server, subject=U123, session=S1)
2. no session token → auth-required GATE parks the executor (DEFER-shaped Handle) [FORGE]
3. platform prompts U123 (Slack DM / console chat), catches the callback at the [PLATFORM]
ONE stable origin https://<platform>/mcp/oauth/callback, completes the exchange,
and HOLDS the token
4. platform signals resume → Forge resumes the gate [FORGE]
5. Forge calls BearerToken(...) → resolves through the broker → tool proceeds
Net: less Forge code, not more — and the refresh token never touches the agent (the access token still transits in Phase 1; the egress proxy closes that in Phase 2, §16.4). Standalone keeps Forge's own loopback callback + link delivery as the default.
This dissolves the redirect-URI problem and removes runtime discovery: one platform callback origin ⇒ one redirect URI ⇒ the DCR client is minted once at admission (no per-deployment mint, no RFC 7592). Endpoints + client_id come from the registry entry; runtime only does the exchange — replacing the "login is laptop-only, so discovery must run at runtime" premise.
Work breakdown
| # |
Item |
Owner |
| 1 |
Identity plumbing — thread the authenticated subject (auth.Identity / Slack user_id) and the raw assertion from the request edge into BearerToken. Today only parsed Claims land on Identity; the raw token is at auth/middleware.go:122 but not propagated. The long pole; every resolver needs it; independent of ID-JAG and the PDP; the highest-value item in either issue. Needs a session id on the invocation. |
Forge |
| 2 |
Session token store — an interface with a local in-memory default, not the architecture. Standalone in-process; managed = broker holds the token (§8 invariant 4 "nothing in the agent's context"). Acceptance: a broker-backed impl substitutes without refactoring callers. |
Forge |
| 3 |
Auth-required gate — factor DEFER park/resume into "pause → request → resume on callback." |
Forge, every mode |
| 4 |
Public callback + state binding |
Platform (managed); Forge loopback = standalone default only |
| 5 |
Consent / link delivery (Slack DM / console chat; auth-required artifact) |
Platform (managed); Forge default only |
| 6 |
Per-user connection lifecycle — MCP presents auth at initialize for the whole session (no per-call override), so the connection must be per subject/session, not just the store key. Establish lazily on demand inside a session, tear down at session end. |
Forge |
Resolver selection (§19)
Selection is per-operation A6 on the registry entry; the platform materializes the split into per-identity connections (two entries, same URL). So:
required: true + type: user is a configuration error — no user exists at startup; it can never succeed. Validate against it.
- The
type: user connection is inherently lazy — established on demand mid-session, after consent.
- Forge needs no per-operation selection logic — the platform splits; Forge sees ordinary per-server config.
Authentication ≠ authorization (§18.5)
- A valid token is necessary, not sufficient. Delegation answers whose token, scoped to what; the PDP answers should this action proceed.
- Ordering — delegation follows authorization, never precedes it. Authorize first, then mint; never mint speculatively.
- Token never enters the agent's reasoning context (§8 invariant 4). Defeats prompt injection, not a malicious builder reading process memory (Phase-1 in-process boundary). Fix: broker/egress proxy holds the token and injects at egress (§16.4 / §17.3) — never through the agent.
- Risk-tiering: reads may be frictionless; writes/destructive route through DEFER regardless of token validity, seeded by A9 side-effect class at admission (§4.2). DEFER (v0.17.0) shipped — enforceable now, pre-PDP.
- The inversion: the less interactive the acquisition, the more the per-action gates carry the weight — frictionless delegation and write-gating ship together.
- Policy authorship vs. materialization (§18.6): DEFER the mechanism is Forge; which tools defer is policy — initializ authors/distributes, Forge enforces locally.
Sequencing — buildable now, pre-PDP (§18.7)
- User identity propagation — the long pole; everything delegated depends on it; independent of ID-JAG and the PDP. Invest here first.
- The resolver seam —
type: field + BearerToken(server, subject, session) + the token-endpoint contract.
- Interactive OAuth resolver — the Forge-local standalone default.
- Risk-tiering via DEFER — writes gated regardless of token validity.
ID-JAG drops in platform-side when its prerequisites land (#318). Build the seam; don't block on the spec.
Status (2026-07)
The token + connection resolution half is DONE and merged — #327 (per-user tokens) + #329 (per-user connection lifecycle: pool + routing seam + materialized type: user servers).
The runtime auth-required gate + consent core is DONE — #330 (PR #331): the authgate park/resume engine, the AuthGate seam in MCPTool.Execute (park on ErrNoToken, resume on grant), the runtime gate + POST /mcp/consent resume endpoint (managed, no token), the standalone state-binding + GET /mcp/oauth/callback, and the swappable SubjectTokenStore.
Remaining after #331 — three Forge/standalone pieces, none blocking the managed path:
Acceptance criteria
Risks
Related
#318 (platform ID-JAG broker resolver) · #330 (auth-required gate — DONE, PR #331) · #332 (standalone consent delivery — remaining) · #333 (per-session teardown — remaining) · #324 (agent-principal type: platform resolver — done) · initializ/aip/mcp-delegated-identity-broker.md · #316 (discovery/DCR — standalone) · AARM R10 #319 · authority design-tool-registry.md §18–§19.
Problem
Forge's remote-MCP OAuth is a single, process-global, deploy-time grant (
storeKey = "mcp_" + name): every end user shares one downstream identity — no per-user authorization, attribution, or isolation. This epic is the delegated (per-requesting-user) path: each user authorizes with their own account, scoped to their session.Why the delegated path is Forge (§18.1)
The seam and the resolver axis (§18.2 / §19)
BearerToken(server, subject, session) → short-lived token. Forge-facing config declares the principal, not the flow — the method (interactive vs ID-JAG) is platform-decided and opaque to Forge (§19 supersedes §18.2'smethod:-in-forge.yaml):type: uservia the local interactive-OAuth default (loopbackforge mcp login).type: uservia the platform token endpoint — the platform picks interactive-consent or ID-JAG; Forge never learns which.Token-endpoint contract (the wire seam)
POST {token_endpoint}+ agent identity → access token only:The
user_assertionis exactly what a broker-side ID-JAG/token-exchange consumes (#318).The managed flow (§18.4)
Net: less Forge code, not more — and the refresh token never touches the agent (the access token still transits in Phase 1; the egress proxy closes that in Phase 2, §16.4). Standalone keeps Forge's own loopback callback + link delivery as the default.
This dissolves the redirect-URI problem and removes runtime discovery: one platform callback origin ⇒ one redirect URI ⇒ the DCR client is minted once at admission (no per-deployment mint, no RFC 7592). Endpoints +
client_idcome from the registry entry; runtime only does the exchange — replacing the "login is laptop-only, so discovery must run at runtime" premise.Work breakdown
auth.Identity/ Slackuser_id) and the raw assertion from the request edge intoBearerToken. Today only parsedClaimsland onIdentity; the raw token is atauth/middleware.go:122but not propagated. The long pole; every resolver needs it; independent of ID-JAG and the PDP; the highest-value item in either issue. Needs a session id on the invocation.statebindingauth-requiredartifact)initializefor the whole session (no per-call override), so the connection must be per subject/session, not just the store key. Establish lazily on demand inside a session, tear down at session end.Resolver selection (§19)
Selection is per-operation A6 on the registry entry; the platform materializes the split into per-identity connections (two entries, same URL). So:
required: true+type: useris a configuration error — no user exists at startup; it can never succeed. Validate against it.type: userconnection is inherently lazy — established on demand mid-session, after consent.Authentication ≠ authorization (§18.5)
Sequencing — buildable now, pre-PDP (§18.7)
type:field +BearerToken(server, subject, session)+ the token-endpoint contract.ID-JAG drops in platform-side when its prerequisites land (#318). Build the seam; don't block on the spec.
Status (2026-07)
The token + connection resolution half is DONE and merged — #327 (per-user tokens) + #329 (per-user connection lifecycle: pool + routing seam + materialized
type: userservers).The runtime auth-required gate + consent core is DONE — #330 (PR #331): the
authgatepark/resume engine, theAuthGateseam inMCPTool.Execute(park onErrNoToken, resume on grant), the runtime gate +POST /mcp/consentresume endpoint (managed, no token), the standalonestate-binding +GET /mcp/oauth/callback, and the swappableSubjectTokenStore.Remaining after #331 — three Forge/standalone pieces, none blocking the managed path:
Issuestate + deliver login link; supply theCallbackCompleter). Managed delivery is platform-side.user_assertionpropagation (work-item 1) — the delegated body currently sends{server, subject}only; the raw assertion is only needed once the broker does ID-JAG/token-exchange, so it rides with MCP delegated identity: platform-side ID-JAG broker (Forge-side collapses to assertion-forward) #318.Acceptance criteria
type:-shaped;required: true+type: useris rejected at validate. (feat(mcp): platform + user auth resolvers — reads via platform, writes via user (§19) #326/feat(mcp): delegated per-user token resolution for type=user (#317) #327)statebinding rejects cross-session / replayed / expired callbacks (standalone default). (Auth-required gate: DEFER park/resume for delegated MCP consent (#317 follow-up) #330 / PR feat(mcp): auth-required gate for delegated MCP consent (#330) #331 — mandatory, fail-closed)SubjectTokenStore, feat(mcp): auth-required gate for delegated MCP consent (#330) #331); session end drops token + connection; new session re-consents → Per-session teardown: drop delegated MCP token + connection at session end (#317 follow-up) #333.SubjectTokenStore)Risks
initialize, whole-session). Connection-pool / cap / eviction strategy needed; call it out before P0 lands the lazy-connection path.type: platform, Agent-principal MCP OAuth resolver (type: platform) — unblocks headless-agent startup #324) is the deployment-unblocking path and must not be conflated with this delegated epic.Related
#318 (platform ID-JAG broker resolver) · #330 (auth-required gate — DONE, PR #331) · #332 (standalone consent delivery — remaining) · #333 (per-session teardown — remaining) · #324 (agent-principal
type: platformresolver — done) ·initializ/aip/mcp-delegated-identity-broker.md· #316 (discovery/DCR — standalone) · AARM R10 #319 · authoritydesign-tool-registry.md§18–§19.