openapi: "3.1.0"
info:
  title: kombify StackKits API
  version: "1.0.0"
  description: |
    kombify StackKits HTTP API — pre-packaged homelab infrastructure templates
    with CUE schema validation and OpenTofu generation. Designed for consumption
    by the kombify Stack control plane and AI agents via the kombify Gateway.
  contact:
    name: kombify
    url: https://kombify.io
  license:
    name: MIT

servers:
  - url: http://localhost:8082
    description: Local development
  - url: https://api.kombify.io/stackkits
    description: Production (via Cloudflare Edge)

paths:
  /health:
    get:
      operationId: healthRoot
      summary: Root health check
      tags: [Health]
      responses:
        "200":
          $ref: "#/components/responses/HealthResponse"

  /api/v1/health:
    get:
      operationId: health
      summary: Service health check
      tags: [Health]
      responses:
        "200":
          $ref: "#/components/responses/HealthResponse"

  /api/v1/capabilities:
    get:
      operationId: getCapabilities
      summary: List all API capabilities (AI agent discovery)
      tags: [Discovery]
      responses:
        "200":
          description: List of capabilities
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessEnvelope"

  /api/v1/openapi.yaml:
    get:
      operationId: getOpenAPISpec
      summary: Get OpenAPI 3.1 specification
      tags: [Discovery]
      responses:
        "200":
          description: YAML OpenAPI spec
          content:
            application/yaml:
              schema:
                type: string

  /api/v1/stackkits:
    get:
      operationId: listStackKits
      summary: List all available StackKits
      tags: [Catalog]
      responses:
        "200":
          description: List of StackKit summaries
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/StackKitSummary"

  /api/v1/stackkits/{name}:
    get:
      operationId: getStackKit
      summary: Get full StackKit details
      tags: [Catalog]
      parameters:
        - $ref: "#/components/parameters/StackKitName"
      responses:
        "200":
          description: Full StackKit configuration
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - properties:
                      data:
                        $ref: "#/components/schemas/StackKit"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/stackkits/{name}/schema:
    get:
      operationId: getStackKitSchema
      summary: Get raw CUE schema for a StackKit
      tags: [Catalog]
      parameters:
        - $ref: "#/components/parameters/StackKitName"
      responses:
        "200":
          description: CUE schema content
          content:
            text/plain:
              schema:
                type: string
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/stackkits/{name}/defaults:
    get:
      operationId: getStackKitDefaults
      summary: Get default StackSpec values for a StackKit
      tags: [Catalog]
      parameters:
        - $ref: "#/components/parameters/StackKitName"
      responses:
        "200":
          description: Default spec values
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - properties:
                      data:
                        $ref: "#/components/schemas/StackSpec"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/validate:
    post:
      operationId: validateSpec
      summary: Validate a stack-spec against its StackKit schema
      tags: [Validation]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StackSpec"
      responses:
        "200":
          description: Validation result
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - properties:
                      data:
                        $ref: "#/components/schemas/ValidationResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /api/v1/validate/partial:
    post:
      operationId: validatePartial
      summary: Validate partial spec fields (for wizard step-by-step validation)
      tags: [Validation]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Partial StackSpec fields to validate
      responses:
        "200":
          description: Partial validation result
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - properties:
                      data:
                        $ref: "#/components/schemas/ValidationResult"
        "400":
          $ref: "#/components/responses/BadRequest"

  /api/v1/generate/tfvars:
    post:
      operationId: generateTFVars
      summary: Generate terraform.tfvars from a validated spec
      tags: [Generation]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/GenerateRequest"
      responses:
        "200":
          description: Generated tfvars
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - properties:
                      data:
                        $ref: "#/components/schemas/GenerateResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /api/v1/generate/preview:
    post:
      operationId: generatePreview
      summary: Preview generated infrastructure without writing files
      tags: [Generation]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/GenerateRequest"
      responses:
        "200":
          description: Preview of generated output
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - properties:
                      data:
                        $ref: "#/components/schemas/PreviewResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /api/v1/status:
    get:
      operationId: getManagementStatus
      summary: Read node-local StackKit rollout status
      description: Read-only management endpoint for local agents and dashboards.
      tags: [Management]
      responses:
        "200":
          description: Node-local rollout status
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - properties:
                      data:
                        $ref: "#/components/schemas/ManagementStatus"

  /api/v1/verify:
    post:
      operationId: runManagementVerify
      summary: Run node-local StackKit verification
      description: Runs the same read-only verification surface as `stackkit verify`.
      tags: [Management]
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VerifyRequest"
      responses:
        "200":
          description: Verification report
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - properties:
                      data:
                        $ref: "#/components/schemas/VerifyReport"
        "400":
          $ref: "#/components/responses/BadRequest"

  /api/v1/doctor:
    post:
      operationId: runManagementDoctor
      summary: Run read-only node-local diagnostics
      tags: [Management]
      responses:
        "200":
          description: Doctor checks
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - properties:
                      data:
                        $ref: "#/components/schemas/DoctorResult"

  /api/v1/plan:
    post:
      operationId: runManagementPlan
      summary: Preview local rollout readiness without mutation
      description: Mutating apply/remove actions are intentionally not exposed by default.
      tags: [Management]
      responses:
        "200":
          description: Read-only local plan readiness
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - properties:
                      data:
                        $ref: "#/components/schemas/ManagementPlan"

  /api/v1/runs/{runID}/evidence:
    get:
      operationId: getRunEvidence
      summary: Read rollout evidence by run ID
      tags: [Management]
      parameters:
        - $ref: "#/components/parameters/RunID"
      responses:
        "200":
          description: Run evidence bundle
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - properties:
                      data:
                        $ref: "#/components/schemas/RunEvidence"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/logs:
    get:
      operationId: listLogs
      summary: List all deploy log runs
      description: Returns a paginated list of deploy log runs sorted by timestamp.
      tags: [Logs]
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
          description: Maximum number of results
        - name: offset
          in: query
          schema:
            type: integer
            minimum: 0
          description: Number of results to skip
      responses:
        "200":
          description: Paginated list of log runs
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - properties:
                      data:
                        $ref: "#/components/schemas/LogRunList"
        "503":
          description: Log directory not configured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/v1/logs/latest:
    get:
      operationId: getLatestLog
      summary: Get the most recent deploy log
      description: Returns all events from the latest deploy log run.
      tags: [Logs]
      parameters:
        - $ref: "#/components/parameters/LogLevel"
        - $ref: "#/components/parameters/LogPrefix"
      responses:
        "200":
          description: Log events
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - properties:
                      data:
                        $ref: "#/components/schemas/LogRunDetail"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/logs/{runID}:
    get:
      operationId: getLog
      summary: Get deploy log by run ID
      description: |
        Returns events from a specific deploy log run.
        Use query parameters to filter by level or event prefix.
      tags: [Logs]
      parameters:
        - $ref: "#/components/parameters/RunID"
        - $ref: "#/components/parameters/LogLevel"
        - $ref: "#/components/parameters/LogPrefix"
      responses:
        "200":
          description: Log events
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - properties:
                      data:
                        $ref: "#/components/schemas/LogRunDetail"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/logs/{runID}/stream:
    get:
      operationId: streamLog
      summary: Stream live deploy log events (SSE)
      description: |
        Server-Sent Events stream for real-time deploy progress.
        Events are sent as `data:` lines with JSON payloads.
        A final `event: done` is sent when the deploy completes.

        **Client usage:**
        ```javascript
        const es = new EventSource('/api/v1/logs/20260311-104539/stream');
        es.onmessage = (e) => console.log(JSON.parse(e.data));
        es.addEventListener('done', (e) => { es.close(); });
        ```
      tags: [Logs]
      parameters:
        - $ref: "#/components/parameters/RunID"
      responses:
        "200":
          description: SSE event stream
          content:
            text/event-stream:
              schema:
                type: string
                description: Server-Sent Events stream
        "400":
          $ref: "#/components/responses/BadRequest"

  # ── Node-local setup actions ────────────────────────────────────

  /api/v1/setup/base-hub/protection:
    get:
      operationId: getBaseHubProtection
      summary: Read Base Hub protection state
      description: Used by the Node Hub to hide first-run bootstrap warnings once Base Hub is protected.
      tags: [Setup]
      responses:
        "200":
          description: Base Hub protection state
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - properties:
                      data:
                        $ref: "#/components/schemas/BaseHubProtectionResponse"
        "409":
          $ref: "#/components/responses/BadRequest"
    post:
      operationId: protectBaseHub
      summary: Protect Base Hub and the node-local API with TinyAuth
      description: |
        Used by the Node Hub `Protect Base Hub` action after owner setup.
        The action persists the protection setting and updates the local Traefik
        dynamic middleware so Base Hub and its node-local API are protected
        without requiring the user to edit variables or run OpenTofu manually.
      tags: [Setup]
      responses:
        "200":
          description: Base Hub protection was activated
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - properties:
                      data:
                        $ref: "#/components/schemas/BaseHubProtectionResponse"
        "202":
          description: Protection was validated but not executed in dry-run mode
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - properties:
                      data:
                        $ref: "#/components/schemas/BaseHubProtectionResponse"
        "409":
          $ref: "#/components/responses/BadRequest"
        "500":
          $ref: "#/components/responses/BadRequest"

  /api/v1/setup/initial-access:
    get:
      operationId: getInitialAccess
      summary: Read one-time technical bootstrap credential reveal state
      description: |
        Used by the Node Hub after Base Hub protection. The response never
        includes the plaintext password, but it labels the credential boundary
        so clients do not confuse technical bootstrap admin material with the
        PocketID Owner/passkey login.
      tags: [Setup]
      responses:
        "200":
          description: Technical bootstrap credential reveal state
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - properties:
                      data:
                        $ref: "#/components/schemas/InitialAccessResponse"
        "409":
          $ref: "#/components/responses/BadRequest"

  /api/v1/setup/initial-access/reveal:
    post:
      operationId: revealInitialAccess
      summary: Reveal technical bootstrap credentials once
      description: |
        Reveals generated TinyAuth/PaaS/service bootstrap credentials one time
        after Base Hub is protected. The Owner login remains PocketID passkey.
        The persisted marker records role and boundary metadata, never the
        plaintext password.
      tags: [Setup]
      responses:
        "200":
          description: Technical bootstrap credentials were revealed and marked consumed
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - properties:
                      data:
                        $ref: "#/components/schemas/InitialAccessResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/BadRequest"
        "410":
          $ref: "#/components/responses/BadRequest"
        "500":
          $ref: "#/components/responses/BadRequest"

  /api/v1/setup/services/{service}/run:
    post:
      operationId: runServiceSetup
      summary: Run or validate an on-demand service setup drop
      description: |
        Used by the StackKits Node Hub `Do the setup for me` action. L1/L2
        platform services report that setup is already automatic; L3 services
        with `setupPolicy: automatic` or `setupPolicy: on_demand` are matched
        against the generated platform-app manifest and executed only when
        stackkit-server is running with `STACKKITS_SETUP_ACTION_MODE=apply`.
        Every runner attempt persists a `SetupRun` with phase logs,
        machine-readable evidence, failure class, retry timestamps, and
        rollback notes in `.stackkit/state.yaml`.
      tags: [Setup]
      parameters:
        - name: service
          in: path
          required: true
          schema:
            type: string
          example: photos
      responses:
        "200":
          description: Automatic setup was already present or a setup drop completed
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - properties:
                      data:
                        $ref: "#/components/schemas/ServiceSetupResponse"
        "202":
          description: Setup was validated but not executed in dry-run mode
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - properties:
                      data:
                        $ref: "#/components/schemas/ServiceSetupResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/BadRequest"
        "502":
          $ref: "#/components/responses/BadRequest"

  # ── Internal runtime actions (TechStack servicecall) ────────────

  /api/v1/internal/runtime-actions/stackkit-rollout:
    post:
      operationId: runtimeStackKitRollout
      summary: Run or dry-run StackKits rollout for TechStack
      description: Requires X-Kombify-Service-Auth from caller techstack.
      tags: [Runtime]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RuntimeActionRequest"
      responses:
        "200":
          description: Runtime action accepted
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - properties:
                      data:
                        $ref: "#/components/schemas/RuntimeActionResponse"
        "400":
          $ref: "#/components/responses/BadRequest"

  /api/v1/internal/runtime-actions/stackkit-verify:
    post:
      operationId: runtimeStackKitVerify
      summary: Verify StackKits rollout state for TechStack
      description: Requires X-Kombify-Service-Auth from caller techstack.
      tags: [Runtime]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RuntimeActionRequest"
      responses:
        "200":
          description: Runtime action accepted
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - properties:
                      data:
                        $ref: "#/components/schemas/RuntimeActionResponse"
        "400":
          $ref: "#/components/responses/BadRequest"

  /api/v1/internal/runtime-actions/restore-drill:
    post:
      operationId: runtimeRestoreDrill
      summary: Run or dry-run StackKits restore-drill handoff for TechStack
      description: Requires X-Kombify-Service-Auth from caller techstack.
      tags: [Runtime]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RuntimeActionRequest"
      responses:
        "200":
          description: Runtime action accepted
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - properties:
                      data:
                        $ref: "#/components/schemas/RuntimeActionResponse"
        "400":
          $ref: "#/components/responses/BadRequest"

  # ── Registry (Direct Connect) ───────────────────────────────────

  /api/v1/registry/instances:
    post:
      operationId: registerInstance
      summary: Register a stackkit-server instance for Direct Connect
      description: |
        Called by stackkit-server after `stackkit apply` to register itself with
        kombify. Cloudflare Edge uses this registry to discover and proxy requests to the
        instance directly.
      tags: [Registry]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InstanceRegistration"
      responses:
        "201":
          description: Instance registered
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - properties:
                      data:
                        $ref: "#/components/schemas/RegistryResponse"
        "400":
          $ref: "#/components/responses/BadRequest"

  /api/v1/registry/instances/{instanceId}:
    delete:
      operationId: deregisterInstance
      summary: Remove an instance from the registry
      tags: [Registry]
      parameters:
        - $ref: "#/components/parameters/InstanceId"
      responses:
        "204":
          description: Instance deregistered
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/registry/instances/{instanceId}/heartbeat:
    put:
      operationId: heartbeat
      summary: Send a heartbeat for a registered instance
      description: |
        Sent every 60 seconds by stackkit-server. If no heartbeat is received
        within 3 minutes, the platform edge registry marks the instance as unreachable and stops
        routing traffic to it.
      tags: [Registry]
      parameters:
        - $ref: "#/components/parameters/InstanceId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [instance_id, status]
              properties:
                instance_id:
                  type: string
                status:
                  type: string
                  enum: [running, degraded, stopped]
      responses:
        "204":
          description: Heartbeat acknowledged
        "404":
          $ref: "#/components/responses/NotFound"

components:
  parameters:
    StackKitName:
      name: name
      in: path
      required: true
      description: StackKit name (e.g., base-kit, ha-kit)
      schema:
        type: string
        pattern: "^[a-z0-9][a-z0-9-]*[a-z0-9]$"

    RunID:
      name: runID
      in: path
      required: true
      description: "Deploy run ID (format: YYYYMMDD-HHMMSS)"
      schema:
        type: string
        pattern: "^\\d{8}-\\d{6}$"
        example: "20260311-104539"

    InstanceId:
      name: instanceId
      in: path
      required: true
      description: Unique instance identifier
      schema:
        type: string
        example: "sh-mylab-abc123-base-kit-def456"

    LogLevel:
      name: level
      in: query
      description: Filter events by log level
      schema:
        type: string
        enum: [INFO, WARN, ERROR, DEBUG]

    LogPrefix:
      name: prefix
      in: query
      description: "Filter events by message prefix (e.g., apply., decision., tofu.)"
      schema:
        type: string
        example: "apply."

  schemas:
    SuccessEnvelope:
      type: object
      required: [data]
      properties:
        data: {}
        meta:
          $ref: "#/components/schemas/ResponseMeta"

    ResponseMeta:
      type: object
      properties:
        request_id:
          type: string
          format: uuid
        timestamp:
          type: string
          format: date-time

    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: integer
            message:
              type: string
        meta:
          $ref: "#/components/schemas/ResponseMeta"

    StackKitSummary:
      type: object
      properties:
        name:
          type: string
          example: base-kit
        displayName:
          type: string
          example: Base Kit
        description:
          type: string
        version:
          type: string
          example: "1.0.0"
        tags:
          type: array
          items:
            type: string

    StackKit:
      type: object
      properties:
        metadata:
          $ref: "#/components/schemas/StackKitMetadata"
        supportedOS:
          type: array
          items:
            type: string
        requirements:
          $ref: "#/components/schemas/Requirements"
        modes:
          $ref: "#/components/schemas/Modes"
        features:
          $ref: "#/components/schemas/Features"

    StackKitMetadata:
      type: object
      properties:
        apiVersion:
          type: string
        kind:
          type: string
        name:
          type: string
        version:
          type: string
        displayName:
          type: string
        description:
          type: string
        author:
          type: string
        license:
          type: string
        homepage:
          type: string
        repository:
          type: string
        tags:
          type: array
          items:
            type: string

    Requirements:
      type: object
      properties:
        minimum:
          $ref: "#/components/schemas/ResourceSpec"
        recommended:
          $ref: "#/components/schemas/ResourceSpec"

    ResourceSpec:
      type: object
      properties:
        cpu:
          type: integer
        ram:
          type: integer
          description: RAM in GB
        disk:
          type: integer
          description: Disk in GB

    Modes:
      type: object
      properties:
        simple:
          $ref: "#/components/schemas/ModeSpec"
        advanced:
          $ref: "#/components/schemas/ModeSpec"

    ModeSpec:
      type: object
      properties:
        name:
          type: string
        description:
          type: string
        engine:
          type: string
          enum: [opentofu, terramate]
        default:
          type: boolean

    Features:
      type: object
      properties:
        multiNode:
          type: boolean
        vpnOverlay:
          type: boolean
        publicAccess:
          type: boolean

    StackSpec:
      type: object
      required: [stackkit]
      properties:
        name:
          type: string
        stackkit:
          type: string
          example: base-kit
        mode:
          type: string
          enum: [simple, advanced]
        context:
          type: string
          enum: [local, cloud, pi]
        domain:
          type: string
        subdomainPrefix:
          type: string
        email:
          type: string
          format: email
        adminEmail:
          type: string
          format: email
        owner:
          $ref: "#/components/schemas/OwnerConfig"
        tls:
          $ref: "#/components/schemas/TLSSpec"
        paas:
          type: string
          enum: [dokploy, coolify]
        network:
          $ref: "#/components/schemas/NetworkSpec"
        compute:
          $ref: "#/components/schemas/ComputeSpec"
        ssh:
          $ref: "#/components/schemas/SSHSpec"
        nodes:
          type: array
          items:
            $ref: "#/components/schemas/NodeSpec"
        services:
          type: object
        environment:
          type: object
          additionalProperties:
            type: string

    OwnerConfig:
      type: object
      description: |
        Lane-aware Owner bootstrap contract. `auto` is for TechStack/kombify
        Cloud handoff and must not require public owner email or username.
        `custom` is explicit self-hosted local Owner bootstrap. `none` skips
        Owner bootstrap. Plaintext recovery passphrases are never accepted.
      properties:
        bootstrapMode:
          type: string
          enum: [auto, custom, none]
        source:
          type: string
          enum: [local, cloud]
        email:
          type: string
          format: email
          description: Required only for bootstrapMode=custom.
        username:
          type: string
          description: Required only for bootstrapMode=custom.
        displayName:
          type: string
        recoveryPassphraseHash:
          type: string
          description: Argon2id PHC hash only; never plaintext.
          pattern: "^\\$argon2id\\$"
        recoveryMaterialRef:
          type: string
          description: TechStack or secret-store reference to generated recovery material.
          pattern: "^(techstack|secret|doppler|vault)://.+|^(env|file):.+"
        cloudOidcIssuer:
          type: string
          format: uri
        cloudOidcClientId:
          type: string
        cloudOidcClientSecretRef:
          type: string
        cloudOidcForeignSubject:
          type: string

    TLSSpec:
      type: object
      properties:
        provider:
          type: string
        challenge:
          type: string
          enum: [tls, dns]

    NetworkSpec:
      type: object
      properties:
        mode:
          type: string
          enum: [local, public, hybrid]
        subnet:
          type: string
        gateway:
          type: string

    ComputeSpec:
      type: object
      properties:
        tier:
          type: string
          enum: [low, standard, high]

    SSHSpec:
      type: object
      properties:
        keyPath:
          type: string
        user:
          type: string
        port:
          type: integer

    NodeSpec:
      type: object
      properties:
        name:
          type: string
        role:
          type: string
          enum: [main, worker, storage, control-plane, standalone]
        ip:
          type: string
          format: ipv4
        services:
          type: array
          items:
            type: string

    ValidationResult:
      type: object
      required: [valid]
      properties:
        valid:
          type: boolean
        errors:
          type: array
          items:
            $ref: "#/components/schemas/ValidationError"
        warnings:
          type: array
          items:
            $ref: "#/components/schemas/ValidationError"

    ValidationError:
      type: object
      properties:
        path:
          type: string
        message:
          type: string
        code:
          type: string

    GenerateRequest:
      type: object
      required: [spec]
      properties:
        spec:
          $ref: "#/components/schemas/StackSpec"
        outputDir:
          type: string
          description: Custom output directory (server-side only)

    GenerateResult:
      type: object
      properties:
        tfvars:
          type: object
          description: Generated terraform variable values
        outputDir:
          type: string
        file:
          type: string

    PreviewResult:
      type: object
      properties:
        tfvars:
          type: object
        preview:
          type: boolean
          const: true
        stackkit:
          $ref: "#/components/schemas/StackKitSummary"

    VerifyRequest:
      type: object
      properties:
        http:
          type: boolean
          description: Verify generated service URLs.
        strict:
          type: boolean
          description: Treat warnings as failures.

    VerifyReport:
      type: object
      properties:
        status:
          type: string
          enum: [pass, warn, fail]
        stackkit:
          type: string
        mode:
          type: string
        remote:
          type: boolean
        targetHost:
          type: string
        generatedAt:
          type: string
          format: date-time
        checks:
          type: array
          items:
            $ref: "#/components/schemas/VerifyCheck"

    VerifyCheck:
      type: object
      properties:
        name:
          type: string
        status:
          type: string
          enum: [pass, warn, fail]
        target:
          type: string
        message:
          type: string

    ManagementStatus:
      type: object
      properties:
        status:
          type: string
        baseDir:
          type: string
        specPath:
          type: string
        specLoaded:
          type: boolean
        statePath:
          type: string
        stateLoaded:
          type: boolean
        stackkit:
          type: string
        mode:
          type: string
        domain:
          type: string
        deploymentStatus:
          type: string
        logDir:
          type: string
        latestRunId:
          type: string
        mutationPolicy:
          type: string

    DoctorResult:
      type: object
      properties:
        status:
          type: string
          enum: [pass, warn, fail]
        checks:
          type: array
          items:
            $ref: "#/components/schemas/ManagementCheck"

    ManagementCheck:
      type: object
      properties:
        name:
          type: string
        status:
          type: string
          enum: [pass, warn, fail]
        target:
          type: string
        message:
          type: string

    ManagementPlan:
      type: object
      properties:
        dryRun:
          type: boolean
          const: true
        mutation:
          type: boolean
          const: false
        ready:
          type: boolean
        status:
          type: string
        specPath:
          type: string
        deployDir:
          type: string
        terraformFiles:
          type: array
          items:
            type: string
        nextCommands:
          type: array
          items:
            type: string

    RunEvidence:
      type: object
      properties:
        runId:
          type: string
        runDir:
          type: string
        metadata:
          type: object
          additionalProperties: true
        summary:
          type: object
          additionalProperties: true
        events:
          type: array
          items:
            type: object
            additionalProperties: true

    RuntimeActionRequest:
      type: object
      required: [action, stack_id]
      properties:
        action:
          type: string
          enum: [stackkit_rollout, verify_rollout, restore_drill]
        stack_id:
          type: string
        stack_name:
          type: string
        stackkit:
          type: string
        tofu_dir:
          type: string
        unified_path:
          type: string
        owner_spec_bootstrap:
          $ref: "#/components/schemas/RuntimeActionOwnerSpecBootstrap"

    RuntimeActionOwnerSpecBootstrap:
      type: object
      required: [endpoint, token, expires_at]
      properties:
        endpoint:
          type: string
        token:
          type: string
        expires_at:
          type: string
          format: date-time
        scopes:
          type: array
          items:
            type: string

    RuntimeActionResponse:
      type: object
      properties:
        status:
          type: string
          enum: [accepted, ready, applied, verified, skipped, failed]
        action:
          type: string
          enum: [stackkit_rollout, verify_rollout, restore_drill]
        stack_id:
          type: string
        stack_name:
          type: string
        stackkit:
          type: string
        tofu_dir:
          type: string
        unified_path:
          type: string
        mode:
          type: string
          enum: [dry-run, apply]
        checks:
          type: array
          items:
            $ref: "#/components/schemas/RuntimeActionCheck"
        stackkit_outputs:
          $ref: "#/components/schemas/RuntimeActionStackKitOutputs"

    RuntimeActionCheck:
      type: object
      properties:
        name:
          type: string
        status:
          type: string
          enum: [ok, warning, missing, reference, skipped, failed]
        detail:
          type: string

    RuntimeActionStackKitOutputs:
      type: object
      properties:
        identity:
          $ref: "#/components/schemas/RuntimeActionIdentityOutputs"
        login_gateway:
          $ref: "#/components/schemas/RuntimeActionLoginGatewayOutput"
        recovery:
          $ref: "#/components/schemas/RuntimeActionRecoveryOutput"
        services:
          type: array
          items:
            $ref: "#/components/schemas/RuntimeActionServiceOutput"
        metadata:
          type: object
          additionalProperties:
            type: string

    RuntimeActionIdentityOutputs:
      type: object
      properties:
        owner:
          $ref: "#/components/schemas/RuntimeActionOwnerIdentity"
        recovery:
          $ref: "#/components/schemas/RuntimeActionRecoveryOutput"

    RuntimeActionOwnerIdentity:
      type: object
      properties:
        username:
          type: string
        email:
          type: string
        display_name:
          type: string
        subject:
          type: string
        provider:
          type: string

    RuntimeActionLoginGatewayOutput:
      type: object
      properties:
        url:
          type: string
        label:
          type: string
        admin_url:
          type: string

    RuntimeActionRecoveryOutput:
      type: object
      properties:
        bundle_ref:
          type: string
        secret_ref:
          type: string
        machine_secret_ref:
          type: string
        passphrase_hash_present:
          type: boolean

    RuntimeActionServiceOutput:
      type: object
      properties:
        name:
          type: string
        url:
          type: string
        admin_url:
          type: string
        metadata:
          type: object
          additionalProperties:
            type: string

    BaseHubProtectionResponse:
      type: object
      properties:
        status:
          type: string
          enum: [bootstrap_open, protected, ready, completed]
        mode:
          type: string
          enum: [dry-run, apply]
        protected:
          type: boolean
        message:
          type: string
        tfvarsUpdated:
          type: boolean
        dynamicConfigUpdated:
          type: boolean

    InitialAccessResponse:
      type: object
      properties:
        status:
          type: string
          enum: [available, consumed, unavailable, protected_required]
        mode:
          type: string
          enum: [one-time]
        protected:
          type: boolean
        available:
          type: boolean
        consumed:
          type: boolean
        credentialRole:
          type: string
          enum: [technical-admin]
        ownerLogin:
          type: string
          enum: [pocketid-passkey]
        credentialBoundary:
          type: string
          example: Bootstrap/service setup credentials; Owner login stays PocketID passkey.
        message:
          type: string
        credentials:
          $ref: "#/components/schemas/InitialAccessCredentials"

    InitialAccessCredentials:
      type: object
      properties:
        adminEmail:
          type: string
          format: email
          description: Technical bootstrap admin email, not the PocketID Owner login.
        adminPassword:
          type: string
          format: password
          description: One-time technical bootstrap password. Only present on reveal.
        selectedPaaS:
          type: string
          example: Coolify
        credentialRole:
          type: string
          enum: [technical-admin]
        ownerLogin:
          type: string
          enum: [pocketid-passkey]
        credentialBoundary:
          type: string
        intendedFor:
          type: array
          items:
            type: string
          example: [TinyAuth gateway technical admin, Coolify technical admin]

    ServiceSetupResponse:
      type: object
      properties:
        serviceKey:
          type: string
          example: photos
        displayName:
          type: string
          example: Immich
        appName:
          type: string
          example: immich
        setupPolicy:
          type: string
          enum: [manual, on_demand, automatic]
        setupActionLabel:
          type: string
          example: Do the setup for me
        mode:
          type: string
          enum: [dry-run, apply]
        status:
          type: string
          enum: [already_automatic, ready, waiting, completed]
        message:
          type: string
        drops:
          type: array
          items:
            $ref: "#/components/schemas/SetupDropResult"

    SetupDropResult:
      type: object
      properties:
        name:
          type: string
          example: immich-owner-bootstrap
        runId:
          type: string
          description: Persistent SetupRun id from .stackkit/state.yaml when executed.
        version:
          type: string
          example: 0.1.0
        runner:
          type: string
          example: stackkit-script
        description:
          type: string
        status:
          type: string
          enum: [ready, running, waiting, completed, failed]
        phase:
          type: string
          enum: [desired, prepared, owner_activated, configured, verified]
        attempts:
          type: integer
          minimum: 1
          description: Number of persisted runner attempts for this setup drop.
        message:
          type: string
        error:
          type: string
        failureClass:
          type: string
          example: setup_credentials_missing
        evidence:
          type: object
          additionalProperties:
            type: string
          description: Machine-readable setup proof such as credential boundaries, token posture, seeded content, or verified handoff state. Secret values are never included.
        logs:
          type: array
          items:
            $ref: "#/components/schemas/SetupRunLogEntry"
        rollbackNotes:
          type: array
          items:
            type: string
        lastRequested:
          type: string
          format: date-time
        lastStarted:
          type: string
          format: date-time
        lastFinished:
          type: string
          format: date-time

    SetupRunLogEntry:
      type: object
      properties:
        timestamp:
          type: string
          format: date-time
        phase:
          type: string
          enum: [desired, prepared, owner_activated, configured, verified]
        level:
          type: string
          enum: [info, warning, error]
        message:
          type: string

    LogRunSummary:
      type: object
      properties:
        run_id:
          type: string
          description: "Unique run identifier (format: YYYYMMDD-HHMMSS)"
          example: "20260311-104539"
        timestamp:
          type: string
          format: date-time
          description: Run start time in RFC3339
        file:
          type: string
          description: Log filename
          example: "20260311-104539.jsonl"

    LogRunList:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/LogRunSummary"
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer

    LogRunDetail:
      type: object
      properties:
        run_id:
          type: string
          example: "20260311-104539"
        events:
          type: array
          items:
            $ref: "#/components/schemas/LogEvent"
        total:
          type: integer
          description: Number of events (after filtering)

    LogEvent:
      type: object
      description: |
        A structured deploy log event. Common fields include:
        - `time` (RFC3339Nano timestamp)
        - `level` (INFO, WARN, ERROR, DEBUG)
        - `msg` (event name, e.g. apply.start, tofu.apply, decision.template)
        - `elapsed_ms` (milliseconds since CLI run start)
        Additional fields vary by event type.
      properties:
        time:
          type: string
          format: date-time
        level:
          type: string
          enum: [INFO, WARN, ERROR, DEBUG]
        msg:
          type: string
          description: "Event name (e.g., apply.start, tofu.apply, decision.template)"
        elapsed_ms:
          type: integer
          description: Milliseconds since CLI run started
      additionalProperties: true

    InstanceRegistration:
      type: object
      required: [instance_id, endpoint_url, stackkit, services, status]
      properties:
        instance_id:
          type: string
          description: Unique instance identifier (device fingerprint + stackkit name)
          example: "sh-mylab-abc123-base-kit-def456"
        endpoint_url:
          type: string
          format: uri
          description: Public URL where stackkit-server is reachable
          example: "https://api.sh-mylab-abc123.kombify.me"
        stackkit:
          type: string
          example: "base-kit"
        version:
          type: string
          example: "1.0.0"
        services:
          type: array
          items:
            $ref: "#/components/schemas/ServiceInfo"
        status:
          type: string
          enum: [running, degraded, stopped]
        api_port:
          type: integer
          example: 8082
        last_seen:
          type: string
          format: date-time

    ServiceInfo:
      type: object
      required: [name, status]
      properties:
        name:
          type: string
          example: "traefik"
        url:
          type: string
          format: uri
        status:
          type: string
          enum: [running, stopped, error]

    RegistryResponse:
      type: object
      properties:
        instance_id:
          type: string
        status:
          type: string
        message:
          type: string

    HealthData:
      type: object
      properties:
        status:
          type: string
          enum: [healthy]
        service:
          type: string
        version:
          type: string

  responses:
    HealthResponse:
      description: Service is healthy
      content:
        application/json:
          schema:
            allOf:
              - $ref: "#/components/schemas/SuccessEnvelope"
              - properties:
                  data:
                    $ref: "#/components/schemas/HealthData"

    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"

    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"

    UnprocessableEntity:
      description: Validation or generation error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"

tags:
  - name: Health
    description: Health checks
  - name: Discovery
    description: API capabilities and documentation
  - name: Catalog
    description: StackKit catalog — browse, search, and inspect available templates
  - name: Validation
    description: CUE-based spec validation
  - name: Generation
    description: OpenTofu variable generation from validated specs
  - name: Management
    description: Node-local read-only management endpoints for agents, dashboards, and MCP tools
  - name: Logs
    description: Deploy log retrieval and live streaming for dashboards and agents
  - name: Setup
    description: Node-local service setup actions for Dashboard-triggered first-run bootstraps
  - name: Runtime
    description: Servicecall-protected runtime actions consumed by TechStack
  - name: Registry
    description: Direct Connect instance registry — deployed stackkit-servers register here so the platform edge can proxy to them
