Files
SentryAgent.ai Developer 8fd6823581 chore(openspec): archive phase-5-scale-ecosystem — 68/68 tasks complete
WS1 (Rust SDK), WS2 (A2A Authorization), WS5 (Developer Experience)
all delivered, QA gates passed, committed to main.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 02:54:45 +00:00

9.8 KiB

WS2: Agent-to-Agent (A2A) Authorization

Purpose

Enable AI agents to delegate authority to other AI agents via verifiable, auditable, revocable delegation chains. This is a first-class authorization primitive aligned with the AGNTCY multi-agent orchestration model: an orchestrator agent issues sub-tasks to worker agents and must grant those workers scoped authority to act on its behalf.

A delegation chain is: Agent A (delegator) issues a delegation token granting Agent B (delegatee) a subset of A's own scopes for a bounded time period. Agent B presents this token to verify its delegated authority. The chain is stored in PostgreSQL, signed cryptographically, and audited in the existing audit log.

New Endpoints

POST /oauth2/token/delegate

Summary: Delegate authority from one agent to another.

Authentication: Bearer token (the delegating agent's access token).

Request Body (application/json):

{
  "delegateeAgentId": "string",
  "scopes": ["string"],
  "ttlSeconds": 3600
}
Field Type Required Constraints
delegateeAgentId string yes Must be an existing, active agent in the same tenant
scopes string[] yes Min 1 item. Each scope must be a subset of the delegator's own scopes
ttlSeconds integer yes Min: 60, Max: 86400 (24 hours)

Response 201 (application/json):

{
  "delegationToken": "string",
  "chainId": "string (UUID)",
  "delegatorAgentId": "string",
  "delegateeAgentId": "string",
  "scopes": ["string"],
  "expiresAt": "string (ISO 8601)"
}

Error Responses:

Status Code Description
400 INVALID_SCOPES Requested scopes exceed delegator's own scopes
400 INVALID_TTL ttlSeconds outside allowed range [60, 86400]
401 UNAUTHORIZED Missing or invalid Bearer token
404 AGENT_NOT_FOUND delegateeAgentId does not exist or is in a different tenant
422 SELF_DELEGATION Delegator and delegatee are the same agent
429 RATE_LIMITED Rate limit exceeded

Business Rules:

  • Delegated scopes MUST be a strict subset of the delegator's own scopes (no privilege escalation)
  • The delegatee must be an active agent in the same tenant as the delegator
  • An agent may not delegate to itself
  • A delegation entry is written to delegation_chains and an audit log entry is created with event_type: "delegation.created"

POST /oauth2/token/verify-delegation

Summary: Verify a delegation token and return the delegation chain details.

Authentication: Bearer token (any authenticated agent in the same tenant, or unauthenticated if A2A_PUBLIC_VERIFY=true).

Request Body (application/json):

{
  "delegationToken": "string"
}
Field Type Required Constraints
delegationToken string yes The delegationToken value returned by POST /oauth2/token/delegate

Response 200 (application/json):

{
  "valid": true,
  "chainId": "string (UUID)",
  "delegatorAgentId": "string",
  "delegateeAgentId": "string",
  "scopes": ["string"],
  "issuedAt": "string (ISO 8601)",
  "expiresAt": "string (ISO 8601)",
  "revokedAt": null
}

Response when delegation is expired or revoked (HTTP 200, not 4xx — the token exists but is not valid):

{
  "valid": false,
  "chainId": "string (UUID)",
  "delegatorAgentId": "string",
  "delegateeAgentId": "string",
  "scopes": ["string"],
  "issuedAt": "string (ISO 8601)",
  "expiresAt": "string (ISO 8601)",
  "revokedAt": "string (ISO 8601) | null"
}

Error Responses:

Status Code Description
400 MALFORMED_TOKEN Token is not a valid delegation token format
401 UNAUTHORIZED Missing Bearer token (when A2A_PUBLIC_VERIFY=false)
404 CHAIN_NOT_FOUND No delegation chain found for the given token
429 RATE_LIMITED Rate limit exceeded

Business Rules:

  • Expired delegations return valid: false — not an error response
  • Revoked delegations return valid: false with revokedAt populated
  • Verification is non-destructive (does not consume or modify the delegation)
  • An audit log entry is created with event_type: "delegation.verified" on every call

DELETE /oauth2/token/delegate/:chainId

Summary: Revoke a delegation chain. Only the delegator agent can revoke.

Authentication: Bearer token (must be the delegator agent's token).

Path Parameter:

Parameter Type Description
chainId string (UUID) The chain ID returned at delegation creation

Response 204: No body.

Error Responses:

Status Code Description
401 UNAUTHORIZED Missing or invalid Bearer token
403 FORBIDDEN Authenticated agent is not the delegator of this chain
404 CHAIN_NOT_FOUND No delegation chain with this ID
409 ALREADY_REVOKED Delegation chain has already been revoked

Business Rules:

  • Sets revoked_at timestamp on the delegation_chains row
  • Audit log entry created with event_type: "delegation.revoked"
  • Revoking a parent chain does NOT cascade-revoke child chains — each link must be revoked explicitly

Database Schema Changes

Migration: 008_add_delegation_chains.sql

CREATE TABLE delegation_chains (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id       UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    delegator_agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
    delegatee_agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
    scopes          TEXT[] NOT NULL,
    delegation_token TEXT NOT NULL UNIQUE,
    signature       TEXT NOT NULL,          -- HMAC-SHA256 of delegation payload, keyed by delegator secret
    ttl_seconds     INTEGER NOT NULL CHECK (ttl_seconds BETWEEN 60 AND 86400),
    issued_at       TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    expires_at      TIMESTAMPTZ NOT NULL,
    revoked_at      TIMESTAMPTZ,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Index for token lookup (verify-delegation hot path)
CREATE UNIQUE INDEX idx_delegation_chains_token ON delegation_chains(delegation_token);

-- Index for listing delegations by agent
CREATE INDEX idx_delegation_chains_delegator ON delegation_chains(delegator_agent_id, tenant_id);
CREATE INDEX idx_delegation_chains_delegatee ON delegation_chains(delegatee_agent_id, tenant_id);

-- Index for cleanup of expired chains
CREATE INDEX idx_delegation_chains_expires_at ON delegation_chains(expires_at);

New Source Files

File Description
src/services/DelegationService.ts Business logic: create delegation, verify chain, revoke chain
src/controllers/DelegationController.ts HTTP handlers for delegation endpoints
src/routes/delegation.ts Express router: POST /oauth2/token/delegate, POST /oauth2/token/verify-delegation, DELETE /oauth2/token/delegate/:chainId
src/types/delegation.ts TypeScript interfaces: DelegationChain, CreateDelegationRequest, VerifyDelegationRequest, DelegationTokenPayload
src/utils/delegationCrypto.ts HMAC-SHA256 signing and verification for delegation payloads — extracted utility, no duplication

Modified Source Files

File Change
src/routes/index.ts Register delegation router
src/infrastructure/migrations/ Add 008_add_delegation_chains.sql
docs/openapi.yaml Add delegation endpoints

DelegationService Interface

interface IDelegationService {
    /**
     * Create a delegation chain from delegator to delegatee.
     * Validates scope subset, signs payload, inserts DB row, writes audit log.
     */
    createDelegation(
        tenantId: string,
        delegatorAgentId: string,
        request: CreateDelegationRequest
    ): Promise<DelegationChain>;

    /**
     * Verify a delegation token. Returns chain details with valid flag.
     * Does not throw on expired/revoked — returns valid: false.
     */
    verifyDelegation(delegationToken: string): Promise<DelegationVerificationResult>;

    /**
     * Revoke a delegation chain. Only the delegator may revoke.
     */
    revokeDelegation(chainId: string, requestingAgentId: string): Promise<void>;
}

Prometheus Metrics

Metric Type Labels Description
agentidp_delegations_created_total Counter tenant_id Total delegation chains created
agentidp_delegations_verified_total Counter tenant_id, result (valid/invalid/expired/revoked) Delegation verification outcomes
agentidp_delegations_revoked_total Counter tenant_id Total delegations revoked
agentidp_delegation_chain_depth Histogram tenant_id Distribution of delegation chain nesting depth

Feature Flag

A2A_ENABLED environment variable (default: true). When false, all /oauth2/token/delegate* routes return HTTP 404.

Acceptance Criteria

  • POST /oauth2/token/delegate creates a delegation chain and returns a delegation token
  • Scope subset validation rejects any scope not held by the delegating agent
  • POST /oauth2/token/verify-delegation returns valid: true for active chains
  • POST /oauth2/token/verify-delegation returns valid: false (not 4xx) for expired or revoked chains
  • DELETE /oauth2/token/delegate/:chainId sets revoked_at and subsequent verification returns valid: false
  • A 403 is returned when a non-delegator agent attempts to revoke a chain
  • All delegation events are written to the audit log with correct event_type
  • Delegation crypto signature uses HMAC-SHA256 — verified at verify-delegation time
  • Unit test coverage >= 80% on DelegationService and delegationCrypto
  • Integration tests cover: create, verify (valid), verify (expired), verify (revoked), revoke, unauthorized revoke