Files
monlet/api/openapi.yaml
2026-06-23 15:12:29 +04:00

978 lines
29 KiB
YAML

openapi: 3.1.0
info:
title: Monlet API
version: 0.1.0
description: |
Monlet 0.1.0 API contract. The API namespace is `/api/v1`.
All endpoints under `/api/v1`. Authentication is split (ADR-0007):
- `agentToken` — self-generated agent Bearer key stored locally by the agent. Used by ingestion paths `POST /api/v1/heartbeat` and `POST /api/v1/events`.
- `uiToken` — operator/UI shared Bearer token from `MONLET_AUTH_TOKEN`. Used by every other authenticated endpoint. Supports overlap rotation via space/comma-separated list (PH-008).
Both are presented as `Authorization: Bearer <token>`. UI token comparison is constant-time. Agent keys are stored server-side only as hashes/fingerprints and never appear in logs, metrics, or notifications.
servers:
- url: http://127.0.0.1:8000
security:
- uiToken: []
paths:
/api/v1/health:
get:
summary: Health check
operationId: getHealth
security: []
responses:
"200":
description: Service is alive.
content:
application/json:
schema:
$ref: "#/components/schemas/HealthResponse"
/api/v1/ready:
get:
summary: Readiness check
operationId: getReady
security: []
responses:
"200":
description: Service is ready.
content:
application/json:
schema:
$ref: "#/components/schemas/HealthResponse"
"503":
description: Service is not ready.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/api/v1/heartbeat:
post:
summary: Ingest agent heartbeat
operationId: ingestHeartbeat
security:
- agentToken: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/HeartbeatRequest"
responses:
"202":
description: Heartbeat accepted.
content:
application/json:
schema:
$ref: "#/components/schemas/AcceptedResponse"
"400":
$ref: "#/components/responses/ValidationError"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
description: Agent key is blocked.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"409":
$ref: "#/components/responses/Conflict"
"413":
$ref: "#/components/responses/PayloadTooLarge"
"429":
$ref: "#/components/responses/RateLimited"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/events:
post:
summary: Ingest agent check result events
operationId: ingestEvents
security:
- agentToken: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/EventBatchRequest"
responses:
"202":
description: Events accepted.
content:
application/json:
schema:
$ref: "#/components/schemas/EventBatchAcceptedResponse"
"400":
$ref: "#/components/responses/ValidationError"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
description: >-
Agent key is blocked (`agent_blocked`) or not yet accepted
(`agent_not_accepted`).
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"413":
$ref: "#/components/responses/PayloadTooLarge"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/agents:
get:
summary: List agents
operationId: listAgents
parameters:
- $ref: "#/components/parameters/Cursor"
- $ref: "#/components/parameters/Back"
- $ref: "#/components/parameters/Limit"
responses:
"200":
description: Agent list.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PageEnvelope"
- type: object
required: [items]
properties:
items:
type: array
items:
$ref: "#/components/schemas/Agent"
"401":
$ref: "#/components/responses/Unauthorized"
/api/v1/agents/admission/blacklist:
get:
summary: List blocked agent keys
operationId: listAgentBlacklist
parameters:
- $ref: "#/components/parameters/Cursor"
- $ref: "#/components/parameters/Limit"
responses:
"200":
description: Agent blacklist.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PageEnvelope"
- type: object
required: [items]
properties:
items:
type: array
items:
$ref: "#/components/schemas/AgentBlacklistItem"
"401":
$ref: "#/components/responses/Unauthorized"
delete:
summary: Clear blocked agent keys
operationId: clearAgentBlacklist
responses:
"200":
description: Number of removed blacklist rows.
content:
application/json:
schema:
$ref: "#/components/schemas/ActionCountResponse"
"401":
$ref: "#/components/responses/Unauthorized"
/api/v1/agents/admission/blacklist/{key_id}:
delete:
summary: Remove one blocked agent key
operationId: deleteAgentBlacklistItem
parameters:
- name: key_id
in: path
required: true
schema:
type: string
minLength: 64
maxLength: 64
responses:
"200":
description: Number of removed blacklist rows.
content:
application/json:
schema:
$ref: "#/components/schemas/ActionCountResponse"
"401":
$ref: "#/components/responses/Unauthorized"
/api/v1/agents/admission/accept-all:
post:
summary: Accept all pending agents
operationId: acceptAllPendingAgents
parameters:
- name: include_rotation
in: query
schema:
type: boolean
default: false
description: When false, accepted agents with pending replacement keys are skipped.
responses:
"200":
description: Number of accepted agents.
content:
application/json:
schema:
$ref: "#/components/schemas/ActionCountResponse"
"401":
$ref: "#/components/responses/Unauthorized"
/api/v1/agents/admission/block-all:
post:
summary: Block all pending agents
operationId: blockAllPendingAgents
responses:
"200":
description: Number of blocked agent keys.
content:
application/json:
schema:
$ref: "#/components/schemas/ActionCountResponse"
"401":
$ref: "#/components/responses/Unauthorized"
/api/v1/agents/admission/remove-all:
post:
summary: Remove all pending agents
operationId: removeAllPendingAgents
responses:
"200":
description: Number of removed agents.
content:
application/json:
schema:
$ref: "#/components/schemas/ActionCountResponse"
"401":
$ref: "#/components/responses/Unauthorized"
/api/v1/agents/{agent_id}:
get:
summary: Get agent detail
operationId: getAgent
parameters:
- $ref: "#/components/parameters/AgentId"
responses:
"200":
description: Agent detail.
content:
application/json:
schema:
$ref: "#/components/schemas/Agent"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
delete:
summary: Remove agent and all owned state
operationId: deleteAgent
parameters:
- $ref: "#/components/parameters/AgentId"
responses:
"200":
description: Number of removed agents.
content:
application/json:
schema:
$ref: "#/components/schemas/ActionCountResponse"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
/api/v1/agents/{agent_id}/accept:
post:
summary: Accept pending agent key
operationId: acceptAgent
parameters:
- $ref: "#/components/parameters/AgentId"
responses:
"200":
description: Number of accepted agents.
content:
application/json:
schema:
$ref: "#/components/schemas/ActionCountResponse"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
/api/v1/agents/{agent_id}/block:
post:
summary: Block pending agent key
operationId: blockAgent
parameters:
- $ref: "#/components/parameters/AgentId"
responses:
"200":
description: Number of blocked agent keys.
content:
application/json:
schema:
$ref: "#/components/schemas/ActionCountResponse"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
/api/v1/checks:
get:
summary: List current check states
operationId: listChecks
parameters:
- name: agent_id
in: query
required: false
schema:
type: string
maxLength: 128
pattern: "^[A-Za-z0-9._:-]+$"
- $ref: "#/components/parameters/Cursor"
- $ref: "#/components/parameters/Back"
- $ref: "#/components/parameters/Limit"
responses:
"200":
description: Check state list.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PageEnvelope"
- type: object
required: [items]
properties:
items:
type: array
items:
$ref: "#/components/schemas/CheckState"
"401":
$ref: "#/components/responses/Unauthorized"
/api/v1/incidents:
get:
summary: List incidents
operationId: listIncidents
parameters:
- name: state
in: query
required: false
schema:
type: string
enum: [open, resolved]
- name: severity
in: query
schema:
type: string
enum: [warning, critical, unknown]
- name: agent_id
in: query
schema:
type: string
maxLength: 128
pattern: "^[A-Za-z0-9._:-]+$"
- name: check_id
in: query
schema:
type: string
maxLength: 128
pattern: "^[A-Za-z0-9._:-]+$"
- name: sort
in: query
schema:
type: string
enum: [status, opened_at, resolved_at]
default: status
- name: direction
in: query
schema:
type: string
enum: [asc, desc]
default: desc
- $ref: "#/components/parameters/Cursor"
- $ref: "#/components/parameters/Back"
- $ref: "#/components/parameters/Limit"
responses:
"200":
description: Incident list.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PageEnvelope"
- type: object
required: [items]
properties:
items:
type: array
items:
$ref: "#/components/schemas/Incident"
"401":
$ref: "#/components/responses/Unauthorized"
/api/v1/events/query:
get:
summary: Query check status-change history
operationId: queryEvents
parameters:
- name: agent_id
in: query
required: false
schema:
type: string
maxLength: 128
pattern: "^[A-Za-z0-9._:-]+$"
- name: check_id
in: query
required: false
schema:
type: string
maxLength: 128
pattern: "^[A-Za-z0-9._:-]+$"
- $ref: "#/components/parameters/Cursor"
- $ref: "#/components/parameters/Back"
- $ref: "#/components/parameters/Limit"
responses:
"200":
description: Event list.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PageEnvelope"
- type: object
required: [items]
properties:
items:
type: array
items:
$ref: "#/components/schemas/StoredCheckResultEvent"
"401":
$ref: "#/components/responses/Unauthorized"
/api/v1/system-summary:
get:
summary: System navigation summary
operationId: getSystemSummary
description: |
Bounded aggregate counts for the top navigation. Safe to poll from many
open dashboards because response size is independent of row counts.
responses:
"200":
description: System summary snapshot.
content:
application/json:
schema:
$ref: "#/components/schemas/SystemSummaryResponse"
"401":
$ref: "#/components/responses/Unauthorized"
/api/v1/notifiers/outbox:
get:
summary: List notification outbox items
operationId: listOutbox
parameters:
- name: state
in: query
schema:
type: string
enum: [pending, sending, sent, retry, failed, discarded]
- name: notifier
in: query
schema:
type: string
enum: [debug, telegram, webhook, alertmanager]
- name: event_type
in: query
schema:
type: string
enum: [firing, resolved]
- name: sort
in: query
schema:
type: string
enum: [created_at, updated_at]
default: updated_at
- name: direction
in: query
schema:
type: string
enum: [asc, desc]
default: desc
- $ref: "#/components/parameters/Cursor"
- $ref: "#/components/parameters/Back"
- $ref: "#/components/parameters/Limit"
responses:
"200":
description: Outbox list.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PageEnvelope"
- type: object
required: [items]
properties:
items:
type: array
items:
$ref: "#/components/schemas/NotificationOutboxItem"
"401":
$ref: "#/components/responses/Unauthorized"
components:
securitySchemes:
agentToken:
type: http
scheme: bearer
description: |
Self-generated agent Bearer key. The agent creates and persists it in
local state; the server stores only hash/fingerprint and requires UI
acceptance before events affect state. Used only by `/heartbeat` and
`/events`. See ADR-0007.
uiToken:
type: http
scheme: bearer
description: |
Operator/UI Bearer token from `MONLET_AUTH_TOKEN`. Multiple tokens may
be active simultaneously (space/comma separated) for zero-loss rotation.
See PH-008 and ADR-0007.
parameters:
AgentId:
name: agent_id
in: path
required: true
schema:
type: string
maxLength: 128
pattern: "^[A-Za-z0-9._:-]+$"
Cursor:
name: cursor
in: query
required: false
description: Opaque pagination cursor returned in `next_cursor` (or `prev_cursor` when `back=true`).
schema:
type: string
maxLength: 512
Back:
name: back
in: query
required: false
description: When true, treat the `cursor` as a `prev_cursor` and return the previous page.
schema:
type: boolean
default: false
Limit:
name: limit
in: query
required: false
schema:
type: integer
minimum: 1
maximum: 500
default: 100
responses:
Unauthorized:
description: Authentication failed.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
NotFound:
description: Resource not found.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
Conflict:
description: Request conflicts with current server state.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
PayloadTooLarge:
description: Payload exceeds configured limit (1 MiB).
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
RateLimited:
description: Request rejected by configured rate/volume limits.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
ValidationError:
description: Request body or parameters failed validation.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
InternalError:
description: Unexpected server error.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
schemas:
HealthResponse:
type: object
required: [status]
properties:
status:
type: string
enum: [ok]
ErrorResponse:
type: object
required: [error]
properties:
error:
type: object
required: [code, message, request_id]
properties:
code:
type: string
enum:
- unauthorized
- forbidden
- conflict
- agent_blocked
- agent_not_accepted
- not_found
- validation
- payload_too_large
- rate_limited
- internal
- not_ready
message:
type: string
request_id:
type: string
description: Server-generated request id, mirrored from X-Request-Id.
PageEnvelope:
type: object
properties:
next_cursor:
type: [string, "null"]
description: Pass back as `cursor` to fetch the next page. Null/absent if no more results.
prev_cursor:
type: [string, "null"]
description: Pass back as `cursor` with `back=true` to fetch the previous page. Null on the first page.
AcceptedResponse:
type: object
required: [accepted]
properties:
accepted:
type: boolean
EventBatchAcceptedResponse:
type: object
required: [accepted, deduplicated]
properties:
accepted:
type: integer
minimum: 0
description: |
Number of events that produced a new status-change row on this request.
deduplicated:
type: integer
minimum: 0
description: |
Number of events that were not stored as a change. Includes events whose
`event_id` was already known, events whose `(status, exit_code)` matched
the previously observed value for the same check, and events received late
(older than the current `last_observed_at` of the check).
HeartbeatRequest:
type: object
additionalProperties: false
required: [agent_id, observed_at, hostname, features]
properties:
agent_id:
type: string
maxLength: 128
pattern: "^[A-Za-z0-9._:-]+$"
observed_at:
type: string
format: date-time
description: UTC RFC3339 timestamp. Timezone is required; use `Z` or `+00:00`. Non-UTC offsets are rejected.
hostname:
type: string
maxLength: 253
features:
$ref: "#/components/schemas/AgentFeatures"
labels:
type: object
maxProperties: 32
propertyNames:
maxLength: 64
pattern: "^[A-Za-z0-9._:-]+$"
additionalProperties:
type: string
maxLength: 256
description: Up to 32 labels. Key max 64, value max 256. Monlet agents reserve `monlet_agent_version`. Secret-like keys are rejected.
AgentFeatures:
type: object
additionalProperties: false
required: [push, metrics]
properties:
push:
type: boolean
description: Agent pushes heartbeats/events to the server.
metrics:
type: boolean
description: Agent exposes a Prometheus-compatible metrics endpoint.
EventBatchRequest:
type: object
additionalProperties: false
required: [agent_id, events]
properties:
agent_id:
type: string
maxLength: 128
pattern: "^[A-Za-z0-9._:-]+$"
events:
type: array
minItems: 1
maxItems: 200
items:
$ref: "#/components/schemas/CheckResultEvent"
CheckResultEvent:
type: object
additionalProperties: false
description: |
Event body used inside `EventBatchRequest.events`. The owning `agent_id`
is supplied once at the batch envelope; per-event `agent_id` is intentionally
absent so one HTTP request cannot mix multiple agents.
Server response endpoints (`GET /events/query`) include `agent_id` separately
in their item schema.
required:
- event_id
- check_id
- observed_at
- status
- exit_code
- duration_ms
properties:
event_id:
type: string
description: UUIDv7 string (lower-case, with dashes). See ADR-0006.
minLength: 36
maxLength: 36
pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
check_id:
type: string
maxLength: 128
pattern: "^[A-Za-z0-9._:-]+$"
observed_at:
type: string
format: date-time
description: UTC RFC3339 timestamp. Timezone is required; use `Z` or `+00:00`. Non-UTC offsets are rejected.
status:
type: string
enum: [ok, warning, critical, unknown]
exit_code:
type: integer
duration_ms:
type: integer
minimum: 0
output:
type: string
maxLength: 8192
description: |
Limit is 8 KiB measured in UTF-8 **bytes** (not characters). Agent truncates
on a UTF-8 boundary before send and appends marker `...[truncated N bytes]`.
`maxLength: 8192` here is a coarse schema-level upper bound; servers MUST
additionally reject events whose UTF-8 byte length exceeds 8192 with
`400 validation`.
output_truncated:
type: boolean
default: false
notifications_enabled:
type: boolean
default: true
description: If false, the server stores state/incidents but does not enqueue notifications for this event.
incident_key:
type: string
maxLength: 256
StoredCheckResultEvent:
description: |
Stored status-change event returned by `GET /events/query`. The server persists
only events that change `(status, exit_code)` versus the previous observed value
for the same `(agent_id, check_id)`. Repeated identical results are not stored.
Includes the resolved `agent_id` and server-side `received_at`.
allOf:
- $ref: "#/components/schemas/CheckResultEvent"
- type: object
required: [agent_id, received_at]
properties:
agent_id:
type: string
maxLength: 128
pattern: "^[A-Za-z0-9._:-]+$"
received_at:
type: string
format: date-time
Agent:
type: object
required: [agent_id, hostname, status, admission_state, last_seen_at, features]
properties:
agent_id:
type: string
hostname:
type: string
status:
type: string
enum: [alive, stale, dead]
admission_state:
type: string
enum: [pending, accepted]
rotation_pending:
type: boolean
default: false
description: True when a pending key would replace an already accepted key for this agent.
key_fingerprint:
type: [string, "null"]
minLength: 16
maxLength: 16
last_seen_at:
type: string
format: date-time
features:
$ref: "#/components/schemas/AgentFeatures"
labels:
type: object
additionalProperties:
type: string
AgentBlacklistItem:
type: object
required: [id, key_fingerprint, agent_id, hostname, created_at, last_seen_at]
properties:
id:
type: string
minLength: 64
maxLength: 64
description: Opaque blacklist row id. Use for delete operations; display `key_fingerprint` to humans.
key_fingerprint:
type: string
minLength: 16
maxLength: 16
agent_id:
type: string
hostname:
type: string
created_at:
type: string
format: date-time
last_seen_at:
type: string
format: date-time
ActionCountResponse:
type: object
required: [count]
properties:
count:
type: integer
minimum: 0
CheckState:
type: object
required: [agent_id, check_id, status, last_observed_at]
properties:
agent_id:
type: string
check_id:
type: string
status:
type: string
enum: [ok, warning, critical, unknown]
last_observed_at:
type: string
format: date-time
exit_code:
type: integer
incident_key:
type: string
summary:
type: string
maxLength: 8192
Incident:
type: object
required: [id, incident_key, state, severity, opened_at]
properties:
id:
type: string
incident_key:
type: string
state:
type: string
enum: [open, resolved]
severity:
type: string
enum: [warning, critical, unknown]
agent_id:
type: string
check_id:
type: string
opened_at:
type: string
format: date-time
resolved_at:
type: [string, "null"]
format: date-time
summary:
type: string
NotificationOutboxItem:
type: object
required: [id, notifier, state, incident_id, event_type, attempts]
properties:
id:
type: string
notifier:
type: string
state:
type: string
enum: [pending, sending, sent, retry, failed, discarded]
incident_id:
type: string
event_type:
type: string
enum: [firing, resolved]
attempts:
type: integer
minimum: 0
next_attempt_at:
type: [string, "null"]
format: date-time
last_error:
type: [string, "null"]
updated_at:
type: string
format: date-time
SystemSummaryResponse:
type: object
description: Bounded aggregate counts for the web navigation.
required:
- agents_online
- agents_offline
- checks_ok
- checks_warning
- checks_critical
- checks_unknown
- open_incidents
- generated_at
properties:
agents_online:
type: integer
minimum: 0
agents_offline:
type: integer
minimum: 0
checks_ok:
type: integer
minimum: 0
checks_warning:
type: integer
minimum: 0
checks_critical:
type: integer
minimum: 0
checks_unknown:
type: integer
minimum: 0
open_incidents:
type: integer
minimum: 0
generated_at:
type: string
format: date-time