Skip to Content
ConceptsDelegation Chains

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

Delegation chain with scope narrowing Agent A holds web_search, code_exec, and file_read. Agent A delegates web_search and code_exec to Agent B; Agent B delegates web_search to Agent C. Each hop narrows scope to a subset of its parent, has its own TTL, and is independently revocable. Agent A web_search, code_exec, file_read Agent B web_search, code_exec Agent C web_search delegates [web_search, code_exec] scope narrows · own TTL · revocable delegates [web_search] scope narrows · own TTL · revocable Each hop: scope narrows (subset of parent) · independent TTL · revocable · org-scoped Max chain depth 5 · self-delegation prohibited

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

RuleValueDescription
Max chain depth5Maximum number of delegation hops
Self-delegationNoAgent cannot delegate to itself
Empty scopeNoAt least one capability must be delegated
Default TTL3600s1 hour if ttl_seconds not specified
Scope wideningNoChild 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 by CreateDelegation() or VerifyChain(). It is metadata for application-level use only (e.g., your application can read max_calls or allowed_domains and 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:

  1. Not revokedrevoked_at must be NULL
  2. Not expiredexpires_at must be in the future
  3. Chain continuityto_agent_id[N] must equal from_agent_id[N+1]
  4. 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:

  1. Middleware contextmiddleware.GetOrgID() extracts the org from the authenticated session (set by the gateway when authenticating via session cookie or API key).
  2. Header fallback — If the middleware context is empty, the handler reads X-Org-ID from 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:

StatusError MessageCause
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);
Last updated on