## 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<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
