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:
SentryAgent.ai Developer
2026-03-28 09:14:41 +00:00
parent 245f8df427
commit d3530285b9
78 changed files with 20590 additions and 1 deletions

View 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
View 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'

View 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'

View 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'