Runtime identity

Bind every request to a verified human, agent, team, work mode, and task.

By default (identity.mode: "static") the broker serves the single runtime identity from its config file. In assertion mode, each workspace presents a signed Ed25519 assertion, minted by the issuing service or the issue-runtime.js script, and the broker takes the human, agent, runtime ID, team, work mode, and task from it on every request.

Create an issuer key

Only the public key goes to the broker. The private key goes to the issuing service and never to the broker or any workspace.

umask 077
openssl genpkey -algorithm ed25519 -out issuer.pem
openssl pkey -in issuer.pem -pubout -out issuer.pub.pem

Configure the broker

Replace runtime in config.local.json with (1–10 issuers, unique kids):

"identity": {
  "mode": "assertion",
  "audience": "agentgate:acme",
  "issuers": [{ "kid": "k1", "publicKeyPem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n" }]
}

Issue an assertion

Through the issuing service, or from the command line, bound to the workspace's token and valid for at most 24 hours. When the issuer is on another host, the developer runs agentgate fingerprint in the workspace and sends the printed SHA-256 instead of the token:

node scripts/issue-runtime.js --key issuer.pem --kid k1 \
  --client-token-sha256 <fingerprint> --audience agentgate:acme \
  --runtime-id rt-42 --human dev@example.com --agent claude-code --team payments \
  --mode build --task jira:PAY-12 --ttl 28800 --out ~/.agentgate-runtime-assertion
echo "AGENTGATE_RUNTIME_ASSERTION_PATH=$HOME/.agentgate-runtime-assertion" >> .env

The workspace sends the assertion as the x-agentgate-runtime header next to the bearer token; workspace setup writes it into the managed gitconfig. The broker checks the signature, kid, alg/typ, audience, lifetime, client-token binding, claim formats, and revocation on every request. Requests without a valid assertion get 401 ASSERTION_INVALID. Successful verifications are cached by token hash until expiry; revocation is re-checked on every request.

What changes

  • Audit records carry the verified runtime, including runtimeId and jti, never the raw assertion.
  • Approvals bind to the jti, so a renewed assertion needs fresh approvals.
  • The assertion's human can't approve their own requests.
  • Entitlement modes and teams requirements become available.
  • Each runtime gets its own request budget, a token bucket keyed by jti (AGENTGATE_RUNTIME_RATE_CAPACITY, AGENTGATE_RUNTIME_RATE_REFILL_PER_SECOND, defaulting to the per-address values).

Fleet mode: one broker, many workspaces

With AGENTGATE_CLIENT_AUTH=assertion (and no AGENTGATE_CLIENT_TOKEN_FILE), the broker holds no workspace token at all. Each request must carry a bearer token and an assertion whose binding is the SHA-256 of that exact token; the issuer decided which token belongs to which runtime when it minted the assertion, so adding a workspace is issuing one assertion, and removing it is a revocation. A token presented with another runtime's assertion, or a request without either, gets 401.

Approvals stay bound to the assertion's jti, LFS transfers to the runtime, and every audit record names the runtime, so nothing in the broker's state is shared between workspaces except the repository mirrors. Static identity refuses fleet mode. npm run test:fleet exercises this in real containers.

Note

The egress proxy and model gateway keep their own shared client tokens, but with the issuer keys configured they attribute every tunnel and request to the runtime whose assertion the workspace sends, and budget per runtime; see Model gateway and Egress proxy. Neither can check the token binding, so this attributes rather than authorises.

Delegation bound at verification

The issuer bounds the team at issuance. identity.delegation makes the broker re-check it on every request against its own entitlement sources:

"identity": { "mode": "assertion", "audience": "agentgate:acme", "issuers": [], "delegation": "enforce" }
ValueBehaviour
enforceA team not among the teams or groups the sources report for the human gets 403 DELEGATION_MISMATCH before any route. If membership can't be established because a source failed, 503 DELEGATION_UNKNOWN (retryable), never a pass. Both are audited as delegation-refused with the claimed team.
auditThe request proceeds and a delegation-warning record is written. Use it to find stale or hand-typed teams before enforcing.
off (default)The bound rests on the issuer alone.

delegation requires a membership source (static, githubTeams, okta, or entra) and is only valid in assertion mode.

Renewal

Issue a new assertion before expiry, replace the file, then run agentgate renew in the workspace; it re-runs workspace setup so Git sends the new assertion. The entrypoint also does this on every container start.

Revocation

Without a control plane, set AGENTGATE_REVOCATIONS_FILE to an owner-only JSON file:

{ "jtis": ["…"], "runtimeIds": ["rt-42"] }

agentd re-reads it whenever its modification time, size, or inode changes, polled every AGENTGATE_REVOCATIONS_POLL_MS milliseconds (default 5000; 0 leaves SIGHUP as the only trigger). The issuer dashboard writes this file. A malformed rewrite is logged as revocations.reload_rejected and the previous list stays enforced. With a control plane, revocations come from signed bundles instead.

Warning

Whoever holds the issuer key can mint any identity. The binding proves possession of the client token, not the integrity of the workload: a stolen token-plus-assertion pair works until expiry or revocation.