openapi: 3.1.0

info:
  title: Workflow Orchestration Protocol (openwop) API
  version: "1.1.0"
  summary: REST surface for declaring, executing, suspending, resuming, and observing multi-step workflows.
  description: |
    Canonical OpenAPI 3.1 specification for openwop-compliant servers. Generated from `rest-endpoints.md` and references the JSON Schemas in `schemas/`.

    See spec docs for semantics:
    - `auth.md` — API key + scope vocabulary
    - `idempotency.md` — `Idempotency-Key` header contract
    - `version-negotiation.md` — `engineVersion` + `eventLogSchemaVersion`
    - `capabilities.md` — `/.well-known/openwop` handshake
    - `stream-modes.md` — SSE consumption modes
    - `run-options.md` — `configurable`/`tags`/`metadata`
    - `interrupt.md` — HITL primitive
    - `replay.md` — `:fork` endpoint

    **Registry scope (RFC 0094 §I).** This document specifies the HOST
    surface. The production node-pack registry surface (`/v1/packs/*` —
    publish/get/delete/sig, deprecation, yank, key rotation) is specified in
    `spec/v1/node-packs.md` §"Registry HTTP API" + `spec/v1/registry-operations.md`
    and is served by a registry service (e.g. the planned hosted reference
    registry `packs.openwop.dev`, or a third-party/private registry
    implementation) — a distinct deployable, out of scope for this host
    OpenAPI document. Only the test-mode mirror (`/v1/packs-test/*`, RFC 0025)
    is host-mounted and documented here.
  contact:
    name: openwop spec working group
    url: https://openwop.dev/spec/v1/
  license:
    name: Apache-2.0
    identifier: Apache-2.0

externalDocs:
  description: openwop spec v1 corpus
  url: https://openwop.dev/spec/v1/

servers:
  - url: https://{host}/v1
    description: openwop-compliant server
    variables:
      host:
        default: api.example.com
        description: Replace with your server's hostname.

# ─────────────────────────────────────────────────────────────────────────────
# SECURITY
# ─────────────────────────────────────────────────────────────────────────────
security:
  - ApiKeyAuth: []

# ─────────────────────────────────────────────────────────────────────────────
# TAGS
# ─────────────────────────────────────────────────────────────────────────────
tags:
  - name: discovery
    description: Public capability + spec discovery (no auth required).
  - name: workflows
    description: Workflow definition manifest.
  - name: runs
    description: Run lifecycle — create, read, stream, cancel, fork.
  - name: agents
    description: Manifest-agent inventory (RFC 0072 §A). Read-only; gated on capabilities.agents.manifestRuntime. Dispatch rides the run surface (WorkflowNode.agent + POST /v1/runs).
  - name: tools
    description: Portable tool catalog (RFC 0078 §B). Read-only ToolDescriptor projection across tool sources (node-pack / workflow / mcp / connector / host-extension); gated on capabilities.toolCatalog; authorization-scoped per §F-2.
  - name: hitl
    description: Human-in-the-loop interrupts and approvals.
  - name: artifacts
    description: Run-produced artifacts.
  - name: webhooks
    description: Subscribe to run events via outbound HTTP.
  - name: triggers
    description: Durable trigger-bridge subscriptions (RFC 0083). RFC 0099 adds the external-event create surface (`POST /v1/trigger-subscriptions`); gated on `capabilities.triggerBridge.ingestion.registrationEndpoint`.
  - name: audit
    description: Audit-log integrity verification (gated on the `openwop-audit-log-integrity` profile).
  - name: prompts
    description: Prompt-template library — list, fetch, render, mutate (RFC 0028; gated on `capabilities.prompts.*`).
  - name: content
    description: Localized authored content — public delivery + tenant-scoped admin CRUD (RFC 0103; gated on `capabilities.content.supported`).
  - name: host
    description: Host-capability resources — e.g. the RFC 0059 agent workspace file store (gated on `capabilities.workspace.*`).
  - name: packs-test
    description: |
      RFC 0025 (`Draft`). Test-mode mirror of the production `/v1/packs/*` publish/get/delete/sig surface against
      an isolated catalog. Gated on `capabilities.packs.testMode.supported: true` plus the reference impl's
      `OPENWOP_PACKS_TEST_NAMESPACE_ENABLED=true` env-gate. Lets the conformance suite exercise the documented
      19-code publish error catalog without `packs:publish` scope on the real registry. Hosts that haven't
      mounted this surface MUST return `404 Not Found` for every path under `/v1/packs-test/`.

      Scope note (RFC 0094 §I): the PRODUCTION `/v1/packs/*` surface these paths mirror is specified in
      `spec/v1/registry-operations.md` + `node-packs.md` §"Registry HTTP API" and is served by a registry
      service (a distinct deployable from the host), so it is intentionally NOT defined in this host
      OpenAPI document.

# ─────────────────────────────────────────────────────────────────────────────
# PATHS
# ─────────────────────────────────────────────────────────────────────────────
paths:

  # ── Discovery (unauthenticated) ─────────────────────────────────────────
  /.well-known/openwop:
    get:
      tags: [discovery]
      summary: Capability declaration handshake.
      operationId: getCapabilities
      security: []
      responses:
        '200':
          description: Capabilities object — see `capabilities.md`.
          headers:
            Cache-Control:
              schema: { type: string }
              example: 'public, max-age=300'
            Capabilities-Etag:
              schema: { type: string }
              description: Optional probe handle for mid-session capability change detection.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Capabilities'
        '503':
          description: Server unable to compute capabilities (transient).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /v1/openapi.json:
    get:
      tags: [discovery]
      summary: Self-describing OpenAPI 3.1 spec.
      operationId: getOpenApiSpec
      security: []
      responses:
        '200':
          description: This document.
          content:
            application/json:
              schema:
                type: object
        '503':
          description: Server unable to serve spec (transient).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  # ── Workflows ───────────────────────────────────────────────────────────
  /v1/workflows/{workflowId}:
    get:
      tags: [workflows]
      summary: Read a workflow definition.
      operationId: getWorkflow
      parameters:
        - $ref: '#/components/parameters/WorkflowId'
      responses:
        '200':
          description: Workflow definition.
          content:
            application/json:
              schema:
                $ref: '../schemas/workflow-definition.schema.json'
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  # ── Runs ────────────────────────────────────────────────────────────────
  /v1/runs:
    post:
      tags: [runs]
      summary: Create a new run.
      operationId: createRun
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - in: header
          name: X-Dedup
          schema:
            type: string
            enum: [enforce]
          description: When set, server cross-host claim system rejects duplicate `(tenantId, scopeId)` pairs with `409 Conflict`.
        - in: header
          name: X-Force-Engine-Version
          schema: { type: integer, minimum: 0 }
          description: |
            **Test-keys-only.** When set, the server emits events for this run AS IF it
            were running the specified engine version (must be within the server's
            advertised `Capabilities.testing.forceEngineVersionRange`). Used by the
            conformance suite to verify version-negotiation fold-best-effort tolerance
            across the spec's forward-compat matrix. Servers MUST reject on production
            API keys with `403 force_engine_version_forbidden`. Closes F5.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              # The body is the WorkflowId + inputs + transport-specific
              # routing fields, plus the openwop RunOptions overlay (configurable,
              # tags, metadata) hoisted into a first-class JSON Schema at
              # ../schemas/run-options.schema.json. allOf composes the two
              # so callers see one unified body shape. RFC 0094 §A: neither
              # allOf branch is closed via additionalProperties (two closed
              # branches inside one allOf made every documented body
              # unsatisfiable); the composed request is closed here with
              # `unevaluatedProperties: false` (JSON Schema 2020-12), so
              # undeclared properties still fail at the composition level.
              unevaluatedProperties: false
              allOf:
                - type: object
                  properties:
                    workflowId: { type: string, minLength: 1 }
                    inputs:
                      type: object
                      description: Workflow inputs (consumed by triggers/nodes).
                    tenantId:
                      type: string
                      description: Tenant scoping. Server typically defaults from API key.
                    scopeId:
                      type: string
                      description: Opaque correlation ID for `X-Dedup` semantics.
                    callbackUrl:
                      type: string
                      format: uri
                      description: Signed-token HITL callback URL (see `interrupt.md`).
                    mode:
                      type: string
                      enum: [eval]
                      description: |
                        RFC 0081 §B. When `eval`, this run is an eval-suite projection
                        (not a workflow run): the host runs the `evalSuiteRef` against
                        `agentId`, emits the content-free `eval.*` family, and terminates
                        with an `EvalSummary` readable via `GET /v1/runs/{runId}/eval-summary`.
                        Capability-gated on `capabilities.agents.evalSuite.supported`; a
                        host that omits it rejects `mode: "eval"` with 501. Omit for a
                        normal workflow run.
                    evalSuiteRef:
                      type: string
                      minLength: 1
                      description: RFC 0081 — URI of the `AgentEvalSuite` to run. Required when mode is `eval`.
                    agentId:
                      type: string
                      minLength: 1
                      description: RFC 0081 — the manifest agent the eval suite targets. Required when mode is `eval`.
                  if:
                    properties: { mode: { const: eval } }
                    required: [mode]
                  then:
                    required: [evalSuiteRef, agentId]
                  else:
                    required: [workflowId]
                - $ref: '../schemas/run-options.schema.json'
      responses:
        '201':
          description: Run accepted.
          headers:
            openwop-Idempotent-Replay:
              schema: { type: boolean }
              description: Set when the response was served from the idempotency cache.
          content:
            application/json:
              schema:
                type: object
                required: [runId, status, eventsUrl]
                properties:
                  runId: { type: string }
                  status:
                    type: string
                    enum: [pending, running, waiting-approval, waiting-input, waiting-external]
                  eventsUrl: { type: string, format: uri }
                  statusUrl: { type: string, format: uri }
        '400': { $ref: '#/components/responses/ValidationError' }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409':
          description: '`X-Dedup` collision OR concurrent `Idempotency-Key` collision.'
          headers:
            Retry-After:
              schema: { type: integer }
              description: Seconds until the active claim is stale-eligible.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunClaimConflict'
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/runs/{runId}:
    get:
      tags: [runs]
      summary: Read run state (cached projection).
      operationId: getRun
      parameters:
        - $ref: '#/components/parameters/RunId'
      responses:
        '200':
          description: Projected run state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunSnapshot'
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/runs/{runId}/events:
    get:
      tags: [runs]
      summary: SSE stream of run events.
      operationId: streamRunEvents
      parameters:
        - $ref: '#/components/parameters/RunId'
        - in: query
          name: streamMode
          schema:
            type: string
            default: updates
            pattern: '^(values|updates|messages|debug)(,(values|updates|messages|debug))*$'
          description: |
            Single mode: `values` / `updates` / `messages` / `debug`.
            Mixed mode: comma-separated combination (e.g., `updates,messages`)
            per S4 closure — server emits union-of-filters with per-event
            `event:` field labeling which mode admitted each event.
            `values` MUST NOT combine with other modes (state.snapshot semantics
            need exclusive ownership). See `stream-modes.md`. Default `updates`.
        - in: query
          name: bufferMs
          schema:
            type: integer
            minimum: 0
            maximum: 5000
          description: |
            Optional batching hint per S3 closure. When set, the server
            accumulates events for up to N ms (or until a forced-flush
            trigger fires — terminal events, suspensions, connection close)
            and emits a single SSE event with `event: batch` and `data:` as
            a JSON array of `RunEventDoc`. Range 0..5000; `0` = no buffering.
            See `stream-modes.md` §Aggregation hint.
        - in: header
          name: Last-Event-ID
          schema: { type: string }
          description: Resume from sequence after this ID.
      responses:
        '200':
          description: SSE stream. Auto-closes on terminal event. Keep-alive comments every 30s.
          content:
            text/event-stream:
              schema:
                type: string
                description: SSE events. Each event has `id:`, `event:`, `data:` per RFC 8895.
        '400':
          description: Unsupported `streamMode`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UnsupportedStreamMode'
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/runs/{runId}/events/poll:
    get:
      tags: [runs]
      summary: Long-poll fallback for non-SSE clients.
      operationId: pollRunEvents
      parameters:
        - $ref: '#/components/parameters/RunId'
        - in: query
          name: lastSequence
          schema: { type: integer, minimum: 0 }
        - in: query
          name: timeout
          schema: { type: integer, minimum: 1, maximum: 60, default: 30 }
          description: Seconds to wait for new events. Max 60.
      responses:
        '200':
          description: Events since `lastSequence`.
          content:
            application/json:
              schema:
                type: object
                required: [events, isComplete]
                properties:
                  events:
                    type: array
                    items:
                      $ref: '../schemas/run-event.schema.json'
                  isComplete: { type: boolean }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/runs/{runId}/cancel:
    post:
      tags: [runs]
      summary: Cancel an in-flight run.
      operationId: cancelRun
      parameters:
        - $ref: '#/components/parameters/RunId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason: { type: string }
      responses:
        '200':
          description: Run cancellation accepted (cascade may be async).
          content:
            application/json:
              schema:
                type: object
                properties:
                  runId: { type: string }
                  status: { type: string, enum: [cancelled, cancelling] }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  # ── Run feedback / annotations (RFC 0056) ────────────────────────────
  # Gated on `capabilities.feedback.supported: true`. Annotations are a
  # per-run side-resource (NOT replayable run-event-log entries); recording
  # one also emits a live `run.annotated` SSE notification. Hosts without
  # the advertised capability return `501 capability_not_provided`.
  /v1/runs/{runId}/annotations:
    post:
      tags: [runs]
      summary: Record a non-blocking quality annotation on a run (RFC 0056).
      operationId: createAnnotation
      parameters:
        - $ref: '#/components/parameters/RunId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '../schemas/annotation-create.schema.json'
      responses:
        '201':
          description: Annotation recorded. Returns the persisted annotation.
          content:
            application/json:
              schema:
                $ref: '../schemas/annotation.schema.json'
        '400': { $ref: '#/components/responses/ValidationError' }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '501':
          description: 'Host does not advertise capabilities.feedback.supported (RFC 0056).'
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'
    get:
      tags: [runs]
      summary: List the annotations recorded on a run (RFC 0056).
      operationId: listAnnotations
      parameters:
        - $ref: '#/components/parameters/RunId'
      responses:
        '200':
          description: Annotations for the run (tenant-scoped).
          content:
            application/json:
              schema:
                type: object
                required: [annotations]
                properties:
                  annotations:
                    type: array
                    items:
                      $ref: '../schemas/annotation.schema.json'
                additionalProperties: false
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '501':
          description: 'Host does not advertise capabilities.feedback.supported (RFC 0056).'
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'

  # ── External-event trigger subscriptions (RFC 0099) ──────────────────
  # Gated on `capabilities.triggerBridge.ingestion.registrationEndpoint: true`.
  # The portable create surface for an external-event (webhook/email/form)
  # subscription that binds a source to a workflow to start. Returns the
  # created TriggerSubscription (RFC 0083 §B) plus a source-specific binding
  # (ingest URL/address + secret fingerprint), returned once at creation.
  # Hosts without the advertised capability return `501 capability_not_provided`.
  /v1/trigger-subscriptions:
    post:
      tags: [triggers]
      summary: Register an external-event trigger subscription (RFC 0099).
      description: |
        Creates an external-event (`webhook`/`email`/`form`) TriggerSubscription
        bound to a workflow, with a dedup config and a source-authenticity
        verification policy (RFC 0099 §F.2). The `workflowId` MUST resolve under
        the caller's RFC 0048 owner triple. The response carries the created
        TriggerSubscription plus a source-specific `binding`; the binding
        secret/URL is returned ONCE and is not re-fetchable in cleartext.
      operationId: createTriggerSubscription
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '../schemas/trigger-subscription-registration.schema.json'
      responses:
        '201':
          description: Subscription created. Returns the TriggerSubscription + a source-specific binding.
          content:
            application/json:
              schema:
                type: object
                required: [subscription, binding]
                additionalProperties: false
                properties:
                  subscription:
                    $ref: '../schemas/trigger-subscription.schema.json'
                  binding:
                    type: object
                    description: |
                      Source-specific binding the caller needs to wire the external
                      source. For `webhook`: `{ ingestUrl, secretFingerprint }`; for
                      `email`: `{ ingestAddress }`; for `form`: `{ ingestUrl }`. The
                      secret is returned once at creation (SR-1).
                    additionalProperties: true
                    properties:
                      ingestUrl: { type: string, format: uri }
                      ingestAddress: { type: string }
                      secretFingerprint: { type: string, maxLength: 32 }
        '400': { $ref: '#/components/responses/ValidationError' }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '501':
          description: 'Host does not advertise capabilities.triggerBridge.ingestion.registrationEndpoint (RFC 0099).'
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'

  # ── Localized content surface (RFC 0103) ─────────────────────────────
  # Public, cacheable delivery + tenant-scoped admin CRUD for authored
  # localized content (pages → sections). Locale on the public path is
  # negotiated via `Accept-Language` per the Stable i18n.md annex (no
  # `?locale=`); admin writes target a locale in the body. Gated on
  # `capabilities.content.supported: true` (requires `i18n.supported`).
  # Hosts without the capability return `501 capability_not_provided`.
  /v1/content/pages/{slug}:
    get:
      tags: [content]
      # Public delivery: anonymous-capable, cacheable. Tenant is host-resolved
      # (credential-derived when authenticated; host-defined when anonymous,
      # localized-content.md §F). Clears global ApiKeyAuth like getCapabilities.
      security: []
      summary: Deliver a published content page resolved for the negotiated locale (RFC 0103 §D).
      description: |
        Resolves the published page `slug` for the locale negotiated from
        `Accept-Language` (i18n.md fallback: q-value order → language family →
        `content.baseLocale`), applies the per-section field merge
        (`localized-content.md` §C), and returns the already-merged sections in
        render order. Sets `Content-Language` to the locale used,
        `Vary: Accept-Language, Accept-Encoding`, and
        `Cache-Control: public, max-age=300, stale-while-revalidate=3600`.
        Serves `status: "published"` content only. Tenant is host-resolved
        (`localized-content.md` §F): credential-derived when authenticated,
        host-defined (e.g. domain) when anonymous. A `slug` absent for the
        resolved tenant returns the same `404` as a nonexistent slug (no
        cross-tenant enumeration).
      operationId: getContentPage
      parameters:
        - name: slug
          in: path
          required: true
          schema: { type: string, pattern: '^[a-z][a-z0-9-]*$' }
          description: The page slug.
        - name: Accept-Language
          in: header
          required: false
          schema: { type: string }
          description: BCP-47 preference list; authoritative for locale selection (i18n.md). A malformed value MUST NOT 400.
      responses:
        '200':
          description: The resolved, published page for the negotiated locale.
          headers:
            Content-Language:
              schema: { type: string }
              description: The BCP-47 locale actually used (equals the response `locale`).
          content:
            application/json:
              schema:
                $ref: '../schemas/localized-content-page-response.schema.json'
        '404': { $ref: '#/components/responses/NotFound' }
        '501':
          description: 'Host does not advertise capabilities.content.supported (RFC 0103).'
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'

  /v1/content/pages:
    get:
      tags: [content]
      summary: List content pages for the caller's tenant (RFC 0103 §D, admin).
      description: |
        Tenant-scoped admin listing of pages (draft + published). Requires a
        principal with `content.read` scope.
      operationId: listContentPages
      responses:
        '200':
          description: The caller-tenant's pages.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '../schemas/localized-content-page.schema.json'
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '501':
          description: 'Host does not advertise capabilities.content.supported (RFC 0103).'
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'
    post:
      tags: [content]
      summary: Create a content page (RFC 0103 §D, admin).
      description: Tenant-scoped page creation. Requires `content.write` scope.
      operationId: createContentPage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '../schemas/localized-content-page.schema.json'
      responses:
        '201':
          description: The created page.
          content:
            application/json:
              schema:
                $ref: '../schemas/localized-content-page.schema.json'
        '400':
          description: 'Invalid request (e.g. baseLocale in supportedLocales, or a malformed locale/slug).'
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /v1/content/pages/{pageId}/sections/{sectionId}:
    put:
      tags: [content]
      summary: Upsert a section's base or per-locale fields (RFC 0103 §D, admin).
      description: |
        Locale-targeted write: `locale == content.baseLocale` upserts the
        section's base `data`; any other (BCP-47-subset) locale upserts
        `localizations[locale]`. Tenant-scoped; requires `content.write`.
      operationId: putContentSection
      parameters:
        - name: pageId
          in: path
          required: true
          schema: { type: string, minLength: 1 }
        - name: sectionId
          in: path
          required: true
          schema: { type: string, minLength: 1 }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [locale, data]
              properties:
                locale:
                  type: string
                  pattern: '^[a-z]{2}(-[A-Z]{2})?$'
                  description: Target locale; baseLocale upserts `data`, else `localizations[locale]`.
                data:
                  type: object
                  additionalProperties: true
                  description: The field overlay for the target locale.
      responses:
        '200':
          description: The updated section record.
          content:
            application/json:
              schema:
                $ref: '../schemas/localized-content-section.schema.json'
        '400':
          description: 'Invalid request (e.g. baseLocale in supportedLocales, or a malformed locale/slug).'
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/content/settings:
    get:
      tags: [content]
      summary: Read the tenant's content language settings (RFC 0103 §B, admin).
      description: Tenant-scoped read of `{ baseLocale, supportedLocales, autoTranslateOnPublish }`. Requires `content.read`.
      operationId: getContentSettings
      responses:
        '200':
          description: The tenant's language settings.
          content:
            application/json:
              schema:
                $ref: '../schemas/localized-content-language-settings.schema.json'
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '501':
          description: 'Host does not advertise capabilities.content.supported (RFC 0103).'
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'
    put:
      tags: [content]
      summary: Update the tenant's content language settings (RFC 0103 §B, admin).
      description: |
        Tenant-scoped settings update. The invariant `baseLocale ∉
        supportedLocales` MUST hold (else `400`). Requires `content.write`.
      operationId: putContentSettings
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '../schemas/localized-content-language-settings.schema.json'
      responses:
        '200':
          description: The updated language settings.
          content:
            application/json:
              schema:
                $ref: '../schemas/localized-content-language-settings.schema.json'
        '400':
          description: 'Invalid request (e.g. baseLocale in supportedLocales, or a malformed locale/slug).'
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }

  # ── Durable A2A task read seam (RFC 0100) ────────────────────────────
  # Host-extension convenience read of the persisted A2ATaskState projection
  # (RFC 0100 §2). The NORMATIVE A2A surface stays the A2A `tasks/get`
  # JSON-RPC method per A2A v0.3 — this is a non-normative host-extension
  # name under `/v1/host/sample/*`. Gated on `capabilities.a2a.durableTasks`.
  /v1/host/sample/a2a/tasks/{taskId}:
    get:
      tags: [host]
      summary: Read the persisted durable A2A task projection (RFC 0100, host-extension).
      description: |
        Returns the persisted `A2ATaskState` for `taskId` (== the backing
        `runId`) per RFC 0100 §2 — a host-extension convenience read of the
        durable projection that survives caller disconnect. The normative A2A
        read surface remains the A2A `tasks/get` JSON-RPC method. Hosts that do
        not advertise `capabilities.a2a.durableTasks` return `501`.
      operationId: getA2ATaskState
      parameters:
        - name: taskId
          in: path
          required: true
          schema: { type: string, minLength: 1 }
          description: The A2A Task.id (equals the backing OpenWOP runId).
      responses:
        '200':
          description: The persisted durable A2A task projection.
          content:
            application/json:
              schema:
                $ref: '../schemas/a2a-task-state.schema.json'
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '501':
          description: 'Host does not advertise capabilities.a2a.durableTasks (RFC 0100).'
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'

  # ── Agent workspace files (RFC 0059) ─────────────────────────────────
  # Gated on `capabilities.workspace.supported: true`. A versioned,
  # tenant·workspace-scoped (RFC 0048) ground-truth file store with atomic,
  # optimistically-concurrent (`If-Match`) writes. A successful PUT/DELETE
  # emits a content-free `workspace.updated` event. Hosts without the
  # advertised capability return `501 capability_not_provided`.
  /v1/host/workspace/files:
    get:
      tags: [host]
      summary: List workspace file metadata for the caller's tenant·workspace (RFC 0059).
      description: |
        Returns file metadata (no bodies) for the caller's `{tenant,
        workspace}` per RFC 0059 §C. Optional `?prefix=` filters the flat
        `path` namespace to entries starting with the given prefix.
      operationId: listWorkspaceFiles
      parameters:
        - $ref: '#/components/parameters/WorkspacePrefix'
      responses:
        '200':
          description: Workspace file metadata (tenant·workspace-scoped; bodies omitted).
          content:
            application/json:
              schema:
                type: object
                required: [files]
                properties:
                  files:
                    type: array
                    items:
                      $ref: '../schemas/workspace-file.schema.json'
                additionalProperties: false
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '501':
          description: 'Host does not advertise capabilities.workspace.supported (RFC 0059).'
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'

  /v1/host/workspace/files/{path}:
    get:
      tags: [host]
      summary: Read one workspace file (RFC 0059).
      description: |
        Returns the `WorkspaceFile` at `path` for the caller's `{tenant,
        workspace}`. When `capabilities.workspace.versioned: true`, an
        optional `?version=N` returns the historical snapshot at version N.
      operationId: getWorkspaceFile
      parameters:
        - $ref: '#/components/parameters/WorkspacePath'
        - $ref: '#/components/parameters/WorkspaceVersion'
      responses:
        '200':
          description: The workspace file (current version, or `?version=N` when versioned).
          content:
            application/json:
              schema:
                $ref: '../schemas/workspace-file.schema.json'
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '501':
          description: 'Host does not advertise capabilities.workspace.supported (RFC 0059).'
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'
    put:
      tags: [host]
      summary: Atomic create/replace of a workspace file (RFC 0059).
      description: |
        Atomically creates or replaces the file at `path` per RFC 0059 §C.
        MUST honor `If-Match: <etag>` — a stale token returns `409
        workspace_conflict` (`details.currentVersion` carries the live
        version). On success the host bumps `version`, recomputes `etag`,
        and emits a `workspace.updated` event. A `content` exceeding
        `capabilities.workspace.maxFileBytes` returns `workspace_too_large`.
      operationId: putWorkspaceFile
      parameters:
        - $ref: '#/components/parameters/WorkspacePath'
        - $ref: '#/components/parameters/IfMatch'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '../schemas/workspace-file-create.schema.json'
      responses:
        '200':
          description: File created or replaced. Returns the persisted WorkspaceFile.
          content:
            application/json:
              schema:
                $ref: '../schemas/workspace-file.schema.json'
        '400': { $ref: '#/components/responses/ValidationError' }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409':
          description: 'Stale `If-Match` — the file changed since the supplied etag (`workspace_conflict`).'
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'
        '413':
          description: 'Content exceeds `capabilities.workspace.maxFileBytes` (`workspace_too_large`).'
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'
        '501':
          description: 'Host does not advertise capabilities.workspace.supported (RFC 0059).'
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'
    delete:
      tags: [host]
      summary: Delete a workspace file (RFC 0059).
      description: |
        Removes the file at `path` (and, when `versioned: true`, writes a
        tombstone). Emits a `workspace.updated` event on success.
      operationId: deleteWorkspaceFile
      parameters:
        - $ref: '#/components/parameters/WorkspacePath'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '204':
          description: File deleted.
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '501':
          description: 'Host does not advertise capabilities.workspace.supported (RFC 0059).'
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'

  /v1/runs:bulk-cancel:
    post:
      tags: [runs]
      summary: Cancel a set of in-flight runs in a single request.
      description: |
        Per `spec/v1/rest-endpoints.md` §"POST /v1/runs:bulk-cancel". Accepts
        a non-empty array of runIds and processes each cancellation
        independently. Returns `200` with a per-id results array even when
        some individual cancellations fail; the top-level operation succeeds
        when the request reached the host, regardless of per-id outcomes.
        Hosts enforce a host-defined cap on the array length (RECOMMENDED
        100); over-cap requests return `400 validation_error`.
      operationId: bulkCancelRuns
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [runIds]
              properties:
                runIds:
                  type: array
                  minItems: 1
                  maxItems: 100
                  items: { type: string, minLength: 1, maxLength: 128 }
                reason: { type: string, maxLength: 512 }
              additionalProperties: false
      responses:
        '200':
          description: Per-id cancel results.
          content:
            application/json:
              schema:
                type: object
                required: [results]
                properties:
                  results:
                    type: array
                    items:
                      type: object
                      required: [runId, ok]
                      properties:
                        runId: { type: string, minLength: 1 }
                        ok: { type: boolean }
                        status: { type: string, enum: [cancelled, cancelling] }
                        error:
                          $ref: '../schemas/error-envelope.schema.json'
                      additionalProperties: false
                additionalProperties: false
        '400': { $ref: '#/components/responses/ValidationError' }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /v1/runs/{runId}:fork:
    post:
      tags: [runs]
      summary: Fork the run for replay or branch (see `replay.md`).
      operationId: forkRun
      parameters:
        - $ref: '#/components/parameters/RunId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [mode]
              properties:
                fromSeq:
                  type: integer
                  minimum: 0
                  description: |
                    Inclusive — events `< fromSeq` are fixed history; `>= fromSeq` are re-executed.
                    Required for `branch` (the branch point). Optional for `replay`; when omitted,
                    defaults to `0` (full re-execution from source-run start) per `replay.md`
                    §"Replay-mode defaults".
                mode:
                  type: string
                  enum: [replay, branch]
                runOptionsOverlay:
                  type: object
                  description: For `branch` mode only — caller-supplied `RunOptions` to overlay.
              additionalProperties: false
      responses:
        '201':
          description: Fork accepted, new run started.
          content:
            application/json:
              schema:
                type: object
                required: [runId, sourceRunId, mode, status, eventsUrl]
                properties:
                  runId: { type: string }
                  sourceRunId: { type: string }
                  fromSeq: { type: integer }
                  mode: { type: string, enum: [replay, branch] }
                  status: { type: string }
                  eventsUrl: { type: string, format: uri }
        '400':
          description: Invalid `fromSeq`, `replay` with non-empty `runOptionsOverlay`, etc.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422':
          description: "`fromSeq` references a sequence number that doesn't exist in the source run's event log."
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
  /v1/runs/{runId}/ancestry:
    get:
      tags: [runs]
      summary: |
        RFC 0040 §C — return the run's immediate parent in the cross-host
        composition chain. Capability-gated on
        `capabilities.multiAgent.executionModel.crossHostCausation.ancestryEndpointSupported: true`;
        hosts that don't advertise return 404 not_found. Clients walk the full
        chain by following `parent.wellKnownUrl` per response, one hop at a
        time.
      operationId: getRunAncestry
      parameters:
        - $ref: '#/components/parameters/RunId'
      responses:
        '200':
          description: |
            Run's immediate parent (or `parent: null` for top-level runs).
          content:
            application/json:
              schema:
                $ref: '../schemas/run-ancestry-response.schema.json'
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404':
          description: |
            Either the run doesn't exist, OR the host doesn't advertise
            `crossHostCausation.ancestryEndpointSupported: true` and treats
            the endpoint as absent. Clients can disambiguate by inspecting
            the host's discovery doc.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /v1/agents:
    get:
      tags: [agents]
      summary: |
        RFC 0072 §A — list the manifest agents this host has installed into its
        AgentRegistry (RFC 0070). Capability-gated on
        `capabilities.agents.manifestRuntime.supported: true`; hosts that don't
        advertise it return 404. Read-only projection — never carries the
        system-prompt body, resolved handoff schemas, or credential material (SR-1).
        Dispatch is not a bespoke endpoint: a manifest agent is invoked as a run
        whose node pins it via `WorkflowNode.agent` + `POST /v1/runs` (RFC 0072 §B).
        RFC 0074 — the result is scoped to the authenticated principal's owner
        triple (RFC 0048). When `capabilities.agents.manifestRuntime.installScope`
        is `'tenant'`, only the agents available to the caller's tenant·workspace
        are returned (an agent another workspace installed is absent, never
        disclosed); when `'host'` (default) the inventory is host-global as in
        RFC 0072. A `'tenant'`-scoped host MUST reject unauthenticated/unscoped
        requests per its standard auth contract rather than fall back to a global list.
      operationId: listAgents
      responses:
        '200':
          description: Installed manifest agents (agentId-sorted).
          content:
            application/json:
              schema:
                $ref: '../schemas/agent-inventory-response.schema.json'
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404':
          description: |
            Host does not advertise `capabilities.agents.manifestRuntime` and
            treats the endpoint as absent.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /v1/agents/{agentId}:
    get:
      tags: [agents]
      summary: |
        RFC 0072 §A — return one installed manifest agent's inventory entry, or
        404 when no such agent is installed (or the host doesn't advertise
        `capabilities.agents.manifestRuntime`). RFC 0074 — resolved within the
        authenticated principal's owner triple (RFC 0048): on an
        `installScope: 'tenant'` host an agent the caller's workspace has not
        approved 404s identically to "not installed", so the surface never
        discloses another tenant's inventory.
      operationId: getAgent
      parameters:
        - in: path
          name: agentId
          required: true
          schema: { type: string, pattern: '^(?!roster$|org-chart$).+$' }
          description: |
            The manifest agentId. MUST NOT be the reserved literals `roster` or
            `org-chart` — those name the sibling collection routes
            (`/v1/agents/roster`, `/v1/agents/org-chart`), so excluding them here
            keeps `/v1/agents/{agentId}` unambiguous against them.
      responses:
        '200':
          description: The agent's inventory entry.
          content:
            application/json:
              schema:
                $ref: '../schemas/agent-inventory-response.schema.json#/$defs/AgentInventoryEntry'
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404':
          description: No such agent, or the host doesn't advertise the capability.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /v1/agents/{agentId}/deployments:
    get:
      tags: [agents]
      summary: |
        RFC 0082 §C/§E — list the deployment records (per-(agentId, version)) for
        a manifest agent: the lifecycle `state`, the named `channels`, the canary
        share, the rollback pointer, and the last-transition provenance. Read-only,
        content-free of any manifest body or credential (SR-1). Capability-gated on
        `capabilities.agents.deployment.supported: true`; hosts that don't advertise
        it return 404. Tenant-scoped to the caller's owner triple (RFC 0048/0074)
        when `installScope: 'tenant'`.
      operationId: listAgentDeployments
      parameters:
        - in: path
          name: agentId
          required: true
          schema: { type: string, pattern: '^(?!roster$|org-chart$).+$' }
          description: |
            The manifest agentId. MUST NOT be the reserved literals `roster` or
            `org-chart` — those name the sibling collection routes
            (`/v1/agents/roster/{rosterId}`, `/v1/agents/org-chart/{departmentId}`),
            so excluding them here keeps `/v1/agents/{agentId}/deployments`
            unambiguous against them.
      responses:
        '200':
          description: The agent's deployment records (version-sorted).
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '../schemas/agent-deployment.schema.json' }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404':
          description: No such agent, or the host doesn't advertise `capabilities.agents.deployment`.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
    post:
      tags: [agents]
      summary: |
        RFC 0082 §E — request a deployment state transition (promote / pause /
        deprecate / rollback / adjust-canary). The host MUST authorize fail-closed
        against the RFC 0049 `deploy:*` scope (absent/unseeded role denies), run any
        configured RFC 0051 approvalGate, and — when the gate carries `requiredEval`
        — verify the referenced RFC 0081 eval run is terminal and `EvalSummary.passed`
        BEFORE emitting `deployment.promoted`. On success returns the updated
        deployment record and emits the matching content-free `deployment.*` event.
      operationId: transitionAgentDeployment
      parameters:
        - in: path
          name: agentId
          required: true
          schema: { type: string }
          description: The manifest agentId.
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '../schemas/agent-deployment-transition.schema.json'
      responses:
        '200':
          description: The deployment record after the applied transition.
          content:
            application/json:
              schema:
                $ref: '../schemas/agent-deployment.schema.json'
        '400':
          description: |
            Validation error, or a transition that the host's advertised
            `states`/`canary` cannot satisfy, or `no_active_deployment` when a
            referenced channel resolves to no active version.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403':
          description: |
            Fail-closed authorization denial (the principal lacks the required
            `deploy:*` scope — RFC 0049), or `eval_gate_unmet` when a `requiredEval`
            gate's referenced eval run is not terminal-and-passed (RFC 0081).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '404':
          description: No such agent, or the host doesn't advertise `capabilities.agents.deployment`.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /v1/agents/roster:
    get:
      tags: [agents]
      summary: |
        RFC 0086 §B — list the standing agent roster (named "digital-twin
        employee" instances + their workflow portfolios) visible to the
        caller. Capability-gated on `capabilities.agents.roster.supported:
        true`; hosts that don't advertise it return 404. Tenant-scoped per
        RFC 0074 — on an `installScope: 'tenant'` host only the caller's
        owner-triple entries are returned. Read-only; content-free (SR-1).
      operationId: listAgentRoster
      responses:
        '200':
          description: The caller's standing roster (rosterId-sorted).
          content:
            application/json:
              schema:
                $ref: '../schemas/agent-roster-response.schema.json'
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404':
          description: Host does not advertise `capabilities.agents.roster`.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /v1/agents/roster/{rosterId}:
    get:
      tags: [agents]
      summary: |
        RFC 0086 §B — return one standing roster entry, or 404 when no such
        entry exists, the host doesn't advertise `capabilities.agents.roster`,
        or (on an `installScope: 'tenant'` host) the entry is outside the
        caller's owner triple — a cross-tenant entry 404s identically to
        "not found", never disclosing another tenant's roster.
      operationId: getAgentRosterEntry
      parameters:
        - in: path
          name: rosterId
          required: true
          schema: { type: string }
          description: The standing instance id (a `host:<id>` AgentRef agentId).
      responses:
        '200':
          description: The roster entry.
          content:
            application/json:
              schema:
                $ref: '../schemas/agent-roster-entry.schema.json'
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404':
          description: No such entry, cross-tenant, or capability unadvertised.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /v1/agents/org-chart:
    get:
      tags: [agents]
      summary: |
        RFC 0087 §C — return the caller's agent org-chart (departments + roles
        + `reportsTo` edges over roster members). Capability-gated on
        `capabilities.agents.orgChart.supported: true`; hosts that don't
        advertise it return 404. Tenant-scoped per RFC 0074. DESCRIPTIVE only:
        an org edge confers no authority (§B `org-position-no-authority-escalation`).
      operationId: getAgentOrgChart
      responses:
        '200':
          description: The caller's org-chart.
          content:
            application/json:
              schema:
                $ref: '../schemas/agent-org-chart.schema.json'
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404':
          description: Host does not advertise `capabilities.agents.orgChart`.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /v1/agents/org-chart/{departmentId}:
    get:
      tags: [agents]
      summary: |
        RFC 0087 §D — one department's subtree + responsibility roll-up (the
        union of its members' RFC 0086 portfolios). `?recursive=false` narrows
        the roll-up to direct members without changing the response shape.
        404 when the department is unknown, cross-tenant, or the host doesn't
        advertise `capabilities.agents.orgChart`. The roll-up grants nothing (§B).
      operationId: getAgentOrgChartDepartment
      parameters:
        - in: path
          name: departmentId
          required: true
          schema: { type: string }
          description: The department id to root the subtree + roll-up at.
        - in: query
          name: recursive
          required: false
          schema: { type: boolean, default: true }
          description: When `false`, the roll-up scopes to direct members only.
      responses:
        '200':
          description: The department subtree + responsibility roll-up.
          content:
            application/json:
              schema:
                $ref: '../schemas/org-chart-responsibility-view.schema.json'
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404':
          description: Unknown/cross-tenant department, or capability unadvertised.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /v1/tools:
    get:
      tags: [tools]
      summary: |
        RFC 0078 §B — list the portable `ToolDescriptor`s visible to the caller
        across every tool source (node-pack / workflow / mcp / connector /
        host-extension). Capability-gated on
        `capabilities.toolCatalog.supported: true`; hosts that don't advertise
        it return 404. §F-2 — the projection is authorization-scoped: a
        principal sees only the tools it may invoke, and a second principal MUST
        NOT see another principal's tools (non-disclosure). Read-only; each
        descriptor is content-free of any credential material (SR-1).
      operationId: listTools
      responses:
        '200':
          description: The caller's authorized tool catalog.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '../schemas/tool-descriptor.schema.json' }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404':
          description: Host does not advertise `capabilities.toolCatalog`.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /v1/tools/{toolId}:
    get:
      tags: [tools]
      summary: |
        RFC 0078 §B — return one `ToolDescriptor` by its stable `toolId`, or 404
        when no such tool exists, the caller isn't authorized for it (§F-2
        non-disclosure — a cross-principal tool 404s identically to "not
        found"), or the host doesn't advertise `capabilities.toolCatalog`.
      operationId: getTool
      parameters:
        - in: path
          name: toolId
          required: true
          schema: { type: string }
          description: The stable tool id (`ToolDescriptor.toolId`).
      responses:
        '200':
          description: The tool descriptor.
          content:
            application/json:
              schema: { $ref: '../schemas/tool-descriptor.schema.json' }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404':
          description: No such tool, unauthorized, or capability unadvertised.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /v1/runs/{runId}/eval-summary:
    get:
      tags: [runs]
      summary: |
        RFC 0081 §C — return the `EvalSummary` scorecard for a terminal eval run
        (a run started with `mode: "eval"`): aggregate + per-task scores, cost,
        latency, schema-validity, and redaction-safe safety findings, plus the
        suite provenance and (regression mode) the score delta vs a baseline.
        Content-free of task output / rubric prose / credentials (SR-1; the
        `eval-summary-no-content-leak` invariant). Capability-gated on
        `capabilities.agents.evalSuite.supported: true`; hosts that don't advertise
        it return 404. 409 when the run is not yet terminal.
      operationId: getEvalSummary
      parameters:
        - $ref: '#/components/parameters/RunId'
      responses:
        '200':
          description: The eval run's scorecard.
          content:
            application/json:
              schema:
                $ref: '../schemas/eval-summary.schema.json'
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404':
          description: |
            No such run, the run is not an eval run, or the host doesn't advertise
            `capabilities.agents.evalSuite`.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '409':
          description: The eval run is still running; the summary is not yet final.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /v1/runs/{runId}:diff:
    get:
      tags: [runs]
      summary: |
        RFC 0054 — return a deterministic, replay-aware structured diff of
        two runs (typically a run and its RFC 0011 fork): `divergedAtSeq` +
        ordered `eventDiffs[]` + `stateDiff`. The diff is a pure function of
        the two event logs (see `replay.md` determinism contract). Requires
        `runs:read` on BOTH runs. Hosts that don't implement it return 404.
      operationId: diffRun
      parameters:
        - $ref: '#/components/parameters/RunId'
        - name: against
          in: query
          required: true
          description: The other run id to diff `{runId}` against (the `b` run).
          schema: { type: string }
      responses:
        '200':
          description: |
            Structured diff of the two runs. `divergedAtSeq: null` + empty
            `eventDiffs` when the logs are identical.
          content:
            application/json:
              schema:
                $ref: '../schemas/run-diff-response.schema.json'
        '400':
          description: Missing or malformed `against` query parameter.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403':
          description: |
            Caller lacks `runs:read` on `{runId}` and/or on `against`
            (`forbidden`); composes with RFC 0048 cross-workspace
            isolation.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '404':
          description: |
            Either run doesn't exist, OR the host doesn't implement the diff
            endpoint and treats the path as absent.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /v1/runs/{runId}:pause:
    post:
      tags: [runs]
      summary: Administratively pause an in-flight run (RFC Track 13).
      description: |
        Operator-driven pause distinct from cancel (terminal) and HITL suspend (workflow-driven).
        Emits a `run.paused` event when the pause takes effect; exit only via `:resume` or `:cancel`.
      operationId: pauseRun
      parameters:
        - $ref: '#/components/parameters/RunId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
                  description: Free-form rationale, persisted on the `run.paused` event payload.
                drainPolicy:
                  type: string
                  enum: [immediate, drain-current-node]
                  default: drain-current-node
                  description: |
                    `immediate` snapshots between events; `drain-current-node` lets the running node
                    reach a terminal before transitioning to `paused`.
              additionalProperties: false
      responses:
        '202':
          description: Pause requested; transition emits `run.paused` when complete.
          content:
            application/json:
              schema:
                type: object
                required: [runId, status]
                properties:
                  runId: { type: string }
                  status: { type: string, enum: [paused] }
                  pausedAt: { type: string, format: date-time }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: Run is already paused, terminal, or in a state that cannot be paused.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /v1/runs/{runId}:resume:
    post:
      tags: [runs]
      summary: Resume a paused run (RFC Track 13).
      description: |
        Reverses a prior `:pause`. Run transitions from `paused` to `running` and emits `run.resumed`.
      operationId: resumeRun
      parameters:
        - $ref: '#/components/parameters/RunId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason: { type: string }
              additionalProperties: false
      responses:
        '202':
          description: Resume requested.
          content:
            application/json:
              schema:
                type: object
                required: [runId, status]
                properties:
                  runId: { type: string }
                  status: { type: string, enum: [running] }
                  resumedAt: { type: string, format: date-time }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: Run is not currently paused.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  # ── HITL ────────────────────────────────────────────────────────────────
  /v1/runs/{runId}/interrupts/{nodeId}:
    post:
      tags: [hitl]
      summary: Resolve an interrupt via the run-scoped surface.
      operationId: resolveInterruptByRun
      parameters:
        - $ref: '#/components/parameters/RunId'
        - in: path
          name: nodeId
          required: true
          schema: { type: string, minLength: 1 }
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [resumeValue]
              properties:
                resumeValue:
                  description: Validated against the interrupt's `resumeSchema` if declared.
              additionalProperties: false
      responses:
        '200':
          description: Interrupt resolved; executor unblocks.
          content:
            application/json:
              schema:
                type: object
                properties:
                  runId: { type: string }
                  nodeId: { type: string }
                  status: { type: string }
        '400': { $ref: '#/components/responses/ValidationError' }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404':
          description: Interrupt not found or already resolved.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '409':
          description: Concurrent resolve — only one wins.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '422':
          description: Run was cancelled while interrupt was pending.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /v1/interrupts/{token}:
    parameters:
      - in: path
        name: token
        required: true
        schema: { type: string }
        description: HMAC-signed token issued by the server at suspension time. Format `base64url(payload).hmac_sha256(secret, payload)`.
    get:
      tags: [hitl]
      summary: Inspect an interrupt without resolving (signed-token surface).
      operationId: inspectInterruptByToken
      security: []  # token is the auth
      responses:
        '200':
          description: Interrupt details.
          content:
            application/json:
              schema: { $ref: '../schemas/suspend-request.schema.json' }
        '410':
          description: Token expired.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
    post:
      tags: [hitl]
      summary: Resolve an interrupt via signed token (asynchronous callback).
      operationId: resolveInterruptByToken
      security: []  # token is the auth
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [resumeValue]
              properties:
                resumeValue: {}
              additionalProperties: false
      responses:
        '200':
          description: Resolution accepted.
          content:
            application/json:
              schema: { type: object }
        '410':
          description: Token expired.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  # ── Artifacts ───────────────────────────────────────────────────────────
  /v1/runs/{runId}/artifacts/{artifactId}:
    get:
      tags: [artifacts]
      summary: Read a run-produced artifact.
      operationId: getArtifact
      parameters:
        - $ref: '#/components/parameters/RunId'
        - in: path
          name: artifactId
          required: true
          schema: { type: string, minLength: 1 }
      responses:
        '200':
          description: Artifact payload.
          content:
            application/json:
              schema:
                type: object
                description: Implementation-defined artifact shape.
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  # ── Webhooks ────────────────────────────────────────────────────────────
  /v1/webhooks:
    post:
      tags: [webhooks]
      summary: Register a webhook subscription.
      operationId: registerWebhook
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url: { type: string, format: uri }
                events:
                  type: array
                  items: { type: string }
                  description: Event types to subscribe to (see `run-event.schema.json` enum).
                secret:
                  type: string
                  description: Server signs payloads with this secret using HMAC-SHA256.
                tags:
                  type: array
                  items: { type: string }
                  description: Filter to runs carrying these tags (see `run-options.md`).
              additionalProperties: false
      responses:
        '201':
          description: Webhook registered.
          content:
            application/json:
              schema:
                type: object
                required: [webhookId]
                properties:
                  webhookId: { type: string }
        '400': { $ref: '#/components/responses/ValidationError' }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /v1/webhooks/{webhookId}:
    delete:
      tags: [webhooks]
      summary: Unregister a webhook.
      operationId: unregisterWebhook
      parameters:
        - in: path
          name: webhookId
          required: true
          schema: { type: string, minLength: 1 }
      responses:
        '204':
          description: Unregistered.
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  # ── Audit-log integrity verification (gated on profile) ────────────────
  /v1/audit/verify:
    get:
      tags: [audit]
      summary: Verify the audit-log hash chain over [fromSeq, toSeq].
      description: |
        Per `spec/v1/auth-profiles.md` §`openwop-audit-log-integrity` §4. The
        verifier re-walks audit-log entries in the requested range,
        re-computes each entry's `prevHash` from the canonical RFC 8785 JCS
        serialization of the prior entry, verifies signed checkpoints
        against the host's advertised `auditLogIntegrity.checkpointPublicKey`,
        and returns `chainValid` + an enumeration of any anomalies.
        Hosts MUST require the `audit:read` scope. Hosts that do NOT
        advertise the `openwop-audit-log-integrity` profile MAY omit this
        endpoint entirely (clients SHOULD pre-flight via `/.well-known/openwop`).
      operationId: verifyAuditLog
      parameters:
        - in: query
          name: fromSeq
          required: true
          schema: { type: integer, minimum: 0 }
          description: First audit-log sequence to include (inclusive).
        - in: query
          name: toSeq
          required: true
          schema: { type: integer, minimum: 0 }
          description: Last audit-log sequence to include (inclusive). MUST be >= fromSeq.
      responses:
        '200':
          description: Verification result.
          content:
            application/json:
              schema:
                $ref: '../schemas/audit-verify-result.schema.json'
        '400': { $ref: '#/components/responses/ValidationError' }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404':
          description: Host does not advertise the audit-log-integrity profile.
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'

  # ── Prompt library (RFC 0028) ────────────────────────────────────────
  # Surface gated on `capabilities.prompts.supported: true`. Mutating
  # endpoints (POST / PUT / DELETE) are additionally gated on
  # `capabilities.prompts.mutableLibrary: true`. Hosts without the
  # advertised capability return `501 capability_not_provided`.
  /v1/prompts:
    get:
      tags: [prompts]
      summary: List prompt templates available to the caller.
      operationId: listPromptTemplates
      parameters:
        - in: query
          name: kind
          schema: { type: string, enum: [system, user, few-shot, schema-hint] }
          description: Filter by `PromptTemplate.kind`.
        - in: query
          name: tag
          schema: { type: string }
          description: |
            Filter to templates whose `tags[]` contains this exact tag.
            Hosts MAY accept the parameter multiple times; the semantic
            when repeated is AND (every named tag must be present).
        - in: query
          name: modelClass
          schema: { type: string }
          description: Filter to templates whose `modelHints.modelClass` matches.
        - in: query
          name: source
          schema: { type: string, enum: [host, pack, user] }
          description: Filter by `meta.source` provenance.
        - in: query
          name: cursor
          schema: { type: string }
          description: Opaque pagination cursor.
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
          description: Maximum entries per page.
      responses:
        '200':
          description: Paginated list of templates.
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items:
                      $ref: '../schemas/prompt-template.schema.json'
                  nextCursor:
                    type: string
                    description: Opaque cursor; absent on the final page.
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '501':
          description: 'Host does not advertise capabilities.prompts.endpointsSupported. (RFC 0028 §A — supported gates Phase A node-execution composition; endpointsSupported gates this REST surface independently.)'
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'
    post:
      tags: [prompts]
      summary: Create a new prompt template (mutable libraries only).
      operationId: createPromptTemplate
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '../schemas/prompt-template.schema.json'
      responses:
        '201':
          description: Template created. `Location` header carries the canonical URI.
          headers:
            Location:
              schema: { type: string }
              description: 'Canonical URI of the new template.'
        '400': { $ref: '#/components/responses/ValidationError' }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409':
          description: A template with this `(templateId, version)` pair already exists.
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'
        '501':
          description: 'Host does not advertise capabilities.prompts.mutableLibrary.'
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'

  /v1/prompts/{templateId}:
    parameters:
      - in: path
        name: templateId
        required: true
        schema:
          type: string
          pattern: '^[a-z0-9][a-z0-9._-]{0,127}$'
        description: PromptTemplate.templateId per RFC 0027.
      - in: query
        name: version
        schema:
          type: string
          pattern: '^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$'
        description: Pin to a specific SemVer version; latest when omitted.
      - in: query
        name: libraryId
        schema:
          type: string
          pattern: '^[a-z0-9][a-z0-9._-]{0,127}$'
        description: |
          Disambiguate when multiple installed packs ship the same
          templateId. Hosts MUST return `prompt_ref_ambiguous` if
          ambiguous and libraryId is omitted.
    get:
      tags: [prompts]
      summary: Fetch a single prompt template.
      operationId: getPromptTemplate
      responses:
        '200':
          description: The PromptTemplate.
          headers:
            ETag:
              schema: { type: string }
              description: SHA-256 of the canonical body.
            Cache-Control:
              schema: { type: string }
              description: Honors immutable semantics when version was pinned.
          content:
            application/json:
              schema:
                $ref: '../schemas/prompt-template.schema.json'
        '304':
          description: Conditional revalidation succeeded.
        '400':
          description: '`prompt_ref_ambiguous` when libraryId disambiguation is required.'
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404':
          description: No such template (or version).
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'
        '501':
          description: 'Host does not advertise capabilities.prompts.endpointsSupported. (RFC 0028 §A — supported gates Phase A node-execution composition; endpointsSupported gates this REST surface independently.)'
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'
    put:
      tags: [prompts]
      summary: Replace a prompt template (mutable libraries; user-source only).
      operationId: updatePromptTemplate
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '../schemas/prompt-template.schema.json'
      responses:
        '200':
          description: Template updated.
          content:
            application/json:
              schema:
                $ref: '../schemas/prompt-template.schema.json'
        '400': { $ref: '#/components/responses/ValidationError' }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403':
          description: Template is pack-sourced or host-built-in (read-only).
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'
        '404':
          description: No such template.
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'
        '409':
          description: Submitted version does not exceed stored version (SemVer).
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'
        '501':
          description: 'Host does not advertise capabilities.prompts.mutableLibrary.'
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'
    delete:
      tags: [prompts]
      summary: Delete a prompt template (mutable libraries; user-source only).
      operationId: deletePromptTemplate
      responses:
        '204':
          description: Template deleted.
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403':
          description: Template is pack-sourced or host-built-in (read-only).
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'
        '404':
          description: No such template.
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'
        '501':
          description: 'Host does not advertise capabilities.prompts.mutableLibrary.'
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'

  /v1/prompts:render:
    post:
      tags: [prompts]
      summary: Render a prompt template with supplied variable bindings.
      description: |
        Returns the composed body + sha256 hash + per-variable hashes.
        The response's `hash` MUST equal the `hash` that a matching
        `prompt.composed` event would carry at dispatch time for the
        same `(ref, variables, contentTrust)` inputs (RFC 0028 §A
        deterministic-render invariant; RFC 0027 §F replay invariant).
        Does NOT dispatch an LLM call. Secret-source variable values
        MUST be supplied as `[REDACTED:<credentialRef>]` markers; the
        host resolves the plaintext internally and never echoes it in
        the `composed` response field per SR-1.
      operationId: renderPromptTemplate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ref, variables]
              properties:
                ref:
                  $ref: '../schemas/prompt-ref.schema.json'
                variables:
                  type: object
                  description: |
                    Variable bindings keyed by `PromptVariable.name`.
                    Secret-source bindings carry `[REDACTED:<credentialRef>]`
                    markers; the host resolves the real value internally.
                  additionalProperties: true
                contentTrust:
                  type: string
                  enum: [trusted, untrusted]
                  description: |
                    Aggregate trust marker for the supplied bindings,
                    propagated through composition per RFC 0027 §E.
      responses:
        '200':
          description: Composed result.
          content:
            application/json:
              schema:
                type: object
                required: [hash, refs, variableHashes]
                properties:
                  composed:
                    type: string
                    description: Full composed body. Present only when observability is `full`.
                  hash:
                    type: string
                    pattern: '^sha256:[0-9a-f]{64}$'
                  refs:
                    type: array
                    items:
                      type: string
                  variableHashes:
                    type: object
                    additionalProperties:
                      type: string
                      pattern: '^sha256:[0-9a-f]{64}$'
                  contentTrust:
                    type: string
                    enum: [trusted, untrusted]
        '400':
          description: |
            `prompt_variable_unresolved` (required variable missing),
            `prompt_variable_type_mismatch` (bound type vs. declared type),
            or `prompt_ref_invalid` (malformed PromptRef).
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404':
          description: Referenced template does not exist.
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'
        '501':
          description: 'Host does not advertise capabilities.prompts.endpointsSupported. (RFC 0028 §A — supported gates Phase A node-execution composition; endpointsSupported gates this REST surface independently.)'
          content:
            application/json:
              schema:
                $ref: '../schemas/error-envelope.schema.json'

  # ── Test-mode pack registry namespace (RFC 0025) ─────────────────────────
  # Mirrors the production /v1/packs/* PUT/GET/DELETE/.sig surface against an
  # isolated catalog. Conformance scenarios under
  # `conformance/src/scenarios/pack-registry-publish.test.ts` exercise the
  # 19-code publish error catalog through this namespace. Hosts that don't
  # advertise `capabilities.packs.testMode.supported: true` MUST return
  # 404 for every path below.

  /v1/packs-test/{name}/-/{version}.tgz:
    parameters:
      - $ref: '#/components/parameters/PackName'
      - $ref: '#/components/parameters/PackVersion'
    put:
      tags: [packs-test]
      summary: Publish a pack tarball to the isolated test catalog.
      description: |
        Mirror of `PUT /v1/packs/{name}/-/{version}.tgz` per
        `spec/v1/node-packs.md` §"PUT /v1/packs/{name}/-/{version}.tgz".
        Request shape, response shape, status codes, and the 19-code
        publish error catalog (`invalid_pack_scope`, `invalid_pack_name`,
        `invalid_version`, `invalid_body`, eight `tarball_*` codes,
        `invalid_manifest`, `manifest_mismatch` (or the granular
        `manifest_name_mismatch` / `manifest_version_mismatch` pair),
        `pack_integrity_failure`, `unsupported_runtime`, `forbidden`,
        `conflict`/`version_conflict`) MUST be served verbatim. The
        test catalog MUST be isolated per RFC 0025 §C — a pack PUT'd
        here MUST NOT appear in `GET /v1/packs/{name}` listings.
        The mirrored production path is registry-service surface
        (`registry-operations.md`), not defined in this host document
        (see the `packs-test` tag scope note).
      operationId: putTestPackTarball
      parameters:
        - in: header
          name: X-Pack-Signing-Method
          required: false
          schema: { type: string, enum: [sigstore, manual, none] }
        - in: header
          name: X-Pack-Sha256
          required: false
          schema:
            type: string
            pattern: '^sha256-[A-Za-z0-9+/=]+$'
          description: Caller-asserted SHA-256 (server verifies; mismatch surfaces `pack_integrity_failure`).
      requestBody:
        required: true
        description: Gzipped tarball bytes (`application/tar+gzip`, `application/gzip`, `application/x-gzip`, or `application/octet-stream`).
        content:
          application/gzip:
            schema: { type: string, format: binary }
          application/x-gzip:
            schema: { type: string, format: binary }
          application/tar+gzip:
            schema: { type: string, format: binary }
          application/octet-stream:
            schema: { type: string, format: binary }
      responses:
        '200':
          description: Idempotent re-publish — identical sha256 content already published; existing record returned.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TestPackPublishRecord' }
        '201':
          description: New version published to the test catalog.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TestPackPublishRecord' }
        '400':
          description: |
            One of the 17 spec-documented 400-class publish error codes:
            URL/scope (`invalid_pack_scope`, `invalid_pack_name`, `invalid_version`),
            body shape (`invalid_body`),
            tarball extraction (`tarball_gunzip_failed`, `tarball_too_large`,
            `tarball_manifest_missing`, `tarball_manifest_too_large`,
            `tarball_manifest_not_json`, `tarball_entry_missing`,
            `tarball_entry_too_large`, `tarball_path_traversal`,
            `tarball_tar_parse_failed`),
            manifest contents (`invalid_manifest`, `manifest_mismatch` or
            the granular `manifest_name_mismatch` / `manifest_version_mismatch` pair,
            `pack_integrity_failure`, `unsupported_runtime`).
          content:
            application/json:
              schema: { $ref: '../schemas/error-envelope.schema.json' }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403':
          description: '`forbidden` — caller lacks `packs:publish` scope or the namespace claim.'
          content:
            application/json:
              schema: { $ref: '../schemas/error-envelope.schema.json' }
        '404':
          description: 'Host does not advertise `capabilities.packs.testMode.supported: true`, or the test-mode env-gate is unset.'
          content:
            application/json:
              schema: { $ref: '../schemas/error-envelope.schema.json' }
        '409':
          description: '`conflict` (or `version_conflict`) — `(name, version)` already published with different content.'
          content:
            application/json:
              schema: { $ref: '../schemas/error-envelope.schema.json' }
    get:
      tags: [packs-test]
      summary: Fetch a published test-catalog tarball.
      description: 'Mirror of `GET /v1/packs/{name}/-/{version}.tgz`. Returns the gzipped tarball bytes with `Content-Type: application/tar+gzip` and an `ETag: "sha256-..."` matching the manifest''s `tarballSha256`. The mirrored production path is registry-service surface (`registry-operations.md`), not defined in this host document (see the `packs-test` tag scope note).'
      operationId: getTestPackTarball
      responses:
        '200':
          description: Tarball bytes.
          content:
            application/tar+gzip:
              schema: { type: string, format: binary }
        '400':
          description: '`invalid_pack_name` or `invalid_version` — URL params malformed.'
          content:
            application/json:
              schema: { $ref: '../schemas/error-envelope.schema.json' }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403':
          description: '`forbidden` — caller lacks `packs:read` scope.'
          content:
            application/json:
              schema: { $ref: '../schemas/error-envelope.schema.json' }
        '404':
          description: 'Pack version not found in the test catalog (or host does not advertise `capabilities.packs.testMode.supported: true`).'
          content:
            application/json:
              schema: { $ref: '../schemas/error-envelope.schema.json' }
  /v1/packs-test/{name}/-/{version}:
    parameters:
      - $ref: '#/components/parameters/PackName'
      - $ref: '#/components/parameters/PackVersion'
    delete:
      tags: [packs-test]
      summary: Unpublish a test-catalog version (mirrors unpublish-window semantics).
      description: |
        Mirror of `DELETE /v1/packs/{name}/-/{version}` per
        `spec/v1/node-packs.md`. Returns `400 unpublish_window_expired`
        for versions older than the registry's unpublish window
        (default 72h). Test-mode implementations MAY shorten the
        window for tractable conformance fixtures but MUST surface
        the same error code. The mirrored production path is
        registry-service surface (`registry-operations.md`), not
        defined in this host document (see the `packs-test` tag
        scope note).
      operationId: deleteTestPackVersion
      responses:
        '204':
          description: Version successfully unpublished from the test catalog.
        '400':
          description: '`unpublish_window_expired`, `invalid_pack_name`, or `invalid_version`.'
          content:
            application/json:
              schema: { $ref: '../schemas/error-envelope.schema.json' }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403':
          description: '`forbidden` — caller lacks `packs:publish` scope.'
          content:
            application/json:
              schema: { $ref: '../schemas/error-envelope.schema.json' }
        '404':
          description: Version doesn't exist in the test catalog, or host does not advertise the test-mode capability.
          content:
            application/json:
              schema: { $ref: '../schemas/error-envelope.schema.json' }

  /v1/packs-test/{name}/-/{version}.sig:
    parameters:
      - $ref: '#/components/parameters/PackName'
      - $ref: '#/components/parameters/PackVersion'
    get:
      tags: [packs-test]
      summary: Fetch the detached Ed25519 signature for a test-catalog pack.
      description: |
        Mirror of `GET /v1/packs/{name}/-/{version}.sig`. Returns the
        signature blob over `pack.json` for this version. MAY 302-redirect
        to a storage-backend signed URL. The mirrored production path is
        registry-service surface (`registry-operations.md`), not defined
        in this host document (see the `packs-test` tag scope note).
      operationId: getTestPackSignature
      responses:
        '200':
          description: Signature blob.
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        '302':
          description: Redirect to a storage-backend signed URL (clients SHOULD follow).
          headers:
            Location:
              schema: { type: string, format: uri }
        '400':
          description: '`invalid_pack_name` or `invalid_version` — URL params malformed.'
          content:
            application/json:
              schema: { $ref: '../schemas/error-envelope.schema.json' }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403':
          description: '`forbidden` — caller lacks `packs:read` scope.'
          content:
            application/json:
              schema: { $ref: '../schemas/error-envelope.schema.json' }
        '404':
          description: |
            `signature_not_available` — version is missing, yanked,
            unsigned at publish time, OR the registry's storage backend
            is unwired. The four cases are intentionally
            indistinguishable per spec/v1/node-packs.md §"GET /v1/packs/{name}/-/{version}.sig".
            Also returned when the host does not advertise
            `capabilities.packs.testMode.supported: true`.
          content:
            application/json:
              schema: { $ref: '../schemas/error-envelope.schema.json' }

# ─────────────────────────────────────────────────────────────────────────────
# COMPONENTS
# ─────────────────────────────────────────────────────────────────────────────
components:

  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: API key
      description: |
        openwop API key. Format implementation-defined; reference impl uses `hk_`/`hk_test_` prefixes.
        Each key carries one or more scopes from the canonical vocabulary
        (`manifest:read`, `runs:create`, `runs:read`, `runs:cancel`,
        `artifacts:read`, `webhooks:manage`, `approvals:respond`, `audit:read`).
        See `auth.md`.

  parameters:
    WorkflowId:
      in: path
      name: workflowId
      required: true
      schema: { type: string, minLength: 1, maxLength: 128 }

    RunId:
      in: path
      name: runId
      required: true
      schema: { type: string, minLength: 1, maxLength: 128 }

    IdempotencyKey:
      in: header
      name: Idempotency-Key
      required: false
      schema:
        type: string
        maxLength: 255
      description: |
        Per-mutation idempotency token (see `idempotency.md` Layer 1).
        Server caches `(tenantId, endpoint, key)` → response for ≥24h.
        Duplicate requests return the cached response with header
        `openwop-Idempotent-Replay: true`.

    WorkspacePath:
      in: path
      name: path
      required: true
      schema:
        type: string
        pattern: '^[A-Za-z0-9][A-Za-z0-9._/-]{0,255}$'
      description: |
        RFC 0059 workspace-relative file path. Flat namespace with
        `/`-in-names; no `..`, no leading `/`. Matches
        `workspace-file.schema.json#path`.

    WorkspacePrefix:
      in: query
      name: prefix
      required: false
      schema: { type: string, maxLength: 256 }
      description: RFC 0059. Optional prefix filter over the flat `path` namespace for `listWorkspaceFiles`.

    WorkspaceVersion:
      in: query
      name: version
      required: false
      schema: { type: integer, minimum: 1 }
      description: |
        RFC 0059. When `capabilities.workspace.versioned: true`, request the
        historical snapshot at this version. Absent = latest.

    IfMatch:
      in: header
      name: If-Match
      required: false
      schema: { type: string, maxLength: 255 }
      description: |
        RFC 0059 optimistic-concurrency token — the file's current `etag`.
        A `PUT` carrying a stale `If-Match` returns `409 workspace_conflict`.

    PackName:
      in: path
      name: name
      required: true
      schema:
        type: string
        minLength: 3
        maxLength: 214
      description: Reverse-DNS pack name per `spec/v1/node-packs.md` §Naming (e.g. `vendor.acme.salesforce-tools`).

    PackVersion:
      in: path
      name: version
      required: true
      schema:
        type: string
        pattern: '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[\w.-]+)?(?:\+[\w.-]+)?$'
      description: SemVer 2.0.0 version of the pack.

  responses:
    Unauthenticated:
      description: Missing or invalid credential.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

    Forbidden:
      description: Credential valid but lacks required scope or fails resource binding.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

    NotFound:
      description: Resource doesn't exist or caller can't see it (do not leak existence).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

    ValidationError:
      description: Request body or parameters malformed.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

    RateLimited:
      description: Too many requests.
      headers:
        Retry-After:
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

  schemas:

    # Hoisted to first-class JSON Schemas in ../schemas/ so the SDK and
    # conformance suite can validate against the same source. The
    # in-line aliases below pull them via $ref so existing
    # `#/components/schemas/Error` references keep working.

    Error:
      $ref: '../schemas/error-envelope.schema.json'

    Capabilities:
      $ref: '../schemas/capabilities.schema.json'

    # Pre-loaded so redocly's $ref resolver registers them at lint time.
    # capabilities.schema.json `$ref`s prompt-kind.schema.json by its
    # canonical openwop.dev URL — redocly resolves absolute URIs against
    # already-loaded `$id`s, so the schema must be pulled in here even
    # though no operation references it directly. See RFC 0027 §A.
    PromptKind:
      $ref: '../schemas/prompt-kind.schema.json'

    PromptTemplate:
      $ref: '../schemas/prompt-template.schema.json'

    PromptRef:
      $ref: '../schemas/prompt-ref.schema.json'

    RunSnapshot:
      $ref: '../schemas/run-snapshot.schema.json'

    RunClaimConflict:
      description: |
        Specialization of the canonical `ErrorEnvelope` shape for
        `run_already_active`. Conflict metadata lives under `details`
        so the top-level error shape remains `{error, message, details?}`.
      allOf:
        - $ref: '#/components/schemas/Error'
        - type: object
          required: [error, message, details]
          properties:
            error:
              type: string
              enum: [run_already_active]
            message: { type: string }
            details:
              type: object
              required: [activeRunId, activeHost]
              properties:
                activeRunId: { type: string }
                activeHost:
                  type: string
                  enum: [browser, cloud]
                retryAfter:
                  type: integer
                  description: 'Seconds. Mirrors the `Retry-After` header.'

    UnsupportedStreamMode:
      description: |
        Specialization of the canonical `ErrorEnvelope` shape (see
        `error-envelope.schema.json`) for the `unsupported_stream_mode`
        case. The `supported` array lives in `details` per the canonical
        envelope's contextual-data slot, NOT at the top level. SDK
        consumers using a generic ErrorEnvelope parser will find the
        list under `details.supported` regardless of which validator
        fired.
      allOf:
        - $ref: '#/components/schemas/Error'
        - type: object
          required: [error, message, details]
          properties:
            error:
              type: string
              enum: [unsupported_stream_mode]
            message: { type: string }
            details:
              type: object
              required: [supported]
              properties:
                supported:
                  type: array
                  items: { type: string, enum: [values, updates, messages, debug] }

    TestPackPublishRecord:
      description: |
        Response body for a successful publish against the test-mode
        registry namespace (RFC 0025). Mirror of the production publish
        record returned by `PUT /v1/packs/{name}/-/{version}.tgz`.
      type: object
      required: [name, version, tarballSha256, publishedAt]
      properties:
        name:
          type: string
          description: Reverse-DNS pack name as PUT'd.
        version:
          type: string
          description: SemVer 2.0.0 version as PUT'd.
        tarballSha256:
          type: string
          pattern: '^sha256-[A-Za-z0-9+/=]+$'
          description: Server-computed SHA-256 over the uploaded tarball bytes.
        publishedAt:
          type: string
          format: date-time
        signed:
          type: boolean
          description: Whether a sibling `.sig` signature blob was persisted.
        signingMethod:
          type: string
          enum: [sigstore, manual, none]
