feat: Phase 1 MVP — complete AgentIdP implementation
Implements all P0 features per OpenSpec change phase-1-mvp-implementation: - Agent Registry Service (CRUD) — full lifecycle management - OAuth 2.0 Token Service (Client Credentials flow) - Credential Management (generate, rotate, revoke) - Immutable Audit Log Service Tech: Node.js 18+, TypeScript 5.3+ strict, Express 4.18+, PostgreSQL 14+, Redis 7+ Standards: OpenAPI 3.0 specs, DRY/SOLID, zero `any` types Quality: 18 unit test suites, 244 tests passing, 97%+ coverage OpenAPI: 4 complete specs (14 endpoints total) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
816
docs/openapi/agent-registry.yaml
Normal file
816
docs/openapi/agent-registry.yaml
Normal file
@@ -0,0 +1,816 @@
|
||||
openapi: 3.0.3
|
||||
|
||||
info:
|
||||
title: SentryAgent.ai — Agent Registry Service
|
||||
version: 1.0.0
|
||||
description: |
|
||||
The Agent Registry Service provides full lifecycle management for AI agent
|
||||
identities on the SentryAgent.ai AgentIdP platform. Every AI agent is treated
|
||||
as a first-class non-human identity, aligned with the AGNTCY standard
|
||||
(Linux Foundation).
|
||||
|
||||
Agents receive unique, immutable identifiers (UUIDs), typed capabilities,
|
||||
and lifecycle status management. The registry is the authoritative source of
|
||||
truth for all registered agent identities.
|
||||
|
||||
**Free Tier Limits**:
|
||||
- Max 100 registered agents per account
|
||||
- API rate limit: 100 requests/minute
|
||||
|
||||
servers:
|
||||
- url: http://localhost:3000/api/v1
|
||||
description: Local development server
|
||||
- url: https://api.sentryagent.ai/v1
|
||||
description: Production server
|
||||
|
||||
tags:
|
||||
- name: Agent Registry
|
||||
description: CRUD operations for AI agent identities
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
JWT access token obtained via the OAuth 2.0 Client Credentials flow
|
||||
(`POST /token`). Include in the `Authorization` header as:
|
||||
`Authorization: Bearer <token>`
|
||||
|
||||
schemas:
|
||||
AgentType:
|
||||
type: string
|
||||
description: The functional classification of the AI agent.
|
||||
enum:
|
||||
- screener
|
||||
- classifier
|
||||
- orchestrator
|
||||
- extractor
|
||||
- summarizer
|
||||
- router
|
||||
- monitor
|
||||
- custom
|
||||
example: screener
|
||||
|
||||
DeploymentEnv:
|
||||
type: string
|
||||
description: The target deployment environment for the agent.
|
||||
enum:
|
||||
- development
|
||||
- staging
|
||||
- production
|
||||
example: production
|
||||
|
||||
AgentStatus:
|
||||
type: string
|
||||
description: |
|
||||
Lifecycle status of the agent.
|
||||
- `active`: Agent is operational and can authenticate.
|
||||
- `suspended`: Agent is temporarily disabled; credentials cannot be used.
|
||||
- `decommissioned`: Agent is permanently retired; soft-deleted record remains.
|
||||
enum:
|
||||
- active
|
||||
- suspended
|
||||
- decommissioned
|
||||
example: active
|
||||
|
||||
Agent:
|
||||
type: object
|
||||
description: Full representation of a registered AI agent identity.
|
||||
required:
|
||||
- agentId
|
||||
- email
|
||||
- agentType
|
||||
- version
|
||||
- capabilities
|
||||
- owner
|
||||
- deploymentEnv
|
||||
- status
|
||||
- createdAt
|
||||
- updatedAt
|
||||
properties:
|
||||
agentId:
|
||||
type: string
|
||||
format: uuid
|
||||
description: >
|
||||
Immutable, system-assigned unique identifier for the agent.
|
||||
Assigned at registration and never changes.
|
||||
readOnly: true
|
||||
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
description: >
|
||||
Unique email-format identifier for the agent. Acts as the human-readable
|
||||
stable name for this agent identity.
|
||||
example: "screener-001@sentryagent.ai"
|
||||
agentType:
|
||||
$ref: '#/components/schemas/AgentType'
|
||||
version:
|
||||
type: string
|
||||
description: Semantic version string of the agent software.
|
||||
pattern: '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'
|
||||
example: "1.4.2"
|
||||
capabilities:
|
||||
type: array
|
||||
description: >
|
||||
List of capability strings representing what this agent is permitted
|
||||
to do. Uses a `resource:action` convention.
|
||||
items:
|
||||
type: string
|
||||
pattern: '^[a-z0-9_-]+:[a-z0-9_*-]+$'
|
||||
minItems: 1
|
||||
example:
|
||||
- "resume:read"
|
||||
- "email:send"
|
||||
- "candidate:score"
|
||||
owner:
|
||||
type: string
|
||||
description: Team or organisation that owns and is responsible for this agent.
|
||||
minLength: 1
|
||||
maxLength: 128
|
||||
example: "talent-acquisition-team"
|
||||
deploymentEnv:
|
||||
$ref: '#/components/schemas/DeploymentEnv'
|
||||
status:
|
||||
$ref: '#/components/schemas/AgentStatus'
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: ISO 8601 timestamp when the agent was first registered.
|
||||
readOnly: true
|
||||
example: "2026-03-28T09:00:00.000Z"
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: ISO 8601 timestamp of the most recent update to this agent record.
|
||||
readOnly: true
|
||||
example: "2026-03-28T11:30:00.000Z"
|
||||
|
||||
CreateAgentRequest:
|
||||
type: object
|
||||
description: Request body for registering a new AI agent identity.
|
||||
required:
|
||||
- email
|
||||
- agentType
|
||||
- version
|
||||
- capabilities
|
||||
- owner
|
||||
- deploymentEnv
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
description: >
|
||||
Unique email-format identifier for the agent.
|
||||
Must be unique across all registered agents in the system.
|
||||
example: "screener-001@sentryagent.ai"
|
||||
agentType:
|
||||
$ref: '#/components/schemas/AgentType'
|
||||
version:
|
||||
type: string
|
||||
description: Semantic version string of the agent software.
|
||||
pattern: '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'
|
||||
example: "1.0.0"
|
||||
capabilities:
|
||||
type: array
|
||||
description: List of capability strings for this agent.
|
||||
items:
|
||||
type: string
|
||||
pattern: '^[a-z0-9_-]+:[a-z0-9_*-]+$'
|
||||
minItems: 1
|
||||
example:
|
||||
- "resume:read"
|
||||
- "email:send"
|
||||
owner:
|
||||
type: string
|
||||
description: Team or organisation that owns this agent.
|
||||
minLength: 1
|
||||
maxLength: 128
|
||||
example: "talent-acquisition-team"
|
||||
deploymentEnv:
|
||||
$ref: '#/components/schemas/DeploymentEnv'
|
||||
|
||||
UpdateAgentRequest:
|
||||
type: object
|
||||
description: >
|
||||
Request body for updating agent metadata. All fields are optional;
|
||||
only provided fields are updated. `agentId`, `email`, and `createdAt`
|
||||
are immutable and cannot be changed.
|
||||
minProperties: 1
|
||||
properties:
|
||||
agentType:
|
||||
$ref: '#/components/schemas/AgentType'
|
||||
version:
|
||||
type: string
|
||||
description: Updated semantic version string.
|
||||
pattern: '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'
|
||||
example: "1.5.0"
|
||||
capabilities:
|
||||
type: array
|
||||
description: Updated list of capability strings. Replaces the full list.
|
||||
items:
|
||||
type: string
|
||||
pattern: '^[a-z0-9_-]+:[a-z0-9_*-]+$'
|
||||
minItems: 1
|
||||
example:
|
||||
- "resume:read"
|
||||
- "email:send"
|
||||
- "candidate:score"
|
||||
- "report:write"
|
||||
owner:
|
||||
type: string
|
||||
description: Updated owner team or organisation.
|
||||
minLength: 1
|
||||
maxLength: 128
|
||||
example: "platform-team"
|
||||
deploymentEnv:
|
||||
$ref: '#/components/schemas/DeploymentEnv'
|
||||
status:
|
||||
$ref: '#/components/schemas/AgentStatus'
|
||||
|
||||
PaginatedAgentsResponse:
|
||||
type: object
|
||||
description: Paginated list of agent identities.
|
||||
required:
|
||||
- data
|
||||
- total
|
||||
- page
|
||||
- limit
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Agent'
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of agents matching the query filters.
|
||||
example: 47
|
||||
page:
|
||||
type: integer
|
||||
description: Current page number (1-based).
|
||||
example: 1
|
||||
limit:
|
||||
type: integer
|
||||
description: Number of items per page.
|
||||
example: 20
|
||||
|
||||
ErrorResponse:
|
||||
type: object
|
||||
description: Standard error response envelope used across all SentryAgent.ai APIs.
|
||||
required:
|
||||
- code
|
||||
- message
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
description: >
|
||||
Machine-readable error code. Use this field for programmatic error handling.
|
||||
example: "AGENT_NOT_FOUND"
|
||||
message:
|
||||
type: string
|
||||
description: Human-readable description of the error.
|
||||
example: "Agent with the specified ID was not found."
|
||||
details:
|
||||
type: object
|
||||
description: >
|
||||
Optional structured details providing additional context about the error,
|
||||
such as field-level validation failures.
|
||||
additionalProperties: true
|
||||
example:
|
||||
field: "email"
|
||||
reason: "Email address is already registered to another agent."
|
||||
|
||||
responses:
|
||||
Unauthorized:
|
||||
description: Missing or invalid Bearer token.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "UNAUTHORIZED"
|
||||
message: "A valid Bearer token is required to access this resource."
|
||||
|
||||
Forbidden:
|
||||
description: Valid token but insufficient permissions.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "FORBIDDEN"
|
||||
message: "You do not have permission to perform this action."
|
||||
|
||||
NotFound:
|
||||
description: The requested resource was not found.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "AGENT_NOT_FOUND"
|
||||
message: "Agent with the specified ID was not found."
|
||||
|
||||
TooManyRequests:
|
||||
description: Rate limit exceeded. Retry after the reset time.
|
||||
headers:
|
||||
X-RateLimit-Limit:
|
||||
schema:
|
||||
type: integer
|
||||
description: Maximum number of requests allowed per minute.
|
||||
example: 100
|
||||
X-RateLimit-Remaining:
|
||||
schema:
|
||||
type: integer
|
||||
description: Number of requests remaining in the current window.
|
||||
example: 0
|
||||
X-RateLimit-Reset:
|
||||
schema:
|
||||
type: integer
|
||||
description: Unix timestamp (seconds) when the rate limit window resets.
|
||||
example: 1743155400
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "RATE_LIMIT_EXCEEDED"
|
||||
message: "Too many requests. Please retry after the rate limit window resets."
|
||||
|
||||
InternalServerError:
|
||||
description: Unexpected server error. Contact support if the issue persists.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "INTERNAL_SERVER_ERROR"
|
||||
message: "An unexpected error occurred. Please try again later."
|
||||
|
||||
security:
|
||||
- BearerAuth: []
|
||||
|
||||
paths:
|
||||
/agents:
|
||||
post:
|
||||
operationId: registerAgent
|
||||
tags:
|
||||
- Agent Registry
|
||||
summary: Register a new AI agent
|
||||
description: |
|
||||
Creates a new AI agent identity in the SentryAgent.ai registry.
|
||||
|
||||
A unique immutable `agentId` (UUID) is system-assigned on creation.
|
||||
The `email` must be unique across all registered agents.
|
||||
|
||||
**Free Tier**: Maximum 100 registered agents per account. Attempting to
|
||||
register beyond this limit returns `403 Forbidden` with code `FREE_TIER_LIMIT_EXCEEDED`.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateAgentRequest'
|
||||
example:
|
||||
email: "screener-001@sentryagent.ai"
|
||||
agentType: "screener"
|
||||
version: "1.0.0"
|
||||
capabilities:
|
||||
- "resume:read"
|
||||
- "email:send"
|
||||
owner: "talent-acquisition-team"
|
||||
deploymentEnv: "production"
|
||||
responses:
|
||||
'201':
|
||||
description: Agent registered successfully.
|
||||
headers:
|
||||
X-RateLimit-Limit:
|
||||
schema:
|
||||
type: integer
|
||||
example: 100
|
||||
X-RateLimit-Remaining:
|
||||
schema:
|
||||
type: integer
|
||||
example: 99
|
||||
X-RateLimit-Reset:
|
||||
schema:
|
||||
type: integer
|
||||
example: 1743155400
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Agent'
|
||||
example:
|
||||
agentId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
email: "screener-001@sentryagent.ai"
|
||||
agentType: "screener"
|
||||
version: "1.0.0"
|
||||
capabilities:
|
||||
- "resume:read"
|
||||
- "email:send"
|
||||
owner: "talent-acquisition-team"
|
||||
deploymentEnv: "production"
|
||||
status: "active"
|
||||
createdAt: "2026-03-28T09:00:00.000Z"
|
||||
updatedAt: "2026-03-28T09:00:00.000Z"
|
||||
'400':
|
||||
description: Invalid request body. Check `details` for field-level errors.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "VALIDATION_ERROR"
|
||||
message: "Request validation failed."
|
||||
details:
|
||||
field: "email"
|
||||
reason: "Must be a valid email address."
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
description: Forbidden. Either insufficient permissions or free tier limit reached.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
insufficientPermissions:
|
||||
summary: Insufficient permissions
|
||||
value:
|
||||
code: "FORBIDDEN"
|
||||
message: "You do not have permission to register agents."
|
||||
freeTierLimit:
|
||||
summary: Free tier agent limit reached
|
||||
value:
|
||||
code: "FREE_TIER_LIMIT_EXCEEDED"
|
||||
message: "Free tier limit of 100 registered agents has been reached."
|
||||
details:
|
||||
limit: 100
|
||||
current: 100
|
||||
'409':
|
||||
description: An agent with the provided email address is already registered.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "AGENT_ALREADY_EXISTS"
|
||||
message: "An agent with this email address is already registered."
|
||||
details:
|
||||
email: "screener-001@sentryagent.ai"
|
||||
'429':
|
||||
$ref: '#/components/responses/TooManyRequests'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
get:
|
||||
operationId: listAgents
|
||||
tags:
|
||||
- Agent Registry
|
||||
summary: List registered agents
|
||||
description: |
|
||||
Returns a paginated list of all registered AI agent identities accessible
|
||||
to the authenticated caller.
|
||||
|
||||
Results can be filtered by `owner`, `agentType`, and/or `status`.
|
||||
Results are ordered by `createdAt` descending (most recent first).
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
description: Page number (1-based). Defaults to `1`.
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
default: 1
|
||||
example: 1
|
||||
- name: limit
|
||||
in: query
|
||||
description: Number of results per page. Defaults to `20`, maximum `100`.
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
default: 20
|
||||
example: 20
|
||||
- name: owner
|
||||
in: query
|
||||
description: Filter agents by owner name (exact match).
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
example: "talent-acquisition-team"
|
||||
- name: agentType
|
||||
in: query
|
||||
description: Filter agents by type.
|
||||
required: false
|
||||
schema:
|
||||
$ref: '#/components/schemas/AgentType'
|
||||
- name: status
|
||||
in: query
|
||||
description: Filter agents by lifecycle status.
|
||||
required: false
|
||||
schema:
|
||||
$ref: '#/components/schemas/AgentStatus'
|
||||
responses:
|
||||
'200':
|
||||
description: Paginated list of agents returned successfully.
|
||||
headers:
|
||||
X-RateLimit-Limit:
|
||||
schema:
|
||||
type: integer
|
||||
example: 100
|
||||
X-RateLimit-Remaining:
|
||||
schema:
|
||||
type: integer
|
||||
example: 95
|
||||
X-RateLimit-Reset:
|
||||
schema:
|
||||
type: integer
|
||||
example: 1743155400
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PaginatedAgentsResponse'
|
||||
example:
|
||||
data:
|
||||
- agentId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
email: "screener-001@sentryagent.ai"
|
||||
agentType: "screener"
|
||||
version: "1.4.2"
|
||||
capabilities:
|
||||
- "resume:read"
|
||||
- "email:send"
|
||||
- "candidate:score"
|
||||
owner: "talent-acquisition-team"
|
||||
deploymentEnv: "production"
|
||||
status: "active"
|
||||
createdAt: "2026-03-01T08:00:00.000Z"
|
||||
updatedAt: "2026-03-28T09:00:00.000Z"
|
||||
- agentId: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
|
||||
email: "classifier-002@sentryagent.ai"
|
||||
agentType: "classifier"
|
||||
version: "2.1.0"
|
||||
capabilities:
|
||||
- "document:classify"
|
||||
- "label:write"
|
||||
owner: "talent-acquisition-team"
|
||||
deploymentEnv: "staging"
|
||||
status: "active"
|
||||
createdAt: "2026-03-10T10:00:00.000Z"
|
||||
updatedAt: "2026-03-10T10:00:00.000Z"
|
||||
total: 47
|
||||
page: 1
|
||||
limit: 20
|
||||
'400':
|
||||
description: Invalid query parameters.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "VALIDATION_ERROR"
|
||||
message: "Invalid query parameter value."
|
||||
details:
|
||||
field: "limit"
|
||||
reason: "Must be an integer between 1 and 100."
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'429':
|
||||
$ref: '#/components/responses/TooManyRequests'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/agents/{agentId}:
|
||||
parameters:
|
||||
- name: agentId
|
||||
in: path
|
||||
description: The unique UUID identifier of the agent.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
|
||||
get:
|
||||
operationId: getAgentById
|
||||
tags:
|
||||
- Agent Registry
|
||||
summary: Get agent by ID
|
||||
description: |
|
||||
Retrieves the full identity record for a single AI agent by its immutable `agentId`.
|
||||
responses:
|
||||
'200':
|
||||
description: Agent record returned successfully.
|
||||
headers:
|
||||
X-RateLimit-Limit:
|
||||
schema:
|
||||
type: integer
|
||||
example: 100
|
||||
X-RateLimit-Remaining:
|
||||
schema:
|
||||
type: integer
|
||||
example: 94
|
||||
X-RateLimit-Reset:
|
||||
schema:
|
||||
type: integer
|
||||
example: 1743155400
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Agent'
|
||||
example:
|
||||
agentId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
email: "screener-001@sentryagent.ai"
|
||||
agentType: "screener"
|
||||
version: "1.4.2"
|
||||
capabilities:
|
||||
- "resume:read"
|
||||
- "email:send"
|
||||
- "candidate:score"
|
||||
owner: "talent-acquisition-team"
|
||||
deploymentEnv: "production"
|
||||
status: "active"
|
||||
createdAt: "2026-03-01T08:00:00.000Z"
|
||||
updatedAt: "2026-03-28T09:00:00.000Z"
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'429':
|
||||
$ref: '#/components/responses/TooManyRequests'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
patch:
|
||||
operationId: updateAgent
|
||||
tags:
|
||||
- Agent Registry
|
||||
summary: Update agent metadata
|
||||
description: |
|
||||
Partially updates the metadata for an existing agent.
|
||||
|
||||
Only the fields provided in the request body are updated. Omitted fields
|
||||
are left unchanged. The following fields are immutable and cannot be
|
||||
updated: `agentId`, `email`, `createdAt`.
|
||||
|
||||
Setting `status` to `decommissioned` is a one-way operation — a
|
||||
decommissioned agent cannot be reactivated.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UpdateAgentRequest'
|
||||
example:
|
||||
version: "1.5.0"
|
||||
capabilities:
|
||||
- "resume:read"
|
||||
- "email:send"
|
||||
- "candidate:score"
|
||||
- "report:write"
|
||||
deploymentEnv: "production"
|
||||
responses:
|
||||
'200':
|
||||
description: Agent updated successfully. Returns the full updated agent record.
|
||||
headers:
|
||||
X-RateLimit-Limit:
|
||||
schema:
|
||||
type: integer
|
||||
example: 100
|
||||
X-RateLimit-Remaining:
|
||||
schema:
|
||||
type: integer
|
||||
example: 93
|
||||
X-RateLimit-Reset:
|
||||
schema:
|
||||
type: integer
|
||||
example: 1743155400
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Agent'
|
||||
example:
|
||||
agentId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
email: "screener-001@sentryagent.ai"
|
||||
agentType: "screener"
|
||||
version: "1.5.0"
|
||||
capabilities:
|
||||
- "resume:read"
|
||||
- "email:send"
|
||||
- "candidate:score"
|
||||
- "report:write"
|
||||
owner: "talent-acquisition-team"
|
||||
deploymentEnv: "production"
|
||||
status: "active"
|
||||
createdAt: "2026-03-01T08:00:00.000Z"
|
||||
updatedAt: "2026-03-28T11:30:00.000Z"
|
||||
'400':
|
||||
description: Invalid request body or attempt to modify an immutable field.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
validationError:
|
||||
summary: Validation failure
|
||||
value:
|
||||
code: "VALIDATION_ERROR"
|
||||
message: "Request validation failed."
|
||||
details:
|
||||
field: "version"
|
||||
reason: "Must be a valid semantic version string."
|
||||
immutableField:
|
||||
summary: Attempt to modify immutable field
|
||||
value:
|
||||
code: "IMMUTABLE_FIELD"
|
||||
message: "The field 'email' cannot be modified after registration."
|
||||
details:
|
||||
field: "email"
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
description: Forbidden. Insufficient permissions or agent is decommissioned.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
forbidden:
|
||||
summary: Insufficient permissions
|
||||
value:
|
||||
code: "FORBIDDEN"
|
||||
message: "You do not have permission to update this agent."
|
||||
decommissioned:
|
||||
summary: Agent is decommissioned
|
||||
value:
|
||||
code: "AGENT_DECOMMISSIONED"
|
||||
message: "Decommissioned agents cannot be updated."
|
||||
details:
|
||||
agentId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'429':
|
||||
$ref: '#/components/responses/TooManyRequests'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
delete:
|
||||
operationId: deactivateAgent
|
||||
tags:
|
||||
- Agent Registry
|
||||
summary: Deactivate (soft-delete) an agent
|
||||
description: |
|
||||
Permanently decommissions an AI agent. This is a **soft delete** — the
|
||||
agent record is retained in the database for audit purposes, but the
|
||||
agent's status is set to `decommissioned`.
|
||||
|
||||
**Effects of decommissioning**:
|
||||
- All active credentials for this agent are immediately revoked.
|
||||
- The agent can no longer authenticate or obtain tokens.
|
||||
- The agent record remains visible in the registry with status `decommissioned`.
|
||||
- This operation is **irreversible**.
|
||||
responses:
|
||||
'204':
|
||||
description: Agent decommissioned successfully. No response body.
|
||||
headers:
|
||||
X-RateLimit-Limit:
|
||||
schema:
|
||||
type: integer
|
||||
example: 100
|
||||
X-RateLimit-Remaining:
|
||||
schema:
|
||||
type: integer
|
||||
example: 92
|
||||
X-RateLimit-Reset:
|
||||
schema:
|
||||
type: integer
|
||||
example: 1743155400
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'409':
|
||||
description: Agent is already decommissioned.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "AGENT_ALREADY_DECOMMISSIONED"
|
||||
message: "This agent has already been decommissioned."
|
||||
details:
|
||||
agentId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
'429':
|
||||
$ref: '#/components/responses/TooManyRequests'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
497
docs/openapi/audit-log.yaml
Normal file
497
docs/openapi/audit-log.yaml
Normal file
@@ -0,0 +1,497 @@
|
||||
openapi: 3.0.3
|
||||
|
||||
info:
|
||||
title: SentryAgent.ai — Audit Log Service
|
||||
version: 1.0.0
|
||||
description: |
|
||||
The Audit Log Service provides a queryable, immutable, compliance-ready
|
||||
event log of all significant actions performed by agents and administrators
|
||||
on the SentryAgent.ai AgentIdP platform.
|
||||
|
||||
**Immutability**: Audit events are written internally only — there are no
|
||||
API endpoints to create, modify, or delete audit records. The log is
|
||||
append-only by design.
|
||||
|
||||
**Automatic event capture**: The following actions are automatically logged:
|
||||
| Action | Description |
|
||||
|--------|-------------|
|
||||
| `agent.created` | A new agent was registered |
|
||||
| `agent.updated` | Agent metadata was modified |
|
||||
| `agent.decommissioned` | An agent was decommissioned |
|
||||
| `agent.suspended` | An agent was suspended |
|
||||
| `agent.reactivated` | A suspended agent was reactivated |
|
||||
| `token.issued` | An access token was issued |
|
||||
| `token.revoked` | An access token was revoked |
|
||||
| `token.introspected` | A token was introspected |
|
||||
| `credential.generated` | New credentials were generated |
|
||||
| `credential.rotated` | A credential was rotated |
|
||||
| `credential.revoked` | A credential was revoked |
|
||||
| `auth.failed` | An authentication attempt failed |
|
||||
|
||||
**Free Tier**: Audit log retention is 90 days.
|
||||
Events older than 90 days are automatically purged on the free tier.
|
||||
|
||||
**Required scope**: `audit:read`
|
||||
|
||||
servers:
|
||||
- url: http://localhost:3000/api/v1
|
||||
description: Local development server
|
||||
- url: https://api.sentryagent.ai/v1
|
||||
description: Production server
|
||||
|
||||
tags:
|
||||
- name: Audit Log
|
||||
description: Query immutable audit events for compliance and governance
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
JWT access token with `audit:read` scope, obtained via `POST /token`.
|
||||
Include as: `Authorization: Bearer <token>`
|
||||
|
||||
schemas:
|
||||
AuditAction:
|
||||
type: string
|
||||
description: The action that triggered the audit event.
|
||||
enum:
|
||||
- agent.created
|
||||
- agent.updated
|
||||
- agent.decommissioned
|
||||
- agent.suspended
|
||||
- agent.reactivated
|
||||
- token.issued
|
||||
- token.revoked
|
||||
- token.introspected
|
||||
- credential.generated
|
||||
- credential.rotated
|
||||
- credential.revoked
|
||||
- auth.failed
|
||||
example: token.issued
|
||||
|
||||
AuditOutcome:
|
||||
type: string
|
||||
description: Whether the action succeeded or failed.
|
||||
enum:
|
||||
- success
|
||||
- failure
|
||||
example: success
|
||||
|
||||
AuditEvent:
|
||||
type: object
|
||||
description: |
|
||||
An immutable audit event record representing a single significant action
|
||||
that occurred within the SentryAgent.ai platform.
|
||||
required:
|
||||
- eventId
|
||||
- agentId
|
||||
- action
|
||||
- outcome
|
||||
- ipAddress
|
||||
- userAgent
|
||||
- metadata
|
||||
- timestamp
|
||||
properties:
|
||||
eventId:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Immutable, system-assigned unique identifier for this audit event.
|
||||
readOnly: true
|
||||
example: "f1e2d3c4-b5a6-7890-cdef-123456789012"
|
||||
agentId:
|
||||
type: string
|
||||
format: uuid
|
||||
description: >
|
||||
The `agentId` of the agent that triggered this event. For system-generated
|
||||
events (e.g. automatic token expiry), this field refers to the affected agent.
|
||||
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
action:
|
||||
$ref: '#/components/schemas/AuditAction'
|
||||
outcome:
|
||||
$ref: '#/components/schemas/AuditOutcome'
|
||||
ipAddress:
|
||||
type: string
|
||||
description: >
|
||||
IP address of the client that initiated the request.
|
||||
IPv4 or IPv6 format. May be `0.0.0.0` for system-generated events.
|
||||
example: "203.0.113.42"
|
||||
userAgent:
|
||||
type: string
|
||||
description: >
|
||||
HTTP `User-Agent` header value from the originating request.
|
||||
May be `SentryAgent-System/1.0` for internally generated events.
|
||||
example: "SentryAgent-SDK/1.0.0 Node.js/18.19.0"
|
||||
metadata:
|
||||
type: object
|
||||
description: |
|
||||
Action-specific structured data providing additional context.
|
||||
Schema varies by `action`:
|
||||
- `token.issued`: includes `scope`, `expiresAt`
|
||||
- `credential.rotated`: includes `credentialId`
|
||||
- `agent.created`: includes `agentType`, `owner`
|
||||
- `auth.failed`: includes `reason`, `clientId`
|
||||
additionalProperties: true
|
||||
example:
|
||||
scope: "agents:read agents:write"
|
||||
expiresAt: "2026-03-28T10:00:00.000Z"
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
description: ISO 8601 timestamp when the event occurred.
|
||||
readOnly: true
|
||||
example: "2026-03-28T09:01:00.000Z"
|
||||
|
||||
PaginatedAuditEventsResponse:
|
||||
type: object
|
||||
description: Paginated list of audit events.
|
||||
required:
|
||||
- data
|
||||
- total
|
||||
- page
|
||||
- limit
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/AuditEvent'
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of audit events matching the query filters.
|
||||
example: 1423
|
||||
page:
|
||||
type: integer
|
||||
description: Current page number (1-based).
|
||||
example: 1
|
||||
limit:
|
||||
type: integer
|
||||
description: Number of items per page.
|
||||
example: 50
|
||||
|
||||
ErrorResponse:
|
||||
type: object
|
||||
description: Standard error response envelope.
|
||||
required:
|
||||
- code
|
||||
- message
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
description: Machine-readable error code.
|
||||
example: "AUDIT_EVENT_NOT_FOUND"
|
||||
message:
|
||||
type: string
|
||||
description: Human-readable description of the error.
|
||||
example: "Audit event with the specified ID was not found."
|
||||
details:
|
||||
type: object
|
||||
description: Optional structured details about the error.
|
||||
additionalProperties: true
|
||||
example: {}
|
||||
|
||||
responses:
|
||||
Unauthorized:
|
||||
description: Missing or invalid Bearer token.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "UNAUTHORIZED"
|
||||
message: "A valid Bearer token is required to access this resource."
|
||||
|
||||
Forbidden:
|
||||
description: Valid token but insufficient permissions. Requires `audit:read` scope.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "INSUFFICIENT_SCOPE"
|
||||
message: "The 'audit:read' scope is required to access audit logs."
|
||||
|
||||
NotFound:
|
||||
description: The requested audit event was not found.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "AUDIT_EVENT_NOT_FOUND"
|
||||
message: "Audit event with the specified ID was not found."
|
||||
|
||||
TooManyRequests:
|
||||
description: Rate limit exceeded.
|
||||
headers:
|
||||
X-RateLimit-Limit:
|
||||
schema:
|
||||
type: integer
|
||||
description: Maximum requests allowed per minute.
|
||||
example: 100
|
||||
X-RateLimit-Remaining:
|
||||
schema:
|
||||
type: integer
|
||||
description: Requests remaining in the current window.
|
||||
example: 0
|
||||
X-RateLimit-Reset:
|
||||
schema:
|
||||
type: integer
|
||||
description: Unix timestamp when the rate limit window resets.
|
||||
example: 1743155400
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "RATE_LIMIT_EXCEEDED"
|
||||
message: "Too many requests. Please retry after the rate limit window resets."
|
||||
|
||||
InternalServerError:
|
||||
description: Unexpected server error.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "INTERNAL_SERVER_ERROR"
|
||||
message: "An unexpected error occurred. Please try again later."
|
||||
|
||||
security:
|
||||
- BearerAuth: []
|
||||
|
||||
paths:
|
||||
/audit:
|
||||
get:
|
||||
operationId: queryAuditLog
|
||||
tags:
|
||||
- Audit Log
|
||||
summary: Query audit log
|
||||
description: |
|
||||
Returns a paginated, filtered list of audit events. Results are ordered
|
||||
by `timestamp` descending (most recent first).
|
||||
|
||||
**Requires**: Bearer token with `audit:read` scope.
|
||||
|
||||
**Retention**: On the free tier, only events from the last 90 days are
|
||||
accessible. Requests for older events will return an empty result set,
|
||||
not an error.
|
||||
|
||||
**Filtering**: Multiple filters can be combined (logical AND).
|
||||
All filter parameters are optional.
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
description: Page number (1-based). Defaults to `1`.
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
default: 1
|
||||
example: 1
|
||||
- name: limit
|
||||
in: query
|
||||
description: Number of results per page. Defaults to `50`, maximum `200`.
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 200
|
||||
default: 50
|
||||
example: 50
|
||||
- name: agentId
|
||||
in: query
|
||||
description: Filter events to those triggered by a specific agent (UUID).
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
- name: action
|
||||
in: query
|
||||
description: Filter events by action type.
|
||||
required: false
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuditAction'
|
||||
- name: outcome
|
||||
in: query
|
||||
description: Filter events by outcome.
|
||||
required: false
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuditOutcome'
|
||||
- name: fromDate
|
||||
in: query
|
||||
description: |
|
||||
Filter events at or after this ISO 8601 timestamp (inclusive).
|
||||
On free tier, cannot be older than 90 days from today.
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2026-03-01T00:00:00.000Z"
|
||||
- name: toDate
|
||||
in: query
|
||||
description: Filter events at or before this ISO 8601 timestamp (inclusive).
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2026-03-28T23:59:59.999Z"
|
||||
responses:
|
||||
'200':
|
||||
description: Audit events returned successfully.
|
||||
headers:
|
||||
X-RateLimit-Limit:
|
||||
schema:
|
||||
type: integer
|
||||
example: 100
|
||||
X-RateLimit-Remaining:
|
||||
schema:
|
||||
type: integer
|
||||
example: 95
|
||||
X-RateLimit-Reset:
|
||||
schema:
|
||||
type: integer
|
||||
example: 1743155400
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PaginatedAuditEventsResponse'
|
||||
example:
|
||||
data:
|
||||
- eventId: "f1e2d3c4-b5a6-7890-cdef-123456789012"
|
||||
agentId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
action: "token.issued"
|
||||
outcome: "success"
|
||||
ipAddress: "203.0.113.42"
|
||||
userAgent: "SentryAgent-SDK/1.0.0 Node.js/18.19.0"
|
||||
metadata:
|
||||
scope: "agents:read agents:write"
|
||||
expiresAt: "2026-03-28T10:01:00.000Z"
|
||||
timestamp: "2026-03-28T09:01:00.000Z"
|
||||
- eventId: "e2d3c4b5-a6f7-8901-bcde-f23456789013"
|
||||
agentId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
action: "credential.generated"
|
||||
outcome: "success"
|
||||
ipAddress: "203.0.113.42"
|
||||
userAgent: "SentryAgent-SDK/1.0.0 Node.js/18.19.0"
|
||||
metadata:
|
||||
credentialId: "c9d8e7f6-a5b4-3210-fedc-ba9876543210"
|
||||
timestamp: "2026-03-28T09:00:00.000Z"
|
||||
- eventId: "d3c4b5a6-f7e8-9012-cdef-345678901234"
|
||||
agentId: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
|
||||
action: "auth.failed"
|
||||
outcome: "failure"
|
||||
ipAddress: "198.51.100.17"
|
||||
userAgent: "python-requests/2.31.0"
|
||||
metadata:
|
||||
reason: "invalid_client_secret"
|
||||
clientId: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
|
||||
timestamp: "2026-03-28T08:45:00.000Z"
|
||||
total: 1423
|
||||
page: 1
|
||||
limit: 50
|
||||
'400':
|
||||
description: Invalid query parameters.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
invalidDate:
|
||||
summary: Invalid date format
|
||||
value:
|
||||
code: "VALIDATION_ERROR"
|
||||
message: "Invalid query parameter value."
|
||||
details:
|
||||
field: "fromDate"
|
||||
reason: "Must be a valid ISO 8601 date-time string."
|
||||
invalidDateRange:
|
||||
summary: fromDate is after toDate
|
||||
value:
|
||||
code: "VALIDATION_ERROR"
|
||||
message: "Invalid date range."
|
||||
details:
|
||||
reason: "fromDate must be before or equal to toDate."
|
||||
retentionExceeded:
|
||||
summary: Requested date is outside retention window
|
||||
value:
|
||||
code: "RETENTION_WINDOW_EXCEEDED"
|
||||
message: "Free tier audit log retention is 90 days. Requested date is outside the retention window."
|
||||
details:
|
||||
retentionDays: 90
|
||||
earliestAvailable: "2025-12-28T00:00:00.000Z"
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'429':
|
||||
$ref: '#/components/responses/TooManyRequests'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/audit/{eventId}:
|
||||
parameters:
|
||||
- name: eventId
|
||||
in: path
|
||||
description: The unique UUID identifier of the audit event.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "f1e2d3c4-b5a6-7890-cdef-123456789012"
|
||||
|
||||
get:
|
||||
operationId: getAuditEventById
|
||||
tags:
|
||||
- Audit Log
|
||||
summary: Get a single audit event by ID
|
||||
description: |
|
||||
Retrieves a single, immutable audit event by its unique `eventId`.
|
||||
|
||||
**Requires**: Bearer token with `audit:read` scope.
|
||||
|
||||
**Retention**: Free tier events older than 90 days are not accessible
|
||||
and will return `404 Not Found`.
|
||||
responses:
|
||||
'200':
|
||||
description: Audit event returned successfully.
|
||||
headers:
|
||||
X-RateLimit-Limit:
|
||||
schema:
|
||||
type: integer
|
||||
example: 100
|
||||
X-RateLimit-Remaining:
|
||||
schema:
|
||||
type: integer
|
||||
example: 94
|
||||
X-RateLimit-Reset:
|
||||
schema:
|
||||
type: integer
|
||||
example: 1743155400
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuditEvent'
|
||||
example:
|
||||
eventId: "f1e2d3c4-b5a6-7890-cdef-123456789012"
|
||||
agentId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
action: "token.issued"
|
||||
outcome: "success"
|
||||
ipAddress: "203.0.113.42"
|
||||
userAgent: "SentryAgent-SDK/1.0.0 Node.js/18.19.0"
|
||||
metadata:
|
||||
scope: "agents:read agents:write"
|
||||
expiresAt: "2026-03-28T10:01:00.000Z"
|
||||
timestamp: "2026-03-28T09:01:00.000Z"
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'429':
|
||||
$ref: '#/components/responses/TooManyRequests'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
687
docs/openapi/credential-management.yaml
Normal file
687
docs/openapi/credential-management.yaml
Normal file
@@ -0,0 +1,687 @@
|
||||
openapi: 3.0.3
|
||||
|
||||
info:
|
||||
title: SentryAgent.ai — Credential Management Service
|
||||
version: 1.0.0
|
||||
description: |
|
||||
The Credential Management Service provides secure generation, listing,
|
||||
rotation, and revocation of OAuth 2.0 client credentials for registered
|
||||
AI agents.
|
||||
|
||||
Each agent can hold multiple credentials simultaneously to support
|
||||
zero-downtime rotation. A credential consists of a `client_id` (= `agentId`)
|
||||
and a `client_secret`.
|
||||
|
||||
**Security model**:
|
||||
- `client_secret` is returned **once only** — at creation or rotation time.
|
||||
- Secrets are stored as a bcrypt hash; plain-text is never persisted.
|
||||
- An agent may only manage its own credentials unless the caller holds an
|
||||
admin-scoped token.
|
||||
- Rotating a credential immediately revokes the previous `client_secret`.
|
||||
|
||||
**Auth**: Bearer token (JWT) required on all endpoints.
|
||||
|
||||
servers:
|
||||
- url: http://localhost:3000/api/v1
|
||||
description: Local development server
|
||||
- url: https://api.sentryagent.ai/v1
|
||||
description: Production server
|
||||
|
||||
tags:
|
||||
- name: Credential Management
|
||||
description: Generate, list, rotate, and revoke agent credentials
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
JWT access token obtained via `POST /token`.
|
||||
Include as: `Authorization: Bearer <token>`
|
||||
|
||||
schemas:
|
||||
CredentialStatus:
|
||||
type: string
|
||||
description: |
|
||||
Lifecycle status of a credential.
|
||||
- `active`: Credential is valid and can be used to obtain tokens.
|
||||
- `revoked`: Credential has been explicitly revoked and is permanently invalid.
|
||||
enum:
|
||||
- active
|
||||
- revoked
|
||||
example: active
|
||||
|
||||
Credential:
|
||||
type: object
|
||||
description: |
|
||||
A credential record for an AI agent. The `clientSecret` is **never**
|
||||
returned in this schema — it is only returned once in `CredentialWithSecret`
|
||||
at the moment of creation or rotation.
|
||||
required:
|
||||
- credentialId
|
||||
- clientId
|
||||
- status
|
||||
- createdAt
|
||||
properties:
|
||||
credentialId:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Immutable, system-assigned unique identifier for this credential.
|
||||
readOnly: true
|
||||
example: "c9d8e7f6-a5b4-3210-fedc-ba9876543210"
|
||||
clientId:
|
||||
type: string
|
||||
format: uuid
|
||||
description: >
|
||||
The `agentId` this credential belongs to. Equal to the `agentId`
|
||||
path parameter.
|
||||
readOnly: true
|
||||
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
status:
|
||||
$ref: '#/components/schemas/CredentialStatus'
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: ISO 8601 timestamp when the credential was created.
|
||||
readOnly: true
|
||||
example: "2026-03-28T09:00:00.000Z"
|
||||
expiresAt:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
description: |
|
||||
ISO 8601 timestamp when the credential expires.
|
||||
`null` indicates the credential does not expire (valid until revoked).
|
||||
example: "2027-03-28T09:00:00.000Z"
|
||||
revokedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
description: >
|
||||
ISO 8601 timestamp when the credential was revoked.
|
||||
`null` if the credential has not been revoked.
|
||||
readOnly: true
|
||||
example: null
|
||||
|
||||
CredentialWithSecret:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Credential'
|
||||
- type: object
|
||||
description: |
|
||||
Extended credential record returned **only** at creation or rotation time.
|
||||
The `clientSecret` is shown once and never retrievable again.
|
||||
Store it securely immediately.
|
||||
required:
|
||||
- clientSecret
|
||||
properties:
|
||||
clientSecret:
|
||||
type: string
|
||||
description: |
|
||||
The plain-text client secret. **Shown once only** — store securely immediately.
|
||||
This value is not persisted in plain text on the server.
|
||||
format: password
|
||||
example: "sk_live_7f3a2b1c9d8e4f0a6b5c3d2e1f0a9b8c"
|
||||
|
||||
GenerateCredentialRequest:
|
||||
type: object
|
||||
description: |
|
||||
Optional request body for generating new credentials.
|
||||
If `expiresAt` is omitted, the credential does not expire.
|
||||
properties:
|
||||
expiresAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: |
|
||||
Optional ISO 8601 expiry timestamp for the credential.
|
||||
Must be a future date. If omitted, the credential has no expiry.
|
||||
example: "2027-03-28T09:00:00.000Z"
|
||||
|
||||
PaginatedCredentialsResponse:
|
||||
type: object
|
||||
description: Paginated list of credentials for an agent.
|
||||
required:
|
||||
- data
|
||||
- total
|
||||
- page
|
||||
- limit
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Credential'
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of credentials for this agent.
|
||||
example: 3
|
||||
page:
|
||||
type: integer
|
||||
description: Current page number (1-based).
|
||||
example: 1
|
||||
limit:
|
||||
type: integer
|
||||
description: Number of items per page.
|
||||
example: 20
|
||||
|
||||
ErrorResponse:
|
||||
type: object
|
||||
description: Standard error response envelope.
|
||||
required:
|
||||
- code
|
||||
- message
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
description: Machine-readable error code.
|
||||
example: "CREDENTIAL_NOT_FOUND"
|
||||
message:
|
||||
type: string
|
||||
description: Human-readable description of the error.
|
||||
example: "Credential with the specified ID was not found."
|
||||
details:
|
||||
type: object
|
||||
description: Optional structured details about the error.
|
||||
additionalProperties: true
|
||||
example: {}
|
||||
|
||||
responses:
|
||||
Unauthorized:
|
||||
description: Missing or invalid Bearer token.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "UNAUTHORIZED"
|
||||
message: "A valid Bearer token is required to access this resource."
|
||||
|
||||
Forbidden:
|
||||
description: Valid token but insufficient permissions.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "FORBIDDEN"
|
||||
message: "You do not have permission to manage credentials for this agent."
|
||||
|
||||
AgentNotFound:
|
||||
description: The specified agent does not exist.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "AGENT_NOT_FOUND"
|
||||
message: "Agent with the specified ID was not found."
|
||||
|
||||
CredentialNotFound:
|
||||
description: The specified credential does not exist.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "CREDENTIAL_NOT_FOUND"
|
||||
message: "Credential with the specified ID was not found."
|
||||
|
||||
TooManyRequests:
|
||||
description: Rate limit exceeded.
|
||||
headers:
|
||||
X-RateLimit-Limit:
|
||||
schema:
|
||||
type: integer
|
||||
example: 100
|
||||
X-RateLimit-Remaining:
|
||||
schema:
|
||||
type: integer
|
||||
example: 0
|
||||
X-RateLimit-Reset:
|
||||
schema:
|
||||
type: integer
|
||||
example: 1743155400
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "RATE_LIMIT_EXCEEDED"
|
||||
message: "Too many requests. Please retry after the rate limit window resets."
|
||||
|
||||
InternalServerError:
|
||||
description: Unexpected server error.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "INTERNAL_SERVER_ERROR"
|
||||
message: "An unexpected error occurred. Please try again later."
|
||||
|
||||
security:
|
||||
- BearerAuth: []
|
||||
|
||||
paths:
|
||||
/agents/{agentId}/credentials:
|
||||
parameters:
|
||||
- name: agentId
|
||||
in: path
|
||||
description: The unique UUID identifier of the agent.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
|
||||
post:
|
||||
operationId: generateCredential
|
||||
tags:
|
||||
- Credential Management
|
||||
summary: Generate new credentials for an agent
|
||||
description: |
|
||||
Generates a new `client_id` + `client_secret` credential pair for the
|
||||
specified agent.
|
||||
|
||||
**Important**: The `clientSecret` is returned **once only** in this response.
|
||||
It is not stored in plain text on the server and cannot be retrieved later.
|
||||
Store it securely immediately (e.g. in a secrets manager).
|
||||
|
||||
An agent may hold multiple active credentials simultaneously. This supports
|
||||
zero-downtime rotation: generate a new credential, update all consumers,
|
||||
then revoke the old one.
|
||||
|
||||
**Restrictions**:
|
||||
- The agent must be in `active` status.
|
||||
- An agent may manage its own credentials via a self-issued token.
|
||||
- Managing another agent's credentials requires an admin-scoped token.
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenerateCredentialRequest'
|
||||
example:
|
||||
expiresAt: "2027-03-28T09:00:00.000Z"
|
||||
responses:
|
||||
'201':
|
||||
description: |
|
||||
Credential generated successfully.
|
||||
**Save the `clientSecret` immediately — it will not be shown again.**
|
||||
headers:
|
||||
X-RateLimit-Limit:
|
||||
schema:
|
||||
type: integer
|
||||
example: 100
|
||||
X-RateLimit-Remaining:
|
||||
schema:
|
||||
type: integer
|
||||
example: 99
|
||||
X-RateLimit-Reset:
|
||||
schema:
|
||||
type: integer
|
||||
example: 1743155400
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CredentialWithSecret'
|
||||
example:
|
||||
credentialId: "c9d8e7f6-a5b4-3210-fedc-ba9876543210"
|
||||
clientId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
clientSecret: "sk_live_7f3a2b1c9d8e4f0a6b5c3d2e1f0a9b8c"
|
||||
status: "active"
|
||||
createdAt: "2026-03-28T09:00:00.000Z"
|
||||
expiresAt: "2027-03-28T09:00:00.000Z"
|
||||
revokedAt: null
|
||||
'400':
|
||||
description: Invalid request body.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "VALIDATION_ERROR"
|
||||
message: "Request validation failed."
|
||||
details:
|
||||
field: "expiresAt"
|
||||
reason: "expiresAt must be a future date-time."
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
description: Insufficient permissions or agent is not active.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
forbidden:
|
||||
summary: Insufficient permissions
|
||||
value:
|
||||
code: "FORBIDDEN"
|
||||
message: "You do not have permission to manage credentials for this agent."
|
||||
agentNotActive:
|
||||
summary: Agent not in active status
|
||||
value:
|
||||
code: "AGENT_NOT_ACTIVE"
|
||||
message: "Credentials can only be generated for active agents."
|
||||
details:
|
||||
agentId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
status: "suspended"
|
||||
'404':
|
||||
$ref: '#/components/responses/AgentNotFound'
|
||||
'429':
|
||||
$ref: '#/components/responses/TooManyRequests'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
get:
|
||||
operationId: listCredentials
|
||||
tags:
|
||||
- Credential Management
|
||||
summary: List credentials for an agent
|
||||
description: |
|
||||
Returns a paginated list of all credentials (active and revoked) for the
|
||||
specified agent. The `clientSecret` is **never** returned in list responses.
|
||||
|
||||
Results are ordered by `createdAt` descending (most recent first).
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
description: Page number (1-based). Defaults to `1`.
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
default: 1
|
||||
example: 1
|
||||
- name: limit
|
||||
in: query
|
||||
description: Number of results per page. Defaults to `20`, maximum `100`.
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
default: 20
|
||||
example: 20
|
||||
- name: status
|
||||
in: query
|
||||
description: Filter credentials by status.
|
||||
required: false
|
||||
schema:
|
||||
$ref: '#/components/schemas/CredentialStatus'
|
||||
responses:
|
||||
'200':
|
||||
description: Credential list returned successfully.
|
||||
headers:
|
||||
X-RateLimit-Limit:
|
||||
schema:
|
||||
type: integer
|
||||
example: 100
|
||||
X-RateLimit-Remaining:
|
||||
schema:
|
||||
type: integer
|
||||
example: 98
|
||||
X-RateLimit-Reset:
|
||||
schema:
|
||||
type: integer
|
||||
example: 1743155400
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PaginatedCredentialsResponse'
|
||||
example:
|
||||
data:
|
||||
- credentialId: "c9d8e7f6-a5b4-3210-fedc-ba9876543210"
|
||||
clientId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
status: "active"
|
||||
createdAt: "2026-03-28T09:00:00.000Z"
|
||||
expiresAt: "2027-03-28T09:00:00.000Z"
|
||||
revokedAt: null
|
||||
- credentialId: "d8e7f6a5-b4c3-2109-edcb-a98765432109"
|
||||
clientId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
status: "revoked"
|
||||
createdAt: "2026-01-15T08:00:00.000Z"
|
||||
expiresAt: null
|
||||
revokedAt: "2026-03-28T08:59:00.000Z"
|
||||
total: 2
|
||||
page: 1
|
||||
limit: 20
|
||||
'400':
|
||||
description: Invalid query parameters.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "VALIDATION_ERROR"
|
||||
message: "Invalid query parameter value."
|
||||
details:
|
||||
field: "status"
|
||||
reason: "Must be 'active' or 'revoked'."
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/components/responses/AgentNotFound'
|
||||
'429':
|
||||
$ref: '#/components/responses/TooManyRequests'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/agents/{agentId}/credentials/{credentialId}/rotate:
|
||||
parameters:
|
||||
- name: agentId
|
||||
in: path
|
||||
description: The unique UUID identifier of the agent.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
- name: credentialId
|
||||
in: path
|
||||
description: The unique UUID identifier of the credential to rotate.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "c9d8e7f6-a5b4-3210-fedc-ba9876543210"
|
||||
|
||||
post:
|
||||
operationId: rotateCredential
|
||||
tags:
|
||||
- Credential Management
|
||||
summary: Rotate a credential
|
||||
description: |
|
||||
Rotates an existing credential by:
|
||||
1. Immediately revoking the current `client_secret`.
|
||||
2. Generating and returning a new `client_secret` for the same `credentialId`.
|
||||
|
||||
The `credentialId` remains the same after rotation; only the secret changes.
|
||||
The new `clientSecret` is returned **once only** and must be stored securely.
|
||||
|
||||
**Use case**: Periodic secret rotation or emergency rotation after
|
||||
credential compromise.
|
||||
|
||||
Only `active` credentials can be rotated. Attempting to rotate a `revoked`
|
||||
credential returns `409 Conflict`.
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenerateCredentialRequest'
|
||||
example:
|
||||
expiresAt: "2028-03-28T09:00:00.000Z"
|
||||
responses:
|
||||
'200':
|
||||
description: |
|
||||
Credential rotated successfully.
|
||||
**Save the new `clientSecret` immediately — it will not be shown again.**
|
||||
The previous secret is permanently invalidated.
|
||||
headers:
|
||||
X-RateLimit-Limit:
|
||||
schema:
|
||||
type: integer
|
||||
example: 100
|
||||
X-RateLimit-Remaining:
|
||||
schema:
|
||||
type: integer
|
||||
example: 97
|
||||
X-RateLimit-Reset:
|
||||
schema:
|
||||
type: integer
|
||||
example: 1743155400
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CredentialWithSecret'
|
||||
example:
|
||||
credentialId: "c9d8e7f6-a5b4-3210-fedc-ba9876543210"
|
||||
clientId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
clientSecret: "sk_live_9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d"
|
||||
status: "active"
|
||||
createdAt: "2026-03-28T09:00:00.000Z"
|
||||
expiresAt: "2028-03-28T09:00:00.000Z"
|
||||
revokedAt: null
|
||||
'400':
|
||||
description: Invalid request body.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "VALIDATION_ERROR"
|
||||
message: "Request validation failed."
|
||||
details:
|
||||
field: "expiresAt"
|
||||
reason: "expiresAt must be a future date-time."
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'404':
|
||||
description: Agent or credential not found.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
agentNotFound:
|
||||
summary: Agent not found
|
||||
value:
|
||||
code: "AGENT_NOT_FOUND"
|
||||
message: "Agent with the specified ID was not found."
|
||||
credentialNotFound:
|
||||
summary: Credential not found
|
||||
value:
|
||||
code: "CREDENTIAL_NOT_FOUND"
|
||||
message: "Credential with the specified ID was not found."
|
||||
'409':
|
||||
description: Credential is already revoked and cannot be rotated.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "CREDENTIAL_ALREADY_REVOKED"
|
||||
message: "Revoked credentials cannot be rotated. Generate a new credential instead."
|
||||
details:
|
||||
credentialId: "c9d8e7f6-a5b4-3210-fedc-ba9876543210"
|
||||
revokedAt: "2026-03-20T10:00:00.000Z"
|
||||
'429':
|
||||
$ref: '#/components/responses/TooManyRequests'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/agents/{agentId}/credentials/{credentialId}:
|
||||
parameters:
|
||||
- name: agentId
|
||||
in: path
|
||||
description: The unique UUID identifier of the agent.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
- name: credentialId
|
||||
in: path
|
||||
description: The unique UUID identifier of the credential to revoke.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "c9d8e7f6-a5b4-3210-fedc-ba9876543210"
|
||||
|
||||
delete:
|
||||
operationId: revokeCredential
|
||||
tags:
|
||||
- Credential Management
|
||||
summary: Revoke a credential
|
||||
description: |
|
||||
Permanently revokes a credential, immediately preventing it from being
|
||||
used to obtain new tokens.
|
||||
|
||||
**Effects of revocation**:
|
||||
- The credential's status is set to `revoked`.
|
||||
- Any tokens issued using this credential remain valid until they expire
|
||||
naturally (token revocation is handled separately via `POST /token/revoke`).
|
||||
- The credential record is retained for audit purposes.
|
||||
- This operation is **irreversible** — a revoked credential cannot be re-activated.
|
||||
|
||||
Revoking an already-revoked credential returns `409 Conflict`.
|
||||
responses:
|
||||
'204':
|
||||
description: Credential revoked successfully. No response body.
|
||||
headers:
|
||||
X-RateLimit-Limit:
|
||||
schema:
|
||||
type: integer
|
||||
example: 100
|
||||
X-RateLimit-Remaining:
|
||||
schema:
|
||||
type: integer
|
||||
example: 96
|
||||
X-RateLimit-Reset:
|
||||
schema:
|
||||
type: integer
|
||||
example: 1743155400
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'404':
|
||||
description: Agent or credential not found.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
agentNotFound:
|
||||
summary: Agent not found
|
||||
value:
|
||||
code: "AGENT_NOT_FOUND"
|
||||
message: "Agent with the specified ID was not found."
|
||||
credentialNotFound:
|
||||
summary: Credential not found
|
||||
value:
|
||||
code: "CREDENTIAL_NOT_FOUND"
|
||||
message: "Credential with the specified ID was not found."
|
||||
'409':
|
||||
description: Credential is already revoked.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "CREDENTIAL_ALREADY_REVOKED"
|
||||
message: "This credential has already been revoked."
|
||||
details:
|
||||
credentialId: "c9d8e7f6-a5b4-3210-fedc-ba9876543210"
|
||||
revokedAt: "2026-03-20T10:00:00.000Z"
|
||||
'429':
|
||||
$ref: '#/components/responses/TooManyRequests'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
586
docs/openapi/oauth2-token.yaml
Normal file
586
docs/openapi/oauth2-token.yaml
Normal file
@@ -0,0 +1,586 @@
|
||||
openapi: 3.0.3
|
||||
|
||||
info:
|
||||
title: SentryAgent.ai — OAuth 2.0 Token Service
|
||||
version: 1.0.0
|
||||
description: |
|
||||
The OAuth 2.0 Token Service provides agent authentication via the
|
||||
**Client Credentials grant** (RFC 6749 Section 4.4). It issues signed JWT
|
||||
access tokens, supports token introspection (RFC 7662), and token revocation
|
||||
(RFC 7009).
|
||||
|
||||
Agents authenticate using their `client_id` (= `agentId`) and
|
||||
`client_secret` obtained during credential provisioning.
|
||||
|
||||
**Supported Grant Type**: `client_credentials` only. All other grant types
|
||||
are rejected with `unsupported_grant_type`.
|
||||
|
||||
**Token Lifetime**: 3600 seconds (1 hour) by default.
|
||||
|
||||
**Scopes**:
|
||||
| Scope | Description |
|
||||
|-------|-------------|
|
||||
| `agents:read` | Read agent identity records |
|
||||
| `agents:write` | Create, update, and deactivate agent records |
|
||||
| `tokens:read` | Introspect tokens |
|
||||
| `audit:read` | Query the audit log |
|
||||
|
||||
**Rate Limit**: 100 requests/minute per `client_id`.
|
||||
|
||||
**Free Tier**: 10,000 token requests per month.
|
||||
|
||||
servers:
|
||||
- url: http://localhost:3000/api/v1
|
||||
description: Local development server
|
||||
- url: https://api.sentryagent.ai/v1
|
||||
description: Production server
|
||||
|
||||
tags:
|
||||
- name: OAuth 2.0 Tokens
|
||||
description: Token issuance, introspection, and revocation
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
JWT access token obtained via `POST /token`.
|
||||
Required for `/token/introspect` and `/token/revoke`.
|
||||
|
||||
BasicAuth:
|
||||
type: http
|
||||
scheme: basic
|
||||
description: |
|
||||
HTTP Basic authentication using `client_id` as the username and
|
||||
`client_secret` as the password. Used as an alternative credential
|
||||
method for token endpoint requests (in addition to request body).
|
||||
|
||||
schemas:
|
||||
GrantType:
|
||||
type: string
|
||||
description: OAuth 2.0 grant type. Only `client_credentials` is supported.
|
||||
enum:
|
||||
- client_credentials
|
||||
example: client_credentials
|
||||
|
||||
Scope:
|
||||
type: string
|
||||
description: |
|
||||
Space-separated list of requested OAuth 2.0 scopes.
|
||||
Available scopes: `agents:read`, `agents:write`, `tokens:read`, `audit:read`.
|
||||
pattern: '^(agents:read|agents:write|tokens:read|audit:read)(\s(agents:read|agents:write|tokens:read|audit:read))*$'
|
||||
example: "agents:read agents:write"
|
||||
|
||||
TokenRequest:
|
||||
type: object
|
||||
description: |
|
||||
OAuth 2.0 Client Credentials token request body.
|
||||
Credentials may be provided in the request body (as `client_id` +
|
||||
`client_secret`) or via HTTP Basic authentication header.
|
||||
required:
|
||||
- grant_type
|
||||
properties:
|
||||
grant_type:
|
||||
$ref: '#/components/schemas/GrantType'
|
||||
client_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: >
|
||||
The agent's `agentId` (UUID). Required if not using HTTP Basic auth.
|
||||
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
client_secret:
|
||||
type: string
|
||||
description: >
|
||||
The agent's client secret. Required if not using HTTP Basic auth.
|
||||
Treated as a sensitive value — never logged or stored in plain text.
|
||||
format: password
|
||||
example: "sk_live_7f3a2b1c9d8e4f0a6b5c3d2e1f0a9b8c"
|
||||
scope:
|
||||
$ref: '#/components/schemas/Scope'
|
||||
|
||||
TokenResponse:
|
||||
type: object
|
||||
description: Successful OAuth 2.0 token response.
|
||||
required:
|
||||
- access_token
|
||||
- token_type
|
||||
- expires_in
|
||||
- scope
|
||||
properties:
|
||||
access_token:
|
||||
type: string
|
||||
description: >
|
||||
Signed JWT access token. Include this value in the `Authorization`
|
||||
header as `Bearer <access_token>` when calling other API endpoints.
|
||||
example: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAiLCJjbGllbnRfaWQiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAiLCJzY29wZSI6ImFnZW50czpyZWFkIGFnZW50czp3cml0ZSIsImlhdCI6MTc0MzE1MTIwMCwiZXhwIjoxNzQzMTU0ODAwfQ.signature"
|
||||
token_type:
|
||||
type: string
|
||||
description: Token type. Always `Bearer`.
|
||||
enum:
|
||||
- Bearer
|
||||
example: "Bearer"
|
||||
expires_in:
|
||||
type: integer
|
||||
description: Token lifetime in seconds from the time of issuance. Default is `3600`.
|
||||
example: 3600
|
||||
scope:
|
||||
type: string
|
||||
description: Space-separated list of scopes granted by this token.
|
||||
example: "agents:read agents:write"
|
||||
|
||||
OAuth2ErrorResponse:
|
||||
type: object
|
||||
description: |
|
||||
OAuth 2.0 error response as defined in RFC 6749 Section 5.2.
|
||||
Used exclusively for token endpoint errors.
|
||||
required:
|
||||
- error
|
||||
- error_description
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: >
|
||||
Machine-readable OAuth 2.0 error code.
|
||||
enum:
|
||||
- invalid_request
|
||||
- invalid_client
|
||||
- invalid_grant
|
||||
- unauthorized_client
|
||||
- unsupported_grant_type
|
||||
- invalid_scope
|
||||
example: "invalid_client"
|
||||
error_description:
|
||||
type: string
|
||||
description: Human-readable description of the error.
|
||||
example: "Client authentication failed. Invalid client_id or client_secret."
|
||||
|
||||
IntrospectRequest:
|
||||
type: object
|
||||
description: Token introspection request (RFC 7662).
|
||||
required:
|
||||
- token
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
description: The token to introspect.
|
||||
example: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAifQ.signature"
|
||||
token_type_hint:
|
||||
type: string
|
||||
description: >
|
||||
Optional hint about the type of token being introspected.
|
||||
Currently only `access_token` is supported.
|
||||
enum:
|
||||
- access_token
|
||||
example: "access_token"
|
||||
|
||||
IntrospectResponse:
|
||||
type: object
|
||||
description: |
|
||||
Token introspection response (RFC 7662).
|
||||
When `active` is `false`, no other fields are guaranteed to be present.
|
||||
required:
|
||||
- active
|
||||
properties:
|
||||
active:
|
||||
type: boolean
|
||||
description: >
|
||||
Whether the token is currently active (valid, not expired, not revoked).
|
||||
example: true
|
||||
sub:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Subject — the `agentId` the token was issued for.
|
||||
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
client_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: The `client_id` (agentId) that requested the token.
|
||||
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
scope:
|
||||
type: string
|
||||
description: Space-separated list of scopes granted by this token.
|
||||
example: "agents:read agents:write"
|
||||
token_type:
|
||||
type: string
|
||||
description: Token type. Always `Bearer` for active tokens.
|
||||
example: "Bearer"
|
||||
iat:
|
||||
type: integer
|
||||
description: Unix timestamp (seconds) when the token was issued.
|
||||
example: 1743151200
|
||||
exp:
|
||||
type: integer
|
||||
description: Unix timestamp (seconds) when the token expires.
|
||||
example: 1743154800
|
||||
|
||||
RevokeRequest:
|
||||
type: object
|
||||
description: Token revocation request (RFC 7009).
|
||||
required:
|
||||
- token
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
description: The token to revoke.
|
||||
example: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAifQ.signature"
|
||||
token_type_hint:
|
||||
type: string
|
||||
description: Optional hint about the token type.
|
||||
enum:
|
||||
- access_token
|
||||
example: "access_token"
|
||||
|
||||
ErrorResponse:
|
||||
type: object
|
||||
description: Standard error response envelope used across all SentryAgent.ai APIs.
|
||||
required:
|
||||
- code
|
||||
- message
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
description: Machine-readable error code.
|
||||
example: "UNAUTHORIZED"
|
||||
message:
|
||||
type: string
|
||||
description: Human-readable description of the error.
|
||||
example: "A valid Bearer token is required."
|
||||
details:
|
||||
type: object
|
||||
description: Optional structured details providing additional context.
|
||||
additionalProperties: true
|
||||
example: {}
|
||||
|
||||
responses:
|
||||
Unauthorized:
|
||||
description: Missing or invalid Bearer token.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "UNAUTHORIZED"
|
||||
message: "A valid Bearer token is required to access this resource."
|
||||
|
||||
TooManyRequests:
|
||||
description: Rate limit exceeded. Retry after the reset time.
|
||||
headers:
|
||||
X-RateLimit-Limit:
|
||||
schema:
|
||||
type: integer
|
||||
description: Maximum requests allowed per minute.
|
||||
example: 100
|
||||
X-RateLimit-Remaining:
|
||||
schema:
|
||||
type: integer
|
||||
description: Requests remaining in the current window.
|
||||
example: 0
|
||||
X-RateLimit-Reset:
|
||||
schema:
|
||||
type: integer
|
||||
description: Unix timestamp when the rate limit window resets.
|
||||
example: 1743155400
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "RATE_LIMIT_EXCEEDED"
|
||||
message: "Too many requests. Please retry after the rate limit window resets."
|
||||
|
||||
InternalServerError:
|
||||
description: Unexpected server error.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "INTERNAL_SERVER_ERROR"
|
||||
message: "An unexpected error occurred. Please try again later."
|
||||
|
||||
paths:
|
||||
/token:
|
||||
post:
|
||||
operationId: issueToken
|
||||
tags:
|
||||
- OAuth 2.0 Tokens
|
||||
summary: Issue an access token (Client Credentials)
|
||||
description: |
|
||||
Issues a signed JWT access token for an agent using the OAuth 2.0
|
||||
**Client Credentials grant** (RFC 6749 §4.4).
|
||||
|
||||
The agent authenticates by providing its `client_id` (agentId) and
|
||||
`client_secret`. Credentials may be passed either:
|
||||
- In the **request body** (`client_id` + `client_secret` fields), or
|
||||
- Via **HTTP Basic authentication** header (username = `client_id`, password = `client_secret`).
|
||||
|
||||
The token is a signed JWT containing the agent's identity claims.
|
||||
Use it as a `Bearer` token on subsequent API calls.
|
||||
|
||||
**Free Tier Limit**: 10,000 token requests per month. Exceeding this
|
||||
returns `403` with `FREE_TIER_LIMIT_EXCEEDED`.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TokenRequest'
|
||||
example:
|
||||
grant_type: client_credentials
|
||||
client_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
client_secret: "sk_live_7f3a2b1c9d8e4f0a6b5c3d2e1f0a9b8c"
|
||||
scope: "agents:read agents:write"
|
||||
responses:
|
||||
'200':
|
||||
description: Access token issued successfully.
|
||||
headers:
|
||||
X-RateLimit-Limit:
|
||||
schema:
|
||||
type: integer
|
||||
example: 100
|
||||
X-RateLimit-Remaining:
|
||||
schema:
|
||||
type: integer
|
||||
example: 99
|
||||
X-RateLimit-Reset:
|
||||
schema:
|
||||
type: integer
|
||||
example: 1743155400
|
||||
Cache-Control:
|
||||
schema:
|
||||
type: string
|
||||
description: Token responses must not be cached.
|
||||
example: "no-store"
|
||||
Pragma:
|
||||
schema:
|
||||
type: string
|
||||
example: "no-cache"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TokenResponse'
|
||||
example:
|
||||
access_token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAiLCJjbGllbnRfaWQiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAiLCJzY29wZSI6ImFnZW50czpyZWFkIGFnZW50czp3cml0ZSIsImlhdCI6MTc0MzE1MTIwMCwiZXhwIjoxNzQzMTU0ODAwfQ.signature"
|
||||
token_type: "Bearer"
|
||||
expires_in: 3600
|
||||
scope: "agents:read agents:write"
|
||||
'400':
|
||||
description: Malformed or missing required request parameters.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/OAuth2ErrorResponse'
|
||||
examples:
|
||||
missingGrantType:
|
||||
summary: Missing grant_type
|
||||
value:
|
||||
error: "invalid_request"
|
||||
error_description: "The 'grant_type' parameter is required."
|
||||
invalidScope:
|
||||
summary: Invalid scope requested
|
||||
value:
|
||||
error: "invalid_scope"
|
||||
error_description: "Requested scope 'admin:all' is not available."
|
||||
unsupportedGrantType:
|
||||
summary: Unsupported grant type
|
||||
value:
|
||||
error: "unsupported_grant_type"
|
||||
error_description: "Only 'client_credentials' grant type is supported."
|
||||
'401':
|
||||
description: Client authentication failed. Invalid `client_id` or `client_secret`.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/OAuth2ErrorResponse'
|
||||
example:
|
||||
error: "invalid_client"
|
||||
error_description: "Client authentication failed. Invalid client_id or client_secret."
|
||||
'403':
|
||||
description: >
|
||||
Client is not authorised to request a token. May indicate the agent
|
||||
is suspended, decommissioned, or the free tier monthly limit has been reached.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/OAuth2ErrorResponse'
|
||||
examples:
|
||||
agentSuspended:
|
||||
summary: Agent is suspended
|
||||
value:
|
||||
error: "unauthorized_client"
|
||||
error_description: "Agent is currently suspended and cannot obtain tokens."
|
||||
freeTierLimit:
|
||||
summary: Monthly token limit reached
|
||||
value:
|
||||
error: "unauthorized_client"
|
||||
error_description: "Free tier monthly token limit of 10,000 requests has been reached."
|
||||
'429':
|
||||
$ref: '#/components/responses/TooManyRequests'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/token/introspect:
|
||||
post:
|
||||
operationId: introspectToken
|
||||
tags:
|
||||
- OAuth 2.0 Tokens
|
||||
summary: Introspect a token (RFC 7662)
|
||||
description: |
|
||||
Determines whether a given access token is currently active (valid,
|
||||
not expired, not revoked). Returns the token's metadata if active.
|
||||
|
||||
Compliant with RFC 7662 (OAuth 2.0 Token Introspection).
|
||||
|
||||
The caller must present a valid Bearer token with `tokens:read` scope
|
||||
to use this endpoint.
|
||||
security:
|
||||
- BearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IntrospectRequest'
|
||||
example:
|
||||
token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAifQ.signature"
|
||||
token_type_hint: "access_token"
|
||||
responses:
|
||||
'200':
|
||||
description: |
|
||||
Token introspection result. Note: a `200` response is returned even
|
||||
for inactive tokens — check the `active` field to determine token validity.
|
||||
headers:
|
||||
X-RateLimit-Limit:
|
||||
schema:
|
||||
type: integer
|
||||
example: 100
|
||||
X-RateLimit-Remaining:
|
||||
schema:
|
||||
type: integer
|
||||
example: 98
|
||||
X-RateLimit-Reset:
|
||||
schema:
|
||||
type: integer
|
||||
example: 1743155400
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IntrospectResponse'
|
||||
examples:
|
||||
activeToken:
|
||||
summary: Active token
|
||||
value:
|
||||
active: true
|
||||
sub: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
client_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
scope: "agents:read agents:write"
|
||||
token_type: "Bearer"
|
||||
iat: 1743151200
|
||||
exp: 1743154800
|
||||
inactiveToken:
|
||||
summary: Inactive (expired or revoked) token
|
||||
value:
|
||||
active: false
|
||||
'400':
|
||||
description: Missing or malformed `token` parameter.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "VALIDATION_ERROR"
|
||||
message: "The 'token' parameter is required."
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
description: Caller's token does not have the `tokens:read` scope.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "INSUFFICIENT_SCOPE"
|
||||
message: "The 'tokens:read' scope is required to introspect tokens."
|
||||
'429':
|
||||
$ref: '#/components/responses/TooManyRequests'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/token/revoke:
|
||||
post:
|
||||
operationId: revokeToken
|
||||
tags:
|
||||
- OAuth 2.0 Tokens
|
||||
summary: Revoke a token (RFC 7009)
|
||||
description: |
|
||||
Revokes an access token, immediately invalidating it for all subsequent
|
||||
requests. Compliant with RFC 7009 (OAuth 2.0 Token Revocation).
|
||||
|
||||
Revoking an already-revoked or expired token is considered a success
|
||||
(idempotent operation per RFC 7009 §2.1).
|
||||
|
||||
The caller must present a valid Bearer token to revoke another token.
|
||||
An agent may revoke its own tokens; admin scope is required to revoke
|
||||
tokens belonging to other agents.
|
||||
security:
|
||||
- BearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RevokeRequest'
|
||||
example:
|
||||
token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAifQ.signature"
|
||||
token_type_hint: "access_token"
|
||||
responses:
|
||||
'200':
|
||||
description: |
|
||||
Token revoked successfully (or was already inactive).
|
||||
Per RFC 7009, revocation always returns `200` for any valid request,
|
||||
even if the token was already revoked or expired.
|
||||
headers:
|
||||
X-RateLimit-Limit:
|
||||
schema:
|
||||
type: integer
|
||||
example: 100
|
||||
X-RateLimit-Remaining:
|
||||
schema:
|
||||
type: integer
|
||||
example: 97
|
||||
X-RateLimit-Reset:
|
||||
schema:
|
||||
type: integer
|
||||
example: 1743155400
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties: {}
|
||||
example: {}
|
||||
'400':
|
||||
description: Missing or malformed `token` parameter.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "VALIDATION_ERROR"
|
||||
message: "The 'token' parameter is required."
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
description: Insufficient permissions to revoke this token.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "FORBIDDEN"
|
||||
message: "You do not have permission to revoke this token."
|
||||
'429':
|
||||
$ref: '#/components/responses/TooManyRequests'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
Reference in New Issue
Block a user