Start deploying
·5 min read·a2a cloud

Scoped Grants: Agent-to-Agent Delegation Without Over-Privileging

Multi-agent systems break when one agent has to trust another with everything. A2A Cloud ships Ed25519-signed grants that scope workspace access, budget, and runtime per handoff — so trust is bounded, every time.

a2a cloudsecuritygrantsed25519multi-agentdelegationagentsplatform engineering

Scoped Grants: Agent-to-Agent Delegation Without Over-Privileging

Multi-agent systems sound great until you ask the obvious question: what exactly is one agent allowed to do when it calls another?

Most frameworks shrug. "Pass a token." "Use the user's session." "Trust the caller." That's not a security model. That's a foot-cannon.

A2A Cloud takes a different position: every agent-to-agent handoff carries a signed, scoped grant. The grant says exactly which workspace, which files, which LLM models, how much budget, and for how long. The receiving agent verifies the signature, enforces the scope, and runs with strictly bounded authority.

Let's look at how it works.

The Grant Object

Grants are concrete. They're a Pydantic model with named fields:

class Grant(BaseModel):
    grant_id: str
    issuer: str          # caller agent name or URL
    audience: str        # callee agent name or URL
    bucket: str          # workspace bucket
    mode: WorkspaceMode = WorkspaceMode.READ_ONLY
    allow_patterns: tuple[str, ...] = ("**",)
    deny_patterns: tuple[str, ...] = ()
    outputs_prefix: str | None = None
    write_prefixes: tuple[str, ...] = ()
    llm_models: tuple[str, ...] = ()
    llm_max_budget_usd: NonNegativeFloat | None = None
    llm_rpm_limit: PositiveInt | None = None
    llm_tpm_limit: PositiveInt | None = None
    expires_at: NonNegativeInt = 0
    issued_at: NonNegativeInt = 0
    nonce: str

Every field exists because someone needed to bound something. Workspace path? Mode? Allow/deny patterns? Write prefixes? LLM model list? Budget? RPM/TPM limits? Expiry? All in the token.

This is the difference between a session and a grant. A session says "who you are." A grant says "exactly what you may do, where, when, and how much."

Ed25519 Signing — The Real Kind

Grants are signed. With Ed25519. Asymmetric. Real crypto.

  • Issuer signs with A2A_GRANT_SIGNING_KEY (Ed25519 private key)
  • Receivers verify with A2A_GRANT_VERIFYING_KEY (Ed25519 public key)
  • Wire format: <base64url(json(payload))>.<base64url(signature)>

Asymmetric matters. The verifying key can be distributed widely. The signing key stays on the control plane. No shared secret to leak. No "if any agent gets owned, all agents are owned."

Receivers call verify_grant() which:

  1. Splits the token on .
  2. Decodes base64url payload + signature
  3. Verifies the Ed25519 signature
  4. Validates the Pydantic model
  5. Checks expires_at < time.time()
  6. Raises GrantInvalid if anything is off

No signature? Reject. Expired? Reject. Bad shape? Reject. Default deny.

Workspace Pattern Matching: fnmatch + Prefixes

Grants don't just say "yes/no." They say "these paths, not those." Two layers:

Allow/deny patterns (glob via fnmatch):

allowed = any(fnmatch(path, pat) for pat in self.allow_patterns)
denied = any(fnmatch(path, pat) for pat in self._access.deny_patterns)

Glob across the bucket with agents/research-agent/**, deny secrets/**, mix and match.

Write prefixes (exact prefix check):

def _matches_write_prefix(path, write_prefixes):
    clean = path.replace("\\", "/").strip("/")
    for prefix in write_prefixes:
        normalized = prefix.replace("\\", "/").strip("/")
        if normalized and (clean == normalized or clean.startswith(normalized + "/")):
            return True

Write access uses prefix matching, not globs. Tighter. Less surface. Path-traversal attempts (..) get normalized out before comparison.

This is what "least privilege" looks like when it's actually implemented.

The Handoff Path

When one agent calls another through the control plane, here's what happens:

  1. Fetch the target card — see what skills exist, what scopes they declare.
  2. Plan the grantplan_initial_grant() figures out exactly which patterns and prefixes the callee needs, no more.
  3. Mint the grantmint_grant_token() builds the payload, signs it, returns the token.
  4. POST to the skill — the grant ships in the request body, the call uses Accept: text/event-stream.
  5. Stream events back — mid-skill scope_request, question, progress events bridge back through PlatformHooks to the original caller.

The receiving agent unpacks the grant, verifies it, and its `ctx.workspace` operations get filtered through the grant's patterns automatically. The agent code doesn't have to think about it. The platform thinks about it.

Scope Expansion Mid-Skill

This is the elegant part.

If a callee needs more access than its initial grant carries, it calls ctx.request_scope(). That fires a scope_request event up the SSE stream. The control plane evaluates policy:

  • Auto-approve if the new scope is within bounds.
  • Gate via scope_approval_required modal if it isn't.
  • Mint a new, expanded grant on approval.
  • Deny otherwise.

The agent's runtime grant is dynamic. It can grow with consent. It never silently expands.

Compare this to "give the subagent the user's OAuth token." One is auditable, bounded, and revocable. The other is a security nightmare.

Budget and Rate Limits, In the Token

Grants don't stop at workspace scope. They carry:

  • llm_models — which models the callee may use
  • llm_max_budget_usd — hard spend cap
  • llm_rpm_limit — requests per minute
  • llm_tpm_limit — tokens per minute

When the callee uses ctx.llm, those caps get enforced by LiteLLM. A runaway agent can't bill you into oblivion. The bound is in the token.

Why This Matters For Multi-Agent Systems

The industry is racing toward multi-agent. Agent X calls Agent Y calls Agent Z. The composition is powerful. The trust model is usually catastrophic.

A2A Cloud's grant system is the answer:

  • Cryptographically signed — Ed25519, asymmetric, verifiable.
  • Workspace-scoped — fnmatch allow/deny + exact write prefixes.
  • Time-boundedexpires_at enforced on every verify.
  • Budget-bounded — LLM spend, rate, and model list.
  • Audience-bound — grants name their callee, can't be re-routed.
  • Issuer-tracked — every grant identifies who minted it.
  • Dynamic — scope can grow via explicit consent, never silently.
  • Default deny — invalid signatures reject. Stale tokens reject.

This is what serious multi-agent infrastructure looks like. Not "trust me bro." Cryptographic, time-bounded, scope-pinned authority — minted per handoff, verified on receipt.

Why You Should Care

If you're building agents: you don't have to design your own delegation protocol. The platform did it. You write ctx.call("other-agent") and the right grant gets minted and shipped.

If you're buying agents: you can run a marketplace agent knowing it can't pivot, can't escalate, can't burn your LLM budget, and can't access anything outside the bounded grant.

If you're an operator: the grant log is your audit trail. Every handoff, every scope expansion, every signature — recorded.

Multi-agent without grants is a security incident waiting to happen. A2A Cloud's grants are the safety net the agent economy actually needs.

Scoped Grants: Agent-to-Agent Delegation Without Over-Privileging · a2a cloud journal