openapi: 3.0.3
info:
  title: Oikos API
  version: 1.0.0
  contact:
    name: dtoro
  license:
    name: Private
    url: https://example.invalid/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: clients
  description: Client enrollment, context distribution, secrets
- 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:
    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
                    nullable: true
        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
      - name: include
        in: query
        schema:
          type: array
          items:
            type: string
            enum:
            - status
        style: form
        explode: true
        description: include=status joins entity_status and populates GraphView.health
      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'
  /clients/enroll:
    post:
      tags:
      - clients
      operationId: enrollClient
      summary: Enroll a new client — issue age key, create Infisical identity
      description: >
        Validates mesh IP, generates an age keypair, creates an Infisical
        machine identity, and transitions the entity to provisioning.
        Caller must already have an entity in planned or provisioning state.
      x-required-scope: agent
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EnrollRequest'
      responses:
        '200':
          description: Enrollment response with keys and identity
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EnrollResponse'
        default:
          $ref: '#/components/responses/Problem'
  /clients/{slug}/context:
    parameters:
    - $ref: '#/components/parameters/EntitySlug'
    get:
      tags:
      - clients
      operationId: getClientContext
      summary: Get agent context delta since a timestamp
      description: >
        Returns which agent files, tools, and SOPS config changed since the
        given timestamp. Thin clients poll this instead of git pull.
      x-required-scope: agent
      parameters:
      - name: since
        in: query
        schema:
          type: string
          format: date-time
        description: Return only changes since this timestamp (RFC 3339)
      responses:
        '200':
          description: Context delta
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientContext'
        default:
          $ref: '#/components/responses/Problem'
  /clients/{slug}/secrets:
    parameters:
    - $ref: '#/components/parameters/EntitySlug'
    get:
      tags:
      - clients
      operationId: getClientSecrets
      summary: List secrets accessible to this client
      description: Infisical-secured secrets scoped to the client's machine identity
      x-required-scope: agent
      responses:
        '200':
          description: Secret keys accessible to this client
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientSecrets'
        default:
          $ref: '#/components/responses/Problem'
  /entities/provision:
    post:
      tags:
      - entities
      operationId: provisionEntity
      summary: Provision a compute entity (LXC, VM, container) on a host
      description: >
        Creates the entity in planned state, validates constraints (VMID, IP,
        capacity, template), classifies the action against policy, and
        transitions to provisioning on operator approval.
      x-required-scope: operator
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProvisionRequest'
      responses:
        '201':
          description: Entity created, provisioning queued
          headers:
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProvisionResponse'
        default:
          $ref: '#/components/responses/Problem'
  /entities/{slug}/provision/status:
    parameters:
    - $ref: '#/components/parameters/EntitySlug'
    get:
      tags:
      - entities
      operationId: getProvisionStatus
      summary: Poll provisioning progress for a compute entity
      x-required-scope: viewer
      responses:
        '200':
          description: Provisioning steps with status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProvisionStatus'
        default:
          $ref: '#/components/responses/Problem'

  /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:
    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
                    nullable: true
        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
                    nullable: true
        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:
    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
                    nullable: true
        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
                    nullable: true
        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
                    nullable: true
        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
                token:
                  type: string
                  description: HMAC approval token (single-use, verified server-side)
      responses:
        '200':
          description: Decision recorded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Approval'
        default:
          $ref: '#/components/responses/Problem'
  /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
                    nullable: true
        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
                    nullable: true
        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/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/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'
  /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
                    nullable: true
        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
                    nullable: true
        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
                    nullable: true
        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'
  /dashboard/summary:
    get:
      tags:
      - observability
      operationId: getDashboardSummary
      summary: One-round-trip overview for the control room home page
      x-required-scope: viewer
      responses:
        '200':
          description: Dashboard summary
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DashboardSummary'
        default:
          $ref: '#/components/responses/Problem'
  /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
    EntitySlug:
      name: slug
      in: path
      required: true
      schema:
        type: string
      description: Entity slug (e.g. `ws:mac-mini`, `lxc:caddy`)
  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
          example: host:hubris
        type:
          type: string
        name:
          type: string
        state:
          type: string
          nullable: true
        attributes:
          type: object
        maintenance_until:
          type: string
          format: date-time
          nullable: true
        version:
          type: integer
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        health:
          type: string
          description: last observed health, when the entity is monitored
          nullable: true
          enum:
          - healthy
          - degraded
          - down
          - unknown
          - stale
        last_check_at:
          type: string
          format: date-time
          nullable: true
          description: when health was last observed
    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
          format: date-time
          nullable: true
    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
          nullable: true
        valid_from:
          type: string
          format: date-time
        valid_to:
          type: string
          format: date-time
          nullable: true
    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
        health:
          type: object
          description: entity id -> health, present when include=status was requested
          additionalProperties:
            type: string
            enum:
            - healthy
            - degraded
            - down
            - unknown
            - stale
    EntityType:
      type: object
      required:
      - name
      - domain
      - layer
      - is_abstract
      - status
      properties:
        name:
          type: string
        parent_type:
          type: string
          nullable: true
        is_abstract:
          type: boolean
        domain:
          type: string
        layer:
          type: string
          enum:
          - meta
          - infrastructure
          - governance
          - cognition
        description:
          type: string
        lifecycle_id:
          type: string
          nullable: true
        attribute_schema:
          type: object
          description: JSON Schema
          nullable: true
        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
          nullable: true
        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
          example: service-down
        severity:
          type: string
          enum:
          - info
          - warning
          - critical
        state:
          type: string
          enum:
          - raised
          - acknowledged
          - acting
          - muted
          - resolved
          - failed
        target:
          type: string
          description: Slug of the entity this concerns
          nullable: true
        check_id:
          type: string
          nullable: true
        evidence:
          type: string
          nullable: true
        likely_cause:
          type: string
          nullable: true
        occurrence_count:
          type: integer
        flap_count:
          type: integer
        hold_down_until:
          type: string
          format: date-time
          nullable: true
        mute_until:
          type: string
          format: date-time
          nullable: true
        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
          - ping
          - ssh-script
        target:
          type: string
          description: Entity slug (instance-scoped)
          nullable: true
        target_type:
          type: string
          description: Entity type (type-scoped)
          nullable: true
        config:
          type: object
          description: Validated per-kind
        interval_s:
          type: integer
        timeout_s:
          type: integer
        zone:
          type: string
          nullable: true
        enabled:
          type: boolean
        version:
          type: integer
        last_health:
          type: string
          description: >-
            This check's own most recent verdict. An entity's health is the
            worst of these across its enabled checks, so this is what explains
            *why* an entity is degraded. Null until the check first runs.
          nullable: true
          enum:
          - healthy
          - degraded
          - down
          - unknown
        last_run_at:
          type: string
          format: date-time
          description: When this check last executed. Null = never run.
          nullable: true
    CheckCreate:
      type: object
      required:
      - slug
      - kind
      properties:
        slug:
          type: string
        kind:
          type: string
          enum:
          - http
          - tcp
          - disk
          - cert-expiry
          - drift
          - ping
          - 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
          nullable: true
        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
          nullable: true
        signal_id:
          type: string
          nullable: true
        approval_id:
          type: string
          nullable: true
        agent_id:
          type: string
          nullable: true
        skill_id:
          type: string
          nullable: true
        skill_version:
          type: integer
          nullable: true
        params:
          type: object
          description: Skill params (validated against params_schema)
        result:
          type: object
          nullable: true
        duration_ms:
          type: integer
          nullable: true
        verified:
          type: boolean
        correlation_id:
          type: string
        started_at:
          type: string
          format: date-time
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true
        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
          example: restart
        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
          nullable: true
        target:
          type: string
          nullable: true
        action:
          type: string
        recommended_action:
          type: object
          nullable: true
        risk_class:
          type: string
        route:
          type: string
          enum:
          - auto-act
          - escalate
          - hold
        blast_radius:
          type: array
          items:
            type: string
        pattern_confidence:
          type: number
          nullable: true
        skill_id:
          type: string
          nullable: true
        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
          description: Entity slug the approval concerns
          nullable: true
        action:
          type: string
        risk_class:
          type: string
        kind:
          type: string
          enum:
          - execution
          - policy-change
          - pattern-activation
        payload:
          type: object
          description: e.g. proposed policy diff
          nullable: true
        status:
          type: string
          enum:
          - pending
          - approved
          - denied
          - expired
          - revoked
        expires_at:
          type: string
          format: date-time
        decided_at:
          type: string
          format: date-time
          nullable: true
        decided_by:
          type: string
          nullable: true
        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
          format: date-time
          nullable: true
    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
          nullable: true
        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
          nullable: true
        changed_by:
          type: string
          nullable: true
        change_reason:
          type: string
          nullable: true
        last_used_at:
          type: string
          format: date-time
          nullable: true
    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
          description: May be abstract (inherits down)
          nullable: true
        action:
          type: string
        risk_class:
          type: string
        autonomy_level:
          type: string
          enum:
          - auto
          - escalate
          - never
        scope_entity:
          type: string
          description: Entity slug for per-entity overrides
          nullable: true
        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
          example: 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
          nullable: true
        snippet:
          type: string
          description: Highlighted match context
          nullable: true
        linked_entities:
          type: array
          items:
            type: string
        rank:
          type: number
          nullable: true
    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
                description: Raw sample value
                nullable: true
              avg:
                type: number
                nullable: true
              min:
                type: number
                nullable: true
              max:
                type: number
                nullable: true
              count:
                type: integer
                nullable: true
        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
          description: Linear fit per day
          nullable: true
        anomaly:
          type: boolean
        forecast:
          type: number
          description: Simple linear projection, 7d out
          nullable: true
    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
          nullable: true
        action:
          type: string
        entity_id:
          type: string
          nullable: true
        method:
          type: string
          nullable: true
        path:
          type: string
          nullable: true
        status_code:
          type: integer
          nullable: true
        detail:
          type: object
        source_ip:
          type: string
          nullable: true
        correlation_id:
          type: string
          nullable: true
    Event:
      type: object
      required:
      - id
      - ts
      - type
      - severity
      - source
      properties:
        id:
          type: integer
        ts:
          type: string
          format: date-time
        type:
          type: string
          example: signal.raised
        entity_id:
          type: string
          nullable: true
        severity:
          type: string
          enum:
          - info
          - warning
          - critical
        source:
          type: string
        data:
          type: object
        correlation_id:
          type: string
          nullable: true
    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
          nullable: true
        activity_type:
          type: string
          enum:
          - tool_call
          - reasoning
          - decision
          - mcp_query
          - escalation
        tool_name:
          type: string
          nullable: true
        entity_id:
          type: string
          nullable: true
        input_summary:
          type: string
          nullable: true
        output_summary:
          type: string
          nullable: true
        duration_ms:
          type: integer
          nullable: true
        token_count:
          type: integer
          nullable: true
        success:
          type: boolean
          nullable: true
        correlation_id:
          type: string
          nullable: true
    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
            stale:
              type: integer
              description: last observation older than the check's expected cadence
        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
                - stale
              trend:
                type: string
                enum:
                - improving
                - degrading
                - stable
                - unknown
                nullable: true
              last_check_at:
                type: string
                format: date-time
                nullable: true
    DashboardSummary:
      type: object
      required:
      - entities_by_type
      - entities_by_state
      - health
      - signals_by_severity
      - approvals_pending
      - executions_by_state
      - event_rate
      properties:
        entities_by_type:
          type: object
          description: entity counts keyed by type
          additionalProperties:
            type: integer
        entities_by_state:
          type: object
          description: entity counts keyed by state
          additionalProperties:
            type: integer
        health:
          type: object
          required:
          - healthy
          - degraded
          - down
          - unknown
          properties:
            healthy:
              type: integer
            degraded:
              type: integer
            down:
              type: integer
            unknown:
              type: integer
            stale:
              type: integer
              description: last observation older than the check's expected cadence
        signals_by_severity:
          type: object
          description: open (non-resolved) signal counts keyed by severity
          additionalProperties:
            type: integer
        approvals_pending:
          type: integer
        executions_by_state:
          type: object
          description: execution counts keyed by state, last 24h
          additionalProperties:
            type: integer
        event_rate:
          type: array
          description: event counts bucketed by 5-minute interval, most recent last
          items:
            type: object
            required:
            - bucket
            - count
            properties:
              bucket:
                type: string
                format: date-time
              count:
                type: integer
    EnrollRequest:
      type: object
      required:
      - slug
      properties:
        slug:
          type: string
          description: Entity slug (e.g. ws:new-laptop)
        hostname:
          type: string
          description: Actual hostname of the enrolling machine
        mesh_ip:
          type: string
          description: Source mesh IP for identity validation
    EnrollResponse:
      type: object
      required:
      - age_public_key
      - age_private_key
      - infisical_client_id
      - infisical_client_secret
      properties:
        age_public_key:
          type: string
          description: age1... public key for SOPS recipients
        age_private_key:
          type: string
          description: AGE-SECRET-KEY-... for local decryption
        infisical_client_id:
          type: string
          description: Infisical UniversalAuth client ID
        infisical_client_secret:
          type: string
          description: Infisical UniversalAuth client secret
        machine_identity_token:
          type: string
          description: Infisical machine identity access token
    ClientContext:
      type: object
      required:
      - version
      properties:
        version:
          type: integer
          description: Monotonic context version number
        agent_files_changed:
          type: array
          items:
            type: string
          description: Paths of agent instruction files that changed
        sops_config_changed:
          type: boolean
          description: True if .sops.yaml recipients changed
        tools_changed:
          type: array
          items:
            type: string
          description: Paths of tools/*.setup.sh that changed
        since:
          type: string
          format: date-time
          description: Timestamp for the next poll request
    ClientSecrets:
      type: object
      required:
      - keys
      properties:
        keys:
          type: array
          items:
            type: string
          description: Infisical secret keys accessible to this client
    ProvisionRequest:
      type: object
      required:
      - slug
      - type
      - name
      - host
      properties:
        slug:
          type: string
          description: e.g. lxc:jellyfin
        type:
          type: string
          description: Must be lxc, vm, or docker-container
        name:
          type: string
          description: Human-readable name
        host:
          type: string
          description: Slug of the Proxmox host (e.g. host:hubris)
        attributes:
          type: object
          description: VMID, cores, ram_mb, disk_gb, ip, template, mounts, services
    ProvisionResponse:
      type: object
      required:
      - entity
      - execution_id
      properties:
        entity:
          $ref: '#/components/schemas/Entity'
        execution_id:
          type: string
          format: uuid
          description: Execution ID to track provisioning progress
    ProvisionStatus:
      type: object
      required:
      - slug
      - state
      - steps
      properties:
        slug:
          type: string
        state:
          type: string
          description: Current entity state
        steps:
          type: array
          items:
            type: object
            required:
            - step
            - status
            properties:
              step:
                type: string
              status:
                type: string
                enum:
                - pending
                - running
                - ok
                - failed
                - skipped
              error_message:
                type: string
                nullable: true
              started_at:
                type: string
                format: date-time
                nullable: true
              finished_at:
                type: string
                format: date-time
                nullable: true
        error:
          type: string
          nullable: true
          description: Overall error if provisioning failed
