DEV Community

Cover image for The Missing Protocol for AI Agent Authorization
Chudah
Chudah

Posted on

The Missing Protocol for AI Agent Authorization

Your agents have credentials. But can you prove that a specific agent action was authorized by a specific human instruction?


The assumption that broke

OAuth was built for one hop. Human clicks "Allow," client gets a token, token hits an API.

Agent pipelines don't work like that. A user tells an orchestrator to "research our top 3 competitors and email a summary to the board." The orchestrator spawns a research agent, which calls a web search tool, which feeds results into a summarizer, which hands output to an email agent, which calls the Gmail API. Five hops. Three agents. The original instruction is buried in a context window.

OAuth can't represent this. There is no standard mechanism for issuing a narrower downstream token at each hop so the same token gets forwarded, with the same permissions, to every service
in the chain.
The intent — why this access was granted — is nowhere in the token. Scope at each layer is whatever the developer hardcoded. And there's no record of who spawned whom, with what permissions, under whose authority.

If you're deploying agents in a regulated environment — finance, healthcare, anything where an auditor eventually shows up — you have nothing to hand them.


Four gaps that aren't fixable with configuration

1. Multi-hop delegation with scope reduction.
OAuth tokens don't narrow. A token with read write can be forwarded to any downstream service with the same permissions. You can build narrowing in application code, but it's not cryptographically enforced — a compromised agent in the middle can forward the full-privilege token.

2. Intent binding.
No OAuth token carries a commitment to the original instruction. The scope field says what is allowed; nothing says why. An agent with email:send might be running a legitimate task or exfiltrating data. The token is identical either way.

3. Delegation ancestry.
OAuth introspection tells you about one token. It won't tell you that token B came from token A, which came from token C, which exists because Alice said "research competitors." Reconstructing that graph from logs across services is fragile and breaks the moment one service logs differently.

4. Cascade revocation.
Revoke the orchestrator's token and every downstream agent should lose access immediately. OAuth doesn't do this. Each token is independent. Tracking and revoking every descendant is entirely your problem.


What a purpose-built credential looks like

I built the Attest Credential Standard (ACS-01) to fill these gaps. It's a JWT — same base format as OAuth — with additional claims that encode delegation depth, scope constraints, intent provenance, and chain ancestry.

A root credential, issued when a human starts a task:

{
  "iss": "https://api.attestdev.com",
  "sub": "agent:orchestrator-v1",
  "iat": 1742386800,
  "exp": 1742473200,
  "jti": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",

  "att_tid":    "f9e8d7c6-b5a4-3210-fedc-ba9876543210",
  "att_depth":  0,
  "att_scope":  ["email:read", "email:draft", "web:read"],
  "att_intent": "3b4c2a...64 hex chars...d2c1",
  "att_chain":  ["a1b2c3d4-e5f6-7890-abcd-ef1234567890"],
  "att_uid":    "user:alice"
}
Enter fullscreen mode Exit fullscreen mode

When the orchestrator delegates to an email agent:

{
  "sub": "agent:email-agent-v1",
  "jti": "c3d4e5f6-a7b8-9012-cdef-012345678901",

  "att_tid":    "f9e8d7c6-b5a4-3210-fedc-ba9876543210",
  "att_pid":    "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "att_depth":  1,
  "att_scope":  ["email:draft"],
  "att_intent": "3b4c2a...same hash...d2c1",
  "att_chain":  [
    "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "c3d4e5f6-a7b8-9012-cdef-012345678901"
  ],
  "att_uid":    "user:alice"
}
Enter fullscreen mode Exit fullscreen mode
  • att_scope narrowed. Parent had three permissions. Child got one. The server enforced the subset check before signing — not application code, the protocol itself.

  • att_intent didn't change. Same SHA-256 hash of Alice's original instruction, all the way down. A compliance officer can hash the instruction on file and verify it matches any credential in this tree.

  • att_chain grew. Parent's chain plus this credential's jti. Full ancestry in the token. No log correlation needed.

  • att_pid links to the parent. The delegation graph is reconstructable from any single token.

  • att_depth incremented. Capped at 10. Unbounded delegation depth is a security hazard.


How chains grow in practice

Human: "Review Q1 expenses and flag anomalies to the CFO"
  │
  ├─ orchestrator-v1          depth:0  scope:[finance:read, email:send]
  │    att_chain: [jti_A]
  │
  ├─ expense-analyzer-v1      depth:1  scope:[finance:read]
  │    att_chain: [jti_A, jti_B]
  │    att_pid: jti_A
  │
  └─ email-agent-v1           depth:2  scope:[email:send]
       att_chain: [jti_A, jti_B, jti_C]
       att_pid: jti_B
Enter fullscreen mode Exit fullscreen mode

Any service receiving jti_C can see the full path back to Alice's instruction. Scope narrowed at every hop. Intent hash identical across all three. Depth caps at 10.

Scope uses a resource:action grammar with wildcards (email:*, *:read). At delegation time, the server checks that every child scope entry is covered by a parent entry. You can't forge broader scope without the signing key.

Revoke jti_A and the entire tree dies. One call, one transaction.


Cascade revocation

SELECT jti FROM credentials
WHERE jti = $1 OR chain @> ARRAY[$1]::text[]
Enter fullscreen mode Exit fullscreen mode

One query finds every descendant via the chain array (GIN-indexed). Revoke the root, everything downstream is invalidated.


Tamper-evident audit log

Every event — issuance, delegation, verification, revocation, tool calls — goes into an append-only, hash-chained log:

entry_hash = SHA-256(prev_hash || event_type || jti || timestamp)
Enter fullscreen mode Exit fullscreen mode

Modify any historical entry and the chain breaks from that point forward. Partitioned by task tree (att_tid), so pulling a complete trail is one query.


Human-in-the-loop approval that doesn't evaporate

Every agent framework has "ask the human first." The problem is the approval disappears. It's a click event in memory. A month later, nobody can prove it happened.

Attest separates the approval UI from the proof. The framework keeps its own UX. Attest stamps it cryptographically:

1. Agent needs approval for a dangerous action
2. POST /v1/approvals → challenge_id
3. Framework shows the user whatever it normally would:
     "Run git push --force?  [Approve] [Reject]"
4. User clicks Approve → POST /v1/approvals/{id}/grant
   with the user's IdP session token
5. Attest verifies identity via OIDC, mints a new credential:
     att_hitl_req: "challenge_abc123"
     att_hitl_uid: "usr_alice"
     att_hitl_iss: "https://login.okta.com/..."
6. Agent proceeds. Approval is on record permanently.
Enter fullscreen mode Exit fullscreen mode

The credential is like any other — same chain, same scope, same intent hash — but with three extra claims binding a verified human identity to a specific approval. It's a signed artifact, not a log line.

Approvals propagate. If the approved credential gets delegated further, the HITL claims come along:

const approved = await attest.grantApproval(challengeId, idToken);
// approved.claims.att_hitl_uid === "usr_alice"

const formatter = await attest.delegate({
  parent_token: approved.token,
  child_agent:  'html-formatter-v1',
  child_scope:  ['email:send'],
});
// formatter.claims.att_hitl_uid === "usr_alice"  ← inherited
Enter fullscreen mode Exit fullscreen mode

Sub-agents don't re-prompt the human. The proof flows down the chain.


Action reporting

Credentials prove authorization. Auditors also want to know what happened. Agents report tool calls and lifecycle events into the same hash-chained log:

POST /v1/audit/report
{
  "token": "<credential>",
  "tool": "gmail:send",
  "outcome": "success",
  "meta": { "to": "board@example.com", "subject": "Competitor Analysis" }
}
Enter fullscreen mode Exit fullscreen mode

Worth being upfront: action reports are self-reported. An agent can lie, or crash before reporting. The credential chain is server-issued and cryptographically enforced. Action reporting is best-effort. For hard enforcement, the tool should verify the credential via AttestVerifier before executing.


In code

Go server with Postgres. TypeScript and Python SDKs on npm and PyPI.

Issue, delegate, revoke:

import { AttestClient } from '@attest-dev/sdk';

const attest = new AttestClient({
  baseUrl: 'https://api.attestdev.com',
  apiKey: 'att_live_...',
});

const root = await attest.issue({
  agent_id:    'orchestrator-v1',
  user_id:     'usr_alice',
  scope:       ['research:read', 'gmail:send'],
  instruction: 'Research competitors and email a summary to the board',
});

const child = await attest.delegate({
  parent_token: root.token,
  child_agent:  'email-agent-v1',
  child_scope:  ['gmail:send'],
});

await attest.reportAction({
  token:   child.token,
  tool:    'gmail:send',
  outcome: 'success',
});

await attest.revoke(root.claims.jti);

const audit = await attest.audit(root.claims.att_tid);
Enter fullscreen mode Exit fullscreen mode

Python with Anthropic SDK integration:

from attest import AttestClient
from attest.integrations.anthropic_sdk import AttestSession

attest = AttestClient(base_url="https://api.attestdev.com", api_key="...")

with AttestSession(
    client=attest,
    agent_id="orchestrator",
    user_id="usr_alice",
    scope=["web:read", "files:read"],
    instruction="Refactor the authentication module",
    system_prompt=SYSTEM_PROMPT,
) as session:
    child = session.delegate("code-reviewer", ["files:read"])
    review = run_reviewer_agent(child.token, "src/auth.py")
# Block exit revokes the root and all descendants.
Enter fullscreen mode Exit fullscreen mode

The system_prompt parameter SHA-256s the prompt and tool definitions into att_ack. If the agent's configuration is tampered with between issuance and execution, the checksum breaks. Deterministic, not a heuristic.

Verify credentials — no API key needed:

import { AttestVerifier } from '@attest-dev/sdk';

const verifier = new AttestVerifier({ orgId: 'org_abc123' });
const result = await verifier.verify(req.headers.authorization.slice(7));

if (!result.valid) return res.status(401).json({ error: 'invalid credential' });
if (!result.claims.att_scope.includes('gmail:send'))
  return res.status(403).json({ error: 'insufficient scope' });
Enter fullscreen mode Exit fullscreen mode

The verifier fetches the org's JWKS once, caches it, and validates signature, expiry, chain integrity, and live revocation. No API key. No runtime dependency on the Attest server.


What this doesn't solve

Misuse of valid credentials. If an agent with email:send gets prompt-injected, it can send emails the human never intended. The credential is valid — the scope covers the action. Attest limits blast radius and the intent hash helps detect it after the fact. It does not prevent it.

Application-level authorization. A resource server still decides whether to accept a credential. Attest provides the signed token with scope and ancestry. The server decides policy. Same split as OAuth.

Key storage. The RSA signing key is the root of trust. The server handles rotation and exposes JWKS for verifiers. HSM integration is on you.


Why this exists

Multi-agent systems are in production. Companies are deploying pipelines that send emails, execute code, and move money. The authorization model for most of them is the developer's API key, shared across every agent in the pipeline.

That works until something goes wrong — or until someone asks you to prove it was right. In sectors where algorithmic decisions carry legal weight, that question arrives eventually. Most teams currently have no answer.

ACS-01 is a deployable answer. The spec is open. The server is running. The question it answers is straightforward: can you prove this agent action was authorized?


Apache 2.0. Spec, server, and SDKs at github.com/chudah1/attest-dev.
Built with help from Claude Code. The spec, server,
SDKs, and demo were scaffolded and iterated in Claude
Code sessions.

Top comments (0)