openapi: 3.1.0
info:
  title: MIOSA API
  version: 1.0.0
  description: |
    The MIOSA platform API. All endpoints are under `https://api.miosa.ai/api/v1`.

    **Authentication** — every request requires a Bearer token:
    ```
    Authorization: Bearer msk_live_...
    ```
    Workspace keys (`msk_live_*` / `msk_test_*`) and short-lived JWT tokens are both accepted.

    **Idempotency** — mutation requests (`POST`, `DELETE`) should include an
    `Idempotency-Key` header (UUID v4) to enable safe retries. Repeating the same
    key within 24 hours returns the original response without re-executing the mutation.

    **Pagination** — all list endpoints accept `page` (default `1`) and `page_size`
    (default `20`, max `100`). Responses include a `page` envelope:
    ```json
    { "data": [...], "page": { "page": 1, "page_size": 20, "total": 150 } }
    ```

    **Errors** — all errors follow:
    ```json
    { "error": { "code": "sandbox_not_found", "message": "...", "request_id": "req_01jv..." } }
    ```
    Include `request_id` when contacting support.

    **Ownership** — resource create endpoints accept `workspace_id`, `workspace_slug`,
    `workspace_name`, `project_id`, `project_slug`, `project_name`, and optional
    `external_workspace_id`, `external_user_id`, `external_project_id` attribution.
    Responses include canonical `workspace_id` and `project_id` when a resource is
    owned by a workspace/project.
  contact:
    name: MIOSA Support
    email: platform@miosa.ai
    url: https://miosa.ai/docs
  license:
    name: Proprietary

servers:
  - url: https://api.miosa.ai/api/v1
    description: Production

security:
  - bearerAuth: []

tags:
  - name: Sandboxes
    description: Ephemeral compute environments. The primary unit for running agent workloads.
  - name: Computers
    description: Full desktop VMs with persistent state, screenshot capture, and GUI automation.
  - name: Deployments
    description: Static and server deployments published from a sandbox or computer.
  - name: Versions
    description: Immutable deployment snapshots. Each publish creates a new version.
  - name: Custom Domains
    description: Register and verify custom domains on a deployment.
  - name: Workspaces
    description: Customer/client workspaces inside an organization.
  - name: Projects
    description: Apps, websites, documents, and workflows inside workspaces.
  - name: API Keys
    description: Manage workspace API keys (`msk_*`).
  - name: Events
    description: Lifecycle events emitted by sandboxes and computers via SSE.
  - name: Credits
    description: Credit balance, usage transactions, and top-ups.

paths:

  # ─── Sandboxes ───────────────────────────────────────────────────────────────

  /sandboxes:
    post:
      summary: Create a sandbox
      operationId: createSandbox
      tags: [Sandboxes]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSandboxRequest'
            example:
              template: miosa-sandbox
              workspace_slug: dr-smith-clinic
              workspace_name: Dr. Smith Clinic
              project_slug: lead-magnet
              project_name: Lead Magnet
              external_workspace_id: clinic_123
              external_user_id: dr-smith-456
              external_project_id: project_789
              timeout_ms: 300000
      responses:
        '201':
          description: Sandbox created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SandboxEnvelope'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/RateLimited'

    get:
      summary: List sandboxes
      operationId: listSandboxes
      tags: [Sandboxes]
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
        - name: state
          in: query
          schema:
            type: string
            enum: [provisioning, running, paused, destroyed, error]
          description: Filter by sandbox state.
        - name: external_workspace_id
          in: query
          schema:
            type: string
          description: Filter by external workspace attribution.
        - $ref: '#/components/parameters/WorkspaceIdQuery'
        - $ref: '#/components/parameters/ProjectIdQuery'
        - $ref: '#/components/parameters/ExternalUserIdQuery'
        - $ref: '#/components/parameters/ExternalProjectIdQuery'
      responses:
        '200':
          description: Paginated list of sandboxes
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SandboxListEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /sandboxes/{id}:
    get:
      summary: Get a sandbox
      operationId: getSandbox
      tags: [Sandboxes]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Sandbox object
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SandboxEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      summary: Delete (destroy) a sandbox
      operationId: deleteSandbox
      tags: [Sandboxes]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '204':
          description: Sandbox destroyed
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /sandboxes/{id}/exec:
    post:
      summary: Execute a command in a sandbox
      operationId: execInSandbox
      tags: [Sandboxes]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExecRequest'
            example:
              cmd: ["bash", "-c", "echo hello"]
              timeout_ms: 30000
      responses:
        '200':
          description: Command result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExecResultEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/BadGateway'

  /sandboxes/{id}/files:
    post:
      summary: Write a file into a sandbox
      operationId: writeFileToSandbox
      tags: [Sandboxes]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WriteFileRequest'
            example:
              path: /home/user/app/main.py
              content: "print('hello from MIOSA')\n"
              encoding: utf8
      responses:
        '200':
          description: File written
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WriteFileResultEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'

    get:
      summary: Read a file from a sandbox
      operationId: readFileFromSandbox
      tags: [Sandboxes]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - name: path
          in: query
          required: true
          schema:
            type: string
          description: Absolute path inside the sandbox.
          example: /home/user/app/output.txt
      responses:
        '200':
          description: File contents
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReadFileResultEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /sandboxes/{id}/previews:
    post:
      summary: Create a preview URL for a port in a sandbox
      operationId: createSandboxPreview
      tags: [Sandboxes]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [port]
              properties:
                port:
                  type: integer
                  minimum: 1
                  maximum: 65535
                  example: 3000
      responses:
        '201':
          description: Preview URL created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      url:
                        type: string
                        format: uri
                        example: https://sbx-a1b2c3-3000.preview.miosa.ai
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /sandboxes/{id}/snapshots:
    post:
      summary: Snapshot a sandbox
      operationId: snapshotSandbox
      tags: [Sandboxes]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                label:
                  type: string
                  maxLength: 80
                  example: after-npm-install
      responses:
        '202':
          description: Snapshot initiated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SnapshotEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  # ─── Computers ───────────────────────────────────────────────────────────────

  /computers:
    post:
      summary: Create a computer
      operationId: createComputer
      tags: [Computers]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateComputerRequest'
            example:
              name: research-vm-01
              region: us-east
              cpu: 4
              memory_mb: 8192
              disk_gb: 50
              workspace_slug: dr-smith-clinic
              project_slug: records-portal
              external_workspace_id: clinic_123
              external_project_id: records_portal
      responses:
        '201':
          description: Computer created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ComputerEnvelope'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

    get:
      summary: List computers
      operationId: listComputers
      tags: [Computers]
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
        - name: state
          in: query
          schema:
            type: string
            enum: [creating, running, stopped, error, deleted]
        - $ref: '#/components/parameters/WorkspaceIdQuery'
        - $ref: '#/components/parameters/ProjectIdQuery'
        - $ref: '#/components/parameters/ExternalWorkspaceIdQuery'
        - $ref: '#/components/parameters/ExternalUserIdQuery'
        - $ref: '#/components/parameters/ExternalProjectIdQuery'
      responses:
        '200':
          description: Paginated list of computers
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ComputerListEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /computers/{id}:
    get:
      summary: Get a computer
      operationId: getComputer
      tags: [Computers]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Computer object
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ComputerEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /computers/{id}/start:
    post:
      summary: Start a stopped computer
      operationId: startComputer
      tags: [Computers]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '202':
          description: Start initiated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ComputerEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'

  /computers/{id}/stop:
    post:
      summary: Stop a running computer
      operationId: stopComputer
      tags: [Computers]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '202':
          description: Stop initiated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ComputerEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'

  /computers/{id}/screenshot:
    get:
      summary: Capture a screenshot of the desktop
      operationId: screenshotComputer
      tags: [Computers]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - name: format
          in: query
          schema:
            type: string
            enum: [png, jpeg, webp]
            default: png
        - name: quality
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 85
          description: JPEG/WebP quality (ignored for PNG).
      responses:
        '200':
          description: Screenshot image
          content:
            image/png:
              schema:
                type: string
                format: binary
            image/jpeg:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/BadGateway'

  /computers/{id}/desktop/click:
    post:
      summary: Send a mouse click to the desktop
      operationId: desktopClick
      tags: [Computers]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DesktopClickRequest'
            example:
              x: 640
              y: 400
              button: left
              double: false
      responses:
        '200':
          description: Click sent
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DesktopActionResultEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /computers/{id}/desktop/type:
    post:
      summary: Type text into the desktop
      operationId: desktopType
      tags: [Computers]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties:
                text:
                  type: string
                  maxLength: 4096
                  example: Hello from MIOSA
                delay_ms:
                  type: integer
                  minimum: 0
                  default: 0
                  description: Per-character delay in milliseconds.
      responses:
        '200':
          description: Text typed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DesktopActionResultEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  # ─── Deployments ─────────────────────────────────────────────────────────────

  /deployments:
    post:
      summary: Create a deployment
      operationId: createDeployment
      tags: [Deployments]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateDeploymentRequest'
            example:
              name: my-app
              type: static
              sandbox_id: 550e8400-e29b-41d4-a716-446655440000
              workspace_id: 550e8400-e29b-41d4-a716-446655440000
              project_id: 660e8400-e29b-41d4-a716-446655440001
              external_workspace_id: clinic_123
      responses:
        '201':
          description: Deployment created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeploymentEnvelope'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

    get:
      summary: List deployments
      operationId: listDeployments
      tags: [Deployments]
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
        - $ref: '#/components/parameters/WorkspaceIdQuery'
        - $ref: '#/components/parameters/ProjectIdQuery'
        - $ref: '#/components/parameters/ExternalWorkspaceIdQuery'
        - $ref: '#/components/parameters/ExternalUserIdQuery'
        - $ref: '#/components/parameters/ExternalProjectIdQuery'
      responses:
        '200':
          description: Paginated list of deployments
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeploymentListEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /deployments/{id}/publish:
    post:
      summary: Publish (create a new version of) a deployment
      operationId: publishDeployment
      tags: [Deployments]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                build_dir:
                  type: string
                  default: /dist
                  description: Directory inside the sandbox to publish.
                message:
                  type: string
                  maxLength: 256
                  description: Optional version message (like a commit message).
      responses:
        '202':
          description: Publish initiated — version will be available once build completes
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VersionEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /deployments/{id}/rollback:
    post:
      summary: Roll back a deployment to a previous version
      operationId: rollbackDeployment
      tags: [Deployments]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [version_id]
              properties:
                version_id:
                  type: string
                  format: uuid
                  example: 550e8400-e29b-41d4-a716-446655440001
      responses:
        '202':
          description: Rollback initiated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeploymentEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  # ─── API Keys ─────────────────────────────────────────────────────────────────

  /api-keys:
    post:
      summary: Create an API key
      operationId: createApiKey
      tags: [API Keys]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateApiKeyRequest'
            example:
              name: CI/CD pipeline key
              scopes: ["sandboxes:write", "computers:read"]
              expires_at: '2027-01-01T00:00:00Z'
      responses:
        '201':
          description: |
            API key created. **The raw key is only returned once** in `data.key`.
            Store it securely — subsequent GETs return only the masked prefix.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyCreatedEnvelope'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

    get:
      summary: List API keys
      operationId: listApiKeys
      tags: [API Keys]
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      responses:
        '200':
          description: List of API keys (raw key values never included)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyListEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api-keys/{id}:
    delete:
      summary: Revoke an API key
      operationId: deleteApiKey
      tags: [API Keys]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '204':
          description: Key revoked
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  # ─── Events (SSE) ─────────────────────────────────────────────────────────────

  /events:
    get:
      summary: Stream lifecycle events over SSE
      operationId: streamEvents
      tags: [Events]
      description: |
        Server-Sent Events stream. The connection stays open until closed by the client.
        Each event has a `type` field and a `data` payload.

        Typical event types: `sandbox.created`, `sandbox.running`, `sandbox.destroyed`,
        `computer.started`, `computer.stopped`, `deployment.published`.

        **Example** (using `curl`):
        ```bash
        curl -H "Authorization: Bearer msk_live_..." \
             -H "Accept: text/event-stream" \
             https://api.miosa.ai/api/v1/events
        ```
      parameters:
        - name: resource_type
          in: query
          schema:
            type: string
            enum: [sandbox, computer, deployment]
          description: Filter to a specific resource type.
        - name: resource_id
          in: query
          schema:
            type: string
            format: uuid
          description: Filter to a specific resource.
      responses:
        '200':
          description: SSE stream
          content:
            text/event-stream:
              schema:
                type: string
                example: |
                  event: sandbox.running
                  data: {"id":"550e8400-e29b-41d4-a716-446655440000","state":"running","ts":"2026-05-17T10:00:05Z"}
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ─── Credits / Usage ──────────────────────────────────────────────────────────

  /usage:
    get:
      summary: Get workspace credit balance and usage summary
      operationId: getUsage
      tags: [Credits]
      parameters:
        - name: period
          in: query
          schema:
            type: string
            enum: [current_month, last_month, last_7d, last_30d]
            default: current_month
      responses:
        '200':
          description: Usage summary
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ─── Workspaces ───────────────────────────────────────────────────────────────

  /workspaces:
    get:
      summary: List workspaces
      operationId: listWorkspaces
      tags: [Workspaces]
      responses:
        '200':
          description: Workspaces in the authenticated organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkspaceListEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'

    post:
      summary: Create a workspace
      operationId: createWorkspace
      tags: [Workspaces]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWorkspaceRequest'
            example:
              name: Dr. Smith Clinic
              slug: dr-smith-clinic
              external_workspace_id: clinic_123
      responses:
        '201':
          description: Workspace created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkspaceEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'

  /workspaces/{id}/projects:
    get:
      summary: List projects in a workspace
      operationId: listWorkspaceProjects
      tags: [Projects]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Projects in the workspace
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectListEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /workspaces/{id}/preview-domain:
    get:
      summary: Get workspace preview domain
      operationId: getWorkspacePreviewDomain
      tags: [Workspaces, Custom Domains]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Workspace preview-domain configuration
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PreviewDomainResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    put:
      summary: Set workspace preview domain
      operationId: setWorkspacePreviewDomain
      tags: [Workspaces, Custom Domains]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PreviewDomainRequest'
      responses:
        '200':
          description: Workspace preview domain updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PreviewDomainResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'

    delete:
      summary: Clear workspace preview domain
      operationId: deleteWorkspacePreviewDomain
      tags: [Workspaces, Custom Domains]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Workspace preview domain removed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PreviewDomainResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /workspaces/{id}/preview-domain/verify:
    get:
      summary: Verify workspace preview-domain DNS
      operationId: verifyWorkspacePreviewDomain
      tags: [Workspaces, Custom Domains]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: DNS verification result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PreviewDomainDnsCheck'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'

  /workspaces/me:
    get:
      summary: Get the current workspace
      operationId: getWorkspace
      tags: [Workspaces]
      responses:
        '200':
          description: Workspace object
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkspaceEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ─── Projects ────────────────────────────────────────────────────────────────

  /projects:
    get:
      summary: List projects
      operationId: listProjects
      tags: [Projects]
      parameters:
        - $ref: '#/components/parameters/WorkspaceIdQuery'
      responses:
        '200':
          description: Projects in the authenticated organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectListEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'

    post:
      summary: Create a project
      operationId: createProject
      tags: [Projects]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateProjectRequest'
            example:
              workspace_slug: dr-smith-clinic
              name: Lead Magnet
              slug: lead-magnet
              external_workspace_id: clinic_123
              external_project_id: project_789
      responses:
        '201':
          description: Project created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectEnvelope'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'

  /projects/{id}:
    get:
      summary: Get a project
      operationId: getProject
      tags: [Projects]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Project object
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    patch:
      summary: Update a project
      operationId: updateProject
      tags: [Projects]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateProjectRequest'
      responses:
        '200':
          description: Project updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'

  /projects/{id}/preview-domain:
    get:
      summary: Get project preview domain
      operationId: getProjectPreviewDomain
      tags: [Projects, Custom Domains]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Project preview-domain configuration
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PreviewDomainResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    put:
      summary: Set project preview domain
      operationId: setProjectPreviewDomain
      tags: [Projects, Custom Domains]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PreviewDomainRequest'
      responses:
        '200':
          description: Project preview domain updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PreviewDomainResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'

    delete:
      summary: Clear project preview domain
      operationId: deleteProjectPreviewDomain
      tags: [Projects, Custom Domains]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Project preview domain removed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PreviewDomainResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /projects/{id}/preview-domain/verify:
    get:
      summary: Verify project preview-domain DNS
      operationId: verifyProjectPreviewDomain
      tags: [Projects, Custom Domains]
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: DNS verification result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PreviewDomainDnsCheck'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'

# ─── Components ────────────────────────────────────────────────────────────────

components:

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: msk_live_* or msk_test_*

  parameters:
    ResourceId:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Resource UUID.
      example: 550e8400-e29b-41d4-a716-446655440000

    Page:
      name: page
      in: query
      schema:
        type: integer
        minimum: 1
        default: 1

    PageSize:
      name: page_size
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20

    IdempotencyKey:
      name: Idempotency-Key
      in: header
      schema:
        type: string
        format: uuid
      description: UUID v4. Repeating within 24 h returns the original response.
      example: 550e8400-e29b-41d4-a716-446655440099

    WorkspaceIdQuery:
      name: workspace_id
      in: query
      schema:
        type: string
        format: uuid
      description: Filter to one MIOSA workspace.

    ProjectIdQuery:
      name: project_id
      in: query
      schema:
        type: string
        format: uuid
      description: Filter to one MIOSA project.

    ExternalWorkspaceIdQuery:
      name: external_workspace_id
      in: query
      schema:
        type: string
      description: Filter by caller-supplied workspace/customer attribution.

    ExternalUserIdQuery:
      name: external_user_id
      in: query
      schema:
        type: string
      description: Filter by caller-supplied end-user attribution.

    ExternalProjectIdQuery:
      name: external_project_id
      in: query
      schema:
        type: string
      description: Filter by caller-supplied project/app attribution.

  schemas:

    # ── Sandbox ──

    Sandbox:
      type: object
      required: [id, state, created_at]
      properties:
        id:
          type: string
          format: uuid
          example: 550e8400-e29b-41d4-a716-446655440000
        workspace_id:
          type: string
          format: uuid
          nullable: true
          example: 550e8400-e29b-41d4-a716-446655440000
        project_id:
          type: string
          format: uuid
          nullable: true
          example: 660e8400-e29b-41d4-a716-446655440001
        state:
          type: string
          enum: [provisioning, running, paused, destroyed, error]
          example: running
        template:
          type: string
          example: miosa-sandbox
        external_workspace_id:
          type: string
          nullable: true
          example: acme-corp
        external_user_id:
          type: string
          nullable: true
          example: user-7890
        external_project_id:
          type: string
          nullable: true
          example: project_789
        region:
          type: string
          example: us-east
        ipv4:
          type: string
          nullable: true
          example: 10.4.22.7
        created_at:
          type: string
          format: date-time
          example: '2026-05-17T10:00:00Z'
        expires_at:
          type: string
          format: date-time
          nullable: true
          example: '2026-05-17T10:05:00Z'

    SandboxEnvelope:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/Sandbox'

    SandboxListEnvelope:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Sandbox'
        page:
          $ref: '#/components/schemas/Pagination'

    CreateSandboxRequest:
      type: object
      properties:
        template:
          type: string
          default: miosa-sandbox
          description: Base image template name.
        workspace_id:
          type: string
          format: uuid
          nullable: true
          description: Existing MIOSA workspace ID.
        workspace_slug:
          type: string
          nullable: true
          description: Existing or auto-created workspace slug.
        workspace_name:
          type: string
          nullable: true
          description: Workspace display name if auto-created.
        project_id:
          type: string
          format: uuid
          nullable: true
          description: Existing MIOSA project ID.
        project_slug:
          type: string
          nullable: true
          description: Existing or auto-created project slug.
        project_name:
          type: string
          nullable: true
          description: Project display name if auto-created.
        external_workspace_id:
          type: string
          nullable: true
          description: Opaque ID for multi-tenant attribution.
        external_user_id:
          type: string
          nullable: true
        external_project_id:
          type: string
          nullable: true
        timeout_ms:
          type: integer
          minimum: 1000
          default: 300000
          description: Auto-destroy timeout in milliseconds.
        env:
          type: object
          additionalProperties:
            type: string
          description: Environment variables injected into the sandbox.

    # ── Exec ──

    ExecRequest:
      type: object
      required: [cmd]
      properties:
        cmd:
          type: array
          items:
            type: string
          minItems: 1
          example: ["bash", "-c", "ls /tmp"]
        cwd:
          type: string
          default: /home/user
          example: /home/user/project
        env:
          type: object
          additionalProperties:
            type: string
        timeout_ms:
          type: integer
          minimum: 100
          default: 30000

    ExecResult:
      type: object
      properties:
        stdout:
          type: string
          example: "file1.txt\nfile2.txt\n"
        stderr:
          type: string
          example: ''
        exit_code:
          type: integer
          example: 0
        duration_ms:
          type: integer
          example: 47

    ExecResultEnvelope:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/ExecResult'

    # ── Files ──

    WriteFileRequest:
      type: object
      required: [path, content]
      properties:
        path:
          type: string
          description: Absolute path inside the sandbox.
          example: /home/user/app/main.py
        content:
          type: string
        encoding:
          type: string
          enum: [utf8, base64]
          default: utf8

    WriteFileResult:
      type: object
      properties:
        path:
          type: string
        size_bytes:
          type: integer

    WriteFileResultEnvelope:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/WriteFileResult'

    ReadFileResult:
      type: object
      properties:
        path:
          type: string
        content:
          type: string
        encoding:
          type: string
          enum: [utf8, base64]
        size_bytes:
          type: integer

    ReadFileResultEnvelope:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/ReadFileResult'

    # ── Snapshot ──

    Snapshot:
      type: object
      properties:
        id:
          type: string
          format: uuid
        sandbox_id:
          type: string
          format: uuid
        label:
          type: string
          nullable: true
        state:
          type: string
          enum: [pending, ready, error]
        created_at:
          type: string
          format: date-time

    SnapshotEnvelope:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/Snapshot'

    # ── Computer ──

    Computer:
      type: object
      required: [id, state, created_at]
      properties:
        id:
          type: string
          format: uuid
          example: 550e8400-e29b-41d4-a716-446655441000
        workspace_id:
          type: string
          format: uuid
          nullable: true
        project_id:
          type: string
          format: uuid
          nullable: true
        name:
          type: string
          example: research-vm-01
        state:
          type: string
          enum: [creating, running, stopped, error, deleted]
          example: running
        region:
          type: string
          example: us-east
        cpu:
          type: integer
          example: 4
        memory_mb:
          type: integer
          example: 8192
        disk_gb:
          type: integer
          example: 50
        stream_url:
          type: string
          format: uri
          nullable: true
          description: WebRTC/WebSocket URL for desktop streaming.
        external_workspace_id:
          type: string
          nullable: true
        external_user_id:
          type: string
          nullable: true
        external_project_id:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time

    ComputerEnvelope:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/Computer'

    ComputerListEnvelope:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Computer'
        page:
          $ref: '#/components/schemas/Pagination'

    CreateComputerRequest:
      type: object
      properties:
        name:
          type: string
          maxLength: 64
        region:
          type: string
          default: us-east
        cpu:
          type: integer
          enum: [2, 4, 8, 16]
          default: 2
        memory_mb:
          type: integer
          enum: [2048, 4096, 8192, 16384, 32768]
          default: 4096
        disk_gb:
          type: integer
          minimum: 10
          maximum: 500
          default: 20
        workspace_id:
          type: string
          format: uuid
          nullable: true
        workspace_slug:
          type: string
          nullable: true
        workspace_name:
          type: string
          nullable: true
        project_id:
          type: string
          format: uuid
          nullable: true
        project_slug:
          type: string
          nullable: true
        project_name:
          type: string
          nullable: true
        external_workspace_id:
          type: string
          nullable: true
        external_user_id:
          type: string
          nullable: true
        external_project_id:
          type: string
          nullable: true

    # ── Desktop ──

    DesktopClickRequest:
      type: object
      required: [x, y]
      properties:
        x:
          type: integer
          minimum: 0
          maximum: 1920
          description: X coordinate (0–1920 normalized range).
        y:
          type: integer
          minimum: 0
          maximum: 1080
          description: Y coordinate (0–1080 normalized range).
        button:
          type: string
          enum: [left, right, middle]
          default: left
        double:
          type: boolean
          default: false

    DesktopActionResult:
      type: object
      properties:
        ok:
          type: boolean
          example: true
        timestamp_ms:
          type: integer

    DesktopActionResultEnvelope:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/DesktopActionResult'

    # ── Deployment ──

    Deployment:
      type: object
      properties:
        id:
          type: string
          format: uuid
        workspace_id:
          type: string
          format: uuid
          nullable: true
        project_id:
          type: string
          format: uuid
          nullable: true
        name:
          type: string
        type:
          type: string
          enum: [static, server]
        url:
          type: string
          format: uri
          nullable: true
          example: https://my-app.deploy.miosa.ai
        active_version_id:
          type: string
          format: uuid
          nullable: true
        external_workspace_id:
          type: string
          nullable: true
        external_user_id:
          type: string
          nullable: true
        external_project_id:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time

    DeploymentEnvelope:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/Deployment'

    DeploymentListEnvelope:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Deployment'
        page:
          $ref: '#/components/schemas/Pagination'

    CreateDeploymentRequest:
      type: object
      required: [name, type]
      properties:
        name:
          type: string
          maxLength: 64
        type:
          type: string
          enum: [static, server]
        sandbox_id:
          type: string
          format: uuid
          description: Source sandbox for the initial publish.
        workspace_id:
          type: string
          format: uuid
          nullable: true
        workspace_slug:
          type: string
          nullable: true
        workspace_name:
          type: string
          nullable: true
        project_id:
          type: string
          format: uuid
          nullable: true
        project_slug:
          type: string
          nullable: true
        project_name:
          type: string
          nullable: true
        external_workspace_id:
          type: string
          nullable: true
        external_user_id:
          type: string
          nullable: true
        external_project_id:
          type: string
          nullable: true

    Version:
      type: object
      properties:
        id:
          type: string
          format: uuid
        deployment_id:
          type: string
          format: uuid
        message:
          type: string
          nullable: true
        state:
          type: string
          enum: [building, ready, error]
        url:
          type: string
          format: uri
          nullable: true
        created_at:
          type: string
          format: date-time

    VersionEnvelope:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/Version'

    # ── API Keys ──

    ApiKey:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          example: CI/CD pipeline key
        prefix:
          type: string
          description: Visible prefix only — full value never returned after creation.
          example: msk_live_a1b2
        scopes:
          type: array
          items:
            type: string
          example: ["sandboxes:write", "computers:read"]
        last_used_at:
          type: string
          format: date-time
          nullable: true
        expires_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time

    ApiKeyCreated:
      allOf:
        - $ref: '#/components/schemas/ApiKey'
        - type: object
          properties:
            key:
              type: string
              description: Full API key. Shown once only.
              example: msk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

    ApiKeyCreatedEnvelope:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/ApiKeyCreated'

    ApiKeyListEnvelope:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/ApiKey'
        page:
          $ref: '#/components/schemas/Pagination'

    CreateApiKeyRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          maxLength: 80
        scopes:
          type: array
          items:
            type: string
            enum:
              - sandboxes:read
              - sandboxes:write
              - computers:read
              - computers:write
              - deployments:read
              - deployments:write
              - api-keys:read
              - api-keys:write
              - usage:read
          default: []
          description: Empty array grants full workspace access.
        expires_at:
          type: string
          format: date-time
          nullable: true

    # ── Usage ──

    UsageEnvelope:
      type: object
      properties:
        data:
          type: object
          properties:
            credit_balance_usd:
              type: number
              format: float
              example: 42.50
            period:
              type: string
              example: current_month
            compute_hours:
              type: number
              format: float
              example: 12.4
            sandbox_count:
              type: integer
              example: 87
            computer_count:
              type: integer
              example: 3
            egress_gb:
              type: number
              format: float
              example: 1.2

    # ── Workspace ──

    Workspace:
      type: object
      properties:
        id:
          type: string
          format: uuid
        tenant_id:
          type: string
          format: uuid
        name:
          type: string
          example: Acme Corp
        slug:
          type: string
          example: acme-corp
        external_workspace_id:
          type: string
          nullable: true
          example: clinic_123
        description:
          type: string
          nullable: true
        metadata:
          type: object
          additionalProperties: true
        settings:
          type: object
          additionalProperties: true
        is_default:
          type: boolean
        plan:
          type: string
          enum: [free, starter, pro, enterprise]
        created_at:
          type: string
          format: date-time

    WorkspaceEnvelope:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/Workspace'

    WorkspaceListEnvelope:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Workspace'
        total:
          type: integer

    CreateWorkspaceRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          maxLength: 120
        slug:
          type: string
          maxLength: 80
          pattern: '^[a-z0-9][a-z0-9-]*$'
        external_workspace_id:
          type: string
          nullable: true
        description:
          type: string
          nullable: true
        metadata:
          type: object
          additionalProperties: true

    # ── Project ──

    Project:
      type: object
      properties:
        id:
          type: string
          format: uuid
        tenant_id:
          type: string
          format: uuid
        workspace_id:
          type: string
          format: uuid
        external_workspace_id:
          type: string
          nullable: true
        external_project_id:
          type: string
          nullable: true
        name:
          type: string
        slug:
          type: string
        description:
          type: string
          nullable: true
        metadata:
          type: object
          additionalProperties: true
        settings:
          type: object
          additionalProperties: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    ProjectEnvelope:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/Project'

    ProjectListEnvelope:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Project'
        total:
          type: integer

    CreateProjectRequest:
      type: object
      required: [name]
      properties:
        workspace_id:
          type: string
          format: uuid
          nullable: true
        workspace_slug:
          type: string
          nullable: true
        external_workspace_id:
          type: string
          nullable: true
        name:
          type: string
          maxLength: 120
        slug:
          type: string
          maxLength: 80
          pattern: '^[a-z0-9][a-z0-9-]*$'
        external_project_id:
          type: string
          nullable: true
        description:
          type: string
          nullable: true
        metadata:
          type: object
          additionalProperties: true
        settings:
          type: object
          additionalProperties: true

    UpdateProjectRequest:
      type: object
      properties:
        name:
          type: string
          maxLength: 120
        slug:
          type: string
          maxLength: 80
        description:
          type: string
          nullable: true
        external_workspace_id:
          type: string
          nullable: true
        external_project_id:
          type: string
          nullable: true
        metadata:
          type: object
          additionalProperties: true
        settings:
          type: object
          additionalProperties: true

    PreviewDomainRequest:
      type: object
      required: [preview_domain]
      properties:
        preview_domain:
          type: string
          description: Base domain without protocol, wildcard, port, or path.
          example: drsmithclinic.com

    PreviewDomainResponse:
      type: object
      properties:
        scope:
          type: string
          enum: [workspace, project]
        id:
          type: string
          format: uuid
        preview_domain:
          type: string
          nullable: true
          example: drsmithclinic.com
        effective_domain:
          type: string
          example: drsmithclinic.com
        status:
          type: string
          enum: [configured, inherited, pending_dns, removed]
        dns_status:
          type: string
          enum: [configured, inherited, pending, removed]
        dns_instructions:
          type: object
          additionalProperties: true
        url_examples:
          type: object
          properties:
            default_preview:
              type: string
              example: https://<slug>.drsmithclinic.com
            port_preview:
              type: string
              example: https://3000-<slug>.sandbox.drsmithclinic.com
            deployment:
              type: string
              example: https://<deployment-slug>.drsmithclinic.com

    PreviewDomainDnsCheck:
      type: object
      properties:
        verified:
          type: boolean
        domain:
          type: string
          example: drsmithclinic.com
        dns_status:
          type: string
          enum: [verified, pending]
        records:
          type: object
          additionalProperties: true
        required:
          type: object
          additionalProperties: true

    # ── Shared ──

    Pagination:
      type: object
      required: [page, page_size, total]
      properties:
        page:
          type: integer
          example: 1
        page_size:
          type: integer
          example: 20
        total:
          type: integer
          example: 150

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: Machine-readable error code.
              example: sandbox_not_found
            message:
              type: string
              description: Human-readable description.
              example: No sandbox with that id exists in this workspace.
            request_id:
              type: string
              description: Include when contacting support.
              example: req_01jv8kqzh0000abc
            details:
              type: array
              nullable: true
              items:
                type: object
                properties:
                  field:
                    type: string
                  reason:
                    type: string

  responses:
    BadRequest:
      description: Bad request — missing or invalid parameters
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: bad_request
              message: "Field 'template' is required."
              request_id: req_01jv8kqzh0000abc

    Unauthorized:
      description: Unauthorized — invalid or missing Bearer token
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: unauthorized
              message: Bearer token is missing or invalid.
              request_id: req_01jv8kqzh0000abc

    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: not_found
              message: No resource with that id exists in this workspace.
              request_id: req_01jv8kqzh0000abc

    Forbidden:
      description: Forbidden — caller is authenticated but lacks permission
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: forbidden
              message: Owner or admin role required.
              request_id: req_01jv8kqzh0000abc

    Conflict:
      description: Conflict — invalid state transition
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: conflict
              message: Computer is already running.
              request_id: req_01jv8kqzh0000abc

    UnprocessableEntity:
      description: Validation error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: validation_error
              message: Request body failed validation.
              details:
                - field: cpu
                  reason: must be one of [2, 4, 8, 16]

    PayloadTooLarge:
      description: File upload exceeds 10 MB
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: payload_too_large
              message: File upload exceeds the 10 MB limit.

    RateLimited:
      description: Too many requests
      headers:
        Retry-After:
          description: Seconds until the rate limit resets.
          schema:
            type: integer
            example: 30
        X-RateLimit-Limit:
          schema:
            type: integer
            example: 300
        X-RateLimit-Remaining:
          schema:
            type: integer
            example: 0
        X-RateLimit-Reset:
          schema:
            type: integer
            description: Unix timestamp of reset.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: rate_limited
              message: Too many requests. Retry after 30 seconds.

    BadGateway:
      description: In-VM agent unreachable
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: bad_gateway
              message: The in-VM agent did not respond. The sandbox may still be provisioning.
