openapi: 3.1.0
info:
  title: Unified Harness Protocol
  version: '2026-08-11'
  summary: An open standard for running complete agent harnesses as shared infrastructure.
  description: |
    UHP defines how a product drives a complete agent harness — Codex, Claude Code, Hermes, or any
    other — over HTTP: selecting and configuring a harness, sending work, following progress,
    continuing a session, cancelling execution, handling errors and files, and receiving results.

    This document is normative for **structure**. The prose specification in `protocol/versions/`
    is normative for **behaviour**; where they disagree about behaviour, the prose wins, because
    behaviour is not expressible in OpenAPI.

    Conformance is defined by the suite in `protocol/conformance/`, not by this document.
  license:
    name: Apache-2.0
    identifier: Apache-2.0
  contact:
    name: UHP maintainers
    url: https://github.com/HarnessRouter/harnessrouter

servers:
  - url: https://your-uhp-server
    description: Any conformant UHP server. The protocol requires no hosted service.
  - url: http://127.0.0.1:3000/api/harness
    description: HarnessRouter Community Edition, default local deployment

security:
  - bearerAuth: []

tags:
  - name: Discovery
    description: Protocol version and capability discovery (conformance class Core)
  - name: Harnesses
    description: Discovering, selecting and managing configured harnesses
  - name: Tasks
    description: Running work and reading results
  - name: Sessions
    description: Continuing, inspecting and cancelling sessions
  - name: Files
    description: File input and artifact retrieval (conformance class Extended)

paths:
  /v1/uhp:
    get:
      tags: [Discovery]
      operationId: getDiscovery
      summary: Protocol discovery document
      description: |
        Served without authentication: a client must be able to learn whether it is talking to a UHP
        server, and which versions it speaks, before presenting credentials. The document contains
        nothing principal-specific.
      security: []
      responses:
        '200':
          description: The discovery document
          headers:
            UHP-Version: { $ref: '#/components/headers/UHPVersion' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Discovery' }

  /v1/harnesses:
    get:
      tags: [Harnesses]
      operationId: listHarnesses
      summary: List configured harnesses
      responses:
        '200':
          description: Harnesses within the caller's scope. May be empty.
          content:
            application/json:
              schema:
                type: object
                required: [harnesses]
                properties:
                  harnesses:
                    type: array
                    items: { $ref: '#/components/schemas/Harness' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [Harnesses]
      operationId: createHarness
      summary: Create a configured harness
      description: Conformance class Full. Check the `harness_management` capability first.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/HarnessCreate' }
      responses:
        '200':
          description: The created harness
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Harness' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '422': { $ref: '#/components/responses/UnprocessableEntity' }

  /v1/harnesses/{harness_id}:
    parameters:
      - $ref: '#/components/parameters/HarnessId'
    get:
      tags: [Harnesses]
      operationId: getHarness
      summary: Get one harness
      responses:
        '200':
          description: The harness
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Harness' }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Harnesses]
      operationId: updateHarness
      summary: Update a harness configuration
      description: |
        Conformance class Full. `id`, `base` and `createdAt` are immutable — changing the base of an
        existing harness would silently change the behaviour of every session attached to it.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/HarnessCreate' }
      responses:
        '200':
          description: The updated harness
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Harness' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Harnesses]
      operationId: deleteHarness
      summary: Delete a harness
      description: Must not delete the sessions or responses that used it.
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string }
                  deleted: { type: boolean }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/models:
    get:
      tags: [Harnesses]
      operationId: listModels
      summary: The model catalogue, by backend
      responses:
        '200':
          description: Models grouped by backend, each with computed availability
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ModelCatalog' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /v1/harnesses/{harness_id}/models:
    parameters:
      - $ref: '#/components/parameters/HarnessId'
    get:
      tags: [Harnesses]
      operationId: listHarnessModels
      summary: Models this harness can run
      responses:
        '200':
          description: The harness's allowed models, default and authorized fallback
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HarnessModels' }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/responses:
    post:
      tags: [Tasks]
      operationId: createResponse
      summary: Run a task
      description: |
        The core of the protocol. With `stream: false` the server returns one Response object when
        the task reaches a terminal state. With `stream: true` it returns `text/event-stream`
        carrying the events defined in the Streaming chapter.

        Retries MUST carry `Idempotency-Key`: without one, a retry after a timeout runs the task a
        second time while the first may still be running.
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema: { type: string, maxLength: 255 }
          description: Repeating a key returns the first request's result and does not re-execute.
        - name: UHP-Version
          in: header
          required: false
          schema: { type: string, examples: ['2026-08-11'] }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateResponseRequest' }
      responses:
        '200':
          description: |
            The finished Response (non-streaming), or the event stream (streaming).
          headers:
            UHP-Version: { $ref: '#/components/headers/UHPVersion' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Response' }
            text/event-stream:
              schema:
                type: string
                description: SSE; each `data:` line is one Event object.
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/UnprocessableEntity' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/responses/{response_id}:
    parameters:
      - $ref: '#/components/parameters/ResponseId'
    get:
      tags: [Tasks]
      operationId: getResponse
      summary: Read a task back
      responses:
        '200':
          description: The Response
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Response' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Tasks]
      operationId: deleteResponse
      summary: Delete a stored response
      description: Must not cancel a running task — deletion and cancellation are different intentions.
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string }
                  deleted: { type: boolean }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/responses/{response_id}/input_items:
    parameters:
      - $ref: '#/components/parameters/ResponseId'
    get:
      tags: [Tasks]
      operationId: getResponseInputItems
      summary: The input the task was created with
      responses:
        '200':
          description: Input items
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, const: list }
                  data: { type: array, items: { type: object, additionalProperties: true } }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/responses/{response_id}/cancel:
    parameters:
      - $ref: '#/components/parameters/ResponseId'
    post:
      tags: [Sessions]
      operationId: cancelResponse
      summary: Cancel a running task
      description: |
        Idempotent. Cancelling an already-terminal task succeeds and changes nothing — a client
        retrying a cancel after a dropped connection must not be punished for having succeeded.
      responses:
        '200':
          description: The Response, now cancelling or terminal
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Response' }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/sessions:
    get:
      tags: [Sessions]
      operationId: listSessions
      summary: List sessions
      description: Conformance class Extended.
      parameters:
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
        - { name: cursor, in: query, schema: { type: string } }
        - { name: harness, in: query, schema: { type: string }, description: Filter by harness id }
      responses:
        '200':
          description: A page of sessions
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SessionList' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /v1/sessions/{session_id}:
    parameters:
      - $ref: '#/components/parameters/SessionId'
    get:
      tags: [Sessions]
      operationId: getSession
      summary: Get one session
      responses:
        '200':
          description: The session
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Session' }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/sessions/{session_id}/turns:
    parameters:
      - $ref: '#/components/parameters/SessionId'
    get:
      tags: [Sessions]
      operationId: getSessionTurns
      summary: The ordered task history of a session
      responses:
        '200':
          description: Turns, oldest first
          content:
            application/json:
              schema:
                type: object
                properties:
                  turns: { type: array, items: { $ref: '#/components/schemas/TurnItem' } }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/sessions/{session_id}/share:
    parameters:
      - $ref: '#/components/parameters/SessionId'
    post:
      tags: [Sessions]
      operationId: shareSession
      summary: Publish a read-only view of the session (Sessions §5, class Full)
      description: |
        A body is OPTIONAL; no body means publish. A server MAY accept `{"enabled": bool}` as a
        toggle. Sharing itself is a MAY; a server that does not implement it answers 404/405/501.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                enabled: { type: boolean, default: true }
              additionalProperties: true
      responses:
        '200':
          description: The share
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SessionShare' }
        '404': { $ref: '#/components/responses/NotFound' }
    get:
      tags: [Sessions]
      operationId: getSessionShare
      summary: Read the session's share back
      responses:
        '200':
          description: The share, agreeing with what POST published
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SessionShare' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Sessions]
      operationId: revokeSessionShare
      summary: Revoke — every link minted for the session stops resolving
      responses:
        '200':
          description: Revoked
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SessionShare' }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/sessions/{session_id}/cancel:
    parameters:
      - $ref: '#/components/parameters/SessionId'
    post:
      tags: [Sessions]
      operationId: cancelSession
      summary: Cancel whatever is running in a session
      responses:
        '200':
          description: Accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string }
                  status: { type: string }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/files:
    post:
      tags: [Files]
      operationId: uploadFile
      summary: Upload a file for use as task input
      description: Conformance class Extended.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: { type: string, format: binary }
                purpose: { type: string, default: user_data }
      responses:
        '200':
          description: The stored file
          content:
            application/json:
              schema: { $ref: '#/components/schemas/File' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }

  /v1/sessions/{session_id}/files:
    parameters:
      - $ref: '#/components/parameters/SessionId'
    get:
      tags: [Files]
      operationId: listSessionFiles
      summary: Every artifact of a session
      description: Must include artifacts from earlier tasks, not only the most recent.
      responses:
        '200':
          description: Artifacts
          content:
            application/json:
              schema:
                type: object
                required: [files]
                properties:
                  files: { type: array, items: { $ref: '#/components/schemas/File' } }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/sessions/{session_id}/files/archive:
    parameters:
      - $ref: '#/components/parameters/SessionId'
    get:
      tags: [Files]
      operationId: downloadSessionArchive
      summary: Every artifact as one archive
      responses:
        '200':
          description: An archive of the session's artifacts
          content:
            application/zip:
              schema: { type: string, format: binary }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/containers/{container_id}/files/{file_id}/content:
    parameters:
      - $ref: '#/components/parameters/ContainerId'
      - $ref: '#/components/parameters/FileId'
    get:
      tags: [Files]
      operationId: downloadFile
      summary: Download an artifact
      description: |
        Returns raw bytes with the file's own media type — never JSON-wrapped. Servers must send
        `X-Content-Type-Options: nosniff`: artifacts are attacker-influenceable content, and serving
        them without it turns an artifact into stored XSS against the client's origin.
      responses:
        '200':
          description: The bytes
          headers:
            Content-Disposition:
              schema: { type: string }
            X-Content-Type-Options:
              required: true
              schema: { type: string, const: nosniff }
          content:
            '*/*':
              schema: { type: string, format: binary }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/containers/{container_id}/files/{file_id}/pdf:
    parameters:
      - $ref: '#/components/parameters/ContainerId'
      - $ref: '#/components/parameters/FileId'
    get:
      tags: [Files]
      operationId: previewFile
      summary: A rendered PDF preview of a document artifact
      responses:
        '200':
          description: The rendered PDF
          content:
            application/pdf:
              schema: { type: string, format: binary }
        '404': { $ref: '#/components/responses/NotFound' }
        '501':
          description: This server does not implement preview conversion
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        '502':
          description: Conversion of this file failed
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: How tokens are issued is out of scope for this protocol.

  headers:
    UHPVersion:
      description: The protocol version actually used to serve this response.
      required: true
      schema: { type: string, examples: ['2026-08-11'] }

  parameters:
    HarnessId:
      name: harness_id
      in: path
      required: true
      schema: { type: string, pattern: '^chrn_' }
    ResponseId:
      name: response_id
      in: path
      required: true
      schema: { type: string, pattern: '^resp_' }
    SessionId:
      name: session_id
      in: path
      required: true
      schema: { type: string }
    ContainerId:
      name: container_id
      in: path
      required: true
      schema: { type: string }
    FileId:
      name: file_id
      in: path
      required: true
      schema: { type: string }

  responses:
    Unauthorized:
      description: Missing, malformed or unknown credential
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    NotFound:
      description: |
        No such object in the caller's scope. Servers return 404 rather than 403 for objects outside
        the caller's scope, so that an id's existence is not disclosed.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    Conflict:
      description: '`session_busy` or `harness_mismatch`'
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    UnprocessableEntity:
      description: '`model_unavailable` or `unsupported_base`'
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    PayloadTooLarge:
      description: '`file_too_large`'
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    RateLimited:
      description: '`rate_limited` or `quota_exhausted`'
      headers:
        Retry-After:
          schema: { type: string }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }

  schemas:
    Discovery:
      type: object
      required: [object, protocol, versions, default_version, conformance_class, capabilities]
      properties:
        object: { type: string, const: uhp.discovery }
        protocol: { type: string, const: uhp }
        versions:
          type: array
          minItems: 1
          items: { type: string }
        default_version: { type: string }
        conformance_class: { type: string, enum: [core, extended, full] }
        capabilities: { $ref: '#/components/schemas/Capabilities' }
        implementation:
          type: object
          properties:
            name: { type: string }
            version: { type: string }
          additionalProperties: true
      additionalProperties: true

    Capabilities:
      type: object
      description: |
        Named booleans. A server reports `false` for a capability it does not implement rather than
        omitting it, so a client can distinguish "not supported" from "server predates this field".
        A client treats an absent key as `false`.
      properties:
        streaming: { type: boolean }
        sessions: { type: boolean }
        cancellation: { type: boolean }
        files_input: { type: boolean }
        files_output: { type: boolean }
        session_listing: { type: boolean }
        harness_management: { type: boolean }
        session_sharing: { type: boolean }
        idempotency: { type: boolean }
      additionalProperties: { type: boolean }

    Harness:
      type: object
      required: [id, name, base]
      properties:
        id: { type: string, pattern: '^chrn_' }
        object: { type: string, const: harness }
        name: { type: string }
        base:
          type: string
          description: |
            Opaque. Not enumerated by this specification — a client must treat it as a string, or the
            protocol would need revising every time a harness is released.
          examples: [codex, claude-code, hermes]
        baseLabel: { type: string }
        defaultModel: { type: string }
        systemPrompt: { type: string }
        mcpServers: { type: array, items: { $ref: '#/components/schemas/McpServer' } }
        skills: { type: array, items: { $ref: '#/components/schemas/Skill' } }
        disabledTools: { type: array, items: { type: string } }
        maxStep: { type: [integer, 'null'] }
        timeoutSeconds: { type: [integer, 'null'] }
        createdAt: { type: integer, description: Unix milliseconds }
      additionalProperties: true

    McpServer:
      type: object
      required: [name, url]
      description: |
        A remote MCP server attached to a harness. Only enabled entries are connected for a turn; a
        disabled entry must not be contacted at all. An unreachable server must not fail the task.
      properties:
        name: { type: string, description: Sanitised to a CLI-safe identifier by the server. }
        url: { type: string, format: uri }
        transport: { type: string, enum: [http, sse], default: http }
        enabled: { type: boolean, default: true }
        headers: { type: object, additionalProperties: { type: string } }
        auth:
          type: string
          description: |
            Bearer token, or a server-side reference the server resolves. A server must never return
            a resolved credential to a client.
      additionalProperties: true

    Skill:
      type: object
      required: [name]
      description: |
        A skill is a FOLDER, not a file. A server must materialise the whole folder where the agent
        can read it — materialising only SKILL.md breaks every skill carrying references, scripts or
        data. Round-tripping a harness through GET and PUT must not lose skill contents.
      properties:
        name: { type: string }
        enabled:
          type: boolean
          default: true
          description: false suppresses the skill, including one inherited from the base.
        files:
          type: array
          items: { $ref: '#/components/schemas/SkillFile' }
          description: The bundle. Must contain a SKILL.md.
        content:
          type: string
          description: Shorthand for a single-file bundle whose only member is SKILL.md.
        blob:
          type: string
          description: |
            Server-assigned handle for a bundle stored out of line. A client receives it, passes it
            back unchanged, and reads the files from the skill files endpoint.
      additionalProperties: true

    SkillFile:
      type: object
      required: [path]
      properties:
        path:
          type: string
          description: |
            Relative to the skill's own folder; nested directories are supported. A server must
            reject a path that escapes the folder.
          examples: ['SKILL.md', 'references/codes.md', 'assets/logo.png']
        content: { type: string, description: Text content. }
        content_b64: { type: string, description: Base64 for binary content; preserved byte-for-byte. }
      additionalProperties: true

    HarnessCreate:
      type: object
      required: [base]
      properties:
        name: { type: string }
        base: { type: string }
        default_model: { type: string }
        system_prompt: { type: string }
        mcp_servers: { type: array, items: { $ref: '#/components/schemas/McpServer' } }
        skills: { type: array, items: { $ref: '#/components/schemas/Skill' } }
        disabled_tools: { type: array, items: { type: string } }
        max_step: { type: [integer, 'null'] }
        timeout_seconds: { type: [integer, 'null'] }
      additionalProperties: true

    ModelCatalog:
      type: object
      required: [backends]
      properties:
        backends:
          type: object
          additionalProperties:
            type: object
            required: [default, models]
            properties:
              default: { type: string }
              models: { type: array, items: { $ref: '#/components/schemas/Model' } }

    HarnessModels:
      type: object
      required: [models]
      properties:
        harness_id: { type: string }
        backend: { type: string }
        default: { type: string }
        fallback: { type: string }
        models: { type: array, items: { $ref: '#/components/schemas/Model' } }

    Model:
      type: object
      required: [id, available]
      properties:
        id: { type: string }
        label: { type: string }
        backend: { type: string }
        available:
          type: boolean
          description: |
            Computed, not asserted: true means the server can serve this model for this harness right
            now. Listing a model as available and then failing the task is the worst outcome for a
            client, because a user has already chosen it.
        default: { type: boolean }
      additionalProperties: true

    CreateResponseRequest:
      type: object
      required: [input]
      properties:
        input:
          oneOf:
            - type: string
            - type: array
              items: { type: object, additionalProperties: true }
          description: A bare string is shorthand for one user message.
        model: { type: string, description: Canonical model id. Omitted means the harness default. }
        metadata:
          type: object
          description: Client metadata. `harness_id` selects the configured harness.
          properties:
            harness_id: { type: string }
          additionalProperties: true
        stream: { type: boolean, default: false }
        previous_response_id: { type: [string, 'null'] }
        instructions: { type: string }
        store: { type: boolean, default: true }
        max_output_tokens: { type: [integer, 'null'] }
        max_step: { type: [integer, 'null'], description: Agent step (tool-call round) budget }
        timeout_seconds: { type: [integer, 'null'], description: Wall-clock budget }
        tools:
          type: array
          items: { type: object, additionalProperties: true }
          description: |
            Reserved and ignored. Accepted for wire compatibility, never acted on, and reported in
            `metadata.ignored_fields` on the response. A UHP harness invokes and executes tools
            itself and reports them in `output`; there is no input path for a tool result, so the
            client-executed tool loop this field implies cannot be completed by a conformant server.
            Configure tools on the harness instead — see Harnesses §4.1. Tasks §1.4.
        include:
          type: array
          items: { type: string }
          description: |
            Reserved and ignored. Accepted for wire compatibility, never acted on, and reported in
            `metadata.ignored_fields` on the response. No values are enumerated, so any string a
            server recognised would be one it named itself. Tasks §1.4.
        background: { type: boolean, default: false }
      additionalProperties: true

    Response:
      type: object
      required: [id, object, created_at, status, output, model]
      properties:
        id: { type: string, pattern: '^resp_' }
        object: { type: string, const: response }
        created_at: { type: integer, description: Unix seconds }
        status: { $ref: '#/components/schemas/ResponseStatus' }
        error:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/Error'
          description: Non-null only when status is `failed`.
        incomplete_details: { type: [object, 'null'], additionalProperties: true }
        previous_response_id: { type: [string, 'null'] }
        model: { type: string, description: The model that actually ran. }
        output:
          type: array
          items: { $ref: '#/components/schemas/OutputItem' }
        store: { type: boolean }
        usage:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/Usage'
          description: |
            null when the server cannot account for usage. A fabricated zero would be worse than an
            honest absence, because a client cannot tell it from a free task.
        metadata:
          type: object
          properties:
            session_id: { type: string }
            requested_model:
              type: string
              description: Present when the server ran a different model than was requested.
            model_fallback: { type: boolean }
            model_fallback_reason: { type: string }
            ignored_fields:
              type: array
              items: { type: string }
              description: |
                Request fields the server did not act on, by name, in any order. Required when the
                request carried `tools` or `include`, which are reserved and ignored. A silently
                ignored field is indistinguishable from an honoured one. Tasks §1.1 and §1.4.
          additionalProperties: true
      additionalProperties: true

    ResponseStatus:
      type: string
      enum: [in_progress, completed, failed, incomplete, cancelled]
      description: |
        `incomplete` means a budget stopped the work and is usually worth continuing.
        `failed` means it could not be done. `cancelled` means the client asked for a stop, and must
        never be reported as `failed`.

    OutputItem:
      type: object
      required: [type]
      description: |
        A client must tolerate item types it does not recognise. A client that renders only `message`
        items and ignores the rest is a valid client.
      properties:
        id: { type: string }
        type:
          type: string
          examples: [message, reasoning, function_call, function_call_output]
        status: { type: string }
        role: { type: string }
        content:
          type: array
          items: { $ref: '#/components/schemas/ContentPart' }
        summary:
          type: array
          items: { type: object, additionalProperties: true }
        call_id: { type: string }
        name: { type: string }
        arguments: { type: string, description: JSON, as a string }
        output: { type: string }
      additionalProperties: true

    ContentPart:
      type: object
      required: [type]
      properties:
        type: { type: string, examples: [output_text] }
        text: { type: string }
        annotations:
          type: array
          items: { $ref: '#/components/schemas/Annotation' }
      additionalProperties: true

    Annotation:
      type: object
      required: [type]
      properties:
        type: { type: string, const: container_file_citation }
        container_id: { type: string }
        file_id: { type: string }
        filename: { type: string }
        download_url: { type: string, format: uri }
        start_index: { type: integer }
        end_index: { type: integer }
      additionalProperties: true

    Usage:
      type: object
      properties:
        input_tokens: { type: integer, minimum: 0 }
        output_tokens: { type: integer, minimum: 0 }
        total_tokens: { type: integer, minimum: 0 }
        cache_read_tokens: { type: integer, minimum: 0 }
        cache_write_tokens: { type: integer, minimum: 0 }
      additionalProperties: true

    SessionShare:
      type: object
      description: |
        A published read-only view of a session (Sessions §5). `url` may be relative, resolving
        against the base URL the caller is already using — a server behind a proxy cannot know its
        public origin, and a base-relative path survives every fronting. The share's id MUST NOT
        function as a credential for the rest of the API.
      required: [id, url]
      properties:
        id: { type: string }
        url: { type: string }
        object: { type: string, description: 'SHOULD be "session.share"' }
        enabled: { type: boolean }
      additionalProperties: true

    TurnItem:
      type: object
      description: |
        One turn of a session's history (Sessions §3). Previously an object with no stated shape,
        which kept every conformance check at "the endpoint answered 200" and made transcript
        rebuilding an exercise in one implementation's habits.
      required: [id, status]
      properties:
        id: { type: string, description: "The response id, usable with GET /v1/responses/{id}" }
        status: { type: string }
        user: { type: string }
        assistant: { type: string }
        tools: { type: array, items: { type: object, additionalProperties: true } }
        files: { type: array, items: { type: object, additionalProperties: true } }
      additionalProperties: true

    Session:
      type: object
      required: [id]
      properties:
        id: { type: string }
        object: { type: string, const: session }
        harness_id: { type: string }
        title: { type: string }
        status: { type: string }
        created_at: { type: integer }
        updated_at: { type: integer }
      additionalProperties: true

    SessionList:
      type: object
      required: [sessions]
      properties:
        sessions: { type: array, items: { $ref: '#/components/schemas/Session' } }
        next_cursor:
          type: [string, 'null']
          description: |
            null on the last page. A client must not be required to detect the end by receiving fewer
            items than requested — that heuristic is wrong whenever a page is exactly full.
      additionalProperties: true

    File:
      type: object
      required: [id, filename]
      properties:
        id: { type: string }
        object: { type: string, const: file }
        container_id: { type: string }
        filename: { type: string }
        bytes: { type: integer, minimum: 0 }
        created_at: { type: integer }
      additionalProperties: true

    ErrorEnvelope:
      type: object
      required: [error]
      properties:
        error: { $ref: '#/components/schemas/Error' }
        detail:
          type: string
          deprecated: true
          description: |
            A human-readable alias of `error.message`, retained for clients written against
            implementations that predate this envelope. Carries no information not in `error`.
      additionalProperties: true

    Error:
      type: object
      required: [type, code, message]
      properties:
        type:
          type: string
          enum:
            - invalid_request_error
            - authentication_error
            - permission_error
            - rate_limit_error
            - harness_error
            - server_error
        code:
          type: string
          description: |
            Specific and machine-readable. Servers may define additional codes for conditions this
            specification does not cover, and must namespace them with a vendor prefix so a future
            version cannot collide with them.
          examples:
            - unsupported_protocol_version
            - invalid_input
            - harness_not_found
            - response_not_found
            - session_not_found
            - file_not_found
            - session_expired
            - harness_mismatch
            - session_busy
            - file_too_large
            - model_unavailable
            - unsupported_base
            - missing_credential
            - invalid_credential
            - insufficient_scope
            - rate_limited
            - quota_exhausted
            - harness_error
            - harness_unavailable
            - provider_error
            - timeout
            - cancelled
            - preview_unavailable
            - preview_failed
        message:
          type: string
          description: |
            One sentence, safe to show a user. Must not contain credentials, internal hostnames, file
            paths, or stack traces.
        param: { type: [string, 'null'], description: Dotted path to the offending field. }
        detail: { type: [object, 'null'], additionalProperties: true }
      additionalProperties: true

    Event:
      type: object
      required: [type, sequence_number]
      description: |
        One streamed event. `sequence_number` starts at 0 and increases by exactly 1 per event, so a
        client can detect a dropped event rather than silently rendering a gap.
      properties:
        type:
          type: string
          examples:
            - response.created
            - response.in_progress
            - response.output_item.added
            - response.output_item.done
            - response.content_part.added
            - response.content_part.done
            - response.output_text.delta
            - response.output_text.done
            - response.output_text.annotation.added
            - response.reasoning_summary_part.added
            - response.reasoning_summary_text.delta
            - response.reasoning_summary_part.done
            - response.function_call_arguments.delta
            - response.function_call_arguments.done
            - response.completed
            - response.incomplete
            - response.failed
            - error
        sequence_number: { type: integer, minimum: 0 }
        response: { $ref: '#/components/schemas/Response' }
        item: { $ref: '#/components/schemas/OutputItem' }
        part: { $ref: '#/components/schemas/ContentPart' }
        annotation: { $ref: '#/components/schemas/Annotation' }
        delta: { type: string }
        text: { type: string }
        arguments: { type: string }
        item_id: { type: string }
        output_index: { type: integer, minimum: 0 }
        content_index: { type: integer, minimum: 0 }
        summary_index: { type: integer, minimum: 0 }
        annotation_index: { type: integer, minimum: 0 }
        code: { type: string }
        message: { type: string }
        param: { type: [string, 'null'] }
      additionalProperties: true
