openapi: 3.1.0
info:
  title: Orchestra - cross-site API
  # API contract version, deliberately independent of the module release tag.
  # Bump only when the wire contract changes, not on every module release.
  version: "1.0"
  description: |
    The HTTP surface a remote **consumer** site uses to drive workflows on an
    Orchestra **server** site: start process instances, inspect them, read and
    write their variables, and signal parked tokens.

    This API maps one-to-one to the server's internal `OrchestraClientInterface`
    (the same contract `LocalOrchestraClient` implements in-process). The
    controllers are a thin transport: they carry scalars and plain arrays over
    HTTP, never entity objects, so a remote client reconstructs the result from
    JSON unchanged.

    Two things gate every call:

    - **OAuth2 (machine identity).** Each `/orchestra-api/*` call carries a
      bearer token obtained via the OAuth2 *client-credentials* grant. The token
      proves *which consumer* is calling.
    - **Tenant scoping (isolation).** Each consumer is bound to one tenant by an
      immutable field on its OAuth consumer. A tagged tenant resolver reads that
      binding, so every operation is scoped to the consumer's tenant: an
      instance, token or variable in another tenant is invisible and cannot be
      acted on. There is no cross-tenant call.

    Authorization is by the `access orchestra api` permission granted to the
    consumer's role, not by an OAuth scope. All responses are sent
    `Cache-Control: no-store`.

servers:
  - url: '{orchestra_base_url}'
    description: The Orchestra server site (scheme and host).
    variables:
      orchestra_base_url:
        default: https://orchestra.example

security:
  - oauth2: []

paths:
  /oauth/token:
    post:
      operationId: token
      summary: Obtain a client-credentials access token
      description: |
        Standard OAuth2 client-credentials grant, served by simple_oauth. The
        consumer authenticates with its `client_id` and `client_secret` (the
        secret is held in a Key module Key on the consumer, never in config) and
        receives a bearer token. The consumer's tenant binding and the
        `access orchestra api` permission then gate what that token can do.
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [grant_type, client_id, client_secret]
              properties:
                grant_type:
                  type: string
                  enum: [client_credentials]
                client_id:
                  type: string
                client_secret:
                  type: string
      responses:
        '200':
          description: A bearer token.
          content:
            application/json:
              schema:
                type: object
                properties:
                  access_token:
                    type: string
                  expires_in:
                    type: integer
                    example: 3600
                  token_type:
                    type: string
                    example: Bearer
        '401':
          description: Invalid client credentials.

  /orchestra-api/process/{definition_id}/start:
    post:
      operationId: startProcess
      summary: Start a new instance of a process definition
      description: |
        Creates an instance of the definition in the consumer's tenant and
        places a token on the start node. The definition must exist and be
        available in the tenant (shared, or scoped to it). Optional initial
        variables seed the instance.
      parameters:
        - $ref: '#/components/parameters/definition_id'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                variables:
                  type: object
                  additionalProperties: true
                  description: Initial process variables, keyed by name.
      responses:
        '201':
          description: The new instance was created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  instance_id: { type: string }
        '400':
          $ref: '#/components/responses/BadRequest'
        '422':
          $ref: '#/components/responses/Unprocessable'

  /orchestra-api/instances:
    get:
      operationId: listInstances
      summary: List instances in the consumer's tenant
      description: Lists instance summaries, newest first, optionally filtered.
      parameters:
        - name: definition_id
          in: query
          required: false
          schema: { type: string }
          description: Restrict to one process definition machine name.
        - name: status
          in: query
          required: false
          schema: { type: string }
          description: Restrict to one instance status (e.g. `running`, `completed`).
      responses:
        '200':
          description: The matching instance summaries.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/InstanceSummary' }

  /orchestra-api/instance/{instance_id}:
    get:
      operationId: getInstance
      summary: Read the state of one instance
      description: |
        Returns the instance with its live tokens. Returns 404 when the instance
        does not exist or belongs to another tenant -- the two are
        indistinguishable, so this is not a cross-tenant existence oracle.
      parameters:
        - $ref: '#/components/parameters/instance_id'
      responses:
        '200':
          description: The instance state with its tokens.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/InstanceState' }
        '404':
          $ref: '#/components/responses/NotFound'

  /orchestra-api/token/{token_id}/signal:
    post:
      operationId: signalToken
      summary: Signal a parked token to resume its process
      description: |
        The completion primitive: a human finished a task, an external handler
        reported back, a timer fired. Advances the token past its waiting node.
        The token must be parked and in the consumer's tenant.
      parameters:
        - $ref: '#/components/parameters/token_id'
      responses:
        '200':
          description: The token was signalled.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [signalled]
        '422':
          $ref: '#/components/responses/Unprocessable'

  /orchestra-api/instance/{instance_id}/variables:
    get:
      operationId: getVariables
      summary: Read an instance's variables
      description: |
        Returns the instance variables as a name to value map. An inaccessible
        instance (absent, or another tenant's) yields an empty map.
      parameters:
        - $ref: '#/components/parameters/instance_id'
      responses:
        '200':
          description: The variable values keyed by name.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /orchestra-api/instance/{instance_id}/variable/{name}:
    put:
      operationId: setVariable
      summary: Set (create or replace) one variable on an instance
      parameters:
        - $ref: '#/components/parameters/instance_id'
        - name: name
          in: path
          required: true
          schema: { type: string }
          description: The variable name.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [value]
              properties:
                value:
                  description: The value to store (any JSON type).
      responses:
        '200':
          description: The variable was set.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [set]
        '400':
          $ref: '#/components/responses/BadRequest'
        '422':
          $ref: '#/components/responses/Unprocessable'

components:
  securitySchemes:
    oauth2:
      type: oauth2
      flows:
        clientCredentials:
          tokenUrl: /oauth/token
          scopes: {}

  parameters:
    definition_id:
      name: definition_id
      in: path
      required: true
      schema: { type: string }
      description: A process definition machine name.
    instance_id:
      name: instance_id
      in: path
      required: true
      schema: { type: string }
    token_id:
      name: token_id
      in: path
      required: true
      schema: { type: string }

  schemas:
    InstanceSummary:
      type: object
      properties:
        id: { type: string }
        definition_id: { type: string }
        status: { type: string }
    TokenState:
      type: object
      properties:
        id: { type: string }
        node_id: { type: string }
        status: { type: string }
    InstanceState:
      type: object
      properties:
        id: { type: string }
        definition_id: { type: string }
        status: { type: string }
        tenant_id: { type: string }
        tokens:
          type: array
          items: { $ref: '#/components/schemas/TokenState' }
    Error:
      type: object
      properties:
        error: { type: string }

  responses:
    BadRequest:
      description: The request body is malformed (e.g. a missing or wrong-typed field).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: No such instance in the consumer's tenant.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unprocessable:
      description: |
        The request was well-formed but could not be carried out -- an unknown
        definition, a token that is not parked, an instance in another tenant.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
