Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions docs/proposals/cascade-revocation.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
---
title: "Cascade Revocation for Agent Delegation Chains"
description: "Proposal for propagating revocation through agent delegation trees"
---

## Problem

When agents delegate authority to other agents, revocation must propagate. If Agent A delegates to Agent B, and B sub-delegates to C, revoking A→B must also invalidate C's authority. Without cascade revocation, revoking a compromised agent leaves its downstream delegations active.

In multi-agent systems where agents autonomously sub-delegate to specialists, a single compromised delegation can create an unbounded tree of active permissions that the human principal cannot recall.

## Data Structures

All data structures are defined in [cascade-revocation.schema.json](/spec/schema/cascade-revocation.schema.json).

The core objects are:

- **Delegation** — authority granted from one agent to another, with scope, spend limit, depth limit, and expiry
- **RevocationRecord** — signed proof that a delegation was revoked, including cascade count
- **ChainRegistry** — parent-child index populated at delegation creation time (not reconstructed at revocation time)
- **RevocationEvent** — event emitted for each revoked delegation in a cascade

The `parentDelegationId` field uses `string | null` (not optional). Root delegations set this to `null` explicitly. This aligns with the ChainRegistry.parents map behavior.

## Algorithm

### Cascade Revocation

When a delegation is revoked, all downstream delegations MUST be revoked synchronously.

```
cascadeRevoke(delegationId, reason, revokerKey):
1. Look up delegation in store by delegationId
2. If already revoked, return (idempotent)
3. Mark delegation as revoked (revoked=true, revokedAt, reason)
4. Sign revocation record with revokerKey
5. Get all child delegationIds from chain registry
6. For each childId: recursively cascadeRevoke(childId, reason, revokerKey)
7. Emit revocation event
8. Return RevocationRecord with cascadeCount
```

**Properties:**
- **Synchronous** — all descendants revoked in the same operation. No eventual consistency.
- **Deterministic** — same input always produces the same result.
- **Idempotent** — revoking an already-revoked delegation is a no-op.
- **Total (Transactional)** — no partial cascade. Either all descendants are revoked and their revocation records durably persisted, or none are. Implementations MUST execute steps 3-7 for the entire cascade within a single atomic transaction or equivalent mechanism that guarantees all-or-nothing visibility. In systems without multi-record transactions, implementations MUST document that cascade is best-effort and may leave partial state on failure.

### Batch Revocation by Agent

Revoke all delegations granted TO a specific agent, with cascade.

```
batchRevokeByAgent(agentId, reason, revokerKey):
1. Find all delegations where delegateId == agentId
2. For each: cascadeRevoke(delegation, reason, revokerKey)
3. Return list of RevocationRecords
```

Use case: an agent is compromised. The principal revokes everything granted to that agent. All sub-delegations also die.

### Chain Validation

Before any action is permitted, the entire delegation chain back to the principal MUST be validated.

```
validateChain(delegationIds[]):
0. Resolve delegations from registry:
- For each delegationId in delegationIds (in order provided):
- Lookup delegation in registry by delegationId
- If not found: return invalid ("unknown delegation")
- Append to orderedDelegations[]
1. For each delegation in orderedDelegations:
- If revoked: return invalid
- If expired: return invalid
2. For each adjacent pair (parent, child) in orderedDelegations:
- If parent.delegationId != child.parentDelegationId: return invalid ("invalid parent reference")
- If parent.delegateId != child.delegatorId: return invalid ("chain break")
3. Return valid
```

## Sub-delegation Constraints

When an agent sub-delegates, the child MUST be strictly narrower than the parent:

- **Scope** — child scope MUST be a subset of parent scope. For array-valued scopes: for each `requiredScope` in the child's scope array, there MUST exist some `grantedScope` in the parent's scope array such that `scopeCovers(grantedScope, requiredScope)` is true, where `scopeCovers` operates on individual scope strings.
- **Spend limit** — child MUST NOT exceed parent spend limit. Currency is explicit (ISO 4217) or inherited from protocol default.
- **Depth** — child currentDepth MUST equal parent currentDepth + 1, MUST NOT exceed parent maxDepth
- **Expiry** — child MUST NOT exceed parent expiration

Violation of any constraint MUST cause sub-delegation to fail.

## Security Properties

| Property | Requirement |
|----------|------------|
| No orphan delegations | Every non-root delegation MUST have a traceable parent |
| Idempotent revocation | Double-revoke MUST NOT re-emit events or increment counts |
| Irreversible | Revoked delegations CANNOT be un-revoked |
| Branching | Cascade MUST traverse all branches of multi-child delegations |
| Event propagation | Implementations SHOULD support revocation event subscriptions |

## Integration with Policy Engines

Cascade revocation is an identity-layer primitive. Policy engines (such as AIP's AgentPolicy) SHOULD reference delegation chain validity as a precondition for policy evaluation.

```
1. Agent presents action request with delegationId
2. Policy engine calls validateChain() on the delegation chain
3. If chain invalid (any link revoked/expired), deny immediately
4. If chain valid, proceed to policy evaluation
```

This ensures revocation takes effect immediately without requiring policy engines to maintain their own revocation state.

## Adversarial Scenarios

Any conforming implementation MUST mitigate the following:

| Attack | Mitigation |
|--------|-----------|
| Replay revoked delegation | Chain validation checks revoked flag before any action |
| Sub-delegate after revocation | Sub-delegation checks delegation validity |
| Scope escalation via sub-delegation | Strict scope narrowing enforced |
| Spend limit escalation | Child spend limit capped at parent |
| Depth bomb (unbounded sub-delegation) | maxDepth enforced at sub-delegation time |
| Double-revoke event spam | Idempotent revocation |
| Orphan delegation after parent delete | Cascade revocation before deletion |

## Scope Resolution

Cascade revocation depends on correct scope matching. All implementations MUST use a single scope matching function:

- Exact match: `code` covers `code`
- Hierarchical: `code` covers `code:deploy`
- Universal wildcard: `*` covers everything
- Prefix wildcard: `commerce:*` covers `commerce` and `commerce:checkout`
- No reverse: `code:deploy` does NOT cover `code`
171 changes: 171 additions & 0 deletions spec/schema/cascade-revocation.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://aip.io/schema/proposals/cascade-revocation.schema.json",
"title": "Cascade Revocation",
"description": "Data structures for cascade revocation in agent delegation chains",
"type": "object",
"$defs": {
"Delegation": {
"type": "object",
"description": "A delegation of authority from one agent to another",
"required": ["delegationId", "delegatorId", "delegateId", "scope", "maxDepth", "currentDepth", "expiresAt", "createdAt", "parentDelegationId", "revoked", "signature"],
"additionalProperties": false,
"properties": {
"delegationId": {
"type": "string",
"format": "uuid",
"description": "Unique identifier (UUID v4)"
},
"delegatorId": {
"type": "string",
"description": "Agent granting authority"
},
"delegateId": {
"type": "string",
"description": "Agent receiving authority"
},
"scope": {
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"description": "Permitted action scopes"
},
"currency": {
"type": "string",
"description": "Delegation currency (ISO 4217 code, e.g. 'USD'). If omitted, protocol default applies"
},
"spendLimit": {
"type": "number",
"minimum": 0,
"description": "Maximum spend in smallest unit of currency (e.g. cents). If omitted, no spend constraint"
},
"maxDepth": {
"type": "integer",
"minimum": 0,
"description": "Maximum sub-delegation depth allowed"
},
"currentDepth": {
"type": "integer",
"minimum": 0,
"description": "Current depth in the chain (0 = root)"
},
"expiresAt": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 expiration timestamp"
},
"createdAt": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 creation timestamp"
},
"parentDelegationId": {
"oneOf": [
{ "type": "string", "format": "uuid" },
{ "type": "null" }
],
"description": "ID of parent delegation (null for root)"
},
"revoked": {
"type": "boolean",
"default": false,
"description": "Revocation status"
},
"revokedAt": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 revocation timestamp"
},
"revokedReason": {
"type": "string",
"description": "Human-readable revocation reason"
},
"signature": {
"type": "string",
"description": "Ed25519 signature by delegator"
}
}
},
"RevocationRecord": {
"type": "object",
"description": "Record of a delegation revocation",
"required": ["delegationId", "revokedBy", "revokedAt", "reason", "cascadeCount", "signature"],
"additionalProperties": false,
"properties": {
"delegationId": {
"type": "string",
"format": "uuid",
"description": "Delegation being revoked"
},
"revokedBy": {
"type": "string",
"description": "Agent performing the revocation"
},
"revokedAt": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp"
},
"reason": {
"type": "string",
"description": "Revocation reason"
},
"cascadeCount": {
"type": "integer",
"minimum": 0,
"description": "Number of downstream delegations also revoked"
},
"signature": {
"type": "string",
"description": "Ed25519 signature by revoker"
}
}
},
"RevocationEvent": {
"type": "object",
"description": "Event emitted when a delegation is revoked",
"required": ["delegationId", "cascadeDepth", "totalCascaded"],
"properties": {
"delegationId": {
"type": "string",
"format": "uuid"
},
"cascadeDepth": {
"type": "integer",
"minimum": 0,
"description": "0 for the directly revoked delegation"
},
"totalCascaded": {
"type": "integer",
"minimum": 0,
"description": "Running count of all revoked in this cascade"
}
}
},
"ChainRegistry": {
"type": "object",
"description": "Tracks parent-child relationships between delegations. MUST be populated at delegation creation time.",
"required": ["children", "parents"],
"properties": {
"children": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": { "type": "string", "format": "uuid" }
},
"description": "Maps delegationId to list of child delegationIds"
},
"parents": {
"type": "object",
"additionalProperties": {
"oneOf": [
{ "type": "string", "format": "uuid" },
{ "type": "null" }
]
},
"description": "Maps delegationId to parent delegationId (null for root)"
}
}
}
}
}