# FINAL — this document is the FROZEN contract for the Conifer Host's native
# planes (the planes landed: P2 agent, P3 lab, P6-P10 workflows). It is the
# single input the generated @conifer/client (PLAN-platform-core §6, O2) is
# produced from — generated, never hand-written — so it is held TRUE to the
# code: every path here is mounted by the Host's route() (src/substrate/server.rs),
# and every native path route() mounts appears here (the path-inventory pin test
# in tests/contracts.rs reconciles both directions). Property names and enums
# pinned by the golden fixtures in this directory are exact and normative. The
# OpenAI-compatible /v1 plane is deliberately excluded: it is specified by
# OpenAI's own API surface, and every OpenAI SDK is already its client. Likewise
# (1.1.0) the Ollama-compatible /api plane and /infill: specified upstream, every
# Ollama client is already their client. Growth is ADDITIVE only (x-status stays
# final): 1.1.0 adds the observation-plane v1.1 fields (Session waiting_on/cwd/
# tokens/route, the host row, the suspended state), GET /agent/v1/runs/{id}, the
# telemetry serve fold, and the bearer security scheme for non-loopback binds.
# In-place hygiene (CLI-1, no version bump — no wire change): the already-mounted
# /lab/v1/ops read surface (K3c) is documented, and the run-by-name allowlist on
# POST /agent/v1/workflows/{name}/runs is disclosed in prose — documenting a
# mounted route / an existing behavior is additive-legal.
# 1.2.0 (CLI-2, all additive, every value beside its emitter): Session.state
# gains `cancelled` (journal-outcome projection, flag-gated per its EMISSION
# CONDITION below), Session.waiting_on gains `event`, Session gains
# suspend_kind + wrapper_cycle (recorded at suspend time), RunDetail gains
# `inputs` (the journaled declared-inputs map), the run request gains
# `inputs` + `retried_from` (verified retry provenance, stamped derived),
# and SubstrateInfo gains `contract_version` (the capability probe).
# 1.2.0 (CLI-6, all additive, every value beside its emitter): EXTERNAL session
# registration lands on the observation plane — POST /agent/v1/sessions (the
# x-id mint), POST /agent/v1/sessions/{id}/heartbeat, POST
# /agent/v1/sessions/{id}/end, the ExternalSessionRegistration schema, and the
# Session additions (kind `external`, state `waiting`, harness/client/stale/
# tokens_claimed). Registration is attribution, not authority (SPEC §4.1-4.3).
# Normative fixtures: session-states.fixtures.json + skew.fixtures.json.
# P3 (SMC3-CTRL, additive): POST /agent/v1/sessions/{id}/revoke lands on the
# agent plane — the store control verb that durably revokes a session's
# credential (capability- or operator-authorized; forward-denial; history stays
# readable). The ControlAct outcome body (verb/target/by/outcome[/reason]) is
# the store-verb wire face; wire_v1/control_verbs.json is the oracle.
# 1.3.0 (Team Pulse, all additive): the /observe/v1 plane lands — the queryable
# observability surface. Four read-only routes (GET /observe/v1/surfaces, GET
# /observe/v1/surfaces/{surface}/schema, POST /observe/v1/query, POST
# /observe/v1/questions/{name}) over the ObserveEnvelope schema. Read-only BY
# CONSTRUCTION (enrollment lives off-plane in observe-roots.json); bearer-required
# even on loopback (owner O2); the async 202/poll/cancel door is RESERVED, not
# mounted (owner O3). Governed by the owner-approved IPC-EXEMPTIONS observe-plane
# amendment + contracts/observe-conventions.md (the OTel-style naming law).
# 1.4.0 (2026-07-09, Flight Recorder Phase 1, all additive): the write/control
# family /recorder/v1 MOUNTS (recorder-contract.md §5, the ratified T-O table) —
# four nouns (PUT racks/{rack_id} arm/disarm/policy, GET racks grants review,
# POST ingest write-token-gated capture, DELETE racks/{rack_id}/history purge),
# every response the ONE RecorderEnvelope, bearer FAIL-CLOSED on loopback
# including the GETs (T-O4: transcript content is strictly more sensitive than
# git metadata). Reads mount NO new noun — they ride /observe/v1 as registry
# surface `transcript` (recorder-contract.md §13). The recorder-*.fixtures.json
# files beside this document are normative for the family.
openapi: 3.1.0
x-status: final
info:
  title: Conifer native planes
  description: >-
    The Conifer Host's three native API families: the agent plane (/agent/v1 —
    tasks, TurnEvent streams, approvals, the tool catalog, AgentSpecs, and the
    workflows sub-plane: Ensemble CRUD, lint, the built-in catalog, fork, and
    RunEvent run streams), the lab plane (/lab/v1 — the kernel lab as API: graph,
    parts, kernels, bindings, parity, validation, bench, Configs), and the
    control plane (/host/v1 — identity and health). The fourth plane, the
    OpenAI-compatible /v1, is not documented here. TurnEvent, RunEvent, and
    SubstrateInfo schemas agree with the golden fixtures in this directory; the
    fixtures are normative. This document is FROZEN: the generated @conifer/client
    (PLAN-platform-core §6) is produced from it.
    As of 1.3.0 a fifth family lands additively: the observe plane (/observe/v1 —
    Team Pulse, the queryable observability surface), documented in its own
    section below.
    As of 1.4.0 a sixth family lands additively: the Flight
    Recorder write/control plane (/recorder/v1 — per-rack grants, write-token
    ingest, purge), documented in its own section below; recorder READS ride
    the observe plane as surface `transcript`, never a new noun.
  version: 1.4.0
# The generated-client contract (PLAN-platform-core §6, lane O2): @conifer/client
# is GENERATED from this document, never hand-written. /v1 (OpenAI-compatible) is
# excluded — every OpenAI SDK is already its client; the generated client covers
# only the native planes (foremost the GUI's /lab/v1 calls). The package name is
# @conifer/client (no crate or internal-codename tokens leak onto the wire).
x-generated-client:
  package: '@conifer/client'
  source: contracts/openapi-native-planes.yaml
  # /v1 (OpenAI-compatible), /api (Ollama-compatible), and /infill are specified
  # upstream — their SDKs/clients already exist; the generated client covers only
  # the native planes.
  excludes: ['/v1', '/api', '/infill']
# Conifer-served FACADE routes: mounted, deliberately OUTSIDE the versioned
# native-planes contract — unlike the upstream-spec'd /v1|/api|/infill planes
# these carry Conifer's OWN unversioned shape (the proxy-parity liveness door),
# never a /host/v1 key, shape pinned by the server's own unit tests. Declared
# here so the route-reconciliation test disclaims them from a contract key
# rather than a hard-coded test literal — an undeclared facade route fails.
x-facade-routes: ['/health']
servers:
  - url: http://127.0.0.1:8080
    description: >-
      The local Conifer Host (default loopback bind; `conifer serve --port`
      moves it). 8402 is conifer-proxy — a separate organ, not this server.
# Auth posture (SPEC-one-serve §4, FROZEN; W3-B health-liveness amendment): a
# loopback bind is OPEN (plus the Host-header guard at ingress); a non-loopback
# bind (--host) REQUIRES the bearer token on EVERY route, fail-closed — with the
# sole liveness carve-outs GET /host/v1/health (in-contract) and the facade-level
# bare GET /health (deliberately outside the versioned native-planes contract),
# both of which stay unauthenticated but BEHIND the Host-header guard so a
# token-holder's liveness probe answers before the token is sent. Their only
# disclosure is liveness facts — the served model id (/host/v1/health) and the
# planes list (/health) — the accepted W3-B trade-off, recorded here so this
# frozen block, the bearerAuth scheme below, and the code (src/substrate/server.rs
# bearer gate) agree instead of contradicting.
# The empty security object below is the loopback-open alternative.
security:
  - {}
  - bearerAuth: []

paths:
  # ---------------------------------------------------------------- agent plane
  /agent/v1/tasks:
    post:
      operationId: runTask
      summary: Start an agent task and stream its turn as NDJSON TurnEvents.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TaskRequest'
      responses:
        '200':
          description: >-
            One TurnEvent JSON object per line (NDJSON). The stream begins
            with a start event (plane-synthesized: it assigns the task_id and
            echoes the resolved agent name) and ends with an answer or
            aborted event.
          content:
            application/x-ndjson:
              schema:
                $ref: '#/components/schemas/TurnEvent'

  /agent/v1/tasks/{id}:
    get:                                     # NEW route — /runs/{id}'s pinned "r<n> ids only" prose is spared, not widened
      operationId: getTask
      summary: (1.2.0) Durable detail for a journaled task (t<n> ids only).
      parameters:
        - $ref: '#/components/parameters/TaskId'
      responses:
        '200': { description: 'Session-shaped detail + turn summary + ok.' }
        '404': { description: Unknown task id. }

  /agent/v1/pending:
    get:
      operationId: listPending
      summary: Staged destructive changes awaiting approval.
      responses:
        '200':
          description: The pending changes.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/PendingChange'

  /agent/v1/pending/{id}/approve:
    post:
      operationId: approvePending
      summary: Apply a staged change.
      parameters:
        - $ref: '#/components/parameters/PendingId'
      responses:
        '200':
          description: Applied.

  /agent/v1/pending/{id}/discard:
    post:
      operationId: discardPending
      summary: Discard a staged change.
      parameters:
        - $ref: '#/components/parameters/PendingId'
      # (1.2.0, CLI-5) The formerly-planned 409 for "run-owned ids" is DROPPED
      # (red-team): the pending queue is one flat broker queue — nothing marks
      # an id run-owned, so the refusal class has no mechanical definition, and
      # its sentence named the deleted request-changes verb. Reserved in prose:
      # a run-staged-pending refusal returns only if run-staged pending ever
      # exists on the plane, defined mechanically as "the session that staged
      # the op".
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                note:
                  type: string
                  maxLength: 2000
                  description: >-
                    (1.2.0) The reviewer's reason, recorded in the Host's action
                    journal beside the discard. RECORDED, NOT DELIVERED: no
                    runtime consumes it as a revision instruction; a client that
                    labels this "request changes" is lying until one does.
      responses:
        '200':
          description: Discarded.
        '400':
          description: >-
            (1.2.0) Malformed discard body — not a JSON object, a non-string
            note, or a note over the 2000-character cap. Validated BEFORE the
            discard: the staged change is untouched.

  /agent/v1/tools:
    get:
      operationId: toolCatalog
      summary: The three-state tool catalog.
      responses:
        '200':
          description: Every known tool with its availability state.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ToolCatalogEntry'

  /agent/v1/agents:
    get:
      operationId: listProfiles
      summary: List AgentSpecs (the one store under the Host).
      responses:
        '200':
          description: The stored AgentSpecs.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/AgentSpec'
    post:
      operationId: createProfile
      summary: Create an AgentSpec.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AgentSpec'
      responses:
        '201':
          description: Created (the stored AgentSpec is echoed back).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentSpec'
        '400':
          description: Invalid spec (a malformed body, grants, or agent name).
        '409':
          description: An agent of this name already exists (PUT replaces it).

  /agent/v1/agents/{name}:
    get:
      operationId: getProfile
      summary: Fetch one AgentSpec.
      parameters:
        - $ref: '#/components/parameters/AgentName'
      responses:
        '200':
          description: The AgentSpec.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentSpec'
        '404':
          description: No agent of this name.
    put:
      operationId: updateProfile
      summary: Replace an AgentSpec (upsert — creates it when absent).
      parameters:
        - $ref: '#/components/parameters/AgentName'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AgentSpec'
      responses:
        '200':
          description: Stored (replaced or created); the AgentSpec is echoed back.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentSpec'
        '400':
          description: Invalid spec, or the body's name does not match the path.
    delete:
      operationId: deleteProfile
      summary: Delete an AgentSpec.
      parameters:
        - $ref: '#/components/parameters/AgentName'
      responses:
        '204':
          description: Deleted.
        '404':
          description: No agent of this name.

  # The workflows plane: the Ensemble artifact's CRUD/lint/runs/builtins/fork.
  # "workflows" on the wire; the stored artifact is an Ensemble (P6-P10, landed).
  /agent/v1/workflows:
    get:
      operationId: listWorkflows
      summary: List Ensembles (the workflows store under the Host).
      responses:
        '200':
          description: The stored Ensembles.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Ensemble'
    post:
      operationId: createWorkflow
      summary: Create an Ensemble.
      requestBody:
        required: true
        content:
          text/plain:
            schema:
              type: string
              description: The Ensemble artifact as TOML source (stored verbatim).
      responses:
        '201':
          description: Created (the stored Ensemble is echoed back).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Ensemble'
        '400':
          description: >-
            Invalid Ensemble — a malformed body, a failed validation, or a name
            conflict with a reserved segment. The body is the LintReport.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LintReport'
        '409':
          description: An Ensemble of this name already exists (PUT replaces it).

  /agent/v1/workflows/runs:
    get:
      operationId: listWorkflowRuns
      summary: >-
        Read the run journal (recent RunRecords from runs.jsonl). A reserved
        literal segment — an Ensemble cannot be named "runs".
      responses:
        '200':
          description: Recent run records.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/RunRecord'

  /agent/v1/workflows/{name}:
    get:
      operationId: getWorkflow
      summary: Fetch one Ensemble.
      parameters:
        - $ref: '#/components/parameters/WorkflowName'
      responses:
        '200':
          description: The Ensemble.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Ensemble'
        '404':
          description: No Ensemble of this name.
    put:
      operationId: updateWorkflow
      summary: Replace an Ensemble (upsert — creates it when absent).
      parameters:
        - $ref: '#/components/parameters/WorkflowName'
      requestBody:
        required: true
        content:
          text/plain:
            schema:
              type: string
              description: The Ensemble artifact as TOML source (stored verbatim).
      responses:
        '200':
          description: Stored (replaced or created); the Ensemble is echoed back.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Ensemble'
        '400':
          description: >-
            Invalid Ensemble, or the body's name does not match the path. The
            body is the LintReport.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LintReport'
    delete:
      operationId: deleteWorkflow
      summary: Delete an Ensemble.
      parameters:
        - $ref: '#/components/parameters/WorkflowName'
      responses:
        '204':
          description: Deleted.
        '404':
          description: No Ensemble of this name.

  /agent/v1/workflows/{name}/lint:
    post:
      operationId: lintWorkflow
      summary: >-
        Validate the stored Ensemble (or a body, when provided) without running
        it; returns the LintReport plus the derived consent summary.
      parameters:
        - $ref: '#/components/parameters/WorkflowName'
      requestBody:
        required: false
        content:
          text/plain:
            schema:
              type: string
              description: The Ensemble artifact as TOML source (stored verbatim).
      responses:
        '200':
          description: The lint report and derived consent summary.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LintReport'
        '404':
          description: No Ensemble of this name (when linting by name).

  /agent/v1/workflows/{name}/fork:
    post:
      operationId: forkWorkflow
      summary: >-
        Fork ("customize") a built-in into the user's workflows store. Copies
        the built-in named {name} (from the read-only catalog) into
        ensembles/<target>.toml, stamping [ensemble].origin =
        `builtin:<name>@<version>` as a one-shot snapshot of WHICH built-in
        version was forked (it does NOT track upstream updates). The materialized
        copy is then a NORMAL stored Ensemble — it lints, runs, and is
        PUT-editable. A reserved literal segment: an Ensemble cannot be named
        "fork", so a stored workflow can never shadow this route.
      parameters:
        - $ref: '#/components/parameters/WorkflowName'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ForkRequest'
      responses:
        '201':
          description: >-
            Forked — the materialized stored Ensemble (its TOML source carries
            the stamped origin) is echoed back.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Ensemble'
        '404':
          description: No built-in of this name in the catalog.
        '409':
          description: >-
            A stored workflow already holds the target name — a fork never
            silently overwrites. Fork under a different name via the `as` field.

  /agent/v1/workflows/builtins:
    get:
      operationId: listBuiltinWorkflows
      summary: >-
        The built-in Ensemble catalog — the default workflows Sage ships
        embedded in the Host binary, read-only. A reserved literal segment: an
        Ensemble cannot be named "builtins", so a stored workflow can never
        shadow this route. Each entry is self-contained (inline agents, no
        agent-store dependency) and carries its origin provenance tag.
      responses:
        '200':
          description: The built-in Ensembles (name + projected metadata).
          content:
            application/json:
              schema:
                type: object
                properties:
                  builtins:
                    type: array
                    items:
                      $ref: '#/components/schemas/BuiltinWorkflow'
                required: [builtins]

  /agent/v1/workflows/{name}/runs:
    post:
      operationId: runWorkflow
      summary: >-
        Run a stored Ensemble and stream its RunEvent envelope as NDJSON. The
        body is the declared inputs map. One run at a time per Host.
        DENY-BY-DEFAULT ALLOWLIST: a workflow is callable by name ONLY when
        the operator explicitly enabled it (`conifer serve --enable <name>`,
        or the loud `--enable-all` opt-out of the per-name allowlist — off by
        default). An un-enabled name is refused with the SAME name-free 404 a
        truly-unknown name gets, byte-identical on purpose, so the refusal
        never reveals whether the workflow exists in the store.
      parameters:
        - $ref: '#/components/parameters/WorkflowName'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              description: >-
                The run request. Unknown extra fields are ignored
                (protocol-permissive, the 1.1.0 behavior) — which is exactly
                why the 1.2.0 additions below land as ONE delta: a
                split-landed field would be silently dropped with no refusal.
              additionalProperties: true
              properties:
                input:
                  type: string
                  description: >-
                    The run input, surfaced as the {{input}} template default
                    (the 1.1.0 body — stays honored beside `inputs`).
                inputs:
                  type: object
                  additionalProperties: true
                  description: >-
                    (1.2.0) The declared-inputs map: each key seeds the
                    declared root input of that name. Journaled at run start
                    and served back as RunDetail.inputs — the durable
                    precondition for retry.
                retried_from:
                  type: string
                  description: >-
                    (1.2.0) Retry provenance: the terminal-FAILED run this run
                    retries (r<n>). VERIFIED against the journal — a name that
                    is not an existing terminal-failed run answers 409 — and
                    then STAMPED into the new run's journal record by the
                    plane (derived provenance; a client cannot author journal
                    rows). Retry mints a NEW run: the source row is never
                    mutated.
      responses:
        '200':
          description: >-
            One RunEvent JSON object per line (NDJSON). The stream begins with
            run_start, brackets each step with step_start/step_end (with
            step_waiting when a step parks for approval) and wraps every agent
            TurnEvent verbatim in step_event.event, and ends with run_end.
          content:
            application/x-ndjson:
              schema:
                $ref: '#/components/schemas/RunEvent'
        '404':
          description: >-
            No Ensemble of this name — OR the name is not enabled for the
            workflow-calling API (deny-by-default; see the summary). The two
            cases answer one deliberately name-free, byte-identical body: an
            un-enabled name must not become an existence oracle.
        '409':
          description: >-
            A run is already in flight (one run at a time per Host) — or
            (1.2.0) retried_from failed verification: it names no journaled
            run, or names one that is not terminal-failed (v1 retry
            re-dispatches failed runs only). The refusal sentence names which.

  /agent/v1/sessions:
    get:
      operationId: listSessions
      summary: >-
        The observation plane's session feed (ONE-PLATFORM invariant 3): the
        agent work this Host truly knows about, as ONE resource — live sessions
        (in-flight tasks and workflow runs, newest first) followed by the
        journal's Host-driven history (newest first): workflow runs AND, since
        1.2.0 (CLI-7), finished agent TASKS (kind "task", state
        completed|failed — tasks journal beside runs at end-of-turn, SPEC §4.5).
        Honest subset: what remains deliberately absent is a CLI-local run
        (flow run/listen/schedule — no Host-minted id, never a session on this
        plane) and a closed EXTERNAL row (it survives only as its durable
        receipt, GET /agent/v1/sessions/{id}/receipt, never re-entering this
        feed). What a fleet board shows and what this returns are the same
        object. (1.2.0, CLI-6) EXTERNAL sessions registered through the POST
        below appear beside the live rows (kind "external") for as long as they
        are live — registered and not yet ended or heartbeat-lost-closed.
      responses:
        '200':
          description: The sessions — live first (newest first), then history.
          content:
            application/json:
              schema:
                type: object
                properties:
                  sessions:
                    type: array
                    items:
                      $ref: '#/components/schemas/Session'
                required: [sessions]
        '500':
          description: >-
            The run journal (the durable history half) could not be read — loud,
            never a silently half-empty list.
    post:                                    # NEW (1.2.0, CLI-6) — GET untouched
      operationId: registerSession
      summary: >-
        (1.2.0) Register EXTERNAL work as a session on the observation plane.
        The Host does not execute or broker this work — it OBSERVES a claim.
        Registration is attribution, not authority: NO grants, NO broker, NO
        tools from this route. The Host mints the id and a heartbeat deadline;
        unrenewed sessions go stale, then close (heartbeat_lost). INGRESS CLASS
        (all three POSTs here): Content-Type application/json REQUIRED — not a
        CORS-safelisted value, so a cross-origin browser POST must preflight
        and the Host answers no CORS-allow (the proxy guard_post precedent);
        the external registry is BOUNDED — past the Host's max-live-external-
        sessions cap (default 64) registration answers 429.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExternalSessionRegistration'
      responses:
        '201':
          description: Registered.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    description: >-
                      x-family — NEW, never t<n>/r<n> (reserved permanently);
                      RANDOM and RESTART-STABLE (x<nanos><rand>, the r<nanos>
                      mint pattern + entropy): /end journals durable receipts
                      keyed by this id, and an enumerable per-process counter
                      would collide across restarts and invite probing.
                  secret:
                    type: string
                    description: >-
                      The per-session WRITE credential: heartbeat/end require
                      it (401 otherwise). Random, returned ONCE, never readable
                      back. This is liveness-write binding, NOT the reserved
                      scoped-token system — a durable receipt without writer
                      binding is theatre (SPEC §4.2).
                  heartbeat_secs:
                    type: integer
                    description: The deadline contract, Host-owned.
                required: [id, secret, heartbeat_secs]
        '400':
          description: >-
            Unparseable/invalid — ALL errors at once, refused whole, never a
            half row.
        '429':
          description: >-
            External-session cap reached — the registry is bounded, never
            unbounded.

  /agent/v1/sessions/{id}/heartbeat:
    post:
      operationId: heartbeatSession
      summary: >-
        (1.2.0) Liveness + claimed usage for an EXTERNAL session. Requires the
        201-minted x-conifer-session-secret header (401 without it — any local
        process could otherwise spoof another agent's row: loopback auth is
        Open, and the non-loopback bearer is one shared token). x ids only —
        an r<n>/t<n> session's liveness is Host-observed, never self-reported (409).
      parameters:
        - $ref: '#/components/parameters/ExternalSessionId'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                status:
                  type: string
                  enum: [running, waiting]   # terminal claims go through /end; review is NOT self-claimable.
                  description: >-
                    waiting projects as Session.state "waiting" — an additive
                    state value whose ONLY emitter is this projection (external
                    rows; SPEC §1.3) — and classes RESTING on the board, never
                    needs-you: a self-report with no actable affordance can
                    never own the amber tap (SPEC §1.4).
                tokens:
                  type: object
                  properties:
                    prompt:
                      type: integer
                    completion:
                      type: integer
                  description: >-
                    Cumulative, CLAIMED — folded into Session.tokens_claimed and
                    NOWHERE else. Session.tokens is measured-only by its 1.1.0
                    docstring and heartbeats NEVER write it (R9's wire).
                last_event:
                  type: string
                  maxLength: 200
      responses:
        '200':
          description: Deadline renewed.
        '401':
          description: Missing/wrong session secret — writes bind to the registrant.
        '404':
          description: Unknown or already-closed session.
        '409':
          description: Not an external session.

  /agent/v1/sessions/{id}/end:
    post:
      operationId: endSession
      summary: >-
        (1.2.0) Close an external session with an outcome; journaled — the
        durable receipt. Requires the session secret (401 otherwise) — a
        forgeable /end would journal lies permanently.
      parameters:
        - $ref: '#/components/parameters/ExternalSessionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                outcome:
                  type: string
                  enum: [completed, failed, cancelled]   # RunOutcome VERBATIM
                exit_code:
                  type: integer
                summary:
                  type: string
                  maxLength: 500
              required: [outcome]
      responses:
        '200':
          description: Ended and journaled.
        '401':
          description: Missing/wrong session secret.
        '404':
          description: Unknown session.

  /agent/v1/sessions/{id}/revoke:
    post:
      operationId: revokeSession
      summary: >-
        (P3, SMC3-CTRL) Durably revoke a store session's credential — the store
        control verb. Store-shaped ids only (salted <prefix><8hex>-<seq>); an
        observe-shaped id has no store row and 404s honestly. Authorized by the
        session's OWN capability (x-conifer-session-secret, constant-time) or a
        valid operator credential (x-conifer-operator, loopback) — a
        missing/wrong credential is 401 (names the header, never the secret),
        and another session's valid capability is the SAME 401. Forward-denial
        only: the revoked capability stops authorizing verbs immediately and the
        durable deny (a capability HASH) persists across restart; history stays
        readable (the ingress-door 403 is the P4 layer). Every act — applied AND
        refused — appends to control.jsonl.
      parameters:
        - $ref: '#/components/parameters/SessionId'
      responses:
        '200':
          description: >-
            The ControlAct outcome — verb "revoke", the target id, `by`
            (operator | session:<id>, derived from which credential validated),
            and `outcome` (applied, or refused with a reason code).
          content:
            application/json:
              schema:
                type: object
                properties:
                  verb:
                    const: revoke
                  target:
                    type: string
                  by:
                    type: string
                  outcome:
                    type: string
                    enum: [applied, refused]
                  reason:
                    type: string
                required: [verb, target, by, outcome]
        '401':
          description: >-
            Missing/wrong session capability, or another session's capability —
            the same refusal (a capability is not an identity oracle).
        '404':
          description: Unknown session (or an observe-shaped id — no store row).

  /agent/v1/sessions/{id}/receipt:
    get:
      operationId: getWorkReceipt
      summary: >-
        (1.2.0) The session's durable receipt — execution-report v1 grammar
        lifted to the work plane. Absent fields are not-disclosed, NEVER zero;
        unknown values are opaque to clients; evolution is add-only.
      parameters:
        - $ref: '#/components/parameters/SessionId'
      responses:
        '200': { content: { application/json: { schema: { $ref: '#/components/schemas/WorkReceipt' } } } }
        '404': { description: Not ended / never journaled — the reason says which. }

  /agent/v1/sessions/{id}/cancel:
    post:
      operationId: cancelSession
      summary: >-
        Request cancellation of a live session — the plane's one action. A
        workflow run has a real seam (the runner observes the cancel flag at
        every step boundary, ending the stream with run_end
        outcome "cancelled"), so the request is accepted and the cancel lands
        at the next boundary — never a mid-step kill. A task is one atomic
        turn with no seam, so asking is refused honestly, never silently
        accepted.
      parameters:
        - $ref: '#/components/parameters/SessionId'
      responses:
        '202':
          description: >-
            Cancellation requested; the run observes it at the next step
            boundary.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  cancel:
                    const: requested
                required: [id, cancel]
        '404':
          description: No live session of this id (unknown, or already finished).
        '409':
          description: >-
            The session is a task — one atomic turn with no cancellation seam.
            Or (1.2.0) the synthetic host row: the interactive host is not a
            cancellable session — the refusal says to interrupt its turn, or
            stop the Host process. Or (1.2.0) a resume-dispatched segment,
            which carries no cancel seam this increment. Every class is
            refused honestly, never silently accepted.

  /agent/v1/runs/{id}:
    get:
      operationId: getRun
      summary: >-
        One workflow run's record (v1.1): the live observe state joined with
        the durable halves — a running run answers from the live plane
        (state "running" — or "paused" since 1.2.0 — cancellable, route
        facts); a durably-suspended run answers from its resume record (state
        "suspended", waiting_on); a finished run answers from the run journal,
        with its per-step detail (step_records) and, on a clean run, the
        harvested result. r<n> ids only — this "r<n> only" refusal is PINNED
        (SPEC §4.5: spared, not widened). Honest refusals: a task id is 404
        WITH the reason and pointed at its OWN route (tasks now journal, on
        GET /agent/v1/tasks/{id} — the runs route never grew a second id
        family). Run control (1.2.0, CLI-3/CLI-4): the formerly-reserved
        pause/resume verbs are now served — POST /agent/v1/runs/{id}/pause,
        POST /agent/v1/runs/{id}/resume, and POST /agent/v1/runs/{id}/run-now
        below.
      parameters:
        - $ref: '#/components/parameters/RunId'
      responses:
        '200':
          description: The run's record.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunDetail'
        '404':
          description: >-
            No such run — an unknown id, or a task id (this route serves r<n>
            only; a task's durable record is GET /agent/v1/tasks/{id}, and the
            error message says so).

  /agent/v1/runs/{id}/pause:
    post:
      operationId: pauseRun
      summary: >-
        (1.2.0) Park a live run at the next STEP BOUNDARY — the cancel seam's
        boundary discipline, never mid-step. r<n> ids only. TWO DISCLOSED
        RUNNER FACTS (contract, fixture-pinned — never surprises): a pause
        AUTO-RESUMES at 300s (MAX_PAUSE — the shared runner clears the flag and
        continues, protecting staged approvals from the 1h TTL reaper;
        lastEvent reads "auto-resumed after 300s"), and a paused run KEEPS the
        one-run-at-a-time slot — other runs/retry/run-now answer 409 for the
        pause's duration.
      parameters:
        - $ref: '#/components/parameters/RunId'
      responses:
        '202':
          description: Pause requested; lands at the next boundary.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  pause:
                    const: requested
                required: [id, pause]
        '404':
          description: No live run of this id.
        '409':
          description: >-
            Not running (already parked/finished); tasks/external have no
            boundary seam.

  /agent/v1/runs/{id}/resume:
    post:
      operationId: resumeRun
      summary: >-
        (1.2.0) Un-park a run. Body ABSENT = plain resume of a PAUSED run
        (inputs on a paused run are refused 409 — a pause declares no form).
        Body {inputs:{...}} = release a durably-suspended INPUT gate, validated
        against the gate's declared inputSchema BEFORE any state change
        (typed, required-checked, ALL errors at once); {} keeps declared
        defaults (continue-without-edit). A time/value predicate gate is
        REFUSED 409 — its predicate resumes it, a human never early-fires it.
        A scheduled entry gate is REFUSED 409 — run-now fires it early.
      parameters:
        - $ref: '#/components/parameters/RunId'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                inputs:
                  type: object
                  additionalProperties: true
      responses:
        '200':
          description: >-
            Resumed — the run continues as a new run_start..run_end segment;
            observers poll getRun.
        '400':
          description: >-
            Inputs failed the gate's typed schema — the FULL violation list,
            state unchanged.
        '404':
          description: No such parked/paused run.
        '409':
          description: >-
            Not resumable in its current state — the refusal names the state.

  /agent/v1/runs/{id}/run-now:
    post:
      operationId: runNow
      summary: >-
        (1.2.0) Early-fire an ARMED entry time-gate's cycle now — 1:1 with the
        desktop's workflow_run_scheduled_now. Scheduled parks only. HEADLESS
        DISCLOSURE (red-team): `conifer serve` runs NO scheduler sweep
        (`sweep_due` is driven by CLI flow commands and the desktop IPC only) —
        a plane-visible scheduled park on a headless Host fires via THIS route
        or its owning scheduler elsewhere (SchedulerOwner); a Host-side sweeper
        is reserved in prose.
      parameters:
        - $ref: '#/components/parameters/RunId'
      responses:
        '200': { description: Cycle dispatched. }
        '404': { description: No such run. }
        '409': { description: Not an armed entry gate (interior gates are predicate- or input-driven). }

  # ------------------------------------------------------------------ lab plane
  /lab/v1/graph:
    get:
      operationId: getGraph
      summary: >-
        The model's execution topology — the node/edge/span schema shared by
        the kernel-lab canvas and conifer lab graph --json.
      responses:
        '200':
          description: The graph.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Graph'

  /lab/v1/parts:
    get:
      operationId: listParts
      summary: >-
        The parts taxonomy — the bindable surface of the resident model: the
        component summaries (every bindable op) and the slot summaries (every
        bindable site, with its oracle/swappable/compatible facts). Read-only.
      responses:
        '200':
          description: The components and slots.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Parts'
        '503':
          description: >-
            No resident model (a stub build, or no model loaded yet) — the lab
            plane needs an engine to read a model's structure.

  /lab/v1/kernels:
    get:
      operationId: listKernels
      summary: >-
        Registered custom kernels. An alias of GET /lab/v1/parts — the same
        parts taxonomy (components + slots), under the kernel-lab name.
      responses:
        '200':
          description: The components and slots.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Parts'
        '503':
          description: No resident model.
    post:
      operationId: registerKernel
      summary: Register a local kernel source for binding.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/KernelRegistration'
      responses:
        '201':
          description: >-
            Registered — the registration report (the parity-gate outcome the
            facade returns) is echoed back.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KernelRegistrationReport'
        '503':
          description: No resident model.

  /lab/v1/ops:
    get:
      operationId: describeOps
      summary: >-
        The op-signature descriptors for the resident model's bindable ops
        (K3c) — the backend-agnostic dispatch ABI, the cli/host twin of the
        GUI's describe_ops. Surfaced verbatim under an `ops` key (the same
        envelope discipline the parts taxonomy uses). Read-only.
      responses:
        '200':
          description: The op descriptors.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ops:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
                      description: >-
                        One op-signature descriptor (shape owned by the
                        engine's describe_ops).
                required: [ops]
        '503':
          description: No resident model.

  /lab/v1/ops/{op}/stub:
    get:
      operationId: getKernelStub
      summary: >-
        The Metal kernel skeleton for one bindable op of the resident model —
        the same stub the GUI emits through kernel_stub. The source rides as a
        JSON string under `source` (with the op and target echoed) so the JSON
        form stays structured and a human surface can print the source
        verbatim. Read-only.
      parameters:
        - $ref: '#/components/parameters/OpName'
      responses:
        '200':
          description: The stub source, with the op and target echoed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  op:
                    type: string
                  target:
                    type: string
                  source:
                    type: string
                required: [op, target, source]
        '404':
          description: >-
            Unknown bindable op — not one of the resident model's ops (a
            user-correctable miss, distinct from the no-model 503).
        '503':
          description: No resident model.

  /lab/v1/bindings:
    post:
      operationId: bindComponents
      summary: Bind a registered kernel to components or a fused span.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BindRequest'
      responses:
        '200':
          description: Bound.
          content:
            application/json:
              schema:
                type: object
                properties:
                  bound:
                    const: true
                required: [bound]
        '503':
          description: No resident model.
    delete:
      operationId: clearBindings
      summary: Clear every binding (back to the built-in kernels).
      responses:
        '200':
          description: Cleared.
          content:
            application/json:
              schema:
                type: object
                properties:
                  cleared:
                    const: true
                required: [cleared]
        '503':
          description: No resident model.

  /lab/v1/parity:
    get:
      operationId: getParityMode
      summary: The current parity enforcement mode.
      responses:
        '200':
          description: The mode.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ParityMode'
        '503':
          description: No resident model.
    put:
      operationId: setParityMode
      summary: >-
        Set the parity enforcement mode (affects future registrations). POST is
        accepted as an alias of PUT.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ParityMode'
      responses:
        '200':
          description: Set; the mode is echoed back.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ParityMode'
        '503':
          description: No resident model.
    post:
      operationId: setParityModeAlias
      summary: Alias of PUT /lab/v1/parity (set the parity enforcement mode).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ParityMode'
      responses:
        '200':
          description: Set; the mode is echoed back.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ParityMode'
        '503':
          description: No resident model.

  /lab/v1/parity/run:
    post:
      operationId: runParity
      summary: >-
        Run the per-kernel oracle parity sweep against the current bindings (the
        CI gate carrier). Executes a forward pass — serializes against /v1
        completions and /agent/v1 turns on the one resident engine.
      responses:
        '200':
          description: The parity report (per-component outcomes).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ParityRunResult'
        '503':
          description: No resident model.

  /lab/v1/validate:
    post:
      operationId: validateConfig
      summary: >-
        Independently validate an on-disk Config envelope against its GGUF;
        returns the staged ValidationReport (signature, then safety, then parity
        findings). A static structural check (no decode loop).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ValidateRequest'
      responses:
        '200':
          description: The staged report.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationReport'
        '503':
          description: No resident model.

  /lab/v1/bench:
    post:
      operationId: benchKernel
      summary: >-
        Benchmark the bound kernels against the built-in implementation,
        returning the tok/s delta. Executes a forward pass — serializes against
        /v1 completions and /agent/v1 turns on the one resident engine.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BenchRequest'
      responses:
        '200':
          description: The bench delta.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BenchResult'
        '503':
          description: No resident model.

  /lab/v1/configs:
    get:
      operationId: listConfigs
      summary: Saved Config envelope names.
      responses:
        '200':
          description: The Config names.
          content:
            application/json:
              schema:
                type: array
                items:
                  type: string
    post:
      operationId: saveConfig
      summary: >-
        Save the currently-bound model's Config envelope under a name (writes
        configs/{name}.json — the binding manifest, arch, and a weights ref
        pinning the GGUF by hash).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SaveConfigRequest'
      responses:
        '201':
          description: Saved.
          content:
            application/json:
              schema:
                type: object
                properties:
                  name:
                    type: string
                  saved:
                    const: true
                required: [name, saved]
        '400':
          description: An unsafe Config name (path traversal, empty, or slash-bearing).
        '503':
          description: No resident model.

  /lab/v1/configs/{name}:
    get:
      operationId: getConfig
      summary: Fetch a saved Config envelope's canonical JSON.
      parameters:
        - $ref: '#/components/parameters/ConfigName'
      responses:
        '200':
          description: The canonical Config envelope (shape owned by the engine).
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        '400':
          description: An unsafe Config name.
        '404':
          description: No Config of this name.
        '503':
          description: No resident model.

  # -------------------------------------------------------------- control plane
  /host/v1/info:
    get:
      operationId: hostInfo
      summary: >-
        The Host's identity card: protocol version, capabilities, enforcement
        tier, loaded model, planner disclosure, plus completions_served (the
        running count of completions this Host has answered — clients observe one
        resident model serving all). Wire shape pinned by
        substrate-info.fixtures.json (extended additively with completions_served).
      responses:
        '200':
          description: The info card.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubstrateInfo'

  /host/v1/health:
    get:
      operationId: hostHealth
      summary: Cheap reachability check; reports the served model id.
      responses:
        '200':
          description: Healthy; reports the served model id.
          content:
            application/json:
              schema:
                type: object
                properties:
                  model:
                    type: string
                required: [model]
                additionalProperties: true

  /host/v1/telemetry:
    get:
      operationId: hostTelemetry
      summary: >-
        The observation plane's totals since startup (ONE-PLATFORM invariant 3
        — telemetry is a plane resource, not an IPC struct): the served-work
        counters (completions, tasks, workflow runs) and the MEASURED token
        sums. Honesty rule: the token block covers exactly the completions
        that reported usage (measured_completions is the denominator;
        coverage names the measured surface) — streamed completions and agent
        turns measure no usage on this Host, so they are counted in served and
        absent from tokens, never padded with fake zeros. No cost field: this
        Host settles no money, so it discloses none.
      responses:
        '200':
          description: Work + token totals since startup.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HostTelemetry'

  /host/v1/sessions:
    get:
      operationId: hostSessions
      summary: >-
        (P2, SM2-INGRESS) The served-sessions feed — the desktop board's
        federation input: this Host's own open store sessions plus its live and
        recently-terminal turns (newest-first, terminal turns held a 10-minute
        window, turns capped at 200). Bearer-gated; sight, never command (verbs
        ride POST /agent/v1/sessions/{id}/*). Sessions/turns carry the store's
        own wire shapes; unknown fields are tolerated (additive evolution).
      responses:
        '200':
          description: The served-sessions feed (contract 1).
          content:
            application/json:
              schema:
                type: object
                properties:
                  contract:
                    const: 1
                  endpoint_id:
                    type: string
                  sessions:
                    type: array
                    items: { type: object, additionalProperties: true }
                  turns:
                    type: array
                    items: { type: object, additionalProperties: true }
                required: [contract, endpoint_id, sessions, turns]
                additionalProperties: true
        '401':
          description: Missing/wrong bearer.

  /host/v1/served:
    get:
      operationId: hostServed
      summary: >-
        (P3, SMC3-CTRL) The capability descriptor — the adopt-and-disclose input
        (§1.1, §4.4, §4.6): the verbs this daemon honors over the wire, the bind
        fact (loopback|lan), the control_port the control planes answer on, and
        whether an operator credential was provisioned (a BOOLEAN, never the
        token). offering.models lists the exposed suite with each member's kind.
        Bearer-gated; sight, never command (§13.2). Wire shape pinned by
        wire_v1/served_descriptor.json.
      responses:
        '200':
          description: The capability descriptor (contract 1).
          content:
            application/json:
              schema:
                type: object
                properties:
                  contract:
                    const: 1
                  endpoint_id:
                    type: string
                  sessions_plane:
                    type: boolean
                  verbs:
                    type: array
                    items: { type: string }
                  bind:
                    type: string
                    enum: [loopback, lan]
                  control_port:
                    type: integer
                  operator_credential:
                    type: boolean
                  offering:
                    type: object
                    properties:
                      models:
                        type: array
                        items:
                          type: object
                          properties:
                            id: { type: string }
                            kind: { type: string }
                          required: [id, kind]
                    required: [models]
                required:
                  [contract, endpoint_id, sessions_plane, verbs, bind,
                   control_port, operator_credential, offering]
                additionalProperties: true
        '401':
          description: Missing/wrong bearer.

  /host/v1/harnesses:
    get:
      operationId: hostHarnesses
      summary: >-
        (P3, SMC3-CTRL) The served harness registry (§1.6, §3.4) — one row per
        the Rust harness::REGISTRY entry (the ONE authority), shaped to the TS
        HarnessSpec so the desktop's harness-registry becomes a fetch with the
        current table as its cached fallback. Bearer-gated; sight-only. Wire
        shape pinned by wire_v1/harnesses.json.
      responses:
        '200':
          description: The harness registry (contract 1).
          content:
            application/json:
              schema:
                type: object
                properties:
                  contract:
                    const: 1
                  harnesses:
                    type: array
                    items: { type: object, additionalProperties: true }
                required: [contract, harnesses]
                additionalProperties: true
        '401':
          description: Missing/wrong bearer.

  /host/v1/peers:
    get:
      operationId: hostPeersList
      summary: >-
        (P5, §13.1) List the minted per-peer serve tokens — SECRET-FREE: each
        row carries the name, mint time, and revoked bit, NEVER the token.
        Operator-gated (which peers exist is control disclosure, §13.2);
        control-socket-mounted (loopback only).
      responses:
        '200':
          description: The per-peer token roster (secret-free).
          content:
            application/json:
              schema:
                type: object
                properties:
                  peers:
                    type: array
                    items: { type: object, additionalProperties: true }
                required: [peers]
                additionalProperties: true
        '401':
          description: Missing/wrong bearer or operator credential.
    post:
      operationId: hostPeerMint
      summary: >-
        (P5, §13.1) Mint a NAMED per-peer serve token — the two-tier scheme's
        second tier beside the operator credential. Returns the token ONCE
        (never re-readable). Operator-gated; control-socket-mounted.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
              required: [name]
      responses:
        '200':
          description: The minted token (returned once).
          content:
            application/json:
              schema:
                type: object
                properties:
                  name: { type: string }
                  token: { type: string }
                required: [name, token]
                additionalProperties: true
        '400':
          description: Empty name or non-durable host.
        '409':
          description: A live token already exists under this name.
        '401':
          description: Missing/wrong bearer or operator credential.

  /host/v1/peers/{name}/revoke:
    post:
      operationId: hostPeerRevoke
      summary: >-
        (P5, §13.1, §10.2 the scalpel) Revoke ONE peer's token — denies every
        hash minted under the name in the durable deny-set and journals the act;
        a revoked token 403s pre-bind thereafter. Operator-gated;
        control-socket-mounted. Response is the ControlAct face
        ({verb,target,by,outcome[,reason]}).
      parameters:
        - name: name
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The revoke ControlAct outcome.
          content:
            application/json:
              schema:
                type: object
                properties:
                  verb: { type: string }
                  target: { type: string }
                  by: { type: string }
                  outcome: { type: string }
                  reason: { type: string }
                required: [verb, target, by, outcome]
                additionalProperties: true
        '404':
          description: Unknown peer name.
        '409':
          description: The revoke was refused (not applied).
        '401':
          description: Missing/wrong bearer or operator credential.

  # ------------------------------------------------------ observe plane (1.3.0)
  # Team Pulse — the queryable observability surface. Read-only BY CONSTRUCTION:
  # enrollment lives OFF the plane (the desktop/CLI write observe-roots.json), so
  # no write verb mounts here. The store is engine-enforced read-only. The plane
  # is BEARER-REQUIRED even on a loopback bind (owner decision O2) — git-activity
  # metadata must not be readable by any local process. Every response is the ONE
  # envelope {data, meta, error}. Fails CLOSED: unknown field/measure carry
  # did_you_mean; a query before the first sweep completes is 503 NOT_SYNCED.
  # The async 202/poll/cancel door is contract-RESERVED (owner O3) — documented
  # in observe-conventions.md, deliberately NOT mounted in 1.3.0.
  /observe/v1/surfaces:
    get:
      operationId: observeSurfaces
      summary: >-
        The observability surface catalog — one row per queryable surface (git
        today; host/router/chat reserved), each with its title, description, and
        verified-question names. Read-only; bearer-required.
      responses:
        '200':
          description: The surface catalog in the observe envelope.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ObserveEnvelope'
        '401':
          description: Missing/wrong bearer, or no serve token configured (O2).
        '403':
          description: The observe plane requires a serve token (O2 fail-closed).

  /observe/v1/surfaces/{surface}/schema:
    get:
      operationId: observeSchema
      summary: >-
        The schema-discovery document for a surface (<4KB, model-facing): its
        measures and dimensions with descriptions, types, trust annotations
        (external = attacker-writable), and verified-question names. This
        document — not the model — is the accuracy lever; call it first.
      parameters:
        - name: surface
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The surface's schema-discovery document in the envelope.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ObserveEnvelope'
        '404':
          description: Unknown surface (error.code = UNKNOWN_SURFACE, did_you_mean set).
        '401':
          description: Missing/wrong bearer, or no serve token configured (O2).

  /observe/v1/query:
    post:
      operationId: observeQuery
      summary: >-
        Run a typed DSL query against a surface. Body carries the surface name
        and a typed query (measures / dimensions / filters / time_range /
        bucket / order / limit) — NEVER a query-language string. The filter
        grammar is a typed recursive union (broker-inspectable). Compiled to
        parameterized SQL; the compiled query is echoed in meta.compiled_query.
        Fails CLOSED (UNKNOWN_FIELD / UNKNOWN_MEASURE carry did_you_mean).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                surface:
                  type: string
                  description: The surface to query (default "git").
                query:
                  type: object
                  additionalProperties: true
                  description: The typed DSL query (see observe-conventions.md).
              required: [query]
      responses:
        '200':
          description: The query result rows in the observe envelope.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ObserveEnvelope'
        '400':
          description: >-
            Invalid query — UNKNOWN_FIELD / UNKNOWN_MEASURE / INVALID_FILTER /
            RANGE_TOO_LARGE (error.code set; did_you_mean set where applicable).
        '404':
          description: Unknown surface (error.code = UNKNOWN_SURFACE).
        '503':
          description: NOT_SYNCED — the first sweep has not completed yet.
        '401':
          description: Missing/wrong bearer, or no serve token configured (O2).

  /observe/v1/questions/{name}:
    post:
      operationId: observeQuestion
      summary: >-
        Run a verified parameterized question (Genie trusted-asset pattern) —
        the named fast-path the UI drill-ins and the model's observe_question
        tool both call, so a human view and an agent answer are provably the
        same data. Response carries meta.verified = true. v1 questions:
        team_activity, whats_merged_since, contributor_summary.
      parameters:
        - name: name
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                surface:
                  type: string
                  description: The surface (default "git").
                params:
                  type: object
                  additionalProperties: true
                  description: Typed question parameters (e.g. since, repo, identity).
      responses:
        '200':
          description: The verified question result in the envelope (verified = true).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ObserveEnvelope'
        '400':
          description: Missing/mistyped params (error.code = INVALID_FILTER).
        '404':
          description: >-
            Unknown surface or question (UNKNOWN_SURFACE / UNKNOWN_QUESTION;
            did_you_mean lists the surface's questions).
        '503':
          description: NOT_SYNCED — the first sweep has not completed yet.
        '401':
          description: Missing/wrong bearer, or no serve token configured (O2).

  # Overstory Phase B (additive to the 1.3.0 contract): the /canopy/v1 plane —
  # the metaharness's queryable nouns over HTTP, so a remote client / the desktop
  # tile reads exactly what the `conifer canopy` verbs read. READ-ONLY: stateless
  # queries over $CONIFER_HOME, reusing the same canopy functions, returning the
  # same Envelope{data,meta,error} shape. Bearer-gated by the ahead-of-routes
  # gate; fails CLOSED without a serve token (the observe-plane twin, O2). The
  # routes are added additively and consume no version slot of their own —
  # 1.4.0 belongs to the Flight Recorder Phase-1 mount (the /recorder/v1 plane
  # below, recorder-contract.md). The formal canopy version slot is assigned when the
  # write/drive routes (POST /canopy/v1/run, /mcp/operator/v1) mount — those need
  # the pump as a persistent server resource and coordinate against that
  # reservation. This read slice is stateless and reuses shipped functions.
  /canopy/v1/instances:
    get:
      operationId: canopyInstances
      summary: >-
        The harness-instance registry — the named, credential-bound instances the
        operator may place work on (secret-free by construction; a credential ref
        is a scheme-prefixed name, never a literal). The same rows `conifer canopy
        instances --json` returns.
      responses:
        '200':
          description: '{version, instances[]} in the canopy envelope.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CanopyEnvelope'
        '401':
          description: Missing/wrong bearer.
        '500':
          description: The registry is corrupt (a malfunction, not a refusal).

  /canopy/v1/tasks:
    get:
      operationId: canopyTasks
      summary: >-
        The task records — every placed leg's durable record (running / settled /
        killed / orphaned), the same rows `conifer canopy tasks --json` returns. A
        record exists from the moment a leg is spawned, so a crashed supervisor's
        task is still listable.
      responses:
        '200':
          description: An array of TaskRecords in the canopy envelope.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CanopyEnvelope'
        '401':
          description: Missing/wrong bearer.

  /canopy/v1/tasks/{id}/events:
    get:
      operationId: canopyTaskEvents
      summary: >-
        One task's CanopyEvent stream, replayed from its events.ndjson — the
        `task_started → leg_event* → leg_settled → task_settled` sequence (each
        leg_event wraps a verbatim TurnEvent). The same bytes `conifer canopy
        events <id>` replays.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: '{task, events[]} in the canopy envelope.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CanopyEnvelope'
        '404':
          description: No task with that id.
        '401':
          description: Missing/wrong bearer.

  /canopy/v1/plan:
    post:
      operationId: canopyPlan
      summary: >-
        A DRY RUN of the placement ladder — answers "would this task be admitted,
        and if not exactly why?" without spawning or writing anything (place() is
        a pure decision; it blocks only on a loopback proxy probe). The same
        ordered refusal ladder `conifer canopy run` walks: budget → instance →
        routable → credential → capability → capacity → proxy. The first canopy
        write-family verb; the real spawn (POST /canopy/v1/run) is a later slice
        that needs the persistent Host pump.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CanopyPlanRequest'
      responses:
        '200':
          description: >-
            The decision in the canopy envelope. An ADMITTED plan carries
            `{admitted: true, instance, harness, port}`; a REFUSAL rides as
            `{data: null, meta.refusal: true, error: {code, message}}` — a
            refusal is an ANSWER, not a transport error, so the status stays 200
            (a client must not retry-storm on "at capacity").
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CanopyEnvelope'
        '400':
          description: Malformed request body.
        '401':
          description: Missing/wrong bearer.
        '500':
          description: >-
            A structural malfunction (a corrupt registry, a jail that escapes
            $CONIFER_HOME) — loud, never typed away as a refusal.

  /canopy/v1/run:
    post:
      operationId: canopyRun
      summary: >-
        PLACE AND SPAWN a real leg — the operator's core action over HTTP. Runs
        the same placement ladder as /plan and, on admission, spawns the agent
        leg (actor "agent"), hands it to the Host's persistent pump to drive to a
        clean settle, and returns the task id at once. The client then polls
        GET /canopy/v1/tasks/{id}/events. CAPABILITY-BEARING: it spawns a process
        with a caller-supplied goal and cwd, so it rides the SAME fail-closed,
        bearer-required, loopback-control-socket gate as the rest of /canopy/v1 —
        no broader than `conifer canopy run` grants a local user, and never
        reachable from the LAN inference socket.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CanopyRunRequest'
      responses:
        '200':
          description: >-
            On admission, a 200 ok envelope `{task, disposition: "running",
            instance}` — the task record is written at spawn, so the id is
            immediately queryable via the tasks/events routes. A placement
            REFUSAL rides as the same 200 refused envelope as /plan (data null,
            meta.refusal, the typed code).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CanopyEnvelope'
        '400':
          description: >-
            A malformed body, a missing goal/instance/cwd, a non-existent cwd, or
            a negative budget.
        '401':
          description: Missing/wrong bearer.
        '500':
          description: >-
            A structural malfunction (a corrupt registry, a spawn failure) —
            loud, never typed away as a refusal.

  # ----------------------------------------------------- recorder plane (1.4.0)
  # Flight Recorder — the write/control family (recorder-contract.md §3–§7).
  # Four nouns, no read noun EVER (reads ride /observe/v1 surface `transcript`,
  # §13 — one door per direction). Every route is bearer-gated FAIL-CLOSED on
  # loopback including the GETs (T-O4: with no serve token configured the family
  # answers 403 — transcript content must not be readable or writable by any
  # local process that can reach loopback). Ingest additionally requires the
  # per-rack write token in x-conifer-recorder-write (P0-3: minted/rotated by
  # the armed PUT, hash-stored, shown once). Every response is the ONE
  # RecorderEnvelope; the error codes are the closed §6 enum. The
  # recorder-acts / recorder-events / recorder-states fixture files are
  # normative for this family.
  /recorder/v1/racks/{rack_id}:
    put:
      operationId: recorderArm
      summary: >-
        Arm/disarm the per-rack recording grant (the literal-opt-in law:
        armed is the string "true" or "false", nothing else), set retention
        policy, and — on an armed PUT — mint/rotate the per-rack write token
        (returned exactly once in data.write_token; the Host stores only its
        hash). Disarm keeps the store and history (purge is DELETE's job).
        The response's data.consent_line is the exact flip-time consent copy
        the UI MUST render — served by the plane so the words are
        contract-pinned, never client-drifted.
      parameters:
        - $ref: '#/components/parameters/RackId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RecorderGrantPut'
      responses:
        '200':
          description: >-
            The grant fact (RecorderGrant in the envelope's data) —
            data.write_token present iff this PUT minted/rotated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecorderEnvelope'
        '400':
          description: >-
            INVALID_GRANT_LITERAL (armed is not the literal "true"/"false") /
            INVALID_POLICY (bounds, or policy on a disarm) / INVALID_BODY
            (unknown key named, credential-shaped key anywhere, malformed
            rack_id) — all violations at once; the grant is untouched.
        '503':
          description: >-
            STORE_LOCKED (another Host holds the per-store writer lock, P1-10)
            / RECORDER_BROKEN (keychain unwrap, SQLCipher open, or redaction
            registry failure at arm time — P1-5: a grant is never armed into a
            broken recorder silently).
        '401':
          description: Missing/wrong bearer (constant-time, 401 before 404).
        '403':
          description: No serve token configured — the family fails closed (T-O4).
    # The deletion duty verb (recorderPurge) mounts on the /history sub-path
    # (its own path item below) — the bare rack noun serves PUT only.
  /recorder/v1/racks:
    get:
      operationId: recorderGrants
      summary: >-
        Grants review — one row per rack with a live grant OR a surviving
        store (a disarmed-but-unpurged rack stays visible: the review surface
        shows data-at-rest honestly until purged). Each row carries armed,
        policy, health (the REC-chip truth source: recording | degraded |
        broken | off, with broken_reason), size, turn/gap counts, and
        oldest/newest/last-ingest timestamps. NEVER returns the write token
        or its hash. An empty population is data.racks: [], never a 404.
      responses:
        '200':
          description: The RecorderRackList in the envelope's data.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecorderEnvelope'
        '401':
          description: Missing/wrong bearer (constant-time, 401 before 404).
        '403':
          description: No serve token configured — the family fails closed (T-O4).
  /recorder/v1/ingest:
    post:
      operationId: recorderIngest
      summary: >-
        The write-token-gated capture ingress (proxy → Host): one rack per
        request, up to 256 turn/gap events, idempotent per event via
        event_id dedupe. Gate order (§4.2): bearer → the per-rack token in
        x-conifer-recorder-write verified constant-time against EVERY stored
        grant hash → only then is the body read (4 MiB cap — 413 on declared
        or observed size, refused never truncated) → per-event validation
        (a failing event is a per-event INVALID_EVENT in data.refused[],
        siblings unaffected). Secret redaction runs Host-side before first
        write; a broken redactor converts events to typed gaps
        (data.gapped[]), never content (P0-1 fail-closed).
      parameters:
        - name: x-conifer-recorder-write
          in: header
          required: true
          schema:
            type: string
          description: >-
            The per-rack write token the armed PUT minted (43-char base64url
            of 256 bits). Missing, wrong, and rotated tokens all draw the SAME
            constant-time 403 WRITE_TOKEN_INVALID — no oracle within the
            class; the body is never parsed.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RecorderIngestRequest'
      responses:
        '200':
          description: >-
            Per-event outcomes (RecorderIngestResult in the envelope's data:
            accepted / deduped / gapped[] / refused[] — refused events carry
            the per-event INVALID_EVENT code).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecorderEnvelope'
        '400':
          description: >-
            INVALID_BODY — not a JSON object, unknown key (named), events over
            maxItems, or a credential-shaped key anywhere (whole request
            refused, nothing written).
        '403':
          description: >-
            WRITE_TOKEN_INVALID (missing/wrong/rotated token — body unread) /
            NOT_ARMED (token verified but the grant was disarmed meanwhile) /
            the T-O4 no-token-configured fail-closed refusal. Nothing written.
        '413':
          description: >-
            PAYLOAD_TOO_LARGE — the body exceeds the 4 MiB cap (declared or
            observed); refused whole, never truncated.
        '503':
          description: STORE_LOCKED / RECORDER_BROKEN (typed; nothing written).
        '401':
          description: Missing/wrong bearer (constant-time, 401 before 404).
  /recorder/v1/racks/{rack_id}/history:
    delete:
      operationId: recorderPurge
      summary: >-
        The deletion duty verb — one transaction of intent, in order:
        invalidate the write token, discard the per-rack keychain key
        (crypto-shred, provable under T-O2), rm the store file (+ WAL/SHM),
        journal the act. Grant state is untouched (disarm is PUT's job).
        Response data is the RecorderPurgeAct (the ControlAct wire face):
        outcome ok with bytes_freed + key_discarded, or the idempotent
        refused/"empty" terminal on a repeat; a rack with neither grant nor
        store is 404 UNKNOWN_RACK with did_you_mean (nearest granted
        rack_ids). Actor label (data.by) follows the store-verb convention:
        "operator" when the operator credential authorized, else "bearer".
      parameters:
        - $ref: '#/components/parameters/RackId'
      responses:
        '200':
          description: >-
            The RecorderPurgeAct in the envelope's data (ok, or the idempotent
            refused/"empty" terminal).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecorderEnvelope'
        '404':
          description: UNKNOWN_RACK — no grant and no store; did_you_mean set.
        '503':
          description: STORE_LOCKED — another Host holds the writer lock (P1-10).
        '401':
          description: Missing/wrong bearer (constant-time, 401 before 404).
        '403':
          description: No serve token configured — the family fails closed (T-O4).

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        Required on EVERY route — with the ONE /host/v1/health liveness
        carve-out — whenever a bearer token is configured; fail-closed (401 +
        WWW-Authenticate on any missing/wrong token, constant-time compare,
        no detail). A LAN bind (--bind lan) REFUSES to start without a token;
        the loopback default with no token is open, plus the Host-header
        guard at ingress. The token is a local secret, resolved
        registry-first: --auth-token flag > CONIFER_SERVE_AUTH_TOKEN env >
        the host:local ServeConfig's keychain ref — never a cloud
        credential.
        The "ONE" carve-out is the sole IN-CONTRACT exemption; the
        facade-level bare GET /health is an ADDITIONAL liveness carve-out the
        bearer gate also takes (src/substrate/server.rs), deliberately outside
        the versioned native-planes contract (declared in x-facade-routes) and
        so not counted among the versioned routes here.
  parameters:
    PendingId:
      name: id
      in: path
      required: true
      schema:
        type: string
    AgentName:
      name: name
      in: path
      required: true
      schema:
        type: string
    WorkflowName:
      name: name
      in: path
      required: true
      schema:
        type: string
    ConfigName:
      name: name
      in: path
      required: true
      schema:
        type: string
    OpName:
      name: op
      in: path
      required: true
      schema:
        type: string
      description: >-
        A bindable op name — one of the resident model's op-signature
        descriptors (GET /lab/v1/ops).
    SessionId:
      name: id
      in: path
      required: true
      schema:
        type: string
      description: >-
        A session id — the plane-minted task id (t<n>) or workflow-run id
        (r<n>), exactly as the task stream's start.task_id / the run stream's
        run_start.run_id carried it.
    RunId:
      name: id
      in: path
      required: true
      schema:
        type: string
      description: >-
        A workflow-run id — r<n> (Host-minted) or r<nanos> (the CLI's
        restart-stable suspend-capable mint). Task ids (t<n>) are refused with
        an honest 404 — tasks journal on their OWN route (GET /agent/v1/tasks/{id});
        this route serves r<n> only (SPEC §4.5: spared, not widened).
    TaskId:
      name: id
      in: path
      required: true
      schema:
        type: string
      description: >-
        (1.2.0, CLI-7) A task id — the t family the task stream's
        start.task_id carried (t<nanos><n>, restart-stable: a journaled task's
        durable id must not collide across Host restarts). Run ids (r<n>) are
        refused with an honest 404 — GET /agent/v1/runs/{id} serves those.
    ExternalSessionId:
      name: id
      in: path
      required: true
      schema:
        type: string
      description: >-
        (1.2.0, CLI-6) An external session id — the x family minted by
        POST /agent/v1/sessions (x<nanos><rand>, random and restart-stable).
        Internal ids (t<n>/r<n>) are refused: their liveness is Host-observed,
        never self-reported.
    RackId:
      name: rack_id
      in: path
      required: true
      schema:
        type: string
        pattern: '^rack_[a-z0-9-]{1,64}$'
      description: >-
        (1.4.0) A rack id — the typed `rack_` prefix, uniform with the
        trn_/msg_ read-side prefixes (recorder-contract.md §5.1); the
        desktop's rack ids are minted to fit. A malformed id is a 400
        INVALID_BODY naming rack_id (it is also a store FILENAME, so the
        shape law is load-bearing, never advisory).

  schemas:
    # ----- the observe plane (Team Pulse — 1.3.0) -----------------------------
    ObserveEnvelope:
      type: object
      description: >-
        The ONE envelope every /observe/v1 route returns. `data` is the route's
        payload (a surface list, a schema document, or query rows) or null on
        error. `meta` carries surface, row_count, truncated, freshness
        (synced_at + schema_version), the echoed query, the compiled query
        (Genie-style audit), and verified. `error` is null on success or a
        closed-code object on failure. Grows additively — a consumer tolerates
        unknown meta fields.
      properties:
        data:
          description: The payload, or null on error.
        meta:
          type: object
          additionalProperties: true
          properties:
            surface: { type: string }
            row_count: { type: integer }
            truncated: { type: boolean }
            verified: { type: boolean }
            compiled_query: { type: string }
            query_echo: { type: object, additionalProperties: true }
            freshness:
              type: object
              additionalProperties: true
              properties:
                synced_at: { type: [integer, 'null'] }
                schema_version: { type: integer }
        error:
          type: [object, 'null']
          properties:
            code:
              type: string
              description: >-
                Closed enum: UNKNOWN_SURFACE, UNKNOWN_FIELD, UNKNOWN_MEASURE,
                UNKNOWN_QUESTION, INVALID_FILTER, INVALID_CURSOR,
                RANGE_TOO_LARGE, LIMIT_EXCEEDED, NOT_WATCHED, NOT_SYNCED,
                EGRESS_BLOCKED, TIMEOUT, CANCELLED, INTERNAL.
            message: { type: string }
            hint: { type: string }
            did_you_mean:
              type: array
              items: { type: string }
          required: [code, message]
      required: [data, meta, error]
    # ----- the /canopy/v1 plane (Phase B) -------------------------------------
    CanopyEnvelope:
      type: object
      description: >-
        The ONE envelope every /canopy/v1 route returns — the serialized
        `canopy::protocol::Envelope`. `data` is the route's payload (an instances
        object, a TaskRecord array, or a task's events) or absent on error;
        `meta.refusal` is true when the payload is a typed refusal (data absent,
        the pinned sentence in `error`); `error` carries the stable code + human
        message. The SAME shape the operator's MCP toolset returns, so a consumer
        written against `conifer canopy`, this plane, or a tool call reads one
        contract. Grows additively.
      properties:
        data:
          description: The payload, or absent on error/refusal.
        meta:
          type: object
          additionalProperties: true
          properties:
            refusal: { type: boolean }
        error:
          type: [object, 'null']
          properties:
            code: { type: string }
            message: { type: string }
          required: [code, message]
      required: [meta]
    CanopyPlanRequest:
      type: object
      description: >-
        A placement dry-run request. `instance` names the harness instance to
        place on. The budget is REQUIRED (the metering law) as EITHER
        `budget_nanousd` (integer nanodollars, wins if present) OR `budget_usd`
        (human dollars, converted identically to `conifer canopy run`); omitting
        both yields the `budget_missing` refusal. `port` is the loopback proxy
        port the ladder's last rung probes (default 8402).
      properties:
        instance:
          type: string
        budget_usd:
          type: number
        budget_nanousd:
          type: integer
          format: int64
        port:
          type: integer
      required: [instance]
    CanopyRunRequest:
      type: object
      description: >-
        A place-and-spawn request. `goal` is the leg's mission, `instance` the
        harness instance to place on, `cwd` the (existing) directory the agent
        runs in — all three required. The budget follows CanopyPlanRequest's
        rules (nanodollars wins, else dollars; required by the ladder).
        `wall_clock_secs` (default 1800) and `max_turns` (default 12) bound the
        leg; `port` is the loopback proxy port (default 8402).
      properties:
        goal:
          type: string
        instance:
          type: string
        cwd:
          type: string
        budget_usd:
          type: number
        budget_nanousd:
          type: integer
          format: int64
        wall_clock_secs:
          type: integer
        max_turns:
          type: integer
        port:
          type: integer
      required: [goal, instance, cwd]
    # ----- the recorder plane (Flight Recorder — 1.4.0) -----------------------
    # Lifted VERBATIM from contracts/recorder-contract.md §5.0–§5.4 (the
    # NORMATIVE home); every request schema is additionalProperties: false
    # (fail-closed boundary) and a credential-shaped key anywhere in a request
    # refuses the WHOLE request.
    RecorderEnvelope:
      type: object
      description: >-
        The ONE envelope every /recorder/v1 route returns — the ObserveEnvelope
        discipline applied to the recorder family. data is the route's payload or
        null on error; meta carries plane facts; error is null on success or a
        closed-code object. Grows additively; consumers tolerate unknown meta keys.
      properties:
        data: { description: The payload, or null on error. }
        meta:
          type: object
          additionalProperties: true
          properties:
            rack_id: { type: string }
            schema_version: { type: integer }
        error:
          type: [object, 'null']
          properties:
            code:
              type: string
              description: >-
                Closed enum (§6): UNKNOWN_RACK, INVALID_GRANT_LITERAL,
                INVALID_POLICY, INVALID_BODY, NOT_ARMED, WRITE_TOKEN_INVALID,
                INVALID_EVENT, PAYLOAD_TOO_LARGE, STORE_LOCKED, RECORDER_BROKEN,
                INTERNAL.
            message: { type: string }
            hint: { type: string }
            did_you_mean:
              type: array
              items: { type: string }
          required: [code, message]
      required: [data, meta, error]
    RecorderGrantPut:
      type: object
      additionalProperties: false
      properties:
        armed:
          type: string
          enum: ['true', 'false']
          description: >-
            The literal-opt-in law (PHILOSOPHY rule 14): the STORED grant fact arms
            only on the literal string "true". On this API verb a non-literal value
            ("yes", true-as-boolean, 1) is a 400 INVALID_GRANT_LITERAL and the
            grant is UNTOUCHED — an API refusal is more honest than a silent
            parse-to-OFF (fail-closed schema boundary; the rule-14 garbage-parses-
            to-OFF clause governs the persisted marker file, not this verb).
        policy:
          type: object
          additionalProperties: false
          properties:
            days:      { type: integer, minimum: 1, maximum: 365 }
            max_bytes: { type: integer, minimum: 67108864, maximum: 34359738368 }
          description: >-
            Retention bounds. Defaults are the T-O1 owner cell — the packet anchors
            on 30d / 2 GiB recommended (red-team P1-9) with 7d / 512 MiB as the
            conservative option; the shipped default is a named contract constant
            pinned by fixture. Omitted ⇒ defaults; present on a disarm ⇒ 400
            INVALID_POLICY (policy attaches to an armed grant).
        tool_io_full:
          type: string
          enum: ['true', 'false']
          description: >-
            (P1-7 graft) Separate per-rack literal opt-in for whole tool bodies.
            Default absent ⇒ tool_result parts are capped per part (cap value is a
            contract constant in recorder-contract.md, T-O6 cell; truncated parts
            carry truncated: true). Same literal law as armed.
      required: [armed]
    RecorderGrant:
      type: object
      properties:
        rack_id: { type: string }
        armed:   { type: boolean }
        policy:
          type: object
          properties:
            days: { type: integer }
            max_bytes: { type: integer }
        write_token:
          type: string
          description: >-
            PRESENT ONLY when this PUT minted/rotated (i.e. armed:"true"). Shown
            once, never retrievable again (GET never returns it); the Host stores
            only the hash. 43-char base64url of 256 random bits.
        token_rotated: { type: boolean }
        armed_ms:  { type: [integer, 'null'], description: Unix ms of the arm; null when disarmed. }
        consent_line:
          type: string
          description: >-
            The exact one-sentence consent copy the UI MUST render at flip time —
            served by the plane so copy is contract-pinned, not client-drifted
            (marketing/consent copy is an owner-gated cell of the packet).
      required: [rack_id, armed, policy]
    RecorderRackList:
      type: object
      properties:
        racks:
          type: array
          items:
            type: object
            properties:
              rack_id:   { type: string }
              armed:     { type: boolean }
              policy:
                type: object
                properties:
                  days: { type: integer }
                  max_bytes: { type: integer }
              health:
                type: string
                enum: [recording, degraded, broken, off]
                description: >-
                  The REC-chip truth source (the chip renders FROM this row).
                  degraded carries dropped/gapped counts in the sibling fields;
                  broken carries broken_reason.
              broken_reason:
                type: string
                enum: [redactor_unavailable, keychain_unavailable, store_locked,
                       store_open_failed, disk_low]
                description: Present only when health = broken (P0-1 chip-demotion law).
              size_bytes:      { type: integer }
              turns:           { type: integer }
              gaps:            { type: integer }
              oldest_ts_ms:    { type: [integer, 'null'] }
              newest_ts_ms:    { type: [integer, 'null'] }
              last_ingest_ms:  { type: [integer, 'null'] }
              armed_ms:        { type: [integer, 'null'] }
            required: [rack_id, armed, policy, health, size_bytes]
      required: [racks]
    RecorderIngestRequest:
      type: object
      additionalProperties: false
      properties:
        rack_id:     { type: string }
        session_key: { type: string, description: The proxy session x-id (launch-handoff identity graft). }
        events:
          type: array
          maxItems: 256
          items: { $ref: '#/components/schemas/RecorderEvent' }
      required: [rack_id, session_key, events]
    RecorderEvent:
      oneOf:
        - $ref: '#/components/schemas/RecorderTurnEvent'
        - $ref: '#/components/schemas/RecorderGapEvent'
      discriminator: { propertyName: kind }
    RecorderGapEvent:
      type: object
      additionalProperties: false
      properties:
        kind:     { type: string, enum: [gap] }
        event_id: { type: string, description: 'Proxy-minted evt_<nanos><rand> — the idempotency key.' }
        seq:      { type: integer, description: Per-session_key monotonic capture sequence. }
        ts_ms:    { type: integer }
        gap:
          type: string
          enum: [dropped_queue_full, recording_off, proxy_restart, host_offline,
                 prefix_reset]
          description: >-
            Proxy-mintable gap kinds. evicted and redactor_unavailable are
            HOST-minted gap kinds (they appear in the store and on the read
            surface, never on this ingress) — the closed read-side gap enum is the
            union; each kind is listed beside its ONE emitter in
            recorder-contract.md.
        seq_from: { type: integer }
        seq_to:   { type: integer }
      required: [kind, event_id, seq, ts_ms, gap]
    RecorderTurnEvent:
      type: object
      additionalProperties: false
      description: >-
        One captured turn — the parsed wire struct's projection, dialect-tagged.
        STRUCTURALLY header-free: there is no field a request header can occupy;
        an object carrying a credential-shaped key (authorization, x-api-key,
        cookie, set-cookie) anywhere refuses the WHOLE request (INVALID_BODY,
        nothing written) — the byte-checked conformance gate's wire twin.
        Secret redaction runs HOST-SIDE at TAPE ingress before first write
        (P0-1 fail-closed: redactor down ⇒ gap, never the unredacted event).
      properties:
        kind:       { type: string, enum: [turn] }
        event_id:   { type: string }
        seq:        { type: integer }
        ts_ms:      { type: integer }
        operation:  { type: string, description: gen_ai.operation.name ("chat"). }
        dialect:    { type: string, enum: [anthropic, openai-chat, openai-responses] }
        provider:   { type: string, description: gen_ai.provider.name. }
        model_requested: { type: string }
        model_effective: { type: [string, 'null'] }
        request_id:  { type: [string, 'null'] }
        response_id: { type: [string, 'null'], description: gen_ai.response.id — recorded-fact join key. }
        venue:        { type: string, enum: [local, cloud] }
        endpoint_id:  { type: [string, 'null'] }
        route_reason: { type: [string, 'null'] }
        status:
          type: string
          description: 'served | refused:<code> — refused turns are first-class rows.'
        finish_reasons: { type: array, items: { type: string } }
        usage:
          type: object
          additionalProperties: false
          properties:
            input_tokens:  { type: [integer, 'null'] }
            output_tokens: { type: [integer, 'null'] }
            measured:      { type: boolean, description: measured vs provider-claimed. }
        request_delta:
          type: object
          additionalProperties: false
          description: >-
            The delta-capture wire face (kills the O(n²) resend trap): only
            messages not previously seen for this session, plus the prefix-hash
            chain link. A diverged prefix ⇒ the proxy sends full:true AND a
            prefix_reset gap event in the same batch (honest, larger, never wrong).
          properties:
            prefix_hash: { type: string }
            prev_prefix_hash: { type: [string, 'null'] }
            full: { type: boolean }
            new_messages:
              type: array
              items: { $ref: '#/components/schemas/RecorderMessage' }
          required: [prefix_hash, full, new_messages]
        response_messages:
          type: array
          items: { $ref: '#/components/schemas/RecorderMessage' }
        stream_timing:
          type: object
          additionalProperties: false
          properties:
            chunk_offsets_ms: { type: array, items: { type: integer } }
          description: Local-stream scrub pacing; cloud turns omit it (whole-block playback, disclosed — P2-11).
        first_byte_ms: { type: [integer, 'null'] }
        last_byte_ms:  { type: [integer, 'null'] }
        estimated_bytes: { type: integer, description: codex logs_2 size-accounting precedent. }
      required: [kind, event_id, seq, ts_ms, operation, dialect, venue, status, request_delta]
    RecorderMessage:
      type: object
      additionalProperties: false
      description: >-
        OTel GenAI {role, parts[]} content model — dialect-neutral; Anthropic
        blocks and OpenAI messages both project in; any future OTLP export is a
        rename-free mapping.
      properties:
        role: { type: string, enum: [system, user, assistant, tool] }
        parts:
          type: array
          items:
            type: object
            additionalProperties: false
            properties:
              type: { type: string, enum: [text, tool_call, tool_call_response, thinking, uri, other] }
              text: { type: string }
              tool_call:
                type: object
                additionalProperties: false
                properties:
                  id: { type: string }
                  name: { type: string }
                  arguments: { type: string }
              uri:  { type: string, description: Blob/base64 parts by reference only — never inline. }
              hash: { type: string }
              other:
                type: object
                additionalProperties: true
                description: >-
                  The TYPED escape hatch (Bedrock additionalModelRequestFields
                  lesson — explicit escape, never a silent drop): dialect-raw JSON
                  for blocks outside the closed part enum. Capped like any part;
                  taint-inherited (trust:'external').
              truncated: { type: boolean }
            required: [type]
      required: [role, parts]
    RecorderIngestResult:
      type: object
      properties:
        accepted: { type: integer }
        deduped:  { type: integer, description: event_id already stored — acknowledged, not double-written. }
        gapped:
          type: array
          description: >-
            Events the Host converted to typed gap rows instead of content
            (P0-1: redactor_unavailable; eviction pressure: disk_low). The caller
            learns its content did NOT land so the REC chip can demote — a lit
            chip over a wall of gaps is a product-dead recorder that looks healthy.
          items:
            type: object
            properties:
              event_id: { type: string }
              gap: { type: string, enum: [redactor_unavailable, disk_low] }
        refused:
          type: array
          description: Per-event closed-schema refusals; refused events are never retried — the proxy mints a gap.
          items:
            type: object
            properties:
              event_id: { type: string }
              code: { type: string, enum: [INVALID_EVENT] }
              message: { type: string }
      required: [accepted, deduped, gapped, refused]
    RecorderPurgeAct:
      type: object
      properties:
        verb:    { type: string, enum: [purge] }
        target:  { type: string, description: The rack_id. }
        by:      { type: string, description: 'Actor label, store-verb convention: "operator" | "bearer".' }
        outcome: { type: string, enum: [ok, refused] }
        reason:  { type: string, enum: [empty], description: Present only on refused. }
        bytes_freed:   { type: integer }
        key_discarded: { type: boolean, description: True iff a keychain key existed and was destroyed. }
      required: [verb, target, by, outcome]
    # ----- the observation plane (sessions + telemetry) -----------------------
    Session:
      type: object
      description: >-
        One session on the Host's observation plane — the synthetic host row
        (id "host", kind "host": the Host itself, so a fleet board renders the
        host from the same feed), a live task or workflow run (state
        "running", with started_ts and cancellable), a durably-suspended
        workflow run parked under resume/ (state "suspended" — it holds no
        thread and no model — with waiting_on from the parked gate's
        predicate), or a finished Host-driven workflow run projected from the
        run journal (state "completed"/"failed", with ok/ended_ts/ms/steps).
        Extend by adding kinds/states/fields, never by renaming values.
        (1.2.0, CLI-6) Or an EXTERNAL session (kind "external"): work another
        agent registered through POST /agent/v1/sessions — the Host OBSERVES
        the claim, it does not execute or broker it. External rows carry the
        registrant's self-declared facts (name, harness, cwd), the derived
        client label, self-reported state (running/waiting — never review,
        never a terminal), tokens_claimed (never tokens), and are NEVER
        cancellable here (this Host does not own that process).
      properties:
        id:
          type: string
          description: >-
            The plane-minted session id (t<n> task, r<n> run), or the literal
            "host" for the synthetic host row.
        kind:
          type: string
          enum: [task, workflow_run, host, external]
        name:
          type: string
          description: >-
            The agent name (a task), the workflow name (a run), or the
            resident model (the host row).
        state:
          type: string
          enum: [running, suspended, completed, failed, cancelled, paused, waiting]
          description: >-
            cancelled (1.2.0): ended via the cancel seam — a deliberate stop,
            distinct from completed and failed. Emitted only by hosts that
            journal the run_end outcome; older journal rows project exactly as
            1.1.0 — never re-guessed. EMISSION CONDITION (SPEC R3): this value
            appears on the EXISTING sessions route only once the WEB-0
            tolerance floor is the fielded minimum (v1.2.15), else it stays
            behind a Host flag ([serve] emit_cancelled_state in config.toml,
            default off) — fielded pre-floor boards break on it.
            paused (1.2.0, CLI-3): a live run parked (or parking, at its next
            step boundary) by POST /agent/v1/runs/{id}/pause — its emitter is
            that verb, in the same increment; a run nothing paused never wears
            it. Left by resume, or by the runner's MAX_PAUSE auto-resume at
            300s (disclosed via last_event). (waiting arrives with CLI-6.)
            waiting (1.2.0, CLI-6): an EXTERNAL row's self-reported idle — its
            ONLY emitter is the external heartbeat projection (a heartbeat
            carrying status "waiting"); it never appears on internal rows, and
            it classes RESTING on the board, never needs-you (a self-report
            with no actable affordance can never own the amber tap, SPEC §1.4).
        started_ts:
          type: integer
          description: >-
            Unix seconds the plane accepted the work (live rows), or the Host
            process start (the host row). Absent on history/suspended rows —
            never fabricated.
        cancellable:
          type: boolean
          description: >-
            Whether POST /agent/v1/sessions/{id}/cancel can land — true only
            for a live workflow run (a task has no cancellation seam). Live
            only.
        waiting_on:
          type: string
          enum: [input, time, value, event]
          description: >-
            What a durably-suspended run's parked gate awaits (its predicate
            kind). Present ONLY on state "suspended", and ONLY when the
            predicate kind is in this vocabulary (a future kind is absent —
            never guessed). Mirrors the website driver's waitingOn.
            event (1.2.0): a parked event gate (the resume machinery's
            predicateKind vocabulary). Until this value no wire carried it and
            the absent-fallback rendered every event gate as rank-0 amber —
            the false-alarm class the state machine exists to kill; its
            emitter (the Host's waiting_on projection) lands in the same
            increment.
        suspend_kind:
          type: string
          enum: [entry_time_gate, interior_gate]
          description: >-
            (1.2.0) VERBATIM the run_end SuspendKind vocabulary (fixtures pin
            it). Present ONLY on state "suspended" and only when the host
            recorded one. Absent on a 1.1.0 host — AND on every record parked
            BEFORE this upgrade, forever (the record field is written at
            suspend time; old parked records are never re-resolved) — clients
            fall back per the skew fixture (consult waiting_on; never guessed
            scheduled).
        wrapper_cycle:
          type: integer
          description: >-
            (1.2.0) Completed perpetual cycles for an entry_time_gate park
            (0 = first-armed, >0 = re-arming). Present only beside
            suspend_kind entry_time_gate.
        last_event:
          type: string
          description: >-
            (1.2.0, CLI-3) The session's most recent disclosed lifecycle note.
            ONE emitter today: the pause seam's MAX_PAUSE auto-resume — after
            the shared runner clears a pause at the 300s bound the row reads
            VERBATIM "auto-resumed after 300s" (fixture-pinned; the string
            derives from the runner's own constant). Absent otherwise — never
            fabricated, never a synthesized activity feed.
        cwd:
          type: string
          description: >-
            The Host's watch root — the directory the session's agent runtime
            executes under. Live rows + the host row only (the journal records
            no cwd; never back-filled).
        tokens:
          type: object
          description: >-
            Per-session measured token usage. ABSENT when the session measured
            none (agent-plane turns report no usage on this Host today) —
            absent-when-unmeasured, never a fake zero.
          properties:
            prompt:
              type: integer
            completion:
              type: integer
          required: [prompt, completion]
        harness:
          type: string
          description: >-
            (1.2.0, CLI-6) The external session's self-declared harness — an
            optional registry id; free string tolerated (G3). External rows
            only, and only when the registrant declared one.
        client:
          type: string
          description: >-
            (1.2.0, CLI-6) DERIVED from x-conifer-client (execution-report v1
            §3 register) — telemetry-grade, never authorization;
            client-authored provenance fields are 400'd (G15). External rows
            only, and only when the registering request carried the header.
        stale:
          type: boolean
          description: >-
            (1.2.0, CLI-6) Derived: ≥3 missed heartbeat windows — a FACT about
            reporting, never a guessed state transition. External rows only;
            present only when the fact holds (absent = reporting within its
            window, never a fake false). last_event carries the companion
            disclosure ("no heartbeat for Ns").
        tokens_claimed:
          type: object
          description: >-
            (1.2.0, CLI-6 — R9's wire) The claimed register, structurally
            separate from measured: self-reported cumulative usage
            (heartbeat-folded). NEVER summed into any measured total; renders
            per-row under the external kind chip. External rows only, and only
            when the registrant reported usage.
          properties:
            prompt:
              type: integer
            completion:
              type: integer
        route:
          type: object
          description: >-
            Where the session's turns are served — mirrors the routing
            record's chosen-model/chosen-endpoint axes. Live rows + the host
            row only; history carries no back-filled route. lane "local" is a
            FACT on this host (no cloud vocabulary until a session can
            actually be cloud-routed).
          properties:
            lane:
              type: string
              enum: [local]
            model:
              type: string
              description: The resident model id (the /v1/models id).
            endpoint:
              type: string
              description: >-
                The canonical managed-host endpoint id — the literal
                "host:local" (pinned across cli/desktop/web by
                contracts/endpoints-residency.fixtures.json).
          required: [lane, model, endpoint]
        ok:
          type: boolean
          description: Whether the finished run succeeded (history only).
        ended_ts:
          type: integer
          description: Unix seconds the run finished (history only).
        ms:
          type: integer
          description: Total wall-clock milliseconds (history only).
        steps:
          type: integer
          description: How many steps the run executed (history only).
      required: [id, kind, name, state]
    RunDetail:
      type: object
      description: >-
        One workflow run's record (GET /agent/v1/runs/{id}) — the Session
        shape for that run (whichever half currently owns it: live, suspended,
        or journal history) plus, for a journaled (finished) run, the
        per-step detail and the harvested result. Extend by adding fields.
      allOf:
        - $ref: '#/components/schemas/Session'
      properties:
        step_records:
          type: array
          description: >-
            The journal's per-step outcomes, in execution order (finished
            runs only — a live/suspended run has no journal record yet).
          items:
            type: object
            properties:
              id:
                type: string
              ok:
                type: boolean
              ms:
                type: integer
              retries:
                type: integer
            required: [id, ok, ms]
            additionalProperties: true
        result:
          type: string
          description: >-
            The run's harvested final output (the terminal step's text) —
            present only when a clean finished run harvested one.
        inputs:
          type: object
          additionalProperties: true
          description: >-
            (1.2.0) The declared-inputs map this run started with — present
            when journaled, absent otherwise (never fabricated). The durable
            precondition for retry.
    ExternalSessionRegistration:
      type: object
      description: >-
        (1.2.0, CLI-6) The body of POST /agent/v1/sessions — EXTERNAL work
        claiming a row on the observation plane. Validated all-errors-at-once
        and refused whole (never a half row); client-authored provenance
        fields (client, route, stale, tokens, tokens_claimed, …) are 400'd —
        the Host derives or observes those, it never trusts a claim of them.
      properties:
        kind:
          type: string
          const: external   # only external is client-mintable; absent/unknown → 400, never defaulted
        name:
          type: string
          description: display name ("openclaw", "claude-code")
        harness:
          type: string
          description: optional registry id; free string tolerated (G3)
        cwd:
          type: string
        # route is DROPPED from registration (red-team): the lane enum is [local]
        # only, so an external agent doing cloud work could only omit or lie — and
        # the page field map fabricates "local" on absence. Returns only beside a
        # lane vocabulary that can be honest (reserved).
      required: [kind, name]
    SessionRoute:
      type: object
      description: >-
        (1.2.0) What actually served a session — the routing record's
        chosen-model/chosen-endpoint axes, factored out so the WorkReceipt can
        reference the SAME shape Session.route inlines. lane "local" is a FACT
        on this host (no cloud vocabulary until a session can actually be
        cloud-routed); NEVER back-filled onto a row the Host did not serve.
      properties:
        lane:
          type: string
          enum: [local]
        model:
          type: string
          description: The resident model id (the /v1/models id).
        endpoint:
          type: string
          description: >-
            The canonical managed-host endpoint id — the literal "host:local"
            (pinned by contracts/endpoints-residency.fixtures.json).
      required: [lane, model, endpoint]
    WorkReceipt:
      type: object
      properties:
        session_id: { type: string }
        kind:       { type: string, enum: [task, workflow_run, external, host] }
        name:       { type: string }
        outcome:
          type: string
          enum: [completed, failed, cancelled]           # RunOutcome VERBATIM
          description: Absent ONLY when end_reason names why the Host could not know.
        end_reason: { type: string }                     # e.g. "heartbeat_lost" — a fact about reporting
        started_ts: { type: integer }
        ended_ts:   { type: integer }
        route:      { $ref: '#/components/schemas/SessionRoute' }   # what actually served it — never back-filled
        tokens:
          type: object
          properties: { prompt: {type: integer}, completion: {type: integer} }
          description: Present ONLY as a settlement emitter exists; absent = not-disclosed, never 0.
        cost_nanousd:
          type: integer
          description: >-
            Settled figure per execution-report v1 rules — parsed from a real
            report (gateway now; proxy/local receipts in Wave 2), never fabricated.
            The fleet's cost column fills through THIS route and no other way.
        reporter:
          type: string
          enum: [host, client]
          description: >-
            WHO measured — the single provenance word, carried on the receipt
            only. host = the Host's own measurement; client = self-reported
            (every v1 external close). The value `proxy` is RESERVED IN PROSE
            (red-team/R2: it has no emitter — the Host holds no credential that
            distinguishes a proxy writer from any local process; it enters
            alongside the Host-minted proxy credential, SPEC §4.4).
        evidence:
          type: object
          properties:
            ops_approved:  { type: integer }
            ops_discarded: { type: integer }
            retried_from:  { type: string }              # stamped by the plane after verifying the source (G15)
          additionalProperties: true
      required: [session_id, kind, name, ended_ts, reporter]
    HostTelemetry:
      type: object
      description: >-
        Work + token totals since startup. The tokens block carries its own
        coverage disclosure: measured_completions counts exactly the
        completions the sums cover; work with no measured usage is in served
        and absent from tokens — never padded.
      properties:
        model:
          type: string
          description: The served model id (the /v1/models id).
        served:
          type: object
          properties:
            completions:
              type: integer
              description: Chat completions served since startup.
            tasks:
              type: integer
              description: Agent tasks accepted since startup.
            workflow_runs:
              type: integer
              description: Workflow runs accepted since startup.
          required: [completions, tasks, workflow_runs]
        tokens:
          type: object
          properties:
            prompt:
              type: integer
            completion:
              type: integer
            total:
              type: integer
            measured_completions:
              type: integer
              description: >-
                How many completions actually reported usage — the coverage
                denominator for the sums above.
            coverage:
              type: string
              description: >-
                Which surface the sums cover. Extend by adding values only —
                and only alongside the build that actually emits the new
                value (a declared-but-never-emitted value is drift bait, so
                none is listed ahead of its emitter). "chat_completions" —
                the native non-stream chat handler's measured usage; on real
                builds that handler is retired, so this block stays honestly
                zero (measured_completions 0) and real token measurement
                lives in the sibling serve fold below.
              enum: [chat_completions]
          required: [prompt, completion, total, measured_completions, coverage]
        serve:
          type: object
          description: >-
            The serve plane's usage-registry snapshot (v1.1, present once the
            Stage-4 delegation lands its fold — absent until then, never a
            fabricated zero block): cumulative UsageCounters in total and per
            model id.
          properties:
            total:
              $ref: '#/components/schemas/UsageCounters'
            models:
              type: object
              description: Per-model-id counters.
              additionalProperties:
                $ref: '#/components/schemas/UsageCounters'
          required: [total, models]
      required: [model, served, tokens]
    UsageCounters:
      type: object
      description: >-
        Cumulative serve-plane usage counters for one model (or the all-models
        total) — the conifer-serve metrics shape, mirrored additively.
      properties:
        requests:
          type: integer
        prompt_tokens:
          type: integer
        completion_tokens:
          type: integer
        reused_prefix_tokens:
          type: integer
        queue_wait_ms:
          type: number
        ttft_ms:
          type: number
        decode_ms:
          type: number
      required: [requests, prompt_tokens, completion_tokens]
      additionalProperties: true
    # ----- pinned by contracts/turn-event.fixtures.json (normative) -----------
    TurnEvent:
      description: >-
        The platform's one event vocabulary for a streamed agent turn. Tagged
        union on "type". Pinned by turn-event.fixtures.json; extend by adding
        variants/fields, never by renaming tags.
      oneOf:
        - $ref: '#/components/schemas/TurnEventStart'
        - $ref: '#/components/schemas/TurnEventToolCall'
        - $ref: '#/components/schemas/TurnEventToolResult'
        - $ref: '#/components/schemas/TurnEventDenied'
        - $ref: '#/components/schemas/TurnEventPending'
        - $ref: '#/components/schemas/TurnEventAnswer'
        - $ref: '#/components/schemas/TurnEventAborted'
      discriminator:
        propertyName: type
        mapping:
          start: '#/components/schemas/TurnEventStart'
          tool_call: '#/components/schemas/TurnEventToolCall'
          tool_result: '#/components/schemas/TurnEventToolResult'
          denied: '#/components/schemas/TurnEventDenied'
          pending: '#/components/schemas/TurnEventPending'
          answer: '#/components/schemas/TurnEventAnswer'
          aborted: '#/components/schemas/TurnEventAborted'
    TurnEventStart:
      type: object
      description: >-
        The task began — the FIRST event of every task NDJSON stream.
        Plane-synthesized: the agent plane emits it when it accepts a task
        (it has no event source in the runtime), assigning task_id and
        echoing the resolved AgentSpec name as agent.
      properties:
        type:
          const: start
        task_id:
          type: string
          description: Plane-assigned task identifier.
        agent:
          type: string
          description: The resolved AgentSpec name the task runs as.
      required: [type, task_id, agent]
    TurnEventToolCall:
      type: object
      properties:
        type:
          const: tool_call
        name:
          type: string
        args:
          description: The tool's arguments (free-form JSON).
      required: [type, name, args]
    TurnEventToolResult:
      type: object
      properties:
        type:
          const: tool_result
        name:
          type: string
        ok:
          type: boolean
        summary:
          type: string
          description: Presentation-sized, never the raw payload.
      required: [type, name, ok, summary]
    TurnEventDenied:
      type: object
      properties:
        type:
          const: denied
        action:
          type: string
        fix:
          type: [string, 'null']
          description: The exact command that would grant the action.
      required: [type, action, fix]
    TurnEventPending:
      type: object
      properties:
        type:
          const: pending
        id:
          type: string
        summary:
          type: string
      required: [type, id, summary]
    TurnEventAnswer:
      type: object
      properties:
        type:
          const: answer
        text:
          type: string
      required: [type, text]
    TurnEventAborted:
      type: object
      properties:
        type:
          const: aborted
        reason:
          type: string
      required: [type, reason]

    # ----- pinned by contracts/run-event.fixtures.json (normative) ------------
    RunEvent:
      description: >-
        The Ensemble run envelope — the tagged union the workflows runs stream
        emits (NDJSON, one per line). Tagged on "type". A run is bracketed
        run_start ... run_end; each step is bracketed step_start ... step_end
        (with step_waiting while it parks for approval); step_event wraps a
        VERBATIM TurnEvent in its event field — it references the TurnEvent
        vocabulary, it never duplicates or redefines it. Pinned by
        run-event.fixtures.json; extend by adding variants/fields, never by
        renaming tags.
      oneOf:
        - $ref: '#/components/schemas/RunEventRunStart'
        - $ref: '#/components/schemas/RunEventStepStart'
        - $ref: '#/components/schemas/RunEventStepEvent'
        - $ref: '#/components/schemas/RunEventStepWaiting'
        - $ref: '#/components/schemas/RunEventStepModelLoading'
        - $ref: '#/components/schemas/RunEventStepPlaced'
        - $ref: '#/components/schemas/RunEventStepEnd'
        - $ref: '#/components/schemas/RunEventRunEnd'
        - $ref: '#/components/schemas/RunEventActiveSet'
        - $ref: '#/components/schemas/RunEventSuperstepEnd'
        - $ref: '#/components/schemas/RunEventNodeSkipped'
        - $ref: '#/components/schemas/RunEventChannelsUpdated'
      discriminator:
        propertyName: type
        mapping:
          run_start: '#/components/schemas/RunEventRunStart'
          step_start: '#/components/schemas/RunEventStepStart'
          step_event: '#/components/schemas/RunEventStepEvent'
          step_waiting: '#/components/schemas/RunEventStepWaiting'
          step_model_loading: '#/components/schemas/RunEventStepModelLoading'
          step_placed: '#/components/schemas/RunEventStepPlaced'
          step_end: '#/components/schemas/RunEventStepEnd'
          run_end: '#/components/schemas/RunEventRunEnd'
          active_set: '#/components/schemas/RunEventActiveSet'
          superstep_end: '#/components/schemas/RunEventSuperstepEnd'
          node_skipped: '#/components/schemas/RunEventNodeSkipped'
          channels_updated: '#/components/schemas/RunEventChannelsUpdated'
    RunEventRunStart:
      type: object
      description: The run began — the FIRST event of every run stream.
      properties:
        type:
          const: run_start
        run_id:
          type: string
          description: Plane-assigned run identifier.
        workflow:
          type: string
          description: The Ensemble name the run executes.
        steps:
          type: array
          description: The step ids in topological execution order.
          items:
            type: string
      required: [type, run_id, workflow, steps]
    RunEventStepStart:
      type: object
      description: A step began (re-emitted per retry attempt).
      properties:
        type:
          const: step_start
        step:
          type: string
        agent:
          type: string
          description: The resolved AgentSpec name the step runs as.
        attempt:
          type: integer
          description: 1-based attempt counter (incremented on retry).
      required: [type, step, agent, attempt]
    RunEventStepEvent:
      type: object
      description: >-
        A VERBATIM TurnEvent emitted by a step's agent turn, wrapped with its
        step id. event is a TurnEvent unchanged — the envelope references the
        TurnEvent schema, never redefines it.
      properties:
        type:
          const: step_event
        step:
          type: string
        event:
          $ref: '#/components/schemas/TurnEvent'
      required: [type, step, event]
    RunEventStepWaiting:
      type: object
      description: >-
        The step parked at a Prompt approval boundary; the run released the
        gen lock and suspended the pending reaper until these ids settle through
        the existing /agent/v1/pending/{id}/approve|discard endpoints.
      properties:
        type:
          const: step_waiting
        step:
          type: string
        pending:
          type: array
          description: The staged pending ids the run is waiting on.
          items:
            type: string
      required: [type, step, pending]
    RunEventStepModelLoading:
      type: object
      description: >-
        The step is loading the model it runs as before its first turn — emitted
        when a step's resolved model is not yet resident and the run pauses to
        bring it up. Advisory progress; the step's turns follow once it loads.
      properties:
        type:
          const: step_model_loading
        step:
          type: string
        model:
          type: string
          description: The model id the step is loading.
      required: [type, step, model]
    RunEventStepPlaced:
      type: object
      description: >-
        P3 placement provenance for a model-bearing step: emitted once when the
        step's placement resolves, naming the model + endpoint that serves it and
        (when the step authored a soft endpoint hint) echoing the hint with an
        honest hint_outcome. Additive; hint/hint_outcome are skip-serialized
        together when no hint was authored.
      properties:
        type:
          const: step_placed
        step:
          type: string
        model:
          type: string
          description: The resolved model that serves the step.
        endpoint:
          type: string
          description: The endpoint id that serves (or will serve) the step.
        hint:
          type: object
          description: The step's authored soft "run at" endpoint hint, echoed. Optional.
          properties:
            kind:
              type: string
              enum: [id, kind]
            value:
              type: string
          required: [kind, value]
        hint_outcome:
          type: string
          enum: [honored, unresolvable, masked, inapplicable]
          description: >-
            What honestly happened to the hint. honored = applied as a filter and
            the chosen endpoint matches; unresolvable = matched no live endpoint
            (ran unfiltered); masked = matched but masks/posture overrode it;
            inapplicable = placement machinery not in play (a pinned/plain-local
            turn, or a consumer that does not act on hints).
      required: [type, step, model, endpoint]
    RunEventStepEnd:
      type: object
      description: A step finished (after any retries).
      properties:
        type:
          const: step_end
        step:
          type: string
        ok:
          type: boolean
        ms:
          type: integer
          description: Wall-clock duration of the step, milliseconds.
        retries:
          type: integer
          description: How many retries the step took (0 on first-try success).
        output:
          type: string
          description: >-
            The step's produced text — the value that lands in the run context and,
            for the terminal step, the run's final result — echoed on the step
            boundary. Additive and OPTIONAL: emitted by a value-producing OK step,
            capped at 4 KiB, and skip-serialized (absent) for a control/failure/
            value-less step, so an output-less step_end stays byte-identical.
      required: [type, step, ok, ms, retries]
    RunEventRunEnd:
      type: object
      description: >-
        The run (this SEGMENT) finished — the LAST event of the segment's stream.
        wf-loops-v2 Phase A redefines a run as a sequence of run_start..run_end
        segments; `outcome` is the mutually-exclusive done-state tag (always
        emitted; an old-cli consumer that sees it absent falls back to `ok`).
      properties:
        type:
          const: run_end
        ok:
          type: boolean
        ms:
          type: integer
          description: Wall-clock duration of the whole run, milliseconds.
        outcome:
          type: string
          enum: [completed, failed, suspended, cancelled]
          description: >-
            The whole-segment done-state. completed = a live exit reached its
            terminal; failed = the terminal set is unsatisfiable or a hard
            structural/recursion error fired; suspended = work remains only
            behind parked gate(s) and a durable resumption record was written;
            cancelled = the cancel flag was tripped (cancel wins over all).
        suspend_kind:
          type: string
          enum: [entry_time_gate, interior_gate]
          description: >-
            Present ONLY when outcome == suspended: WHY the segment parked.
            entry_time_gate = a seed time-gate re-arming each tick (an external
            schedule); interior_gate = a mid-body wait on human/external input.
      required: [type, ok, ms, outcome]
    RunEventActiveSet:
      type: object
      description: >-
        The control-graph wave (C1): the set of steps the runner activates for
        one super-step, in declaration order. Additive — a linear run emits one
        wave per step.
      properties:
        type:
          const: active_set
        superstep:
          type: integer
          description: 1-based super-step counter.
        steps:
          type: array
          description: The step ids active in this super-step.
          items:
            type: string
      required: [type, superstep, steps]
    RunEventSuperstepEnd:
      type: object
      description: >-
        A super-step's barrier was reached (C1) — every node in the wave
        completed or was skipped, channel writes committed, control edges
        resolved. Additive.
      properties:
        type:
          const: superstep_end
        superstep:
          type: integer
          description: 1-based super-step counter.
      required: [type, superstep]
    RunEventNodeSkipped:
      type: object
      description: >-
        A node was SKIPPED (C1) — a switch arm marked it dead, or all its live
        predecessors were skipped; it never runs and produces no port. Additive.
      properties:
        type:
          const: node_skipped
        step:
          type: string
      required: [type, step]
    RunEventChannelsUpdated:
      type: object
      description: >-
        One or more channels were committed at a barrier (C1 §3.5). channels
        names them; values carries [channel, rendered value] pairs.
      properties:
        type:
          const: channels_updated
        channels:
          type: array
          items:
            type: string
        values:
          type: array
          description: '[channel, rendered value] pairs, one per commit.'
          items:
            type: array
            minItems: 2
            maxItems: 2
            items:
              type: string
      required: [type, channels, values]

    # ----- workflows plane -----------------------------------------------------
    Ensemble:
      type: object
      description: >-
        A multi-step agent workflow artifact — Flow's strict superset (the
        existing [[step]] table plus [ensemble]/[inputs]/[policy]). "workflows"
        on the wire; the artifact is an Ensemble; stored as ensembles/<name>.toml
        under the Host's config dir. The artifact is created/replaced as TOML
        source (text/plain) and projected to JSON on read; name is the identity
        the store keys on, the remaining fields are the [ensemble] table.
      properties:
        name:
          type: string
          description: Unique within the Host's workflows store; the filename.
        version:
          type: integer
          description: Schema version (a must-understand gate refuses unknown-higher).
        description:
          type: string
        origin:
          type: string
          description: >-
            Fork provenance — `builtin:<name>@<version>` when this Ensemble was
            materialized ("forked") from a built-in, a one-shot snapshot of WHICH
            built-in version it was copied from (it does NOT track upstream
            updates — v1 references by name, hash-pinning is deferred). Absent on
            a hand-written Ensemble. The only provenance vehicle (derived, never
            authored, elsewhere); a built-in projects it from its own identity.
      required: [name]
      additionalProperties: true
    BuiltinWorkflow:
      type: object
      description: >-
        One entry in the read-only built-in Ensemble catalog (GET
        /agent/v1/workflows/builtins). A projection of an embedded, self-contained
        Ensemble v1 artifact — never its full source — carrying the metadata a
        gallery needs plus its origin provenance tag. Byte-stable across calls
        (the catalog is compiled-in data, parse-checked by the Host's own tests).
      properties:
        name:
          type: string
          description: The built-in's name (its [ensemble].name).
        description:
          type: string
          description: The built-in's one-line [ensemble].description (empty when absent).
        steps:
          type: integer
          description: The number of steps in the built-in's DAG.
        origin:
          type: string
          description: >-
            The snapshot provenance tag a fork stamps — `builtin:<name>@<version>`,
            derived from the built-in's own [ensemble].name + version.
      required: [name, description, steps, origin]
    ForkRequest:
      type: object
      description: >-
        The optional body of POST /agent/v1/workflows/{name}/fork. Empty ⇒ the
        fork is stored under the built-in's own name; `as` overrides the target
        store name (fork-as), so a user who already has a stored workflow of the
        built-in's name can fork under a fresh name instead of being blocked.
      properties:
        as:
          type: string
          description: >-
            The target store name for the forked copy (a safe single path
            segment). Defaults to the built-in's name.
      additionalProperties: false
    LintReport:
      type: object
      description: >-
        The validation report a lint or a rejected save returns: per-finding
        diagnostics plus the derived consent summary (the union of effective
        grants across steps — derived, never stored).
      properties:
        ok:
          type: boolean
        findings:
          type: array
          items:
            type: object
            additionalProperties: true
        consent:
          type: object
          description: Derived consent summary (steps/agents/writes/network/exec/mcp).
          additionalProperties: true
      required: [ok]
      additionalProperties: true
    RunRecord:
      type: object
      description: >-
        A journal entry from runs.jsonl — the run id and its per-step outcomes
        (the journal::RunRecord/StepRecord shape, extended additively with a run
        id).
      properties:
        run_id:
          type: string
      required: [run_id]
      additionalProperties: true

    # ----- pinned by contracts/substrate-info.fixtures.json (normative) -------
    SubstrateInfo:
      type: object
      description: >-
        Identity + capability card. Pinned by substrate-info.fixtures.json;
        extend by adding fields/values only.
      properties:
        protocol:
          type: integer
        kind:
          type: string
          enum: [embedded, remote]
        endpoint:
          type: [string, 'null']
        chosen_because:
          type: string
        capabilities:
          type: array
          items:
            type: string
        model:
          type: [string, 'null']
        enforcement:
          type: string
          enum: [kernel, app_level]
        planner:
          type: string
          enum: [real, stub]
        completions_served:
          type: integer
          description: >-
            The running count of completions this Host has answered (added by
            GET /host/v1/info — an additive field over the fixture-pinned card,
            so clients observe one resident model serving every client). Optional
            because it is not part of the normative SubstrateInfo fixture set.
        contract_version:
          type: string
          description: >-
            (1.2.0) The native-planes contract version this Host serves — the
            capability probe pages gate additive-affordance rendering on
            (retry, WEB-3) instead of fire-and-hope against a permissive
            parser. Optional because it is not part of the normative
            SubstrateInfo fixture set (a 1.1.0 host omits it).
      required:
        [protocol, kind, endpoint, chosen_because, capabilities, model, enforcement, planner]

    # ----- agent plane (fixture-agreed where the fixtures reach) ---------------
    TaskRequest:
      type: object
      description: >-
        The body of POST /agent/v1/tasks. The plane answers with the task
        NDJSON stream, whose start event carries the plane-assigned task_id
        and echoes the resolved AgentSpec name (resolved from the agent field,
        or the Host's default agent when omitted).
      properties:
        task:
          type: string
          description: The user's task text.
        agent:
          description: >-
            An AgentSpec name (must exist in the Host's store — a dangling ref
            is refused 400, never run without its persona), or an inline
            AgentSpec used for this task only (not stored). Omitted: the
            Host's default agent — the stored spec of that name when one
            exists, else a bare built-in. The resolved name is echoed back as
            the agent field of the stream's start event.
          oneOf:
            - type: string
            - $ref: '#/components/schemas/AgentSpec'
        grants:
          type: object
          description: Per-task grant overrides (deny-by-default semantics).
          additionalProperties: true
      required: [task]
      additionalProperties: true
    PendingChange:
      type: object
      description: >-
        A staged destructive change awaiting approval. id and summary are the
        SAME values the turn's pending TurnEvent carried (the fixture-pinned
        {id, summary} pair) — approve/discard by that id.
      properties:
        id:
          type: string
          description: Matches the id of the pending TurnEvent that staged it.
        summary:
          type: string
          description: Presentation-sized, never the raw payload.
        files:
          type: array
          items: { type: string }
          description: >-
            (1.2.0) Paths the staged op touches — projected from the broker's
            ChangeDiff (per-path ChangeKind Added/Modified/Removed, the evidence
            that exists today). Absent = not disclosed, never [].
        seq:
          type: integer
          description: (1.2.0) Stage order within the session — the queue's declared ordering field.
        diff:
          type: string
          description: >-
            (1.2.0) The staged op's unified diff, verbatim from the broker's stage
            (TY-1's pre-image capture — never a synthesized rendering), capped at
            256KiB. Absent when the op has no textual diff.
        truncated:
          type: boolean
          description: (1.2.0) True when diff exceeded the 256KiB cap — evidence is never silently partial.
        added_lines:   { type: integer }
        removed_lines: { type: integer }
      required: [id, summary]
      additionalProperties: true
    ToolCatalogEntry:
      type: object
      description: >-
        Three-state availability — available, locked, or disabled. name is the
        vocabulary the turn's tool_call/tool_result TurnEvents use, so a
        catalog read up front explains every tool name the stream mentions.
      properties:
        name:
          type: string
          description: The tool name as it appears in tool_call/tool_result events.
        state:
          type: string
          enum: [available, locked, disabled]
        missing:
          type: array
          description: Locked only — what is missing (e.g. a credential).
          items:
            type: string
        unlock_hint:
          type: string
          description: Locked only — the exact command that unlocks it.
        reason:
          type: string
          description: Disabled only — why.
      required: [name, state]
      additionalProperties: true
    AgentSpec:
      type: object
      description: >-
        A persona + grants artifact — the SAME shape as the CLI's agent TOML
        (one store: agents/<name>.toml under the Host's config dir; the name is
        the filename, the body is the TOML). name is the identity everything
        correlates on: the /agent/v1/agents store keys on it, TaskRequest.agent
        names it, and the task stream's start event echoes it. Presentation
        concerns (folders, hooks) stay client-side, keyed by agent name.
      properties:
        name:
          type: string
          description: Unique within the Host's agent store; echoed by start events.
        description:
          type: string
          description: A one-line description (what agent listings show).
        persona:
          type: string
          description: >-
            The system persona the agent's turns run with — the canonical
            field name everywhere (store and wire).
        system:
          type: string
          description: >-
            Deprecated input alias for persona (the draft's early name) —
            accepted on input, never emitted.
        model:
          type: object
          description: >-
            The agent's model REQUEST — a ModelRequest: `model` selects WHICH
            model (`{ kind: "named", name, content_hash? }` — a catalog identity,
            never a filesystem path — or `{ kind: "routed", capability? }`, the
            engine picks per turn) and `masks` carries the placement posture
            (`cloud` = off | tainted-only | permitted, fail-closed off; `quality`
            0..10 detents 2/5/8; `keep_sensitive_local`; an optional `endpoint`
            "run at" hint `{ kind: "id" | "kind", value }`). Advisory on a
            self-hosted Host, which serves its one resident model regardless
            (responses and the host card name what actually answered).
          properties:
            model:
              type: object
              additionalProperties: true
            masks:
              type: object
              additionalProperties: true
          additionalProperties: true
        grants:
          type: object
          description: >-
            The agent's capability grants (the GrantSpec shape,
            deny-by-default). Grants are COMPOSE-TIME on a Host: a task whose
            agent's grants would widen the Host's composed grants is refused
            400 (never silently dropped); grants the Host already composed
            with are simply available.
          additionalProperties: true
        approvals:
          description: >-
            How the agent's staged changes are settled after its turns. The
            bare policy string is canonical (the TOML form, always emitted);
            the {policy} object form is accepted on input.
          oneOf:
            - type: string
              enum: [prompt, auto, deny]
            - type: object
              properties:
                policy:
                  type: string
                  enum: [prompt, auto, deny]
              additionalProperties: true
      required: [name]
      additionalProperties: true

    # ----- lab plane (node/edge/span schema from the platform plan) ------------
    Graph:
      type: object
      properties:
        nodes:
          type: array
          items:
            $ref: '#/components/schemas/GraphNode'
        edges:
          type: array
          items:
            $ref: '#/components/schemas/GraphEdge'
        spans:
          type: array
          items:
            $ref: '#/components/schemas/GraphSpan'
      required: [nodes, edges, spans]
    GraphNode:
      type: object
      properties:
        id:
          type: string
        op_kind:
          type: string
        role:
          type: string
        layer:
          type: [integer, 'null']
        phase:
          type: string
        binding:
          type: [string, 'null']
          description: The bound custom kernel, if any.
        fused_span:
          type: [string, 'null']
          description: The span this node is fused into, if any.
        oracle:
          type: boolean
          description: Whether an oracle reference exists for this node.
        swappable:
          type: boolean
      required: [id, op_kind]
      additionalProperties: true
    GraphEdge:
      type: object
      properties:
        from:
          type: string
        to:
          type: string
        slot:
          type: string
      required: [from, to, slot]
      additionalProperties: true
    GraphSpan:
      type: object
      properties:
        members:
          type: array
          items:
            type: string
        fused_component:
          type: [string, 'null']
      required: [members]
      additionalProperties: true
    Parts:
      type: object
      description: >-
        The bindable surface of the resident model (GET /lab/v1/parts and the
        /lab/v1/kernels GET alias): the component summaries (every bindable op)
        and the slot summaries (every bindable site, with its oracle/swappable/
        compatible facts). The element shapes are the conifer-tauri facade DTOs;
        only the top-level keys are load-bearing here.
      properties:
        components:
          type: array
          items:
            type: object
            additionalProperties: true
        slots:
          type: array
          items:
            type: object
            additionalProperties: true
      required: [components, slots]
      additionalProperties: true
    KernelRegistration:
      type: object
      properties:
        name:
          type: string
        source:
          type: string
          description: The kernel source text.
      required: [name, source]
      additionalProperties: true
    KernelRegistrationReport:
      type: object
      description: >-
        The outcome a kernel registration returns (the facade's report — its
        parity-gate verdict for the registered source). Shape owned by the
        conifer-tauri facade; permissive here.
      additionalProperties: true
    BindRequest:
      type: object
      properties:
        kernel:
          type: string
        components:
          type: array
          items:
            type: string
        span:
          type: array
          description: Fusion — bind a kernel over a span of members.
          items:
            type: string
      required: [kernel]
      additionalProperties: true
    ParityMode:
      type: object
      description: >-
        The parity enforcement mode (the conifer-tauri ParityModeDto). The mode
        vocabulary is owned by the facade; the wire shape is { mode }.
      properties:
        mode:
          type: string
      required: [mode]
      additionalProperties: true
    ParityRunResult:
      type: object
      properties:
        passed:
          type: boolean
        findings:
          type: array
          items:
            type: object
            additionalProperties: true
      required: [passed]
      additionalProperties: true
    ValidationReport:
      type: object
      description: >-
        The staged validation theater the canvas renders and the CLI prints
        line-by-line, in stage order.
      properties:
        stages:
          type: array
          items:
            type: object
            properties:
              stage:
                type: string
                enum: [signature, safety, parity]
              passed:
                type: boolean
              findings:
                type: array
                items:
                  type: object
                  additionalProperties: true
            required: [stage, passed]
            additionalProperties: true
        passed:
          type: boolean
      required: [stages, passed]
      additionalProperties: true
    ValidateRequest:
      type: object
      description: >-
        The POST /lab/v1/validate body: the on-disk Config envelope to validate
        and the GGUF it pins, as paths on the loopback Host's own filesystem (the
        plane is loopback-only). Unknown extra fields are ignored.
      properties:
        envelope_path:
          type: string
          description: Path to the Config envelope JSON on the Host's filesystem.
        weights_path:
          type: string
          description: Path to the GGUF the envelope pins.
      required: [envelope_path, weights_path]
      additionalProperties: true
    SaveConfigRequest:
      type: object
      description: >-
        The POST /lab/v1/configs body: the artifact name to save under, the GGUF
        to pin (a path on the loopback Host), and an optional weights_ref_path
        (the path the saved envelope records as its weights ref — defaults to
        weights_path). Unknown extra fields are ignored.
      properties:
        name:
          type: string
        weights_path:
          type: string
        weights_ref_path:
          type: string
      required: [name, weights_path]
      additionalProperties: true
    BenchRequest:
      type: object
      description: >-
        The optional POST /lab/v1/bench body: a context length for the bench
        geometry. Omitted ⇒ the engine picks its default (the ctx-trap
        discipline: never pin a window implicitly).
      properties:
        context_length:
          type: integer
          description: The bench-geometry context length (optional).
      additionalProperties: true
    BenchResult:
      type: object
      description: The kernel-vs-stock bench delta (shape owned by the facade).
      additionalProperties: true
