openapi: "3.1.0"
info:
  title: Glasswarp Platform API
  version: "1.0.0"
  description: |
    Real-time visual access and native input control on Windows machines.
    See any screen. Control any PC. One API.

    ## Authentication

    Most `/v1/*` endpoints require a bearer token:

    ```
    Authorization: Bearer gw_live_sk_...
    ```

    Exception: `GET /v1/agent/latest` is public (host auto-update).

    API keys are created in the [Console](/console/keys).
    Keys are scoped to your account and can access any rig with API access enabled.

    ## Base URL

    `https://signal.glasswarp.com`
  contact:
    name: Glasswarp
    url: https://glasswarp.com
  license:
    name: Proprietary

servers:
  - url: https://signal.glasswarp.com
    description: Production

security:
  - apiKey: []

components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: "API key in the format `gw_(live|test)_sk_<hex>`"
    userJwt:
      type: http
      scheme: bearer
      description: Supabase user JWT (for console/dashboard endpoints)

  schemas:
    Rig:
      type: object
      properties:
        id:
          type: string
          example: "rig_abc123"
        name:
          type: string
          example: "Gaming PC"
        online:
          type: boolean
        gpu:
          type: string
          example: "NVIDIA RTX 4090"
        os:
          type: string
          example: "Windows 11"
        api_access_enabled:
          type: boolean

    Session:
      type: object
      properties:
        session_id:
          type: string
          example: "s_xyz789"
        rig_id:
          type: string
        mode:
          type: string
          enum: [desktop, app]
        status:
          type: string
          enum: [active, ended, failed]
        started_at:
          type: string
          format: date-time
        action_count:
          type: integer
        idle_timeout_seconds:
          type: integer
          description: Inactivity seconds before automatic idle end (60–3600).

    InputEvent:
      type: object
      required: [type]
      properties:
        type:
          type: string
          enum: [mouse_move, mouse_click, mouse_scroll, key_press, key_down, key_up, type_text]
        x:
          type: integer
          description: Absolute X coordinate (for mouse events)
        y:
          type: integer
          description: Absolute Y coordinate (for mouse events)
        button:
          type: string
          enum: [left, right, middle]
          description: Mouse button (for mouse_click)
        dx:
          type: integer
          description: Horizontal scroll delta (for mouse_scroll)
        dy:
          type: integer
          description: Vertical scroll delta (for mouse_scroll, negative = down)
        key:
          type: string
          description: Key name (for key_press, key_down, key_up)
        text:
          type: string
          description: Text to type (for type_text)
        delay_ms:
          type: integer
          minimum: 0
          maximum: 5000
          description: |
            Optional pause after this event before the next (host-side). Useful
            when a UI needs settling time between clicks or keystrokes.

    GroundingTarget:
      type: object
      required: [id, x, y, w, h]
      properties:
        id:
          type: string
          description: Stable id for click_target / SoM mark
        x:
          type: integer
          description: Native capture origin X
        y:
          type: integer
          description: Native capture origin Y
        w:
          type: integer
        h:
          type: integer
        role:
          type: string
          example: button
        name:
          type: string
          example: "Submit"
        source:
          type: string
          enum: [uia, cv, grid, client]
          description: Where the target came from
        focused:
          type: boolean
          description: True when this control has keyboard focus
        masked:
          type: boolean
          description: Password / masked field — value is always redacted
        value:
          type: string
          description: |
            ValuePattern text when available. Always `[redacted]` when
            `masked` is true — plaintext passwords are never returned.

    ObserveText:
      type: object
      description: Compact structured text for agent decisions without image tokens
      properties:
        window_title:
          type: string
          example: "Save As"
        window_role:
          type: string
          example: window
        targets:
          type: array
          items:
            type: string
          example:
            - "[1] Filename (edit, focused)"
            - "[2] Save (button)"
        summary:
          type: string
          example: 'Focused: "Save As" (window); 2 target(s)'

    ObserveResult:
      type: object
      properties:
        width:
          type: integer
        height:
          type: integer
        native_width:
          type: integer
        native_height:
          type: integer
        timestamp:
          type: integer
        jpeg_base64:
          type: string
          format: byte
          description: Omitted when `image=0` (text/targets-only observe)
        dirty:
          type: object
          properties:
            width:
              type: integer
            height:
              type: integer
            rects:
              type: array
              items:
                type: array
                items:
                  type: integer
        changed:
          type: boolean
          description: False when dirty rects are empty since the last take
        targets:
          type: array
          items:
            $ref: "#/components/schemas/GroundingTarget"
        text:
          $ref: "#/components/schemas/ObserveText"
        marked:
          type: boolean
        timing:
          type: object
          description: |
            Server-side timing for this observe (milliseconds). Host fields are
            null until the paired host daemon reports them.
          properties:
            total_ms:
              type: integer
              description: Wall-clock for the whole observe on the gateway
            screenshot_rtt_ms:
              type: integer
              description: Gateway ↔ host RTT for the screenshot leg (0 when image=0)
            dirty_rtt_ms:
              type: integer
              description: Gateway ↔ host RTT for the dirty-rects leg
            targets_rtt_ms:
              type: integer
              description: Gateway ↔ host RTT for the UIA targets leg
            host_jpeg_ms:
              type: integer
              nullable: true
              description: Host-side capture + JPEG encode time when reported
            host_uia_ms:
              type: integer
              nullable: true
              description: Host-side UIA snapshot time when reported
            image:
              type: boolean
              description: Whether a JPEG was requested

    ApiKey:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        key_prefix:
          type: string
          example: "gw_live_sk_ab12"
        environment:
          type: string
          enum: [live, test]
        scopes:
          type: array
          items:
            type: string
        created_at:
          type: string
          format: date-time
        last_used_at:
          type: string
          format: date-time
          nullable: true

    ApiWebhook:
      type: object
      properties:
        id:
          type: string
          format: uuid
        url:
          type: string
          format: uri
        events:
          type: array
          items:
            type: string
            enum: [session.started, session.ended]
        enabled:
          type: boolean
        created_at:
          type: string
          format: date-time
        last_delivery_at:
          type: string
          format: date-time
          nullable: true
        last_status:
          type: integer
          nullable: true
        last_error:
          type: string
          nullable: true

    ApiWebhookCreated:
      allOf:
        - $ref: "#/components/schemas/ApiWebhook"
        - type: object
          properties:
            secret:
              type: string
              description: HMAC secret — shown only on create
              example: "whsec_..."

    Error:
      type: object
      properties:
        error:
          type: string

paths:
  /v1/agent/latest:
    get:
      security: []
      summary: Latest host agent release (auto-update)
      description: |
        Public. Windows host agents poll this when idle to download a newer
        signed zip. See `docs/AGENT_AUTO_UPDATE.md`.
      operationId: getAgentLatest
      tags: [Agent]
      responses:
        "200":
          description: Latest agent metadata
          content:
            application/json:
              schema:
                type: object
                required: [version, download_url, sha256_url]
                properties:
                  version:
                    type: string
                    example: "0.1.0"
                  download_url:
                    type: string
                    format: uri
                  sha256_url:
                    type: string
                    format: uri
                  sha256:
                    type: string
                    nullable: true

  /v1/rigs:
    get:
      operationId: listRigs
      summary: List rigs
      description: List all rigs on your account with online status and capabilities.
      tags: [Rigs]
      responses:
        "200":
          description: List of rigs
          content:
            application/json:
              schema:
                type: object
                properties:
                  rigs:
                    type: array
                    items:
                      $ref: "#/components/schemas/Rig"
        "401":
          description: Missing or invalid API key

  /v1/rigs/{rigId}:
    get:
      operationId: getRig
      summary: Get rig details
      description: Get details and capabilities for a specific rig.
      tags: [Rigs]
      parameters:
        - name: rigId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Rig details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Rig"
        "404":
          description: Rig not found

  /v1/sessions:
    post:
      operationId: createSession
      summary: Create a session
      description: |
        Start a new desktop session on a rig. The rig must be online
        and have API access enabled by the owner.
      tags: [Sessions]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [rig_id]
              properties:
                rig_id:
                  type: string
                mode:
                  type: string
                  enum: [desktop, app]
                  default: desktop
                idle_timeout_seconds:
                  type: integer
                  minimum: 60
                  maximum: 3600
                  default: 900
                  description: |
                    Seconds with no screenshot, observe, or input before the
                    session is ended automatically (`ended_reason=idle_timeout`).
                    Longer timeouts keep the concurrency slot and continue
                    wall-clock billing. Default 900 (15 minutes).
      responses:
        "201":
          description: Session created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Session"
        "400":
          description: Invalid request
        "403":
          description: API access not enabled on this rig

    get:
      operationId: listSessions
      summary: List active sessions
      description: List all active sessions for the authenticated API key.
      tags: [Sessions]
      responses:
        "200":
          description: Active sessions
          content:
            application/json:
              schema:
                type: object
                properties:
                  sessions:
                    type: array
                    items:
                      $ref: "#/components/schemas/Session"

  /v1/sessions/{sessionId}:
    get:
      operationId: getSession
      summary: Get session details
      description: Get the status, duration, and action count for a session.
      tags: [Sessions]
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Session details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Session"
        "404":
          description: Session not found

    delete:
      operationId: endSession
      summary: End a session
      description: |
        End a session. Triggers safety_restore on the host
        (restores desktop state, disarms input).
      tags: [Sessions]
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Session ended
        "404":
          description: Session not found

  /v1/sessions/{sessionId}/screenshot:
    get:
      operationId: takeScreenshot
      summary: Take a screenshot
      description: |
        Return a JPEG of the current desktop from the host's last-frame
        buffer (DXGI capture, not VNC). Capture is always native resolution
        (including 4K). Use `max_width` / `quality` to downscale on the host
        for faster LLM ingest. Response headers include both JPEG size and
        native desktop size for click coordinate mapping.
      tags: [Screenshots]
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
        - name: max_width
          in: query
          required: false
          schema:
            type: integer
            example: 1280
          description: Long-edge cap for the returned JPEG (native capture unchanged)
        - name: quality
          in: query
          required: false
          schema:
            type: integer
            minimum: 40
            maximum: 100
            default: 85
          description: JPEG quality
        - name: x
          in: query
          required: false
          schema:
            type: integer
          description: ROI crop origin X (native capture coords)
        - name: y
          in: query
          required: false
          schema:
            type: integer
          description: ROI crop origin Y
        - name: w
          in: query
          required: false
          schema:
            type: integer
          description: ROI crop width
        - name: h
          in: query
          required: false
          schema:
            type: integer
          description: ROI crop height
      responses:
        "200":
          description: JPEG screenshot
          headers:
            X-Screenshot-Width:
              schema:
                type: integer
              description: Width of the returned JPEG
            X-Screenshot-Height:
              schema:
                type: integer
              description: Height of the returned JPEG
            X-Screenshot-Native-Width:
              schema:
                type: integer
              description: Native desktop capture width (use for clicks)
            X-Screenshot-Native-Height:
              schema:
                type: integer
              description: Native desktop capture height (use for clicks)
          content:
            image/jpeg:
              schema:
                type: string
                format: binary
        "504":
          description: Screenshot timed out (host did not respond within 5s)

  /v1/sessions/{sessionId}/dirty:
    get:
      operationId: getDirtyRects
      summary: Get accumulated dirty rectangles
      description: |
        Returns DXGI dirty rects accumulated since the last poll, in
        capture-buffer coordinates `[x, y, w, h]`. Empty `rects` means no
        visual change — agents can skip a vision model call. Pair with
        ROI screenshot (`x,y,w,h`) to encode only changed regions.
      tags: [Screenshots]
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Dirty rects since last take
          content:
            application/json:
              schema:
                type: object
                properties:
                  width:
                    type: integer
                  height:
                    type: integer
                  rects:
                    type: array
                    items:
                      type: array
                      items:
                        type: integer
                      minItems: 4
                      maxItems: 4
        "504":
          description: Dirty request timed out

  /v1/sessions/{sessionId}/observe:
    get:
      operationId: observeSession
      summary: "Observe: frame, changes, and click targets in one round trip"
      description: |
        One-round-trip agent observe: JPEG (same options as screenshot),
        dirty rects take, and sparse click targets from the host UI Automation
        snapshot. Response includes structured `text` (window title + readable
        target lines) and `changed` (false when dirty rects are empty).

        Password / masked fields are redacted in `text` and target `value`
        (`masked=true`, value always `[redacted]`).

        Set `image=0` for text/targets-only verification (skips JPEG encode —
        much faster). Prefer `max_width=960` and `quality=60` for cheap
        verification-grade frames when you do need an image.

        SDK applies Set-of-Mark when `mark=1` and targets are present.
        Coords are native capture space.
      tags: [Screenshots, Grounding]
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
        - name: max_width
          in: query
          schema:
            type: integer
            example: 1280
        - name: quality
          in: query
          schema:
            type: integer
            minimum: 40
            maximum: 100
        - name: x
          in: query
          schema:
            type: integer
        - name: y
          in: query
          schema:
            type: integer
        - name: w
          in: query
          schema:
            type: integer
        - name: h
          in: query
          schema:
            type: integer
        - name: mark
          in: query
          schema:
            type: integer
            enum: [0, 1]
          description: Request SoM marking when host targets are non-empty (ignored when image=0)
        - name: image
          in: query
          schema:
            type: integer
            enum: [0, 1]
            default: 1
          description: |
            Set to 0 for text/targets-only observe (no JPEG). Use for simple
            verification — did the dialog close? is the field focused?
      responses:
        "200":
          description: Observe payload
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ObserveResult"
        "504":
          description: Screenshot timed out

  /v1/sessions/{sessionId}/targets:
    get:
      operationId: listTargets
      summary: List click targets (grounding v1)
      description: |
        Sparse click targets in native capture coords from the host focused-window
        UI Automation snapshot. Empty if UIA finds nothing or the host is
        unavailable. Clients may still inject CV/grid targets via the SDK.
      tags: [Grounding]
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Target list
          content:
            application/json:
              schema:
                type: object
                properties:
                  width:
                    type: integer
                  height:
                    type: integer
                  targets:
                    type: array
                    items:
                      $ref: "#/components/schemas/GroundingTarget"
        "404":
          description: Session not found

  /v1/sessions/{sessionId}/stream:
    get:
      operationId: getStreamConnection
      summary: Get live stream connection
      description: |
        Get WebRTC signaling credentials for a live 60fps video stream
        of the remote desktop. Returns a one-time connect URL — open a
        WebSocket to it, send an SDP OFFER, and receive an ANSWER to
        start receiving H.264 video over WebRTC.

        The stream is the same GPU-captured feed used for game streaming:
        native resolution, hardware-encoded, sub-30ms latency on LAN.
      tags: [Streaming]
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Stream connection details
          content:
            application/json:
              schema:
                type: object
                properties:
                  signaling_url:
                    type: string
                    description: WebSocket signaling server URL
                    example: "wss://signal.glasswarp.com"
                  viewer_token:
                    type: string
                    description: Short-lived JWT for WebRTC negotiation (1 hour)
                  rig_id:
                    type: string
                  session_id:
                    type: string
                  connect_url:
                    type: string
                    description: Ready-to-use WebSocket URL with auth params
                    example: "wss://signal.glasswarp.com/?type=client&host_id=rig_abc&token=eyJ..."
                  instructions:
                    type: object
                    description: Step-by-step WebRTC connection flow
        "404":
          description: Session not found

  /v1/sessions/{sessionId}/app/launch:
    post:
      operationId: launchApp
      summary: Launch an application on the rig
      description: |
        Spawn a process on the host by absolute path or PATH name
        (e.g. `notepad.exe`). Does not enter Game Mode shell/display
        policy — desktop API session stays active. Returns the new PID.
        Tracked apps are killed on session end or via `/app/kill`.
      tags: [Apps]
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [path]
              properties:
                path:
                  type: string
                  example: notepad.exe
                args:
                  type: array
                  items:
                    type: string
            example:
              path: notepad.exe
              args: []
      responses:
        "200":
          description: Process spawned
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  pid:
                    type: integer
        "502":
          description: Host refused or spawn failed
        "504":
          description: Host timed out

  /v1/sessions/{sessionId}/app/kill:
    post:
      operationId: killApp
      summary: Kill apps launched in this session
      description: |
        Terminate processes previously spawned via `/app/launch`.
        Omit `pid` to kill all session-launched apps; pass `pid` to
        kill one. Uses `taskkill /T /F` with protected-process guards.
      tags: [Apps]
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                pid:
                  type: integer
                  description: Optional PID from a prior launch
      responses:
        "200":
          description: Processes terminated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  killed:
                    type: array
                    items:
                      type: integer
        "502":
          description: Kill failed
        "504":
          description: Host timed out

  /v1/sessions/{sessionId}/input:
    post:
      operationId: sendInput
      summary: Send input events
      description: |
        Send mouse and keyboard events to the remote desktop. Events are
        executed in order on the host. Optional per-event `delay_ms` pauses
        between events.

        **Batching:** prefer a short ordered list when the next screens are
        predictable. Cap is 100 events per call (MCP `send_actions` uses the
        same endpoint with a tighter cap of 10 and an optional verification
        observe in the same MCP turn).

        **Rate limits:** each event counts individually against the per-plan
        input quota — a 10-event batch consumes 10 tokens, not 1. Responses
        use HTTP 429 with `Retry-After` when exceeded.

        **Safety:** the owner kill switch and session end interrupt an
        in-flight batch mid-sequence on the host.
      tags: [Input]
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [events]
              properties:
                events:
                  type: array
                  minItems: 1
                  maxItems: 100
                  items:
                    $ref: "#/components/schemas/InputEvent"
            example:
              events:
                - type: mouse_click
                  x: 400
                  y: 300
                  button: left
                - type: type_text
                  text: "Hello from the API"
                  delay_ms: 50
                - type: key_press
                  key: Enter
      responses:
        "200":
          description: Input accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  action_count:
                    type: integer
                    description: Session action counter after this call
        "400":
          description: Missing events or batch longer than 100
        "429":
          description: Input rate limit exceeded (counts each event in the batch)
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: rate_limited
                  message:
                    type: string
                  retry_after_ms:
                    type: integer
        "502":
          description: Failed to deliver input to the host

  /v1/keys:
    get:
      operationId: listKeys
      summary: List API keys
      description: List all active API keys (prefix only — full key is never stored).
      tags: [API Keys]
      security:
        - userJwt: []
      responses:
        "200":
          description: List of keys
          content:
            application/json:
              schema:
                type: object
                properties:
                  keys:
                    type: array
                    items:
                      $ref: "#/components/schemas/ApiKey"

    post:
      operationId: createKey
      summary: Create an API key
      description: |
        Create a new API key. The full key is returned once in the response
        and never stored — only a SHA-256 hash is persisted.
      tags: [API Keys]
      security:
        - userJwt: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  default: Default
                environment:
                  type: string
                  enum: [live, test]
                  default: live
      responses:
        "201":
          description: Key created (full key shown once)
          content:
            application/json:
              schema:
                type: object
                properties:
                  key:
                    $ref: "#/components/schemas/ApiKey"
                  raw_key:
                    type: string
                    example: "gw_live_sk_a1b2c3d4e5f6..."
                  warning:
                    type: string

  /v1/keys/{keyId}:
    delete:
      operationId: revokeKey
      summary: Revoke an API key
      description: Immediately revoke an API key. Existing sessions are not affected.
      tags: [API Keys]
      security:
        - userJwt: []
      parameters:
        - name: keyId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Key revoked

  /v1/webhooks:
    get:
      operationId: listWebhooks
      summary: List session webhooks
      description: |
        List outbound HTTPS endpoints that receive `session.started` and
        `session.ended` events. Secrets are never returned on list.
        Management auth: console JWT (not API keys).
      tags: [Webhooks]
      security:
        - userJwt: []
      responses:
        "200":
          description: Webhook list
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhooks:
                    type: array
                    items:
                      $ref: "#/components/schemas/ApiWebhook"
    post:
      operationId: createWebhook
      summary: Create a session webhook
      description: |
        Register a public HTTPS endpoint. Management uses the **console JWT**
        (same as `/v1/keys`) — API keys (`gw_*`) cannot create webhooks.

        The response includes `webhook.secret` once — verify deliveries with
        `X-Glasswarp-Signature` over `"{timestamp}.{body}"` and
        `X-Glasswarp-Timestamp` (reject skew > 5 minutes).

        URLs are DNS-resolved at create and again at every delivery; private /
        loopback / link-local / cloud-metadata addresses are rejected. Redirects
        are not followed. Max 5 webhooks per account.
      tags: [Webhooks]
      security:
        - userJwt: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  format: uri
                  example: "https://example.com/glasswarp/hooks"
                events:
                  type: array
                  items:
                    type: string
                    enum: [session.started, session.ended]
      responses:
        "201":
          description: Webhook created
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhook:
                    $ref: "#/components/schemas/ApiWebhookCreated"
                  warning:
                    type: string
        "400":
          description: Invalid URL, events, or webhook limit reached

  /v1/webhooks/{webhookId}:
    delete:
      operationId: deleteWebhook
      summary: Delete a session webhook
      tags: [Webhooks]
      security:
        - userJwt: []
      parameters:
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Webhook deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: boolean
