feat: choose upstream credentials for apps that sign in without a browser

Merged
DA
3d ago
+15347-3397

AIM-597 (2 of 2)

Stacked on #8041.

Summary

  • Token requests choose per server. The ID-JAG grant and the workload grant accept upstream_auth_client=<mcp_server_id>:<auth_client_id|none>, repeatable (at most 100). A choice for a server the token does not reach is invalid_request.
  • Decision order per server, for a direct server or each proxied gateway member the subject reaches:
    1. The choice the request names. A credential the subject may not use is access_denied, one not connected or set up is setup_required; neither is silently replaced.
    2. The caller's issuance profile. A profile choice that no longer validates gives way to step 3.
    3. The only ready credential the subject may use. With several, the request fails with selection_required, naming the server.
  • Readiness without a browser. Personal connections are ready only when a stored token exists and needs no reconnect. Identity-provider (EMA) clients are judged when used. The consent preselection never applies.
  • Issuance profiles (writes serialized per caller; authClients.listIssuanceProfiles / setIssuanceProfile / deleteIssuanceProfile, org:admin to write): one per OAuth client, or per workload issuer and subject, on a sign-in. Choices are stored in upstream_client_selections under a new issuance_profile credential kind. A workload acts for nobody, so its profile takes shared credentials only. Choices grant nothing: each issuance checks access again. Audited as issuance-selection-profile:create|update|delete.
  • upstreamclients.CheckConfiguredChoice is now shared by agent keys and profiles.
  • Only where upstream_auth_enforced is on. Elsewhere nothing is decided at issuance, and a request naming a choice is refused rather than ignored.
  • Not covered: issuer-wide workload sessions (they target no single server), and assistant workloads (they bind execution profiles; AIM-598).
  • Dashboard: server settings → Upstream credentials gets an Apps without a browser row when the server offers two or more credentials. It lists each app's choice on this server, and admins can choose or remove one. Workloads are offered shared credentials only.
  • Demo seed: "Nightly report job", a Linear client that signs in without a browser, with a profile choosing the shared token on Linear. Postflight checks it.
  • Platform MCP: new choose_mcp_headless_caller_credential (org admin, external only), next to offer_mcp_upstream_credential. It previews the server's credentials and every headless caller's choice, then changes one caller's choice for that one server through the same serialized write the API uses, so the caller's other servers keep theirs.

Motivation

RFC sections 7.5 and 7.6: callers that cannot see the consent picker still need a definite credential per server. That credential comes from the request, from an admin-configured profile, or from an unambiguous default. Without one, the request fails with an actionable error instead of a silent guess.

Gutternote review steps

  1. Setup: the seeded demo data covers it. Linear offers its personal client and the shared "Engineering automation token".
  2. Sidebar → MCP → Linear → Settings → Upstream credentials.
  3. The Apps without a browser row lists Nightly report job using Engineering automation token.
  4. Click Choose for an app, pick an app, then a credential, and Save. The list updates.
  5. Remove the choice with the trash button. The row shows "None set."

🚀 Preview Environment (PR #8042)

Preview URL: https://pr-8042.dev.getgram.ai

ComponentStatusDetailsUpdated (UTC)
✅ DatabaseReadyExisting database reused2026-10-30 08:51:11.
✅ ImagesAvailableContainer images ready2026-10-30 08:57:03.

Gram Preview Bot