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:
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'
|
||||
Reference in New Issue
Block a user