SCENARIO
The 3-agent chain
Orchestrator holds the root authority. It delegates research to Researcher, who in turn delegates writing to Writer. Each hop attenuates scope. If anyone in the chain misbehaves, the leaf agent's verification catches it.
Orchestrator [research:read, write:draft, delete]
|--> delegates to Researcher [research:read, write:draft] (drops `delete`)
|--> delegates to Writer [write:draft] (drops `research:read`)
STEP 1
Mint the root authority token
from aip_core.crypto import KeyPair
from aip_token.chained import ChainedToken
orchestrator_kp = KeyPair.generate()
authority = ChainedToken.create_authority(
issuer="aip:web:acme.com/orchestrator",
scopes=["research:read", "write:draft", "delete"],
budget_cents=500,
max_depth=3,
ttl_seconds=3600,
keypair=orchestrator_kp,
)
print("depth:", authority.current_depth()) # 0
The authority block is signed by the orchestrator's private key. max_depth=3 caps how many delegation hops can be appended. budget_cents=500 is the total ceiling for the chain — children may set their own ceilings, but the runtime tracks aggregate spend against this.
STEP 2
Delegate to the researcher (chain grows to depth 1)
from aip_a2a import append_delegation_block
to_researcher = append_delegation_block(
authority,
delegator="aip:web:acme.com/orchestrator",
delegate="aip:web:acme.com/researcher",
scopes=["research:read", "write:draft"], # subset of parent
context="research-task-2026-05-08", # required, non-empty
budget_cents=200, # narrower than 500
)
print("depth:", to_researcher.current_depth()) # 1
context: a per-task string that binds the delegation to a specific intent. A token issued for "research-task-1" is not the same authorisation as one for "research-task-2". Empty context is rejected at delegation time.STEP 3
Researcher delegates to writer (chain grows to depth 2)
# Researcher receives the chain via metadata.aip_token, verifies it,
# then attenuates further:
to_writer = append_delegation_block(
to_researcher,
delegator="aip:web:acme.com/researcher",
delegate="aip:web:acme.com/writer",
scopes=["write:draft"], # drops research:read
context="draft-summary",
budget_cents=50,
)
print("depth:", to_writer.current_depth()) # 2
STEP 4
Writer verifies the full chain — fails closed
from aip_a2a import A2AVerifyMiddleware
def writer_handler(body, *, context):
# Only runs if the chain is valid end-to-end.
return {"result": "drafted"}
mw = A2AVerifyMiddleware(
writer_handler,
own_aip_id="aip:web:acme.com/writer",
root_public_key_bytes=orchestrator_kp.public_key_bytes(),
required_scope="write:draft",
)
response = mw({"params": {"metadata": {"aip_token": to_writer.to_base64()}}})
print(response) # {'result': 'drafted'}
The verifier checks: every signature, scope-attenuation at every hop, depth ≤ max_depth, no expired block, declared budget non-negative, and that the final delegation targets the writer's own AIP id.
STEP 5
What you can't do
Try modifying the example to reproduce these — every one fails:
- Widen scope: in step 3 set
scopes=["write:draft", "delete"]. Researcher's parent scope did not includedelete, so the next verifier rejects withaip_scope_insufficient. - Drop context: set
context="".append_delegation_blockraisesValueErrorimmediately (spec §4 step 3). - Bypass depth: set
max_depth=1in step 1. The third delegation in step 3 raises a depth error before serializing. - Replay: let the token expire (set
ttl_seconds=1, sleep 2s). The leaf verifier rejects withaip_token_expired.
STEP 6
When to use max_depth vs budget
max_depth caps the structural extent of delegation. Use it to express "at most one specialist redelegation," or "no further hops past this point." It's a per-chain hard limit set at the root.
budget_cents caps the cumulative cost or work the chain can authorise. Each block declares its own ceiling; the runtime tracks aggregate spend. Use it to express "this whole task is worth at most $5."
They are complementary: depth limits structure, budget limits effect.
FULL EXAMPLE
Working code
The runnable version of this scenario lives at examples/a2a-multi-agent. Clone, install, run ./run.sh. For the normative spec, see spec §Delegation.