feat(mcp): platform + user auth resolvers — reads via platform, writes via user (§19)#326
Conversation
|
Merged One real conflict, in Everything else auto-merged cleanly and correctly — notably The two agent-principal approaches are complementary and both survive: |
…s via user (§19)
Fixes the deployed-agent dead end: 'mcp: no stored token — login
required' with no browser in the pod. forge mcp login is laptop-only;
discovery/DCR give a client, not a grant. The managed answer is the
resolver seam: the agent fetches SHORT-LIVED tokens from its platform.
MCP auth is connection-scoped (one connection = one identity), so
per-operation identity materializes platform-side as per-identity
server entries; Forge gains exactly two resolver types and no
per-operation selection logic:
- auth.type=platform — agent-principal (service) identity. New
top-level platform block {token_endpoint, agent_identity} (both
${VAR}-expanded at USE, so rotated pod secrets apply live). The
resolver POSTs {server: auth.ref} with the agent credential and
caches the access token to TTL (30s skew, singleflight via mutex);
a refresh_token in the response is deliberately ignored — the agent
never holds one (invariant 8). Startup-viable: no human, no login,
no stored token. Construction fails without the platform block.
The resolver host merges into the egress allowlist.
- auth.type=user — delegated user identity, inherently lazy (no user
at startup): required:true rejected at validate AND NewServer; auth
attempts fail with ErrNoToken + an actionable message until the
platform consent flow (#317) produces a grant.
- MCPAuth.Ref names the registry entry the platform authorizes
against (platform-materialized; defaults to the server name).
Standalone oauth / forge mcp login path untouched (air-gap
regression: existing suites unchanged and green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ce6dcbb to
f77d456
Compare
#326 landed the type=user resolver as a pure stub — every call returned ErrNoToken. This makes it actually resolve a per-REQUESTING-USER token from the platform token endpoint (§19 delegated contract), the token-acquisition core of the delegated path. - `delegatedTokenSource`: POSTs `{server, subject}` to the platform token endpoint and caches the access token PER SUBJECT, so distinct users never share a token. The lock is not held across the network fetch, so a slow fetch for user A never blocks user B (the multi-user path is the point of #317). A platform 401/403/404 → ErrNoToken (auth-required), so a user without a grant stays lazy/non-blocking. Refresh token never reaches the agent (invariant 8). - `buildAuthFn` type=user: reads the requesting user from `auth.IdentityFromContext(ctx)` (email preferred, then user id) and resolves that subject's token. authFn runs per-request with the request ctx, so a shared connection carries per-user tokens on the call path. No user in ctx → ErrNoToken (lazy; never at startup). - `NewServer` type=user now requires the top-level platform block (it resolves via the platform endpoint), same as type=platform. - Extracted `doPlatformTokenRequest` shared by the agent-principal and delegated sources; the agent-principal path (#326) is behavior-preserved. Docs: configuration.md gains a "Managed identity (platform / user)" section — #326 shipped both types without config docs. Scope: this is the token-acquisition layer. The per-user CONNECTION lifecycle (lazy establishment so `initialize` runs under a user's session) and carrying the raw OIDC assertion for a broker-side ID-JAG exchange are the follow-ons; the platform's vaulted-3LO path resolves on subject alone today. Tests: per-subject fetch+cache (distinct users → distinct tokens, same user cached), platform-403 → ErrNoToken, buildAuthFn resolves subject from ctx / lazy without a user. Updated #326's rules test for the new platform-block requirement on type=user.
Forge's half of the platform token resolver (design §19; layer split per §18/§14.2). Fixes the deployed-agent dead end —
mcp: no stored token — login required: "atlassian"— where config materialization worked (client_id + endpoints resolved) but no grant could ever exist:forge mcp loginis laptop-only and there is no browser in a pod.The constraint that shapes it
MCP auth is connection-scoped: the token is presented at
initializefor the whole session — one connection = one identity. So per-operation identity materializes as per-identity server entries (same URL, split platform-side), and Forge needs only per-server auth config — the shape it already has. Two resolver types, nothing more:What's here
auth.type=platform— agent-principal (service) identity. New top-levelplatformblock; both fields${VAR}-expanded at use (liketoken_env— rotated pod secrets apply without restart). The resolver POSTs{"server": <ref>}with the agent credential as Bearer, caches the access token to TTL (30s skew), re-fetches on expiry, and ignores anyrefresh_tokenin the response — the agent never holds one (invariant 8). Missing platform block fails atNewServer, not on the first call. The resolver host merges into the egress allowlist.auth.type=user— delegated user identity, inherently lazy:required:trueis rejected at both validate andNewServer; auth attempts fail withErrNoToken+ an actionable message until the platform consent flow (Epic: runtime per-user, ephemeral per-session MCP OAuth (interactive login via Slack / A2A UI) #317) completes. Never blocks startup.MCPAuth.Ref— the platform tool-registry entry the token resolver authorizes against (platform-materialized; defaults to the server name).Acceptance mapping (Forge side)
TestNewServer_PlatformAndUserRules, resolver tests)TestPlatformTokenSource_FetchCacheAndIgnoreRefresh,_RefetchOnExpiry— the fake resolver emits a forbiddenrefresh_tokenand the source ignores it)type: user + required: truerejected at validate ✓ (TestValidateMCPConfig_ManagedIdentityTypes)forge mcp loginpath untouched; full suites green,golangci-lint0 issuesPlatform half (Connect grant storage,
POST /mcp/tokenwith entitlement, materialization split) lands in agent-builder alongside this.🤖 Generated with Claude Code