Delegation Chains
Secure capability delegation between agents with scope narrowing enforcement and chain depth limits.
Overview
Delegation allows Agent A to authorize Agent B to act on its behalf with a narrowed set of capabilities. AgentTrust ID enforces that delegated scopes can only shrink (never expand) and limits chain depth to prevent unbounded trust propagation.
How It Works
Each hop in the chain:
- Narrows scope — child scope must be a subset of parent scope
- Has its own TTL — each delegation expires independently
- Can be independently revoked — revoking any link breaks the chain
- Is org-scoped — delegations only work within the same organization
Constraints
| Rule | Value | Description |
|---|---|---|
| Max chain depth | 5 | Maximum number of delegation hops |
| Self-delegation | No | Agent cannot delegate to itself |
| Empty scope | No | At least one capability must be delegated |
| Default TTL | 3600s | 1 hour if ttl_seconds not specified |
| Scope widening | No | Child scope must be subset of parent scope |
API Reference
POST /api/v1/delegations — Create Delegation
curl -X POST http://localhost:8080/api/v1/delegations \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"from_agent_id": "agent-a-uuid",
"to_agent_id": "agent-b-uuid",
"scope": ["web_search", "code_exec"],
"ttl_seconds": 7200,
"restrictions": {
"max_calls": 100,
"allowed_domains": ["example.com"]
}
}'Response (201):
{
"delegation": {
"id": "delegation-uuid",
"from_agent_id": "agent-a-uuid",
"to_agent_id": "agent-b-uuid",
"org_id": "org-uuid",
"scope": ["web_search", "code_exec"],
"restrictions": {"max_calls": 100, "allowed_domains": ["example.com"]},
"delegation_chain": [],
"token_hash": "a1b2c3d4e5f6...",
"expires_at": "2026-02-28T14:00:00Z",
"created_at": "2026-02-28T12:00:00Z"
}
}Field notes:
token_hash— SHA-256 hash of a cryptographically random 32-byte token, generated server-side at creation time. Stored in the database for token verification. Omitted from JSON when empty.restrictions— Stored as a JSON object (map[string]interface{}) but is not enforced byCreateDelegation()orVerifyChain(). It is metadata for application-level use only (e.g., your application can readmax_callsorallowed_domainsand enforce them in its own logic). Defaults to{}if not provided.delegation_chain— Array of ancestor delegation IDs. Empty for root delegations.
Extending a Chain
To create a sub-delegation from an existing one, pass parent_delegation_id:
curl -X POST http://localhost:8080/api/v1/delegations \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"from_agent_id": "agent-b-uuid",
"to_agent_id": "agent-c-uuid",
"scope": ["web_search"],
"parent_delegation_id": "delegation-uuid",
"ttl_seconds": 3600
}'Chain building mechanics: When extending via parent_delegation_id, the new delegation’s chain is built as chain = parent.delegation_chain + [parent.id]. This means the chain contains every ancestor delegation ID in order, allowing VerifyChain to walk the full lineage.
This will fail if:
["web_search"]is not a subset of the parent’s["web_search", "code_exec"]- The parent is revoked or expired
- The chain would exceed 5 hops
GET /api/v1/delegations — List Active Delegations
Returns all non-revoked, non-expired delegations for the organization.
curl http://localhost:8080/api/v1/delegations \
-H "Authorization: Bearer sk_live_..."Response:
{
"delegations": [
{
"id": "delegation-uuid",
"from_agent_id": "agent-a-uuid",
"to_agent_id": "agent-b-uuid",
"scope": ["web_search", "code_exec"],
"delegation_chain": [],
"expires_at": "2026-02-28T14:00:00Z"
}
]
}DELETE /api/v1/delegations/{delegationID} — Revoke Delegation
Sets revoked_at timestamp. Returns 404 for both “not found” and “already revoked” cases — the error message is "delegation not found or already revoked". This means the endpoint is not idempotent: a second revocation of the same delegation returns 404, not 200.
curl -X DELETE http://localhost:8080/api/v1/delegations/delegation-uuid \
-H "Authorization: Bearer sk_live_..."Success (200):
{"status": "revoked"}Not found or already revoked (404):
{"error": "delegation not found or already revoked"}Chain Verification
AgentTrust ID verifies the entire delegation chain before honoring a delegated action. The VerifyChain function checks each hop:
- Not revoked —
revoked_atmust be NULL - Not expired —
expires_atmust be in the future - Chain continuity —
to_agent_id[N]must equalfrom_agent_id[N+1] - Scope narrowing — each hop’s scope must be a subset of the previous hop’s scope
If any check fails, the entire chain is rejected.
Parent revocation cascading: Revoking a parent delegation does not delete or revoke child delegations in the database. However, children are effectively invalidated because VerifyChain loads and checks every link in the chain. When it encounters the revoked parent, it returns an error like "delegation <id> at position <N> has been revoked", which rejects the entire chain. This is a lazy-invalidation strategy — children remain in the database but can never pass verification.
Authentication & Org Resolution
All delegation handlers resolve the organization ID using a two-step fallback:
- Middleware context —
middleware.GetOrgID()extracts the org from the authenticated session (set by the gateway when authenticating via session cookie or API key). - Header fallback — If the middleware context is empty, the handler reads
X-Org-IDfrom the request header. This supports service-to-service calls where the gateway has already authenticated and proxied the request with the header set.
If neither source provides an org ID, the request is rejected with 400 Bad Request:
{"error": "organization ID is required"}Error Responses
All error responses use the format {"error": "<message>"}. Common errors:
| Status | Error Message | Cause |
|---|---|---|
| 400 | "cannot delegate to self" | from_agent_id equals to_agent_id |
| 400 | "scope must not be empty" | Empty scope array |
| 400 | "child scope must be a subset of parent scope" | Scope widening attempt when extending a chain |
| 400 | "parent delegation not found: ..." | parent_delegation_id does not exist or wrong org |
| 400 | "parent delegation has been revoked" | Parent delegation was previously revoked |
| 400 | "parent delegation has expired" | Parent delegation’s TTL has elapsed |
| 400 | "delegation chain depth exceeds maximum of 5" | Chain would exceed MaxChainDepth |
| 400 | "from_agent_id and to_agent_id are required" | Missing required agent IDs |
| 400 | "organization ID is required" | No org ID from middleware or header |
| 404 | "delegation not found or already revoked" | DELETE target does not exist or was already revoked |
Database Constraints
The delegation_tokens table includes a database-level constraint as defense-in-depth:
CONSTRAINT no_self_delegation CHECK (from_agent_id != to_agent_id)This ensures that even if application-level validation is bypassed, the database will reject any attempt to create a self-delegation. The application also validates this ("cannot delegate to self" error) before reaching the database.
SDK Examples
Python
from agenttrustid import AgentTrustClient
client = AgentTrustClient(api_key="sk_live_...")
# Create delegation
delegation = client.delegations.create(
from_agent_id="agent-a-uuid",
to_agent_id="agent-b-uuid",
scope=["web_search", "code_exec"],
ttl_seconds=7200,
)
# List active delegations
active = client.delegations.list()
# Revoke
client.delegations.revoke(delegation["id"])TypeScript
import { AgentTrustClient } from "@agenttrustid/sdk";
const client = new AgentTrustClient({ apiKey: "sk_live_..." });
const delegation = await client.delegations.create({
fromAgentId: "agent-a-uuid",
toAgentId: "agent-b-uuid",
scope: ["web_search", "code_exec"],
ttlSeconds: 7200,
});
await client.delegations.revoke(delegation.id);