Skip to content

Epic: runtime per-user, ephemeral per-session MCP OAuth (interactive login via Slack / A2A UI) #317

Description

@initializ-mk

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.

  1. Air-gapped, no control plane? Yes for standalone — interactive OAuth needs only the user's browser and the MCP server. → Forge default.
  2. Mechanism or decision? Park/resume, identity plumbing, calling the seam — mechanism. → Forge. (Consent delivery + callback custody = platform in managed mode.)
  3. 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)

  1. User identity propagation — the long pole; everything delegated depends on it; independent of ID-JAG and the PDP. Invest here first.
  2. The resolver seamtype: field + BearerToken(server, subject, session) + the token-endpoint contract.
  3. Interactive OAuth resolver — the Forge-local standalone default.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestforge-cliAffects the forge-cli command-line tool (init, run, build, mcp commands)forge-coreAffects the forge-core library (runtime, security, types, llm, mcp, auth)securitySecurity vulnerability fixes

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions