feat(openspec): propose phase-5-scale-ecosystem change
6 workstreams, 119 tasks — Scale & Ecosystem: - WS1: Rust SDK - WS2: Agent-to-Agent (A2A) Authorization - WS3: Advanced Analytics Dashboard - WS4: Public API Gateway & Rate Limiting SaaS - WS5: Developer Experience (DX) improvements - WS6: AGNTCY Compliance Certification Package Awaiting CEO approval to begin implementation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user