DEV Community

Jack M
Jack M

Posted on

A2A Authorization Ledger: Stop Agent Chains From Borrowing the Wrong Permissions

An AI agent rarely acts alone anymore. One agent researches, another writes, another calls tools, and a fourth may approve or retry the work. That chain feels powerful until one weak link borrows permissions it should never have had.

The next failure mode for production AI apps is not only “the model hallucinated.” It is “nobody can explain why this downstream agent was allowed to send that request.”

If you are building AI workflows for customers, internal teams, support operations, sales ops, analytics, or developer automation, you need more than a prompt that says “be careful.” You need an authorization ledger that records who delegated what, to which agent, for which purpose, under which limits, and whether every tool call stayed inside that chain of trust.

The goal is simple: every agent action should carry a verifiable permission story.

The problem: agent chains blur responsibility

A single-agent workflow is already hard to secure. A multi-agent workflow is harder because intent gets fragmented.

Imagine this workflow:

  1. A user asks an assistant to prepare renewal notes.
  2. The planner agent creates subtasks.
  3. A research agent pulls CRM and support data.
  4. A writing agent drafts the message.
  5. A send agent pushes it to email or Slack.

Each step may look harmless in isolation. The planner never sends an email. The research agent never writes to production. The writing agent only creates text. The send agent only executes a final action.

But the chain can still fail.

A prompt-injected support ticket may convince the research agent to include hidden instructions. The writing agent may transform those instructions into a confident recommendation. The send agent may inherit the original user's broad OAuth token and deliver the message to the wrong external recipient.

The issue is not only authorization at the final API call. The issue is authorization across the chain.

A safe system should answer these questions before every risky action:

  • Who requested this work?
  • Which agent delegated this step?
  • What scopes were delegated?
  • What constraints apply?
  • Was the task purpose preserved?
  • Did any agent try to expand its authority?
  • Is the tool call allowed for this tenant, user, workflow, and risk tier?
  • Can we replay the decision later?

If the answer lives only in logs, prompts, or vibes, the system is fragile.

What is an A2A authorization ledger?

An A2A authorization ledger is an append-only record of delegation and authorization decisions across agent-to-agent workflows.

It does not replace OAuth, API keys, IAM, row-level security, or your existing policy engine. It connects them.

Think of it as the permission receipt layer for multi-agent execution.

A good ledger records:

  • the original human or system principal
  • each agent in the chain
  • the task purpose
  • delegated scopes
  • constraints such as time, cost, count, tenant, recipient, and data class
  • policy decisions
  • tool calls
  • approvals
  • denials
  • expiry
  • evidence links

The ledger should be easy to query. When something goes wrong, you should not need to reconstruct a timeline from chat transcripts.

The architecture in one flow

Here is a simple model:

User request
  -> creates root delegation
  -> planner receives scoped delegation
  -> planner creates child delegation for research
  -> research calls read tools through policy sidecar
  -> writer receives only allowed context and draft scope
  -> send agent requests write action
  -> policy sidecar checks full delegation chain
  -> approval gate may pause risky action
  -> tool executes or is denied
  -> ledger stores the decision and evidence
Enter fullscreen mode Exit fullscreen mode

The key design choice is this: agents can request actions, but a policy layer decides whether those actions fit the delegation chain.

The core data model

Start with four objects.

1. Principal

A principal is the identity that originally authorized the work.

{
  "principal_id": "user_123",
  "principal_type": "human_user",
  "tenant_id": "tenant_acme",
  "auth_source": "oauth",
  "session_id": "sess_789"
}
Enter fullscreen mode Exit fullscreen mode

This must map to your real auth system. Do not invent a fake principal just because the request came through an agent.

2. Delegation

A delegation says one actor gave another actor limited authority for a specific purpose.

{
  "delegation_id": "del_01JABC",
  "parent_delegation_id": null,
  "issuer": "user_123",
  "subject": "planner_agent:v2",
  "tenant_id": "tenant_acme",
  "purpose": "prepare_renewal_summary",
  "scopes": ["crm:read", "tickets:read", "draft:create"],
  "constraints": {
    "max_tool_calls": 30,
    "max_cost_usd": 1.50,
    "allowed_data_classes": ["customer_profile", "support_ticket_summary"],
    "denied_data_classes": ["payment_card", "secret", "private_note"],
    "expires_at": "2026-08-29T12:30:00Z"
  },
  "created_at": "2026-08-29T12:00:00Z"
}
Enter fullscreen mode Exit fullscreen mode

Child delegations must be narrower than parent delegations. If the planner has crm:read, it can delegate crm:read:account_summary, but it should not delegate crm:write.

3. Authorization decision

Every tool call should produce a decision.

{
  "decision_id": "authz_01JXYZ",
  "run_id": "run_456",
  "delegation_id": "del_01JCHILD",
  "agent_id": "research_agent:v4",
  "tool": "crm.get_account",
  "action": "crm:read:account_summary",
  "resource": "account_456",
  "decision": "allow",
  "policy_version": "agent-authz-2026-08-29.3",
  "reason_codes": ["scope_match", "tenant_match", "budget_available"],
  "created_at": "2026-08-29T12:04:11Z"
}
Enter fullscreen mode Exit fullscreen mode

Reason codes matter. They make debugging faster and help you find policy gaps.

4. Tool receipt

The tool receipt records the actual execution outcome.

{
  "tool_receipt_id": "tool_01J999",
  "decision_id": "authz_01JXYZ",
  "tool": "crm.get_account",
  "status": "success",
  "input_hash": "sha256:...",
  "output_hash": "sha256:...",
  "cost_units": 1,
  "latency_ms": 240,
  "redactions_applied": ["email", "phone"],
  "created_at": "2026-08-29T12:04:12Z"
}
Enter fullscreen mode Exit fullscreen mode

Policy checks that catch real failures

A useful authorization ledger needs policy rules. These are the checks I would implement first.

Scope narrowing

Every child delegation must be equal to or narrower than its parent.

Bad:

Parent: tickets:read
Child: tickets:read, tickets:write, email:send
Enter fullscreen mode Exit fullscreen mode

Good:

Parent: tickets:read
Child: tickets:read:summary
Enter fullscreen mode Exit fullscreen mode

This prevents a planner agent from accidentally creating a more powerful worker agent.

Purpose binding

The action must fit the original purpose.

If the purpose is prepare_renewal_summary, the system may allow CRM reads and draft creation. It should not allow discount:apply or user:delete just because the same user has those permissions elsewhere.

Tenant match

Every delegation and tool call must include a tenant. The tenant must match the principal and the resource.

Data-class limits

Classify tool outputs before they enter context. A research agent may be allowed to read ticket summaries but not payment fields, secrets, raw credentials, private admin notes, or health data.

Budget limits

Authorization is not only about security. It is also about cost.

Track:

  • maximum model calls
  • maximum tool calls
  • maximum external API calls
  • maximum dollars or credits
  • maximum retries
  • maximum wall-clock time

When the budget is gone, the agent should ask for help, downgrade, or stop.

Write-action approvals

Some actions should pause even when technically allowed.

Examples:

  • sending external messages
  • issuing credits
  • deleting records
  • changing permissions
  • exporting data
  • running code against production

A ledger makes approvals cleaner because the reviewer sees the full chain, not just a button that says “approve.”

A minimal TypeScript policy check

Here is a simplified policy function. Real systems should use a policy engine, but the shape matters more than the tool.

type Delegation = {
  delegationId: string;
  parentDelegationId?: string;
  tenantId: string;
  subject: string;
  purpose: string;
  scopes: string[];
  constraints: {
    expiresAt: string;
    maxToolCalls?: number;
    allowedDataClasses?: string[];
    deniedDataClasses?: string[];
  };
};

type ToolRequest = {
  tenantId: string;
  agentId: string;
  action: string;
  resourceTenantId: string;
  dataClasses: string[];
  purpose: string;
};

function isScopeAllowed(scopes: string[], action: string) {
  return scopes.some(scope =>
    action === scope || action.startsWith(scope + ":")
  );
}

function authorizeToolCall(
  delegation: Delegation,
  request: ToolRequest,
  now = new Date()
) {
  const reasons: string[] = [];

  if (new Date(delegation.constraints.expiresAt) < now) {
    return { decision: "deny", reasons: ["delegation_expired"] };
  }

  if (delegation.tenantId !== request.tenantId || request.tenantId !== request.resourceTenantId) {
    return { decision: "deny", reasons: ["tenant_mismatch"] };
  }

  if (delegation.subject !== request.agentId) {
    return { decision: "deny", reasons: ["wrong_agent_subject"] };
  }

  if (!isScopeAllowed(delegation.scopes, request.action)) {
    return { decision: "deny", reasons: ["scope_missing"] };
  }

  if (delegation.purpose !== request.purpose) {
    return { decision: "deny", reasons: ["purpose_mismatch"] };
  }

  const denied = delegation.constraints.deniedDataClasses ?? [];
  if (request.dataClasses.some(dataClass => denied.includes(dataClass))) {
    return { decision: "deny", reasons: ["denied_data_class"] };
  }

  reasons.push("scope_match", "tenant_match", "purpose_match");
  return { decision: "allow", reasons };
}
Enter fullscreen mode Exit fullscreen mode

Where OPA or a sidecar fits

Open Policy Agent-style sidecars are attractive because they separate policy decisions from application code. Instead of sprinkling if agent can do X checks across every tool handler, each tool call asks a local or nearby policy service:

Can agent research_agent:v4 perform crm:read:account_summary
on account_456 for tenant_acme under delegation del_01JCHILD?
Enter fullscreen mode Exit fullscreen mode

The sidecar can load policy bundles, check the delegation chain, evaluate budgets, and return a signed decision. The app then stores that decision in the ledger.

Start with a policy module inside your backend. Move to a sidecar when policy reuse becomes painful across tools and services.

How to prevent permission drift between agents

Permission drift happens when each agent adds a tiny assumption. Use these guardrails:

Make delegation explicit

Never pass raw user tokens from agent to agent. Pass delegation IDs and short-lived tool tokens minted from those delegations.

Narrow on every hop

Each child delegation should remove power, not add it.

Bind context to permissions

If an agent receives customer context, record which delegation allowed it. If that context is reused later, the next agent should inherit the same restrictions.

Block privilege laundering

An agent should not be able to turn read access into write access by summarizing hidden instructions into a new task.

Treat “agent said user wanted it” as untrusted

The ledger should trust signed delegations and policy decisions, not model-generated claims about user intent.

What to log without creating a privacy problem

A ledger can become a liability if it stores too much. Keep it useful but lean.

Log these by default:

  • IDs for tenant, user, agent, run, delegation, decision, and tool receipt
  • scopes and constraints
  • policy version
  • allow or deny
  • reason codes
  • data classifications
  • hashes of inputs and outputs
  • cost and latency
  • approval references

Avoid storing these by default:

  • raw prompts with secrets
  • raw tool outputs
  • full customer records
  • OAuth tokens
  • private user messages not needed for audit
  • embeddings that cannot be deleted or scoped safely

For sensitive workflows, store detailed traces in encrypted, short-retention storage and link them from the ledger.

Common implementation mistakes

Mistake 1: using prompts as policy

A system prompt can instruct the agent. It cannot enforce authorization. If the tool accepts the call, the prompt is not a boundary.

Mistake 2: only checking the final tool call

By the time the final send or write happens, the harmful instruction may have passed through three agents. Check every hop.

Mistake 3: treating all agents as the same identity

assistant is not a useful identity. Use stable agent IDs with versions, roles, owners, and allowed tools.

Mistake 4: skipping deny logs

Deny logs are gold. They show attempted drift, missing scopes, bad tool design, and confusing UX.

Mistake 5: making approvals context-free

A human approval screen should show the delegation chain, risk reasons, resource, recipient, diff, and rollback plan. Otherwise reviewers rubber-stamp blind actions.

A practical rollout plan

You do not need to build the whole system in one sprint.

Phase 1: ledger-only visibility

Add delegation IDs, run IDs, agent IDs, tool receipt IDs, and decision logs. Even if policy is basic, visibility will show where risk lives.

Phase 2: hard-deny obvious violations

Deny expired delegations, tenant mismatches, missing scopes, wrong agent subjects, and denied data classes.

Phase 3: add budgets and approvals

Track tool counts, model spend, retries, and write-action approvals. This catches cost and trust failures before they become normal.

Phase 4: chain-aware policy

Evaluate the full parent-child delegation chain. Ensure every child is narrower than its parent and every action fits the original purpose.

Phase 5: customer-facing receipts

For high-trust workflows, expose safe receipts to admins: who authorized the work, what category of data was accessed, what action happened, and when.

Builder checklist

Before your next multi-agent workflow goes live, check this list:

  • [ ] Every run has a root delegation.
  • [ ] Every agent has a stable versioned identity.
  • [ ] Every child delegation is narrower than its parent.
  • [ ] Every tool call asks the policy layer before execution.
  • [ ] Every decision stores reason codes.
  • [ ] Tenant and resource scope are checked on every call.
  • [ ] Sensitive data classes are denied or redacted.
  • [ ] Budgets apply to model calls, tools, retries, and time.
  • [ ] Risky writes require approval.
  • [ ] The approval screen shows the full chain.
  • [ ] Deny logs are reviewed.
  • [ ] Receipts can be replayed during incidents.

Final thought

Multi-agent workflows make AI products feel more capable, but they also make responsibility harder to see. The answer is not to avoid agents. The answer is to make permission flow visible, narrow, and enforceable.

A2A authorization is not just an identity problem. It is a product trust problem.

If an agent chain can touch customer data, send messages, update records, run code, or spend money, every action should carry proof that it was allowed. Build the ledger before your users ask for the explanation.

FAQ

What is A2A authorization?

A2A authorization means checking what one AI agent is allowed to delegate or request from another AI agent. It is especially important when agents call tools, access customer data, or perform write actions across a workflow chain.

How is an A2A authorization ledger different from normal audit logs?

Normal audit logs often record what happened after the fact. An A2A authorization ledger records the permission chain behind the action: who delegated authority, which agent received it, what constraints applied, which policy allowed or denied it, and what tool receipt was produced.

Do small AI products need this much structure?

Not all at once. But even small products benefit from delegation IDs, scoped tool calls, tenant checks, and decision logs. Start with a simple ledger table and add chain-aware policy as workflows become riskier.

Should agents ever receive raw user OAuth tokens?

Usually no. A safer pattern is to keep OAuth tokens in your backend or secret broker, then mint short-lived, scoped tool permissions based on a delegation. The agent receives the ability to request work, not the raw credential.

Can prompts enforce A2A permissions?

Prompts can guide behavior, but they cannot enforce permissions. Authorization must happen outside the model in a tool gateway, API layer, sidecar, or policy service that can deny execution.

What should trigger human approval?

Trigger approval for external messages, destructive writes, permission changes, exports, high-cost runs, production code execution, billing changes, and any action that crosses a trust boundary. The approval view should include the delegation chain and risk reasons.

How does this help with prompt injection?

Prompt injection often works by tricking an agent into changing goals or using tools incorrectly. A ledger and policy layer reduce the damage by checking purpose, scope, tenant, data class, and approval requirements before tools execute.

Top comments (1)

Collapse
 
bert_programmer profile image
Bert Shim

Mistake 2 landed for me from an angle I wasn't expecting. The gate sitting in front of my tool calls started denying reads as well as writes.

A denied write is survivable. The agent sees the deny, replans, goes another way. A denied read took away the thing it needed to work out what state it was even in — I couldn't confirm whether an earlier action had gone through, because the channel I'd check it with was the one being blocked. Ended up verifying through an entirely different tool.

Probably worth separating in the policy design. Read denials and write denials have very different blast radii, even when the scope check that produced them is identical.