openapi: 3.1.0
info:
  title: Archil Control Plane API
  description: |
    The Archil Control Plane API provides programmatic access to manage disks,
    persistent sandboxes, mounts, and API keys in the Archil distributed
    filesystem platform.

    API keys authenticate requests to this control plane and are scoped to
    your account. They are distinct from *disk tokens*, which are per-disk
    credentials used by clients when mounting a disk.

    ## Authentication

    All endpoints require an API key, sent as a bearer token:

    ```
    Authorization: Bearer key-...
    ```

    Use the full key, including its `key-` prefix. The older form without the `Bearer` scheme is still accepted.

    Create API keys in the [Archil Console](https://console.archil.com) or via the API.

    ## Response Format

    All responses use a consistent envelope:

    ```json
    {
      "success": true,
      "data": { ... }
    }
    ```

    Or on error:

    ```json
    {
      "success": false,
      "error": "Error message"
    }
    ```
  version: 1.0.0
  contact:
    email: support@archil.com
    url: https://archil.com

servers:
  - url: https://control.green.us-east-1.aws.prod.archil.com
    description: AWS US East (N. Virginia) — aws-us-east-1
  - url: https://control.green.eu-west-1.aws.prod.archil.com
    description: AWS EU West (Ireland) — aws-eu-west-1
  - url: https://control.green.us-west-2.aws.prod.archil.com
    description: AWS US West (Oregon) — aws-us-west-2
  - url: https://control.blue.us-central1.gcp.prod.archil.com
    description: GCP US Central (Iowa) — gcp-us-central1

security:
  - ApiKeyAuth: []

tags:
  - name: Disks
    description: Create, read, update, and delete disks
  - name: Serverless Execution
    description: Run commands on a disk without provisioning compute
  - name: Sandboxes
    description: Manage persistent sandbox virtual machines
  - name: Disk Users
    description: Manage authorized users on disks
  - name: API Tokens
    description: Manage API keys (also called API tokens) used to authenticate Control Plane API requests. Distinct from disk tokens.

paths:
  /api/disks:
    get:
      operationId: listDisks
      summary: List all disks
      description: Returns all disks owned by the authenticated account.
      tags:
        - Disks
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: name
          in: query
          description: Filter disks by exact name match.
          required: false
          schema:
            type: string
      responses:
        '200':
          description: List of disks
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_DiskList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'

    post:
      operationId: createDisk
      summary: Create a new disk
      description: |
        Creates a new disk with the specified configuration. A default disk
        token user is automatically generated and returned in the response,
        so the disk is immediately mountable. The one-time disk token appears
        in `authorizedUsers[].token` and cannot be retrieved again. (This
        disk token is scoped to the new disk and is separate from the API
        key you used to make this request.)

        To provide your own users instead, pass the deprecated `authMethods`
        field or call AddDiskUser after creation.
      tags:
        - Disks
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateDiskRequest'
      responses:
        '200':
          description: Disk with this name already exists and configuration matches (idempotent)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_CreateDisk'
        '201':
          description: Disk created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_CreateDisk'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Access denied (e.g., preview region not enabled)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Disk with this name already exists but configuration differs
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/disks/{id}:
    get:
      operationId: getDisk
      summary: Get disk details
      description: Returns detailed information about a specific disk.
      tags:
        - Disks
      parameters:
        - $ref: '#/components/parameters/DiskId'
      responses:
        '200':
          description: Disk details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Disk'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'

    delete:
      operationId: deleteDisk
      summary: Delete a disk
      description: Deletes a disk and all associated resources.
      tags:
        - Disks
      parameters:
        - $ref: '#/components/parameters/DiskId'
      responses:
        '200':
          description: Disk deleted successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Empty'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/disks/{id}/share:
    post:
      operationId: createShareUrl
      summary: Create a share URL for a file
      description: |
        Creates a signed, time-limited URL for downloading a file without
        authentication. The URL expires after 24 hours by default; pass
        `expiresIn` to set a custom lifetime up to 7 days.

        The returned URL points to a public endpoint (`GET /api/shared/{token}`)
        that verifies the signature, checks expiry, and streams the file with a
        sensible `Content-Type` and `Content-Disposition`. Range requests are
        supported. No credentials are needed to open the URL.
      tags:
        - Disks
      parameters:
        - $ref: '#/components/parameters/DiskId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateShareUrlRequest'
      responses:
        '200':
          description: Share URL created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_ShareUrl'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/disks/{id}/users:
    post:
      operationId: addDiskUser
      summary: Add user to disk
      description: |
        Adds an authorized user to a disk. Users can authenticate via:
        - **token**: A shared token with a nickname and 4-character suffix
        - **awssts**: AWS STS role assumption with an IAM principal ARN
      tags:
        - Disk Users
      parameters:
        - $ref: '#/components/parameters/DiskId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DiskUser'
      responses:
        '201':
          description: User added successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_AuthorizedUser'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/disks/{id}/users/{userType}:
    delete:
      operationId: removeDiskUser
      summary: Remove user from disk
      description: Removes an authorized user from a disk.
      tags:
        - Disk Users
      parameters:
        - $ref: '#/components/parameters/DiskId'
        - name: userType
          in: path
          required: true
          description: The type of user authentication
          schema:
            type: string
            enum:
              - token
              - awssts
        - name: identifier
          in: query
          required: false
          description: >
            Identifier of the user to remove, as returned in the creation
            or list response. For awssts users, this is the IAM ARN.
          schema:
            type: string
        - name: principal
          in: query
          required: false
          deprecated: true
          description: Use identifier instead.
          schema:
            type: string
      responses:
        '200':
          description: User removed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Message'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/disks/{id}/exec:
    post:
      operationId: execDisk
      summary: Execute a command on a disk
      description: |
        Launches a container with the specified disk mounted, runs the given
        command to completion, shuts down the container, and returns the command
        result.
      tags:
        - Serverless Execution
      parameters:
        - $ref: '#/components/parameters/DiskId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExecDiskRequest'
      responses:
        '200':
          description: Command completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_ExecDisk'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
        '504':
          description: Command timed out
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/disks/{id}/grep:
    post:
      operationId: grepDisk
      summary: Parallel grep over a directory on a disk
      description: |
        Searches files under a directory on the disk for lines matching a
        regular expression. Listing and matching are fanned out across
        ephemeral exec containers, so the request finishes within the user's
        time budget regardless of the size of the directory.

        The user controls cost and latency with three knobs:

        - `maxDurationSeconds` is the wall-clock deadline (capped at 30s).
        - `concurrency` is the maximum number of parallel grep workers.
          Higher concurrency finishes larger datasets within the deadline,
          at proportionally more compute.
        - `maxResults` causes the search to short-circuit after the
          aggregator has collected this many matches.

        With `recursive: false` only files directly under `directory` are
        searched. With `recursive: true` subdirectories are walked
        breadth-first and grep workers are dispatched as soon as each level
        finishes listing — listing and matching overlap.

        The matches returned when stopping early on `maxResults` are a sample
        of whichever workers reported first, not the lexicographically first
        N. The response surfaces `stoppedReason` so callers can distinguish
        completion from early termination.
      tags:
        - Serverless Execution
      parameters:
        - $ref: '#/components/parameters/DiskId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GrepDiskRequest'
      responses:
        '200':
          description: Grep completed (possibly stopped early)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_GrepDisk'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/exec:
    post:
      operationId: exec
      summary: Execute a command with multiple disks mounted
      description: |
        Launches a container with the supplied set of disks each mounted at its
        own relative path under `/mnt/archil`, runs the command to completion,
        and shuts down the container. Activation is atomic: every disk mounts
        or none of them do.

        Relative paths must be non-empty, non-absolute, and contain no `.` /
        `..` segments. Mounting two disks at the same relative path is an
        error.
      tags:
        - Serverless Execution
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExecRequest'
      responses:
        '200':
          description: Command completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Exec'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
        '504':
          description: Command timed out
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/images:
    post:
      operationId: createImage
      summary: Build a sandbox image
      description: |
        Builds an OCI image for use by sandboxes. Poll [Get Image](/api-reference/images/get-image)
        until it is `ready`, then pass its `image_id` when creating a sandbox.

        Repeated requests join an active build. Completed tags rebuild; completed digest references
        are reused. During a rebuild, sandboxes can use the last successful digest. Submit the request
        again to retry a failed or unresponsive build.
      tags:
        - Sandboxes
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateImageRequest'
            example:
              source: ghcr.io/acme/agent-runtime:v2
              registry_auth:
                username: YOUR_REGISTRY_USERNAME
                password: YOUR_REGISTRY_TOKEN
      responses:
        '202':
          description: The image build is running or finished
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Image'
              example:
                success: true
                data:
                  image_id: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
                  source: ghcr.io/acme/agent-runtime:v2
                  private: true
                  status: building
                  created_at: '2026-10-06T12:00:00Z'
                  updated_at: '2026-10-06T12:00:00Z'
        '400':
          description: Invalid request body, image reference, or registry credentials format
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                invalidBody:
                  summary: Invalid JSON request body
                  value:
                    success: false
                    error: Invalid request body
                    code: bad_request
                incompleteRegistryAuth:
                  summary: Missing registry username or password
                  value:
                    success: false
                    error: registry_auth requires a username and password
                    code: bad_request
        '401':
          $ref: '#/components/responses/PlainTextUnauthorized'
        '500':
          $ref: '#/components/responses/SandboxInternalError'
        '503':
          description: No build capacity is available or sandbox support is disabled in this region
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                noCapacity:
                  summary: No build capacity available
                  value:
                    success: false
                    error: No sandbox capacity is available; retry
                    code: no_capacity
                notEnabled:
                  summary: Sandbox support is disabled
                  value:
                    success: false
                    error: Sandboxes are not enabled on this controlplane
                    code: not_enabled
        '504':
          $ref: '#/components/responses/SandboxTimeout'

  /api/images/{image_id}:
    get:
      operationId: getImage
      summary: Get a sandbox image
      description: >-
        Returns the image's build status and latest successful digest, if available.
        Failed builds include a `failure_reason`. Use
        [Build Image](/api-reference/images/build-image) (`POST /api/images`)
        to retry interrupted builds.
      tags:
        - Sandboxes
      parameters:
        - name: image_id
          in: path
          required: true
          schema:
            type: string
            pattern: '^[0-9a-f]{64}$'
      responses:
        '200':
          description: The sandbox image
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Image'
              example:
                success: true
                data:
                  image_id: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
                  source: ghcr.io/acme/agent-runtime:v2
                  private: true
                  status: ready
                  digest: sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789
                  canonical_source: ghcr.io/acme/agent-runtime@sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789
                  created_at: '2026-10-06T12:00:00Z'
                  updated_at: '2026-10-06T12:01:00Z'
        '401':
          $ref: '#/components/responses/PlainTextUnauthorized'
        '404':
          description: Image not found or private image belongs to another account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error: Sandbox image not found
                code: not_found
        '500':
          $ref: '#/components/responses/SandboxInternalError'
        '503':
          description: Sandbox support is disabled in this region
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error: Sandboxes are not enabled on this controlplane
                code: not_enabled
        '504':
          $ref: '#/components/responses/SandboxTimeout'

  /api/sandboxes:
    get:
      operationId: listSandboxes
      summary: List sandboxes
      description: All the account's sandboxes, oldest first.
      tags:
        - Sandboxes
      parameters:
        - name: filesystem
          in: query
          required: false
          description: Only sandboxes that mount this disk
          schema:
            type: string
            pattern: '^dsk-[0-9a-f]{16}$'
      responses:
        '200':
          description: Sandboxes for the account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_SandboxList'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: createSandbox
      summary: Create a sandbox
      description: |
        Provisions a sandbox VM with the requested shape and an internal,
        persistent Archil disk backing its root filesystem. This backing disk
        cannot be disabled or replaced with an existing disk. Use `mounts`
        to attach additional existing disks at guest paths; omit it or pass
        an empty list to create a sandbox without additional disks.
        By default the response reports `pending` after
        the runtime accepts the start. Set `wait=true` to hold for `running`; if
        the wait budget expires, the response remains `pending` and startup
        continues.
      tags:
        - Sandboxes
      parameters:
        - $ref: '#/components/parameters/Wait'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSandboxRequest'
            example:
              name: agent-workspace
              base_image: python:3.13
              vcpu_count: 2
              mem_size_mib: 4096
              mounts:
              - disk_id: dsk-0123456789abcdef
                path: /mnt/data
      responses:
        '202':
          description: The sandbox was created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Sandbox'
              example:
                success: true
                data:
                  sandbox_id: 019d158e-7100-7000-8000-0123456789ab
                  name: agent-workspace
                  status: pending
                  vcpu_count: 2
                  mem_size_mib: 4096
                  max_ttl_seconds: 86400
                  idle_ttl_seconds: 0
                  max_concurrent_execs: 32
                  enable_service_ingress: false
                  base_image: python:3.13
                  mounts:
                  - disk_id: dsk-0123456789abcdef
                    path: /mnt/data
                  created_at: '2026-10-06T12:00:00Z'
                  last_active_at: '2026-10-06T12:00:00Z'
        '400':
          description: Invalid request body or parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                invalidBody:
                  summary: Malformed JSON body
                  value:
                    success: false
                    error: Invalid request body
                    code: bad_request
                conflictingImages:
                  summary: Both image selectors supplied
                  value:
                    success: false
                    error: base_image and image_id are mutually exclusive
                    code: bad_request
        '401':
          $ref: '#/components/responses/PlainTextUnauthorized'
        '404':
          description: Image or mount disk not found or not accessible to this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                imageNotFound:
                  summary: Image not found
                  value:
                    success: false
                    error: Sandbox image not found
                    code: not_found
                diskNotFound:
                  summary: Mount disk not found or not owned by this account
                  value:
                    success: false
                    error: 'mounts[0]: disk dsk-0123456789abcdef not found'
                    code: not_found
        '409':
          description: Sandbox name already exists, or the selected image has no successful build
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                nameConflict:
                  summary: Name already in use
                  value:
                    success: false
                    error: A sandbox with this name already exists
                    code: sandbox_name_conflict
                imageNotReady:
                  summary: No successful image build
                  value:
                    success: false
                    error: Sandbox image has no finished build; poll it until it is ready
                    code: image_not_ready
        '500':
          $ref: '#/components/responses/SandboxInternalError'
        '503':
          $ref: '#/components/responses/SandboxUnavailable'
        '504':
          $ref: '#/components/responses/SandboxTimeout'

  /api/sandboxes/{sid}:
    get:
      operationId: getSandbox
      summary: Get a sandbox
      tags:
        - Sandboxes
      parameters:
        - $ref: '#/components/parameters/SandboxId'
      responses:
        '200':
          description: The sandbox
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Sandbox'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      operationId: deleteSandbox
      summary: Delete a sandbox
      description: >-
        Permanently deletes a stopped, exited, or failed sandbox and its backing
        disk. A sandbox with child forks cannot be deleted until those children
        are deleted, starting with the deepest ones.
      tags:
        - Sandboxes
      parameters:
        - $ref: '#/components/parameters/SandboxId'
      responses:
        '204':
          description: The sandbox was deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: >-
            The sandbox must be stopped, exited, or failed and have no child
            forks. The error includes the number of child disks and up to 10
            disk IDs that must be deleted first.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/sandboxes/{sid}/connections:
    post:
      operationId: createSandboxConnection
      summary: Create a sandbox process connection
      description: |
        Returns a short-lived WebSocket URL used by the sandbox process API.
        The URL contains a signed token that expires at `expires_at`; an open
        WebSocket can remain connected after the token expires.
      tags:
        - Sandboxes
      parameters:
        - $ref: '#/components/parameters/SandboxId'
      responses:
        '200':
          description: A short-lived direct connection URL
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_SandboxConnection'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: The sandbox is not running
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/sandboxes/{sid}/ports:
    get:
      operationId: listSandboxPorts
      summary: List exposed sandbox ports
      description: Lists the explicitly exposed public ports for the sandbox.
      tags:
        - Sandboxes
      parameters:
        - $ref: '#/components/parameters/SandboxId'
      responses:
        '200':
          description: Exposed sandbox ports
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_SandboxPortList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/sandboxes/{sid}/ports/{port}:
    put:
      operationId: exposeSandboxPort
      summary: Expose a sandbox port
      description: Creates or returns public access to a TCP port in the sandbox.
      tags:
        - Sandboxes
      parameters:
        - $ref: '#/components/parameters/SandboxId'
        - name: port
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
            maximum: 65535
      responses:
        '200':
          description: Existing exposed port
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_SandboxPort'
        '201':
          description: Port was exposed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_SandboxPort'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'

    delete:
      operationId: deleteSandboxPort
      summary: Stop exposing a sandbox port
      description: >-
        Removes an explicitly exposed public port. An enabled sandbox service
        publishing the same TCP port remains publicly reachable.
      tags:
        - Sandboxes
      parameters:
        - $ref: '#/components/parameters/SandboxId'
        - name: port
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
            maximum: 65535
      responses:
        '204':
          description: Sandbox port is no longer explicitly exposed
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/sandboxes/{sid}/port-tokens:
    get:
      operationId: listSandboxPortTokens
      summary: List sandbox port tokens
      description: Lists unexpired private port token metadata without returning raw tokens.
      tags:
        - Sandboxes
      parameters:
        - $ref: '#/components/parameters/SandboxId'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Sandbox port tokens
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_SandboxPortTokenList'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: createSandboxPortToken
      summary: Create a private sandbox port token
      description: |
        Creates token-authorized HTTP access to one port. Send the raw token returned
        by this request in the `X-Archil-Token` request header at the stable
        `<port>-<sandbox-route-id>.<zone>` hostname. Public ports accept connections
        without a token; otherwise a valid token for that sandbox and port is required.
        The token is not returned again.
      tags:
        - Sandboxes
      parameters:
        - $ref: '#/components/parameters/SandboxId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSandboxPortTokenRequest'
      responses:
        '201':
          description: Port token was created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_CreatedSandboxPortToken'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/sandboxes/{sid}/port-tokens/{token_id}:
    get:
      operationId: getSandboxPortToken
      summary: Get a sandbox port token
      description: Returns token metadata without its raw token. Expired tokens return not found.
      tags:
        - Sandboxes
      parameters:
        - $ref: '#/components/parameters/SandboxId'
        - name: token_id
          in: path
          required: true
          schema:
            type: string
            pattern: '^[0-9a-f]{64}$'
      responses:
        '200':
          description: Sandbox port token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_SandboxPortToken'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      operationId: deleteSandboxPortToken
      summary: Revoke a sandbox port token
      description: |
        New private connections using the token fail after this request
        completes; established connections remain open. Expired tokens return not found.
      tags:
        - Sandboxes
      parameters:
        - $ref: '#/components/parameters/SandboxId'
        - name: token_id
          in: path
          required: true
          schema:
            type: string
            pattern: '^[0-9a-f]{64}$'
      responses:
        '204':
          description: Port token was revoked
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/sandboxes/{sid}/pause:
    post:
      operationId: pauseSandbox
      summary: Pause a sandbox
      description: |
        Snapshot the CPU & memory state, then stop the VM. The sandbox reports
        `pausing` until the snapshot commit marker is durable and the VM exits,
        then reports `paused`. Attached process connections close, while
        agent-owned processes remain available for reattachment after resume.
        A sandbox that is still `pending` must reach `running` before it can be paused.
        To restore the snapshot, call `/resume`.
      tags:
        - Sandboxes
      parameters:
        - $ref: '#/components/parameters/SandboxId'
      responses:
        '200':
          description: The sandbox was already inactive
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Sandbox'
        '202':
          description: The sandbox pause is in progress
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Sandbox'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: The sandbox is still starting, or a plain stop is already in progress
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/RetryableUnavailable'

  /api/sandboxes/{sid}/fork:
    post:
      operationId: forkSandbox
      summary: Fork a sandbox
      description: |
        Checkpoints a running source sandbox and creates an isolated writable
        disk branch. Forks from running or paused sources resume from the
        source's CPU, memory, and disk state. Forks from stopped sources
        cold-boot from the saved disk checkpoint. A running source is
        resumed after its checkpoint; paused and stopped sources remain
        inactive. Forked sandboxes may themselves be forked.

        Passing `checkpoint` forks that saved state without touching the
        source, so clients can pause once, wait for the checkpoint, and fork
        it from any number of places. Without `checkpoint` the server pauses a
        running source inside the request and resumes it afterwards; that path
        is kept for older clients and is bounded by the request timeout.
        Sandboxes with additional disk mounts must be stopped before forking.
        The fork inherits mounts of the same external disks; only the internal
        root disk is branched, so external disk contents are not isolated.
      tags:
        - Sandboxes
      parameters:
        - $ref: '#/components/parameters/SandboxId'
        - $ref: '#/components/parameters/Wait'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ForkSandboxRequest'
            example:
              name: agent-fork
      responses:
        '202':
          description: The fork was created; status is pending or running when wait=true completes startup
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Sandbox'
              example:
                success: true
                data:
                  sandbox_id: 019d158f-5b60-7000-8000-0123456789ab
                  name: agent-fork
                  status: pending
                  vcpu_count: 2
                  mem_size_mib: 4096
                  max_ttl_seconds: 86400
                  idle_ttl_seconds: 0
                  max_concurrent_execs: 32
                  enable_service_ingress: false
                  base_image: python:3.13
                  mounts:
                  - disk_id: dsk-0123456789abcdef
                    path: /mnt/data
                  created_at: '2026-10-06T12:00:00Z'
                  last_active_at: '2026-10-06T12:00:00Z'
        '400':
          $ref: '#/components/responses/SandboxValidationError'
        '401':
          $ref: '#/components/responses/PlainTextUnauthorized'
        '404':
          $ref: '#/components/responses/SandboxNotFound'
        '409':
          description: Source state or mounted disks prevent the fork, the checkpoint is unavailable, or the name is already
            in use
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                mountedSource:
                  summary: Stop the source before forking mounted disks
                  value:
                    success: false
                    error: 'sandbox has mounted disks: disk dsk-0123456789abcdef is mounted at /mnt/data; stop the sandbox
                      before forking it'
                    code: sandbox_fork_mounted
                checkpointUnavailable:
                  summary: Checkpoint unavailable
                  value:
                    success: false
                    error: The sandbox fork checkpoint is unavailable
                    code: sandbox_fork_checkpoint
                notForkable:
                  summary: Source cannot be forked
                  value:
                    success: false
                    error: Only a running, paused, or stopped sandbox can be forked
                    code: sandbox_not_forkable
                nameConflict:
                  summary: Name already in use
                  value:
                    success: false
                    error: A sandbox with this name already exists
                    code: sandbox_name_conflict
        '500':
          $ref: '#/components/responses/SandboxInternalError'
        '503':
          $ref: '#/components/responses/SandboxUnavailable'
        '504':
          $ref: '#/components/responses/SandboxTimeout'

  /api/sandboxes/{sid}/resume:
    post:
      operationId: resumeSandbox
      summary: Resume a sandbox
      description: |
        Restores a paused sandbox's committed CPU and memory snapshot.
        Resuming a running sandbox or a pending resume is idempotent. Only a
        `paused` sandbox can begin a new resume; `stopped`, `exited`, `failed`,
        `stopping`, `pausing`, or a pending cold start returns 409.
      tags:
        - Sandboxes
      parameters:
        - $ref: '#/components/parameters/SandboxId'
        - $ref: '#/components/parameters/Wait'
      responses:
        '200':
          description: The sandbox was already running
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Sandbox'
        '202':
          description: The sandbox resume is pending
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Sandbox'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: The sandbox is not paused, is shutting down, or a cold start is already in progress
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/RetryableUnavailable'

  /api/sandboxes/{sid}/network:
    get:
      operationId: getSandboxNetwork
      summary: Get a sandbox's network policy
      description: Returns the last committed network policy for a running, paused, or stopped sandbox.
      tags:
        - Sandboxes
      parameters:
        - $ref: '#/components/parameters/SandboxId'
      responses:
        '200':
          description: The last committed network policy
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_SandboxNetwork'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/RetryableUnavailable'
    put:
      operationId: updateSandboxNetwork
      summary: Replace a running sandbox's network policy
      description: |
        Applies the complete replacement policy to a running sandbox, then
        records it as the current policy. A rejected update leaves the previous
        policy unchanged. Free-plan accounts cannot update network policies and
        receive 403 with code sandbox_network_plan_required. For paid accounts,
        an empty object restores unrestricted egress. Existing network sessions
        are not terminated; the replacement governs new connections and is
        retained across later stops and resumes. Sandboxes
        that are not running, or whose policy changes concurrently, return 409.
      tags:
        - Sandboxes
      parameters:
        - $ref: '#/components/parameters/SandboxId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SandboxNetwork'
      responses:
        '200':
          description: The network policy was replaced
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_SandboxNetwork'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Network policy updates require a paid plan (sandbox_network_plan_required)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: The sandbox is not running or the network policy changed concurrently
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/RetryableUnavailable'

  /api/sandboxes/{sid}/timeout:
    post:
      operationId: setSandboxTimeout
      summary: Set a sandbox's timeouts
      description: |
        Replaces one or both sandbox timeout settings. Updating `timeout` while
        the sandbox is running resets its hard expiration deadline to that many
        seconds from now, capped at 24 hours after the current session started.
        Updating `idle_ttl_seconds` resets the idle
        countdown when no direct process connection is active; zero disables
        idle expiry. Omitted settings remain unchanged. Updates are retained
        across later stops and starts. For an inactive sandbox, they apply to
        the next powered-on session. Sandboxes that are starting or shutting
        down, or whose timeout changes concurrently, return 409.
      tags:
        - Sandboxes
      parameters:
        - $ref: '#/components/parameters/SandboxId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SandboxTimeoutRequest'
      responses:
        '200':
          description: The sandbox timeouts were set
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Sandbox'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: The sandbox is transitioning or its timeout changed concurrently
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/RetryableUnavailable'

  /api/sandboxes/{sid}/start:
    post:
      operationId: startSandbox
      summary: Cold-start a sandbox
      description: |
        Cold-boots from the persisted configuration and disks. If the sandbox
        was paused, its memory snapshot is consumed and discarded before the
        new VM runs; disk state remains. Without mount changes, starting a running
        sandbox or a pending cold start is idempotent. A sandbox that is stopping,
        pausing, or pending a resume returns 409. Providing `mounts` replaces the
        additional disk mounts for this and later sessions and requires an inactive
        sandbox; omit it to preserve saved mounts.
      tags:
        - Sandboxes
      parameters:
        - $ref: '#/components/parameters/SandboxId'
        - $ref: '#/components/parameters/Wait'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StartSandboxRequest'
            example:
              mounts:
              - disk_id: dsk-0123456789abcdef
                path: /mnt/data
      responses:
        '200':
          description: The sandbox is running
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Sandbox'
              example:
                success: true
                data:
                  sandbox_id: 019d158e-7100-7000-8000-0123456789ab
                  name: agent-workspace
                  status: running
                  vcpu_count: 2
                  mem_size_mib: 4096
                  max_ttl_seconds: 86400
                  idle_ttl_seconds: 0
                  max_concurrent_execs: 32
                  enable_service_ingress: false
                  base_image: python:3.13
                  mounts:
                  - disk_id: dsk-0123456789abcdef
                    path: /mnt/data
                  created_at: '2026-10-06T12:00:00Z'
                  last_active_at: '2026-10-06T12:00:05Z'
                  running_at: '2026-10-06T12:00:05Z'
        '202':
          description: The sandbox start is pending
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Sandbox'
              example:
                success: true
                data:
                  sandbox_id: 019d158e-7100-7000-8000-0123456789ab
                  name: agent-workspace
                  status: pending
                  vcpu_count: 2
                  mem_size_mib: 4096
                  max_ttl_seconds: 86400
                  idle_ttl_seconds: 0
                  max_concurrent_execs: 32
                  enable_service_ingress: false
                  base_image: python:3.13
                  mounts:
                  - disk_id: dsk-0123456789abcdef
                    path: /mnt/data
                  created_at: '2026-10-06T12:00:00Z'
                  last_active_at: '2026-10-06T12:00:00Z'
        '400':
          $ref: '#/components/responses/SandboxValidationError'
        '401':
          $ref: '#/components/responses/PlainTextUnauthorized'
        '404':
          description: Sandbox or mount disk not found or not accessible to this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                sandboxNotFound:
                  summary: Sandbox not found
                  value:
                    success: false
                    error: Sandbox not found
                    code: not_found
                diskNotFound:
                  summary: Mount disk not found or not owned by this account
                  value:
                    success: false
                    error: 'mounts[0]: disk dsk-0123456789abcdef not found'
                    code: not_found
        '409':
          description: Sandbox is stopping, pausing, being deleted, or resuming; mount changes also conflict with a running
            or pending session
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                mountsWhileActive:
                  summary: Mount changes require an inactive sandbox
                  value:
                    success: false
                    error: mounts can only change on a cold start of an inactive sandbox
                    code: sandbox_mounts_while_active
                stopping:
                  summary: Sandbox is stopping
                  value:
                    success: false
                    error: Sandbox is still stopping; retry once it is stopped
                    code: sandbox_stopping
                pausing:
                  summary: Sandbox is pausing
                  value:
                    success: false
                    error: Sandbox is still pausing; retry once it is paused
                    code: sandbox_pausing
                startConflict:
                  summary: Resume already in progress
                  value:
                    success: false
                    error: Sandbox is already starting with a different lifecycle intent
                    code: sandbox_start_conflict
                terminated:
                  summary: Sandbox is being deleted
                  value:
                    success: false
                    error: Sandbox is no longer accepting requests
                    code: sandbox_terminated
        '500':
          $ref: '#/components/responses/SandboxInternalError'
        '503':
          $ref: '#/components/responses/SandboxUnavailable'
        '504':
          $ref: '#/components/responses/SandboxTimeout'

  /api/sandboxes/{sid}/stop:
    post:
      operationId: stopSandbox
      summary: Stop a sandbox
      description: |
        Gracefully shuts down the guest, then checkpoints its disk before
        entering `stopped`. The sandbox and any forks from it cold-boot from
        disk state. Stopping a paused sandbox reuses its existing disk
        checkpoint. In-flight processes receive the guest shutdown signal.
      tags:
        - Sandboxes
      parameters:
        - $ref: '#/components/parameters/SandboxId'
      responses:
        '200':
          description: The sandbox was already inactive
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Sandbox'
        '202':
          description: The sandbox stop is in progress
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Sandbox'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: A pause is already in progress
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/RetryableUnavailable'

  /api/tokens:
    get:
      operationId: listApiTokens
      summary: List API tokens
      description: Returns all API tokens for the authenticated account.
      tags:
        - API Tokens
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: List of API tokens
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_TokenList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'

    post:
      operationId: createApiToken
      summary: Create API token
      description: |
        Creates a new API token for programmatic access. The full token value
        is only returned once at creation time.
      tags:
        - API Tokens
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateApiTokenRequest'
      responses:
        '201':
          description: Token created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_TokenCreated'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/tokens/{id}:
    delete:
      operationId: deleteApiToken
      summary: Delete API token
      description: Revokes and deletes an API token.
      tags:
        - API Tokens
      parameters:
        - name: id
          in: path
          required: true
          description: The token ID (hash)
          schema:
            type: string
      responses:
        '200':
          description: Token deleted successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Message'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'

components:
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      description: Archil API key, including its `key-` prefix

  parameters:
    DiskId:
      name: id
      in: path
      required: true
      description: Disk ID (format `dsk-{16 hex chars}`)
      schema:
        type: string
        pattern: '^dsk-[0-9a-f]{16}$'
        example: dsk-0123456789abcdef
    SandboxId:
      name: sid
      in: path
      required: true
      description: Sandbox UUID
      schema:
        type: string
        format: uuid
    Wait:
      name: wait
      in: query
      required: false
      description: Hold the request for a completed sandbox lifecycle transition
      schema:
        type: boolean
        default: false
    Limit:
      name: limit
      in: query
      required: false
      description: Maximum number of items to return
      schema:
        type: integer
        default: 50
        maximum: 100
    Cursor:
      name: cursor
      in: query
      required: false
      description: Pagination cursor from a previous response
      schema:
        type: string

  responses:
    SandboxValidationError:
      description: Invalid request body or parameters
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error: Invalid request body
            code: bad_request
    SandboxNotFound:
      description: Sandbox not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error: Sandbox not found
            code: not_found
    SandboxUnavailable:
      description: Sandbox capacity or runtime temporarily unavailable, or sandboxes are disabled
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            noCapacity:
              summary: No capacity
              value:
                success: false
                error: No sandbox capacity is available; retry
                code: no_capacity
            runtimeRetryable:
              summary: Retryable runtime failure
              value:
                success: false
                error: Sandbox lifecycle request is temporarily blocked; retry
                code: runtime_retryable
            notEnabled:
              summary: Sandboxes disabled
              value:
                success: false
                error: Sandboxes are not enabled on this controlplane
                code: not_enabled
    PlainTextUnauthorized:
      description: Missing or invalid authentication credentials
      content:
        text/plain:
          schema:
            type: string
          examples:
            missingAuthorization:
              summary: Missing Authorization header
              value: Must provide an Authorization header
            invalidCredentials:
              summary: Invalid credentials
              value: Unauthorized
    SandboxInternalError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error: Internal error processing sandbox request
            code: internal_server_error
    SandboxTimeout:
      description: Request timed out; retry
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error: Sandbox request timed out; retry
            code: timeout
    Unauthorized:
      description: Invalid or missing authentication credentials
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    ValidationError:
      description: Validation error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    InternalError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    RetryableUnavailable:
      description: The lifecycle transition is temporarily blocked and can be retried
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'

  schemas:
    ErrorResponse:
      type: object
      required:
        - success
        - error
      properties:
        success:
          type: boolean
          example: false
        error:
          type: string
          example: Invalid request parameters
        code:
          type: string
          description: Stable machine-readable error code.

    ApiResponse_Empty:
      type: object
      required:
        - success
      properties:
        success:
          type: boolean
          example: true

    ApiResponse_Message:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          properties:
            message:
              type: string

    SandboxState:
      type: string
      enum: [pending, running, pausing, paused, stopping, stopped, exited, failed, deleting, deleted]

    SandboxNetworkAction:
      type: string
      enum: [allow, deny]

    SandboxEgressPolicy:
      type: object
      description: Deny targets take precedence when an address or domain matches both lists. When domain rules or drain selectors are present, TCP ports 80 and 443 are restricted to HTTP/1.1 or HTTP/2. Plaintext HTTP authority is enforced on port 80, but requests matching a transformation rule are rejected unless the rule also forwards. Port 443 is TLS-terminated; both TLS SNI and HTTP authority are evaluated, and the HTTP authority selects the upstream, any request transformations, and any request forwarding. DNS to the sandbox's configured resolvers is allowed and UDP port 443 is denied.
      required:
        - default
      properties:
        default:
          $ref: '#/components/schemas/SandboxNetworkAction'
        allow:
          type: array
          items:
            oneOf:
              - type: string
              - $ref: '#/components/schemas/SandboxEgressRule'
          description: Allowed IPv4 addresses, CIDR ranges, exact domains, wildcard domains beginning with `*.`, or target objects with optional outbound HTTPS request transformations. A wildcard matches subdomains but not the apex domain, including when applying transformations.
        deny:
          type: array
          items:
            type: string
          description: Denied IPv4 addresses, CIDR ranges, exact domains, or wildcard domains beginning with `*.`. A wildcard matches subdomains but not the apex domain.

        drain_on_pause:
          type: array
          description: Optional hosts whose HTTP(S) requests should finish before pausing, regardless of URL path. Pausing gates new matching requests and waits up to ten minutes by default for active responses, including streams, before taking a snapshot. Errors stop counting, and expiry proceeds with the snapshot. This does not limit requests during normal operation. Selectors do not grant network access. Matching uses the original request host before transformations or forwarding. Only TCP ports 80 and 443 are supported. Connections may need to be retried after resume.
          items:
            type: string
            description: Lowercase hostname pattern without a scheme, port, or path. Asterisk matches zero or more characters; *.example.com matches subdomains, not example.com. Use * for every host.
            example: bedrock-runtime.*.amazonaws.com

    SandboxEgressRule:
      type: object
      additionalProperties: false
      required:
        - target
      properties:
        target:
          type: string
          description: Target allowed by this rule. Effective transformations and forwarding require an exact or wildcard lowercase domain.
        transform:
          $ref: '#/components/schemas/SandboxEgressTransform'
        forward_url:
          type: string
          format: uri
          description: >-
            Absolute public HTTPS URL that receives this rule's permitted HTTP and HTTPS requests
            instead of their original upstream. Requires a lowercase domain; IP addresses,
            credentials, queries, and fragments are rejected. Protected and private destination
            addresses are blocked. The original path and query are appended to this URL's path
            prefix. Archil overwrites the archil-forwarded-host, archil-forwarded-scheme,
            archil-forwarded-port, archil-forwarded-path, and archil-sandbox-id headers with request
            metadata. A transform on the same rule is applied before forwarding.

    SandboxEgressTransform:
      type: object
      description: Optional outbound HTTPS request transformations. An omitted or empty transform leaves the rule as an ordinary allow without request mutations.
      additionalProperties: false
      properties:
        headers:
          type: object
          additionalProperties:
            type: string
          description: Outbound HTTPS request headers to set, overwriting values supplied by the sandbox.

    SandboxNetwork:
      type: object
      description: |
        Sandbox network policy. New sandboxes on free plans receive deny-all
        egress with no allowlist exceptions. Start/resume and forks retain the
        stored policy. For paid accounts, egress is unrestricted when omitted.
      properties:
        egress:
          $ref: '#/components/schemas/SandboxEgressPolicy'

    SandboxTimeoutRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        timeout:
          type: integer
          minimum: 60
          maximum: 86400
          description: Seconds from now until a running sandbox pauses, capped at 24 hours after its current session started. Also sets the lifetime budget for its next powered-on session.
        idle_ttl_seconds:
          type: integer
          minimum: 0
          maximum: 86400
          description: Seconds without a direct process connection before the sandbox pauses. Zero disables idle expiry.

    Sandbox:
      type: object
      required:
        - sandbox_id
        - name
        - status
        - vcpu_count
        - mem_size_mib
        - max_ttl_seconds
        - idle_ttl_seconds
        - max_concurrent_execs
        - base_image
        - created_at
        - last_active_at
      properties:
        sandbox_id:
          type: string
          format: uuid
        name:
          type: string
          minLength: 1
          maxLength: 63
          pattern: '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'
          description: Sandbox name.
        status:
          $ref: '#/components/schemas/SandboxState'
        vcpu_count:
          type: integer
        mem_size_mib:
          type: integer
        max_ttl_seconds:
          type: integer
          description: Lifetime budget applied independently to each powered-on session. Expiry pauses the sandbox, preserving memory and processes for resume. Defaults to 24 hours. Timeout resets cannot extend a session beyond 24 hours.
        idle_ttl_seconds:
          type: integer
          description: Seconds without a direct process connection before the sandbox pauses, preserving memory and processes for resume. Zero disables idle expiry.
        max_concurrent_execs:
          type: integer
          description: Maximum number of concurrently attached process sessions. Detached processes and one-shot process controls do not count.
        base_image:
          type: string
          description: OCI reference requested when the sandbox was created. Empty for a sandbox created from `image_id`.
        image_digest:
          type: string
          description: Image digest the sandbox was created from, if any.
        platform:
          type: string
          enum: [arm64, amd64]
          description: Sandbox CPU architecture.
        endpoints:
          type: array
          description: Public hostnames published by enabled sandbox services.
          items:
            $ref: '#/components/schemas/SandboxEndpoint'
        enable_service_ingress:
          type: boolean
          default: false
          description: Whether services inside the sandbox can expose ingress. Explicit API port exposure remains available regardless of this setting.
        mounts:
          type: array
          items:
            $ref: '#/components/schemas/SandboxMount'
        created_at:
          type: string
          format: date-time
        running_at:
          type: string
          format: date-time
        finished_at:
          type: string
          format: date-time
        last_active_at:
          type: string
          format: date-time
        exit_reason:
          type: string
        checkpoint:
          type: string
          description: |
            Disk checkpoint the current session leaves behind. Present while
            pausing, paused, stopping, or stopped, and committed once the
            sandbox is paused or stopped. Pass it to the fork endpoint to fork
            exactly this state.

    SandboxEndpoint:
      type: object
      required:
        - port
        - hostname
      properties:
        port:
          type: integer
          minimum: 1
          maximum: 65535
        hostname:
          type: string

    SandboxPort:
      type: object
      required:
        - port
        - hostname
      properties:
        port:
          type: integer
          minimum: 1
          maximum: 65535
        hostname:
          type: string

    SandboxPortList:
      type: object
      required:
        - ports
      properties:
        ports:
          type: array
          items:
            $ref: '#/components/schemas/SandboxPort'

    ApiResponse_SandboxPort:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/SandboxPort'

    ApiResponse_SandboxPortList:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/SandboxPortList'

    SandboxPortToken:
      type: object
      required:
        - id
        - port
        - created_at
      properties:
        id:
          type: string
          pattern: '^[0-9a-f]{64}$'
          description: Stable token identifier used for get and revoke operations.
        port:
          type: integer
          minimum: 1
          maximum: 65535
        created_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
          description: When the token expires. Absent for a non-expiring token.

    CreatedSandboxPortToken:
      allOf:
        - $ref: '#/components/schemas/SandboxPortToken'
        - type: object
          required:
            - hostname
            - token
          properties:
            hostname:
              type: string
              description: Stable sandbox port hostname, shared with public access.
            token:
              type: string
              description: Raw token returned only when the token is created.

    CreateSandboxPortTokenRequest:
      type: object
      required:
        - port
      properties:
        port:
          type: integer
          minimum: 1
          maximum: 65535
        ttl:
          type: string
          description: |
            Token lifetime as a Go duration string (for example, "4m", "1h",
            or "24h"). Maximum 365 days ("8760h"). Omit for no expiration.
          example: "4m"

    SandboxPortTokenList:
      type: object
      required:
        - tokens
      properties:
        tokens:
          type: array
          items:
            $ref: '#/components/schemas/SandboxPortToken'

    ApiResponse_CreatedSandboxPortToken:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/CreatedSandboxPortToken'

    ApiResponse_SandboxPortToken:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/SandboxPortToken'

    ApiResponse_SandboxPortTokenList:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/SandboxPortTokenList'
        nextCursor:
          type: string
          description: Set when more port tokens remain.

    SandboxConnection:
      type: object
      required:
        - url
        - expires_at
      properties:
        url:
          type: string
          format: uri
        expires_at:
          type: string
          format: date-time

    ApiResponse_SandboxConnection:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/SandboxConnection'

    ApiResponse_Sandbox:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/Sandbox'

    ApiResponse_SandboxNetwork:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/SandboxNetwork'

    SandboxMount:
      type: object
      required:
        - disk_id
      properties:
        disk_id:
          type: string
          pattern: '^dsk-[0-9a-f]{16}$'
          description: Existing disk owned by your account in the sandbox's region. A sandbox's internal root disk cannot be used here.
        path:
          type: string
          description: >-
            Absolute guest directory to mount at. Required when more than one
            disk is mounted; a sole mount defaults to /mnt/archil. Paths cannot
            overlap or contain whitespace, control characters, empty components,
            . or .. components, or a trailing slash. The root directory and guest
            system directories (/usr, /etc, /var, /opt, /dev, ...) and their
            descendants are reserved. A disk can be mounted once per sandbox.
        subdirectory:
          type: string
          description: Relative subdirectory of the disk to expose instead of its root.
        read_only:
          type: boolean
          default: false
        conditional:
          type: boolean
          default: false
          description: >-
            Send mutating operations directly to the server without a
            delegation checkout, allowing concurrent writers.
        queue_ms:
          type: integer
          minimum: 1
          description: >-
            Milliseconds to wait for the disk's exclusive root delegation
            before the mount fails. Not allowed with read_only or conditional.

    StartSandboxRequest:
      type: object
      properties:
        mounts:
          type: array
          description: >-
            Replace the sandbox's additional disk mounts for this and later sessions.
            Omit to keep the current ones; an empty list removes all additional mounts
            without deleting their disks. The internal root disk is unaffected.
            Not accepted by resume, and 409
            while the sandbox is running or pending.
          items:
            $ref: '#/components/schemas/SandboxMount'

    RegistryAuth:
      type: object
      description: >-
        Registry credentials required for private images. Supply them on every build
        request; Archil does not store them. Omit for public images.
      required:
        - username
        - password
      properties:
        username:
          type: string
          minLength: 1
          description: Registry username.
        password:
          type: string
          minLength: 1
          description: Password or access token with pull access to the image.

    CreateImageRequest:
      type: object
      required:
        - source
      properties:
        source:
          type: string
          description: Linux OCI image reference. Docker shorthand and tags are accepted.
        registry_auth:
          $ref: '#/components/schemas/RegistryAuth'

    ImageState:
      type: string
      enum: [building, ready, failed]

    Image:
      type: object
      required:
        - image_id
        - source
        - private
        - status
        - created_at
        - updated_at
      properties:
        image_id:
          type: string
          pattern: '^[0-9a-f]{64}$'
          description: >-
            Stable ID for the normalized source and its public or private scope.
            Private image IDs are scoped to your account. Adding or removing
            registry credentials changes the scope and the ID; rebuilding in
            the same scope keeps the ID.
        source:
          type: string
          description: Normalized OCI reference the image is pulled from.
        private:
          type: boolean
          description: Whether the image is pulled with registry credentials.
        status:
          $ref: '#/components/schemas/ImageState'
        digest:
          type: string
          pattern: '^sha256:[0-9a-f]{64}$'
          description: >-
            The most recent successful build. Sandboxes created from this
            `image_id` boot it, and keep it after the image is rebuilt.
        canonical_source:
          type: string
          description: The pulled manifest, as `repository@sha256:...`.
        failure_reason:
          type: string
          enum: [invalid_source, interrupted]
          description: >-
            Set when `failed`. `invalid_source`: the registry reported the image
            missing or denied access, so requesting it again will not help.
            `interrupted`: the build did not finish; request the image again.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    ApiResponse_Image:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/Image'

    CreateSandboxRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 63
          pattern: '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'
          description: Sandbox name, unique within the account. A random word-list name is generated when omitted.
        vcpu_count:
          type: integer
          minimum: 1
          maximum: 32
          default: 1
        mem_size_mib:
          type: integer
          minimum: 256
          maximum: 65536
          default: 2048
        base_image:
          type: string
          description: >-
            Public Linux OCI image reference. Docker shorthand and tags are
            accepted; the selected platform manifest is pinned at creation.
            Defaults to `ubuntu:26.04` when neither this nor `image_id` is
            set. Mutually exclusive with `image_id`.
        image_id:
          type: string
          pattern: '^[0-9a-f]{64}$'
          description: >-
            ID from [Build Image](/api-reference/images/build-image), for a public or private image.
            For private images, build with `registry_auth` first, then pass the returned ID here.
            The sandbox boots the image's current `digest`.
            Mutually exclusive with `base_image`.
        ports:
          type: array
          description: TCP ports to expose publicly when the sandbox is created.
          items:
            type: integer
            minimum: 1
            maximum: 65535
        enable_service_ingress:
          type: boolean
          default: false
          description: Allow services inside the sandbox to expose ingress. When false, services still run but their ports must be exposed explicitly through the API. Retained across starts, resumes, and forks.
        env:
          type: object
          additionalProperties:
            type: string
          description: Environment variables applied to every process
        network:
          $ref: '#/components/schemas/SandboxNetwork'
        mounts:
          type: array
          description: >-
            Archil disks mounted inside the guest for the first session;
            `start` can replace them for later sessions. Inherited by forks. A
            sandbox with mounts must be stopped before it can be forked.
          items:
            $ref: '#/components/schemas/SandboxMount'
        max_ttl_seconds:
          type: integer
          minimum: 60
          maximum: 86400
          default: 86400
          description: Lifetime budget applied independently to each powered-on session. Expiry pauses the sandbox, preserving memory and processes for resume. Timeout resets cannot extend a session beyond 24 hours.
        idle_ttl_seconds:
          type: integer
          minimum: 0
          maximum: 86400
          description: Pause after this many seconds without a direct process connection, preserving memory and processes for resume. Omit or set to zero to disable idle expiry.
        max_concurrent_execs:
          type: integer
          minimum: 1
          maximum: 256
          default: 32
          description: Maximum number of concurrently attached process sessions. Detached processes and one-shot process controls do not count.

    ForkSandboxRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 63
          pattern: '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'
          description: Name for the fork. A random word-list name is generated when omitted.
        checkpoint:
          type: string
          description: |
            Fork this checkpoint of the source, as returned by pause, stop, or
            describe, instead of the source's live state. The source is neither
            paused nor resumed. Only the current session's checkpoint and the
            one before it are accepted; an uncommitted or superseded checkpoint
            is rejected with 409.

    ApiResponse_SandboxList:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          properties:
            sandboxes:
              type: array
              items:
                $ref: '#/components/schemas/Sandbox'

    # Disk schemas
    CreateDiskRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          description: Disk name (alphanumeric, dashes, underscores)
          minLength: 1
          maxLength: 100
          pattern: '^[a-zA-Z0-9_-]+$'
          example: my-data-disk
        mounts:
          type: array
          description: Storage mount to attach. Omit for archil-managed storage.
          minItems: 0
          maxItems: 1
          items:
            $ref: '#/components/schemas/MountConfig'
        authMethods:
          type: array
          deprecated: true
          description: >
            Deprecated. Use AddDiskUser after creation instead. When
            provided, suppresses the default auto-generated token user.
          items:
            $ref: '#/components/schemas/DiskUser'

    MountConfig:
      oneOf:
        - $ref: '#/components/schemas/S3Mount'
        - $ref: '#/components/schemas/GCSMount'
        - $ref: '#/components/schemas/R2Mount'
        - $ref: '#/components/schemas/S3CompatibleMount'
        - $ref: '#/components/schemas/AzureBlobMount'
      discriminator:
        propertyName: type
        mapping:
          s3: '#/components/schemas/S3Mount'
          gcs: '#/components/schemas/GCSMount'
          r2: '#/components/schemas/R2Mount'
          s3-compatible: '#/components/schemas/S3CompatibleMount'
          azure-blob: '#/components/schemas/AzureBlobMount'

    S3Mount:
      type: object
      title: S3
      description: Mount configuration for Amazon S3 buckets
      required:
        - type
        - bucketName
      properties:
        type:
          type: string
          enum: [s3]
          description: Mount type identifier
        bucketName:
          type: string
          description: S3 bucket name
          example: my-bucket
        accessKeyId:
          type: string
          description: AWS access key ID (optional for public buckets or IAM role auth)
        secretAccessKey:
          type: string
          description: AWS secret access key
          format: password
          writeOnly: true
        sessionToken:
          type: string
          description: Session token for temporary credentials
          writeOnly: true
        bucketPrefix:
          type: string
          description: Prefix within the bucket
          example: data/

    GCSMount:
      type: object
      title: Google Cloud Storage
      description: Mount configuration for Google Cloud Storage buckets
      required:
        - type
        - bucketName
        - accessKeyId
        - secretAccessKey
      properties:
        type:
          type: string
          enum: [gcs]
          description: Mount type identifier
        bucketName:
          type: string
          description: GCS bucket name
          example: my-gcs-bucket
        accessKeyId:
          type: string
          description: HMAC access key ID
          writeOnly: true
        secretAccessKey:
          type: string
          description: HMAC secret access key
          format: password
          writeOnly: true
        bucketPrefix:
          type: string
          description: Prefix within the bucket (optional)
          example: data/

    R2Mount:
      type: object
      title: Cloudflare R2
      description: Mount configuration for Cloudflare R2 buckets
      required:
        - type
        - bucketName
        - bucketEndpoint
        - accessKeyId
        - secretAccessKey
      properties:
        type:
          type: string
          enum: [r2]
          description: Mount type identifier
        bucketName:
          type: string
          description: R2 bucket name
          example: my-r2-bucket
        bucketEndpoint:
          type: string
          description: R2 endpoint URL
          example: https://accountid.r2.cloudflarestorage.com
        accessKeyId:
          type: string
          description: R2 access key ID
          writeOnly: true
        secretAccessKey:
          type: string
          description: R2 secret access key
          format: password
          writeOnly: true
        bucketPrefix:
          type: string
          description: Prefix within the bucket (optional)
          example: data/

    S3CompatibleMount:
      type: object
      title: S3-Compatible
      description: Mount configuration for S3-compatible storage (MinIO, Backblaze B2, etc.)
      required:
        - type
        - bucketName
        - bucketEndpoint
        - accessKeyId
        - secretAccessKey
      properties:
        type:
          type: string
          enum: [s3-compatible]
          description: Mount type identifier
        bucketName:
          type: string
          description: Bucket name
          example: my-bucket
        bucketEndpoint:
          type: string
          description: S3-compatible endpoint URL
          example: https://s3.us-west-001.backblazeb2.com
        accessKeyId:
          type: string
          description: Access key ID
          writeOnly: true
        secretAccessKey:
          type: string
          description: Secret access key
          format: password
          writeOnly: true
        bucketPrefix:
          type: string
          description: Prefix within the bucket (optional)
          example: data/

    AzureBlobMount:
      type: object
      title: Azure Blob Storage
      description: Mount configuration for Azure Blob Storage containers
      required:
        - type
        - containerName
        - tenantId
        - clientId
        - clientSecret
      properties:
        type:
          type: string
          enum: [azure-blob]
          description: Mount type identifier
        containerName:
          type: string
          description: Azure blob container name
          example: my-container
        endpoint:
          type: string
          description: Azure blob endpoint URL (optional if storageAccountName provided)
          example: https://myaccount.blob.core.windows.net
        storageAccountName:
          type: string
          description: Azure storage account name (used to derive endpoint if not provided)
          example: myaccount
        tenantId:
          type: string
          description: Azure AD tenant ID
        clientId:
          type: string
          description: Azure AD application client ID
        clientSecret:
          type: string
          description: Azure AD application client secret
          format: password
          writeOnly: true
        bucketPrefix:
          type: string
          description: Prefix within the container
          example: data/

    TokenUser:
      type: object
      required: [type, nickname]
      properties:
        type:
          type: string
          enum: [token]
        principal:
          type: string
          maxLength: 2048
          deprecated: true
          description: >
            Deprecated. Client-provided token. If omitted, the server generates
            a cryptographically secure token and returns it in the response.
        nickname:
          type: string
          maxLength: 255
        tokenSuffix:
          type: string
          minLength: 4
          maxLength: 4
          deprecated: true
          description: >
            Deprecated. Last 4 characters of the token. Required when principal
            is provided; ignored when the server generates the token.

    AwsStsUser:
      type: object
      required: [type, principal]
      properties:
        type:
          type: string
          enum: [awssts]
        principal:
          type: string
          description: IAM principal ARN
          maxLength: 2048

    DiskUser:
      oneOf:
        - $ref: '#/components/schemas/TokenUser'
        - $ref: '#/components/schemas/AwsStsUser'
      discriminator:
        propertyName: type
        mapping:
          token: '#/components/schemas/TokenUser'
          awssts: '#/components/schemas/AwsStsUser'

    DiskResponse:
      type: object
      required:
        - id
        - name
        - organization
        - status
        - provider
        - region
        - createdAt
      properties:
        id:
          type: string
          description: Disk ID
          example: dsk-0123456789abcdef
        name:
          type: string
          description: Disk name
        organization:
          type: string
          description: Owning organization ID
        status:
          type: string
          description: Disk status
          enum:
            - available
            - creating
            - deleting
            - deleted
            - failed
        fsHandlerStatus:
          type: string
          description: Filesystem handler status
        provider:
          type: string
          description: Cloud provider
        region:
          type: string
          description: "Disk region (e.g., aws-us-east-1, gcp-us-central1)"
        createdAt:
          type: string
          format: date-time
          description: Creation timestamp
        lastAccessed:
          type: string
          format: date-time
          description: Last access timestamp
        dataSize:
          type: integer
          format: int64
          minimum: 0
          description: Total data size in bytes
        monthlyUsage:
          type: string
          description: "Monthly usage amount formatted as a currency string (e.g., \"$1.23\")"
        mounts:
          type: array
          items:
            $ref: '#/components/schemas/MountResponse'
        metrics:
          $ref: '#/components/schemas/DiskMetrics'
        connectedClients:
          type: array
          items:
            $ref: '#/components/schemas/ConnectedClient'
        authorizedUsers:
          type: array
          items:
            $ref: '#/components/schemas/AuthorizedUser'

    MountResponse:
      type: object
      properties:
        id:
          type: string
          description: Mount identifier
        type:
          type: string
          description: Storage backend type
          enum:
            - s3
            - gcs
            - r2
            - s3-compatible
            - azure-blob
        path:
          type: string
          description: Mount path
        name:
          type: string
          description: Bucket/container name
        accessMode:
          type: string
          description: Access mode
          enum:
            - rw
            - ro
          example: rw
        config:
          $ref: '#/components/schemas/MountConfigResponse'
        connectionStatus:
          type: string
          description: Current connection status
          enum:
            - connected
            - disconnected
        authError:
          type: string
          description: Authentication error message (if disconnected)
        authorizationType:
          type: string
          description: How the mount authenticates to the storage backend
          enum:
            - iam
            - accessKeys
            - oauth

    MountConfigResponse:
      type: object
      description: Mount configuration details (secrets omitted)
      properties:
        bucketName:
          type: string
          description: Bucket name
        bucketEndpoint:
          type: string
          description: Storage endpoint URL
        bucketPrefix:
          type: string
          description: Prefix within the bucket
        sessionId:
          type: string
          description: Session identifier for IAM-authorized mounts

    DiskMetrics:
      type: object
      properties:
        dataTransfer:
          type: string
          description: "Data transfer amount with unit (e.g., \"1.5 GB\")"
        requests:
          type: string
          description: "Total request count as a formatted string (e.g., \"1,234\")"
        avgResponseTime:
          type: string
          description: "Average response time with unit (e.g., \"45ms\")"

    ConnectedClient:
      type: object
      properties:
        id:
          type: string
        ipAddress:
          type: string
        connectedAt:
          type: string
          format: date-time

    AuthorizedUser:
      type: object
      properties:
        type:
          type: string
          enum:
            - token
            - awssts
        principal:
          type: string
          deprecated: true
          description: >
            Use identifier instead. Only populated for awssts type
            (the IAM ARN).
        nickname:
          type: string
        tokenSuffix:
          type: string
        token:
          type: string
          description: >
            The generated disk token (used by clients when mounting the disk).
            Only present in the response when the server generates the token
            (i.e. principal was not provided). This value is shown exactly once
            and cannot be retrieved again.
        identifier:
          type: string
          description: >
            Stable identifier for this user, returned in creation and list
            responses. Use this value with
            DELETE /api/disks/{id}/users/{type}?identifier={identifier}
            to remove the user. For awssts users, this is the IAM ARN.
        createdAt:
          type: string
          format: date-time

    ApiResponse_DiskList:
      description: >
        All API responses use a standard envelope with `success: boolean` and
        `data` (on success) or `error: string` (on failure). The ApiResponse_*
        schemas each define the specific `data` shape for their endpoint.
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/DiskResponse'

    ApiResponse_Disk:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/DiskResponse'

    ApiResponse_CreateDisk:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          properties:
            diskId:
              type: string
              example: dsk-0123456789abcdef
            authorizedUsers:
              type: array
              items:
                $ref: '#/components/schemas/AuthorizedUser'

    # Disk User schemas
    ApiResponse_AuthorizedUser:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/AuthorizedUser'

    # API Token schemas
    CreateApiTokenRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          description: Token name
          minLength: 1
          maxLength: 100
        description:
          type: string
          description: Token description
          maxLength: 500

    ApiTokenResponse:
      type: object
      properties:
        id:
          type: string
          description: Token hash/ID
        name:
          type: string
        description:
          type: string
        tokenSuffix:
          type: string
          description: Last 4 characters of the token
        createdAt:
          type: string
          format: date-time
        lastUsedAt:
          type: string
          format: date-time

    ApiResponse_TokenList:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          properties:
            tokens:
              type: array
              items:
                $ref: '#/components/schemas/ApiTokenResponse'

    ApiResponse_TokenCreated:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          allOf:
            - $ref: '#/components/schemas/ApiTokenResponse'
            - type: object
              properties:
                token:
                  type: string
                  description: Full token value (only shown at creation)
                  example: key-abc123...

    # Exec schemas
    ExecDiskRequest:
      type: object
      required:
        - command
      properties:
        command:
          type: string
          description: Shell command to execute inside the container
          example: ls -la /mnt/archil

    ExecDiskResult:
      type: object
      required:
        - exitCode
        - stdout
        - stderr
        - timing
      properties:
        exitCode:
          type: integer
          description: Exit code of the command (0 = success)
          example: 0
        stdout:
          type: string
          description: Standard output from the command
        stderr:
          type: string
          description: Standard error from the command
        timing:
          $ref: '#/components/schemas/ExecTiming'

    ExecTiming:
      type: object
      description: Server-measured timings for an exec request.
      required:
        - totalMs
        - queueMs
        - executeMs
      properties:
        totalMs:
          type: integer
          description: >
            End-to-end wall clock measured on the server, from request arrival
            to response.
          example: 2450
        queueMs:
          type: integer
          description: >
            Time spent queueing, scheduling, booting/claiming a VM, and
            mounting the filesystem before the command started running.
          example: 150
        executeMs:
          type: integer
          description: >
            Time the user's command itself ran, measured by the runtime.
          example: 2300

    ApiResponse_ExecDisk:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/ExecDiskResult'

    ExecRequest:
      type: object
      required:
        - disks
        - command
      properties:
        disks:
          type: object
          description: |
            Map of relative path under `/mnt/archil` to the disk to mount
            there. At least one entry is required. Relative paths must be
            non-empty, non-absolute, and contain no `.` or `..` segments.

            Each value is either a plain disk ID string (mounts the disk's
            root, read-write) or an object that additionally selects a
            subdirectory of the disk and/or marks the mount as read-only.
          additionalProperties:
            oneOf:
              - type: string
                description: Disk ID; mounts the disk's root as read-write
                example: dsk-abc123
              - $ref: '#/components/schemas/ExecMount'
          example:
            data: dsk-abc123
            logs:
              disk: dsk-def456
              subdirectory: app/logs
              readOnly: true
        command:
          type: string
          description: Shell command to execute inside the container
          example: ls -la /mnt/archil/data /mnt/archil/logs

    ExecMount:
      type: object
      required:
        - disk
      properties:
        disk:
          type: string
          description: Disk ID to mount at this relative path
          example: dsk-abc123
        subdirectory:
          type: string
          description: |
            Subdirectory of the disk to expose at the mountpoint. Must be a
            relative path with no `.` or `..` segments. When omitted, the
            disk's root is exposed.
          example: app/logs
        readOnly:
          type: boolean
          default: false
          description: |
            When true, the disk is mounted read-only inside the container.
            Writes against the mount fail with EROFS.

    ApiResponse_Exec:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          # Same shape as the single-disk exec result.
          $ref: '#/components/schemas/ExecDiskResult'

    GrepDiskRequest:
      type: object
      required:
        - directory
        - pattern
      properties:
        directory:
          type: string
          description: |
            Directory on the disk to search, relative to the disk root.
            An empty string or "/" means the disk root.
          example: data/logs
        pattern:
          type: string
          description: Extended regular expression (passed to `grep -E`)
          example: ERROR|FATAL
        recursive:
          type: boolean
          description: When true, walks subdirectories breadth-first.
          default: false
        maxDurationSeconds:
          type: integer
          minimum: 1
          maximum: 30
          default: 30
          description: |
            Wall-clock deadline for the entire request. Capped at 30
            seconds because the runtime exec container itself is bounded
            at ~30s; longer requests would have their workers killed
            mid-scan.
        concurrency:
          type: integer
          minimum: 1
          maximum: 100
          default: 50
          description: |
            Maximum number of parallel grep workers. Higher values finish
            larger datasets within the deadline but consume proportionally
            more runtime capacity.
        maxResults:
          type: integer
          minimum: 1
          maximum: 10000
          default: 1000
          description: |
            Stop scanning once the aggregator has this many matches.
            Returned matches are a sample of whichever workers reported
            first, not the lexicographically first N.

    GrepMatch:
      type: object
      required:
        - file
        - line
        - text
      properties:
        file:
          type: string
          description: Path to the file (relative to the disk root).
          example: data/logs/2026-05-03.log
        line:
          type: integer
          description: 1-based line number where the match occurred.
          example: 142
        text:
          type: string
          description: The matching line.

    GrepStoppedReason:
      type: string
      enum:
        - completed
        - incomplete
        - max_results
        - deadline
        - list_failed
      description: |
        Why the search stopped.
        - `completed`: every file under the directory was scanned successfully.
        - `incomplete`: pipeline ran to its natural end but one or more
          batches errored (invalid regex, unreadable file, runtime issue).
          Results may be partial or wrong; do not rely on completeness.
        - `max_results`: hit `maxResults` before scanning everything.
        - `deadline`: hit `maxDurationSeconds`.
        - `list_failed`: directory listing failed; partial results
          may be present.

    GrepDiskResult:
      type: object
      required:
        - matches
        - stoppedReason
        - filesScanned
        - containersDispatched
        - computeSecondsUsed
        - durationMs
        - listingMs
        - grepMs
      properties:
        matches:
          type: array
          items:
            $ref: '#/components/schemas/GrepMatch'
        stoppedReason:
          $ref: '#/components/schemas/GrepStoppedReason'
        filesScanned:
          type: integer
          description: Files actually fed to a grep container.
        containersDispatched:
          type: integer
          description: Number of grep containers started for this request.
        computeSecondsUsed:
          type: number
          format: double
          description: |
            Sum of per-container execution time in seconds, measured by the
            runtime. Approximates billable container-seconds.
        durationMs:
          type: integer
          description: End-to-end wall clock measured by the server.
        listingMs:
          type: integer
          description: |
            Wall-clock time spent enumerating files via listObjects, from
            the request's start to the moment listing fully drained (or
            was canceled). Listing and matching overlap, so listingMs +
            grepMs typically exceeds durationMs.
        grepMs:
          type: integer
          description: |
            Wall-clock time spent matching, from the first grep container
            being dispatched to the last container reporting results. 0 if
            no batches ran.

    ApiResponse_GrepDisk:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          $ref: '#/components/schemas/GrepDiskResult'

    CreateShareUrlRequest:
      type: object
      required:
        - key
      properties:
        key:
          type: string
          minLength: 1
          description: File key (path) within the disk.
          example: reports/2026-08/summary.csv
        expiresIn:
          type: integer
          minimum: 1
          maximum: 604800
          description: |
            URL lifetime in seconds. Any integer from 1 to 604800 (7 days).
            Defaults to 86400 (24 hours).
          example: 3600

    ApiResponse_ShareUrl:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          required:
            - url
            - expiresIn
          properties:
            url:
              type: string
              format: uri
              description: The signed, public download URL.
            expiresIn:
              type: integer
              description: URL lifetime in seconds.
