Chain delegation enables hierarchical permission management for multi-agent systems. An orchestrator agent holds a root mandate and delegates narrower scopes to child agents, enforcing the principle of least privilege.
In multi-agent systems, every agent typically runs with the same ambient OS permissions. If any agent is compromised (prompt injection, jailbreak), it can access resources beyond its role.
Chain delegation solves this by:
┌─────────────────────────────────────────────────────────────────────┐
│ POST /v1/authorize (multi-scope root mandate) │
│ Orchestrator scopes: │
│ - action: browser.* | resource: https://www.amazon.com/* │
│ - action: fs.* | resource: **/workspace/data/** │
│ mandate_token: eyJhbGci... (depth=0, TTL=300s) │
└───────────────────────────────┬─────────────────────────────────────┘
│
┌───────────────────────┴───────────────────────┐
▼ ▼
┌───────────────────────┐ ┌───────────────────────┐
│ POST /v1/delegate │ │ POST /v1/delegate │
│ target: agent:scraper│ │ target: agent:analyst│
│ action: browser.* │ │ action: fs.write │
│ resource: amazon.com │ │ resource: workspace/ │
│ │ │ │
│ ✓ Subset of scope 1 │ │ ✓ Subset of scope 2 │
└───────────────────────┘ └───────────────────────┘
The orchestrator requests a multi-scope mandate covering all capabilities it needs to delegate. Child agents receive derived mandates with narrower scopes that are cryptographically linked to the parent.
Creates a derived mandate for a child agent.
Request:
{
"parent_mandate_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...",
"target_agent_id": "agent:scraper",
"requested_action": "browser.navigate",
"requested_resource": "https://www.amazon.com/dp/*",
"intent_hash": "scrape:product-page",
"ttl_seconds": 300
}Response (Success):
{
"mandate_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...",
"mandate_id": "m_200d9756fd87d9bd",
"expires_at": 1710000300,
"delegation_depth": 1,
"delegation_chain_hash": "sha256:7f3a2b1c..."
}Response (Error):
{
"code": "DELEGATION_EXCEEDS_SCOPE",
"message": "Requested scope (fs.write, *) exceeds parent scope (browser.*, https://*)"
}Request authorization for multiple scopes in a single call:
curl -X POST http://127.0.0.1:8787/v1/authorize \
-H "Content-Type: application/json" \
-d '{
"principal": "agent:orchestrator",
"scopes": [
{"action": "browser.*", "resource": "https://www.amazon.com/*"},
{"action": "fs.*", "resource": "**/workspace/data/**"}
],
"intent_hash": "orchestrate:ecommerce:run-123"
}'The response includes scopes_authorized showing which scopes matched:
{
"allowed": true,
"mandate_token": "m_abc123...",
"scopes_authorized": [
{"action": "browser.*", "resource": "https://www.amazon.com/*", "matched_rule": "allow-browser-https"},
{"action": "fs.*", "resource": "**/workspace/data/**", "matched_rule": "allow-workspace-fs"}
]
}| Property | Description |
|---|---|
| Scope Narrowing | Child scope must be ⊆ at least one parent scope (OR semantics for multi-scope parents) |
| TTL Capping | Child TTL is always capped to the parent's remaining TTL |
| Cascade Revocation | Revoking a parent mandate invalidates all derived child mandates (O(1) via HashSet) |
| Cryptographic Linking | delegation_chain_hash ties each child to its parent for audit trail |
| Depth Limits | Max delegation depth (default: 5) prevents infinite chains |
The sidecar automatically supports chain delegation. Key CLI arguments:
./predicate-authorityd \
--policy-file policy.yaml \
--mandate-ttl-s 300 \
run| Flag | Description | Default |
|---|---|---|
--policy-file | Policy file (JSON/YAML) with rules | Required |
--mandate-ttl-s | Default mandate TTL in seconds | 300 |
--port | HTTP port for the sidecar | 8787 |
--host | Bind host | 127.0.0.1 |
--log-level | Logging level (trace/debug/info/warn/error) | info |
Important: CLI arguments must come before the subcommand:
# Correct
./predicate-authorityd --port 9000 --policy-file policy.yaml run
# Wrong
./predicate-authorityd run --port 9000The sidecar validates that child scopes are subsets of parent scopes using wildcard matching:
| Parent Action | Child Action | Result |
|---|---|---|
browser.* | browser.navigate | ✓ Valid |
browser.* | fs.write | ✗ Rejected |
fs.* | fs.read | ✓ Valid |
* | any.action | ✓ Valid |
| Parent Resource | Child Resource | Result |
|---|---|---|
https://www.amazon.com/* | https://www.amazon.com/dp/B123 | ✓ Valid |
/workspace/data/ | /app/workspace/data/reports/analysis.json | ✓ Valid |
/workspace/ | /etc/passwd | ✗ Rejected |
https://* | http://internal:8080 | ✗ Rejected (scheme mismatch) |
| Code | HTTP | Description |
|---|---|---|
INVALID_PARENT_MANDATE | 401 | Parent mandate signature verification failed |
PARENT_MANDATE_EXPIRED | 401 | Parent mandate has expired |
PARENT_MANDATE_REVOKED | 403 | Parent mandate has been revoked |
DELEGATION_EXCEEDS_SCOPE | 403 | Requested scope is not a subset of parent scope |
MAX_DELEGATION_DEPTH_EXCEEDED | 403 | Delegation chain exceeds maximum depth (default: 5) |
Here's how chain delegation works in a CrewAI e-commerce demo:
curl -X POST http://127.0.0.1:8787/v1/authorize \
-H "Content-Type: application/json" \
-d '{
"principal": "agent:orchestrator",
"scopes": [
{"action": "browser.*", "resource": "https://www.amazon.com/*"},
{"action": "fs.*", "resource": "**/workspace/data/**"}
],
"intent_hash": "orchestrate:ecommerce-monitoring"
}'Response:
{
"allowed": true,
"mandate_token": "eyJhbGciOiJFUzI1NiIs...",
"mandate_id": "m_bc3a42ef63d45fc0"
}curl -X POST http://127.0.0.1:8787/v1/delegate \
-H "Content-Type: application/json" \
-d '{
"parent_mandate_token": "eyJhbGciOiJFUzI1NiIs...",
"target_agent_id": "agent:scraper",
"requested_action": "browser.*",
"requested_resource": "https://www.amazon.com/*",
"intent_hash": "scrape:amazon-products"
}'Response:
{
"mandate_token": "eyJhbGciOiJFUzI1NiIs...",
"mandate_id": "m_200d9756fd87d9bd",
"expires_at": 1710000300,
"delegation_depth": 1,
"delegation_chain_hash": "sha256:7f3a..."
}curl -X POST http://127.0.0.1:8787/v1/delegate \
-H "Content-Type: application/json" \
-d '{
"parent_mandate_token": "eyJhbGciOiJFUzI1NiIs...",
"target_agent_id": "agent:analyst",
"requested_action": "fs.write",
"requested_resource": "**/workspace/data/reports/**",
"intent_hash": "analyze:price-report"
}'Response:
{
"mandate_token": "eyJhbGciOiJFUzI1NiIs...",
"mandate_id": "m_079e1228ae8dc257",
"expires_at": 1710000300,
"delegation_depth": 1,
"delegation_chain_hash": "sha256:9c2d..."
}[Delegation] Requesting multi-scope root mandate for orchestrator...
✓ Root mandate issued: bc3a42ef63d45fc0 (depth=0)
[Delegation] Delegating to agent:scraper...
✓ Scraper mandate: 200d9756fd87d9bd (depth=1)
[Delegation] Delegating to agent:analyst...
✓ Analyst mandate: 079e1228ae8dc257 (depth=1)
When a parent mandate is revoked, all derived mandates become invalid:
# Revoke the orchestrator's mandate
curl -X POST http://127.0.0.1:8787/v1/revoke \
-H "Content-Type: application/json" \
-d '{"mandate_id": "m_bc3a42ef63d45fc0"}'Now any attempt by agent:scraper or agent:analyst to use their derived mandates will fail:
{
"code": "PARENT_MANDATE_REVOKED",
"message": "Parent mandate has been revoked"
}The sidecar maintains an O(1) revocation cache (HashSet) for instant revocation checks.
| Metric | Target | Actual |
|---|---|---|
| Authorization latency | < 1ms p99 | 0.2-0.8ms |
| Delegation issuance | < 10ms p99 | ~5ms |
| Revocation check | < 1μs | O(1) HashSet |
| Memory footprint | < 50MB | ~15MB idle |