Skip to main content

Agentic Identity Broker - Admin Server API (3.0.0)

Download OpenAPI specification:Download

License: MIT

Administrative REST APIs for the Agentic Identity Broker running on Port 14000.

Overview

The Admin Server provides full CRUD operations for managing:

  • AI Agents: Registry of agents that can request delegated user permissions
  • Third-Party OAuth2 Services: External OAuth2 providers (GitHub, Google, Databricks, etc.)
  • System Health: Monitoring and health check endpoints

External ID Contract (Admin API 3.0.0)

  • A set external_id identifies at most one agent. Comparison is exact and case-sensitive.
  • POST /api/agents and PUT /api/agents/{agent-id} return 409 conflict when another agent holds the resulting external ID, and 400 validation failed for empty, overlong, invalid UTF-8 or NUL-containing values.
  • PUT /api/agents?external_id= creates or updates the agent holding that external ID.
  • GET /api/agents?external_id= returns [] or a one-element array.
  • Agents without an external ID remain unrestricted in number.

Stable Agent Response Presentation

Every Admin Agent response (POST, collection PUT, ID-addressed PUT, single GET, and list/filter GET) returns client_uris in ascending Go string byte order. URI spelling, other collection ordering and empty-field omission are unchanged. Stored order is irrelevant; equal-URI-set collection PUT replay does not write.

Every Admin Agent created_at and updated_at keeps the whole-second UTC RFC 3339 presentation of Admin API 2.1.0. Collection PUT creation sets both stored times to the current time. An actual reconcile update preserves created_at and sets updated_at to the current time, as POST and ID-addressed PUT do. Two changes within one second may therefore present the same updated_at; it is not a revision or ETag. No-op replay preserves stored times and writes nothing.

Dual-Port Architecture

The Agentic Identity Broker uses a dual-port HTTP server architecture:

  • Port 8000 (default): End-user server for consent management
  • Port 14000 (default): Admin server for configuration (this API)

Authentication & Authorization

All admin operations require authentication at a trusted reverse proxy (oauth2-proxy, nginx, or an API gateway). The proxy must authenticate the caller and enforce administrator privilege before the request reaches the admin server. The broker does not identify administrators. Keep port 14000 internal and remove client-supplied identity headers at the proxy.

Configure authentication separately for the admin server:

  • Plain-header mode: Set server.admin.authentication.preauth.principal_header_name explicitly. The broker reads the principal from that proxy-supplied header. There is no global default principal header.
  • JWT mode: Configure server.admin.authentication.jwt. With verification: jwks (default), set jwks_uri. The broker verifies the signature and requires exp. With verification: none, a trusted gateway sets an unsigned JWT after it authenticates the operator and removes client-supplied copies. Do not set jwks_uri in this mode. The default header is Authorization with Bearer stripped. header_name can select another header, such as X-Userinfo. A missing or invalid JWT supplies no broker principal. The broker ignores the plain principal header. The sweep returns 500 server_misconfiguration without an operator principal. The proxy must reject unauthorized admin requests before forwarding.

The proxy enforces administrator privilege in both modes. The health endpoint is public and does not require authentication.

Security Considerations

Browser Request Protection

All /api requests must use the Host authority configured by server.admin.public_url. Hostname comparison is case-insensitive; the scheme's default port may be omitted. Forwarded Host headers are ignored. Reverse proxies must preserve the configured public Host.

Unsafe cross-origin browser requests are rejected using Go's http.CrossOriginProtection, even when the origin is permitted by CORS. POST, PUT, and PATCH requests require Content-Type: application/json, including actions with no request body. Other requests carrying a body also require JSON. Media-type parameters such as charset=utf-8 are accepted.

These checks return 403 Forbidden for an unrecognized Host or a cross-origin mutation, and 415 Unsupported Media Type for a missing, malformed, or non-JSON Content-Type. Boundary rejection bodies are plain text. /health is not subject to these checks.

Client Secret Protection (SR-003)

  • Client secrets are NEVER transmitted in plaintext to the frontend
  • Stored static confidential-service responses redact client_secret as "REDACTED"
  • Static public (none) and both CIMD service modes omit client_secret because they store no shared secret
  • Static confidential-service secrets are encrypted at rest using AES-256-GCM
  • Update operations support static confidential-service secret rotation
  • Discovery-backed DCR client secrets stay encrypted and never appear in responses
  • Filesystem-service responses omit both client_id and client_secret, including redaction placeholders

Credential Source Contract (Admin API 2.0.0)

  • Every service response reports credential_source: stored | filesystem
  • Creation omission selects stored. Update omission preserves the existing source. Explicit null and unknown values are invalid
  • Filesystem mode requires a valid canonical ID and rejects either inline credential field, including null or empty input
  • Static public, all CIMD, Google-flavor, and protected-resource-discovery services reject filesystem mode
  • Administrative requests and responses contain no credential-file paths, file values, or internal source-transition marker
  • Administrative metadata operations never read credential files or require their availability
  • This approved breaking contract replaces released Admin API 1.0.0 on the existing /api/services routes
  • ADR 041 and the major contract bump were explicitly approved on 2026-10-08. Schema approval does not establish runtime implementation

Client Registration Contract (Increment 2, issue #264, Admin API 2.1.0)

client_registration selects the source of the client identifier. It is independent of token_endpoint_auth_method, which selects token-endpoint authentication. Every service response reports static or cimd.

  • static with absent or null authentication is static confidential. static with none is static public.
  • cimd with private_key_jwt is CIMD confidential. cimd with none is CIMD public.
  • Discovery-selected hosted CIMD reports cimd. Discovery-selected DCR reports static. discovery.client_method remains the authoritative discovery method. Discovery never selects CIMD public.
  • Omission on create or full-replacement PUT resolves to cimd for private_key_jwt, and static otherwise. Omission never infers CIMD public. Explicit null, empty, and unknown registrations return 400.
  • static with private_key_jwt, cimd with absent or null authentication, and cimd with Google flavor or filesystem credentials return 400 before discovery or provider traffic.
  • A request with discovery.resource_url must omit client_registration. Presence returns 400 before discovery traffic.
  • Both CIMD modes use the broker-hosted client identifier URL and reject a non-empty caller client_id or client_secret. CIMD public needs no CIMD client-authentication key.
  • Stored services from before this change report cimd for private_key_jwt, and static otherwise.

URL Validation

  • All URLs must use HTTP or HTTPS schemes — except agent redirect_uris, which also accept native-app private-use scheme URIs (see the redirect_uris schema)
  • OAuth2 issuer URIs must use HTTPS
  • Metadata URLs must use HTTPS
  • URL validation prevents injection attacks
  • Protected-resource discovery and every discovered endpoint require public HTTPS.
  • Discovery and registration share a 10-second deadline and a 256-KiB cap per response.
  • Discovery and registration use a guarded client without redirects or proxies. It rejects blocked resolved IP addresses before connection.
  • Discovery-backed token calls use a separate guarded client with the configured upstream token timeout, no redirects, and no proxies.

Referential Integrity

  • Services cannot be deleted if user grants reference them (409 Conflict)
  • Agent deletion cascades to associated grants (logged for audit)

OAuth2 Discovery Support

Direct authorization-server metadata discovery keeps its existing metadata_url behavior. Protected-resource discovery uses discovery.resource_url with enable_discovery: true. The broker validates the resource and issuer, then selects hosted CIMD or DCR. The authenticated discovery-status resource reads stored status without contacting the provider. DCR needs a non-blank deployment-wide third_party_oauth2.client_name; manual and CIMD modes do not.

Discovery and registration failures return safe ErrorResponse.message codes only. They do not return provider bodies, URL queries, secrets, assertions, or tokens. A 400 discovery failure uses one of these codes: resource_metadata_not_found, resource_metadata_unavailable, resource_metadata_invalid, resource_mismatch, authorization_server_missing, issuer_selection_required, issuer_not_advertised, authorization_server_metadata_not_found, authorization_server_metadata_unavailable, authorization_server_metadata_invalid, issuer_mismatch, unsafe_destination, response_too_large. A 400 client-registration failure uses no_compatible_client_method, cimd_unavailable, client_name_unconfigured, client_registration_rejected, client_registration_invalid, or client_method_changed. timeout returns 504; duplicate_client_identity and issuer_change_requires_no_sessions return 409. Local storage or encryption failures return 500 without a failure code. Only issuer_selection_required adds the validated authorization_servers list to its authenticated error response.

Trace Correlation

By default, every HTTP response from this API includes the additive W3C traceresponse response header.

  • The header applies globally across this API, including successful responses and error responses.
  • Its <trace-id> field is the request correlation identifier operators can use to find matching logs.
  • The header is additive only; no request or response body schema changes are introduced by this feature.
  • Emission is enabled by default and can be disabled only through request_context.trace.response_enabled.

Error Handling

Handler error responses follow the ErrorResponse schema with appropriate HTTP status codes. Browser boundary rejections use plain-text bodies as described above.

Health

Health check and monitoring endpoints

Health check endpoint

Returns the current health status of the HTTP server. Each server (end-user and admin) has its own independent health endpoint that reports only that server's status.

This endpoint is public and does not require authentication.

Status Codes:

  • 200 OK: Server is healthy and accepting requests
  • 503 Service Unavailable: Server is starting, shutting down, or unhealthy

Health States:

  • healthy: Server is operational and processing requests normally
  • starting: Server is initializing (binding to port, setting up routes)
  • shutting_down: Graceful shutdown in progress, completing in-flight requests
  • unhealthy: Server encountered an error and requires restart

Responses

Response samples

Content type
application/json
{
  • "status": "healthy",
  • "timestamp": "2025-12-19T10:30:00Z",
  • "uptime_seconds": 3600
}

Agents

Administrative operations for AI agent registry management

List agents, optionally filtered by external ID

Retrieve registered agents. Without external_id, return all agents. With one external_id, return the exact, case-sensitive match, if any. External IDs are unique, so the array contains zero or one agent. Do not trim, normalize, or interpret the value as a pattern. The filter does not match agent IDs, canonical IDs, or client IDs.

Percent-encode reserved characters in the value, such as &, +, and ;. For example, send urn:example:ai-registry:agent:7f3a9c as urn%3Aexample%3Aai-registry%3Aagent%3A7f3a9c. Invalid filters return 400 with error: invalid external_id. This includes empty or repeated values, invalid UTF-8, NUL, more than 255 Unicode code points, malformed percent-encoding, and an unencoded ;. Unknown parameters and malformed query parts outside external_id retain their existing handling.

Returns a direct array of agent objects (no envelope), in no guaranteed order.

Authorizations:
PreAuthProxyJWTPreAuth
query Parameters
external_id
string [ 1 .. 255 ] characters
Example: external_id=urn:example:ai-registry:agent:7f3a9c

Exact, case-sensitive external governance ID. Percent-encode reserved characters.

header Parameters
Prefer
string
Example: reference-id=canonical

Request canonical rendering of nested service and permission-set references. The default representation uses UUIDs. May be combined with other Prefer preferences (e.g. return=minimal, reference-id=canonical); unrecognized preferences are ignored.

Responses

Response samples

Content type
application/json
Example
[
  • {
    },
  • {
    }
]

Create a new agent

Register a new AI agent in the identity broker system.

The client_id field is the upstream OAuth2 client ID used when proxying authorization requests to the upstream server.

client_id uniqueness depends on configuration:

  • When multi_agent_client.enabled = false (default): client_id must be unique across all agents. Returns 409 Conflict if a duplicate client_id is detected.
  • When multi_agent_client.enabled = true: Multiple agents may share the same client_id. No uniqueness validation is performed.

Validation Rules:

  • client_id: Optional; when provided must be non-empty; unique across all agents when multi_agent_client.enabled = false
  • display_name: Required, max 255 characters
  • description: Required, max 1000 characters
  • URLs: Must be valid HTTP/HTTPS URLs if provided; redirect_uris entries follow the redirect_uris schema rules

external_id uniqueness: When supplied, external_id must be valid UTF-8 text with 1–255 Unicode code points and no NUL character, and must not be held by another agent. An empty string returns 400 validation failed. A held value returns 409 conflict with message external_id is already held by another agent. Omit external_id or send null for agents without one; any number of agents may omit it.

ID Generation: System generates a UUID for the agent ID. client_id is not auto-generated — omit it for agents that do not need one.

Authorizations:
PreAuthProxyJWTPreAuth
Request Body schema: application/json
required
canonical_id
string or null [ 1 .. 128 ] characters ^[A-Za-z0-9._-]+$
client_id
string <= 255 characters

OAuth2 client_id for this agent. Optional — omit for agents resolved via client_uris (CIMD) or agents that do not need an upstream client_id. When provided, must be non-empty.

external_id
string or null [ 1 .. 255 ] characters

Optional external governance identifier; valid UTF-8 with 1–255 Unicode code points, no NUL, and globally unique when set. Collection PUT requires a supplied value to equal its selector and rejects null.

display_name
required
string [ 1 .. 255 ] characters

Human-readable name for the agent

description
required
string [ 1 .. 1000 ] characters

Detailed description of the agent's purpose and capabilities

governance_url
string or null <uri>

URL to governance and compliance information (must be HTTP/HTTPS)

user_documentation_url
string or null <uri>

URL to user-facing documentation (must be HTTP/HTTPS)

agent_interface_url
string or null <uri>

URL where users can interact with the agent (must be HTTP/HTTPS)

Array of objects or null (ServiceRequirementRequest)

List of third-party OAuth2 services that the agent requires access to. Referential integrity is validated: service_id must exist and supplied required_scopes must match the service definition. Omit or provide an empty list for scope-less services.

required
Array of objects (AgentPermissionSetEntry) non-empty

List of permission set declarations for this agent (FR-006). Must contain at least one entry — an empty list or omitted field is a 400 error. All referenced permission_set_id values must exist.

redirect_uris
Array of strings or null <uri> [ items <uri > ]

List of allowed redirect URIs for OAuth2 authorization code flow. When the broker acts as an OAuth2 server, only these URIs are accepted as valid redirect targets for this agent. Each entry is an https URI or an http URI with host localhost, 127.0.0.1, or [::1]. The port is ignored when matching loopback http URIs. An entry can also be a native-app private-use scheme URI (RFC 8252 §7.1). It can use scheme:/path (com.example.app:/oauth2redirect/example-provider) or scheme://host/path (github-app://oauth/callback). A private-use URI must contain only ASCII letters, digits, and -._~:/?@!$&'()+,;=. It must be canonical, have a lowercase scheme of at least two characters, and have no opaque path, userinfo, or empty query delimiter. The scheme cannot be http, https, about, blob, data, file, filesystem, ftp, javascript, vbscript, view-source, ws, or wss. The URI must use either a single-slash path or a nonempty lowercase authority host. An authority cannot have an empty, zero, or out-of-range port, and no path segment can be . or ... A private-use URI matches only character for character. No entry may contain a fragment. Invalid entries cause 400 validation failed. Accepted entries are stored and returned unchanged. For an agent with client_uris, the fetched document's redirect_uris are authoritative. This list then only allowlists private-use callbacks that the document also declares (ADR 042).

allowed_scopes
Array of strings or null

List of OAuth2 scopes that this agent is permitted to request when the broker acts as an OAuth2 authorization server.

client_uris
Array of strings or null <uri> [ items <uri > ]

Pre-registered Client ID Metadata Document URLs for this agent. Each entry must be a globally unique HTTPS URL. A * can replace one complete, non-empty path segment. The broker uses an exact entry before matching patterns. It rejects ambiguous pattern matches and URI paths containing literal \, %2F, or %5C. Returns 400 for malformed URLs. Returns 409 Conflict when a URI is already registered to a different agent.

Responses

Request samples

Content type
application/json
Example
{
  • "client_id": "agent-simple-assistant",
  • "display_name": "Simple Assistant",
  • "description": "A basic AI assistant for general tasks",
  • "permission_sets": [
    ]
}

Response samples

Content type
application/json
{}

Reconcile the agent holding an external ID

Idempotently reconcile the agent identified by an external governance ID.

  • No agent holds external_id: create one and return 201 Created with Location.
  • An agent holds it: update that agent and return 200 OK. The selector never matches agent IDs, canonical IDs, or client IDs.
  • Replaying an identical request with no intervening change returns 200 with the unchanged representation, including updated_at. Nothing is written.
  • Concurrent first requests with one external ID create exactly one agent. Without If-Match, concurrent different definitions are last-writer-wins; each successful request applies its complete definition.

Selector: exactly one external_id query parameter, 1–255 Unicode code points, valid UTF-8, no NUL, valid percent-encoding, and no unencoded ;. Percent-encode &, +, and ;. Otherwise 400 invalid external_id.

Body: AgentCreateRequest. Omitted external_id takes the selector. A supplied external_id must equal the decoded selector; a different value, "", or null returns 400 invalid external_id.

Update-branch field policy (FR-012):

Fields Omitted null Value
client_id preserved 400 validation failed replaced
canonical_id preserved removed replaced
governance_url, user_documentation_url, agent_interface_url removed removed replaced
service_requirements, redirect_uris, allowed_scopes, client_uris emptied emptied replaced
display_name, description, permission_sets 400 400 replaced

On creation, omitted optional fields are absent or empty and no client_id is generated. Grants and other relationships of an updated agent are not changed. To clear an existing client_id, use PUT /api/agents/{agent-id} with client_id: null.

Invalid definitions, attributes, and references on either branch return 400 with the existing validation failed envelope. Canonical-ID conflicts, configuration-dependent client_id conflicts (multi_agent_client.enabled), and client-URI identity conflicts return 409 conflict on both branches. Failed requests change no agent. Agent ordering and timestamps follow "Stable Agent Response Presentation" above on create, update, and replay.

Authorizations:
PreAuthProxyJWTPreAuth
query Parameters
external_id
required
string [ 1 .. 255 ] characters
Example: external_id=urn:example:ai-registry:agent:7f3a9c

Exact, case-sensitive external governance ID that selects the agent. Percent-encode reserved characters.

header Parameters
Prefer
string
Example: reference-id=canonical

Request canonical rendering of nested service and permission-set references. The default representation uses UUIDs. May be combined with other Prefer preferences (e.g. return=minimal, reference-id=canonical); unrecognized preferences are ignored.

Request Body schema: application/json
required
canonical_id
string or null [ 1 .. 128 ] characters ^[A-Za-z0-9._-]+$
client_id
string <= 255 characters

OAuth2 client_id for this agent. Optional — omit for agents resolved via client_uris (CIMD) or agents that do not need an upstream client_id. When provided, must be non-empty.

external_id
string or null [ 1 .. 255 ] characters

Optional external governance identifier; valid UTF-8 with 1–255 Unicode code points, no NUL, and globally unique when set. Collection PUT requires a supplied value to equal its selector and rejects null.

display_name
required
string [ 1 .. 255 ] characters

Human-readable name for the agent

description
required
string [ 1 .. 1000 ] characters

Detailed description of the agent's purpose and capabilities

governance_url
string or null <uri>

URL to governance and compliance information (must be HTTP/HTTPS)

user_documentation_url
string or null <uri>

URL to user-facing documentation (must be HTTP/HTTPS)

agent_interface_url
string or null <uri>

URL where users can interact with the agent (must be HTTP/HTTPS)

Array of objects or null (ServiceRequirementRequest)

List of third-party OAuth2 services that the agent requires access to. Referential integrity is validated: service_id must exist and supplied required_scopes must match the service definition. Omit or provide an empty list for scope-less services.

required
Array of objects (AgentPermissionSetEntry) non-empty

List of permission set declarations for this agent (FR-006). Must contain at least one entry — an empty list or omitted field is a 400 error. All referenced permission_set_id values must exist.

redirect_uris
Array of strings or null <uri> [ items <uri > ]

List of allowed redirect URIs for OAuth2 authorization code flow. When the broker acts as an OAuth2 server, only these URIs are accepted as valid redirect targets for this agent. Each entry is an https URI or an http URI with host localhost, 127.0.0.1, or [::1]. The port is ignored when matching loopback http URIs. An entry can also be a native-app private-use scheme URI (RFC 8252 §7.1). It can use scheme:/path (com.example.app:/oauth2redirect/example-provider) or scheme://host/path (github-app://oauth/callback). A private-use URI must contain only ASCII letters, digits, and -._~:/?@!$&'()+,;=. It must be canonical, have a lowercase scheme of at least two characters, and have no opaque path, userinfo, or empty query delimiter. The scheme cannot be http, https, about, blob, data, file, filesystem, ftp, javascript, vbscript, view-source, ws, or wss. The URI must use either a single-slash path or a nonempty lowercase authority host. An authority cannot have an empty, zero, or out-of-range port, and no path segment can be . or ... A private-use URI matches only character for character. No entry may contain a fragment. Invalid entries cause 400 validation failed. Accepted entries are stored and returned unchanged. For an agent with client_uris, the fetched document's redirect_uris are authoritative. This list then only allowlists private-use callbacks that the document also declares (ADR 042).

allowed_scopes
Array of strings or null

List of OAuth2 scopes that this agent is permitted to request when the broker acts as an OAuth2 authorization server.

client_uris
Array of strings or null <uri> [ items <uri > ]

Pre-registered Client ID Metadata Document URLs for this agent. Each entry must be a globally unique HTTPS URL. A * can replace one complete, non-empty path segment. The broker uses an exact entry before matching patterns. It rejects ambiguous pattern matches and URI paths containing literal \, %2F, or %5C. Returns 400 for malformed URLs. Returns 409 Conflict when a URI is already registered to a different agent.

Responses

Request samples

Content type
application/json
{
  • "canonical_id": "research-assistant",
  • "external_id": "urn:example:ai-registry:agent:7f3a9c",
  • "display_name": "Research Assistant",
  • "description": "Helps with academic research",
  • "permission_sets": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "550e8400-e29b-41d4-a716-446655440000",
  • "canonical_id": "research-assistant",
  • "external_id": "urn:example:ai-registry:agent:7f3a9c",
  • "display_name": "Research Assistant v2",
  • "description": "Helps with academic research",
  • "permission_sets": [
    ],
  • "created_at": "2026-10-30T08:00:00Z",
  • "updated_at": "2026-10-30T09:00:00Z"
}

Get agent by ID

Retrieve detailed information about a specific agent.

Returns the complete agent configuration including all optional fields.

Authorizations:
PreAuthProxyJWTPreAuth
path Parameters
agent-id
required
string
Examples:
  • 550e8400-e29b-41d4-a716-446655440000 -
  • research-agent -

UUID or canonical identifier of the agent

header Parameters
Prefer
string
Example: reference-id=canonical

Request canonical rendering of nested service and permission-set references. The default representation uses UUIDs. May be combined with other Prefer preferences (e.g. return=minimal, reference-id=canonical); unrecognized preferences are ignored.

Responses

Response samples

Content type
application/json
{
  • "id": "550e8400-e29b-41d4-a716-446655440000",
  • "client_id": "agent-research-assistant",
  • "display_name": "Research Assistant",
  • "description": "AI assistant that helps with academic research",
  • "permission_sets": [
    ],
  • "created_at": "2025-12-19T10:30:00Z",
  • "updated_at": "2025-12-19T10:30:00Z"
}

Update agent

Update configuration for an existing agent.

When supplied, external_id must be non-empty and must not be held by another agent (400 validation failed / 409 conflict). Retaining its own external ID is not a conflict. Canonical-ID, client-ID, and client-URI conflicts also return 409.

Update Semantics:

  • PUT is a full replacement, not PATCH. Send every field you want to keep
  • Omitted or null external_id, governance_url, user_documentation_url, agent_interface_url, service_requirements, redirect_uris, allowed_scopes, and client_uris are cleared
  • canonical_id and client_id are the exceptions: omitted preserves the existing value, and explicit null clears it
  • created_at timestamp is preserved
  • updated_at timestamp is set to current time

client_id uniqueness depends on configuration:

  • When multi_agent_client.enabled = false (default): client_id must be unique across all agents (excluding the current agent). Returns 409 Conflict if a duplicate client_id is detected on another agent.
  • When multi_agent_client.enabled = true: Multiple agents may share the same client_id. No uniqueness validation is performed.

Validation:

  • client_id: Three-state — omitted preserves existing value, explicit null clears it, string value updates it (must be non-empty); unique across all agents (excluding the current agent) when multi_agent_client.enabled = false
  • Other fields follow the same rules as creation
Authorizations:
PreAuthProxyJWTPreAuth
path Parameters
agent-id
required
string
Examples:
  • 550e8400-e29b-41d4-a716-446655440000 -
  • research-agent -

UUID or canonical identifier of the agent

Request Body schema: application/json
required
canonical_id
string or null [ 1 .. 128 ] characters ^[A-Za-z0-9._-]+$

Omitted preserves the current canonical ID; null removes it; a string replaces it.

client_id
string or null <= 255 characters

OAuth2 client_id for this agent.

Three-state semantics:

  • Omitted (key absent from JSON): existing value is preserved
  • Explicit null ("client_id": null): clears the client_id (sets to NULL)
  • String value ("client_id": "foo"): updates the client_id; must be non-empty

Agents without a client_id cannot participate in proxy-mode OAuth2 flows.

external_id
string or null [ 1 .. 255 ] characters

Optional external governance identifier; valid UTF-8 with 1–255 Unicode code points, no NUL, and globally unique when set. Omission or null clears it; retaining the current agent's value is not a conflict.

display_name
required
string [ 1 .. 255 ] characters

Human-readable name for the agent

description
required
string [ 1 .. 1000 ] characters

Detailed description of the agent's purpose and capabilities

governance_url
string or null <uri>

URL to governance and compliance information (must be HTTP/HTTPS)

user_documentation_url
string or null <uri>

URL to user-facing documentation (must be HTTP/HTTPS)

agent_interface_url
string or null <uri>

URL where users can interact with the agent (must be HTTP/HTTPS)

Array of objects or null (ServiceRequirementRequest)

List of third-party OAuth2 services that the agent requires access to. Referential integrity is validated: service_id must exist and supplied required_scopes must match the service definition. Omit or provide an empty list for scope-less services.

required
Array of objects (AgentPermissionSetEntry) non-empty

List of permission set declarations for this agent (FR-006). Must contain at least one entry — an empty list or omitted field is a 400 error. All referenced permission_set_id values must exist.

redirect_uris
Array of strings or null <uri> [ items <uri > ]

List of allowed redirect URIs for OAuth2 authorization code flow. When the broker acts as an OAuth2 server, only these URIs are accepted as valid redirect targets for this agent. Each entry is an https URI or an http URI with host localhost, 127.0.0.1, or [::1]. The port is ignored when matching loopback http URIs. An entry can also be a native-app private-use scheme URI (RFC 8252 §7.1). It can use scheme:/path (com.example.app:/oauth2redirect/example-provider) or scheme://host/path (github-app://oauth/callback). A private-use URI must contain only ASCII letters, digits, and -._~:/?@!$&'()+,;=. It must be canonical, have a lowercase scheme of at least two characters, and have no opaque path, userinfo, or empty query delimiter. The scheme cannot be http, https, about, blob, data, file, filesystem, ftp, javascript, vbscript, view-source, ws, or wss. The URI must use either a single-slash path or a nonempty lowercase authority host. An authority cannot have an empty, zero, or out-of-range port, and no path segment can be . or ... A private-use URI matches only character for character. No entry may contain a fragment. Invalid entries cause 400 validation failed. Accepted entries are stored and returned unchanged. For an agent with client_uris, the fetched document's redirect_uris are authoritative. This list then only allowlists private-use callbacks that the document also declares (ADR 042).

allowed_scopes
Array of strings or null

List of OAuth2 scopes that this agent is permitted to request when the broker acts as an OAuth2 authorization server.

client_uris
Array of strings or null <uri> [ items <uri > ]

Pre-registered Client ID Metadata Document URLs for this agent. Replaces the previous list on PUT (full replacement semantics). Each entry must be a globally unique HTTPS URL. A * can replace one complete, non-empty path segment. The broker uses an exact entry before matching patterns. It rejects ambiguous pattern matches and URI paths containing literal \, %2F, or %5C. Returns 400 for malformed URLs. Returns 409 Conflict when a URI is already registered to a different agent.

Responses

Request samples

Content type
application/json
Example
{
  • "client_id": "agent-research-assistant",
  • "display_name": "Advanced Research Assistant",
  • "description": "AI assistant that helps with academic research",
  • "permission_sets": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "550e8400-e29b-41d4-a716-446655440000",
  • "client_id": "agent-research-assistant",
  • "display_name": "Advanced Research Assistant",
  • "description": "AI assistant that helps with academic research",
  • "permission_sets": [
    ],
  • "created_at": "2025-12-19T10:30:00Z",
  • "updated_at": "2025-12-19T15:45:00Z"
}

Delete agent

Remove an agent from the system.

Cascade Behavior:

  • All user grants associated with this agent are automatically deleted
  • Deletion is logged for audit purposes

Irreversible: This operation cannot be undone

Authorizations:
PreAuthProxyJWTPreAuth
path Parameters
agent-id
required
string
Examples:
  • 550e8400-e29b-41d4-a716-446655440000 -
  • research-agent -

UUID or canonical identifier of the agent

Responses

Response samples

Content type
application/json
{
  • "error": "agent ID is required"
}

Services

Administrative operations for third-party OAuth2 service configurations

List all OAuth2 services

Retrieve a list of all configured third-party OAuth2 services.

Returns a direct array of service objects (no envelope).

Every object reports credential_source and client_registration. Static confidential services return their client ID and "REDACTED" secret. Static public, CIMD, and DCR services omit the secret. Filesystem services omit inline credentials. Listing never reads files or returns paths or file values.

Authorizations:
PreAuthProxyJWTPreAuth
header Parameters
Prefer
string
Example: reference-id=canonical

Request canonical rendering of nested service and permission-set references. The default representation uses UUIDs. May be combined with other Prefer preferences (e.g. return=minimal, reference-id=canonical); unrecognized preferences are ignored.

Responses

Response samples

Content type
application/json
Example
[
  • {
    },
  • {
    },
  • {
    }
]

Create a new OAuth2 service

Configure a new third-party OAuth2 service provider.

Validation Rules:

  • display_name: Required, max 255 characters
  • credential_source: stored or filesystem; omission selects stored; explicit null and unknown values are invalid
  • client_registration: Optional static or cimd. Omission selects cimd for private_key_jwt and static otherwise. Explicit null, empty, or unknown values return 400
  • Stored manual client_id: Required and unique under existing validation, except Google derives it and CIMD generates it; discovery selects the client identity
  • Stored client_secret: Required for static confidential services. Static public and both CIMD modes accept omitted, null, or empty secret input
  • Filesystem: Requires a valid canonical ID, shared-secret authentication, and standard/GitHub flavor
  • Filesystem: Rejects either inline field whenever present, including null or empty input
  • Manual issuer_uri: Required and HTTPS, subject to existing Google service-account rules; optional for protected-resource discovery with one issuer
  • scopes: Optional. Omit it or provide an empty array when the provider does not use OAuth2 scopes.

Filesystem registration never reads files or requires an available binding. Paths belong only to operator configuration and cannot be supplied through this API. Every successful response reports the selected source. Filesystem responses omit both inline fields.

CIMD modes: client_registration: cimd with private_key_jwt is confidential. With none, it is public and needs no CIMD client-authentication key. Both require omitted or null client_id and omitted, null, or empty client_secret. The broker rejects a non-empty caller value, Google flavor, or filesystem source with 400 before provider traffic. It assigns the HTTPS client identifier URL after it creates the service ID. static with private_key_jwt, or cimd with absent or null authentication, returns 400.

Direct metadata discovery: Without resource_url, metadata_url can select an authorization-server document. The existing manual-endpoint fallback remains unchanged.

Protected-resource discovery: Set discovery.resource_url with enable_discovery: true. Omit client_registration, manual endpoints, client credentials, metadata_url, and token_endpoint_auth_method. A supplied registration returns 400 before discovery traffic. An issuer selection is optional for one issuer. The broker derives authorization_params.resource unless the request supplies an explicit value. A selected DCR client needs a non-blank broker client name from deployment configuration, not the service display name. Failures do not create a service.

Security: Static and DCR confidential secrets are encrypted before storage. A DCR secret never appears in an Admin API response.

Authorizations:
PreAuthProxyJWTPreAuth
Request Body schema: application/json
required
One of
One of
One of
One of
oauth2_flavor
string
Default: "standard"
Enum: "standard" "google" "github"

OAuth2 authentication variant. Defaults to standard if omitted. Auto-detected as github when the token endpoint host equals github.com.

  • standard: Static confidential services require a non-empty plain client_secret; static public services require client_id
  • google: client_secret must be a valid Google service account JSON key (≤32 KB); client_id is derived from the JSON's client_id field and need not be provided; issuer_uri is optional; endpoints are derived from the service account JSON
  • github: Same as static confidential standard but scopes in token responses are parsed as comma-separated

private_key_jwt requires a non-credential-derived variant; google is rejected. Protected-resource discovery requires omitted or standard flavor.

client_id
required
string or null non-empty

OAuth2 client_id for this service.

In stored mode, manual static standard or github clients require this field. Stored google derives it from the service account JSON. Both CIMD methods require omission or null. The broker assigns the HTTPS client identifier URL after it allocates the service ID. Protected-resource discovery selects the identity. Filesystem mode requires omission, including null or empty input.

For protected-resource discovery, this property must be omitted, null, or empty.

client_registration
string
Enum: "static" "cimd"

Optional on create. Omission resolves to cimd for private_key_jwt, or static otherwise. An explicit null, empty, or unknown value returns 400. cimd requires none or private_key_jwt and stored, non-Google credentials. static rejects private_key_jwt. Omit this field with discovery.resource_url. The broker reports the registration selected by discovery.

token_endpoint_auth_method
string or null
Enum: "none" "private_key_jwt" null

Client authentication method for the upstream token endpoint. Omit it or send null for shared-secret authentication for manual clients. Stored mode requires client_secret. Filesystem mode forbids inline credentials.

When none, the service is static public unless client_registration: cimd is explicit. Static public requires client_id. CIMD public requires omission or null client_id. Both reject a non-empty client_secret and oauth2_flavor: google.

When private_key_jwt, the service is CIMD confidential. The caller omits client_id or sends null, and omits client_secret or sends null or "". The broker rejects a non-empty value before discovery or provider traffic. It assigns the HTTPS client identifier URL after it allocates the service ID.

With discovery.resource_url, omit or set this method to null. The broker chooses one method; client_secret_basic and client_secret_post are response-only.

client_secret
required
string or null non-empty

Authentication credential for a static confidential service.

The content depends on oauth2_flavor:

  • standard / github: Non-empty OAuth2 client secret string
  • google: Serialized Google service account JSON key document. Must contain: type (must be "service_account"), private_key (non-empty), client_email (non-empty), token_uri (non-empty), client_id (non-empty). Maximum size: 32 KB.

SECURITY: Static confidential-service secrets are encrypted at rest using AES-256-GCM and return only as "REDACTED" on read responses.

For manual stored services, an omitted or null token_endpoint_auth_method requires a non-empty static secret. Manual none and private_key_jwt accept only omitted, null, or empty secrets. For protected-resource discovery, this property must also be omitted, null, or empty. The broker never accepts a caller-supplied DCR secret. Filesystem mode requires this property to be absent, including null or empty input.

required
object
credential_source
string
Default: "stored"
Enum: "stored" "filesystem"

Omission selects stored. Null is invalid. Filesystem rejects inline fields and excluded authentication modes.

canonical_id
string or null [ 1 .. 128 ] characters ^[A-Za-z0-9._-]+$
display_name
required
string [ 1 .. 255 ] characters

Human-readable name for the service

issuer_uri
string <uri>

OAuth2 issuer URI.

Required for manual standard services and HTTPS. Optional for protected-resource discovery with one issuer. If the resource advertises several issuers, the administrator must select an identical issuer_uri. Google retains its existing service-account rules.

object (OAuth2EndpointsRequest)

OAuth2 endpoints configuration.

Required when discovery.enable_discovery = false Optional when discovery.enable_discovery = true without resource_url (direct-metadata fallback only). Must be absent with resource_url.

Each supplied token_endpoint and authorize_endpoint must use HTTPS. Under the default strict policy, HTTP is accepted only for localhost, 127.0.0.1, or [::1]; HTTP is accepted for any host only when security.skip_thirdparty_https_validation is enabled for development/test.

Array of objects (OAuth2ScopeRequest)

Optional OAuth2 scopes for this service. Omit or provide an empty array when the provider does not use scopes.

protected_resources
Array of strings <uri> [ items <uri > ]

List of protected resource URIs for RFC 8693 token exchange (optional). These URIs identify which resources this service can provide tokens for. URIs are normalized (trailing slashes removed) for consistent matching.

object

Provider parameters. For protected-resource discovery, resource is an optional explicit absolute URI without a fragment. The broker derives it from verified metadata when omitted. Other broker-owned OAuth2 names are rejected.

Responses

Request samples

Content type
application/json
Example
{
  • "display_name": "GitHub Production",
  • "client_id": "Iv1.1234567890abcdef",
  • "client_secret": "ghp_secretkey1234567890abcdef",
  • "issuer_uri": "https://github.com",
  • "discovery": {
    },
  • "scopes": [
    ]
}

Response samples

Content type
application/json
Example
{
  • "id": "770e8400-e29b-41d4-a716-446655440002",
  • "display_name": "GitHub Production",
  • "credential_source": "stored",
  • "client_registration": "static",
  • "oauth2_flavor": "github",
  • "client_id": "Iv1.1234567890abcdef",
  • "client_secret": "REDACTED",
  • "token_endpoint_auth_method": null,
  • "issuer_uri": "https://github.com",
  • "discovery": {
    },
  • "endpoints": {},
  • "scopes": [
    ],
  • "created_at": "2025-12-19T10:30:00Z",
  • "updated_at": "2025-12-19T10:30:00Z"
}

Get OAuth2 service by ID

Retrieve detailed configuration for a specific OAuth2 service.

Every response reports credential_source and client_registration without reading files. Stored responses always include client_id. Static confidential secrets return only "REDACTED"; static public, CIMD, and DCR responses omit the secret. Filesystem responses omit both inline credential fields, file paths, and file values.

Authorizations:
PreAuthProxyJWTPreAuth
path Parameters
service-id
required
string
Examples:
  • 770e8400-e29b-41d4-a716-446655440002 -
  • github-prod -

UUID or canonical identifier of the OAuth2 service

header Parameters
Prefer
string
Example: reference-id=canonical

Request canonical rendering of nested service and permission-set references. The default representation uses UUIDs. May be combined with other Prefer preferences (e.g. return=minimal, reference-id=canonical); unrecognized preferences are ignored.

Responses

Response samples

Content type
application/json
{
  • "id": "770e8400-e29b-41d4-a716-446655440002",
  • "display_name": "GitHub Production",
  • "credential_source": "stored",
  • "client_registration": "static",
  • "oauth2_flavor": "github",
  • "client_id": "Iv1.1234567890abcdef",
  • "client_secret": "REDACTED",
  • "token_endpoint_auth_method": null,
  • "issuer_uri": "https://github.com",
  • "discovery": {
    },
  • "endpoints": {},
  • "scopes": [
    ],
  • "created_at": "2025-12-19T10:30:00Z",
  • "updated_at": "2025-12-19T10:30:00Z"
}

Update OAuth2 service

Update configuration for an existing OAuth2 service.

Update Semantics:

  • PUT remains a full replacement, not PATCH. Existing documented omission exceptions remain unchanged
  • credential_source omission preserves the current source. Explicit null and unknown selectors are invalid
  • Omitted client_registration resolves to cimd for private_key_jwt and static otherwise, not to the old value. Explicit null, empty, or unknown values return 400
  • CIMD public must repeat client_registration: cimd. Without it, PUT resolves to static and fails without an operator client_id
  • For manual replacements, token_endpoint_auth_method is evaluated from the complete request. Omitted or null means shared-secret authentication: stored requires an explicit non-empty secret, while filesystem forbids inline credentials. Protected-resource refresh retains the broker-selected method and credentials
  • token_endpoint_auth_method: private_key_jwt requires client_id to be omitted or null and client_secret to be omitted, null, or empty. A valid replacement removes any prior static secret and assigns the stable broker-generated HTTPS Client ID Metadata Document URL.
  • CIMD confidential to CIMD public or back keeps the client identifier URL. Switching to CIMD confidential requires a usable CIMD client-authentication key
  • Switching static to cimd replaces the operator client ID and removes a stored secret only after validation. Switching back requires an operator client_id. Static confidential also requires a new secret
  • A switch to static makes the hosted document return 404. Providers can cache the previous document for up to 300 seconds. The broker never falls back to another token authentication method
  • Updating a service to token_endpoint_auth_method: none removes the stored credential.
  • created_at timestamp is preserved
  • updated_at timestamp is set to current time

Source Transitions:

  • Stored to filesystem requires explicit filesystem, a valid resulting canonical ID, and neither inline field
  • The source change, removal of both stored credentials, and irreversible internal transition evidence commit atomically
  • Filesystem to stored requires explicit stored and explicitly supplied non-empty client_id and client_secret
  • Returning to stored encrypts the supplied secret before the atomic update. Files never supply reverse-transition credentials
  • No-op source selections preserve transition evidence. A source round trip never resets it or rewrites established identities
  • Omitted canonical ID preserves the existing ID. Null cannot clear a filesystem canonical ID
  • Filesystem rejects static public, both CIMD methods, and Google-flavor replacements and either inline field, including null or empty input
  • Validation failures and version conflicts leave the previous source, credentials, and transition evidence unchanged
  • Updates never read files or require available bindings. File paths and transition evidence are not administrative inputs

Secret Rotation:

  • Provide a new client_secret to rotate stored static confidential-service credentials
  • New secret is encrypted before storage
  • Old secret is securely discarded

Discovery Re-run:

  • If enable_discovery: true, endpoints are re-fetched
  • Useful for services that change endpoint URLs

Protected-resource refresh: An omitted issuer_uri retains the active issuer only if fresh resource metadata still advertises it. A different issuer requires explicit selection and zero user sessions. An effective authorization_params.resource change also requires zero user sessions, even when the issuer stays the same. Keep the client method and the exact token authentication method. Preserve the client ID and secret for an unchanged issuer. For a discovery-backed refresh, omitted authorization_params keeps the map and its explicit or derived source. A replacement without resource restores the derived resource. On replacement with manual configuration, omission keeps the parameter map but clears the discovery source and resource source marker. For a manual replacement, an explicit empty map clears all parameters. A discovery-backed rejection writes a safe failure-only status without changing the active service ETag. A manual conversion rejected for an audience change writes no discovery status. A request with discovery.resource_url must omit client_registration. Presence returns 400 before discovery traffic. Hosted CIMD reports cimd. DCR reports static. The selected discovery.client_method remains authoritative.

Security: Static confidential-service client_secret values are redacted in responses. Static public, CIMD, and DCR responses omit the property. Filesystem responses omit inline credentials, paths, and file values. Every response reports credential_source and client_registration.

  • protected_resources omitted or null: preserves the current protected-resource set
  • protected_resources present (including []): replaces the entire set and requires If-Match
Authorizations:
PreAuthProxyJWTPreAuth
path Parameters
service-id
required
string
Examples:
  • 770e8400-e29b-41d4-a716-446655440002 -
  • github-prod -

UUID or canonical identifier of the OAuth2 service

header Parameters
If-Match
string

Required when a service update request includes protected_resources, because that field replaces the complete set. Supply the current strong service ETag from a prior GET or protected-resource collection GET. Omit it only when protected_resources is omitted or null; a missing required header returns 428 and a stale value returns 412.

Request Body schema: application/json
required
One of
One of
One of
One of
oauth2_flavor
string
Default: "standard"
Enum: "standard" "google" "github"

OAuth2 authentication variant. Defaults to standard if omitted. Auto-detected as github when the token endpoint host equals github.com.

  • standard: Static confidential services require a non-empty plain client_secret; static public services require client_id
  • google: client_secret must be a valid Google service account JSON key (≤32 KB); client_id is derived from the JSON's client_id field and need not be provided; issuer_uri is optional; endpoints are derived from the service account JSON
  • github: Same as static confidential standard but scopes in token responses are parsed as comma-separated

private_key_jwt requires a non-credential-derived variant; google is rejected. Protected-resource discovery requires omitted or standard flavor.

client_id
required
string or null non-empty

OAuth2 client_id for this service.

In stored mode, manual static standard or github clients require this field. Stored google derives it from the service account JSON. Both CIMD methods require omission or null. The broker assigns the same HTTPS client identifier URL while registration stays cimd. Protected-resource discovery keeps or selects the identity. Filesystem mode requires omission. A filesystem-to-stored transition requires an explicit non-empty value.

For protected-resource discovery, this property must be omitted, null, or empty.

client_registration
string
Enum: "static" "cimd"

Optional on full-replacement PUT. Omission resolves to cimd for private_key_jwt, or static otherwise; it never preserves the existing registration. An explicit null, empty, or unknown value returns 400. A CIMD public replacement must send cimd again. Without it, the request resolves to static and fails unless the caller supplies a static client_id. Omit this field with discovery.resource_url. The broker keeps or selects the discovery-backed registration from the selected client method.

token_endpoint_auth_method
string or null
Enum: "none" "private_key_jwt" null

Client authentication method for the upstream token endpoint. This request retains the existing full-replacement semantics: the method is evaluated from the complete replacement request. Omit this property, or send null, for shared-secret authentication. Manual stored mode requires a non-empty client_secret. Filesystem mode forbids inline credentials. When none, the service is static public unless client_registration: cimd is explicit. Static public requires client_id. CIMD public requires omission or null client_id. Both accept omitted, null, or empty client_secret and reject Google flavor. A valid update removes any stored secret.

When private_key_jwt, the service is CIMD confidential. Omit client_id or send null. Omit client_secret or send null or "". The broker rejects a non-empty value before provider traffic. A switch to this mode requires a usable CIMD client-authentication key.

CIMD confidential and CIMD public keep the same client identifier URL when switching. A switch from static to cimd replaces the operator ID with the broker URL. The broker removes a stored secret only after the full replacement validates. A switch from cimd to static requires an operator client_id. Static confidential also requires a new non-empty client_secret. The old hosted document returns 404 after a switch to static. Providers can cache the previous document for up to 300 seconds. The broker never retries token acquisition with another method after provider rejection.

With discovery.resource_url, omit or set the method to null. The broker keeps the exact active method. Basic and POST DCR modes are response-only.

client_secret
required
string or null non-empty

Authentication credential for a static confidential service. Provide a new value to rotate.

The content depends on oauth2_flavor:

  • standard / github: Non-empty OAuth2 client secret string
  • google: Serialized Google service account JSON key document (≤32 KB)

SECURITY: Static confidential-service secrets are encrypted at rest and never returned in responses.

For manual stored services, an omitted or null authentication method requires a new non-empty static secret. Manual public and CIMD modes accept omitted, null, or empty values. Protected-resource updates also require an omitted, null, or empty value; the broker keeps the stored DCR credential when eligible. Filesystem mode forbids this property, including null or empty input. Returning to stored requires an explicit non-empty secret.

required
object
credential_source
required
string
Enum: "stored" "filesystem"

Omission preserves the existing source. Explicit null is invalid. No creation default applies to PUT.

canonical_id
string or null [ 1 .. 128 ] characters ^[A-Za-z0-9._-]+$

Omission preserves the current ID. Null clears it only in stored mode. Filesystem mode requires a valid resulting ID.

display_name
required
string [ 1 .. 255 ] characters

Human-readable name for the service

issuer_uri
string <uri>

OAuth2 issuer URI.

Required for manual standard services and HTTPS. For protected-resource discovery, omission keeps the active issuer if fresh metadata advertises it. A different issuer needs explicit selection and zero user sessions. Google retains its existing service-account rules.

object (OAuth2EndpointsRequest)

OAuth2 endpoints configuration.

Required when discovery.enable_discovery = false Optional when discovery.enable_discovery = true without resource_url (direct-metadata fallback only). Must be absent with resource_url.

Each supplied token_endpoint and authorize_endpoint must use HTTPS. Under the default strict policy, HTTP is accepted only for localhost, 127.0.0.1, or [::1]; HTTP is accepted for any host only when security.skip_thirdparty_https_validation is enabled for development/test.

Array of objects (OAuth2ScopeRequest)

Optional OAuth2 scopes for this service. Omit or provide an empty array when the provider does not use scopes.

protected_resources
Array of strings or null <uri> [ items <uri > ]

Optional protected resource URIs for RFC 8693 token exchange. Omit or set to null to preserve the current set. When present, including an empty array, this field authoritatively replaces the entire set and requires the If-Match header with the service's current strong ETag. URIs are normalized by trailing-slash trim before matching; an empty array clears the set.

object

Provider parameters. Omission preserves the map, including on replacement with manual configuration. For a manual replacement, an explicit empty object clears the map. For discovery-backed services, a replacement with resource sets an explicit absolute URI without a fragment. A replacement without resource clears the override and restores the verified resource URL, including when the replacement object is empty.

Responses

Request samples

Content type
application/json
Example
{
  • "display_name": "GitHub Production",
  • "client_id": "Iv1.1234567890abcdef",
  • "client_secret": "ghp_newsecretkey9876543210zyxwvu",
  • "issuer_uri": "https://github.com",
  • "discovery": {
    },
  • "scopes": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "770e8400-e29b-41d4-a716-446655440002",
  • "display_name": "GitHub Production",
  • "credential_source": "stored",
  • "client_registration": "static",
  • "oauth2_flavor": "github",
  • "client_id": "Iv1.1234567890abcdef",
  • "client_secret": "REDACTED",
  • "token_endpoint_auth_method": null,
  • "issuer_uri": "https://github.com",
  • "discovery": {
    },
  • "endpoints": {},
  • "scopes": [
    ],
  • "created_at": "2025-12-19T10:30:00Z",
  • "updated_at": "2025-12-19T16:00:00Z"
}

Delete OAuth2 service

Remove an OAuth2 service from the system.

Referential Integrity Check:

  • Operation fails with 409 Conflict if user grants reference this service
  • Administrators must revoke or modify grants before deletion
  • This prevents breaking active user permissions

Irreversible: This operation cannot be undone

Authorizations:
PreAuthProxyJWTPreAuth
path Parameters
service-id
required
string
Examples:
  • 770e8400-e29b-41d4-a716-446655440002 -
  • github-prod -

UUID or canonical identifier of the OAuth2 service

Responses

Response samples

Content type
application/json
{
  • "error": "client ID is required"
}

Read the last discovery attempt and active configuration

Read stored status without contacting a resource or authorization server. A missing operator principal returns 401 before the service is read. A failed refresh keeps the last successful issuer, client method, and resource URL. Manual services return not_applicable and null discovery fields.

Authorizations:
PreAuthProxyJWTPreAuth
path Parameters
service-id
required
string
Examples:
  • 770e8400-e29b-41d4-a716-446655440002 -
  • github-prod -

UUID or canonical identifier of the OAuth2 service

Responses

Response samples

Content type
application/json
Example
{}

List a service's protected resources

Authorizations:
PreAuthProxyJWTPreAuth
path Parameters
service-id
required
string
Examples:
  • 770e8400-e29b-41d4-a716-446655440002 -
  • github-prod -

UUID or canonical identifier of the OAuth2 service

Responses

Response samples

Content type
application/json
{}

Add one protected resource from the collection

Adds the body resource_uri after server-side validation and normalization. A newly added URI returns 201. A URI already owned by this service returns 200 with the set unchanged; a URI owned by another service returns 409 Conflict.

Authorizations:
PreAuthProxyJWTPreAuth
path Parameters
service-id
required
string
Examples:
  • 770e8400-e29b-41d4-a716-446655440002 -
  • github-prod -

UUID or canonical identifier of the OAuth2 service

Request Body schema: application/json
required
resource_uri
required
string <uri>

Absolute resource URI (scheme and host required), normalized by trailing-slash trim.

Responses

Request samples

Content type
application/json
{}

Response samples

Content type
application/json
{}

Add one protected resource

Idempotently adds the resource URI identified by the fully percent-encoded final path segment. The server decodes the segment exactly once, then validates and normalizes it. Adding a URI this service already owns returns 200 with the set unchanged; a URI owned by another service returns 409.

Authorizations:
PreAuthProxyJWTPreAuth
path Parameters
service-id
required
string
Examples:
  • 770e8400-e29b-41d4-a716-446655440002 -
  • github-prod -

UUID or canonical identifier of the OAuth2 service

resource
required
string
Example: https%3A%2F%2Fapi.example.com%2Fv2

Normalized protected resource URI, fully RFC 3986 percent-encoded as one path segment (: to %3A, / to %2F, ? to %3F, and # to %23). The server preserves the escaped segment, decodes it exactly once, then validates it as an absolute URI and normalizes its trailing slash.

Responses

Response samples

Content type
application/json
{}

Rename a protected resource in place

Atomically changes the percent-encoded source URI in {resource} to the target URI in the body. A source equal to the normalized target succeeds as a no-op. A target already owned by any service returns 409; a source not owned by this service returns 404.

Authorizations:
PreAuthProxyJWTPreAuth
path Parameters
service-id
required
string
Examples:
  • 770e8400-e29b-41d4-a716-446655440002 -
  • github-prod -

UUID or canonical identifier of the OAuth2 service

resource
required
string
Example: https%3A%2F%2Fapi.example.com%2Fv2

Normalized protected resource URI, fully RFC 3986 percent-encoded as one path segment (: to %3A, / to %2F, ? to %3F, and # to %23). The server preserves the escaped segment, decodes it exactly once, then validates it as an absolute URI and normalizes its trailing slash.

Request Body schema: application/json
required
to
required
string <uri>

Absolute target URI (scheme and host required), normalized by trailing-slash trim.

Responses

Request samples

Content type
application/json

Response samples

Content type
application/json
{}

Remove one protected resource

Removes the resource identified by the fully percent-encoded path segment after decoding exactly once and normalizing it. A resource not owned by this service returns 404.

Authorizations:
PreAuthProxyJWTPreAuth
path Parameters
service-id
required
string
Examples:
  • 770e8400-e29b-41d4-a716-446655440002 -
  • github-prod -

UUID or canonical identifier of the OAuth2 service

resource
required
string
Example: https%3A%2F%2Fapi.example.com%2Fv2

Normalized protected resource URI, fully RFC 3986 percent-encoded as one path segment (: to %3A, / to %2F, ? to %3F, and # to %23). The server preserves the escaped segment, decodes it exactly once, then validates it as an absolute URI and normalizes its trailing slash.

Responses

Response samples

Content type
application/json
{}

PermissionSets

CRUD operations for permission set management

Create a permission set

Creates a new named permission set grouping OAuth2 scopes across one or more third-party services.

Validation rules:

  • name must be unique (409 Conflict if duplicate)
  • service_scopes must contain at least one entry (400)
  • Each service_id in service_scopes must reference an existing service (400)
  • Each scopes list may be empty for a scope-less service; supplied values must be non-empty (400)
Authorizations:
PreAuthProxyJWTPreAuth
Request Body schema: application/json
required
canonical_id
string or null [ 1 .. 128 ] characters ^[A-Za-z0-9._-]+$
name
required
string <= 255 characters

Unique human-readable name for the permission set

description
required
string

Human-readable explanation of what capabilities this set grants

required
Array of objects (ServiceScope) non-empty

One or more service/scope entries covered by this permission set

Responses

Request samples

Content type
application/json
{
  • "canonical_id": "github-read",
  • "name": "GitHub Read Access",
  • "description": "Read repository contents and user profile from GitHub",
  • "service_scopes": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  • "canonical_id": "github-read",
  • "name": "GitHub Read Access",
  • "description": "Read repository contents and user profile from GitHub",
  • "service_scopes": [
    ],
  • "created_at": "2026-03-25T12:00:00Z",
  • "updated_at": "2026-03-25T12:00:00Z"
}

List permission sets

Returns all permission sets. Optionally filter by service ID to find all permission sets that include scopes for a specific third-party service.

Authorizations:
PreAuthProxyJWTPreAuth
query Parameters
service_id
string

UUID or canonical identifier of the service used to filter permission sets

header Parameters
Prefer
string
Example: reference-id=canonical

Request canonical rendering of nested service and permission-set references. The default representation uses UUIDs. May be combined with other Prefer preferences (e.g. return=minimal, reference-id=canonical); unrecognized preferences are ignored.

Responses

Response samples

Content type
application/json
{
  • "items": [
    ]
}

Get permission set by ID

Authorizations:
PreAuthProxyJWTPreAuth
path Parameters
permission-set-id
required
string
Examples:
  • a1b2c3d4-e5f6-7890-abcd-ef1234567890 -
  • github-read -

UUID or canonical identifier of the permission set

header Parameters
Prefer
string
Example: reference-id=canonical

Request canonical rendering of nested service and permission-set references. The default representation uses UUIDs. May be combined with other Prefer preferences (e.g. return=minimal, reference-id=canonical); unrecognized preferences are ignored.

Responses

Response samples

Content type
application/json
{
  • "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  • "canonical_id": "github-read",
  • "name": "GitHub Read Access",
  • "description": "Read repository contents and user profile from GitHub",
  • "service_scopes": [
    ],
  • "created_at": "2026-03-25T12:00:00Z",
  • "updated_at": "2026-03-25T12:00:00Z"
}

Replace a permission set

Fully replaces a permission set's name, description, and service_scopes. All fields are required (PUT semantics — full replacement).

Note: Changing service_scopes takes effect immediately; the next token exchange for any grant referencing this set will use the updated scopes (cache TTL ≤ 60s delay).

Authorizations:
PreAuthProxyJWTPreAuth
path Parameters
permission-set-id
required
string
Examples:
  • a1b2c3d4-e5f6-7890-abcd-ef1234567890 -
  • github-read -

UUID or canonical identifier of the permission set

Request Body schema: application/json
required
canonical_id
string or null [ 1 .. 128 ] characters ^[A-Za-z0-9._-]+$
name
required
string <= 255 characters

Unique human-readable name for the permission set

description
required
string

Human-readable explanation of what capabilities this set grants

required
Array of objects (ServiceScope) non-empty

One or more service/scope entries covered by this permission set

Responses

Request samples

Content type
application/json
{
  • "canonical_id": "github-read",
  • "name": "GitHub Read Access",
  • "description": "Read repository contents and user profile from GitHub",
  • "service_scopes": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  • "canonical_id": "github-read",
  • "name": "GitHub Read Access",
  • "description": "Read repository contents and user profile from GitHub",
  • "service_scopes": [
    ],
  • "created_at": "2026-03-25T12:00:00Z",
  • "updated_at": "2026-03-25T12:00:00Z"
}

Delete a permission set

Deletes a permission set by ID.

Rejected with 409 Conflict if any agent's permission_sets list references this ID, or if any active user grant references this permission set. Admins must remove the permission set from all agents and revoke all active grants referencing it before deletion.

Irreversible: This operation cannot be undone.

Authorizations:
PreAuthProxyJWTPreAuth
path Parameters
permission-set-id
required
string
Examples:
  • a1b2c3d4-e5f6-7890-abcd-ef1234567890 -
  • github-read -

UUID or canonical identifier of the permission set

Responses

Response samples

Content type
application/json
{}

Client Credentials

Broker-issued client credential management for agents

Generate or rotate broker-issued client credentials for an agent

Generate new broker-issued client credentials for an agent, or rotate existing credentials. This allows the broker to act as an OAuth2 authorization server and authenticate agents directly.

Runtime usage: Agents use the client_id returned here as client_id at the /oauth2/token endpoint. The value is the agent's UUID.

First Generation (no existing credentials):

  • Returns 201 Created with new client_id (metadata) and client_secret
  • The client_secret is only returned in this response — store it securely

Rotation (existing credentials):

  • Returns 200 OK with new credentials and previous_invalidated_at timestamp
  • Previous credentials are immediately invalidated
  • Active tokens issued with previous credentials remain valid until expiry
Authorizations:
PreAuthProxyJWTPreAuth
path Parameters
agent-id
required
string
Examples:
  • 550e8400-e29b-41d4-a716-446655440000 -
  • research-agent -

UUID or canonical identifier of the agent

Responses

Response samples

Content type
application/json
{
  • "client_id": "550e8400-e29b-41d4-a716-446655440000",
  • "client_secret": "brk_sec_q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2",
  • "created_at": "2025-12-19T15:45:00Z",
  • "previous_invalidated_at": "2025-12-19T15:45:00Z"
}

Get credential metadata (never returns secret)

Retrieve metadata about an agent's broker-issued client credentials.

Security: The client_secret is never returned by this endpoint. Only the client_id and timestamps are returned.

Returns 404 Not Found if no credentials have been generated for the agent.

Authorizations:
PreAuthProxyJWTPreAuth
path Parameters
agent-id
required
string
Examples:
  • 550e8400-e29b-41d4-a716-446655440000 -
  • research-agent -

UUID or canonical identifier of the agent

Responses

Response samples

Content type
application/json
{
  • "client_id": "550e8400-e29b-41d4-a716-446655440000",
  • "created_at": "2025-12-19T10:30:00Z",
  • "rotated_at": "2025-12-19T15:45:00Z"
}

Revoke broker-issued credentials

Revoke an agent's broker-issued client credentials.

After revocation, the agent can no longer authenticate using these credentials. Active tokens issued with these credentials remain valid until expiry.

Irreversible: New credentials must be generated via POST after revocation.

Authorizations:
PreAuthProxyJWTPreAuth
path Parameters
agent-id
required
string
Examples:
  • 550e8400-e29b-41d4-a716-446655440000 -
  • research-agent -

UUID or canonical identifier of the agent

Responses

Response samples

Content type
application/json
{
  • "error": "invalid request",
  • "message": "agent ID must be a valid UUID"
}

Signing Keys

OAuth2 server signing key management

Add a new signing key

Generate a new signing key for the broker's OAuth2 server. The new key is marked as is_current: true but does not start signing tokens until activates_at (600 s after key preparation). This covers a 300 s compliant JWKS cache lifetime and a 30 s rebuild budget with 270 s of margin under healthy-service conditions. External verifiers must refresh their own caches; publication cannot force them to learn the key.

Algorithm Support:

  • ES256: ECDSA using P-256 curve and SHA-256 (default, only supported algorithm)

Key Lifecycle:

  • New key is available on the next direct GET /oauth2/jwks.json after storage commits
  • New key starts signing tokens only after activates_at (key preparation + 600 s)
  • Previous current key continues signing during the grace period
  • Private key material is never exposed via the API
Authorizations:
PreAuthProxyJWTPreAuth
Request Body schema: application/json
optional
algorithm
string
Default: "ES256"
Value: "ES256"

Signing algorithm for the new key.

  • ES256: ECDSA using P-256 curve and SHA-256 (default, only supported algorithm)

Responses

Request samples

Content type
application/json
Example
{ }

Response samples

Content type
application/json
{
  • "kid": "key-2025-12-19-001",
  • "algorithm": "ES256",
  • "is_current": true,
  • "activates_at": "2025-12-19T10:40:00Z",
  • "created_at": "2025-12-19T10:30:00Z"
}

List active signing keys (no private material)

Retrieve all active signing keys for the broker's OAuth2 server.

Security: Private key material is never included in the response. Only public metadata (kid, algorithm, status, timestamps) is returned.

Keys are returned in creation order (newest first).

Authorizations:
PreAuthProxyJWTPreAuth

Responses

Response samples

Content type
application/json
Example
{
  • "items": [
    ]
}

Promote signing key to current

Promote an existing signing key to be the current signing key. The current key is used for signing new tokens.

Behavior:

  • The specified key becomes is_current: true
  • The previous current key is set to is_current: false
  • Previous key remains active for token verification
Authorizations:
PreAuthProxyJWTPreAuth
path Parameters
kid
required
string
Example: key-2025-12-19-001

Key identifier of the signing key

Responses

Response samples

Content type
application/json
{
  • "kid": "key-2025-12-01-001",
  • "algorithm": "ES256",
  • "is_current": true,
  • "created_at": "2025-12-01T09:00:00Z"
}

Remove signing key (soft delete)

Remove a signing key from the active key set.

Soft Delete: The key is deactivated but retained for audit purposes. Tokens previously signed with this key can no longer be verified.

Constraints:

  • Cannot delete the last remaining signing key (returns 409 Conflict)
  • Cannot delete the current signing key without first promoting another key
Authorizations:
PreAuthProxyJWTPreAuth
path Parameters
kid
required
string
Example: key-2025-12-19-001

Key identifier of the signing key

Responses

Response samples

Content type
application/json
{
  • "error": "not found",
  • "message": "Signing key with kid 'key-unknown' does not exist"
}

CIMD Client Keys

Administrative lifecycle operations for the broker-global CIMD client-authentication key set

Generate a CIMD client-authentication key

Generate an ES256 key in the distinct broker-global CIMD client-authentication key domain. This key signs outbound private_key_jwt client assertions only; it never signs broker-issued access tokens.

Key Lifecycle:

  • The first generated CIMD key becomes usable immediately.
  • A later generated current key is published in the CIMD JWK Set during the existing activation grace period, while the previous usable key continues signing assertions.
  • Private key material is never exposed by this API.
Authorizations:
PreAuthProxyJWTPreAuth
Request Body schema: application/json
optional
algorithm
string
Default: "ES256"
Value: "ES256"

ES256 is the default and only accepted CIMD client-authentication key algorithm.

Responses

Request samples

Content type
application/json
Example
{ }

Response samples

Content type
application/json
{
  • "kid": "cimd-key-2026-09-21-001",
  • "algorithm": "ES256",
  • "is_current": true,
  • "activates_at": "2026-09-21T10:30:00Z",
  • "created_at": "2026-09-21T10:30:00Z"
}

List active CIMD client-authentication keys

Retrieve active CIMD client-authentication keys in creation order (newest first). Responses contain metadata only and never include private key bytes, PEM, ciphertext, branch-key identifiers, or client assertions.

Authorizations:
PreAuthProxyJWTPreAuth

Responses

Response samples

Content type
application/json
{
  • "items": [
    ]
}

Promote a CIMD client-authentication key immediately

Promote an existing CIMD client-authentication key to current immediately. The promoted key becomes usable to sign subsequent client assertions at promotion time. The prior key remains available for verification through the CIMD JWK Set.

Authorizations:
PreAuthProxyJWTPreAuth
path Parameters
kid
required
string
Example: cimd-key-2026-09-21-001

Globally unique identifier of the CIMD client-authentication key

Responses

Response samples

Content type
application/json
{
  • "kid": "cimd-key-2026-09-01-001",
  • "algorithm": "ES256",
  • "is_current": true,
  • "activates_at": "2026-09-21T10:45:00Z",
  • "created_at": "2026-09-01T09:00:00Z"
}

Remove a non-signing CIMD client-authentication key

Remove a CIMD client-authentication key that is neither the last key, the current key, nor an effective-current key that can still sign assertions. Removed keys are no longer published in the CIMD JWK Set.

Authorizations:
PreAuthProxyJWTPreAuth
path Parameters
kid
required
string
Example: cimd-key-2026-09-21-001

Globally unique identifier of the CIMD client-authentication key

Responses

Response samples

Content type
application/json
Example
{
  • "error": "agent not found",
  • "message": "Agent with ID '550e8400-e29b-41d4-a716-446655440000' does not exist"
}

Sessions

Administrative maintenance of third-party user sessions across all principals

Refresh third-party sessions whose access tokens expire soon

Refreshes every stored third-party user session whose access token expires within the lookahead window, across all principals and services. Intended to be called on a schedule by an external job, such as the CronJob the broker Helm chart renders when sessionSweep.enabled is true; the broker runs no internal scheduler.

Selection: A session is a candidate when its access token has a recorded expiry at or before now + lookahead_duration. Sessions with no access-token expiry are never candidates. Healthy sessions outside the window are not evaluated and do not appear in any count.

Processing: Candidates are read in pages of page_size and refreshed one at a time using each session's stored refresh token. Newly issued tokens are encrypted before they are stored. A candidate that another refresh already renewed, or that was deleted during the sweep, is counted as skipped. A candidate that cannot be refreshed (no usable refresh token, upstream error) is counted as failed. The sweep then continues and the stored tokens are left unchanged. When the provider rejects the refresh token with a 4xx invalid_grant (other than 429), the broker records the rejection and counts that attempt as failed. Later sweeps exclude the session from their candidates, so it contributes to no count and the provider is not contacted again until the user reconnects.

Dry run: With dry_run: true, candidates are classified without calling any upstream provider and without writing. Candidates that would be refreshed are counted in refreshed.

Concurrency: Safe to invoke concurrently on several broker replicas. Each session is refreshed under a row lock, so concurrent sweeps never refresh the same session twice and never leave a partial update. Concurrent sweeps do not divide the work between them.

Duration: The request runs until every candidate has been evaluated. The server removes its write timeout for this operation. The caller controls the overall timeout. If the client disconnects, the sweep stops after the session in progress, and sessions already refreshed stay refreshed.

Response semantics: 200 OK is returned whenever the sweep itself completes, even if every candidate failed. 500 is returned if the sweep cannot continue because of an infrastructure failure or the request lacks an operator principal. The response contains only counts and never token material.

Every invocation is audit-logged with the operator principal.

Authorizations:
PreAuthProxyJWTPreAuth
Request Body schema: application/json
optional

All fields are optional. An empty body and {} are equivalent. The Content-Type: application/json header is still required (see Browser Request Protection).

lookahead_duration
string <duration> ^P(?!$)(?:\d+D)?(?:T(?=\d)(?:\d+H)?(?:\d+M)?(...

ISO 8601 duration. Sessions whose access token expires within this duration from the start of the sweep are refreshed. Only day, hour, minute, and second components are accepted (P[nD][T[nH][nM][n[.f]S]]); year, month, and week components are rejected. Must be greater than zero. Defaults to the server-configured token_refresh.lookahead_duration (default 5 minutes).

dry_run
boolean
Default: false

When true, report what would be refreshed without contacting upstream providers or writing.

page_size
integer <int32> [ 1 .. 1000 ]

Number of candidate sessions read per page. Defaults to the server-configured token_refresh.sweep.default_page_size (default 100).

Responses

Request samples

Content type
application/json
Example
{ }

Response samples

Content type
application/json
Example
{
  • "refreshed": 42,
  • "skipped": 3,
  • "failed": 1,
  • "total_evaluated": 46,
  • "dry_run": false
}