openapi: 3.0.3
info:
  title: inis.run Execution API
  version: 1.0.0
  description: >-
    Run untrusted code safely. Each execution gets its own isolated VM. Built in Ireland.

    Session lifecycle controls — checked in priority order: (1) max_lifetime_ms elapsed
    auto-pauses the session unconditionally and PERMANENTLY — it never destroys (unless the
    pause itself fails, in which case destroy is the fallback rather than leaving the
    session running past its declared lifetime), but the session can never resume again
    after this: any resume attempt (explicit, or automatic on the next exec/files/pty/expose
    call) is refused with 410. GET /v1/sessions/{id} (and list) report this ahead of a resume
    attempt too: once (1) has fired, state reads "lifetime_expired" instead of "paused" (see
    SessionResponse.state) so a caller polling for session health can tell a permanently-dead
    session apart from a normal, resumable one without having to try a resume and fail; (2)
    idle_timeout_ms of inactivity auto-pauses the session —
    ordinarily resumable, unlike (1); (3) paused_ttl_ms auto-destroys a paused session after
    the configured idle window — this is what actually bounds how long a paused (including
    permanently-paused) session's storage sticks around; (4) on_pty_detach fires when the
    last terminal (PTY) client disconnects — no-op for API-only sessions.
    destroy_on_completion=true ends the session immediately after the first exec completes,
    regardless of other controls. max_lifetime_ms > 0 combined with a negative paused_ttl_ms
    ("never expire") is rejected at create time (400): a session permanently paused by (1)
    that (3) can never reclaim would otherwise accrue paused-storage billing forever with no
    way to end it.
servers:
  - url: https://api.inis.run
    description: Production
  - url: http://127.0.0.1:8080
    description: Local development
security:
  - bearerAuth: []
paths:
  /v1/healthz:
    get:
      summary: Health check
      operationId: healthCheck
      security: []
      responses:
        "200":
          description: Node is healthy
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
  /v1/sessions/exec:
    post:
      summary: Run a one-shot command in a throwaway session
      description: >-
        Convenience wrapper that creates a session, runs the command, returns the
        output inline, and destroys the session. The same primitive as a session,
        just collapsed into a single call.
      operationId: execute
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ExecuteRequest"
      responses:
        "200":
          description: Execution result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExecuteResponse"
        default:
          $ref: "#/components/responses/Error"

  /v1/org:
    get:
      summary: Organisation capacity
      description: Aggregate session capacity and per-customer limits for the caller.
      operationId: getOrg
      responses:
        "200":
          description: Capacity snapshot
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrgResponse"
        default:
          $ref: "#/components/responses/Error"

  /v1/usage:
    get:
      summary: Organisation usage and cost
      description: >-
        Aggregate compute/storage usage and spend for the caller's org, over
        an optional [since, until) window (default: this calendar month to
        now). Every figure is read from the same credit_transactions/
        usage_events ledger rows billing itself debited from — never
        re-derived from duration × rate — so it always agrees with the
        balance. `balance_cents` (the current prepaid balance) rides along in
        every response. `group_by=label:<key>` additionally breaks
        `cost_to_date_cents` down by the value of each session's
        `labels[key]` (set at session create) — the per-label `cost_cents`
        entries always sum to `cost_to_date_cents` exactly; a debit whose
        session carries no such label (or isn't tied to a session at all,
        e.g. volume storage) falls into the `""` label.
      operationId: getUsage
      parameters:
        - name: since
          in: query
          description: Start of the window (RFC3339). Defaults to the start of the current calendar month (UTC).
          required: false
          schema:
            type: string
            format: date-time
        - name: until
          in: query
          description: End of the window (RFC3339, exclusive). Defaults to now.
          required: false
          schema:
            type: string
            format: date-time
        - name: group_by
          in: query
          description: >-
            Break cost down by an attribute. Only `label:<key>` is
            supported today (e.g. `group_by=label:team`).
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Usage/cost summary
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UsageSummaryResponse"
        default:
          $ref: "#/components/responses/Error"

  /v1/sessions:
    get:
      summary: List sessions
      operationId: listSessions
      parameters:
        - name: state
          in: query
          description: >-
            Filter sessions by state. Live states: live, paused, creating.
            Use "ended" to list retained history rows for destroyed sessions
            (each carries ended_at and end_reason).
          required: false
          schema:
            type: string
            enum: [live, paused, creating, ended]
        - name: limit
          in: query
          description: Max number of sessions to return
          required: false
          schema:
            type: integer
            default: 20
        - name: cursor
          in: query
          description: Pagination cursor
          required: false
          schema:
            type: string
        - name: external_id
          in: query
          description: >-
            Filter to sessions whose external_id exactly matches this value
            (#1375). NOT unique — external_id may be shared across many
            sessions (e.g. every session a workflow spins up over its
            lifetime), so this can return more than one result. Answered
            from the fleet-wide catalog, same scope as the plain (no
            `state`) or `state=live`/`state=paused` list — not combinable
            with `state=ended`.
          required: false
          schema:
            type: string
      responses:
        "200":
          description: List of sessions
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionListResponse"
        default:
          $ref: "#/components/responses/Error"
    post:
      summary: Create session
      operationId: createSession
      parameters:
        - name: Idempotency-Key
          in: header
          description: >-
            Opaque caller-chosen token (#1376) scoping this create for
            replay: a retry with the same key + customer within a 24h TTL
            returns the ORIGINAL session (200, header
            `Idempotent-Replayed: true`) instead of creating a second one or
            failing on a name clash. A concurrent duplicate request for the
            same in-flight key gets `409 idempotency_key_in_progress`
            (retryable, Retry-After set) rather than racing a second create.
            Omit for an ordinary, non-idempotent create. Max 255 characters;
            letters, digits, and `_.:=+/-` only.
          required: false
          schema:
            type: string
            maxLength: 255
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SessionCreateRequest"
      responses:
        "200":
          description: Session created (or, on an Idempotency-Key replay, the original session)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionResponse"
        default:
          $ref: "#/components/responses/Error"
  /v1/sessions/{id}:
    get:
      summary: Get session
      operationId: getSession
      parameters:
        - $ref: "#/components/parameters/SessionId"
      responses:
        "200":
          description: Session state
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionResponse"
        default:
          $ref: "#/components/responses/Error"
    delete:
      summary: Destroy session
      operationId: destroySession
      parameters:
        - $ref: "#/components/parameters/SessionId"
        - name: reason
          in: query
          description: >-
            Why the session is being destroyed, recorded as end_reason on its
            retained history row. Defaults to client_destroy. shell_exit marks an
            interactive-shell teardown.
          required: false
          schema:
            type: string
            enum: [client_destroy, shell_exit]
      responses:
        "204":
          description: >-
            Destroyed. Succeeds even for a session in the terminal
            `node_lost` state (#1637) — unlike every other session
            operation, which returns `session_node_lost` there — since
            destroy is cleanup of a dead thing and needs no live node to
            act on.
        default:
          $ref: "#/components/responses/Error"
    patch:
      summary: Update a session's control-plane settings (#2056)
      description: >-
        Set or clear the session's own daily egress cap. Control-plane
        state on the catalog row: takes effect on the next egress sweep tick
        without touching the running VM. A session another org owns reads as
        404. egress_cap_bytes of 0 clears the cap.
      operationId: patchSession
      parameters:
        - $ref: "#/components/parameters/SessionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SessionPatchRequest"
      responses:
        "200":
          description: The session's egress cap after the change
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionEgressCap"
        "400":
          description: egress_cap_bytes missing or negative
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Session not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        default:
          $ref: "#/components/responses/Error"

  /v1/sessions/{id}/egress-cap:
    get:
      summary: Get a session's own daily egress cap and today's usage (#2056)
      operationId: getSessionEgressCap
      parameters:
        - $ref: "#/components/parameters/SessionId"
      responses:
        "200":
          description: The session's cap and today's egress
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionEgressCap"
        "404":
          description: Session not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        default:
          $ref: "#/components/responses/Error"

  /v1/sessions/{id}/usage:
    get:
      summary: Session usage and cost
      description: >-
        One session's lifetime usage/cost — the same ledger-derived figures
        GET /v1/usage's org totals are built from, scoped to this session
        (no window: cost-to-date means since the session started). A session
        id belonging to a different org than the caller's token returns 404,
        identical to a session id that never existed — never a 403, which
        would confirm the session's existence to a caller who doesn't own it.
      operationId: getSessionUsage
      parameters:
        - $ref: "#/components/parameters/SessionId"
      responses:
        "200":
          description: Session usage/cost summary
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionUsageResponse"
        default:
          $ref: "#/components/responses/Error"
  /v1/sessions/{id}/exec:
    post:
      summary: Run command in session
      description: >-
        Without `stream`, runs the command to completion and returns one
        buffered JSON response (stdout/stderr each capped at 4 MiB). With
        `?stream=true`, upgrades to a Server-Sent Events stream instead:
        `event: stdout` / `event: stderr` frames (base64-encoded, since
        output is arbitrary bytes) as the command produces them — relayed
        chunk-by-chunk with no total-output cap, unlike the buffered mode —
        ending with `event: exit` (JSON body: exit_code, timed_out,
        duration_ms) once the command exits, or `event: error` on a
        guest-side failure. A client that disconnects mid-stream kills the
        command; it does not keep running in the background (use
        POST .../processes for that).
      operationId: sessionExec
      parameters:
        - $ref: "#/components/parameters/SessionId"
        - name: stream
          in: query
          required: false
          description: Stream output live via Server-Sent Events instead of one buffered response.
          schema:
            type: boolean
            default: false
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SessionExecRequest"
      responses:
        "200":
          description: >-
            Buffered command result (application/json), or an SSE stream
            (text/event-stream) when stream=true.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExecResponse"
            text/event-stream:
              schema:
                type: string
        default:
          $ref: "#/components/responses/Error"
  /v1/sessions/{id}/files:
    get:
      summary: Read a file, list a directory, find paths, or grep file contents in session
      operationId: sessionFilesGet
      parameters:
        - $ref: "#/components/parameters/SessionId"
        - name: path
          in: query
          required: false
          description: >-
            Path to the file or base directory inside the session. Required
            for op=read; defaults to /workspace for list/find/grep.
          schema:
            type: string
        - name: op
          in: query
          required: false
          description: >-
            Operation to perform. "read" returns file content; "list" returns
            directory entries; "find" returns paths matching a glob under
            path; "grep" returns lines matching a regex under path. Defaults
            to "read".
          schema:
            type: string
            enum: [read, list, find, grep]
            default: read
        - name: encoding
          in: query
          required: false
          description: >-
            op=read only. "text" (default) returns content as-is; "base64"
            base64-encodes it (for binary files).
          schema:
            type: string
            enum: [text, base64]
            default: text
        - name: pattern
          in: query
          required: false
          description: >-
            op=find: a glob (exact path, "*"/"?"/"[...]", or a trailing "/**"
            for recursive), relative to path. op=grep: a regular expression
            (Go RE2 syntax — a deliberate scope cut vs. PCRE) matched against
            each line.
          schema:
            type: string
        - name: max_results
          in: query
          required: false
          description: >-
            op=find: caps the number of matching paths returned. op=grep:
            caps the number of matching FILES returned (not match lines).
            Response's `truncated` is set rather than erroring when the cap
            is hit. Accepts up to 10,000 — matching the underlying walk/scan
            budget below, so asking for more can't return more anyway.
            op=grep also enforces its own internal scan budgets independent
            of max_results — at most 10,000 files and 256 MiB read in total —
            so a pattern that matches nothing still returns promptly (with
            `truncated: true`) instead of scanning the whole workspace;
            op=find likewise caps the underlying recursive walk at 10,000
            files.
          schema:
            type: integer
        - name: file_pattern
          in: query
          required: false
          description: op=grep only. Restrict matching to files whose base name matches this glob (e.g. "*.go").
          schema:
            type: string
        - name: case_sensitive
          in: query
          required: false
          description: op=grep only. "true" for case-sensitive matching. Defaults to case-insensitive.
          schema:
            type: string
            enum: ["true", "false"]
        - name: context_lines
          in: query
          required: false
          description: op=grep only. Lines of context to include before/after each match (capped).
          schema:
            type: integer
        - name: exclude_dirs
          in: query
          required: false
          description: op=grep only. Comma-separated directory names to skip entirely (e.g. "node_modules,.git").
          schema:
            type: string
      responses:
        "200":
          description: File content, directory listing, find matches, or grep matches
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/FileReadResponse"
                  - $ref: "#/components/schemas/FileListResponse"
                  - $ref: "#/components/schemas/FileFindResponse"
                  - $ref: "#/components/schemas/FileGrepResponse"
        default:
          $ref: "#/components/responses/Error"
    put:
      summary: Write a file in session
      description: >-
        The file (and any parent directories created for it) is owned by the
        session's unprivileged sandbox user, not root (#1829) — the same user
        `exec`/`ssh` run as, so it can chmod/replace/append to what it just
        wrote without needing sudo. This matters for files whose permissions
        are part of their contract, e.g. `~/.ssh/authorized_keys`: sshd
        silently ignores a key file it doesn't own, with no error surfaced
        anywhere.
      operationId: sessionFilesPut
      parameters:
        - $ref: "#/components/parameters/SessionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SessionFilePutRequest"
      responses:
        "200":
          description: Write confirmed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FileWriteResponse"
        default:
          $ref: "#/components/responses/Error"
    delete:
      summary: Delete a file, or a directory (optionally recursive)
      description: >-
        Without recursive=true, a directory target must already be empty
        (rmdir semantics — a distinct "directory not empty" error rather than
        silently recursing); a file target is removed unconditionally either
        way. recursive=true deletes a directory and everything under it. The
        workspace root itself can never be removed.
      operationId: sessionFilesDelete
      parameters:
        - $ref: "#/components/parameters/SessionId"
        - name: path
          in: query
          required: true
          description: Path to the file or directory to delete.
          schema:
            type: string
        - name: recursive
          in: query
          required: false
          description: Delete a directory and everything under it. Defaults to false.
          schema:
            type: string
            enum: ["true", "false"]
      responses:
        "200":
          description: Delete confirmed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FileOpResponse"
        default:
          $ref: "#/components/responses/Error"

  /v1/sessions/{id}/files/mkdir:
    post:
      summary: Create a directory (with parents)
      description: >-
        Always `mkdir -p` semantics: intermediate components are created as
        needed, and mkdir-ing an already-existing directory is not an error.
        Every directory created is owned by the sandbox user (#1829).
      operationId: sessionFilesMkdir
      parameters:
        - $ref: "#/components/parameters/SessionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SessionFileMkdirRequest"
      responses:
        "200":
          description: Directory created (or already existed)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FileOpResponse"
        default:
          $ref: "#/components/responses/Error"

  /v1/sessions/{id}/files/rename:
    post:
      summary: Rename or move a file or directory
      description: >-
        Both path (source) and dest_path (target) must resolve under
        /workspace. The destination's parent directories are created as
        needed.
      operationId: sessionFilesRename
      parameters:
        - $ref: "#/components/parameters/SessionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SessionFileRenameRequest"
      responses:
        "200":
          description: Rename confirmed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FileRenameResponse"
        default:
          $ref: "#/components/responses/Error"

  /v1/sessions/{id}/files/stream:
    put:
      summary: Stream a binary file from local disk into the session
      description: >-
        NOT subject to the 64-file/8 MiB batch cap PUT .../files/batch
        enforces — for pushing a repo tarball or dataset in. The request body
        is raw bytes (any content type); the server chunks it to the guest in
        bounded pieces instead of buffering the whole file in memory on
        either side. Returns the total bytes written and a sha256 digest of
        the uploaded content for round-trip verification. The file lands
        owned by the sandbox user, same as PUT .../files (#1829).
      operationId: sessionFilesStreamPut
      parameters:
        - $ref: "#/components/parameters/SessionId"
        - name: path
          in: query
          required: true
          description: Destination path inside the session.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema:
              type: string
              format: binary
      responses:
        "200":
          description: Upload complete
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FileStreamWriteResponse"
        default:
          $ref: "#/components/responses/Error"
    get:
      summary: Stream a binary file from the session to local disk
      description: >-
        NOT subject to the 4 MiB whole-file cap GET .../files?op=read
        enforces — for pulling results/datasets out. Response body is raw
        bytes (application/octet-stream) with an honest Content-Length; the
        server streams it from the guest in bounded chunks rather than
        buffering the whole file.
      operationId: sessionFilesStreamGet
      parameters:
        - $ref: "#/components/parameters/SessionId"
        - name: path
          in: query
          required: true
          description: Path inside the session to download.
          schema:
            type: string
      responses:
        "200":
          description: File content
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        default:
          $ref: "#/components/responses/Error"

  /v1/sessions/{id}/files/batch:
    put:
      summary: Write multiple small files in one call
      description: >-
        Scaffold N small files in a single request instead of one PUT per
        file. Capped at 64 files / 8 MiB total content — for bulk import, use
        artifacts capture (export) or the session exec/SSH surfaces instead.
        Not fail-fast: the response carries a per-file {path, ok, error?}
        result, so one bad path doesn't sink the rest of the batch. Each file
        lands owned by the sandbox user, same as PUT .../files (#1829).
      operationId: sessionFilesBatchPut
      parameters:
        - $ref: "#/components/parameters/SessionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SessionFileBatchPutRequest"
      responses:
        "200":
          description: Per-file write results
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FileBatchWriteResponse"
        default:
          $ref: "#/components/responses/Error"

  /v1/sessions/{id}/pty:
    get:
      summary: Attach an interactive terminal (WebSocket upgrade)
      description: >-
        Upgrades to a WebSocket carrying an interactive pseudo-terminal in the
        session's guest VM. Not a plain HTTP request/response — connect with a
        WebSocket client (`wss://` in production); the bearer token goes in
        the `Authorization` header on the upgrade request, same as every
        other endpoint. Binary WebSocket messages are `[1 byte frame
        type][payload]`: type `0x01` data (both directions), `0x02` resize
        (host->guest, 4 bytes: rows uint16 BE, cols uint16 BE), `0x03` signal
        (host->guest, 1 byte signal number), `0x05` exit (guest->host, 4
        bytes exit code int32 BE, ends the stream), `0x06` error (guest->host,
        UTF-8 message). An attached terminal keeps the session live — the
        idle timer does not fire while at least one PTY client is attached.
      operationId: sessionPTY
      parameters:
        - $ref: "#/components/parameters/SessionId"
        - name: rows
          in: query
          required: false
          description: Initial terminal rows. Defaults to 24; resizable later via a 0x02 frame.
          schema:
            type: integer
            default: 24
        - name: cols
          in: query
          required: false
          description: Initial terminal columns. Defaults to 80; resizable later via a 0x02 frame.
          schema:
            type: integer
            default: 80
        - name: term
          in: query
          required: false
          description: Connecting client's $TERM, applied to the guest shell. Defaults to xterm-256color.
          schema:
            type: string
        - name: colorterm
          in: query
          required: false
          description: >-
            Connecting client's $COLORTERM (e.g. "truecolor"). Omitted means
            "unknown", which the guest shell treats as the safe 256-color path.
          schema:
            type: string
      responses:
        "101":
          description: Switching Protocols — WebSocket upgrade accepted
        default:
          $ref: "#/components/responses/Error"

  /v1/sessions/{id}/tunnel:
    get:
      summary: Attach an SSH tunnel (WebSocket upgrade)
      description: >-
        Upgrades to a WebSocket that bridges raw bytes to the guest's sshd
        (vsock port 2222) with no additional framing — WebSocket binary
        messages are exactly the SSH wire protocol's bytes. Meant to be
        driven through an SSH client's ProxyCommand, not called directly:
        `ssh -o ProxyCommand="inis sessions tunnel %n" sandbox@<session-id>`.
        Use %n, not %h — ssh lowercases %h, but session IDs are
        case-sensitive. An attached tunnel keeps the session live the same
        way an attached PTY does.
      operationId: sessionTunnel
      parameters:
        - $ref: "#/components/parameters/SessionId"
      responses:
        "101":
          description: Switching Protocols — WebSocket upgrade accepted

  /v1/sessions/{id}/processes:
    post:
      summary: Start a named background process
      description: >-
        Starts a detached command in the session's guest VM under a caller-
        chosen name. The process keeps running (and its output keeps
        accumulating) after this call returns and after the caller
        disconnects — it is independent of any one HTTP request, the same way
        the guest VM itself outlives any one exec. Starting a second process
        under a name still in use by a running process is rejected; a name
        whose process has already exited may be reused. A session may have
        at most 32 named processes (running, or exited but not yet reused)
        at once; starting one more is rejected with a clear error. Each
        process's captured stdout/stderr is independently capped on disk
        (see the logs endpoint) so a single long-lived, verbose process
        cannot fill the guest's disk.
      operationId: startProcess
      parameters:
        - $ref: "#/components/parameters/SessionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProcessStartRequest"
      responses:
        "200":
          description: Process started
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProcessInfo"
        default:
          $ref: "#/components/responses/Error"
    get:
      summary: List named background processes
      description: >-
        Lists every named background process in the session, running and
        exited (an exited process's entry is kept until its name is reused
        or the session is torn down).
      operationId: listProcesses
      parameters:
        - $ref: "#/components/parameters/SessionId"
      responses:
        "200":
          description: Process list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProcessListResponse"
        default:
          $ref: "#/components/responses/Error"

  /v1/sessions/{id}/processes/{name}:
    get:
      summary: Get a named background process
      operationId: getProcess
      parameters:
        - $ref: "#/components/parameters/SessionId"
        - $ref: "#/components/parameters/ProcessName"
      responses:
        "200":
          description: Process state
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProcessInfo"
        default:
          $ref: "#/components/responses/Error"
    delete:
      summary: Kill a named background process
      description: >-
        Sends SIGTERM, then SIGKILL after a grace period if the process
        hasn't exited. Returns its final state. Killing an already-exited
        process is a no-op that just reports that state.
      operationId: killProcess
      parameters:
        - $ref: "#/components/parameters/SessionId"
        - $ref: "#/components/parameters/ProcessName"
      responses:
        "200":
          description: Process killed (or already exited)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProcessInfo"
        default:
          $ref: "#/components/responses/Error"

  /v1/sessions/{id}/processes/{name}/logs:
    get:
      summary: Read a named background process's captured output
      description: >-
        Without `follow`, returns the process's captured stdout/stderr so
        far as one JSON body (each stream capped at 4 MiB from the tail).
        The underlying on-disk log file for each stream is itself capped at
        10 MiB: once a process's output on one stream exceeds that, older
        output is dropped and only the most recent bytes are kept, so a
        long-lived, verbose process cannot grow its log without bound — the
        buffered read here and the live follow below always see the tail of
        whatever survived that cap, never the full lifetime output of a very
        chatty process. With `follow=true`, upgrades to a Server-Sent Events stream instead:
        `event: stdout` / `event: stderr` frames (base64-encoded data, since
        output is arbitrary bytes) as the guest produces them, ending with
        `event: eof` once the process exits and its output is fully drained,
        or `event: error` on a guest-side failure.
      operationId: getProcessLogs
      parameters:
        - $ref: "#/components/parameters/SessionId"
        - $ref: "#/components/parameters/ProcessName"
        - name: follow
          in: query
          required: false
          description: Stream live output via Server-Sent Events instead of one buffered read.
          schema:
            type: boolean
            default: false
      responses:
        "200":
          description: >-
            Buffered log content (application/json), or an SSE stream
            (text/event-stream) when follow=true.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProcessLogsResponse"
            text/event-stream:
              schema:
                type: string
        default:
          $ref: "#/components/responses/Error"

  /v1/sessions/{id}/pause:
    post:
      summary: Pause session
      operationId: sessionPause
      parameters:
        - $ref: "#/components/parameters/SessionId"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                wake_at:
                  type: string
                  format: date-time
                  description: >-
                    Schedule this session to come back live at an absolute
                    RFC3339 timestamp (e.g. "2026-08-13T09:00:00Z"), even with
                    nobody watching. Must be strictly in the future and no
                    more than 90 days out — a past or absurdly far-out value
                    is rejected with a 400. Omit to pause with no scheduled
                    wake, the default; omitting it on a later pause also
                    clears any wake_at set by an earlier one. If the session
                    is resumed (by any means) before the deadline, the
                    scheduled wake becomes a no-op once it comes due — it
                    never fires twice or interrupts a session that's already
                    live.
      responses:
        "200":
          description: Paused
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionResponse"
        default:
          $ref: "#/components/responses/Error"
  /v1/sessions/{id}/resume:
    post:
      summary: Resume session
      operationId: sessionResume
      parameters:
        - $ref: "#/components/parameters/SessionId"
      responses:
        "200":
          description: Live
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionResponse"
        default:
          $ref: "#/components/responses/Error"
  /v1/sessions/{id}/recover:
    post:
      summary: Recover a node_lost session
      description: >-
        Explicitly bring a node_lost session back under the same ID using its
        last completed cold snapshot in object storage. Returns the session
        live with restored_from set.
      operationId: sessionRecover
      parameters:
        - $ref: "#/components/parameters/SessionId"
      responses:
        "200":
          description: Recovered session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionResponse"
        default:
          $ref: "#/components/responses/Error"
  /v1/sessions/{id}/archive/retry:
    post:
      summary: Retry the durable (cold) upload of a paused session
      description: >-
        Re-drive the asynchronous upload of a paused session's snapshot bundle to
        durable EU object storage after a previous upload failed (or is stuck
        pending). The session must be paused and its hot copy still present. The
        upload runs in the background; this returns the now-pending archive
        status immediately.
      operationId: sessionArchiveRetry
      parameters:
        - $ref: "#/components/parameters/SessionId"
      responses:
        "202":
          description: Upload re-queued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ArchiveStatus"
        default:
          $ref: "#/components/responses/Error"
  /v1/sessions/{id}/fork:
    post:
      summary: Fork session into N children
      operationId: sessionFork
      parameters:
        - $ref: "#/components/parameters/SessionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SessionForkRequest"
      responses:
        "200":
          description: Child session IDs
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ForkResponse"
        default:
          $ref: "#/components/responses/Error"
  /v1/sessions/{id}/checkpoints:
    post:
      summary: Create a checkpoint of a session
      description: >-
        Take a named, retained snapshot of a live or paused-warm session. A
        live source keeps running; a paused source stays paused, untouched. The checkpoint survives the parent session's destruction
        and can be restored in place or used as a template for new sessions.
      operationId: createCheckpoint
      parameters:
        - $ref: "#/components/parameters/SessionId"
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CheckpointCreateRequest"
      responses:
        "200":
          description: Checkpoint created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CheckpointResponse"
        default:
          $ref: "#/components/responses/Error"
    get:
      summary: List checkpoints for a session
      operationId: listSessionCheckpoints
      parameters:
        - $ref: "#/components/parameters/SessionId"
      responses:
        "200":
          description: Checkpoints for the session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CheckpointListResponse"
        default:
          $ref: "#/components/responses/Error"
  /v1/sessions/{id}/restore:
    post:
      summary: Roll a session back to a checkpoint in place
      description: >-
        Stop the session's current VM and restore it from a checkpoint,
        rebinding networking. The "run untrusted, then roll back to clean" flow.
      operationId: sessionRestore
      parameters:
        - $ref: "#/components/parameters/SessionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SessionRestoreRequest"
      responses:
        "200":
          description: Session restored
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionResponse"
        default:
          $ref: "#/components/responses/Error"
  /v1/checkpoints/{cid}:
    get:
      summary: Get checkpoint metadata
      operationId: getCheckpoint
      parameters:
        - $ref: "#/components/parameters/CheckpointId"
      responses:
        "200":
          description: Checkpoint metadata
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CheckpointResponse"
        default:
          $ref: "#/components/responses/Error"
    delete:
      summary: Delete a checkpoint
      description: Delete a checkpoint and free its disk. Deletion is explicit.
      operationId: deleteCheckpoint
      parameters:
        - $ref: "#/components/parameters/CheckpointId"
      responses:
        "204":
          description: Deleted
        default:
          $ref: "#/components/responses/Error"
  /v1/checkpoints/{cid}/sessions:
    post:
      summary: Create a new session from a checkpoint
      description: >-
        Start a fresh session from a checkpoint. The new session diverges
        independently — this is the template mechanism.
      operationId: createSessionFromCheckpoint
      parameters:
        - $ref: "#/components/parameters/CheckpointId"
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CheckpointSessionRequest"
      responses:
        "200":
          description: Session created from checkpoint
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionResponse"
        default:
          $ref: "#/components/responses/Error"
  /v1/sessions/{id}/artifacts:
    post:
      summary: Capture session outputs to durable storage
      description: >-
        Capture the listed paths/globs from a live session to durable storage.
        Returns a pending manifest immediately; the upload runs asynchronously.
        Poll the artifact to see when it is ready.
      operationId: createArtifact
      parameters:
        - $ref: "#/components/parameters/SessionId"
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ArtifactCreateRequest"
      responses:
        "202":
          description: Capture accepted (pending)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ArtifactResponse"
        default:
          $ref: "#/components/responses/Error"
    get:
      summary: List artifact captures for a session
      operationId: listSessionArtifacts
      parameters:
        - $ref: "#/components/parameters/SessionId"
      responses:
        "200":
          description: Artifacts for the session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ArtifactListResponse"
        default:
          $ref: "#/components/responses/Error"
  /v1/sessions/{id}/artifacts/{aid}:
    get:
      summary: Get an artifact manifest (session-scoped)
      operationId: getSessionArtifact
      parameters:
        - $ref: "#/components/parameters/SessionId"
        - $ref: "#/components/parameters/ArtifactId"
      responses:
        "200":
          description: Artifact manifest
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ArtifactResponse"
        default:
          $ref: "#/components/responses/Error"
  /v1/artifacts/{aid}:
    get:
      summary: Get an artifact manifest
      operationId: getArtifact
      parameters:
        - $ref: "#/components/parameters/ArtifactId"
      responses:
        "200":
          description: Artifact manifest
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ArtifactResponse"
        default:
          $ref: "#/components/responses/Error"
    delete:
      summary: Delete an artifact
      description: Delete an artifact's stored files and its record.
      operationId: deleteArtifact
      parameters:
        - $ref: "#/components/parameters/ArtifactId"
      responses:
        "204":
          description: Deleted
        default:
          $ref: "#/components/responses/Error"
  /v1/artifacts/{aid}/extend:
    post:
      summary: Extend an artifact's retention
      description: Push the artifact's expiry forward, up to the retention cap.
      operationId: extendArtifact
      parameters:
        - $ref: "#/components/parameters/ArtifactId"
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ArtifactExtendRequest"
      responses:
        "200":
          description: Updated artifact manifest
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ArtifactResponse"
        default:
          $ref: "#/components/responses/Error"
  /v1/sessions/{id}/expose:
    post:
      summary: Ensure a guest port is exposed as a preview URL
      description: >-
        Idempotent. Re-exposing a port that is already exposed with the same
        visibility, auth mode and login_url returns the preview_url and token
        it already handed out rather than minting new ones, and does not wake
        a paused session. A preview URL is a credential a third party may be
        holding (a webhook registered with an external service, a link in
        someone's browser), so a retry or a convergence pass must not silently
        invalidate it. Set rotate: true to deliberately mint a fresh token;
        the old URL stops working the moment that returns. The result field
        says which of the three things happened.
      operationId: sessionExpose
      parameters:
        - $ref: "#/components/parameters/SessionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SessionExposeRequest"
      responses:
        "200":
          description: Preview URL
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionExposeResponse"
        default:
          $ref: "#/components/responses/Error"
  /v1/sessions/{id}/egress:
    get:
      summary: Get session egress policy
      operationId: sessionEgressGet
      parameters:
        - $ref: "#/components/parameters/SessionId"
      responses:
        "200":
          description: Active egress policy
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionEgressResponse"
        default:
          $ref: "#/components/responses/Error"
    post:
      summary: Set or replace session egress policy
      operationId: sessionEgressSet
      parameters:
        - $ref: "#/components/parameters/SessionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EgressPolicy"
      responses:
        "200":
          description: Updated egress policy
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionEgressResponse"
        default:
          $ref: "#/components/responses/Error"
  /v1/sessions/{id}/connections:
    get:
      summary: List a session's Connections
      description: >-
        Every Connection currently bound to the session, redacted — never
        the secret value or the session CA's private key. Same redaction as
        the `connections` field on GET /v1/sessions/{id}; this endpoint
        exists for callers that only want the Connection list. See
        /docs/connections.
      operationId: sessionConnectionsList
      parameters:
        - $ref: "#/components/parameters/SessionId"
      responses:
        "200":
          description: The session's Connections
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConnectionListResponse"
        default:
          $ref: "#/components/responses/Error"
    post:
      summary: Add a new Connection to a live or paused session
      description: >-
        Binds a NEW Connection to an already-created session (#1052 C5) —
        until this endpoint, the Connection set was frozen at session
        create; `PUT .../connections/{name}` only replaces a name that
        already exists (404 if it doesn't, 400 if the origin differs). A
        `name` that already exists in this session is a 409 conflict — use
        PUT to replace it instead. An `origin` already bound to a different
        Connection in this session is also a 409. Refused (409) for a
        session that has never held a Connection: the session CA that makes
        interception possible is installed into the guest only at boot, from
        the create-time inline set, so a session that booted with zero
        Connections has no CA a new one could use — create a new session
        with an inline Connection instead. Same validation as create-time:
        origin/method/path shape, TTL bounded by the session's own remaining
        lifetime, egress-policy containment, and the 16-Connections-per-
        session cap.
      operationId: sessionConnectionAdd
      parameters:
        - $ref: "#/components/parameters/SessionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ConnectionCreate"
      responses:
        "201":
          description: Connection added
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConnectionStatus"
        "409":
          description: >-
            Name or origin already bound in this session, the session has
            never held a Connection (no CA installed), or the session is
            already at the 16-Connection cap.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        default:
          $ref: "#/components/responses/Error"
  /v1/sessions/{id}/connections/{name}:
    put:
      summary: Replace or rotate a Connection
      description: >-
        Atomically replaces the credential and/or allow rules for a
        Connection that already exists in this (live or paused) session.
        Replace-only: 404 if `name` doesn't already exist (use POST to add
        it), 400 if `origin` differs from the existing Connection's origin
        (an origin change is a new Connection, not a rotation — revoke and
        add instead). See /docs/connections#lifecycle.
      operationId: sessionConnectionReplace
      parameters:
        - $ref: "#/components/parameters/SessionId"
        - name: name
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ConnectionCreate"
      responses:
        "200":
          description: Connection replaced
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConnectionStatus"
        default:
          $ref: "#/components/responses/Error"
    delete:
      summary: Revoke a Connection
      description: >-
        Deletes the stored credential immediately and leaves a tombstone
        (`state: revoked`) for the rest of the session's life: a request to
        this origin fails closed rather than falling back to an
        unauthenticated direct connection.
      operationId: sessionConnectionRevoke
      parameters:
        - $ref: "#/components/parameters/SessionId"
        - name: name
          in: path
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Revoked
        default:
          $ref: "#/components/responses/Error"
  /v1/sessions/{id}/tokens:
    get:
      summary: List a session's scoped tokens
      description: >-
        Every scoped, session-bound, capability-limited token (#1651) minted
        for this session, redacted — never the plaintext secret, which is
        returned exactly once, at mint time. Only an unbound, unrestricted
        credential (an ordinary account key) may call this; a scoped token
        cannot list or manage other scoped tokens, including its own
        session's.
      operationId: sessionTokenList
      parameters:
        - $ref: "#/components/parameters/SessionId"
      responses:
        "200":
          description: The session's scoped tokens
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/SessionTokenStatus"
        default:
          $ref: "#/components/responses/Error"
    post:
      summary: Mint a scoped, session-bound token
      description: >-
        Issues a new bearer-compatible token restricted to exactly this
        session and, optionally, a capability subset of `[pty, exec, files,
        tunnel, view]` (omitted or empty means every capability). Used to
        delegate access to one session — a subscriber's own box, a
        collaborator, a CI job — without handing out the full account key.
        `ttl_seconds` is required; the token stops working once it elapses,
        with no way to extend it (mint a new one instead). The returned
        `token` field is shown exactly once and is never recoverable again;
        only its redacted status (see SessionTokenStatus) shows up on later
        list calls. Only an unbound, unrestricted credential may mint —
        a scoped token cannot mint further tokens, even for its own session.
      operationId: sessionTokenMint
      parameters:
        - $ref: "#/components/parameters/SessionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SessionTokenMintRequest"
      responses:
        "201":
          description: Token minted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionTokenMintResponse"
        default:
          $ref: "#/components/responses/Error"
  /v1/sessions/{id}/tokens/{hash}:
    delete:
      summary: Revoke a scoped session token
      description: >-
        Revokes a scoped token by its hash (the `hash` field a mint or list
        call returned) immediately. A hash that doesn't name a live token
        bound to this session — wrong session, already revoked, or never
        existed — renders as a generic 404 either way, never distinguishing
        which case it was.
      operationId: sessionTokenRevoke
      parameters:
        - $ref: "#/components/parameters/SessionId"
        - name: hash
          in: path
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Revoked
        default:
          $ref: "#/components/responses/Error"
  /v1/sessions/{id}/unexpose:
    post:
      summary: Remove exposed port
      operationId: sessionUnexpose
      parameters:
        - $ref: "#/components/parameters/SessionId"
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SessionUnexposeRequest"
      responses:
        "200":
          description: Port removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
        default:
          $ref: "#/components/responses/Error"
  /v1/sessions/batch/exec:
    post:
      summary: Fan a command across sessions
      operationId: batchExec
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BatchExecRequest"
      responses:
        "200":
          description: Batch results
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BatchExecResponse"
        default:
          $ref: "#/components/responses/Error"

  /v1/templates:
    get:
      summary: List available templates
      description: >-
        List the base environments the caller can create sessions from: the
        official templates curated by inis.run plus any the caller has published.
      operationId: listTemplates
      responses:
        "200":
          description: Templates available to the caller
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TemplateListResponse"
        default:
          $ref: "#/components/responses/Error"
    post:
      summary: Import an OCI image (public or private) as a custom template
      description: >-
        Register an OCI image ref as a reusable custom template. The image is
        pulled, its layers flattened into the VM rootfs, and the inis guest
        agent injected as `/init`. Builds are serialized: the import joins the
        node's build queue and is returned immediately with `status: queued`
        (and a `queue_position`); a single worker builds one image at a time,
        so `status` moves `queued` → `building` → `ready`/`failed` (poll
        `GET /v1/templates/{name}`). Over the per-customer or node queue-depth
        cap returns 429.

        Private registries: if the image's registry host (Docker Hub, GHCR, or
        Google Artifact Registry) matches a credential stored via
        `POST /v1/registry-credentials`, the pull authenticates automatically
        — no change to this request. Without a matching credential the pull is
        anonymous; if the registry then rejects it, `status` becomes `failed`
        with a distinct authentication-failure `build_error`.
      operationId: importTemplate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TemplateImportRequest"
      responses:
        "202":
          description: Import accepted; the template is building
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TemplateResponse"
        default:
          $ref: "#/components/responses/Error"

  /v1/sessions/{id}/templates:
    post:
      summary: Save a session as a reusable template
      description: >-
        Promote the current state of a live or paused-warm session into a named, reusable user
        template. New sessions can then start from it by name.
      operationId: createTemplate
      parameters:
        - $ref: "#/components/parameters/SessionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TemplateCreateRequest"
      responses:
        "200":
          description: Template created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TemplateResponse"
        default:
          $ref: "#/components/responses/Error"

  /v1/templates/{name}:
    get:
      summary: Get a single template
      description: >-
        Fetch one template the caller can see, by bare name. Returns its build
        `status` and, for a template still `queued` behind the serial build
        queue, its `queue_position`.
      operationId: getTemplate
      parameters:
        - $ref: "#/components/parameters/TemplateName"
      responses:
        "200":
          description: The template
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TemplateResponse"
        default:
          $ref: "#/components/responses/Error"
    delete:
      summary: Delete a user template
      description: Delete a template the caller has published. Official templates cannot be deleted.
      operationId: deleteTemplate
      parameters:
        - $ref: "#/components/parameters/TemplateName"
      responses:
        "204":
          description: Deleted
        default:
          $ref: "#/components/responses/Error"

  /v1/registry-credentials:
    get:
      summary: List stored private-registry pull credentials
      description: >-
        List the caller's stored private-registry credentials (#979), redacted
        — the secret is never returned, only a last-4 display fragment.
      operationId: listRegistryCredentials
      responses:
        "200":
          description: Stored registry credentials
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RegistryCredentialListResponse"
        default:
          $ref: "#/components/responses/Error"
    post:
      summary: Store a private-registry pull credential
      description: >-
        Register a (registry host, username, secret) credential so a later
        `POST /v1/templates` import from a matching host authenticates
        automatically. Covers Docker Hub, GHCR, and Google Artifact Registry
        (token/user-pass auth); encrypted at rest, never returned by any read
        endpoint. Rotation is delete-then-create for v1 — reusing a name
        already in use returns 409.
      operationId: createRegistryCredential
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RegistryCredentialCreateRequest"
      responses:
        "201":
          description: Credential stored
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RegistryCredentialResponse"
        "409":
          description: A credential with this name already exists
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        default:
          $ref: "#/components/responses/Error"

  /v1/registry-credentials/{name}:
    delete:
      summary: Delete a stored registry credential
      description: Idempotent — deleting a name that doesn't exist still succeeds.
      operationId: deleteRegistryCredential
      parameters:
        - name: name
          in: path
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Deleted
        default:
          $ref: "#/components/responses/Error"

  /v1/connectors:
    get:
      summary: List stored egress connectors
      description: >-
        List the caller's stored egress connectors (#791), redacted — the
        secret is never returned, only a last-4 display fragment.
      operationId: listConnectors
      responses:
        "200":
          description: Stored connectors
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConnectorListResponse"
        default:
          $ref: "#/components/responses/Error"
    post:
      summary: Register an egress connector
      description: >-
        Register a (name, target base URL, auth shape, secret) connector
        binding (#791). A session that later opts into this connector by
        name (session create's `connectors` field) can call
        `http://<its own default gateway>:18080/<name>/<path>` from inside
        the guest and have the host-side proxy inject the credential into
        the real request, without the guest ever holding or seeing it.
        Encrypted at rest, never returned by any read endpoint. Rotation is
        delete-then-create for v1 — reusing a name already in use returns
        409. Sessions without this name in their `connectors` list get a
        distinct "not enabled" error for it, regardless of whether it
        exists.
      operationId: createConnector
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ConnectorCreateRequest"
      responses:
        "201":
          description: Connector stored
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConnectorResponse"
        "409":
          description: A connector with this name already exists
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        default:
          $ref: "#/components/responses/Error"

  /v1/connectors/{name}:
    delete:
      summary: Delete a stored connector
      description: Idempotent — deleting a name that doesn't exist still succeeds.
      operationId: deleteConnector
      parameters:
        - name: name
          in: path
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Deleted
        default:
          $ref: "#/components/responses/Error"

  /v1/org/webhooks:
    get:
      summary: List webhook subscriptions (#976)
      description: >-
        List the caller's org's webhook endpoints. The signing secret is
        never returned — only a last-4 display fragment (secret_last4);
        the full secret is shown exactly once, on create.
      operationId: listWebhooks
      responses:
        "200":
          description: Webhook endpoints
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookListResponse"
        default:
          $ref: "#/components/responses/Error"
    post:
      summary: Create a webhook subscription (#976)
      description: >-
        Register a URL to receive the same auditlog.ClientEvent envelope the
        account-scoped SSE stream (GET /v1/events) sends, signed with
        HMAC-SHA256 (see the Inis-Signature response header contract) and
        retried with bounded exponential backoff. event_types is an allowlist
        of event-type strings (e.g. session.created); omit or leave empty to
        receive every type the stream carries. Owner/admin only.
      operationId: createWebhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookCreateRequest"
      responses:
        "201":
          description: >-
            Webhook created. The `secret` field is populated on THIS response
            only — store it now, it is never returned again.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookResponse"
        default:
          $ref: "#/components/responses/Error"

  /v1/org/webhooks/{id}:
    delete:
      summary: Delete a webhook subscription (#976)
      operationId: deleteWebhook
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Deleted
        default:
          $ref: "#/components/responses/Error"

  /v1/org/webhooks/{id}/test:
    post:
      summary: Fire a synthetic test delivery (#976)
      description: >-
        Sends one signed "webhook.test" event to the endpoint immediately,
        bypassing the retry queue, and returns the delivered/dead result
        synchronously.
      operationId: testWebhook
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Test delivery result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookDelivery"
        default:
          $ref: "#/components/responses/Error"

  /v1/org/webhooks/{id}/deliveries:
    get:
      summary: Delivery log for a webhook subscription (#976, catch-up added #1768)
      description: >-
        Two modes. Default (no since_id): the most recent deliveries
        (attempts + outcome), newest first — a debugging view. With
        ?since_id=<cursor>: the catch-up surface — deliveries with
        id > since_id, OLDEST first, plus next_since_id to pass on the next
        call. A consumer that reconnects after downtime (including downtime
        past the delivery retry horizon) calls this with the highest
        delivery id it has already processed and is whole again regardless
        of how long it was down: pending, delivered AND dead rows all come
        back, since webhook_deliveries is the durable per-endpoint record of
        every event this endpoint's filter ever matched.
      operationId: listWebhookDeliveries
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 50
        - name: since_id
          in: query
          required: false
          description: >-
            Catch-up cursor: return deliveries with id greater than this
            value, oldest first, instead of the default newest-first
            debugging view. Pass 0 to fetch from the very start of this
            endpoint's delivery history.
          schema:
            type: integer
      responses:
        "200":
          description: Delivery log
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookDeliveryListResponse"
        default:
          $ref: "#/components/responses/Error"

  /v1/org/webhooks/{id}/deliveries/{delivery_id}/redeliver:
    post:
      summary: Manually re-attempt one delivery (#1768)
      description: >-
        Resets one delivery (dead or delivered) back to pending with an
        immediate retry, picked up by the background dispatcher on its next
        tick. For debugging a single delivery rather than replaying a whole
        catch-up range via ?since_id. Owner/admin only.
      operationId: redeliverWebhookDelivery
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: delivery_id
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: The delivery, reset to pending
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookDelivery"
        default:
          $ref: "#/components/responses/Error"

  /v1/org/egress-cap:
    get:
      summary: Get the org's daily egress cap and today's usage (#2056)
      description: >-
        Egress is metered per UTC day. When today_bytes passes cap_bytes the
        platform pauses every live session in the org (session.paused with
        reason egress_capped, plus one egress.capped org event per day) and
        refuses create and resume with 409 egress_capped until the day rolls
        or an operator raises the cap. An egress.approaching event fires once
        per day at a fraction of the cap. Read-only for the org: the cap
        protects the platform, so raising it is an operator action.
      operationId: getEgressCap
      responses:
        "200":
          description: The org's effective cap and today's usage
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EgressCap"
        default:
          $ref: "#/components/responses/Error"

  /v1/org/request-endpoint:
    get:
      summary: Get the org's request endpoint (#2046)
      description: >-
        Get the caller's org's request endpoint. Any org member may read.
        The signing secret is never returned — only a last-4 display fragment
        (secret_last4); the full secret is shown exactly once, on create or rotate.
      operationId: getRequestEndpoint
      responses:
        "200":
          description: Request endpoint
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RequestEndpoint"
        "404":
          description: Request endpoint not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        default:
          $ref: "#/components/responses/Error"
    post:
      summary: Create an org request endpoint (#2046)
      description: >-
        Configure a HTTPS endpoint to receive synchronous questions posted by
        sessions created with request=true. Owner/admin only. The signing secret
        is returned in this response ONLY.
      operationId: createRequestEndpoint
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RequestEndpointCreateRequest"
      responses:
        "201":
          description: >-
            Request endpoint created. The `secret` field is populated on THIS response
            only — store it now, it is never returned again.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RequestEndpoint"
        "409":
          description: Request endpoint already configured for this org
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        default:
          $ref: "#/components/responses/Error"
    delete:
      summary: Delete the org's request endpoint (#2046)
      description: >-
        Delete the caller's org's request endpoint. Owner/admin only.
      operationId: deleteRequestEndpoint
      responses:
        "204":
          description: Deleted
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        default:
          $ref: "#/components/responses/Error"

  /v1/org/request-endpoint/rotate-secret:
    post:
      summary: Rotate org request endpoint signing secret (#2046)
      description: >-
        Rotate the signing secret for the caller's org's request endpoint.
        Owner/admin only. The new signing secret is returned in this response ONLY.
      operationId: rotateRequestEndpointSecret
      responses:
        "200":
          description: Secret rotated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RequestEndpoint"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        default:
          $ref: "#/components/responses/Error"

  /v1/org/otlp-export:
    get:
      summary: Get the org's OTLP/HTTP logs export config (#976)
      operationId: getOTLPExport
      responses:
        "200":
          description: Current config (empty object if unset)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OTLPExportResponse"
        default:
          $ref: "#/components/responses/Error"
    put:
      summary: Set the org's OTLP/HTTP logs export endpoint (#976)
      description: >-
        Session lifecycle/exec/egress events are exported as OTLP logs
        (protobuf-JSON encoded ExportLogsServiceRequest) to this endpoint.
        Strictly per-org: only this org's own events are ever sent here.
        Owner/admin only.
      operationId: setOTLPExport
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OTLPExportSetRequest"
      responses:
        "200":
          description: Saved config
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OTLPExportResponse"
        default:
          $ref: "#/components/responses/Error"
    delete:
      summary: Disable OTLP export for this org (#976)
      operationId: deleteOTLPExport
      responses:
        "204":
          description: Deleted
        default:
          $ref: "#/components/responses/Error"

  /v1/org/domains:
    get:
      summary: List registered custom domains (#980)
      description: >-
        List the caller's org's custom domains, verified and unverified.
      operationId: listDomains
      responses:
        "200":
          description: Registered domains
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DomainListResponse"
        default:
          $ref: "#/components/responses/Error"
    post:
      summary: Register a custom domain (#980)
      description: >-
        Registers domain as a pending (unverified) custom domain for the
        caller's org, up to a per-org limit. The response's verify_txt_name /
        verify_txt_value are the TXT record to create before calling verify —
        alternatively, CNAMEing the domain to the fleet's ingress base domain
        also satisfies verification. Owner/admin only.
      operationId: createDomain
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DomainCreateRequest"
      responses:
        "201":
          description: Domain registered (unverified)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DomainResponse"
        default:
          $ref: "#/components/responses/Error"

  /v1/org/domains/{id}:
    delete:
      summary: Delete a registered custom domain (#980)
      operationId: deleteDomain
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Deleted
        default:
          $ref: "#/components/responses/Error"

  /v1/org/domains/{id}/verify:
    post:
      summary: Verify DNS ownership of a registered domain (#980)
      description: >-
        Runs a live DNS check (TXT record at _inis-verify.<domain>, or the
        domain CNAMEing to the fleet's ingress base domain) and marks the
        domain verified on success. Safe to call repeatedly. Only a verified
        domain can be routed to a session (see the route sub-resource below)
        or obtain a TLS certificate.
      operationId: verifyDomain
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Verification result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DomainResponse"
        "409":
          description: Verification failed — the TXT/CNAME record was not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        default:
          $ref: "#/components/responses/Error"

  /v1/org/domains/{id}/route:
    put:
      summary: Bind a verified domain to a session port (#980)
      description: >-
        Routes domain to sessionID's exposed port. Requires the domain to
        already be verified. v1 binds directly to a (session_id, port) pair —
        the same ephemeral unit preview URLs use — rather than a longer-lived
        project-level route; rebinding after a session is destroyed is a
        follow-up call with the new session_id.
      operationId: setDomainRoute
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DomainRouteRequest"
      responses:
        "200":
          description: Updated domain
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DomainResponse"
        "409":
          description: Domain is not verified yet
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        default:
          $ref: "#/components/responses/Error"
    delete:
      summary: Clear a domain's session/port binding (#980)
      description: >-
        Unroutes the domain without deleting the registration — verified
        state is preserved, so it can be rebound later without re-verifying.
      operationId: clearDomainRoute
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Updated domain
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DomainResponse"
        default:
          $ref: "#/components/responses/Error"

  /v1/volumes:
    get:
      summary: List your persistent volumes
      description: >-
        List every persistent volume the caller owns. Served from the
        control-plane registry — never proxied to a node, so this answers
        even before any volume has ever been attached anywhere.
      operationId: listVolumes
      responses:
        "200":
          description: The caller's persistent volumes
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VolumeListResponse"
        default:
          $ref: "#/components/responses/Error"
    post:
      summary: Create a persistent volume
      description: >-
        Creates a new, empty persistent volume and returns its server-issued
        id. This is now the only way a volume comes into existence — naming
        an arbitrary string as `volume_id` on session-create/execute used to
        lazily create one on first attach; that door is closed. Attach it to
        a session or one-shot run afterwards by passing the returned `id` as
        `volume_id`; naming an unrecognized or another tenant's id there
        returns 404. Grow it later with `PATCH /v1/volumes/{id}` (grow-only —
        shrinking isn't supported, since shrinking a filesystem with data in
        it isn't safe).

        Two account-level ceilings apply in addition to the per-volume
        `size_gb` cap: total provisioned GB summed across every volume you
        own (attached or not — a detached volume still occupies storage) and
        the number of volumes you can have at once. Exceeding either is
        rejected with `409 conflict` naming the limit and your current
        usage; both raise with a paid tier.
      operationId: createVolume
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VolumeCreateRequest"
      responses:
        "201":
          description: Volume created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VolumeResponse"
        default:
          $ref: "#/components/responses/Error"

  /v1/volumes/{id}:
    get:
      summary: Get a persistent volume
      description: >-
        Size, creation time, and whether a live or paused session currently
        has this volume attached. Scoped to the caller's own volumes; naming
        another customer's volume id 404s, indistinguishable from a volume
        that never existed.
      operationId: getVolume
      parameters:
        - $ref: "#/components/parameters/VolumeId"
      responses:
        "200":
          description: The volume
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VolumeResponse"
        default:
          $ref: "#/components/responses/Error"
    patch:
      summary: Grow a persistent volume
      description: >-
        Grows a volume's capacity. Grow-only: `size_gb` must be strictly
        greater than the volume's current size — a request at or below the
        current size is rejected (400), never silently accepted as a no-op
        and never treated as a shrink. Requests over the per-volume
        maximum (128 GiB) are rejected the same way create's are.

        Refused with `409 conflict` while the volume is attached to a live or
        paused session, or while an attach/detach is in progress — detach it
        first. This is a deliberate scope decision, not a missing feature:
        growing the filesystem on a disk image mounted read-write inside a
        running VM is unsafe to do live, so the resize is only ever
        performed while the volume is idle. The larger size then takes
        effect the next time the volume is attached.

        The same total-provisioned-GB account ceiling `POST /v1/volumes`
        enforces applies here too — growing a volume consumes quota exactly
        like creating one does, so a grow that would push the account over
        its cap is rejected with `409 conflict` naming the limit and current
        usage.
      operationId: resizeVolume
      parameters:
        - $ref: "#/components/parameters/VolumeId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VolumeResizeRequest"
      responses:
        "200":
          description: Volume resized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VolumeResponse"
        "409":
          description: >-
            Volume is attached to a live or paused session, an attach/detach
            is in progress, or the account's storage quota would be exceeded.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        default:
          $ref: "#/components/responses/Error"
    delete:
      summary: Delete a persistent volume
      description: >-
        Permanently removes a customer persistent volume image
        (right-to-erasure/offboarding). Refused with 409 while the volume is
        attached to a live or paused session — destroy that session first.
        Scoped to the caller's own volumes; naming another customer's volume
        id 404s, indistinguishable from a volume that never existed.
      operationId: deleteVolume
      parameters:
        - $ref: "#/components/parameters/VolumeId"
      responses:
        "204":
          description: Deleted
        "409":
          description: Volume is attached to a live or paused session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        default:
          $ref: "#/components/responses/Error"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
  parameters:
    SessionId:
      name: id
      in: path
      required: true
      schema:
        type: string
    CheckpointId:
      name: cid
      in: path
      required: true
      schema:
        type: string
    ArtifactId:
      name: aid
      in: path
      required: true
      schema:
        type: string
    TemplateName:
      name: name
      in: path
      required: true
      schema:
        type: string
    VolumeId:
      name: id
      in: path
      required: true
      schema:
        type: string
    ProcessName:
      name: name
      in: path
      required: true
      description: The name a background process was started with.
      schema:
        type: string
  responses:
    Error:
      description: Error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
  schemas:
    Error:
      type: object
      required: [error]
      description: >-
        Every non-2xx response uses this envelope. A Retry-After header
        (RFC 9110 §10.2.3, integer delta-seconds) accompanies the response
        whenever the failure is transient and safe to retry after a wait —
        this schema cannot express a response header, so treat its presence
        as the strongest retryability signal, independent of `code`. An
        X-Inis-Request-Id response header (present on every response, not
        just errors) is safe to share with support. See #1235 for the full
        cross-SDK/MCP/CLI contract this schema documents.
      properties:
        error:
          type: string
          description: Human-readable message. Not a stable identifier — wording may change; branch on `code`, never on this text.
        code:
          type: string
          description: >-
            Stable machine-readable identifier for the failure. Treat an
            unrecognized value as a generic failure of the class implied by
            the HTTP status, never as absent — this field intentionally has
            no enum constraint so a client never rejects a code added after
            it was built. Values currently emitted:

            `validation` (400, malformed/invalid request) ·
            `bad_request` (400, legacy validation alias) ·
            `payload_too_large` (400, request content exceeds a transport
            limit — deterministic, do not retry unmodified) ·
            `unauthenticated` (401, missing/invalid/expired credential) ·
            `unauthorized` (401, legacy alias — no longer emitted by the
            public API as of #1420, kept documented for a caller still
            matching the pre-rename spelling; treat as equivalent to
            `unauthenticated`) ·
            `payment_required` (402, zero/negative balance, spend cap
            reached, or the control plane explicitly rejected the balance
            check) ·
            `forbidden` (403, valid credential not allowed to do this) ·
            `not_found` (404) ·
            `method_not_allowed` (405, wrong HTTP method for this path — a
            valid client should never send one; not retryable, check the
            API reference) ·
            `conflict` (409, generic resource-state conflict) ·
            `session_not_live` (409, live-only op against a paused/ending/
            not-yet-ready session — safe to retry after the session
            transitions) ·
            `org_has_other_members` (409, account closure refused because
            the organisation still has members besides the requesting
            owner — remove them first) ·
            `no_restore_point` (409, a POST /v1/sessions/{id}/recover refused
            because the session has no completed cold snapshot in object
            storage, or the storage object was missing — do not retry without a
            usable snapshot) ·
            `idempotency_key_in_progress` (409, a POST /v1/sessions carrying
            an `Idempotency-Key` header matches another request for the same
            key + customer that is still being processed — safe to retry
            shortly, it will resolve to the same session once the other
            request finishes; Retry-After set) ·
            `session_ended` (410, session definitively past recall — do not
            retry; when the cause is a session past its own max_lifetime_ms,
            GET /v1/sessions/{id} reports this ahead of time via
            `state: "lifetime_expired"` rather than only surfacing it once a
            resume attempt fails, #1707) ·
            `session_node_lost` (410, the session's home node was declared
            permanently dead by the platform's node-loss detection with no
            safe restore point remaining — recreate the session from its
            template, restore from your last checkpoint if you have one, or
            call POST /v1/sessions/{id}/recover to restore from cold storage
            if an archive survives; data since then is gone; do not retry.
            Every session operation returns this EXCEPT GET, which still
            reports `state: "node_lost"`, DELETE, which still destroys the
            session, and POST /v1/sessions/{id}/recover, which attempts
            recovery — see DELETE /v1/sessions/{id}) ·
            `rate_limited` / `concurrency_limit_exceeded` (429, node) /
            `rate_limit_exceeded` (429, platform) — back off and retry ·
            `account_concurrency_exceeded` (429, the caller's account is
            already at its concurrent live-session ceiling — back off and
            retry, Retry-After set) ·
            `key_concurrency_exceeded` (429, the caller's API key is already
            at its concurrent live-session ceiling — back off and retry,
            Retry-After set) ·
            `bad_gateway` (502, upstream node unreachable/failing — safe to
            retry with backoff, the router may resolve it) ·
            `session_unavailable` (503, session is live but its VM is
            wedged/unreachable) ·
            `session_node_unavailable` (503, the session's home node is
            unreachable; Retry-After set) ·
            `template_version_unavailable` (502/503, a pinned template
            version could not be materialized on this node) ·
            `archive_unavailable` (503, no working cold tier to durably
            pause to — needs a config/placement change, do not retry as-is) ·
            `size_unavailable` (503, requested size cannot fit on any node
            in the fleet today — pick a smaller size, do not retry as-is) ·
            `node_disk_pressure` (503, the node is too full to take this work
            right now — a create, fork, restore, one-shot run, resume or
            cold adopt refused by the node's disk gate; back off and retry,
            Retry-After set. A refused WAKE also raises a
            session.wake_refused event on the org stream, and a refused
            create a session.create_refused, so a tenant that was not the
            caller still learns of it) ·
            `unavailable` / `service_unavailable` (502/503, a dependency is
            temporarily unreachable — safe to retry) ·
            `internal` (500, unexpected server-side failure; message is
            intentionally generic) ·
            `internal_error` (500, legacy alias — no longer emitted by the
            public API as of #1420, kept documented for a caller still
            matching the pre-rename spelling; treat as equivalent to
            `internal`) ·
            `error` (fallback used only when no more specific code applies).
    HealthResponse:
      type: object
      properties:
        ok:
          type: boolean
        available_restore_slots:
          type: integer
        node_id:
          type: string
        paused_sessions:
          type: integer
    ExecuteRequest:
      type: object
      required: [language, code]
      properties:
        language:
          type: string
          enum: [python, node, bun]
        code:
          type: string
        dependencies:
          type: array
          items:
            type: string
        volume_id:
          type: string
          description: >-
            Attaches an existing persistent volume (POST /v1/volumes) by its
            server-issued id. Must already exist and be owned by the caller —
            naming an unknown or another tenant's id returns 404. Omitted
            starts with a fresh, non-persistent disk instead.
        timeout_ms:
          type: integer
        size:
          type: string
          enum: [small, medium, large, xlarge]
          description: >-
            Sandbox tier for this one-shot run (small 1 vCPU/1 GB, medium 2/4 GB,
            large 4/8 GB, xlarge 8/16 GB). Omitted defaults to the server's
            default tier.
        template:
          type: string
          description: >-
            Base environment to start this one-shot run from. Empty or "base"
            uses the default golden path. Any other name resolves to an official
            template or one the caller has published; an optional "name@version"
            pins a specific version.
        no_sudo:
          type: boolean
          description: >-
            Run this one-shot in hardened mode for untrusted code: the default
            user loses passwordless sudo. Omitted (the default) leaves sudo
            available.
        project_id:
          type: string
          description: >-
            Attribute this one-shot run to this project instead of the
            credential's default project. A project-scoped API key must omit
            this or set it to exactly the project the key was minted into —
            any other value is rejected (403); a request can never widen what
            a key may act on. Omitted falls back to the credential's own
            project.
        command_capture:
          type: string
          enum: [hash, executable, full]
          description: >-
            Command metadata retained in the tamper-evident execution log.
            Omitted defaults to executable; full is an explicit content-capture opt-in.
        connections:
          type: array
          items:
            $ref: "#/components/schemas/ConnectionCreate"
          description: >-
            Inline egress Connection leases for this one-shot run (see
            SessionCreateRequest.connections and /docs/connections for the
            full contract). NOTE: accepted and stored, but the one-shot
            injection datapath is not yet wired end-to-end (#1067) — a
            Connection declared here does not actually intercept or inject
            a credential for this run today. Use `POST /v1/sessions` with
            `connections` for a persistent session instead; this field
            documents the eventual one-shot contract, not a working path
            yet.
    ExecuteResponse:
      type: object
      properties:
        stdout:
          type: string
        stderr:
          type: string
        exit_code:
          type: integer
        duration_ms:
          type: integer
        install_ms:
          type: integer
        restore_ms:
          type: integer
          description: >-
            Host-measured wall time spent acquiring a pool slot and restoring
            (or cold-booting) the VM before the code ran. Observability only;
            billing tracks duration_ms (execution).
        phase:
          type: string
        timed_out:
          type: boolean
        truncated:
          type: boolean
          description: >-
            True when stdout/stderr are an incomplete view of what the code
            actually produced — either the guest's own per-stream capture cap
            cut it or the response was shortened on the way out. Absent/false
            means the response is complete.
        stdout_encoding:
          type: string
          enum: [base64]
          description: >-
            Present (and always "base64") only when stdout is base64-encoded
            rather than plain text — the code's output on this stream was not
            valid UTF-8, so it is returned exactly rather than silently
            corrupted. Absent means stdout is plain text, as always.
        stderr_encoding:
          type: string
          enum: [base64]
          description: Same as stdout_encoding, for stderr.
    ExecResponse:
      type: object
      properties:
        stdout:
          type: string
        stderr:
          type: string
        exit_code:
          type: integer
        duration_ms:
          type: integer
        restore_ms:
          type: integer
          description: >-
            Host-measured wall time spent acquiring a pool slot and restoring
            (or cold-booting) the VM before the command ran. Observability only;
            billing tracks duration_ms (execution).
        timed_out:
          type: boolean
        truncated:
          type: boolean
          description: >-
            True when stdout/stderr are an incomplete view of what the
            command actually produced — either the guest's own per-stream
            capture cap cut it or the response was shortened on the way out.
            Absent/false means the response is complete.
        stdout_encoding:
          type: string
          enum: [base64]
          description: >-
            Present (and always "base64") only when stdout is base64-encoded
            rather than plain text — the command's output on this stream was
            not valid UTF-8, so it is returned exactly rather than silently
            corrupted. Absent means stdout is plain text, as always.
        stderr_encoding:
          type: string
          enum: [base64]
          description: Same as stdout_encoding, for stderr.
    ProcessStartRequest:
      type: object
      required: [name, command]
      properties:
        name:
          type: string
          description: >-
            Caller-chosen identifier, unique among currently-running processes
            in the session. Lowercase alphanumerics and hyphens.
        command:
          type: array
          items:
            type: string
        cwd:
          type: string
        keep_alive:
          type: boolean
          description: >-
            When true, the session's idle timer is suppressed while this
            process is running — the same mechanism an attached PTY or a
            pinned ingress connection uses. Default false: the process runs
            detached, but does not by itself keep an otherwise-idle session
            live.
    ProcessInfo:
      type: object
      properties:
        name:
          type: string
        pid:
          type: integer
        command:
          type: array
          items:
            type: string
        state:
          type: string
          enum: [running, exited]
        exit_code:
          type: integer
        keep_alive:
          type: boolean
        started_at:
          type: string
        ended_at:
          type: string
    ProcessListResponse:
      type: object
      properties:
        processes:
          type: array
          items:
            $ref: "#/components/schemas/ProcessInfo"
    ProcessLogsResponse:
      type: object
      properties:
        name:
          type: string
        stdout:
          type: string
        stderr:
          type: string
        truncated:
          type: boolean
          description: >-
            True when stdout/stderr are an incomplete view of the process's
            actual output — the guest's own per-read tail cap cut it, or the
            response was shortened on the way out. Absent/false means the
            response is complete.
        stdout_encoding:
          type: string
          enum: [base64]
          description: >-
            Present (and always "base64") only when stdout is base64-encoded
            rather than plain text — the process's output on this stream was
            not valid UTF-8, so it is returned exactly rather than silently
            corrupted. Absent means stdout is plain text, as always.
        stderr_encoding:
          type: string
          enum: [base64]
          description: Same as stdout_encoding, for stderr.
    FileReadResponse:
      type: object
      properties:
        path:
          type: string
        content:
          type: string
        encoding:
          type: string
          enum: [text, base64]
          description: Echoes the requested encoding, so the caller knows how to interpret content.
        duration_ms:
          type: integer
    FileWriteResponse:
      type: object
      properties:
        path:
          type: string
        duration_ms:
          type: integer
    FileOpResponse:
      type: object
      description: Returned by mkdir and delete — no interesting payload beyond confirmation.
      properties:
        path:
          type: string
        duration_ms:
          type: integer
    FileRenameResponse:
      type: object
      properties:
        path:
          type: string
        dest_path:
          type: string
        duration_ms:
          type: integer
    FileStreamWriteResponse:
      type: object
      properties:
        path:
          type: string
        bytes:
          type: integer
          format: int64
        sha256:
          type: string
          description: sha256 digest (hex) of the uploaded content, for round-trip verification.
        duration_ms:
          type: integer
    FileListResponse:
      type: object
      properties:
        path:
          type: string
        entries:
          type: array
          items:
            type: string
        duration_ms:
          type: integer
    FileBatchItem:
      type: object
      required: [path, content]
      properties:
        path:
          type: string
        content:
          type: string
        encoding:
          type: string
          enum: [text, base64]
    FileBatchResult:
      type: object
      properties:
        path:
          type: string
        ok:
          type: boolean
        error:
          type: string
    FileBatchWriteResponse:
      type: object
      properties:
        results:
          type: array
          items:
            $ref: "#/components/schemas/FileBatchResult"
        duration_ms:
          type: integer
    FileFindResponse:
      type: object
      properties:
        path:
          type: string
        paths:
          type: array
          items:
            type: string
        truncated:
          type: boolean
          description: True when max_results capped the result set — narrow the pattern or raise max_results for the rest.
        duration_ms:
          type: integer
    GrepMatch:
      type: object
      properties:
        path:
          type: string
        line_number:
          type: integer
        line:
          type: string
        before:
          type: array
          items:
            type: string
        after:
          type: array
          items:
            type: string
    FileGrepResponse:
      type: object
      properties:
        path:
          type: string
        matches:
          type: array
          items:
            $ref: "#/components/schemas/GrepMatch"
        files_searched:
          type: integer
        truncated:
          type: boolean
          description: True when a result cap was hit — narrow the pattern or raise max_results for the rest.
        duration_ms:
          type: integer
    SessionCreateRequest:
      type: object
      properties:
        volume_id:
          type: string
          description: >-
            Attaches an existing persistent volume (POST /v1/volumes) by its
            server-issued id. Must already exist and be owned by the caller —
            naming an unknown or another tenant's id returns 404. Omitted
            starts with a fresh, non-persistent disk instead.
        max_lifetime_ms:
          type: integer
          description: >-
            Hard cap on session age in ms, live or paused, from creation.
            Omitted or 0 = no cap. Reaching it auto-pauses the session
            PERMANENTLY — unlike idle_timeout_ms, it can never resume again
            after this (any resume attempt refuses with 410); the paused
            data itself is still bounded only by paused_ttl_ms, not deleted
            by this. GET reports this ahead of a resume attempt via
            state: "lifetime_expired" (see SessionResponse.state) — poll
            that instead of discovering it from a failed resume. See
            /docs/session-lifecycle. Cannot be combined with a negative
            paused_ttl_ms ("never expire") — a session that can never
            resume again and is also never reclaimed would keep accruing
            paused-storage billing forever with no way out; that
            combination is rejected at create time (400).
        idle_timeout_ms:
          type: integer
        egress_cap_bytes:
          type: integer
          format: int64
          description: >-
            Per-session daily egress cap in bytes (#2056). When today's
            egress passes it the platform pauses this session alone (reason
            egress_capped) and publishes egress.session_capped; the rest of
            the org keeps running. 0 or absent means no per-session cap; the
            org cap still applies. Change later with PATCH /v1/sessions/{id}.
        idle_mode:
          type: string
          enum: [ops, activity]
          description: >-
            Which signal the idle timer trusts before pausing (#1653).
            "ops" (default) is today's behaviour: op/PTY/keep_alive-based
            idle tracking. "activity" defers the pause while the guest
            shows CPU or network activity above threshold — for a session
            doing real work with no API ops (e.g. a process driven by an
            external event source). idle_timeout_ms is floored to 30s in
            activity mode to prevent pause-storms.
        idle_busy_cpu_threshold_pct:
          type: integer
          description: >-
            Per-session override of the activity-mode CPU threshold
            (percent of one vCPU). Only consulted when idle_mode=activity.
            Omitted or 0 uses the fleet-wide default (2%).
        idle_busy_network_bytes_per_sec:
          type: integer
          description: >-
            Per-session override of the activity-mode network threshold
            (combined guest tx+rx bytes/sec). Only consulted when
            idle_mode=activity. Omitted or 0 uses the fleet-wide default
            (1KB/s).
        name:
          type: string
        labels:
          type: object
          additionalProperties:
            type: string
        external_id:
          type: string
          maxLength: 256
          description: >-
            Optional caller-owned index key (#1375): a user id, workflow id,
            or thread id to stamp this session with so it can be looked up
            later via `GET /v1/sessions?external_id=...` without maintaining
            a separate external_id -> session_id mapping table. NOT
            unique — unlike `name`, multiple sessions may share one. Must be
            valid UTF-8 with no control characters.
        size:
          type: string
          enum: [small, medium, large, xlarge]
          description: >-
            Sandbox tier (small 1 vCPU/1 GB, medium 2/4 GB, large 4/8 GB, xlarge
            8/16 GB). Omitted defaults to the server's default tier (or, with `template`
            set, the template's captured tier). The default tier is served warm
            — restored from a golden snapshot rather than booted. Other tiers
            are warm only when the operator builds per-tier golden snapshots and
            enables them; otherwise they cold-boot, which takes seconds rather
            than milliseconds. With `template` set, a size matching the
            template's captured tier warm-restores; any other valid size
            cold-boots that template's disk fresh instead — same installed
            software, but without the captured tier's resumed in-memory
            processes.
        template:
          type: string
          description: >-
            Base environment to start from. Empty or "base" uses the default
            golden path. Any other name resolves to an official template or one
            the caller has published; an optional "name@version" pins a specific
            version. A template is captured at a fixed tier, but `size` is not
            constrained to it: a different size cold-boots the template's disk
            at that tier instead of warm-restoring the captured snapshot.
        egress:
          $ref: "#/components/schemas/EgressPolicy"
        destroy_on_completion:
          type: boolean
          description: >-
            Skip persistence and tear the session down as soon as the first
            command completes (the one-shot fast path).
        no_sudo:
          type: boolean
          description: >-
            Run the session in hardened mode for untrusted code: the default
            user loses passwordless sudo. Omitted (the default) leaves sudo
            available so commands like `sudo apt install ...` work.
        events:
          type: boolean
          description: >-
            Opt this session into the guest events channel: once set, a
            workload inside the session may POST an arbitrary JSON event to
            its own host-served self API (`http://169.254.169.254/v1/self/events`),
            delivered to the org's registered webhook endpoint(s), flagged
            as guest-authored. Omitted (the default, false) means a session
            gets no outbound channel unless asked for. Informational only —
            never an authorisation path back into the platform, and not
            currently a billable/metered unit.
        request:
          type: boolean
          description: >-
            Opt this session into the guest request channel (#2046): once set, a
            workload inside the session may POST a synchronous question to its own
            host-served self API (`http://169.254.169.254/v1/self/request`), delivered
            to the org's registered request endpoint. Omitted (the default, false)
            means the self API rejects requests with 403 self_api_disabled.
        wake_on_http:
          type: boolean
          description: >-
            Whether an inbound request to a preview URL is allowed to wake
            this session from paused or cold storage. Omitted or true (the
            default) keeps today's behaviour: any request to an exposed port
            wakes a paused session automatically. Set to false to opt out —
            a request to a preview URL for a paused or cold session then
            returns 404 instead of waking it, with no charge. Explicit
            resume/exec/other API calls are unaffected either way; this only
            governs preview-URL traffic.
        on_pty_detach:
          type: string
          enum: [keep_live, pause, destroy]
          description: >-
            What the server does when the last interactive terminal (PTY)
            client detaches. "keep_live" leaves the session running until it
            idles out; "pause" (the default) pauses it immediately so it can be
            resumed; "destroy" tears it down. Omitted defaults to "pause".
            Only applies when a terminal (PTY) is attached. Has no effect for
            API-only sessions.
        paused_ttl_ms:
          type: integer
          description: >-
            Per-session paused-session TTL in milliseconds. Once paused this
            long the session is auto-destroyed (end_reason paused_ttl), freeing
            its snapshot/rootfs/volume while its history metadata survives.
            0/omitted uses the daemon default; a negative value disables the TTL
            (never expire). A negative value cannot be combined with a
            positive max_lifetime_ms: once max_lifetime_ms auto-pauses the
            session it can never be resumed again, so pairing that with
            "never expire" would leave it paused forever, unresumable, and
            still billing for paused storage — that combination is rejected
            at create time (400).
        artifacts:
          $ref: "#/components/schemas/ArtifactDeclaration"
        project_id:
          type: string
          description: >-
            Create the session in this project instead of the credential's
            default project. A project-scoped API key must omit this or set it
            to exactly the project the key was minted into — any other value
            is rejected (403); a request can never widen what a key may act
            on. Omitted falls back to the credential's own project.
        command_capture:
          type: string
          enum: [hash, executable, full]
          description: >-
            Command metadata retained for this session. Omitted defaults to
            executable; full is an explicit content-capture opt-in.
        env:
          type: object
          additionalProperties:
            type: string
          description: >-
            Plain, non-sensitive environment variables (config/flags) set for
            every process this session's exec/PTY spawns. Stored as-is and
            echoed back on the session response. Use `secrets` for anything
            sensitive.
        secrets:
          type: object
          additionalProperties:
            type: string
          description: >-
            Sensitive environment variables set for every process this
            session's exec/PTY spawns. Encrypted at rest immediately on
            create, never logged, and decrypted only in memory at the moment
            a guest request is sent. Never echoed back — GET a session to see
            only the configured secret *names* (`secret_names`), never
            values. Requires the server to have a secret-encryption key
            configured; a create carrying `secrets` on a server without one
            fails with a 503.


            Note: this protects the value in storage and in transit, not
            inside the running session. Once a secret is set in a process's
            environment, pausing or checkpointing that session snapshots its
            memory, which can include the secret's plaintext for any process
            that was holding it. These snapshots are stored on the exec node
            and, if cold archiving is enabled for your plan, uploaded to
            object storage — currently under the storage provider's
            default at-rest encryption, not a key of ours tied to your
            secret. Deleting a secret does not retroactively scrub a
            snapshot already taken. Treat any secret injected into a session
            as retrievable for as long as a snapshot containing it exists.
        connectors:
          type: array
          items:
            type: string
          description: >-
            Opts this session into the named egress connectors (#791) —
            explicit, per-session, off by default. A session that lists a
            name here can reach that connector's in-guest endpoint
            (`http://<its own default gateway>:18080/<name>/<path>`); a
            session that does NOT list a name gets a distinct "connector
            not enabled" error for it, even if the caller's account has a
            connector by that name configured elsewhere. Names are not
            validated for existence at session-create time — only at first
            use, by the connector proxy, which also scopes the lookup to
            the caller's own account.
        connections:
          type: array
          items:
            $ref: "#/components/schemas/ConnectionCreate"
          description: >-
            Inline session-scoped Connection leases (#1046) — a credential
            bound to one exact HTTPS origin, injected on the way out for
            requests that clear its `allow` rules. Distinct from the older
            `connectors` field above: a Connection needs no separate
            registration step, is scoped to this one session for its whole
            life, and is redacted on read via the session response's
            `connections` field. See /docs/connections for the full
            contract, including pause/resume, fork (refused outright while
            any Connection is active), and TTL/lifetime bounds.
    EgressPolicy:
      type: object
      description: >-
        Per-session outbound allowlist. Default "allow" keeps full public
        egress; "deny" blocks everything except the resolved IPs of the listed
        domains.
      properties:
        mode:
          type: string
          enum: [allow, deny]
          description: >-
            "allow" (default) keeps full public egress; "deny" restricts
            outbound traffic to the allow list.
        allow:
          type: array
          description: >-
            Domains reachable in deny mode. Exact ("api.openai.com") or a single
            leading wildcard ("*.githubusercontent.com"). Domains only (no
            CIDRs). Max 256 entries.
          items:
            type: string
    SelfAPIStatus:
      type: object
      description: >-
        The host-served self API's (see /docs/self-api.md) per-session gate
        state: a workload inside the VM can reach a small HTTP surface at
        http://169.254.169.254, separate from this public API, to ask
        "pause me" / "what am I?" / emit a structured event.
        SessionResponse.self_api is only ever populated on a "warm" read
        (the session's home node answering directly) -- it is omitted
        entirely on a cold/interrupted read, so its absence means "unknown
        from this read", never "known to be off".
      properties:
        events:
          type: boolean
          description: >-
            Whether a workload inside this session may POST
            /v1/self/events on the self API surface. False is the default,
            true means the session opted in at create via
            SessionCreateRequest.events.
        request:
          type: boolean
          description: >-
            Whether a workload inside this session may POST
            /v1/self/request on the self API surface (#2046). False is the default,
            true means the session opted in at create via
            SessionCreateRequest.request.
    SessionEgressResponse:
      type: object
      properties:
        session_id:
          type: string
        mode:
          type: string
          enum: [allow, deny]
        allow:
          type: array
          items:
            type: string
    ConnectionAuth:
      type: object
      required: [type]
      description: >-
        How the platform authenticates to a Connection's origin on the
        session's behalf. See /docs/connections.
      properties:
        type:
          type: string
          enum: [bearer, header]
          description: >-
            "bearer" injects `Authorization: Bearer <secret>` — header_name
            must be omitted (Authorization is implied). "header" injects
            `<secret>` verbatim under header_name, which is then required.
        secret:
          type: string
          description: >-
            The upstream credential's plaintext value. Write-only: accepted
            on create/replace, never returned by any response, log, or
            snapshot — see ConnectionStatus for the redacted read-back
            shape.
        header_name:
          type: string
          description: >-
            Header the secret is injected under. Required for type
            "header" (validated against a small unsafe-header denylist —
            Host, Cookie, Connection, and a handful of other
            connection-management headers); must be omitted for type
            "bearer".
    ConnectionAllow:
      type: object
      description: >-
        Deny-by-default allowlist a Connection's credential may be used
        under. Both methods and paths are required (non-empty) at create
        time — an omitted list authorizes nothing, it does not fall back to
        "any".
      properties:
        methods:
          type: array
          items:
            type: string
            enum: [GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS]
        paths:
          type: array
          items:
            type: string
          description: >-
            Absolute path prefixes ("/v1/customers", or "/v1/*" for a
            prefix match). No query strings, dot segments, or wildcards
            other than a single trailing "*".
    ConnectionCreate:
      type: object
      required: [name, origin]
      description: >-
        An inline session-scoped Connection lease: a credential bound to
        one exact HTTPS origin, injected on the way out for requests that
        clear `allow`. Used both to create a Connection (session create,
        POST .../connections) and to replace one (PUT .../connections/
        {name}). See /docs/connections for the full contract.
      properties:
        name:
          type: string
          description: >-
            Label for this Connection, unique within the session.
            Lowercase alphanumerics, ".", "_", "-".
        origin:
          type: string
          description: >-
            Exact `https://` origin — hostname only, no path, query, or
            port other than 443. No wildcards; one Connection per origin
            per session.
        authentication:
          $ref: "#/components/schemas/ConnectionAuth"
        allow:
          $ref: "#/components/schemas/ConnectionAllow"
        ttl_seconds:
          type: integer
          description: >-
            Lease TTL in seconds. Omitted defaults to the session's own
            max_lifetime_ms (or 1 hour if the session has no lifetime cap).
            Capped at 24h regardless, and can never exceed the session's
            own remaining lifetime — a value that would make the lease
            outlive its session is rejected.
    ConnectionStatus:
      type: object
      description: >-
        Redacted Connection lifecycle/policy metadata returned by every
        Connections read or write endpoint — never the secret value or the
        session CA's private key.
      properties:
        name:
          type: string
        origin:
          type: string
        state:
          type: string
          enum: [active, expired, revoked]
          description: >-
            "expired" is reported lazily: a lease past its TTL flips from
            "active" to "expired" the next time the session (or this
            Connection) is read, not the instant it lapses. "revoked" is a
            tombstone kept for the rest of the session's life: either way,
            a request to this origin fails closed rather than falling back
            to an unauthenticated direct connection.
        header_name:
          type: string
        allow:
          $ref: "#/components/schemas/ConnectionAllow"
        expires_at:
          type: string
        created_at:
          type: string
    ConnectionListResponse:
      type: object
      properties:
        connections:
          type: array
          items:
            $ref: "#/components/schemas/ConnectionStatus"
    SessionTokenMintRequest:
      type: object
      required: [ttl_seconds]
      description: Mint parameters for POST /v1/sessions/{id}/tokens (#1651).
      properties:
        name:
          type: string
          description: Caller-supplied label, e.g. "botel-room-42". Optional.
        capabilities:
          type: array
          description: >-
            Subset of [pty, exec, files, tunnel, view]. Omitted or empty
            means every capability — the same "unrestricted" default every
            other scope field on a token uses.
          items:
            type: string
            enum: [pty, exec, files, tunnel, view]
        ttl_seconds:
          type: integer
          description: >-
            Required, must be greater than zero. The token stops working
            once this elapses; there is no way to extend it after minting.
    SessionTokenStatus:
      type: object
      description: >-
        Redacted, read-back-safe view of a scoped token (#1651) — everything
        except the plaintext secret, which is only ever shown once, in
        SessionTokenMintResponse.
      properties:
        hash:
          type: string
          description: >-
            Identifies this token for list/revoke calls. Not the secret
            itself — a SHA-256 hash of it, safe to log or put in a URL.
        name:
          type: string
        session_id:
          type: string
        capabilities:
          type: array
          items:
            type: string
        created_at:
          type: string
        expires_at:
          type: string
        last_used_at:
          type: string
        revoked_at:
          type: string
    SessionTokenMintResponse:
      type: object
      description: >-
        SessionTokenStatus plus the plaintext bearer token — shown exactly
        once, like every other token this API mints.
      allOf:
        - $ref: "#/components/schemas/SessionTokenStatus"
        - type: object
          properties:
            token:
              type: string
    SessionResponse:
      type: object
      properties:
        session_id:
          type: string
        state:
          type: string
          description: >-
            One of: creating, live, paused, lifetime_expired, failed, ended,
            or interrupted. "creating" is returned immediately by
            POST /v1/sessions while provisioning (materializing the starting
            template, restoring/booting the VM) runs in the background —
            poll GET until it moves to "live" or "failed". "failed" means
            provisioning died before the session ever came up; see
            status_reason. "lifetime_expired" is a read-time-only
            presentation state: the session is still just paused underneath
            (still occupies its paused slot, still reclaimed by the ordinary
            paused_ttl_ms sweep, not destroyed) but its own max_lifetime_ms
            deadline (created_at + max_lifetime_ms) has passed, so
            POST .../resume will always answer 410 session_ended — reported
            instead of "paused" so a caller polling GET can tell a
            permanently-dead session apart from a normal sleeping one BEFORE
            attempting a resume and hitting the 410, rather than after. See
            status_reason for the same explanation in prose. A caller
            filtering GET /v1/sessions?state=paused still sees these rows
            (the underlying stored state is unchanged) — filter on this
            state field client-side, or check status_reason, to separate
            them out; there is no server-side ?state=lifetime_expired filter
            yet. Deliberately typed as a plain string, not an enum: existing
            SDK/CLI code already treats this field as an opaque string, and
            constraining it here would force every generated client onto a
            new named type.
        status_detail:
          type: string
          enum: [waiting_for_capacity, preparing_template, starting]
          description: >-
            Coarse, user-safe provisioning phase, set only while state ==
            "creating": "waiting_for_capacity", "preparing_template"
            (materializing the starting template — the slow case, a cold
            bundle pull), or "starting" (restore + guest warm-up). Empty
            once the session reaches a terminal state.
        status_reason:
          type: string
          description: >-
            Set when state == "failed" (a short, user-safe explanation of
            why provisioning didn't complete) or state == "lifetime_expired"
            (a short, user-safe explanation of why resume will 410 — see
            state's doc). Empty otherwise.
        volume_id:
          type: string
        created_at:
          type: string
        last_active_at:
          type: string
        node_id:
          type: string
        idle_timeout_ms:
          type: integer
        idle_mode:
          type: string
          enum: [ops, activity]
          description: >-
            Echoes SessionCreateRequest.idle_mode, resolved (always "ops" or
            "activity", never empty) (#1653).
        idle_busy_cpu_threshold_pct:
          type: integer
          description: >-
            Echoes SessionCreateRequest.idle_busy_cpu_threshold_pct. 0 means
            this session uses the fleet-wide default rather than an override.
        idle_busy_network_bytes_per_sec:
          type: integer
          description: >-
            Echoes SessionCreateRequest.idle_busy_network_bytes_per_sec. 0
            means this session uses the fleet-wide default rather than an
            override.
        max_lifetime_ms:
          type: integer
        name:
          type: string
        labels:
          type: object
          additionalProperties:
            type: string
        external_id:
          type: string
          description: >-
            Caller-owned index key set at create, if any (#1375). See
            SessionCreateRequest.external_id.
        size:
          type: string
          enum: [small, medium, large, xlarge]
          description: The resolved sandbox tier this session runs at.
        vcpus:
          type: integer
          description: vCPU count the resolved tier maps to.
        mem_mb:
          type: integer
          description: Memory (MiB) the resolved tier maps to.
        template:
          type: string
          description: >-
            Base environment this session was created from. Omitted when the
            session started from the default golden path.
        no_sudo:
          type: boolean
          description: >-
            Whether the session runs in hardened, no-sudo mode. Omitted (false)
            means sudo is available.
        wake_on_http:
          type: boolean
          description: >-
            Whether an inbound request to a preview URL is allowed to wake
            this session from paused or cold storage. Always present: true
            is the default (matches today's unconditional wake-on-HTTP
            behaviour), false means the session opted out at create — see
            SessionCreateRequest.wake_on_http.
        self_api:
          $ref: "#/components/schemas/SelfAPIStatus"
        on_pty_detach:
          type: string
          enum: [keep_live, pause, destroy]
          description: >-
            The policy enforced when the last interactive terminal (PTY) client
            detaches: "keep_live", "pause", or "destroy". Only applies when a
            terminal (PTY) is attached. Has no effect for API-only sessions.
        paused_ttl_ms:
          type: integer
          description: >-
            Per-session paused-session TTL in milliseconds (0 = daemon default,
            negative = never). Omitted when unset.
        active_since:
          type: string
          description: >-
            When the current continuous-live stretch began (RFC 3339). The
            session is force-paused (never destroyed) once it has been
            continuously live for max_active_window_ms; the clock resets on
            any pause. Omitted unless the session is live with the window
            armed.
        max_active_window_ms:
          type: integer
          description: >-
            The continuous-active window (ms) the current live stretch was
            armed with. The session auto-pauses at
            active_since + max_active_window_ms. Omitted unless the session
            is live with the window armed.
        paused_at:
          type: string
          description: >-
            When the session most recently entered the paused state (RFC
            3339) (#1831). Distinct from last_active_at (the gap between them
            is exactly the idle timeout that triggered the pause) and from
            active_since (only present while live). Durable — written to the
            session's persisted manifest on every pause, so it survives a
            node restart or cross-node cold adopt and is present on a warm
            read of any session that has ever paused. Not yet available from
            the cold catalog / GET-list read path; omitted there regardless
            of history.
        resumed_at:
          type: string
          description: >-
            When the session most recently transitioned from paused back to
            live (RFC 3339) (#1831). Best-effort and in-memory only on the
            session's home node — not persisted, so it goes empty again after
            a node restart or cross-node cold adopt until the next actual
            resume. For a session that has never paused, equals created_at.
            A caller needing a guaranteed record of every resume, not just
            the most recent one, should consume the session event stream
            instead.
        restored_from:
          type: string
          description: >-
            When the session was recovered from a node_lost state using its last
            completed cold snapshot (RFC 3339) (#2065). Set by POST
            /v1/sessions/{id}/recover. Omitted when the session was never recovered.
        mcp_url:
          type: string
          description: >-
            The session-scoped MCP connect URL. Omitted when MCP is disabled
            or no public MCP base URL is configured.
        ended_at:
          type: string
          description: >-
            When the session was torn down. Present only on ended (state=ended)
            history rows.
        end_reason:
          type: string
          enum:
            [
              client_destroy,
              shell_exit,
              on_detach_destroy,
              max_lifetime,
              paused_ttl,
              error,
            ]
          description: >-
            Why the session ended. Present only on ended (state=ended) history
            rows.
        exposed_ports:
          type: array
          items:
            type: integer
        exposed_previews:
          type: array
          description: Exposed ports with their reachable preview URLs (token embedded).
          items:
            type: object
            properties:
              port:
                type: integer
              preview_url:
                type: string
                format: uri
              visibility:
                type: string
                enum: [token, public]
              auth:
                type: string
                description: Inbound access mode for the preview URL.
                enum: [none, bearer]
        egress:
          $ref: "#/components/schemas/EgressPolicy"
        archive:
          $ref: "#/components/schemas/ArchiveStatus"
        snapshot_tier:
          type: string
          enum: [hot, warm, cold]
          description: >-
            Resume-readiness tier of a PAUSED session: "hot" (raw snapshot on
            local disk, resumes instantly), "warm" (compressed on local disk,
            resumes after a quick local decompress), or "cold" (in object
            storage only, resumes after a download). Omitted for non-paused
            sessions. Durability (whether a backup exists yet) is reported
            separately via `archive.status`.
        env:
          type: object
          additionalProperties:
            type: string
          description: >-
            Plain (non-sensitive) environment variables set on this session.
            Omitted when none were set.
        secret_names:
          type: array
          items:
            type: string
          description: >-
            Names of the secrets configured on this session — NEVER their
            values. Check this to confirm a secret is set (or to know what
            you'd be overwriting); the values themselves are never returned
            by the API.
        connector_names:
          type: array
          items:
            type: string
          description: >-
            Egress connectors (#791) this session has opted into, by name —
            never any secret material.
        connections:
          type: array
          items:
            $ref: "#/components/schemas/ConnectionStatus"
          description: >-
            Redacted lifecycle and policy metadata for this session's
            inline egress Connections (#1046) — name, origin, state,
            header name, allow rules, and expiry. Never the secret value or
            the session CA's private key. Same shape as
            GET /v1/sessions/{id}/connections.
    ArchiveStatus:
      type: object
      description: >-
        Durable (cold) object-store copy of a paused session. Present only when
        session archiving is enabled and an upload has started. The hot local
        copy keeps the session resumable regardless of upload status.
      properties:
        status:
          type: string
          description: >-
            Cold upload status. `unavailable` is terminal and NOT retryable
            (#1017): no upload was ever attempted because the node has no
            working cold tier at all, so the session exists only on that
            node's local disk and will be lost if the node is reclaimed.
            `archive/retry` cannot help while the session stays there —
            unlike `failed`, which means an attempt was made and can be
            re-driven.
          enum: [pending, complete, failed, unavailable]
        cold_uri:
          type: string
          description: Per-session object-store prefix for the bundle and manifest.
        uploaded_at:
          type: string
          description: Timestamp the upload completed (set when status is complete).
    SessionListResponse:
      type: object
      properties:
        sessions:
          type: array
          items:
            $ref: "#/components/schemas/SessionResponse"
        next_cursor:
          type: string
    SessionExecRequest:
      type: object
      required: [command]
      properties:
        command:
          type: array
          items:
            type: string
        cwd:
          type: string
        timeout_ms:
          type: integer
    ExecStreamExitPayload:
      type: object
      description: >-
        JSON body of the terminal `event: exit` SSE frame for
        POST .../exec?stream=true. Mirrors ExecResponse's exit_code/
        timed_out/duration_ms fields — stdout/stderr arrived as separate
        stdout/stderr SSE events instead of being included here.
      properties:
        exit_code:
          type: integer
        timed_out:
          type: boolean
        duration_ms:
          type: integer
    SessionFilePutRequest:
      type: object
      required: [path, content]
      properties:
        path:
          type: string
          description: Destination path inside the session (absolute or relative to /workspace).
        content:
          type: string
          description: File content to write.
        encoding:
          type: string
          enum: [text, base64]
          description: >-
            Encoding of the content field. "text" (default) writes the string as-is;
            "base64" decodes first (for binary files).
    SessionFileBatchPutRequest:
      type: object
      required: [files]
      properties:
        files:
          type: array
          items:
            $ref: "#/components/schemas/FileBatchItem"
    SessionFileMkdirRequest:
      type: object
      required: [path]
      properties:
        path:
          type: string
          description: Directory to create (with parents) inside the session.
    SessionFileRenameRequest:
      type: object
      required: [path, dest_path]
      properties:
        path:
          type: string
          description: Source path inside the session.
        dest_path:
          type: string
          description: Destination path inside the session.
    SessionForkRequest:
      type: object
      required: [count]
      properties:
        count:
          type: integer
          minimum: 1
    ForkResponse:
      type: object
      properties:
        parent_session_id:
          type: string
        children:
          type: array
          items:
            type: string
    CheckpointCreateRequest:
      type: object
      properties:
        name:
          type: string
          description: Optional checkpoint name (lowercase alphanumerics and hyphens).
        labels:
          type: object
          additionalProperties:
            type: string
    CheckpointResponse:
      type: object
      properties:
        checkpoint_id:
          type: string
        session_id:
          type: string
        parent_session_id:
          type: string
        name:
          type: string
        labels:
          type: object
          additionalProperties:
            type: string
        size_bytes:
          type: integer
          format: int64
        created_at:
          type: string
        node_id:
          type: string
    CheckpointListResponse:
      type: object
      properties:
        checkpoints:
          type: array
          items:
            $ref: "#/components/schemas/CheckpointResponse"
    SessionRestoreRequest:
      type: object
      required: [checkpoint_id]
      properties:
        checkpoint_id:
          type: string
          description: Checkpoint id (or name) to roll this session back to.
    CheckpointSessionRequest:
      type: object
      properties:
        name:
          type: string
        labels:
          type: object
          additionalProperties:
            type: string
        max_lifetime_ms:
          type: integer
        idle_timeout_ms:
          type: integer
        idle_mode:
          type: string
          enum: [ops, activity]
          description: Mirrors SessionCreateRequest.idle_mode (#1653).
        idle_busy_cpu_threshold_pct:
          type: integer
          description: >-
            Mirrors SessionCreateRequest.idle_busy_cpu_threshold_pct.
        idle_busy_network_bytes_per_sec:
          type: integer
          description: >-
            Mirrors SessionCreateRequest.idle_busy_network_bytes_per_sec.
        wake_on_http:
          type: boolean
          description: >-
            Mirrors SessionCreateRequest.wake_on_http. Not inherited from the
            checkpoint's original session — omitted or true (the default)
            keeps wake-on-HTTP enabled for the restored session.
        project_id:
          type: string
          description: >-
            Attribute the restored session to this project instead of the
            credential's default project. Same resolution rules as
            SessionCreateRequest.project_id. The restored session's egress
            policy still comes from the checkpoint, unaffected by this field.
    ArtifactDestination:
      type: object
      description: >-
        Where a capture is written. Omit (or use type "hosted") for inis.run's
        own EU object store with signed download links. Use type "s3" for your
        own EU bucket (BYOB).
      properties:
        type:
          type: string
          enum: [hosted, s3]
        bucket:
          type: string
        prefix:
          type: string
        region:
          type: string
    ArtifactDeclaration:
      type: object
      required: [paths]
      properties:
        paths:
          type: array
          items:
            type: string
        destination:
          $ref: "#/components/schemas/ArtifactDestination"
    ArtifactCreateRequest:
      type: object
      required: [paths]
      properties:
        paths:
          type: array
          description: Paths or globs under the session workspace (/workspace) to capture (e.g. "/workspace/output/**", "/workspace/*.csv"). Paths outside /workspace are rejected.
          items:
            type: string
        destination:
          $ref: "#/components/schemas/ArtifactDestination"
    ArtifactExtendRequest:
      type: object
      required: [ttl_days]
      properties:
        ttl_days:
          type: integer
          description: New retention in days from now, capped at the hard limit.
    ArtifactFile:
      type: object
      properties:
        path:
          type: string
        url:
          type: string
          description: Pre-signed direct-download URL (no auth header required).
        size_bytes:
          type: integer
          format: int64
        content_type:
          type: string
    ArtifactResponse:
      type: object
      properties:
        id:
          type: string
        session_id:
          type: string
        status:
          type: string
          enum: [pending, ready, failed]
        captured_at:
          type: string
        expires_at:
          type: string
        files:
          type: array
          items:
            $ref: "#/components/schemas/ArtifactFile"
        error:
          type: string
    ArtifactListResponse:
      type: object
      properties:
        artifacts:
          type: array
          items:
            $ref: "#/components/schemas/ArtifactResponse"
    BatchExecRequest:
      type: object
      required: [session_ids, command]
      properties:
        session_ids:
          type: array
          items:
            type: string
        command:
          type: array
          items:
            type: string
        cwd:
          type: string
        timeout_ms:
          type: integer
    BatchExecResult:
      type: object
      properties:
        session_id:
          type: string
        stdout:
          type: string
        stderr:
          type: string
        exit_code:
          type: integer
        duration_ms:
          type: integer
        timed_out:
          type: boolean
        error:
          type: string
        stdout_encoding:
          type: string
          enum: [base64]
          description: >-
            Present (and always "base64") only when stdout is base64-encoded
            rather than plain text — this session's output on this stream was
            not valid UTF-8, so it is returned exactly rather than silently
            corrupted. Absent means stdout is plain text, as always. Was
            missing from this schema even though the server always sends it
            when set (BatchExecResult embeds the same ExecResponse fields
            single-session exec uses) — omitting it here meant generated
            clients silently dropped the signal, so a batch-exec consumer saw
            unlabeled base64 gibberish where single-exec gets a clear label.
        stderr_encoding:
          type: string
          enum: [base64]
          description: Same as stdout_encoding, for stderr.
    BatchExecResponse:
      type: object
      properties:
        results:
          type: array
          items:
            $ref: "#/components/schemas/BatchExecResult"
    SessionExposeRequest:
      type: object
      required: [port]
      properties:
        port:
          type: integer
        visibility:
          type: string
          enum: [token, public]
        auth:
          type: string
          description: >-
            Inbound access control for the preview URL. "none" (default) leaves
            it open; "bearer" requires callers to send Authorization: Bearer
            <auth_token>. Omitting this field on a route that is currently
            "bearer" therefore turns the gate OFF -- and because that would
            otherwise promote everyone already holding the preview URL from
            401 to unauthenticated access, it also mints a fresh ingress
            token, so the previously issued URL stops working. The one
            exception is a "public" route: its URL carries no token, so there
            is nothing to rotate and dropping bearer leaves the same hostname
            reachable with no auth at all. Send
            auth: "bearer" on every convergence pass if you mean to keep it.
          enum: [none, bearer]
        login_url:
          type: string
          format: uri
          description: >-
            Where to send a browser that navigates to this route with no
            valid credential (no ?t=, no session cookie, expired cookie, or a
            bookmarked link on another device) -- instead of a bare 403, the
            gateway 302s a top-level navigation (Sec-Fetch-Mode: navigate)
            there. Anything else (an API call, a webhook, a non-navigation
            GET) still gets 401/403 exactly as before, even on a route that
            sets this. Must be an absolute https:// URL (http:// is only
            accepted for localhost/dev); no fragment. Optional -- omit it and
            an unauthenticated navigation still gets a bare 403.
        rotate:
          type: boolean
          description: >-
            Force a fresh ingress token (and a fresh bearer token when auth is
            "bearer") even if the port is already exposed exactly as
            requested. Expose is otherwise idempotent: re-exposing an
            unchanged port returns the token it already handed out, because a
            preview URL is a credential a third party may be holding and a
            retry must not silently invalidate it. This flag is how a caller
            asks for a rotation deliberately -- every URL carrying the old
            token stops working the moment it returns. Default false.
    SessionExposeResponse:
      type: object
      properties:
        session_id:
          type: string
        port:
          type: integer
        preview_url:
          type: string
          format: uri
        ingress_token:
          type: string
        guest_ip:
          type: string
        auth:
          type: string
          description: Active inbound access mode.
          enum: [none, bearer]
        auth_token:
          type: string
          description: >-
            Bearer token an inbound request must present. Returned only here,
            only when auth is "bearer". An idempotent re-expose returns the
            same one rather than minting another.
        result:
          type: string
          description: >-
            What this call actually did, so a caller converging desired state
            can tell a no-op from a change without diffing tokens.
            "created": the port was not exposed; a route and its credentials
            were minted. "unchanged": the port was already exposed exactly as
            requested -- nothing was minted, preview_url is the URL an earlier
            expose already returned, and a paused session was not woken to
            answer. "updated": a route existed and this call rewrote it,
            because the requested settings differed or rotate was set. On an
            "updated" call the ingress token is preserved wherever the new
            settings leave room for it, so changing (say) login_url does not
            invalidate a URL in use; visibility "public" has no token to keep,
            and auth "none" has no bearer token to keep. The one exception is
            turning bearer auth OFF: dropping the gate while keeping the
            ingress token would let every holder of the already-issued
            preview URL straight through unauthenticated, so that change
            mints a fresh ingress token and the old URL stops working --
            except on a "public" route, which has no token to rotate, so
            dropping bearer there simply leaves the same hostname open.
            Deliberately typed
            as a plain string rather than an enum, for the same reason
            SessionResponse.state is: an enum here generates package-level
            constants named Created/Unchanged/Updated in every generated
            client, which are far too generic to own at that scope and
            collide with the next enum that has a "created" member.
    OrgResponse:
      type: object
      properties:
        capacity:
          $ref: "#/components/schemas/CapacityCounts"
        name:
          type: string
          description: The org's display name.
    UsageSummaryResponse:
      type: object
      properties:
        period_start:
          type: string
          format: date-time
          description: Same value as `since`, kept for backward compatibility.
        since:
          type: string
          format: date-time
        until:
          type: string
          format: date-time
        executions:
          type: integer
          description: Number of executions in the window.
        vm_seconds:
          type: number
          format: double
          description: Billed run time in vCPU-weighted seconds (excludes boot/restore and snapshot/upload).
        vm_ms:
          type: integer
          format: int64
          description: vm_seconds in milliseconds.
        paused_storage_cents:
          type: integer
          description: Paused-session storage charged in the window, in whole euro-cents.
        volume_storage_cents:
          type: integer
          description: Persistent-volume storage charged in the window, in whole euro-cents.
        cost_to_date_cents:
          type: integer
          description: Total spend in the window (compute + paused-storage + volume-storage), in whole euro-cents.
        pending_millicents:
          type: integer
          format: int64
          description: >-
            Metered usage not yet flushed into a whole-cent debit (the
            compute/paused-storage/volume-storage accrual buffers,
            combined). Not window-scoped and not part of cost_to_date_cents.
        compute_pending_millicents:
          type: integer
          format: int64
        paused_pending_millicents:
          type: integer
          format: int64
        volume_pending_millicents:
          type: integer
          format: int64
        balance_cents:
          type: integer
          description: Current prepaid credit balance, in whole euro-cents. Not window-scoped.
        labels:
          type: array
          description: Present only when `group_by=label:<key>` was passed. Sums to cost_to_date_cents.
          items:
            type: object
            properties:
              label:
                type: string
                description: >-
                  The session label's value for the requested key, or "" for
                  spend that can't be attributed to a labeled session
                  (volume storage, an unlabeled session, or a purged one).
              cost_cents:
                type: integer
    SessionUsageResponse:
      type: object
      properties:
        session_id:
          type: string
        executions:
          type: integer
        vm_seconds:
          type: number
          format: double
        vm_ms:
          type: integer
          format: int64
        paused_storage_cents:
          type: integer
        volume_storage_cents:
          type: integer
          description: Storage cost of the session's provisioned workspace disk, in whole euro-cents.
        cost_to_date_cents:
          type: integer
          description: This session's lifetime spend to date, in whole euro-cents.
    CapacityCounts:
      type: object
      properties:
        running:
          type: integer
          description: Live (or starting) sessions.
        warm:
          type: integer
          description: Paused, resumable sessions.
        limits:
          $ref: "#/components/schemas/CapacityLimits"
    CapacityLimits:
      type: object
      properties:
        running:
          type: integer
        warm:
          type: integer
    SessionUnexposeRequest:
      type: object
      properties:
        port:
          type: integer
    TemplateCreateRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          description: Name for the new template (the caller references it on session create).
        description:
          type: string
    TemplateImportRequest:
      type: object
      required: [from_image, name]
      properties:
        from_image:
          type: string
          description: >-
            Public OCI image ref to build the template from
            (e.g. docker.io/library/alpine:3.20, python:3.12-slim, or
            ghcr.io/org/app@sha256:...). Public registries only.
        name:
          type: string
          description: Name for the new template (the caller references it on session create).
        description:
          type: string
    TemplateResponse:
      type: object
      description: >-
        A base environment a session can be created from. Kind is "official"
        (curated by inis.run) or "user" (published by the caller).
      required: [name, kind, created_at]
      properties:
        name:
          type: string
        kind:
          type: string
          enum: [official, user]
        description:
          type: string
        size:
          type: string
          enum: [small, medium, large, xlarge]
        version:
          type: string
          description: >-
            The promoted current version a bare-name reference resolves to
            (e.g. 2026.07.13). A (name, version) pair is immutable once
            published — a pin to name@version never moves; the bare name is
            repointed at a new version on each promoted rebuild.
        versions:
          type: array
          description: Available, non-deprecated versions for optional pinning (name@version).
          items:
            type: string
        built_at:
          type: string
          description: >-
            When the current version's bundle was built (RFC3339 UTC). A stale
            built-date is a visible signal the patch cadence has slipped.
            Official templates only.
        base_image_ref:
          type: string
          description: >-
            Upstream base image the bundle was built from
            (e.g. docker.io/library/python:3.12-slim). Official templates only.
        base_image_digest:
          type: string
          description: >-
            Content digest of the base image (sha256:...), pinning exactly what
            was built. Rendered short in the UI. Official templates only.
        content_hash:
          type: string
          description: >-
            Content hash of the built bundle — the tamper-evident identifier
            recorded at publish and re-checked on restore. Official templates only.
        cadence:
          type: string
          enum: [weekly, monthly]
          description: >-
            Rebuild cadence for this template, surfaced as "rebuilt weekly/
            monthly". Official templates only.
        category:
          type: string
          enum: [language, use-case]
          description: >-
            Grouping for the official set: "language" tracks an upstream base
            image (a language/runtime line), "use-case" is a composed flavor
            built from a Dockerfile. Used to group the official list. Official
            templates only.
        status:
          type: string
          enum: [queued, building, ready, failed]
          description: >-
            Build status. "ready" for every official and promote-a-session
            template; a template imported from an OCI ref
            (POST /v1/templates) is "queued" behind the node's serial build
            queue, then "building" while its image is flattened, then "ready"
            or "failed". A session cannot be created from a non-ready template.
        queue_position:
          type: integer
          description: >-
            1-based place in the node's serial build queue (1 == next to
            build). Present only while status is "queued".
        source_image:
          type: string
          description: >-
            The OCI image ref this template was imported from
            (POST /v1/templates). Omitted for non-imported templates.
        build_error:
          type: string
          description: Reason the import build failed (present when status is "failed").
        created_at:
          type: string
        alias_of:
          type: string
          description: >-
            Set only on an alias entry (#959): a documented bare name that
            isn't itself a published template family but currently resolves
            to one, e.g. name "python" with alias_of "python-3.13" and
            version "2026.07.10" reads as "python -> python-3.13@2026.07.10".
            Omitted for a real (non-alias) template.
    TemplateListResponse:
      type: object
      properties:
        templates:
          type: array
          items:
            $ref: "#/components/schemas/TemplateResponse"
    RegistryCredentialCreateRequest:
      type: object
      required: [name, registry_host, username, secret]
      properties:
        name:
          type: string
          description: Identifies this credential for later deletion/listing.
        registry_host:
          type: string
          description: >-
            Bare registry host to match against an import's from_image
            (e.g. docker.io, ghcr.io, us-docker.pkg.dev).
        username:
          type: string
          description: >-
            Registry username. Token-based registries often use a fixed
            sentinel: GHCR accepts any non-empty username with a PAT as the
            secret; Google Artifact Registry accepts "oauth2accesstoken"
            (secret = OAuth access token) or "_json_key" (secret = the full
            service account JSON key).
        secret:
          type: string
          description: >-
            Password / personal access token / OAuth access token / service
            account JSON key. Encrypted at rest; never returned by any read
            endpoint.
    RegistryCredentialResponse:
      type: object
      description: >-
        The redacted, read-back-safe view of a stored registry credential —
        the secret is NEVER included, on create, list, or any other response.
      required: [id, name, registry_host, username, created_at]
      properties:
        id:
          type: string
        name:
          type: string
        registry_host:
          type: string
        username:
          type: string
        secret_last4:
          type: string
          description: Last 4 characters of the stored secret — a display aid, never enough to reconstruct it.
        created_at:
          type: string
        updated_at:
          type: string
    RegistryCredentialListResponse:
      type: object
      properties:
        credentials:
          type: array
          items:
            $ref: "#/components/schemas/RegistryCredentialResponse"

    VolumeCreateRequest:
      type: object
      properties:
        size_gb:
          type: integer
          description: >-
            Capacity in GiB. Omitted applies the server default (1 GiB);
            capped at a fixed maximum (128 GiB) — a request over the cap is
            rejected (400), never silently clamped down to it. Grow it
            later with PATCH /v1/volumes/{id} (grow-only).
    VolumeResizeRequest:
      type: object
      required: [size_gb]
      properties:
        size_gb:
          type: integer
          description: >-
            The volume's new capacity in GiB. Must be strictly greater than
            the volume's current size_gb (grow-only — a request at or below
            the current size is rejected, 400) and no greater than the
            per-volume maximum (128 GiB).
    VolumeResponse:
      type: object
      description: >-
        Deliberately node-agnostic — no node id or placement hint anywhere in
        this shape, so where a volume's data physically lives can change
        without that ever becoming a breaking response change.
      required: [id, size_gb, created_at, attached]
      properties:
        id:
          type: string
          description: >-
            Server-issued ("vol_" + random hex) — never a caller-chosen name.
        size_gb:
          type: integer
        created_at:
          type: string
        attached:
          type: boolean
          description: Whether a live or paused session currently has this volume mounted.
        session_id:
          type: string
          description: Set only when attached is true.
        last_saved_at:
          type: string
          description: >-
            When this volume's durable copy last finished being written to
            object storage. Pushed on session destroy, not pause — a paused
            session still holds the volume attached, so this reflects the
            last detach, not the last write. Absent (not zeroed) for a
            volume that has never completed a push — a volume that exists
            but has no durable copy yet is distinct from one saved long ago,
            and this field never conflates the two.
    VolumeListResponse:
      type: object
      properties:
        volumes:
          type: array
          items:
            $ref: "#/components/schemas/VolumeResponse"

    ConnectorCreateRequest:
      type: object
      required: [name, target_base_url, auth_shape, secret]
      properties:
        name:
          type: string
          description: Identifies this connector for session opt-in and later deletion/listing.
        target_base_url:
          type: string
          description: >-
            The real destination the proxy forwards to, e.g.
            https://api.stripe.com. Absolute http(s) URL with no path,
            query, or fragment.
        auth_shape:
          type: string
          enum: [bearer, header]
          description: >-
            "bearer" sets `Authorization: Bearer <secret>`; "header" sets a
            custom header named header_name to the raw secret value.
        header_name:
          type: string
          description: Header name to inject under. Required only when auth_shape is "header".
        secret:
          type: string
          description: >-
            Credential value injected into every proxied request. Encrypted
            at rest; never returned by any read endpoint.
    ConnectorResponse:
      type: object
      description: >-
        The redacted, read-back-safe view of a stored connector — the secret
        is NEVER included, on create, list, or any other response.
      required: [id, name, target_base_url, auth_shape, created_at]
      properties:
        id:
          type: string
        name:
          type: string
        target_base_url:
          type: string
        auth_shape:
          type: string
        header_name:
          type: string
        secret_last4:
          type: string
          description: Last 4 characters of the stored secret — a display aid, never enough to reconstruct it.
        created_at:
          type: string
        updated_at:
          type: string
    ConnectorListResponse:
      type: object
      properties:
        connectors:
          type: array
          items:
            $ref: "#/components/schemas/ConnectorResponse"

    WebhookCreateRequest:
      type: object
      required: [url]
      properties:
        url:
          type: string
          description: Absolute http(s) URL to POST signed event deliveries to.
        event_types:
          type: array
          items:
            type: string
          description: Allowlist of event types to receive; empty/omitted means every type.
    WebhookResponse:
      type: object
      required: [id, url, event_types, enabled, created_at]
      properties:
        id:
          type: string
        url:
          type: string
        secret:
          type: string
          description: The HMAC-SHA256 signing secret. Only ever populated on create.
        secret_last4:
          type: string
        event_types:
          type: array
          items:
            type: string
        enabled:
          type: boolean
        created_at:
          type: string
        disabled_at:
          type: string
    WebhookListResponse:
      type: object
      properties:
        endpoints:
          type: array
          items:
            $ref: "#/components/schemas/WebhookResponse"
    WebhookDeliveryAttempt:
      type: object
      properties:
        attempt:
          type: integer
        at:
          type: string
        status_code:
          type: integer
        error:
          type: string
    WebhookDelivery:
      type: object
      required: [id, event_type, status, attempt_count, max_attempts, created_at]
      properties:
        id:
          type: integer
        event_type:
          type: string
        session_id:
          type: string
        status:
          type: string
          description: pending | delivered | dead
        attempt_count:
          type: integer
        max_attempts:
          type: integer
        next_attempt_at:
          type: string
        attempts:
          type: array
          items:
            $ref: "#/components/schemas/WebhookDeliveryAttempt"
        created_at:
          type: string
        delivered_at:
          type: string
    WebhookDeliveryListResponse:
      type: object
      properties:
        deliveries:
          type: array
          items:
            $ref: "#/components/schemas/WebhookDelivery"
        next_since_id:
          type: integer
          description: >-
            Only present when the request used ?since_id. Pass this value as
            the next call's since_id to continue the catch-up scan — present
            even on an empty page, so polling with the returned cursor is
            always safe.
    OTLPExportSetRequest:
      type: object
      required: [endpoint]
      properties:
        endpoint:
          type: string
          description: Absolute http(s) OTLP/HTTP logs ingest URL (e.g. https://collector.example.com:4318/v1/logs).
        headers:
          type: object
          additionalProperties:
            type: string
          description: Extra headers to send with every export request (e.g. an auth header the collector requires).
    OTLPExportResponse:
      type: object
      properties:
        endpoint:
          type: string
        headers:
          type: object
          additionalProperties:
            type: string
        enabled:
          type: boolean
        updated_at:
          type: string
    DomainCreateRequest:
      type: object
      required: [domain]
      properties:
        domain:
          type: string
          description: The hostname to register (e.g. app-preview.customer.com).
    DomainRouteRequest:
      type: object
      required: [session_id, port]
      properties:
        session_id:
          type: string
        port:
          type: integer
          description: Exposed session port (1024-65535) to route this domain to.
    DomainResponse:
      type: object
      required: [id, domain, verified, created_at, verify_txt_name, verify_txt_value]
      properties:
        id:
          type: string
        domain:
          type: string
        verified:
          type: boolean
        verified_at:
          type: string
        session_id:
          type: string
          description: Currently-bound session id, if any.
        port:
          type: integer
          description: Currently-bound port, if any.
        created_at:
          type: string
        verify_txt_name:
          type: string
          description: The TXT record name to create ("_inis-verify.<domain>").
        verify_txt_value:
          type: string
          description: The TXT record value (the verification token).
        ingress_base_domain:
          type: string
          description: >-
            The hostname to CNAME this domain to so requests reach the fleet.
            TXT verification only proves ownership — traffic still needs this
            record. Omitted when the deployment has no ingress domain set.
    DomainListResponse:
      type: object
      properties:
        domains:
          type: array
          items:
            $ref: "#/components/schemas/DomainResponse"
    EgressCap:
      type: object
      required: [cap_bytes, override, default_cap_bytes, today_bytes, date]
      properties:
        cap_bytes:
          type: integer
          format: int64
          description: Effective daily cap in bytes; the override when set, else the fleet default. 0 means no cap.
        override:
          type: boolean
          description: True when an operator set a per-account override.
        default_cap_bytes:
          type: integer
          format: int64
        today_bytes:
          type: integer
          format: int64
          description: The org's egress so far this UTC day.
        date:
          type: string
          description: The UTC day (YYYY-MM-DD) today_bytes counts.
    SessionPatchRequest:
      type: object
      required: [egress_cap_bytes]
      properties:
        egress_cap_bytes:
          type: integer
          format: int64
          description: Per-session daily egress cap in bytes; 0 clears it.
    SessionEgressCap:
      type: object
      required: [session_id, egress_cap_bytes, today_bytes]
      properties:
        session_id:
          type: string
        egress_cap_bytes:
          type: integer
          format: int64
          description: 0 means no per-session cap.
        today_bytes:
          type: integer
          format: int64
        date:
          type: string
          description: The UTC day (YYYY-MM-DD) today_bytes counts; absent when the session has no egress today.
    RequestEndpointCreateRequest:
      type: object
      required: [url]
      properties:
        url:
          type: string
          description: Absolute HTTPS URL to receive guest requests.
    RequestEndpoint:
      type: object
      required: [id, url, enabled, created_at]
      properties:
        id:
          type: string
        url:
          type: string
        secret:
          type: string
          description: The HMAC-SHA256 signing secret. Populated ONLY on create and rotate.
        secret_last4:
          type: string
        enabled:
          type: boolean
        created_at:
          type: string
          format: date-time
        rotated_at:
          type: string
          format: date-time
