Files
sentryagent-idp/openspec/changes/archive/2026-04-02-phase-3-enterprise/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

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