openapi: 3.1.0 info: title: Monlet API version: 1.0.0 description: | Monlet v1 API contract. Stage 1 frozen. 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 `. 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. 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 - 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