From 8715af5e54c1bcd657309d87c4c56323a30d44a9 Mon Sep 17 00:00:00 2001 From: Tymofii Pidlisnyi Date: Thu, 5 Mar 2026 11:15:33 -0800 Subject: [PATCH 1/4] proposal: cascade revocation spec for agent delegation chains --- docs/cascade-revocation.md | 218 +++++++++++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 docs/cascade-revocation.md diff --git a/docs/cascade-revocation.md b/docs/cascade-revocation.md new file mode 100644 index 0000000..c3068e0 --- /dev/null +++ b/docs/cascade-revocation.md @@ -0,0 +1,218 @@ +# Cascade Revocation for Agent Identity Protocols + +**Version:** 0.1.0 +**Author:** Tymofii Pidlisnyi (@aeoess) +**Date:** March 2026 +**Status:** Draft Proposal +**Reference Implementation:** [agent-passport-system](https://github.com/aeoess/agent-passport-system) (Apache-2.0) + +--- + +## 1. Problem + +When AI agents delegate authority to other agents, revocation must propagate. If Agent A delegates to Agent B, and Agent B sub-delegates to Agent C, revoking A's delegation to B must also invalidate C's authority. Without cascade revocation, revoking a compromised agent leaves its downstream delegations active. + +This is not a theoretical concern. 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. + +No current agent identity protocol specifies how revocation propagates through delegation chains. + +## 2. Data Structures + +### 2.1 Delegation + +```typescript +interface Delegation { + delegationId: string // Unique identifier (UUID v4) + delegatorId: string // Agent granting authority + delegateId: string // Agent receiving authority + scope: string[] // Permitted action scopes + spendLimit?: number // Maximum spend in delegation currency + maxDepth: number // Maximum sub-delegation depth allowed + currentDepth: number // Current depth in the chain (0 = root) + expiresAt: string // ISO 8601 expiration timestamp + createdAt: string // ISO 8601 creation timestamp + parentDelegationId?: string // ID of parent delegation (null for root) + revoked: boolean // Revocation status + revokedAt?: string // ISO 8601 revocation timestamp + revokedReason?: string // Human-readable revocation reason + signature: string // Ed25519 signature by delegator +} +``` + +### 2.2 Revocation Record + +```typescript +interface RevocationRecord { + delegationId: string // Delegation being revoked + revokedBy: string // Agent performing the revocation + revokedAt: string // ISO 8601 timestamp + reason: string // Revocation reason + cascadeCount: number // Number of downstream delegations also revoked + signature: string // Ed25519 signature by revoker +} +``` + +### 2.3 Chain Registry + +The chain registry tracks parent-child relationships between delegations. It MUST be populated at delegation creation time, not reconstructed at revocation time. + +```typescript +interface ChainRegistry { + // Maps delegationId -> list of child delegationIds + children: Map + // Maps delegationId -> parent delegationId + parents: Map +} +``` + +**Rationale:** Reconstructing the tree at revocation time requires scanning all delegations, which is O(n) in the total number of delegations. The registry makes cascade O(k) where k is the number of descendants. + +## 3. Algorithm + +### 3.1 Cascade Revocation + +When a delegation is revoked, all downstream delegations MUST be revoked synchronously. + +``` +function cascadeRevoke(delegationId, reason, revokerKey): + 1. Look up delegation in store + 2. If already revoked, return (idempotent, no error) + 3. Mark delegation as revoked (set revoked=true, revokedAt, reason) + 4. Sign revocation record with revokerKey + 5. Get all children from chain registry + 6. For each child: + a. Recursively call cascadeRevoke(child.delegationId, reason, revokerKey) + 7. Emit revocation event + 8. Return RevocationRecord with cascadeCount +``` + +**Properties:** +- **Synchronous:** All descendants are 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, not an error. +- **Total:** There is no partial cascade. Either all descendants are revoked or the operation fails atomically. + +### 3.2 Batch Revocation by Agent + +Revoke all delegations granted TO a specific agent, with cascade. + +``` +function batchRevokeByAgent(agentId, reason, revokerKey): + 1. Find all delegations where delegateId == agentId + 2. For each delegation: + a. Call cascadeRevoke(delegation.delegationId, reason, revokerKey) + 3. Return list of RevocationRecords +``` + +**Use case:** An agent is compromised. The human principal revokes everything granted to that agent. All sub-delegations that agent created also die. + +### 3.3 Chain Validation + +Before any action is permitted, the entire delegation chain from the acting agent back to the human principal MUST be validated. + +``` +function validateChain(delegationIds[]): + 1. For each delegation in chain: + a. If revoked: return {valid: false, error: "revoked link"} + b. If expired: return {valid: false, error: "expired link"} + c. If not found in registry: return {valid: false, error: "unknown delegation"} + 2. For each adjacent pair (parent, child): + a. If parent.delegateId != child.delegatorId: return {valid: false, error: "chain break"} + 3. Return {valid: true} +``` + +## 4. Sub-delegation Constraints + +When an agent sub-delegates, the child delegation MUST be strictly narrower than the parent: + +- **Scope:** Child scope MUST be a subset of parent scope. scopeCovers(parentScope, childScope) must be true for every scope in the child. +- **Spend limit:** Child spend limit MUST NOT exceed parent spend limit. +- **Depth:** Child currentDepth MUST equal parent currentDepth + 1. Child currentDepth MUST NOT exceed parent maxDepth. +- **Expiry:** Child expiration MUST NOT exceed parent expiration. + +Violation of any constraint MUST cause sub-delegation to fail. This ensures that cascade revocation is always safe: revoking a parent can never leave a child with more authority than the parent had. + +## 5. Security Properties + +### 5.1 No Orphan Delegations + +Every non-root delegation MUST have a traceable parent in the chain registry. If a parent delegation is deleted (not just revoked), all children MUST be cascade-revoked first. + +### 5.2 No Double-Revoke Side Effects + +Revoking an already-revoked delegation MUST be idempotent. It MUST NOT re-emit events, re-sign records, or increment cascade counts. Implementations SHOULD check revocation status before traversing children. + +### 5.3 Revocation is Irreversible + +A revoked delegation CANNOT be un-revoked. If the same authority is needed again, a new delegation MUST be created. This prevents time-of-check/time-of-use attacks where a revoked delegation is temporarily reinstated. + +### 5.4 Branching Chains + +A single delegation MAY have multiple children (Agent A delegates to both Agent B and Agent C). Cascade revocation MUST traverse all branches. The traversal order is not specified, but all branches MUST be revoked before the operation returns. + +### 5.5 Event Propagation + +Implementations SHOULD support revocation event subscriptions so that dependent systems (task schedulers, commerce gateways, policy engines) can react to revocations in real time. + +```typescript +interface RevocationEvent { + delegationId: string + cascadeDepth: number // 0 for the directly revoked delegation + totalCascaded: number // Running count of all revoked in this cascade +} +``` + +## 6. 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. + +Suggested integration pattern: + +``` +1. Agent presents action request with delegationId +2. Policy engine calls validateChain() on the delegation chain +3. If chain is invalid (any link revoked/expired), deny immediately +4. If chain is valid, proceed to policy evaluation (scope, rate limits, etc.) +``` + +This ensures that revocation takes effect immediately without requiring policy engines to maintain their own revocation state. + +## 7. Adversarial Scenarios + +The following attacks MUST be mitigated by any conforming implementation: + +| Attack | Mitigation | +|--------|-----------| +| Replay revoked delegation | Chain validation checks revoked flag before any action | +| Sub-delegate after revocation | createReceipt/subDelegate checks delegation validity | +| Scope escalation via sub-delegation | Sub-delegation enforces strict scope narrowing | +| Spend limit escalation | Child spend limit capped at parent spend limit | +| Depth bomb (unbounded sub-delegation) | maxDepth enforced at sub-delegation time | +| Double-revoke event spam | Idempotent revocation, no re-emit on already-revoked | +| Orphan delegation after parent delete | Cascade revocation before deletion | + +## 8. Reference Implementation + +The Agent Passport System SDK implements this specification: + +- **npm:** agent-passport-system (v1.9.2) +- **Source:** [github.com/aeoess/agent-passport-system](https://github.com/aeoess/agent-passport-system) +- **File:** src/core/delegation.ts (~520 lines) +- **Tests:** 276 tests, 73 suites, including 23 adversarial scenarios +- **License:** Apache-2.0 + +Key functions: cascadeRevoke(), batchRevokeByAgent(), validateChain(), getDescendants(), onRevocation() + +--- + +## Appendix A: Scope Resolution + +Cascade revocation depends on correct scope matching for sub-delegation validation. The reference implementation uses a single scopeCovers(granted, required) function: + +- Exact match: code covers code +- Hierarchical: code covers code:deploy (parent covers child) +- Universal wildcard: * covers everything +- Prefix wildcard: commerce:* covers commerce and commerce:checkout +- No reverse: code:deploy does NOT cover code + +All scope checks across the protocol (delegation, policy evaluation, context enforcement, commerce validation) MUST use the same matching function to prevent inconsistencies. From af73a7553fa51129daf2c4afe098e7c663917d5e Mon Sep 17 00:00:00 2001 From: Tymofii Pidlisnyi Date: Thu, 12 Mar 2026 10:19:08 -0700 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20parentDelegationId=20type=20?= =?UTF-8?q?=E2=80=94=20string=20|=20null=20(not=20optional)=20to=20match?= =?UTF-8?q?=20null-for-root=20semantics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Copilot review: optional (?) implies undefined when absent, but the comment specifies null for root delegations. Changed to explicit string | null union type for consistency with ChainRegistry.parents map behavior. --- docs/cascade-revocation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cascade-revocation.md b/docs/cascade-revocation.md index c3068e0..156061b 100644 --- a/docs/cascade-revocation.md +++ b/docs/cascade-revocation.md @@ -31,7 +31,7 @@ interface Delegation { currentDepth: number // Current depth in the chain (0 = root) expiresAt: string // ISO 8601 expiration timestamp createdAt: string // ISO 8601 creation timestamp - parentDelegationId?: string // ID of parent delegation (null for root) + parentDelegationId: string | null // ID of parent delegation (null for root) revoked: boolean // Revocation status revokedAt?: string // ISO 8601 revocation timestamp revokedReason?: string // Human-readable revocation reason From 763d5cbe2f16e85c1897177fed96c25153bf620e Mon Sep 17 00:00:00 2001 From: Tymofii Pidlisnyi Date: Thu, 12 Mar 2026 10:30:01 -0700 Subject: [PATCH 3/4] rewrite: cascade revocation as JSON schema + Mintlify .mdx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback from @JamesCao: - Rewrote data structures as JSON Schema (2020-12 draft) matching AIP conventions - Moved spec to proposals/ directory - Converted docs to Mintlify .mdx format - Removed SDK reference — spec is now protocol-agnostic - Fixed parentDelegationId type: string | null (not optional) --- docs/cascade-revocation.md | 218 --------------------- docs/proposals/cascade-revocation.mdx | 133 +++++++++++++ spec/schema/cascade-revocation.schema.json | 167 ++++++++++++++++ 3 files changed, 300 insertions(+), 218 deletions(-) delete mode 100644 docs/cascade-revocation.md create mode 100644 docs/proposals/cascade-revocation.mdx create mode 100644 spec/schema/cascade-revocation.schema.json diff --git a/docs/cascade-revocation.md b/docs/cascade-revocation.md deleted file mode 100644 index 156061b..0000000 --- a/docs/cascade-revocation.md +++ /dev/null @@ -1,218 +0,0 @@ -# Cascade Revocation for Agent Identity Protocols - -**Version:** 0.1.0 -**Author:** Tymofii Pidlisnyi (@aeoess) -**Date:** March 2026 -**Status:** Draft Proposal -**Reference Implementation:** [agent-passport-system](https://github.com/aeoess/agent-passport-system) (Apache-2.0) - ---- - -## 1. Problem - -When AI agents delegate authority to other agents, revocation must propagate. If Agent A delegates to Agent B, and Agent B sub-delegates to Agent C, revoking A's delegation to B must also invalidate C's authority. Without cascade revocation, revoking a compromised agent leaves its downstream delegations active. - -This is not a theoretical concern. 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. - -No current agent identity protocol specifies how revocation propagates through delegation chains. - -## 2. Data Structures - -### 2.1 Delegation - -```typescript -interface Delegation { - delegationId: string // Unique identifier (UUID v4) - delegatorId: string // Agent granting authority - delegateId: string // Agent receiving authority - scope: string[] // Permitted action scopes - spendLimit?: number // Maximum spend in delegation currency - maxDepth: number // Maximum sub-delegation depth allowed - currentDepth: number // Current depth in the chain (0 = root) - expiresAt: string // ISO 8601 expiration timestamp - createdAt: string // ISO 8601 creation timestamp - parentDelegationId: string | null // ID of parent delegation (null for root) - revoked: boolean // Revocation status - revokedAt?: string // ISO 8601 revocation timestamp - revokedReason?: string // Human-readable revocation reason - signature: string // Ed25519 signature by delegator -} -``` - -### 2.2 Revocation Record - -```typescript -interface RevocationRecord { - delegationId: string // Delegation being revoked - revokedBy: string // Agent performing the revocation - revokedAt: string // ISO 8601 timestamp - reason: string // Revocation reason - cascadeCount: number // Number of downstream delegations also revoked - signature: string // Ed25519 signature by revoker -} -``` - -### 2.3 Chain Registry - -The chain registry tracks parent-child relationships between delegations. It MUST be populated at delegation creation time, not reconstructed at revocation time. - -```typescript -interface ChainRegistry { - // Maps delegationId -> list of child delegationIds - children: Map - // Maps delegationId -> parent delegationId - parents: Map -} -``` - -**Rationale:** Reconstructing the tree at revocation time requires scanning all delegations, which is O(n) in the total number of delegations. The registry makes cascade O(k) where k is the number of descendants. - -## 3. Algorithm - -### 3.1 Cascade Revocation - -When a delegation is revoked, all downstream delegations MUST be revoked synchronously. - -``` -function cascadeRevoke(delegationId, reason, revokerKey): - 1. Look up delegation in store - 2. If already revoked, return (idempotent, no error) - 3. Mark delegation as revoked (set revoked=true, revokedAt, reason) - 4. Sign revocation record with revokerKey - 5. Get all children from chain registry - 6. For each child: - a. Recursively call cascadeRevoke(child.delegationId, reason, revokerKey) - 7. Emit revocation event - 8. Return RevocationRecord with cascadeCount -``` - -**Properties:** -- **Synchronous:** All descendants are 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, not an error. -- **Total:** There is no partial cascade. Either all descendants are revoked or the operation fails atomically. - -### 3.2 Batch Revocation by Agent - -Revoke all delegations granted TO a specific agent, with cascade. - -``` -function batchRevokeByAgent(agentId, reason, revokerKey): - 1. Find all delegations where delegateId == agentId - 2. For each delegation: - a. Call cascadeRevoke(delegation.delegationId, reason, revokerKey) - 3. Return list of RevocationRecords -``` - -**Use case:** An agent is compromised. The human principal revokes everything granted to that agent. All sub-delegations that agent created also die. - -### 3.3 Chain Validation - -Before any action is permitted, the entire delegation chain from the acting agent back to the human principal MUST be validated. - -``` -function validateChain(delegationIds[]): - 1. For each delegation in chain: - a. If revoked: return {valid: false, error: "revoked link"} - b. If expired: return {valid: false, error: "expired link"} - c. If not found in registry: return {valid: false, error: "unknown delegation"} - 2. For each adjacent pair (parent, child): - a. If parent.delegateId != child.delegatorId: return {valid: false, error: "chain break"} - 3. Return {valid: true} -``` - -## 4. Sub-delegation Constraints - -When an agent sub-delegates, the child delegation MUST be strictly narrower than the parent: - -- **Scope:** Child scope MUST be a subset of parent scope. scopeCovers(parentScope, childScope) must be true for every scope in the child. -- **Spend limit:** Child spend limit MUST NOT exceed parent spend limit. -- **Depth:** Child currentDepth MUST equal parent currentDepth + 1. Child currentDepth MUST NOT exceed parent maxDepth. -- **Expiry:** Child expiration MUST NOT exceed parent expiration. - -Violation of any constraint MUST cause sub-delegation to fail. This ensures that cascade revocation is always safe: revoking a parent can never leave a child with more authority than the parent had. - -## 5. Security Properties - -### 5.1 No Orphan Delegations - -Every non-root delegation MUST have a traceable parent in the chain registry. If a parent delegation is deleted (not just revoked), all children MUST be cascade-revoked first. - -### 5.2 No Double-Revoke Side Effects - -Revoking an already-revoked delegation MUST be idempotent. It MUST NOT re-emit events, re-sign records, or increment cascade counts. Implementations SHOULD check revocation status before traversing children. - -### 5.3 Revocation is Irreversible - -A revoked delegation CANNOT be un-revoked. If the same authority is needed again, a new delegation MUST be created. This prevents time-of-check/time-of-use attacks where a revoked delegation is temporarily reinstated. - -### 5.4 Branching Chains - -A single delegation MAY have multiple children (Agent A delegates to both Agent B and Agent C). Cascade revocation MUST traverse all branches. The traversal order is not specified, but all branches MUST be revoked before the operation returns. - -### 5.5 Event Propagation - -Implementations SHOULD support revocation event subscriptions so that dependent systems (task schedulers, commerce gateways, policy engines) can react to revocations in real time. - -```typescript -interface RevocationEvent { - delegationId: string - cascadeDepth: number // 0 for the directly revoked delegation - totalCascaded: number // Running count of all revoked in this cascade -} -``` - -## 6. 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. - -Suggested integration pattern: - -``` -1. Agent presents action request with delegationId -2. Policy engine calls validateChain() on the delegation chain -3. If chain is invalid (any link revoked/expired), deny immediately -4. If chain is valid, proceed to policy evaluation (scope, rate limits, etc.) -``` - -This ensures that revocation takes effect immediately without requiring policy engines to maintain their own revocation state. - -## 7. Adversarial Scenarios - -The following attacks MUST be mitigated by any conforming implementation: - -| Attack | Mitigation | -|--------|-----------| -| Replay revoked delegation | Chain validation checks revoked flag before any action | -| Sub-delegate after revocation | createReceipt/subDelegate checks delegation validity | -| Scope escalation via sub-delegation | Sub-delegation enforces strict scope narrowing | -| Spend limit escalation | Child spend limit capped at parent spend limit | -| Depth bomb (unbounded sub-delegation) | maxDepth enforced at sub-delegation time | -| Double-revoke event spam | Idempotent revocation, no re-emit on already-revoked | -| Orphan delegation after parent delete | Cascade revocation before deletion | - -## 8. Reference Implementation - -The Agent Passport System SDK implements this specification: - -- **npm:** agent-passport-system (v1.9.2) -- **Source:** [github.com/aeoess/agent-passport-system](https://github.com/aeoess/agent-passport-system) -- **File:** src/core/delegation.ts (~520 lines) -- **Tests:** 276 tests, 73 suites, including 23 adversarial scenarios -- **License:** Apache-2.0 - -Key functions: cascadeRevoke(), batchRevokeByAgent(), validateChain(), getDescendants(), onRevocation() - ---- - -## Appendix A: Scope Resolution - -Cascade revocation depends on correct scope matching for sub-delegation validation. The reference implementation uses a single scopeCovers(granted, required) function: - -- Exact match: code covers code -- Hierarchical: code covers code:deploy (parent covers child) -- Universal wildcard: * covers everything -- Prefix wildcard: commerce:* covers commerce and commerce:checkout -- No reverse: code:deploy does NOT cover code - -All scope checks across the protocol (delegation, policy evaluation, context enforcement, commerce validation) MUST use the same matching function to prevent inconsistencies. diff --git a/docs/proposals/cascade-revocation.mdx b/docs/proposals/cascade-revocation.mdx new file mode 100644 index 0000000..69cc90a --- /dev/null +++ b/docs/proposals/cascade-revocation.mdx @@ -0,0 +1,133 @@ +--- +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 + 2. If already revoked, return (idempotent) + 3. Mark delegation as revoked (revoked=true, revokedAt, reason) + 4. Sign revocation record with revokerKey + 5. Get all children from chain registry + 6. For each child: recursively cascadeRevoke(child, 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** — no partial cascade. All descendants revoked or operation fails atomically. + +### 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[]): + 1. For each delegation in chain: + - If revoked: return invalid + - If expired: return invalid + - If not in registry: return invalid + 2. For each adjacent pair (parent, child): + - If parent.delegateId != child.delegatorId: return invalid + 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 +- **Spend limit** — child MUST NOT exceed parent spend limit +- **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` diff --git a/spec/schema/cascade-revocation.schema.json b/spec/schema/cascade-revocation.schema.json new file mode 100644 index 0000000..6630de2 --- /dev/null +++ b/spec/schema/cascade-revocation.schema.json @@ -0,0 +1,167 @@ +{ + "$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" + }, + "spendLimit": { + "type": "number", + "minimum": 0, + "description": "Maximum spend in delegation currency" + }, + "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)" + } + } + } + } +} From 0905c6a310c5ce19a777291d5f09984ea5e17fdf Mon Sep 17 00:00:00 2001 From: Tymofii Pidlisnyi Date: Thu, 12 Mar 2026 13:02:57 -0700 Subject: [PATCH 4/4] address all Copilot review feedback - Add currency field (ISO 4217) to Delegation schema - Fix cascadeRevoke pseudocode: childId not child.delegationId - Add explicit registry lookup step to validateChain - Clarify scopeCovers array algorithm for sub-delegation - Define transactional semantics for Total property - Document best-effort fallback for stores without transactions --- docs/proposals/cascade-revocation.mdx | 25 +++++++++++++--------- spec/schema/cascade-revocation.schema.json | 6 +++++- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/docs/proposals/cascade-revocation.mdx b/docs/proposals/cascade-revocation.mdx index 69cc90a..68105da 100644 --- a/docs/proposals/cascade-revocation.mdx +++ b/docs/proposals/cascade-revocation.mdx @@ -30,12 +30,12 @@ When a delegation is revoked, all downstream delegations MUST be revoked synchro ``` cascadeRevoke(delegationId, reason, revokerKey): - 1. Look up delegation in store + 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 children from chain registry - 6. For each child: recursively cascadeRevoke(child, reason, 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 ``` @@ -44,7 +44,7 @@ cascadeRevoke(delegationId, reason, revokerKey): - **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** — no partial cascade. All descendants revoked or operation fails atomically. +- **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 @@ -65,12 +65,17 @@ Before any action is permitted, the entire delegation chain back to the principa ``` validateChain(delegationIds[]): - 1. For each delegation in chain: + 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 - - If not in registry: return invalid - 2. For each adjacent pair (parent, child): - - If parent.delegateId != child.delegatorId: 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 ``` @@ -78,8 +83,8 @@ validateChain(delegationIds[]): When an agent sub-delegates, the child MUST be strictly narrower than the parent: -- **Scope** — child scope MUST be a subset of parent scope -- **Spend limit** — child MUST NOT exceed parent spend limit +- **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 diff --git a/spec/schema/cascade-revocation.schema.json b/spec/schema/cascade-revocation.schema.json index 6630de2..e34fd2f 100644 --- a/spec/schema/cascade-revocation.schema.json +++ b/spec/schema/cascade-revocation.schema.json @@ -30,10 +30,14 @@ "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 delegation currency" + "description": "Maximum spend in smallest unit of currency (e.g. cents). If omitted, no spend constraint" }, "maxDepth": { "type": "integer",