Files
sentryagent-idp/openspec/specs/federation/spec.md
SentryAgent.ai Developer f1fbe0e29a chore(openspec): archive all completed changes, sync 14 new specs to library
Archived 4 completed OpenSpec changes (2026-04-02):
- phase-3-enterprise (100/100 tasks) — 6 Phase 3 capabilities synced
- devops-documentation (48/48 tasks) — 3 new + 1 merged capability
- bedroom-developer-docs (33/33 tasks) — 4 new capabilities synced
- engineering-docs (superseded by 2026-03-29 archive) — no tasks

Main spec library grows from 21 → 35 capabilities (+14 new):
federation, multi-tenancy, oidc, soc2, w3c-dids, webhooks,
database, operations, system-overview, api-reference, core-concepts,
developer-guides, quick-start + deployment (merged additive requirements)

Active changes: 0 — project board is clear for Phase 4 planning.

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

371 lines
11 KiB
Markdown

# AGNTCY Federation — Specification
**Workstream**: 4 of 6
**Phase**: 3 — Enterprise
**Author**: Virtual Architect
**Date**: 2026-03-29
---
## Overview
Enable cross-instance agent identity federation using signed JWT assertions. Operators register trusted remote AgentIdP instances as federation partners. When an agent presents a token issued by a trusted partner instance, the local AgentIdP can verify it by fetching and caching the partner's JWKS. This enables multi-organization agent identity interoperability aligned with AGNTCY standards.
Federation is opt-in per organization. Only tokens from explicitly registered, trusted partners are accepted.
---
## API Endpoints
### POST /federation/trust
Register a new federation trust partner. Requires `admin:orgs` scope.
```yaml
POST /federation/trust
Authorization: Bearer <token with admin:orgs scope>
Content-Type: application/json
Request Body:
schema:
type: object
required: [name, issuer, jwksUri]
properties:
name:
type: string
minLength: 2
maxLength: 100
description: Human-readable name for this federation partner
example: "Contoso AgentIdP"
issuer:
type: string
format: uri
description: OIDC issuer URL of the partner instance (must match iss claim in tokens)
example: "https://agentidp.contoso.com"
jwksUri:
type: string
format: uri
description: URL of the partner's JWKS endpoint
example: "https://agentidp.contoso.com/.well-known/jwks.json"
allowedOrganizations:
type: array
items:
type: string
description: Optional list of organization IDs in the partner instance whose tokens are accepted. Empty means all partner orgs are trusted.
example: ["org_contoso_engineering"]
expiresAt:
type: string
format: date-time
description: Optional expiry for this trust relationship. If omitted, trust does not expire automatically.
Responses:
201 Created:
schema:
$ref: '#/components/schemas/FederationPartner'
example:
partnerId: "fed_01HXK7Z9P3FKWABCDEF33333"
name: "Contoso AgentIdP"
issuer: "https://agentidp.contoso.com"
jwksUri: "https://agentidp.contoso.com/.well-known/jwks.json"
status: "active"
allowedOrganizations: []
trustedSince: "2026-03-29T12:00:00Z"
expiresAt: null
400 Bad Request:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
duplicate_issuer:
code: "DUPLICATE_ISSUER"
message: "A trust relationship with this issuer already exists"
unreachable_jwks:
code: "JWKS_UNREACHABLE"
message: "Could not fetch JWKS from the provided jwksUri"
401 Unauthorized:
schema:
$ref: '#/components/schemas/ErrorResponse'
403 Forbidden:
schema:
$ref: '#/components/schemas/ErrorResponse'
```
---
### GET /federation/partners
List all registered federation partners for the caller's organization. Requires `admin:orgs` scope.
```yaml
GET /federation/partners
Authorization: Bearer <token with admin:orgs scope>
Query Parameters:
status:
type: string
enum: [active, suspended, expired]
page:
type: integer
default: 1
limit:
type: integer
default: 20
maximum: 100
Responses:
200 OK:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/FederationPartner'
total:
type: integer
page:
type: integer
limit:
type: integer
example:
data:
- partnerId: "fed_01HXK7Z9P3FKWABCDEF33333"
name: "Contoso AgentIdP"
issuer: "https://agentidp.contoso.com"
jwksUri: "https://agentidp.contoso.com/.well-known/jwks.json"
status: "active"
trustedSince: "2026-03-29T12:00:00Z"
expiresAt: null
total: 1
page: 1
limit: 20
401 Unauthorized:
schema:
$ref: '#/components/schemas/ErrorResponse'
403 Forbidden:
schema:
$ref: '#/components/schemas/ErrorResponse'
```
---
### DELETE /federation/partners/:partnerId
Remove a federation trust relationship. Requires `admin:orgs` scope.
```yaml
DELETE /federation/partners/{partnerId}
Authorization: Bearer <token with admin:orgs scope>
Path Parameters:
partnerId:
type: string
Responses:
204 No Content: {}
401 Unauthorized:
schema:
$ref: '#/components/schemas/ErrorResponse'
403 Forbidden:
schema:
$ref: '#/components/schemas/ErrorResponse'
404 Not Found:
schema:
$ref: '#/components/schemas/ErrorResponse'
```
---
### POST /federation/verify
Verify a token issued by a federated partner AgentIdP instance. The caller presents the token; this endpoint resolves the issuer, fetches (or cache-hits) the partner's JWKS, and verifies the signature and claims.
```yaml
POST /federation/verify
Authorization: Bearer <local access_token with agents:read scope>
Content-Type: application/json
Request Body:
schema:
type: object
required: [token]
properties:
token:
type: string
description: The JWT token issued by the remote AgentIdP instance to verify
expectedIssuer:
type: string
format: uri
description: Optional — if provided, verification fails if token issuer does not match
expectedOrganizationId:
type: string
description: Optional — if provided, verification fails if token organization_id does not match
Responses:
200 OK:
schema:
type: object
properties:
valid:
type: boolean
claims:
type: object
description: Decoded JWT claims from the verified token
properties:
sub:
type: string
iss:
type: string
iat:
type: integer
exp:
type: integer
agent_id:
type: string
agent_type:
type: string
organization_id:
type: string
capabilities:
type: array
items:
type: string
did:
type: string
partner:
type: object
description: The federation partner record that vouches for this token
properties:
partnerId:
type: string
name:
type: string
issuer:
type: string
example:
valid: true
claims:
sub: "agt_contoso_abc123"
iss: "https://agentidp.contoso.com"
iat: 1743249600
exp: 1743253200
agent_id: "agt_contoso_abc123"
agent_type: "classifier"
organization_id: "org_contoso_engineering"
capabilities: ["text-classification"]
did: "did:web:agentidp.contoso.com:agents:agt_contoso_abc123"
partner:
partnerId: "fed_01HXK7Z9P3FKWABCDEF33333"
name: "Contoso AgentIdP"
issuer: "https://agentidp.contoso.com"
400 Bad Request:
schema:
$ref: '#/components/schemas/ErrorResponse'
401 Unauthorized (local token invalid):
schema:
$ref: '#/components/schemas/ErrorResponse'
422 Unprocessable Entity (token invalid or untrusted issuer):
schema:
type: object
properties:
valid:
type: boolean
example: false
reason:
type: string
enum:
- TOKEN_EXPIRED
- INVALID_SIGNATURE
- UNTRUSTED_ISSUER
- JWKS_FETCH_FAILED
- ORGANIZATION_NOT_ALLOWED
message:
type: string
example:
valid: false
reason: "UNTRUSTED_ISSUER"
message: "No trust relationship registered for issuer https://unknown.example.com"
```
---
## Database Schema Changes
### New Table: federation_partners
```sql
CREATE TABLE federation_partners (
partner_id VARCHAR(40) PRIMARY KEY,
organization_id VARCHAR(40) NOT NULL REFERENCES organizations(organization_id),
name VARCHAR(100) NOT NULL,
issuer VARCHAR(255) NOT NULL,
jwks_uri VARCHAR(255) NOT NULL,
allowed_organizations JSONB NOT NULL DEFAULT '[]',
status VARCHAR(20) NOT NULL DEFAULT 'active',
trusted_since TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ,
last_jwks_fetch TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT federation_partners_status_check CHECK (status IN ('active', 'suspended', 'expired')),
UNIQUE (organization_id, issuer)
);
CREATE INDEX idx_federation_partners_org_id ON federation_partners(organization_id);
CREATE INDEX idx_federation_partners_issuer ON federation_partners(issuer);
CREATE INDEX idx_federation_partners_status ON federation_partners(status);
```
### Redis: JWKS Cache
Partner JWKS documents are cached in Redis with a TTL:
```
Key: federation:jwks:<issuer_url_sha256>
Value: JSON string of the JWKS document
TTL: 1 hour (configurable via FEDERATION_JWKS_CACHE_TTL_SECONDS)
```
---
## Configuration
| Environment Variable | Description | Default |
|---------------------|-------------|---------|
| `FEDERATION_ENABLED` | Enable federation endpoints | `true` |
| `FEDERATION_JWKS_CACHE_TTL_SECONDS` | Redis TTL for cached partner JWKS | `3600` |
| `FEDERATION_JWKS_FETCH_TIMEOUT_MS` | HTTP timeout for fetching partner JWKS | `5000` |
| `FEDERATION_MAX_PARTNERS_PER_ORG` | Max federation partners per organization | `50` |
---
## Dependencies
No new npm packages. Federation uses `jsonwebtoken` (already present) for JWT verification and the existing HTTP client for JWKS fetches.
---
## Security Considerations
- Only tokens from explicitly registered, active federation partners are accepted in `POST /federation/verify`
- JWKS are cached to prevent JWKS endpoint hammering; cache is invalidated when a partner is updated
- Token signature verification uses the partner's JWKS; `alg: none` is always rejected
- `allowedOrganizations` field enables fine-grained trust: a partner can be trusted but only for tokens from specific organizations within that partner
- Expired federation partners (`expiresAt` in the past) are automatically treated as status `expired` — their tokens are rejected
- `POST /federation/verify` does not grant any local permissions — it is a verification-only endpoint. Callers must make their own access control decisions based on the returned claims.
- Clock skew tolerance: `exp` claim verification allows 30 seconds of clock skew (standard JWT practice)
---
## Acceptance Criteria
- [ ] `POST /federation/trust` registers a partner and fetches JWKS; returns 400 if JWKS unreachable
- [ ] `POST /federation/verify` returns `valid: true` for a correctly signed token from a trusted partner
- [ ] `POST /federation/verify` returns `valid: false` with `reason: UNTRUSTED_ISSUER` for unknown issuers
- [ ] `POST /federation/verify` returns `valid: false` with `reason: TOKEN_EXPIRED` for expired tokens
- [ ] Expired trust relationships (past `expiresAt`) are rejected automatically
- [ ] JWKS cache hit is used on second verification request for same issuer (Redis key present)
- [ ] TypeScript strict, zero `any`, >80% test coverage on FederationService