openapi: 3.0.3
info:
  title: CDX API
  version: 0.1.0-draft
  description: >
    External integration surface for third parties to connect to a user's CDX daemon
    (VM lifecycle, chats, workspace files). A chat call returns as soon as the turn starts
    and the answer arrives over server-sent events.
servers:
  - url: /
security:
  - ApiKeyAuth: []
  - BearerAuth: []
tags:
  - name: virtual-machine
    description: Start, stop, and check the status of the user's VM.
  - name: files
    description: Upload files into the user's workspace so a response can work with them.
  - name: chats
    description: >
      Send a plain user message, optionally against an existing chatId to continue it —
      same newChat / sendMessage split GraphQL exposes, collapsed into one call. The call
      returns as soon as the turn starts and the answer arrives over server-sent events.
paths:
  /api/v1/vm:
    get:
      tags: [virtual-machine]
      operationId: getVmStatus
      summary: Get the current VM status
      responses:
        '200':
          description: Current VM status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VmStatusResponse'
        '401':
          $ref: '#/components/responses/ErrorResponse'
  /api/v1/vm/start:
    post:
      tags: [virtual-machine]
      operationId: startVm
      summary: Start the user's VM
      responses:
        '202':
          description: Start requested; VM is transitioning to RUNNING.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VmStatusResponse'
        '401':
          $ref: '#/components/responses/ErrorResponse'
        '409':
          $ref: '#/components/responses/ErrorResponse'
  /api/v1/vm/stop:
    post:
      tags: [virtual-machine]
      operationId: stopVm
      summary: Stop the user's VM
      responses:
        '202':
          description: Stop requested; VM is transitioning to STOPPED.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VmStatusResponse'
        '401':
          $ref: '#/components/responses/ErrorResponse'
        '409':
          $ref: '#/components/responses/ErrorResponse'
  /api/v1/files:
    post:
      tags: [files]
      operationId: uploadFile
      summary: Upload a file into the user's workspace
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                destination:
                  type: string
                  description: Target directory in the workspace; the default upload directory when omitted.
      responses:
        '201':
          description: File stored.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiFile'
        '400':
          $ref: '#/components/responses/ErrorResponse'
        '401':
          $ref: '#/components/responses/ErrorResponse'
  /api/v1/chats:
    post:
      tags: [chats]
      operationId: sendMessage
      summary: Send a single user message
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApiChatRequest'
      responses:
        '202':
          description: Turn started; the answer is not finished yet.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiChatResponse'
        '400':
          $ref: '#/components/responses/ErrorResponse'
        '401':
          $ref: '#/components/responses/ErrorResponse'
        '404':
          $ref: '#/components/responses/ErrorResponse'
        '409':
          $ref: '#/components/responses/ErrorResponse'
  /api/v1/chats/{chatId}/events:
    get:
      tags: [chats]
      operationId: streamChat
      summary: Stream whatever the chat is answering
      description: >
        Server-sent events for the chat as a whole, every update of its latest assistant
        message. Stays open across turns; the message endpoint is the one that ends with
        a completed answer.
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Stream of chat updates.
          content:
            text/event-stream:
              schema:
                $ref: '#/components/schemas/ApiChatResponse'
        '401':
          $ref: '#/components/responses/ErrorResponse'
        '404':
          $ref: '#/components/responses/ErrorResponse'
  /api/v1/chats/{chatId}/messages/{messageId}/events:
    get:
      tags: [chats]
      operationId: streamMessage
      summary: Stream an answer as it is written
      description: >
        Server-sent events carrying the same ApiChatResponse the polling endpoint returns,
        one event per update of the assistant message. The stream ends with the completed
        answer.
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: messageId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Stream of answer updates.
          content:
            text/event-stream:
              schema:
                $ref: '#/components/schemas/ApiChatResponse'
        '401':
          $ref: '#/components/responses/ErrorResponse'
        '404':
          $ref: '#/components/responses/ErrorResponse'
  /api/v1/chats/{chatId}/messages/{messageId}:
    get:
      tags: [chats]
      operationId: getMessage
      summary: Retrieve an answer of a chat
      description: >
        Returns the same ApiChatResponse the POST returned, addressed by the chat and the
        assistant message id. An answer stays retrievable as long as its chat exists.
      parameters:
        - name: chatId
          in: path
          required: true
          description: The chat the answer belongs to, as returned in ApiChatResponse.chatId.
          schema:
            type: string
            format: uuid
        - name: messageId
          in: path
          required: true
          description: The assistant message id returned as ApiChatResponse.id.
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: The stored answer.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiChatResponse'
        '401':
          $ref: '#/components/responses/ErrorResponse'
        '404':
          $ref: '#/components/responses/ErrorResponse'
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-Api-Key
      description: Create and revoke user-bound keys in Settings → Communication channels → API. The complete key is shown once after creation.
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Same forwarded-bearer JWT auth as the rest of the app.
  responses:
    ErrorResponse:
      description: Error response.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
  schemas:
    ApiError:
      type: object
      properties:
        code:
          type: string
          example: NOT_IMPLEMENTED
        message:
          type: string
    VmStatusResponse:
      type: object
      description: >
        Scoped to the caller's own VM via the auth token — no owner identifier field, internal
        or otherwise, is exposed here.
      properties:
        status:
          type: string
          enum: [RUNNING, STOPPED, STARTING, STOPPING]
    ApiChatRequest:
      type: object
      required: [input]
      properties:
        chatId:
          type: string
          format: uuid
          description: >
            Omit to start a new chat (GraphQL newChat + sendMessage in one call). Pass an
            existing chatId to continue that chat instead — the message is appended and
            the model, fields, webSearchEnabled and jurisdictions of the existing chat are
            reused; this request's copies of those fields are ignored on continuation.
        model:
          $ref: '#/components/schemas/ApiModel'
        fields:
          type: array
          items:
            $ref: '#/components/schemas/ApiModelField'
          description: Same model-specific config knobs as GraphQL ApiChatRequest.fields.
        webSearchEnabled:
          type: boolean
          default: false
        input:
          type: string
          minLength: 1
          description: The user's message. This is the only supported content field.
        skillIds:
          type: array
          items:
            type: string
          description: Optional skill ID/name/fullName references to make available for this turn.
        agentId:
          type: string
          description: Optional agent ID/name/fullName to answer with; the default agent answers when omitted.
        filePaths:
          type: array
          items:
            type: string
          description: >
            Workspace paths to attach to the message, same as GraphQL sendMessage(filePaths).
            Upload through POST /api/v1/files first and pass the returned path here.
        jurisdictions:
          type: array
          items:
            $ref: '#/components/schemas/ApiJurisdiction'
      description: >
        chatId, model, fields, webSearchEnabled, and jurisdictions mirror the GraphQL
        newChat / sendMessage(chatId, ...) pair exactly — full parity with the
        interactive chat API, new-or-continue collapsed into one call. What's deliberately
        out of scope is conversation shaping: instructions, tools, previous_response_id,
        stream, and temperature are outside this contract — if present in the JSON body
        they are accepted but silently ignored, never applied. Only input (plus the config
        fields above) has effect.
    ApiModel:
      type: string
      description: Same enum as GraphQL ApiChatRequest.model / cdx-api-v2 ApiModel.
      enum:
        - GPT_5_4
        - GPT_5_4_MINI
        - GPT_5_4_NANO
        - GPT_5_5
        - GPT_5_6_SOL
        - GPT_5_6_TERRA
        - GPT_5_6_LUNA
    ApiJurisdiction:
      type: string
      description: Same enum as GraphQL ApiChatRequest.jurisdictions / cdx-api-v2 ApiJurisdiction.
      enum:
        - CZ
        - SK
        - EU
        - AT
        - NL
        - FR
    ApiModelField:
      type: object
      properties:
        key:
          type: string
        value:
          type: string
    ApiChatResponse:
      type: object
      description: >
        One answered turn of a chat. The id is the assistant message id; together with chatId
        it addresses the answer in GET /api/v1/chats/{chatId}/messages/{messageId} for as long as
        the chat exists.
      properties:
        id:
          type: string
          format: uuid
          description: The assistant message id.
        chatId:
          type: string
          format: uuid
          description: The chat this response belongs to — pass back as chatId to continue it.
        status:
          type: string
          enum: [in_progress, completed]
        createdAt:
          type: string
          format: date-time
        model:
          $ref: '#/components/schemas/ApiModel'
        text:
          type: string
          description: The answer written so far; complete once status is completed.
    ApiFile:
      type: object
      properties:
        name:
          type: string
        path:
          type: string
          description: Workspace path to pass as ApiChatRequest.filePaths.
        size:
          type: integer
          format: int64
