Cascade Revocation: The Boring Rule That Saves Multi-Agent Systems
When you revoke an agent's authority, every agent downstream should lose theirs too. This is obvious. Most systems don't implement it.
The Delegation Chain
Multi-agent systems delegate. An orchestrator agent delegates to specialist agents. Those agents may delegate further. You end up with chains:
1Orchestrator (A)
2├── Scraper Agent (B)
3│ └── Browser Tool (C)
4└── Analyst Agent (D)
5 └── File Writer (E)A authorizes B. B authorizes C. A also authorizes D. D authorizes E.
Each delegation narrows scope. The orchestrator can "browse and analyze." The scraper can only "browse amazon.com." The browser tool can only "navigate to product pages."
This is the happy path. The system works. Agents stay in their lanes.
Now: what happens when you revoke A's authority?
The Revocation Problem
In a naive implementation, revoking A does nothing to B, C, D, or E. Each agent holds its own credentials. B has a token that says "I can browse." That token is still valid. B keeps browsing.
This creates stale authority—credentials that outlive the trust relationship that created them.
Consider the failure modes:
Scenario 1: Compromised Orchestrator
You detect that the orchestrator has been prompt-injected. You revoke its credentials. But the scraper and analyst are still running with delegated authority. The attacker pivots to the sub-agents.
Scenario 2: Task Completion
The orchestrator finishes its task. The parent system revokes the orchestrator's mandate. But the scraper is still looping on a pagination handler, burning compute and rate limits.
Scenario 3: Policy Change
Your security team updates policy: no more access to competitor websites. You revoke the orchestrator's mandate. The scraper, which was mid-crawl on competitor.com, continues with its old credentials.
In each case, the child agents should have stopped. They didn't, because their authority wasn't linked to the parent's.
Cascade Revocation
The rule is simple: revoking a parent invalidates all descendants.
1Revoke A
2├── B loses authority (derived from A)
3│ └── C loses authority (derived from B)
4└── D loses authority (derived from A)
5 └── E loses authority (derived from D)When the orchestrator's mandate is revoked, every agent in the tree loses its permissions. Not eventually. Immediately.
This requires two things:
- Lineage tracking: Each mandate knows its parent
- Revocation checking: Before honoring a mandate, verify the entire chain is valid
Here's how it works in practice. Each mandate contains:
1{
2"mandate_id": "m_7f3a...",
3"parent_mandate_id": "m_2b8c...",
4"delegation_chain_hash": "sha256:abc123...",
5"delegation_depth": 2
6}When a system receives mandate m_7f3a..., it doesn't just validate that mandate's signature and expiry. It checks:
- Is
m_7f3a...revoked? If yes, deny. - Is
m_2b8c...(the parent) revoked? If yes, deny. - Walk up the chain until you hit the root. Any revocation = deny.
The delegation_chain_hash provides cryptographic proof of lineage. You can't forge a mandate that claims a different parent.
The Strict Subset Rule
Cascade revocation is half the solution. The other half is scope attenuation: ensuring that child scopes never exceed parent scopes.
Consider this delegation:
1Orchestrator: {action: "browser.*", resource: "https://amazon.com/*"}
2 ↓ delegates to
3Scraper: {action: "browser.navigate", resource: "https://amazon.com/dp/*"}The scraper's scope is a strict subset of the orchestrator's:
| Check | Parent Scope | Child Scope | Valid? |
|---|---|---|---|
| Action | browser.* | browser.navigate | ✓ Subset |
| Resource | https://amazon.com/* | https://amazon.com/dp/* | ✓ Subset |
This is valid delegation. Now consider an invalid attempt:
1Orchestrator: {action: "browser.*", resource: "https://amazon.com/*"}
2 ↓ attempts to delegate
3Scraper: {action: "fs.write", resource: "/etc/passwd"}| Check | Parent Scope | Child Scope | Valid? |
|---|---|---|---|
| Action | browser.* | fs.write | ✗ Different namespace |
| Resource | https://amazon.com/* | /etc/passwd | ✗ Different type |
The delegation engine rejects this. The orchestrator can't grant authority it doesn't have.
This matters for cascade revocation because it bounds the blast radius. When you revoke the orchestrator, you know exactly what scopes become invalid—only those that were subsets of what the orchestrator had.
Why Inheritance Doesn't Work
An alternative design: instead of cascade revocation, let child mandates "inherit" their parent's validity. If the parent is revoked, the child automatically becomes invalid because it references the parent.
This sounds equivalent but has a critical flaw: you can't prove a negative.
To validate an inherited mandate, you need to prove the parent wasn't revoked. But revocation lists can be partitioned, delayed, or unavailable. The child mandate holder can claim "I didn't know the parent was revoked."
The correct design requires fresh mandate issuance: each mandate is independently signed with its own expiry, revocation is checked at validation time (not issuance time), and short TTLs ensure mandates expire even if revocation propagation fails.
A scraper mandate with TTL=60s will expire in one minute regardless of whether revocation propagated. If the orchestrator is revoked, at worst the scraper has 60 seconds of stale authority. Compare to a long-lived token that might persist for hours.
Short TTLs as Defense in Depth
Cascade revocation handles the active case: you know something is wrong and trigger revocation.
Short TTLs handle the passive case: you don't know something is wrong, but authority expires anyway.
Consider the timeline:
1t=0: Orchestrator gets mandate (TTL=300s)
2t=10: Orchestrator delegates to Scraper (TTL=300s, capped to 290s remaining)
3t=100: Orchestrator compromised, but nobody knows yet
4t=290: Scraper mandate expires naturally
5t=300: Orchestrator mandate expires naturallyEven without explicit revocation, the scraper's authority is bounded to the orchestrator's remaining TTL at delegation time. The child can never outlive the parent.
In our production systems, we enforce:
| Mandate Type | Maximum TTL |
|---|---|
| Root mandates | 5 minutes maximum |
| Delegated mandates | Capped to parent's remaining TTL |
| Per-action mandates | 30 seconds or less |
This creates a natural "heartbeat." Agents must periodically re-request authority. If the delegation chain is broken, they can't renew.
Multi-Scope Delegation
Real orchestrators need authority over multiple action types. An e-commerce orchestrator might need:
browser.*for web scrapingfs.*for data storagehttp.*for API calls
Rather than issuing three separate mandates, we support multi-scope mandates:
1{
2"mandate_id": "m_root...",
3"principal": "agent:orchestrator",
4"scopes": [
5 {"action": "browser.*", "resource": "https://amazon.com/*"},
6 {"action": "fs.*", "resource": "**/workspace/data/**"}
7],
8"delegation_depth": 0
9}When the orchestrator delegates to the scraper, the scraper requests a subset:
1{
2"parent_mandate_id": "m_root...",
3"requested_scope": {"action": "browser.*", "resource": "https://amazon.com/*"}
4}The delegation engine validates:
- Parent mandate is valid and not revoked
- Requested scope is a subset of at least one parent scope (OR semantics)
- Delegation depth hasn't exceeded maximum
The scraper gets a mandate for browser.* only. It can't request fs.* because that wasn't part of its delegation request—even though the parent had it.
This is scope attenuation in action. The orchestrator's multi-scope mandate becomes multiple single-scope mandates as it flows down the tree.
Implementation: The E-Commerce Demo
Here's a concrete example from a multi-agent e-commerce system:
1Orchestrator (depth=0, TTL=300s)
2├── Multi-scope mandate:
3│ ├── browser.* → https://amazon.com/*
4│ └── fs.* → **/workspace/data/**
5│
6├── Scraper (depth=1, TTL≤290s)
7│ └── browser.navigate → https://amazon.com/dp/*
8│
9└── Analyst (depth=1, TTL≤290s)
10 └── fs.write → **/workspace/data/reports/**The scraper and analyst are siblings—both delegated from the same root mandate. If the orchestrator is revoked:
1[Revoke] mandate_id=m_root...
2↳ [Invalid] agent:scraper mandate (parent revoked)
3↳ [Invalid] agent:analyst mandate (parent revoked)Both children lose authority immediately. The scraper can't complete its crawl. The analyst can't write its report. The system halts cleanly.
Now consider what happens without cascade revocation:
1[Revoke] orchestrator credentials
2↳ [Still Valid] scraper token (independent credential)
3↳ [Still Valid] analyst token (independent credential)The scraper keeps crawling. The analyst keeps writing. The orchestrator is gone but its children persist as orphans with stale authority.
The Validation Path
Every system that receives a mandate should validate the full chain:
1def validate_mandate(mandate):
2 # Step 1: Verify signature
3 if not verify_signature(mandate):
4 return DENY("invalid signature")
5
6 # Step 2: Check expiry
7 if mandate.expires_at < now():
8 return DENY("expired")
9
10 # Step 3: Check direct revocation
11 if is_revoked(mandate.mandate_id):
12 return DENY("revoked")
13
14 # Step 4: Check parent chain (cascade)
15 if mandate.parent_mandate_id:
16 parent = lookup_mandate(mandate.parent_mandate_id)
17 if not validate_mandate(parent): # Recursive
18 return DENY("parent invalid")
19
20 # Step 5: Verify scope is subset of parent
21 if mandate.parent_mandate_id:
22 parent = lookup_mandate(mandate.parent_mandate_id)
23 if not is_subset(mandate.scope, parent.scope):
24 return DENY("scope exceeds parent")
25
26 return ALLOWThis is the "boring" part. Every validation walks the chain. Every validation checks revocation. Every validation verifies scope subset.
It's not clever. It's not optimized. It's correct.
Why This Matters
Multi-agent systems are proliferating. Orchestrators spin up specialist agents. Agents delegate to tools. Tools delegate to sub-tools. The chains get deep.
Without cascade revocation, you can't cleanly terminate a delegation tree. Revoking the root leaves orphans. Orphans have stale authority. Stale authority is a security liability.
The rule is boring: parent revocation invalidates the subtree. But boring rules enforced consistently are what make systems trustworthy.
- Short TTLs provide defense in depth. Even if revocation fails to propagate, mandates expire.
- Strict subset rules prevent escalation. Children can't request more than parents have.
- Fresh mandate issuance prevents inheritance games. Each mandate is independently validated.
These are the mechanics that make multi-agent delegation safe. Not prompt engineering. Not validator agents. Infrastructure that enforces invariants regardless of what the models believe.
Conclusion
Cascade revocation isn't a feature. It's a requirement.
If you're building multi-agent systems with delegation, every mandate needs:
- A parent reference (lineage)
- A chain hash (cryptographic proof)
- A short TTL (bounded lifetime)
- Subset-validated scope (attenuation)
Revocation must propagate instantly through the tree. Validation must check the entire chain. There are no shortcuts.
The alternative is stale authority—credentials that outlive the trust relationships that created them. That's how compromised orchestrators pivot to sub-agents. That's how completed tasks leave zombie processes. That's how policy changes fail to take effect.
Cascade revocation is the boring rule that prevents all of this. Implement it. Enforce it. Trust nothing else.