Runtime client binding

TL;DR — Every Mint resource can carry a policy.runtime.client_ids list — the OAuth client_ids allowed to act AS that resource at /oauth/token. Empty list means default-deny. You need this when an agent or gateway exchanges a token for a Broker resource and there’s no fronting link covering the source→target pair — the broker actor-attestation gate uses this list to resolve which resource the caller represents. Everywhere else, ignore it.

OAuth client vs Resource — two identities

client_id and resource are separate concepts with an N:N runtime relationship — not 1:1.

ConceptIdentifierLifecycle
OAuth clientclient_id (auto-generated, opaque, rotatable)Issued at registration; rotated independently
Resourceslug (operator-meaningful, human-readable)Managed in the resources table; embedded in URLs, audit rows, scope strings

One OAuth client can act in different roles across calls (admin, agent, broker actor). One resource can be served by multiple OAuth client identities (prod tier, canary, multi-region). The role a client plays in any given call cannot be derived from client_id alone — it needs explicit operator declaration. That’s what runtime.client_ids provides.

When you need it

Set runtime.client_ids on a Mint resource whenever an OAuth client authenticates to /oauth/token and the broker actor-attestation gate must resolve that client to its actor MCP. Concretely — any time an agent or gateway exchanges a token for a Broker resource, and there is no fronting link covering the (source→target) pair.

When you DON’T need it

  • Exchange is for a Mint resource (the actor-attestation gate runs only on the broker dispatch path).
  • A fronting link covers the (source → target) pair (the fronted-broker bypass replaces this gate with the operator-vouching declaration).
  • The dispatch path doesn’t hit the gate — direct user→MCP, fronted Mint→Mint, refresh, client_credentials, jwt-bearer, authorization_code — none of these use runtime.client_ids.

If your topology is Agent + single MCP or Direct fanout, skip this whole page.

Default semantics — default-deny (unlike exchange.allowed_client_ids)

Empty runtime.client_ids = no client may act AS this resource.

This is the opposite default of policy.exchange.allowed_client_ids (which is permissive when empty). Both defaults are deliberate:

  • exchange.allowed_client_ids (empty = “any allowed”) — defense-in-depth lives elsewhere on this gate (the per-MCP consent_grants check).
  • runtime.client_ids (empty = “no one allowed”) — this gate is the only place the runtime resolves the actor role. Permissive empty would defeat it.

Configuring it

Admin UI

Resources drawer → Runtime clients (act AS this resource) section. Pick clients from the dropdown; click a chip to remove. Save sends only the dirty section — unrelated edits can’t accidentally widen the list.

Admin API

# Add a client
curl -X POST http://localhost:9001/admin/resources/mcp-gw/policy/runtime/client-ids \
  -H "Authorization: Bearer $AUTHPLANE_ADMIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"client_id":"gw_prod_id"}'

# List authorized clients
curl http://localhost:9001/admin/resources/mcp-gw/policy/runtime/client-ids \
  -H "Authorization: Bearer $AUTHPLANE_ADMIN_API_KEY"

# Remove a client
curl -X DELETE http://localhost:9001/admin/resources/mcp-gw/policy/runtime/client-ids/gw_prod_id \
  -H "Authorization: Bearer $AUTHPLANE_ADMIN_API_KEY"

CLI

authserver admin resource runtime-client add    --slug mcp-gw --client-id gw_prod_id
authserver admin resource runtime-client list   --slug mcp-gw
authserver admin resource runtime-client remove --slug mcp-gw --client-id gw_prod_id

YAML (seed data — first boot only)

resources:
  - slug: mcp-gw
    backend_kind: mint
    policy:
      exchange:
        allowed_client_ids: [agent_x, agent_y]     # who may exchange INTO this Resource
      runtime:
        client_ids: [gw_prod_id, gw_canary_id]     # who may act AS this Resource

Multi-tier deployments

Where runtime.client_ids really pays off: you rotate the OAuth client for your prod gateway (gw_prod_v2_id) while the canary tier still runs the old client (gw_prod_id). Both are simultaneously valid actors for the same Mint resource because both are on the list. Zero downtime, no code change on the gateway side.

Failure modes

SymptomCauseFix
400 invalid_grant "client is not an authorized actor for this resource" on broker exchangeThe calling client_id isn’t in runtime.client_ids for the source Mint resourceAdd via admin resource runtime-client add
Same error, but the client IS in the list per admin resource runtime-client listWrong resource slug — the client is on a different resource’s listConfirm which source resource the exchange is naming
Gateway rotation broke exchange right after rolloutYou added the new client to runtime.client_ids but forgot the old one; or vice versaKeep both while the fleet transitions