openapi: 3.1.2
info:
  title: Oddessy Platform API
  version: 2026.07
  summary: Tenant-scoped fleet, release, enrolment, OTA, agent-credential and IoT integration API.
  description: |
    The browser-facing platform uses Cognito access tokens. Enrolled devices use
    their device ID and device secret. Personal agent tokens are currently
    accepted by the read-only MCP service, not by these platform mutation routes.

    Oddessy software does not replace PLC logic, safety relays, interlocks,
    emergency stops or other safety-rated controls.
  contact:
    name: Oddessy
    email: info@oddessy.io
    url: https://www.oddessy.io
  license:
    name: Proprietary
servers:
  - url: https://platform.oddessy.io/api
    description: Production
tags:
  - name: Service
  - name: Account
  - name: Devices
  - name: Enrolment
  - name: Releases
  - name: Agent credentials
  - name: IoT integrations
  - name: Monitoring
  - name: Webhooks
paths:
  /health:
    get:
      tags: [Service]
      operationId: getPlatformHealth
      summary: Check platform API health
      security: []
      responses:
        "200":
          description: Platform API is reachable.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Health"
  /account:
    get:
      tags: [Account]
      operationId: getAccount
      summary: Get the authenticated tenant user
      security:
        - CognitoAccessToken: []
      responses:
        "200":
          description: Authenticated user claims.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Account"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /devices:
    get:
      tags: [Devices]
      operationId: listDevices
      summary: List tenant devices
      description: Returns only devices owned by the authenticated tenant user.
      security:
        - CognitoAccessToken: []
      responses:
        "200":
          description: Tenant device collection.
          content:
            application/json:
              schema:
                type: object
                required: [devices, count]
                properties:
                  devices:
                    type: array
                    items:
                      $ref: "#/components/schemas/Device"
                  count:
                    type: integer
                    minimum: 0
        "401":
          $ref: "#/components/responses/Unauthorized"
  /devices/{deviceId}:
    patch:
      tags: [Devices]
      operationId: updateDevice
      summary: Update device display metadata
      security:
        - CognitoAccessToken: []
      parameters:
        - $ref: "#/components/parameters/DeviceId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              minProperties: 1
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 80
                location:
                  type: string
                  minLength: 1
                  maxLength: 120
      responses:
        "200":
          description: Updated device.
          content:
            application/json:
              schema:
                type: object
                required: [device]
                properties:
                  device:
                    $ref: "#/components/schemas/Device"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /devices/{deviceId}/ota:
    post:
      tags: [Devices, Releases]
      operationId: queueDeviceOta
      summary: Queue, roll back or cancel a Linux OTA deployment
      description: |
        Schedules an approved, checksum-verified Oddessy Edge release. The edge
        runtime receives the directive on its next authenticated heartbeat.
        Windows devices are updated with approved MSI releases instead.
      security:
        - CognitoAccessToken: []
      parameters:
        - $ref: "#/components/parameters/DeviceId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  additionalProperties: false
                  required: [action, release_key]
                  properties:
                    action:
                      const: install
                    release_key:
                      type: string
                      pattern: ^edge/.+\.tar\.gz$
                    force:
                      type: boolean
                - type: object
                  additionalProperties: false
                  required: [action]
                  properties:
                    action:
                      enum: [rollback, cancel]
      responses:
        "202":
          description: Deployment intent accepted.
          content:
            application/json:
              schema:
                type: object
                required: [device, ota]
                properties:
                  device:
                    $ref: "#/components/schemas/Device"
                  ota:
                    $ref: "#/components/schemas/OtaStatus"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /enrolments:
    post:
      tags: [Enrolment]
      operationId: createEnrolment
      summary: Create a single-use device enrolment
      description: The returned enrolment token is displayed once and expires in 30 minutes.
      security:
        - CognitoAccessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                name:
                  type: string
                  maxLength: 80
                location:
                  type: string
                  maxLength: 120
                hardware:
                  type: string
                  maxLength: 120
      responses:
        "201":
          description: One-time enrolment material.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Enrolment"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /device/bootstrap:
    post:
      tags: [Enrolment, Releases]
      operationId: bootstrapDevice
      summary: Resolve the approved installer for a one-time enrolment
      description: Returns a 15-minute signed URL and required SHA-256 without consuming the enrolment token.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [token]
              properties:
                token:
                  type: string
                  minLength: 32
      responses:
        "200":
          description: Platform-appropriate, checksum-published release.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Bootstrap"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /device/enrol:
    post:
      tags: [Enrolment]
      operationId: enrolDevice
      summary: Exchange a one-time enrolment token for device credentials
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [token]
              properties:
                token:
                  type: string
                  minLength: 32
                hostname:
                  type: string
                  maxLength: 120
                hardware:
                  type: string
                  maxLength: 120
                os:
                  type: string
                  maxLength: 120
                agent_version:
                  type: string
                  maxLength: 40
      responses:
        "201":
          description: Device identity. Store the device key securely; it is shown once.
          content:
            application/json:
              schema:
                type: object
                required: [device_id, device_key, heartbeat_interval_seconds, platform]
                properties:
                  device_id:
                    type: string
                  device_key:
                    type: string
                    writeOnly: true
                  heartbeat_interval_seconds:
                    type: integer
                  platform:
                    type: string
                    format: uri
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /device/heartbeat:
    post:
      tags: [Devices]
      operationId: submitDeviceHeartbeat
      summary: Submit device telemetry and receive an optional OTA directive
      security:
        - DeviceIdentity: []
          DeviceSecret: []
      parameters:
        - name: X-Oddessy-Device-Id
          in: header
          required: true
          schema:
            type: string
            pattern: ^[A-Za-z0-9][A-Za-z0-9_-]{5,79}$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                status:
                  type: string
                  maxLength: 30
                agent_version:
                  type: string
                  maxLength: 40
                telemetry:
                  type: object
                  maxProperties: 100
                  additionalProperties: true
                ota:
                  $ref: "#/components/schemas/OtaStatus"
                determinations:
                  type: array
                  maxItems: 10
                  items:
                    $ref: "#/components/schemas/DeterminationInput"
                camera_preview_request:
                  $ref: "#/components/schemas/CameraPreviewRequest"
                camera_preview:
                  $ref: "#/components/schemas/CameraPreviewReport"
      responses:
        "200":
          description: Heartbeat accepted; may contain a signed, short-lived OTA directive.
          content:
            application/json:
              schema:
                type: object
                required: [accepted, device_id, server_time]
                properties:
                  accepted:
                    const: true
                  device_id:
                    type: string
                  server_time:
                    type: string
                    format: date-time
                  monitors_evaluated:
                    type: integer
                    minimum: 0
                  ota:
                    $ref: "#/components/schemas/OtaDirective"
                  camera_preview_report_accepted:
                    type: boolean
                  camera_preview_upload:
                    $ref: "#/components/schemas/CameraPreviewUpload"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /monitors:
    get:
      tags: [Monitoring]
      operationId: listMonitors
      summary: List tenant alert monitors and current state
      security:
        - CognitoAccessToken: []
      responses:
        "200":
          description: Tenant monitor collection with current worst state.
          content:
            application/json:
              schema:
                type: object
                required: [monitors, count]
                properties:
                  monitors:
                    type: array
                    items:
                      $ref: "#/components/schemas/Monitor"
                  count:
                    type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags: [Monitoring]
      operationId: createMonitor
      summary: Create a device metric or VLM/VLA determination monitor
      description: Requires workspace administrator or operator access.
      security:
        - CognitoAccessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MonitorInput"
      responses:
        "201":
          description: Monitor created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  monitor:
                    $ref: "#/components/schemas/Monitor"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /monitors/{monitorId}:
    parameters:
      - name: monitorId
        in: path
        required: true
        schema:
          type: string
          pattern: ^mon_[0-9a-f]{20}$
    patch:
      tags: [Monitoring]
      operationId: updateMonitor
      summary: Update or pause a monitor
      security:
        - CognitoAccessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MonitorInput"
      responses:
        "200":
          description: Monitor updated.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
    delete:
      tags: [Monitoring]
      operationId: deleteMonitor
      summary: Delete a monitor while retaining alert event history
      security:
        - CognitoAccessToken: []
      responses:
        "200":
          description: Monitor deleted.
        "401":
          $ref: "#/components/responses/Unauthorized"
  /alerts:
    get:
      tags: [Monitoring]
      operationId: listAlerts
      summary: List active alert states and retained transition history
      security:
        - CognitoAccessToken: []
      responses:
        "200":
          description: Active warning/critical states and up to 250 recent transitions.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AlertCollection"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /determinations:
    get:
      tags: [Monitoring]
      operationId: listDeterminations
      summary: List recent retained VLM/VLA determinations
      security:
        - CognitoAccessToken: []
      responses:
        "200":
          description: Up to 100 recent determinations retained for 90 days.
          content:
            application/json:
              schema:
                type: object
                properties:
                  determinations:
                    type: array
                    items:
                      $ref: "#/components/schemas/Determination"
                  count:
                    type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags: [Monitoring]
      operationId: createDetermination
      summary: Submit a normalized VLM/VLA determination for monitor evaluation
      description: Requires workspace administrator or operator access. Stable source event IDs are de-duplicated.
      security:
        - CognitoAccessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DeterminationInput"
      responses:
        "201":
          description: Determination retained and matching monitors evaluated.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /releases:
    get:
      tags: [Releases]
      operationId: listReleases
      summary: List approved release metadata
      security:
        - CognitoAccessToken: []
      responses:
        "200":
          description: Approved release metadata. Download URLs are not returned here.
          content:
            application/json:
              schema:
                type: object
                required: [releases]
                properties:
                  releases:
                    type: array
                    items:
                      $ref: "#/components/schemas/Release"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /download:
    get:
      tags: [Releases]
      operationId: createReleaseDownload
      summary: Create a short-lived release download
      security:
        - CognitoAccessToken: []
      parameters:
        - name: key
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: A ten-minute private download URL.
          content:
            application/json:
              schema:
                type: object
                required: [url, expires_in_seconds, filename]
                properties:
                  url:
                    type: string
                    format: uri
                  expires_in_seconds:
                    const: 600
                  filename:
                    type: string
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /integrations:
    get:
      tags: [IoT integrations]
      operationId: listIntegrations
      summary: List tenant IoT integrations
      security:
        - CognitoAccessToken: []
      responses:
        "200":
          description: Matter, TTN and generic webhook connections without secrets.
          content:
            application/json:
              schema:
                type: object
                required: [integrations, count]
                properties:
                  integrations:
                    type: array
                    items:
                      $ref: "#/components/schemas/Integration"
                  count:
                    type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags: [IoT integrations]
      operationId: createIntegration
      summary: Create an integration and shown-once webhook credential
      security:
        - CognitoAccessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [type, name]
              properties:
                type:
                  enum: [matter, ttn, webhook]
                provider:
                  description: Guided connector preset. The transport remains defined by type.
                  enum: [matter, the-things-stack, openclaw, llm-agent, n8n, zapier, make, generic]
                name:
                  type: string
                  maxLength: 80
                application_id:
                  type: string
                  maxLength: 120
      responses:
        "201":
          description: Integration plus credential. Copy the secret now.
          content:
            application/json:
              schema:
                type: object
                required: [integration, credential]
                properties:
                  integration:
                    $ref: "#/components/schemas/Integration"
                  credential:
                    $ref: "#/components/schemas/IntegrationCredential"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /integrations/{integrationId}:
    patch:
      tags: [IoT integrations]
      operationId: updateIntegration
      summary: Rename, pause or enable an integration
      security:
        - CognitoAccessToken: []
      parameters:
        - $ref: "#/components/parameters/IntegrationId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              minProperties: 1
              properties:
                name:
                  type: string
                  maxLength: 80
                enabled:
                  type: boolean
                application_id:
                  type: string
                  maxLength: 120
      responses:
        "200":
          description: Updated integration.
          content:
            application/json:
              schema:
                type: object
                properties:
                  integration:
                    $ref: "#/components/schemas/Integration"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
    delete:
      tags: [IoT integrations]
      operationId: deleteIntegration
      summary: Delete an integration and reject future events
      security:
        - CognitoAccessToken: []
      parameters:
        - $ref: "#/components/parameters/IntegrationId"
      responses:
        "200":
          description: Integration deleted.
        "401":
          $ref: "#/components/responses/Unauthorized"
  /integrations/{integrationId}/rotate-secret:
    post:
      tags: [IoT integrations]
      operationId: rotateIntegrationSecret
      summary: Rotate and show a new webhook credential once
      security:
        - CognitoAccessToken: []
      parameters:
        - $ref: "#/components/parameters/IntegrationId"
      responses:
        "200":
          description: Rotated integration and one-time credential.
          content:
            application/json:
              schema:
                type: object
                properties:
                  integration:
                    $ref: "#/components/schemas/Integration"
                  credential:
                    $ref: "#/components/schemas/IntegrationCredential"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /integrations/{integrationId}/events:
    get:
      tags: [IoT integrations]
      operationId: listIntegrationEvents
      summary: List recent normalized events for a tenant-owned integration
      security:
        - CognitoAccessToken: []
      parameters:
        - $ref: "#/components/parameters/IntegrationId"
      responses:
        "200":
          description: Up to 100 recent events with 30-day storage retention.
          content:
            application/json:
              schema:
                type: object
                properties:
                  events:
                    type: array
                    items:
                      $ref: "#/components/schemas/IoTEvent"
                  count:
                    type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"
  /iot/devices:
    get:
      tags: [IoT integrations]
      operationId: listIoTDevices
      summary: List endpoints discovered from authenticated IoT events
      security:
        - CognitoAccessToken: []
      responses:
        "200":
          description: Tenant-scoped discovered IoT devices.
          content:
            application/json:
              schema:
                type: object
                properties:
                  devices:
                    type: array
                    items:
                      $ref: "#/components/schemas/IoTDevice"
                  count:
                    type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"
  /webhooks/{integrationId}:
    post:
      tags: [Webhooks]
      operationId: ingestGenericWebhook
      summary: Ingest a signed generic or Matter-edge JSON event
      security:
        - WebhookSecret: []
      parameters:
        - $ref: "#/components/parameters/IntegrationId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "202":
          description: Event authenticated, normalized and accepted.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /webhooks/{integrationId}/uplink:
    post:
      tags: [Webhooks]
      operationId: ingestTtnUplink
      summary: Ingest a The Things Stack uplink webhook
      security:
        - WebhookSecret: []
      parameters:
        - $ref: "#/components/parameters/IntegrationId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [end_device_ids, uplink_message]
              additionalProperties: true
      responses:
        "202":
          description: Uplink authenticated, normalized and accepted.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /tokens:
    get:
      tags: [Agent credentials]
      operationId: listAgentTokens
      summary: List personal agent-token metadata
      security:
        - CognitoAccessToken: []
      responses:
        "200":
          description: Token metadata only; raw token values are never returned.
          content:
            application/json:
              schema:
                type: object
                required: [tokens, count]
                properties:
                  tokens:
                    type: array
                    items:
                      $ref: "#/components/schemas/AgentToken"
                  count:
                    type: integer
                    minimum: 0
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags: [Agent credentials]
      operationId: createAgentToken
      summary: Create an expiring read-only MCP token
      description: The raw `odd_pat_…` value is returned once and stored only as a SHA-256 digest.
      security:
        - CognitoAccessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                name:
                  type: string
                  maxLength: 80
                expires_days:
                  type: integer
                  enum: [7, 30, 90, 180, 365]
                  default: 90
      responses:
        "201":
          description: Newly created token; copy `token` now.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/AgentToken"
                  - type: object
                    required: [token, credential_type]
                    properties:
                      token:
                        type: string
                        writeOnly: true
                        pattern: ^odd_pat_
                      credential_type:
                        const: bearer
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /tokens/{tokenId}:
    delete:
      tags: [Agent credentials]
      operationId: revokeAgentToken
      summary: Revoke a personal agent token immediately
      security:
        - CognitoAccessToken: []
      parameters:
        - name: tokenId
          in: path
          required: true
          schema:
            type: string
            pattern: ^pat_[0-9a-f]{16}$
      responses:
        "200":
          description: Token revoked.
          content:
            application/json:
              schema:
                type: object
                required: [revoked, token_id]
                properties:
                  revoked:
                    const: true
                  token_id:
                    type: string
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /invite-requests:
    post:
      tags: [Account]
      operationId: requestPlatformInvite
      summary: Submit a structured Platform access request
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, email, company, company_size, intent]
              properties:
                name: {type: string, maxLength: 120}
                email: {type: string, format: email, maxLength: 320}
                role: {type: string, maxLength: 120}
                company: {type: string, maxLength: 160}
                company_size:
                  type: string
                  enum: [1-10, 11-50, 51-250, 251-1000, 1000+]
                industry: {type: string, maxLength: 120}
                deployment_stage: {type: string, enum: [evaluation, pilot, production]}
                expected_sites: {type: string, maxLength: 40}
                expected_devices: {type: string, maxLength: 40}
                systems: {type: string, maxLength: 600}
                preferred_region: {type: string, maxLength: 80}
                intent: {type: string, maxLength: 1200}
      responses:
        "202":
          description: Request accepted for review.
components:
  securitySchemes:
    CognitoAccessToken:
      type: oauth2
      description: Cognito Authorization Code + PKCE access token used by platform users.
      flows:
        authorizationCode:
          authorizationUrl: https://oddessy-platform-585548213337.auth.us-east-1.amazoncognito.com/oauth2/authorize
          tokenUrl: https://oddessy-platform-585548213337.auth.us-east-1.amazoncognito.com/oauth2/token
          scopes:
            openid: Identify the signed-in platform user.
            email: Read the signed-in user's email claim.
            profile: Read the signed-in user's profile claims.
    DeviceIdentity:
      type: apiKey
      in: header
      name: X-Oddessy-Device-Id
    DeviceSecret:
      type: http
      scheme: bearer
      bearerFormat: "Device <device-secret>"
      description: "Send `Authorization: Device <device-secret>`."
    WebhookSecret:
      type: http
      scheme: bearer
      bearerFormat: odd_wh_...
      description: Shown-once integration secret. Basic authentication is also accepted for The Things Stack.
  parameters:
    DeviceId:
      name: deviceId
      in: path
      required: true
      schema:
        type: string
        pattern: ^[A-Za-z0-9][A-Za-z0-9_-]{5,79}$
    IntegrationId:
      name: integrationId
      in: path
      required: true
      schema:
        type: string
        pattern: ^int_[0-9a-f]{20}$
  responses:
    BadRequest:
      description: Invalid request.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Valid credentials are required.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: Resource not found in the authenticated tenant.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
  schemas:
    Health:
      type: object
      required: [status, service]
      properties:
        status:
          const: healthy
        service:
          const: oddessy-platform
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
    Account:
      type: object
      required: [sub, email, name, groups]
      properties:
        sub:
          type: string
        email:
          type: string
          format: email
        name:
          type: string
        groups:
          type: string
    Device:
      type: object
      required: [owner_id, device_id, name, location, hardware, status]
      properties:
        owner_id:
          type: string
          description: Tenant owner identifier.
        device_id:
          type: string
        name:
          type: string
        location:
          type: string
        hardware:
          type: string
        os:
          type: string
        agent_version:
          type: string
        status:
          type: string
        sample:
          type: boolean
        last_seen:
          type: string
          format: date-time
        telemetry:
          type: object
          additionalProperties: true
        desired_release:
          type: object
          additionalProperties: true
        ota_status:
          $ref: "#/components/schemas/OtaStatus"
        camera_preview:
          $ref: "#/components/schemas/CameraPreviewView"
    Enrolment:
      type: object
      required: [device, device_platform, enrolment_token, expires_at, install_command]
      properties:
        device:
          $ref: "#/components/schemas/Device"
        device_platform:
          enum: [linux, windows]
        enrolment_token:
          type: string
          writeOnly: true
        expires_at:
          type: string
          format: date-time
        install_command:
          type: string
    OtaStatus:
      type: object
      properties:
        deployment_id:
          type: string
          pattern: ^ota-[0-9a-f]{32}$
        status:
          enum:
            - scheduled
            - downloading
            - queued
            - installing
            - verifying
            - succeeded
            - failed
            - rolling_back
            - rolled_back
            - cancelled
        progress:
          type: integer
          minimum: 0
          maximum: 100
        message:
          type: string
          maxLength: 300
        current_version:
          type: string
        target_version:
          type: string
        previous_version:
          type: string
        action:
          enum: [install, rollback, cancel]
        automatic_rollback:
          type: boolean
        reported_at:
          type: string
          format: date-time
    OtaDirective:
      type: object
      required: [schema, deployment_id, action, version]
      properties:
        schema:
          const: oddessy.ota.v1
        deployment_id:
          type: string
        action:
          enum: [install, rollback]
        version:
          type: string
        release_key:
          type: string
        sha256:
          type: string
          pattern: ^[0-9a-f]{64}$
        size_bytes:
          type: integer
        url:
          type: string
          format: uri
        expires_in_seconds:
          type: integer
    Release:
      type: object
      required: [key, name, version, product, sha256, size_bytes, updated_at]
      properties:
        key:
          type: string
        name:
          type: string
        version:
          type: string
        product:
          enum: [android, edge, windows]
        sha256:
          type: string
        size_bytes:
          type: integer
        updated_at:
          type: string
          format: date-time
    Bootstrap:
      type: object
      required: [schema, device_id, platform, release]
      properties:
        schema:
          const: oddessy.bootstrap.v1
        device_id:
          type: string
        platform:
          enum: [edge, windows]
        release:
          type: object
          required: [key, filename, name, version, sha256, size_bytes, url, expires_in_seconds]
          properties:
            key:
              type: string
            filename:
              type: string
            name:
              type: string
            version:
              type: string
            sha256:
              type: string
              pattern: ^[0-9a-f]{64}$
            size_bytes:
              type: integer
            url:
              type: string
              format: uri
            expires_in_seconds:
              const: 900
    Integration:
      type: object
      required: [integration_id, name, type, provider, enabled, event_count, webhook_url, uplink_url]
      properties:
        integration_id:
          type: string
          pattern: ^int_[0-9a-f]{20}$
        name:
          type: string
        type:
          enum: [matter, ttn, webhook]
        provider:
          enum: [matter, the-things-stack, openclaw, llm-agent, n8n, zapier, make, generic]
        enabled:
          type: boolean
        application_id:
          type: string
        event_count:
          type: integer
        last_event_at:
          type: [string, "null"]
          format: date-time
        credential_hint:
          type: string
        webhook_url:
          type: string
          format: uri
        uplink_url:
          type: string
          format: uri
        connection_model:
          enum: [local-edge-bridge, the-things-stack-webhook, openclaw-mcp-and-webhook, mcp-and-signed-webhook, no-code-signed-webhook, signed-json-webhook]
    IntegrationCredential:
      type: object
      required: [secret, authorization_header, webhook_url, uplink_url]
      properties:
        secret:
          type: string
          writeOnly: true
          pattern: ^odd_wh_
        authorization_header:
          type: string
          writeOnly: true
        webhook_url:
          type: string
          format: uri
        uplink_url:
          type: string
          format: uri
    IoTDevice:
      type: object
      required: [iot_device_id, source_device_id, integration_id, integration_type, last_event_at]
      properties:
        iot_device_id:
          type: string
        source_device_id:
          type: string
        integration_id:
          type: string
        integration_type:
          enum: [matter, ttn, webhook]
        first_seen_at:
          type: string
          format: date-time
        last_event_at:
          type: string
          format: date-time
        last_event_type:
          type: string
        last_payload:
          type: object
          additionalProperties: true
    IoTEvent:
      type: object
      required: [integration_id, event_id, event_type, source_device_id, received_at]
      properties:
        integration_id:
          type: string
        event_id:
          type: string
        event_type:
          type: string
        source_device_id:
          type: string
        received_at:
          type: string
          format: date-time
        payload:
          type: object
          additionalProperties: true
    MonitorInput:
      type: object
      required: [name, monitor_type]
      properties:
        name:
          type: string
          maxLength: 100
        monitor_type:
          enum: [metric, determination]
        enabled:
          type: boolean
          default: true
        target_id:
          type: string
          description: A device or camera ID, or * for all matching subjects.
        metric:
          enum: [cpu_percent, memory_percent, disk_percent, temperature_c]
        direction:
          enum: [above, below]
        warning_threshold:
          type: number
        critical_threshold:
          type: number
        workload:
          enum: [vlm, vla]
        determination_field:
          enum: [confidence, severity, event_type, safety_state, rule_matched, recommended_action]
        operator:
          enum: [above, below, equals, severity_at_least]
        warning_value:
          type: [string, number, boolean]
        critical_value:
          type: [string, number, boolean]
    Monitor:
      allOf:
        - $ref: "#/components/schemas/MonitorInput"
        - type: object
          required: [monitor_id, state, active_subjects]
          properties:
            monitor_id:
              type: string
              pattern: ^mon_[0-9a-f]{20}$
            state:
              enum: [ok, warning, critical]
            active_subjects:
              type: integer
            last_evaluated_at:
              type: string
              format: date-time
    CameraPreviewRequest:
      type: object
      required: [sha256, size_bytes, captured_at]
      properties:
        sha256:
          type: string
          pattern: ^[0-9a-f]{64}$
        size_bytes:
          type: integer
          minimum: 1
          maximum: 2000000
        captured_at:
          type: string
          format: date-time
    CameraPreviewReport:
      allOf:
        - $ref: "#/components/schemas/CameraPreviewRequest"
        - type: object
          required: [key, uploaded_at]
          properties:
            key:
              type: string
            uploaded_at:
              type: string
              format: date-time
    CameraPreviewUpload:
      allOf:
        - $ref: "#/components/schemas/CameraPreviewReport"
        - type: object
          required: [schema, url, headers, expires_in_seconds]
          properties:
            schema:
              const: oddessy.camera-preview-upload.v1
            url:
              type: string
              format: uri
            headers:
              type: object
              additionalProperties:
                type: string
            expires_in_seconds:
              const: 300
    CameraPreviewView:
      type: object
      required: [url, captured_at, uploaded_at, expires_in_seconds]
      properties:
        url:
          type: string
          format: uri
          readOnly: true
        captured_at:
          type: string
          format: date-time
        uploaded_at:
          type: string
          format: date-time
        size_bytes:
          type: integer
        sha256:
          type: string
          pattern: ^[0-9a-f]{64}$
        expires_in_seconds:
          const: 600
    DeterminationInput:
      type: object
      required: [workload, result]
      properties:
        workload:
          enum: [vlm, vla]
        device_id:
          type: string
        camera_id:
          type: string
        source_event_id:
          type: string
          maxLength: 200
        observed_at:
          type: string
          format: date-time
        source:
          type: string
          maxLength: 80
        result:
          type: object
          properties:
            confidence:
              type: number
              minimum: 0
              maximum: 1
            severity:
              enum: [low, medium, high, critical]
            event_type:
              type: string
            safety_state:
              type: string
            rule_matched:
              type: boolean
            recommended_action:
              type: string
    Determination:
      allOf:
        - $ref: "#/components/schemas/DeterminationInput"
        - type: object
          properties:
            determination_id:
              type: string
            subject_id:
              type: string
            confidence:
              type: number
            severity:
              type: string
            event_type:
              type: string
            safety_state:
              type: string
            rule_matched:
              type: boolean
            recommended_action:
              type: string
    AlertCollection:
      type: object
      required: [active, events, counts]
      properties:
        active:
          type: array
          items:
            type: object
            additionalProperties: true
        events:
          type: array
          items:
            type: object
            additionalProperties: true
        counts:
          type: object
          required: [warning, critical]
          properties:
            warning:
              type: integer
            critical:
              type: integer
    AgentToken:
      type: object
      required: [token_id, name, token_hint, scopes, created_at, expires_at, status]
      properties:
        token_id:
          type: string
        name:
          type: string
        token_hint:
          type: string
        scopes:
          type: array
          items:
            enum:
              - oddessy.read
              - devices.read
              - releases.read
              - models.read
              - status.read
              - integrations.read
              - iot.read
        created_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
        status:
          enum: [active, expired]
