## 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`): ```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`): ```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`): ```json { "delegationToken": "string" } ``` | Field | Type | Required | Constraints | |---|---|---|---| | `delegationToken` | string | yes | The `delegationToken` value returned by `POST /oauth2/token/delegate` | **Response 200** (`application/json`): ```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): ```json { "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` ```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 ```typescript 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; /** * Verify a delegation token. Returns chain details with valid flag. * Does not throw on expired/revoked — returns valid: false. */ verifyDelegation(delegationToken: string): Promise; /** * Revoke a delegation chain. Only the delegator may revoke. */ revokeDelegation(chainId: string, requestingAgentId: string): Promise; } ``` ### 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