openapi: 3.1.0 info: title: Oikos API version: 1.0.0 contact: {name: dtoro} license: {name: Private, identifier: LicenseRef-Private} description: | Control-plane API for the Oikos homelab OS. This file is the **source of truth** (contract-first): Go server stubs are generated with oapi-codegen, clients (homelab CLI, future UIs) from the same spec. Conventions (plan R3-3): - Errors are RFC 9457 `application/problem+json`. - Lists use `{items, next_cursor}` with cursor pagination (default limit 50, max 200). - Unsafe POSTs accept `Idempotency-Key` (24h replay window). - Mutable resources carry `version`; GET returns `ETag`, PATCH requires `If-Match` (412 on mismatch). - Timestamps are RFC 3339 UTC. - Entities are addressable by UUID or slug (`host:hubris`). The MCP interface (streamable HTTP, official Go SDK) is mounted at `/mcp` on the same binary and is out of scope for this document; its tools wrap the same service layer as these endpoints. servers: - url: /api/v1 tags: - name: entities description: "Inventory graph — entities and relationships" - name: ontology description: "Entity types, relationship types, lifecycles" - name: signals description: "Signals and checks (observe)" - name: executions description: "Executions, classifications, approvals (decide/act)" - name: learning description: "Patterns and skills (learn)" - name: policy description: "Risk classes, approval rules, autonomy" - name: knowledge description: "Knowledge graph search" - name: observability description: "Metrics, trends, audit, events, health" - name: system description: "Export, health" security: - bearerAuth: [] paths: # ─── Entities ───────────────────────────────────────────────────── /entities: get: tags: [entities] operationId: listEntities summary: List entities x-required-scope: viewer parameters: - {name: type, in: query, schema: {type: string}, description: Filter by entity type (includes descendants)} - {name: state, in: query, schema: {type: string}} - {name: domain, in: query, schema: {type: string}} - {name: layer, in: query, schema: {type: string}} - {name: q, in: query, schema: {type: string}, description: Substring match on slug/name} - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' responses: '200': description: Entity list content: application/json: schema: type: object required: [items] properties: items: {type: array, items: {$ref: '#/components/schemas/Entity'}} next_cursor: {type: [string, "null"]} default: {$ref: '#/components/responses/Problem'} post: tags: [entities] operationId: createEntity summary: Create an entity x-required-scope: operator parameters: [{$ref: '#/components/parameters/IdempotencyKey'}] requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/EntityCreate'} responses: '201': description: Created headers: {ETag: {$ref: '#/components/headers/ETag'}} content: application/json: schema: {$ref: '#/components/schemas/Entity'} default: {$ref: '#/components/responses/Problem'} /entities/{id}: parameters: [{$ref: '#/components/parameters/EntityId'}] get: tags: [entities] operationId: getEntity summary: Get entity by UUID or slug x-required-scope: viewer responses: '200': description: Entity detail headers: {ETag: {$ref: '#/components/headers/ETag'}} content: application/json: schema: {$ref: '#/components/schemas/Entity'} default: {$ref: '#/components/responses/Problem'} patch: tags: [entities] operationId: patchEntity summary: Update attributes or transition lifecycle state description: | Lifecycle transitions are validated against the type's lifecycle_def; illegal transitions return 409 (invalid-transition). Policy-gated actions may return 403 (approval-required). x-required-scope: operator parameters: [{$ref: '#/components/parameters/IfMatch'}] requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/EntityPatch'} responses: '200': description: Updated entity headers: {ETag: {$ref: '#/components/headers/ETag'}} content: application/json: schema: {$ref: '#/components/schemas/Entity'} default: {$ref: '#/components/responses/Problem'} /entities/{id}/relations: parameters: [{$ref: '#/components/parameters/EntityId'}] get: tags: [entities] operationId: getEntityRelations summary: Direct relationships of an entity (both directions) x-required-scope: viewer parameters: - {name: rel_type, in: query, schema: {type: string}} - {name: direction, in: query, schema: {type: string, enum: [out, in, both], default: both}} responses: '200': description: Relationships content: application/json: schema: type: object required: [items] properties: items: {type: array, items: {$ref: '#/components/schemas/Relationship'}} default: {$ref: '#/components/responses/Problem'} /relationships: post: tags: [entities] operationId: createRelationship summary: Create a relationship edge description: Endpoint types validated against relationship_types (hierarchy-aware); cardinality enforced. x-required-scope: operator parameters: [{$ref: '#/components/parameters/IdempotencyKey'}] requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/RelationshipCreate'} responses: '201': description: Created content: application/json: schema: {$ref: '#/components/schemas/Relationship'} default: {$ref: '#/components/responses/Problem'} delete: tags: [entities] operationId: endRelationship summary: End a relationship (sets valid_to; the edge is kept for history) x-required-scope: operator parameters: - {name: source, in: query, required: true, schema: {type: string}} - {name: target, in: query, required: true, schema: {type: string}} - {name: rel_type, in: query, required: true, schema: {type: string}} responses: '204': {description: Relationship ended} default: {$ref: '#/components/responses/Problem'} /graph: get: tags: [entities] operationId: getGraph summary: Subgraph for visualization (nodes + edges) x-required-scope: viewer parameters: - {name: root, in: query, schema: {type: string}, description: Start entity (UUID or slug); omit for whole graph (capped)} - {name: depth, in: query, schema: {type: integer, default: 2, maximum: 5}} - {name: rel_type, in: query, schema: {type: array, items: {type: string}}, style: form, explode: true} responses: '200': description: Graph view content: application/json: schema: {$ref: '#/components/schemas/GraphView'} default: {$ref: '#/components/responses/Problem'} /entities/{id}/blast-radius: parameters: [{$ref: '#/components/parameters/EntityId'}] get: tags: [entities] operationId: getBlastRadius summary: Entities affected if this entity fails x-required-scope: viewer parameters: - {name: depth, in: query, schema: {type: integer, default: 3, maximum: 5}} responses: '200': description: Affected entities with graph distance content: application/json: schema: type: object required: [items] properties: items: type: array items: type: object required: [entity, depth] properties: entity: {$ref: '#/components/schemas/Entity'} depth: {type: integer} default: {$ref: '#/components/responses/Problem'} # ─── Ontology ───────────────────────────────────────────────────── /ontology: get: tags: [ontology] operationId: getOntology summary: Full ontology — entity types, relationship types, lifecycles x-required-scope: viewer responses: '200': description: Ontology content: application/json: schema: type: object required: [entity_types, relationship_types, lifecycles] properties: entity_types: {type: array, items: {$ref: '#/components/schemas/EntityType'}} relationship_types: {type: array, items: {$ref: '#/components/schemas/RelationshipType'}} lifecycles: {type: array, items: {$ref: '#/components/schemas/LifecycleDef'}} default: {$ref: '#/components/responses/Problem'} /ontology/entity-types: post: tags: [ontology] operationId: createEntityType summary: Extend the ontology with a new entity type description: Policy-gated as config_mutation (creates a meta-approval when required). x-required-scope: operator parameters: [{$ref: '#/components/parameters/IdempotencyKey'}] requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/EntityTypeCreate'} responses: '201': description: Created content: application/json: schema: {$ref: '#/components/schemas/EntityType'} '202': {$ref: '#/components/responses/PendingApproval'} default: {$ref: '#/components/responses/Problem'} /ontology/entity-types/{name}: parameters: - {name: name, in: path, required: true, schema: {type: string}} patch: tags: [ontology] operationId: patchEntityType summary: Update or deprecate an entity type description: Hard delete is not supported while instances exist — deprecate instead (plan D3). x-required-scope: operator parameters: [{$ref: '#/components/parameters/IfMatch'}] requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/EntityTypePatch'} responses: '200': description: Updated content: application/json: schema: {$ref: '#/components/schemas/EntityType'} '202': {$ref: '#/components/responses/PendingApproval'} default: {$ref: '#/components/responses/Problem'} # ─── Signals + checks ───────────────────────────────────────────── /signals: get: tags: [signals] operationId: listSignals summary: List signals x-required-scope: viewer parameters: - {name: state, in: query, schema: {type: string}} - {name: severity, in: query, schema: {type: string, enum: [info, warning, critical]}} - {name: entity_id, in: query, schema: {type: string}} - {name: kind, in: query, schema: {type: string}} - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' responses: '200': description: Signals content: application/json: schema: type: object required: [items] properties: items: {type: array, items: {$ref: '#/components/schemas/Signal'}} next_cursor: {type: [string, "null"]} default: {$ref: '#/components/responses/Problem'} /signals/{id}/ack: parameters: [{$ref: '#/components/parameters/EntityId'}] post: tags: [signals] operationId: ackSignal summary: Acknowledge a signal x-required-scope: operator responses: '200': {$ref: '#/components/responses/SignalUpdated'} default: {$ref: '#/components/responses/Problem'} /signals/{id}/resolve: parameters: [{$ref: '#/components/parameters/EntityId'}] post: tags: [signals] operationId: resolveSignal summary: Resolve a signal manually x-required-scope: operator requestBody: content: application/json: schema: type: object properties: {note: {type: string}} responses: '200': {$ref: '#/components/responses/SignalUpdated'} default: {$ref: '#/components/responses/Problem'} /signals/{id}/mute: parameters: [{$ref: '#/components/parameters/EntityId'}] post: tags: [signals] operationId: muteSignal summary: Mute a signal for a TTL x-required-scope: operator requestBody: required: true content: application/json: schema: type: object required: [mute_until] properties: mute_until: {type: string, format: date-time} note: {type: string} responses: '200': {$ref: '#/components/responses/SignalUpdated'} default: {$ref: '#/components/responses/Problem'} /checks: get: tags: [signals] operationId: listChecks summary: List check definitions x-required-scope: viewer parameters: - {name: kind, in: query, schema: {type: string}} - {name: target, in: query, schema: {type: string}} - {name: enabled, in: query, schema: {type: boolean}} - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' responses: '200': description: Checks content: application/json: schema: type: object required: [items] properties: items: {type: array, items: {$ref: '#/components/schemas/Check'}} next_cursor: {type: [string, "null"]} default: {$ref: '#/components/responses/Problem'} post: tags: [signals] operationId: createCheck summary: Create a check (checks-as-data, plan R3-7) x-required-scope: operator parameters: [{$ref: '#/components/parameters/IdempotencyKey'}] requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/CheckCreate'} responses: '201': description: Created content: application/json: schema: {$ref: '#/components/schemas/Check'} default: {$ref: '#/components/responses/Problem'} /checks/{id}: parameters: [{$ref: '#/components/parameters/EntityId'}] patch: tags: [signals] operationId: patchCheck summary: Update or disable a check x-required-scope: operator parameters: [{$ref: '#/components/parameters/IfMatch'}] requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/CheckPatch'} responses: '200': description: Updated content: application/json: schema: {$ref: '#/components/schemas/Check'} default: {$ref: '#/components/responses/Problem'} # ─── Executions / classifications / approvals ───────────────────── /executions: get: tags: [executions] operationId: listExecutions summary: List executions x-required-scope: viewer parameters: - {name: status, in: query, schema: {type: string}} - {name: target, in: query, schema: {type: string}} - {name: action, in: query, schema: {type: string}} - {name: correlation_id, in: query, schema: {type: string}} - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' responses: '200': description: Executions content: application/json: schema: type: object required: [items] properties: items: {type: array, items: {$ref: '#/components/schemas/Execution'}} next_cursor: {type: [string, "null"]} default: {$ref: '#/components/responses/Problem'} post: tags: [executions] operationId: requestExecution summary: Request an execution (classify → approval check → enqueue) description: | The handler classifies the (entity, action), checks policy/autonomy, and either enqueues the execution (auto-approved) or creates an approval request and returns the execution in `proposed` state. Also exposed to the agent role — this is the ONLY way agents act. x-required-scope: agent parameters: [{$ref: '#/components/parameters/IdempotencyKey'}] requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/ExecutionRequest'} responses: '201': description: Execution created (may be pending approval) content: application/json: schema: {$ref: '#/components/schemas/Execution'} default: {$ref: '#/components/responses/Problem'} /executions/{id}: parameters: [{$ref: '#/components/parameters/EntityId'}] get: tags: [executions] operationId: getExecution summary: Execution status + result x-required-scope: viewer responses: '200': description: Execution content: application/json: schema: {$ref: '#/components/schemas/Execution'} default: {$ref: '#/components/responses/Problem'} /executions/{id}/cancel: parameters: [{$ref: '#/components/parameters/EntityId'}] post: tags: [executions] operationId: cancelExecution summary: Cancel a proposed/executing execution x-required-scope: operator responses: '200': description: Cancelled content: application/json: schema: {$ref: '#/components/schemas/Execution'} default: {$ref: '#/components/responses/Problem'} /classifications: get: tags: [executions] operationId: listClassifications summary: Classifier decisions (the autonomous-decision audit trail) x-required-scope: viewer parameters: - {name: signal_id, in: query, schema: {type: string}} - {name: entity_id, in: query, schema: {type: string}} - {name: route, in: query, schema: {type: string, enum: [auto-act, escalate, hold]}} - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' responses: '200': description: Classifications content: application/json: schema: type: object required: [items] properties: items: {type: array, items: {$ref: '#/components/schemas/Classification'}} next_cursor: {type: [string, "null"]} default: {$ref: '#/components/responses/Problem'} /approvals: get: tags: [executions] operationId: listApprovals summary: List approvals x-required-scope: viewer parameters: - {name: status, in: query, schema: {type: string, enum: [pending, approved, denied, expired, revoked]}} - {name: kind, in: query, schema: {type: string, enum: [execution, policy-change, pattern-activation]}} - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' responses: '200': description: Approvals content: application/json: schema: type: object required: [items] properties: items: {type: array, items: {$ref: '#/components/schemas/Approval'}} next_cursor: {type: [string, "null"]} default: {$ref: '#/components/responses/Problem'} /approvals/{id}/decision: parameters: [{$ref: '#/components/parameters/EntityId'}] post: tags: [executions] operationId: decideApproval summary: Approve or deny (single-use token verified server-side) x-required-scope: operator parameters: [{$ref: '#/components/parameters/IdempotencyKey'}] requestBody: required: true content: application/json: schema: type: object required: [decision] properties: decision: {type: string, enum: [approve, deny, revoke]} note: {type: string} responses: '200': description: Decision recorded content: application/json: schema: {$ref: '#/components/schemas/Approval'} default: {$ref: '#/components/responses/Problem'} # ─── Learning ───────────────────────────────────────────────────── /patterns: get: tags: [learning] operationId: listPatterns summary: List patterns x-required-scope: viewer parameters: - {name: entity_type, in: query, schema: {type: string}} - {name: action, in: query, schema: {type: string}} - {name: status, in: query, schema: {type: string}} - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' responses: '200': description: Patterns content: application/json: schema: type: object required: [items] properties: items: {type: array, items: {$ref: '#/components/schemas/Pattern'}} next_cursor: {type: [string, "null"]} default: {$ref: '#/components/responses/Problem'} /patterns/{id}: parameters: [{$ref: '#/components/parameters/EntityId'}] patch: tags: [learning] operationId: patchPattern summary: Transition a pattern (activate / invalidate / deprecate) description: Operator safety valve (plan SG7). Activation is policy-gated config_mutation (S4). x-required-scope: operator parameters: [{$ref: '#/components/parameters/IfMatch'}] requestBody: required: true content: application/json: schema: type: object properties: status: {type: string, enum: [validated, active, deprecated, invalidated]} quarantined: {type: boolean} note: {type: string} responses: '200': description: Updated content: application/json: schema: {$ref: '#/components/schemas/Pattern'} '202': {$ref: '#/components/responses/PendingApproval'} default: {$ref: '#/components/responses/Problem'} /skills: get: tags: [learning] operationId: listSkills summary: List skills (latest version per skill) x-required-scope: viewer parameters: - {name: applies_to, in: query, schema: {type: string}} - {name: action, in: query, schema: {type: string}} - {name: status, in: query, schema: {type: string}} - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' responses: '200': description: Skills content: application/json: schema: type: object required: [items] properties: items: {type: array, items: {$ref: '#/components/schemas/Skill'}} next_cursor: {type: [string, "null"]} default: {$ref: '#/components/responses/Problem'} /skills/{id}: parameters: [{$ref: '#/components/parameters/EntityId'}] patch: tags: [learning] operationId: patchSkill summary: Transition a skill or pin a version x-required-scope: operator parameters: [{$ref: '#/components/parameters/IfMatch'}] requestBody: required: true content: application/json: schema: type: object properties: status: {type: string, enum: [tested, active, deprecated]} pinned_version: {type: integer} note: {type: string} responses: '200': description: Updated content: application/json: schema: {$ref: '#/components/schemas/Skill'} '202': {$ref: '#/components/responses/PendingApproval'} default: {$ref: '#/components/responses/Problem'} /skills/{id}/versions: parameters: [{$ref: '#/components/parameters/EntityId'}] get: tags: [learning] operationId: listSkillVersions summary: Version history of a skill x-required-scope: viewer responses: '200': description: Versions content: application/json: schema: type: object required: [items] properties: items: {type: array, items: {$ref: '#/components/schemas/Skill'}} default: {$ref: '#/components/responses/Problem'} # ─── Policy ─────────────────────────────────────────────────────── /policy/risk-classes: get: tags: [policy] operationId: listRiskClasses summary: List risk classes x-required-scope: viewer responses: '200': description: Risk classes content: application/json: schema: type: object required: [items] properties: items: {type: array, items: {$ref: '#/components/schemas/RiskClass'}} default: {$ref: '#/components/responses/Problem'} /policy/approval-rules: get: tags: [policy] operationId: listApprovalRules summary: List approval rules x-required-scope: viewer responses: '200': description: Rules content: application/json: schema: type: object required: [items] properties: items: {type: array, items: {$ref: '#/components/schemas/ApprovalRule'}} default: {$ref: '#/components/responses/Problem'} post: tags: [policy] operationId: createApprovalRule summary: Propose a new approval rule (dual-control) description: Creates a policy-change approval; the rule applies only after operator approval (plan S3). x-required-scope: operator parameters: [{$ref: '#/components/parameters/IdempotencyKey'}] requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/ApprovalRuleCreate'} responses: '202': {$ref: '#/components/responses/PendingApproval'} default: {$ref: '#/components/responses/Problem'} /policy/approval-rules/{id}: parameters: [{$ref: '#/components/parameters/EntityId'}] patch: tags: [policy] operationId: patchApprovalRule summary: Propose a rule change (dual-control) x-required-scope: operator parameters: [{$ref: '#/components/parameters/IfMatch'}] requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/ApprovalRuleCreate'} responses: '202': {$ref: '#/components/responses/PendingApproval'} default: {$ref: '#/components/responses/Problem'} /policy/autonomy: get: tags: [policy] operationId: getAutonomySettings summary: Autonomy settings (kill-switch, never-auto-act list) x-required-scope: viewer responses: '200': description: Settings content: application/json: schema: type: object required: [items] properties: items: {type: array, items: {$ref: '#/components/schemas/AutonomySetting'}} default: {$ref: '#/components/responses/Problem'} patch: tags: [policy] operationId: patchAutonomySettings summary: Propose autonomy changes (dual-control; kill-switch OFF is immediate) description: | Raising autonomy is dual-controlled (202 + approval). Lowering it — setting `global.auto_act: "off"` or adding a never_auto_act key — applies immediately (200): the kill-switch must never wait for an approval. x-required-scope: operator requestBody: required: true content: application/json: schema: type: object additionalProperties: {type: string} responses: '200': description: Applied (restriction) content: application/json: schema: type: object required: [items] properties: items: {type: array, items: {$ref: '#/components/schemas/AutonomySetting'}} '202': {$ref: '#/components/responses/PendingApproval'} default: {$ref: '#/components/responses/Problem'} # ─── Knowledge ──────────────────────────────────────────────────── /knowledge/search: get: tags: [knowledge] operationId: searchKnowledge summary: Full-text search over knowledge entities (documents, runbooks) x-required-scope: viewer parameters: - {name: q, in: query, required: true, schema: {type: string}} - $ref: '#/components/parameters/Limit' responses: '200': description: Hits content: application/json: schema: type: object required: [items] properties: items: {type: array, items: {$ref: '#/components/schemas/KnowledgeHit'}} default: {$ref: '#/components/responses/Problem'} /knowledge/{entity_id}: parameters: - {name: entity_id, in: path, required: true, schema: {type: string}} get: tags: [knowledge] operationId: getEntityKnowledge summary: All documents/runbooks linked to an entity x-required-scope: viewer responses: '200': description: Linked knowledge content: application/json: schema: type: object required: [items] properties: items: {type: array, items: {$ref: '#/components/schemas/KnowledgeHit'}} default: {$ref: '#/components/responses/Problem'} # ─── Observability ──────────────────────────────────────────────── /metrics: get: tags: [observability] operationId: queryMetrics summary: Query time-series metrics x-required-scope: viewer parameters: - {name: entity_id, in: query, schema: {type: string}} - {name: metric, in: query, schema: {type: array, items: {type: string}}, style: form, explode: true} - {name: rollup, in: query, schema: {type: string, enum: [raw, 1h, 1d, auto], default: auto}} - $ref: '#/components/parameters/FromTime' - $ref: '#/components/parameters/ToTime' responses: '200': description: Metric series content: application/json: schema: type: object required: [items] properties: items: {type: array, items: {$ref: '#/components/schemas/MetricSeries'}} default: {$ref: '#/components/responses/Problem'} /trends/{entity_id}: parameters: - {name: entity_id, in: path, required: true, schema: {type: string}} get: tags: [observability] operationId: getTrends summary: Trend analysis for all metrics on an entity x-required-scope: viewer parameters: - $ref: '#/components/parameters/FromTime' - $ref: '#/components/parameters/ToTime' responses: '200': description: Trends content: application/json: schema: type: object required: [items] properties: items: {type: array, items: {$ref: '#/components/schemas/Trend'}} default: {$ref: '#/components/responses/Problem'} /audit: get: tags: [observability] operationId: queryAudit summary: Audit log x-required-scope: operator parameters: - {name: actor_type, in: query, schema: {type: string}} - {name: actor_id, in: query, schema: {type: string}} - {name: entity_id, in: query, schema: {type: string}} - {name: action, in: query, schema: {type: string}} - {name: correlation_id, in: query, schema: {type: string}} - $ref: '#/components/parameters/FromTime' - $ref: '#/components/parameters/ToTime' - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' responses: '200': description: Audit entries (ts DESC) content: application/json: schema: type: object required: [items] properties: items: {type: array, items: {$ref: '#/components/schemas/AuditEntry'}} next_cursor: {type: [string, "null"]} default: {$ref: '#/components/responses/Problem'} /events: get: tags: [observability] operationId: queryEvents summary: Historical events x-required-scope: viewer parameters: - {name: type, in: query, schema: {type: string}} - {name: entity_id, in: query, schema: {type: string}} - {name: severity, in: query, schema: {type: string}} - {name: correlation_id, in: query, schema: {type: string}} - $ref: '#/components/parameters/FromTime' - $ref: '#/components/parameters/ToTime' - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' responses: '200': description: Events (ts DESC) content: application/json: schema: type: object required: [items] properties: items: {type: array, items: {$ref: '#/components/schemas/Event'}} next_cursor: {type: [string, "null"]} default: {$ref: '#/components/responses/Problem'} /events/stream: get: tags: [observability] operationId: streamEvents summary: Live event stream (SSE) description: | Server-Sent Events. Each event's `id` is the event row id (resume with `Last-Event-ID`), `event` is the event type, `data` is the JSON Event object. Heartbeat comments every 15s. Best-effort delivery — bounded per-subscriber buffer, drop-oldest (plan P6); use GET /events to backfill. x-required-scope: viewer parameters: - {name: type, in: query, schema: {type: string}, description: Filter by event type prefix} - {name: Last-Event-ID, in: header, schema: {type: string}} responses: '200': description: SSE stream content: text/event-stream: schema: {type: string} default: {$ref: '#/components/responses/Problem'} /agent-activity: get: tags: [observability] operationId: queryAgentActivity summary: Agent behavior log x-required-scope: viewer parameters: - {name: agent_id, in: query, schema: {type: string}} - {name: activity_type, in: query, schema: {type: string}} - {name: entity_id, in: query, schema: {type: string}} - $ref: '#/components/parameters/FromTime' - $ref: '#/components/parameters/ToTime' - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Limit' responses: '200': description: Activity entries content: application/json: schema: type: object required: [items] properties: items: {type: array, items: {$ref: '#/components/schemas/AgentActivity'}} next_cursor: {type: [string, "null"]} default: {$ref: '#/components/responses/Problem'} /health: get: tags: [observability] operationId: getFleetHealth summary: Fleet health summary with trend indicators x-required-scope: viewer responses: '200': description: Health summary content: application/json: schema: {$ref: '#/components/schemas/HealthSummary'} default: {$ref: '#/components/responses/Problem'} # ─── System ─────────────────────────────────────────────────────── /export: get: tags: [system] operationId: exportSeeds summary: Regenerate seed YAMLs from current DB state (DR / version control) x-required-scope: operator responses: '200': description: Seed bundle content: application/json: schema: type: object required: [ontology, inventory, policy] properties: ontology: {type: string, description: YAML document} inventory: {type: string, description: YAML document} policy: {type: string, description: YAML document} default: {$ref: '#/components/responses/Problem'} components: securitySchemes: bearerAuth: type: http scheme: bearer description: | Operator/viewer: Authentik OIDC JWT (validated in-API — Caddy forward-auth is defense-in-depth, not the source of truth). Agent: static bearer token from Infisical (scope `agent`). parameters: EntityId: name: id in: path required: true schema: {type: string} description: UUID or slug (e.g. `host:hubris`) Cursor: name: cursor in: query schema: {type: string} description: Opaque cursor from a previous response's next_cursor Limit: name: limit in: query schema: {type: integer, default: 50, maximum: 200, minimum: 1} FromTime: name: from in: query schema: {type: string, format: date-time} ToTime: name: to in: query schema: {type: string, format: date-time} IfMatch: name: If-Match in: header required: true schema: {type: string} description: ETag from a prior GET; 412 on version mismatch IdempotencyKey: name: Idempotency-Key in: header schema: {type: string, maxLength: 128} description: Client-generated key; replays within 24h return the original response headers: ETag: schema: {type: string} description: Resource version for If-Match responses: Problem: description: Error (RFC 9457) content: application/problem+json: schema: {$ref: '#/components/schemas/Problem'} PendingApproval: description: Change staged; a meta-approval was created (dual-control) content: application/json: schema: {$ref: '#/components/schemas/Approval'} SignalUpdated: description: Updated signal content: application/json: schema: {$ref: '#/components/schemas/Signal'} schemas: Problem: type: object required: [title, status] properties: type: {type: string, format: uri, default: "about:blank"} title: {type: string} status: {type: integer} detail: {type: string} instance: {type: string} errors: type: array description: Field-level validation errors (422) items: type: object required: [field, reason] properties: field: {type: string} reason: {type: string} Entity: type: object required: [id, slug, type, name, version, created_at, updated_at] properties: id: {type: string, format: uuid} slug: {type: string, examples: ["host:hubris"]} type: {type: string} name: {type: string} state: {type: [string, "null"]} attributes: {type: object} maintenance_until: {type: [string, "null"], format: date-time} version: {type: integer} created_at: {type: string, format: date-time} updated_at: {type: string, format: date-time} EntityCreate: type: object required: [slug, type, name] properties: slug: {type: string} type: {type: string, description: Must be a non-abstract entity type} name: {type: string} state: {type: string, description: Defaults to the lifecycle's default_state} attributes: {type: object, description: Validated against the type's attribute_schema} EntityPatch: type: object description: At least one of the fields must be present. properties: name: {type: string} state: {type: string, description: Target lifecycle state (transition validated)} attributes: {type: object, description: Merged; validated against attribute_schema} maintenance_until: {type: [string, "null"], format: date-time} Relationship: type: object required: [source, target, type, valid_from] properties: source: {type: string, description: Slug of source entity} target: {type: string, description: Slug of target entity} type: {type: string} attributes: {type: [object, "null"]} valid_from: {type: string, format: date-time} valid_to: {type: [string, "null"], format: date-time} RelationshipCreate: type: object required: [source, target, type] properties: source: {type: string, description: UUID or slug} target: {type: string, description: UUID or slug} type: {type: string} attributes: {type: object} GraphView: type: object required: [nodes, edges] properties: nodes: {type: array, items: {$ref: '#/components/schemas/Entity'}} edges: {type: array, items: {$ref: '#/components/schemas/Relationship'}} truncated: {type: boolean, description: True if node cap was hit} EntityType: type: object required: [name, domain, layer, is_abstract, status] properties: name: {type: string} parent_type: {type: [string, "null"]} is_abstract: {type: boolean} domain: {type: string} layer: {type: string, enum: [meta, infrastructure, governance, cognition]} description: {type: string} lifecycle_id: {type: [string, "null"]} attribute_schema: {type: [object, "null"], description: JSON Schema} schema_version: {type: integer} status: {type: string, enum: [active, deprecated]} version: {type: integer} EntityTypeCreate: type: object required: [name, domain, layer] properties: name: {type: string} parent_type: {type: string} is_abstract: {type: boolean, default: false} domain: {type: string} layer: {type: string, enum: [infrastructure, governance, cognition]} description: {type: string} lifecycle_id: {type: string} attribute_schema: {type: object} EntityTypePatch: type: object properties: description: {type: string} attribute_schema: {type: object} status: {type: string, enum: [active, deprecated]} RelationshipType: type: object required: [name, source_type, target_type, cardinality] properties: name: {type: string} inverse: {type: [string, "null"]} source_type: {type: string, description: May be abstract} target_type: {type: string, description: May be abstract} cardinality: {type: string, enum: [one-to-one, one-to-many, many-to-one, many-to-many]} description: {type: string} LifecycleDef: type: object required: [id, states, default_state, transitions] properties: id: {type: string} states: {type: array, items: {type: string}} default_state: {type: string} terminal_states: {type: array, items: {type: string}} transitions: type: object description: '{from: {to: {requires: [named-check, ...]}}}' Signal: type: object required: [id, slug, kind, severity, state, occurrence_count, first_seen_at, last_seen_at] properties: id: {type: string, format: uuid} slug: {type: string} kind: {type: string, examples: [service-down, disk-threshold, drift, flapping]} severity: {type: string, enum: [info, warning, critical]} state: {type: string, enum: [raised, acknowledged, acting, muted, resolved, failed]} target: {type: [string, "null"], description: Slug of the entity this concerns} check_id: {type: [string, "null"]} evidence: {type: [string, "null"]} likely_cause: {type: [string, "null"]} occurrence_count: {type: integer} flap_count: {type: integer} hold_down_until: {type: [string, "null"], format: date-time} mute_until: {type: [string, "null"], format: date-time} first_seen_at: {type: string, format: date-time} last_seen_at: {type: string, format: date-time} Check: type: object required: [id, slug, kind, interval_s, timeout_s, enabled, version] properties: id: {type: string, format: uuid} slug: {type: string} kind: {type: string, enum: [http, tcp, disk, cert-expiry, drift, ssh-script]} target: {type: [string, "null"], description: Entity slug (instance-scoped)} target_type: {type: [string, "null"], description: Entity type (type-scoped)} config: {type: object, description: Validated per-kind} interval_s: {type: integer} timeout_s: {type: integer} zone: {type: [string, "null"]} enabled: {type: boolean} version: {type: integer} CheckCreate: type: object required: [slug, kind] properties: slug: {type: string} kind: {type: string, enum: [http, tcp, disk, cert-expiry, drift, ssh-script]} target: {type: string} target_type: {type: string} config: {type: object} interval_s: {type: integer, default: 600} timeout_s: {type: integer, default: 10} zone: {type: string} enabled: {type: boolean, default: true} CheckPatch: type: object properties: config: {type: object} interval_s: {type: integer} timeout_s: {type: integer} enabled: {type: boolean} Execution: type: object required: [id, slug, action, risk_class, status, correlation_id, created_at] properties: id: {type: string, format: uuid} slug: {type: string} target: {type: [string, "null"]} action: {type: string} risk_class: {type: string} status: type: string enum: [proposed, approved, auto_approved, denied, expired, executing, verifying, verified, failed, timed_out, cancelled, rolled_back, rollback_failed] classification_id: {type: [string, "null"]} signal_id: {type: [string, "null"]} approval_id: {type: [string, "null"]} agent_id: {type: [string, "null"]} skill_id: {type: [string, "null"]} skill_version: {type: [integer, "null"]} params: {type: object, description: Skill params (validated against params_schema)} result: {type: [object, "null"]} duration_ms: {type: [integer, "null"]} verified: {type: boolean} correlation_id: {type: string} started_at: {type: [string, "null"], format: date-time} completed_at: {type: [string, "null"], format: date-time} created_at: {type: string, format: date-time} ExecutionRequest: type: object required: [target, action] properties: target: {type: string, description: Entity UUID or slug} action: {type: string, examples: [restart, cache-clear]} params: {type: object} signal_id: {type: string, description: Signal that motivated this (optional)} reason: {type: string} Classification: type: object required: [id, action, risk_class, route, reasoning, correlation_id, created_at] properties: id: {type: string, format: uuid} signal_id: {type: [string, "null"]} target: {type: [string, "null"]} action: {type: string} recommended_action: {type: [object, "null"]} risk_class: {type: string} route: {type: string, enum: [auto-act, escalate, hold]} blast_radius: {type: array, items: {type: string}} pattern_confidence: {type: [number, "null"]} skill_id: {type: [string, "null"]} autonomy_check: {type: string} reasoning: {type: object} correlation_id: {type: string} created_at: {type: string, format: date-time} Approval: type: object required: [id, slug, action, risk_class, kind, status, expires_at, created_at] properties: id: {type: string, format: uuid} slug: {type: string} subject: {type: [string, "null"], description: Entity slug the approval concerns} action: {type: string} risk_class: {type: string} kind: {type: string, enum: [execution, policy-change, pattern-activation]} payload: {type: [object, "null"], description: e.g. proposed policy diff} status: {type: string, enum: [pending, approved, denied, expired, revoked]} expires_at: {type: string, format: date-time} decided_at: {type: [string, "null"], format: date-time} decided_by: {type: [string, "null"]} created_at: {type: string, format: date-time} Pattern: type: object required: [id, slug, applies_type, action, pattern, confidence, evidence_count, status, version] properties: id: {type: string, format: uuid} slug: {type: string} applies_type: {type: string} action: {type: string} pattern: {type: string} confidence: {type: number, description: "Wilson lower bound, capped by evidence_count/5"} evidence_count: {type: integer} success_count: {type: integer} failure_count: {type: integer} status: {type: string, enum: [hypothesized, validated, active, deprecated, invalidated]} quarantined: {type: boolean} version: {type: integer} last_validated_at: {type: [string, "null"], format: date-time} Skill: type: object required: [id, slug, name, version, action, status, procedure] properties: id: {type: string, format: uuid} slug: {type: string} name: {type: string} version: {type: integer} action: {type: string} applies_type: {type: [string, "null"]} procedure: type: object description: Structured steps/verify/rollback/params (plan R3-9) required: [steps, verify] properties: params_schema: {type: object} steps: {type: array, items: {$ref: '#/components/schemas/SkillStep'}} verify: {type: array, items: {$ref: '#/components/schemas/SkillStep'}} rollback: {type: array, items: {$ref: '#/components/schemas/SkillStep'}} expected_duration_s: {type: integer} known_failure_modes: {type: array, items: {type: string}} pattern_ids: {type: array, items: {type: string}} status: {type: string, enum: [drafted, tested, active, refined, failed, deprecated]} success_rate: {type: [number, "null"]} changed_by: {type: [string, "null"]} change_reason: {type: [string, "null"]} last_used_at: {type: [string, "null"], format: date-time} SkillStep: type: object required: [runner, command] properties: name: {type: string} runner: {type: string, enum: [ssh, http, internal]} target: {type: string, description: Go template over params} command: {type: string, description: Go template over params} timeout_s: {type: integer, default: 60} expect: type: object properties: exit_code: {type: integer} stdout_contains: {type: string} retry: type: object properties: attempts: {type: integer} delay_s: {type: integer} RiskClass: type: object required: [name, approval_required, autonomy_allowed] properties: name: {type: string} description: {type: string} approval_required: {type: string, enum: [none, operator, operator_confirmed]} autonomy_allowed: {type: boolean} ApprovalRule: type: object required: [id, action, risk_class, autonomy_level, version] properties: id: {type: string, format: uuid} entity_type: {type: [string, "null"], description: May be abstract (inherits down)} action: {type: string} risk_class: {type: string} autonomy_level: {type: string, enum: [auto, escalate, never]} scope_entity: {type: [string, "null"], description: Entity slug for per-entity overrides} version: {type: integer} ApprovalRuleCreate: type: object required: [action, risk_class, autonomy_level] properties: entity_type: {type: string} action: {type: string} risk_class: {type: string} autonomy_level: {type: string, enum: [auto, escalate, never]} scope_entity: {type: string} AutonomySetting: type: object required: [key, value, version] properties: key: {type: string, examples: [global.auto_act]} value: {type: string} version: {type: integer} updated_at: {type: string, format: date-time} KnowledgeHit: type: object required: [id, slug, type, title] properties: id: {type: string, format: uuid} slug: {type: string} type: {type: string, enum: [document, runbook, investigation]} title: {type: string} source_path: {type: [string, "null"]} snippet: {type: [string, "null"], description: Highlighted match context} linked_entities: {type: array, items: {type: string}} rank: {type: [number, "null"]} MetricSeries: type: object required: [entity_id, metric, rollup, samples] properties: entity_id: {type: string} metric: {type: string} rollup: {type: string, enum: [raw, 1h, 1d]} samples: type: array items: type: object required: [ts] properties: ts: {type: string, format: date-time} value: {type: [number, "null"], description: Raw sample value} avg: {type: [number, "null"]} min: {type: [number, "null"]} max: {type: [number, "null"]} count: {type: [integer, "null"]} trend: {$ref: '#/components/schemas/Trend'} Trend: type: object required: [metric, direction] properties: metric: {type: string} direction: {type: string, enum: [improving, degrading, stable, unknown]} slope: {type: [number, "null"], description: Linear fit per day} anomaly: {type: boolean} forecast: {type: [number, "null"], description: "Simple linear projection, 7d out"} AuditEntry: type: object required: [id, ts, actor_type, action] properties: id: {type: integer} ts: {type: string, format: date-time} actor_type: {type: string, enum: [agent, operator, system, scheduler]} actor_id: {type: [string, "null"]} action: {type: string} entity_id: {type: [string, "null"]} method: {type: [string, "null"]} path: {type: [string, "null"]} status_code: {type: [integer, "null"]} detail: {type: object} source_ip: {type: [string, "null"]} correlation_id: {type: [string, "null"]} Event: type: object required: [id, ts, type, severity, source] properties: id: {type: integer} ts: {type: string, format: date-time} type: {type: string, examples: [signal.raised, execution.completed]} entity_id: {type: [string, "null"]} severity: {type: string, enum: [info, warning, critical]} source: {type: string} data: {type: object} correlation_id: {type: [string, "null"]} AgentActivity: type: object required: [id, ts, agent_id, activity_type] properties: id: {type: integer} ts: {type: string, format: date-time} agent_id: {type: string} session_id: {type: [string, "null"]} activity_type: {type: string, enum: [tool_call, reasoning, decision, mcp_query, escalation]} tool_name: {type: [string, "null"]} entity_id: {type: [string, "null"]} input_summary: {type: [string, "null"]} output_summary: {type: [string, "null"]} duration_ms: {type: [integer, "null"]} token_count: {type: [integer, "null"]} success: {type: [boolean, "null"]} correlation_id: {type: [string, "null"]} HealthSummary: type: object required: [summary, entities] properties: summary: type: object required: [healthy, degraded, down, unknown] properties: healthy: {type: integer} degraded: {type: integer} down: {type: integer} unknown: {type: integer} entities: type: array items: type: object required: [slug, type, health] properties: slug: {type: string} type: {type: string} health: {type: string, enum: [healthy, degraded, down, unknown]} trend: {type: [string, "null"], enum: [improving, degrading, stable, unknown, null]} last_check_at: {type: [string, "null"], format: date-time}