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>
11 KiB
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.
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.
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.
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.
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
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: noneis always rejected allowedOrganizationsfield enables fine-grained trust: a partner can be trusted but only for tokens from specific organizations within that partner- Expired federation partners (
expiresAtin the past) are automatically treated as statusexpired— their tokens are rejected POST /federation/verifydoes 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:
expclaim verification allows 30 seconds of clock skew (standard JWT practice)
Acceptance Criteria
POST /federation/trustregisters a partner and fetches JWKS; returns 400 if JWKS unreachablePOST /federation/verifyreturnsvalid: truefor a correctly signed token from a trusted partnerPOST /federation/verifyreturnsvalid: falsewithreason: UNTRUSTED_ISSUERfor unknown issuersPOST /federation/verifyreturnsvalid: falsewithreason: TOKEN_EXPIREDfor 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