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>
This commit is contained in:
SentryAgent.ai Developer
2026-04-03 02:54:45 +00:00
parent eaabaebf52
commit 8fd6823581
7 changed files with 0 additions and 58 deletions

View File

@@ -0,0 +1,254 @@
## 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