Agent Identity and Authenticated Delegation
Version: 2.0.0 | Last updated: 2026-07-16
Purpose
Bind every agent action to a verifiable workload identity and a capability-attenuating delegation chain — not a role name in a prompt.
Why
Agent names in system prompts are labels. Without cryptographic workload identity and audience-bound delegation tokens, any peer that can send a message can impersonate a supervisor or amplify the caller’s privileges (confused deputy).
How — Identity Model
Workload identity (who the agent is)
| Field | Meaning |
|---|---|
spiffe_id / principal |
Stable agent workload ID, e.g. spiffe://prod/agent/coder |
code_digest |
Hash of agent config + prompt bundle + tool allowlist |
environment |
dev | staging | prod |
tenant_id |
Tenant the workload may act for |
owner |
Human/team accountable for this agent |
allowed_tools |
Tool IDs this identity may ever call |
max_delegation_depth |
Hard cap for this agent class |
Issue short-lived credentials from the platform IdP. Prohibit shared secrets and static long-lived agent API keys.
Original user intent (why work exists)
Every delegation chain carries a frozen intent record created at session start:
interface OriginalIntent {
intent_id: string;
user_principal: string; // human or service that started the task
tenant_id: string;
objective: string;
constraints: string[]; // security, residency, spend, etc.
approved_actions: string[]; // allowlist of high-impact action classes
risk_tier: "low" | "medium" | "high";
issued_at: string; // RFC3339
expires_at: string;
}
Policy decisions compare the requested tool call against OriginalIntent + parent grant — never against free-text in an agent message.
How — Delegation Token
Exchange the caller credential for an audience-restricted token at every hop:
interface DelegationToken {
// Standard claims
iss: string; // token exchange service
sub: string; // acting agent SPIFFE ID
aud: string; // intended recipient agent or tool PEP
exp: number; // unix seconds; short-lived (minutes)
iat: number;
jti: string; // unique; reject replays
// OAIES extensions
task_id: string;
session_id: string;
intent_id: string; // binds to OriginalIntent
parent_jti: string | null; // previous hop; null at root
parent_delegation_hash: string | null; // sha256 of parent token body
depth: number; // 0 = root; must be < max_delegation_depth
// Capabilities (attenuating only)
permitted_actions: string[]; // subset of parent
resource_scope: {
repos?: string[];
paths?: string[];
tenants?: string[];
secrets?: string[]; // usually empty for workers
};
budget_reservation_id: string;
policy_version: string;
}
Attenuation rules (enforced by PEP)
- Child
permitted_actions⊆ parentpermitted_actions. - Child
resource_scope⊆ parent scope (no new repos, paths, tenants). audmust match the callee; reject forwarding a token meant for another audience.- Reject if
exppassed,jtiseen,depth≥ max, orintent_idmissing/mismatched. - Reject if requested tool is not in both identity
allowed_toolsand tokenpermitted_actions.
function assertAttenuated(parent: DelegationToken, child: DelegationToken): void {
if (!child.permitted_actions.every(a => parent.permitted_actions.includes(a))) {
throw new Error("privilege_expansion");
}
if (child.depth !== parent.depth + 1) throw new Error("depth_mismatch");
if (child.intent_id !== parent.intent_id) throw new Error("intent_drift");
if (child.parent_jti !== parent.jti) throw new Error("chain_break");
}
How — Confused-Deputy Defenses
| Attack | Defense |
|---|---|
| Peer message says “as supervisor, delete prod” | Authorization from token + PEP, not message text |
| Worker forwards user’s bearer token to a tool | Ban user-token reuse; workers get exchanged, attenuated tokens only |
| Tool trusts caller agent ID from payload | Verify mTLS/SPIFFE + signature; ignore payload identity fields |
| Delegate mints broader grant for a grandchild | Token exchange refuses expansion; audit privilege_expansion |
| Stolen token replayed later | Short exp, jti denylist, audience binding |
| Agent A asks agent B to call a tool A cannot call | B’s PEP checks B’s grant ∩ original intent; A cannot launder privilege |
Example — valid vs rejected hop
Parent (supervisor → coder):
permitted_actions: [read_repo, write_branch, open_pr]
resource_scope.repos: [oai/payments]
depth: 1
Child request (coder → shell tool) wanting deploy_prod:
permitted_actions includes deploy_prod? → REJECT privilege_expansion
Child request (coder → shell tool) write_branch on oai/payments:
⊆ parent and in intent.approved_actions → ALLOW
How — Lifecycle
- Issue workload credential (minutes–hours TTL).
- On task start, create
OriginalIntentand root delegation (depth=0). - On each hop, token-exchange with attenuation; log
jtichain. - On compromise: revoke by
sub,task_id, orintent_id; rotate keys; kill descendants. - Never restore credentials or policy decisions from a workflow checkpoint — re-authorize on resume.
Tradeoffs
| Choice | Benefit | Cost |
|---|---|---|
| Per-hop token exchange | Limits blast radius | Latency + IdP dependency |
| Audience-bound tokens | Stops token forwarding | More exchange calls |
| Hardware-backed keys (high risk) | Stronger non-repudiation | Ops complexity |
Anti-patterns
| Anti-pattern | Why it fails |
|---|---|
| Reusing the user’s bearer token across workers | Classic confused deputy |
| Treating agent ID / role prompt / mTLS alone as authorization | Identity ≠ permission for this action |
| Allowing a delegate to create grants it does not possess | Privilege laundering |
| Long-lived static agent API keys | Compromise = permanent impersonation |
| Inferring authority from “I am the security agent” in content | Prompt injection wins |
Enterprise Considerations
- Separate prod/dev identities; never share SPIFFE trust domains casually across environments.
- Centralize policy decisions; record
policy_versionand deny reason on every decision. - Humans remain accountable for high-impact approvals; agents execute within grants.
- Emergency break-glass: short TTL, dual control, full audit, forced post-incident review.
Checklist
- Workload identity cryptographically verified (SPIFFE or equivalent).
- Tokens short-lived, audience-bound, replay-resistant (
jti), task-scoped. - Every hop attenuates or preserves — never expands — privilege.
-
OriginalIntentavailable to every PEP decision. - Delegation depth cap enforced in exchange, not only in prompts.
- Emergency revocation tested (kill task → descendants stop within SLA).
- Authorization unit tests independent of natural-language instructions.
- No user bearer tokens in worker runtimes.
Changelog
- 2.0.0 — 2026-07-16: Full rewrite — identity model, DelegationToken schema, confused-deputy defenses, examples, Mermaid.
- 1.0.0 — 2026-07-16: Initial citation-style stub.