asyncapi: 3.1.0
info:
  title: pilet device agent
  version: 0.4.0
  description: >
    MQTT messaging contract for the pilet device agent: an over-the-air self-update
    command from the cloud, periodic device-state reporting (the kubelet node-status
    analog), per-container status (the kubelet pod-status analog), and the desired
    container channel (the kubelet LIST + WATCH analog) the device reconciles toward.
    Topics are namespaced pilet/v1/{deviceId}/{subresource}; the version segment allows
    schema-version routing across a staggered-OTA fleet.
servers:
  mosquitto:
    host: '192.168.1.32:1883'
    protocol: mqtt
    protocolVersion: '5.0'
    description: Eclipse Mosquitto broker on the cloud host.
channels:
  updateCommand:
    address: 'pilet/v1/{deviceId}/update'
    title: Update command
    description: Cloud-issued OTA update command for a single device.
    parameters:
      deviceId:
        description: Target device id (e.g. pi-node-01).
    messages:
      updateCommand:
        $ref: '#/components/messages/UpdateCommand'
  nodeStatus:
    address: 'pilet/v1/{deviceId}/status'
    title: Device status
    description: >
      Device-state snapshot — node info, capacity/allocatable, health conditions,
      image inventory, addresses (the kubelet node-status analog).


      Cadence: the device recomputes the snapshot every STATE_INTERVAL (default 30s,
      env PILET_STATE_INTERVAL) and
      publishes only when it changed (compared via a fingerprint that excludes the
      volatile lastHeartbeatTime), or when STATE_REPORT_FLOOR has elapsed (default
       5 min, env PILET_STATE_REPORT_FLOOR).


      Retained (QoS 1) so a late subscriber gets the last-known state on subscribe.
    parameters:
      deviceId:
        description: Reporting device id.
    messages:
      deviceState:
        $ref: '#/components/messages/DeviceState'
  containerStatus:
    address: 'pilet/v1/{deviceId}/containers/{name}'
    title: Container status
    description: >
      Per-container status (the kubelet pod-status shape, one Docker container).
      One retained message per container; event-driven off the Docker event stream
      (start / stop / die), with a CONTAINERS_REPORT_FLOOR (default 300s / 5 min, env
      PILET_CONTAINERS_REPORT_FLOOR) safety re-sync. An empty retained payload is a
      tombstone, published when the container is removed.
    parameters:
      deviceId:
        description: Reporting device id.
      name:
        description: Container name.
    messages:
      containerState:
        $ref: '#/components/messages/ContainerState'
  specWatch:
    address: 'pilet/v1/{deviceId}/spec/watch'
    title: Desired container watch
    description: >
      The desired WATCH stream (the kubelet watch analog): the backend pushes one
      {type, object} envelope per change (ADDED / MODIFIED / DELETED). Not retained —
      the LIST owns the snapshot; the device re-LISTs periodically to heal any gap.
    parameters:
      deviceId:
        description: Target device id.
    messages:
      watchEvent:
        $ref: '#/components/messages/WatchEvent'
  specListRequest:
    address: 'pilet/v1/{deviceId}/spec/list/request'
    title: Desired container LIST request
    description: >
      The LIST request (the kubelet list analog): on (re)connect — and on every
      periodic resync — the device asks the backend for its full desired set via an
      MQTT5 request/response (ResponseTopic + CorrelationData). The payload is empty;
      the device id in the topic scopes the query (the fieldSelector analog).
    parameters:
      deviceId:
        description: Requesting device id.
    messages:
      listRequest:
        $ref: '#/components/messages/ListRequest'
  specListResponse:
    address: 'pilet/v1/{deviceId}/spec/list/response'
    title: Desired container LIST response
    description: >
      The LIST response: the backend replies (echoing the request's CorrelationData)
      with the device's full desired set — an atomic, known-complete snapshot.
    parameters:
      deviceId:
        description: Target device id.
    messages:
      containerList:
        $ref: '#/components/messages/ContainerList'
operations:
  receiveUpdateCommand:
    action: receive
    channel:
      $ref: '#/channels/updateCommand'
    summary: Device receives an OTA update command from the cloud.
    bindings:
      mqtt:
        qos: 1
        retain: false
        bindingVersion: 0.2.0
  sendNodeStatus:
    action: send
    channel:
      $ref: '#/channels/nodeStatus'
    summary: Device publishes its state snapshot (retained, on change / report floor).
    bindings:
      mqtt:
        qos: 1
        retain: true
        bindingVersion: 0.2.0
  sendContainerStatus:
    action: send
    channel:
      $ref: '#/channels/containerStatus'
    summary: Device publishes one container's status (retained); empty payload tombstones it on removal.
    bindings:
      mqtt:
        qos: 1
        retain: true
        bindingVersion: 0.2.0
  receiveSpecWatch:
    action: receive
    channel:
      $ref: '#/channels/specWatch'
    summary: Device receives desired-container change events to reconcile.
    bindings:
      mqtt:
        qos: 1
        retain: false
        bindingVersion: 0.2.0
  sendSpecListRequest:
    action: send
    channel:
      $ref: '#/channels/specListRequest'
    summary: Device requests its full desired set (MQTT5 request/response).
    bindings:
      mqtt:
        qos: 1
        retain: false
        bindingVersion: 0.2.0
  receiveSpecListResponse:
    action: receive
    channel:
      $ref: '#/channels/specListResponse'
    summary: Device receives the desired-set snapshot in reply to its LIST request.
    bindings:
      mqtt:
        qos: 1
        retain: false
        bindingVersion: 0.2.0
components:
  messages:
    UpdateCommand:
      name: UpdateCommand
      title: Update command
      contentType: application/json
      payload:
        $ref: '#/components/schemas/UpdateCommandPayload'
    DeviceState:
      name: DeviceState
      title: Device state
      contentType: application/json
      payload:
        $ref: '#/components/schemas/DeviceStatePayload'
    ContainerState:
      name: ContainerState
      title: Container state
      contentType: application/json
      payload:
        $ref: '#/components/schemas/ContainerStatePayload'
    WatchEvent:
      name: WatchEvent
      title: Desired container watch event
      contentType: application/json
      payload:
        $ref: '#/components/schemas/WatchEventPayload'
    ListRequest:
      name: ListRequest
      title: Desired container LIST request
      contentType: application/json
      payload:
        type: 'null'
        description: Empty — the device id in the topic scopes the query.
    ContainerList:
      name: ContainerList
      title: Desired container LIST response
      contentType: application/json
      payload:
        $ref: '#/components/schemas/ContainerListPayload'
  schemas:
    UpdateCommandPayload:
      type: object
      additionalProperties: false
      required:
        - version
        - url
        - sha256
      properties:
        version:
          type: string
          description: Target version; ignored if not newer than the running one.
          pattern: ^\d+\.\d+\.\d+$
          examples:
            - 0.2.0
        url:
          type: string
          format: uri
          description: Where the device pulls the wheel from (your cloud).
          examples:
            - 'http://192.168.1.32:8000/wheels/pilet-0.2.0-py3-none-any.whl'
        sha256:
          type: string
          description: 'Lowercase hex SHA-256 of the wheel, verified before install.'
          pattern: '^[a-f0-9]{64}$'
        force:
          type: boolean
          default: false
          description: Reinstall even if the target version equals the running one.
    DeviceStatePayload:
      type: object
      description: Device-state envelope (k8s Node-status shape).
      additionalProperties: false
      required:
        - apiVersion
        - kind
        - metadata
        - status
      properties:
        apiVersion:
          type: string
          const: pilet/v1
        kind:
          type: string
          const: Device
        metadata:
          $ref: '#/components/schemas/Metadata'
        status:
          $ref: '#/components/schemas/DeviceStatus'
    Metadata:
      type: object
      additionalProperties: false
      required:
        - name
      properties:
        name:
          type: string
          description: Device id.
          examples:
            - pi-node-01
    DeviceStatus:
      type: object
      additionalProperties: false
      required:
        - nodeInfo
        - capacity
        - allocatable
        - conditions
        - images
        - addresses
      properties:
        nodeInfo:
          $ref: '#/components/schemas/NodeInfo'
        capacity:
          $ref: '#/components/schemas/Resources'
        allocatable:
          $ref: '#/components/schemas/Resources'
        conditions:
          type: array
          items:
            $ref: '#/components/schemas/Condition'
        images:
          type: array
          items:
            $ref: '#/components/schemas/Image'
        addresses:
          type: array
          items:
            $ref: '#/components/schemas/Address'
    NodeInfo:
      type: object
      additionalProperties: false
      required:
        - machineID
        - model
        - kernelVersion
        - osImage
        - operatingSystem
        - architecture
        - containerRuntimeVersion
        - piletVersion
      properties:
        machineID:
          type: string
          description: Pi hardware serial (stable per board; survives SD-card cloning).
          examples:
            - 100000003d1f9a7c
        model:
          type: string
          examples:
            - Raspberry Pi 4 Model B Rev 1.4
        kernelVersion:
          type: string
          examples:
            - 6.8.0-rpi8-rpi-2712
        osImage:
          type: string
          examples:
            - Debian GNU/Linux 12 (bookworm)
        operatingSystem:
          type: string
          examples:
            - linux
        architecture:
          type: string
          examples:
            - aarch64
        containerRuntimeVersion:
          type: string
          examples:
            - 'docker://27.1.1'
        piletVersion:
          type: string
          examples:
            - 0.1.1
    Resources:
      type: object
      description: A resource quantity set (k8s quantities; counts as ints).
      additionalProperties: false
      required:
        - cpu
        - memory
        - ephemeralStorage
        - maxContainers
      properties:
        cpu:
          type: string
          description: CPU cores.
          examples:
            - '4'
        memory:
          type: string
          examples:
            - 7820Mi
        ephemeralStorage:
          type: string
          examples:
            - 29900Mi
        maxContainers:
          type: integer
          description: Per-device deploy cap (scheduler budget); the kubelet pods analog.
          examples:
            - 20
    Condition:
      type: object
      description: A health condition (k8s node-condition shape).
      additionalProperties: false
      required:
        - type
        - status
        - reason
        - message
        - lastHeartbeatTime
        - lastTransitionTime
      properties:
        type:
          type: string
          enum:
            - MemoryPressure
            - DiskPressure
            - Undervoltage
            - ThermalThrottled
        status:
          type: string
          description: '"True" = problem present, "False" = healthy.'
          enum:
            - 'True'
            - 'False'
        reason:
          type: string
        message:
          type: string
        lastHeartbeatTime:
          type: string
          format: date-time
          description: Last time the condition was re-evaluated.
        lastTransitionTime:
          type: string
          format: date-time
          description: Last time the status changed.
    Image:
      type: object
      additionalProperties: false
      required:
        - names
        - sizeBytes
      properties:
        names:
          type: array
          description: RepoTags; empty for a dangling image.
          items:
            type: string
          examples:
            - - nginx:alpine
        sizeBytes:
          type: integer
          examples:
            - 20512000
    Address:
      type: object
      additionalProperties: false
      required:
        - type
        - address
      properties:
        type:
          type: string
          enum:
            - InternalIP
            - Hostname
        address:
          type: string
          examples:
            - 192.168.1.32
    ContainerStatePayload:
      type: object
      description: Per-container envelope (k8s Pod-status shape, one Docker container).
      additionalProperties: false
      required:
        - apiVersion
        - kind
        - metadata
        - status
      properties:
        apiVersion:
          type: string
          const: pilet/v1
        kind:
          type: string
          const: Container
        metadata:
          $ref: '#/components/schemas/Metadata'
        status:
          $ref: '#/components/schemas/ContainerStatusBlock'
    ContainerStatusBlock:
      type: object
      additionalProperties: false
      required:
        - phase
        - conditions
        - containerStatuses
      properties:
        phase:
          type: string
          enum:
            - Pending
            - Running
            - Succeeded
            - Failed
            - Unknown
        conditions:
          type: array
          items:
            $ref: '#/components/schemas/PodCondition'
        containerStatuses:
          type: array
          description: Always length 1 — one Docker container per message.
          items:
            $ref: '#/components/schemas/ContainerStatusEntry'
    PodCondition:
      type: object
      additionalProperties: false
      required:
        - type
        - status
      properties:
        type:
          type: string
          examples:
            - Ready
        status:
          type: string
          enum:
            - 'True'
            - 'False'
    ContainerStatusEntry:
      type: object
      description: One container's runtime status (k8s ContainerStatus shape).
      additionalProperties: false
      required:
        - name
        - image
        - imageID
        - containerID
        - ready
        - started
        - restartCount
        - state
      properties:
        name:
          type: string
        image:
          type: string
          examples:
            - nginx:alpine
        imageID:
          type: string
        containerID:
          type: string
          examples:
            - 'docker://415d0a1a81b0'
        ready:
          type: boolean
        started:
          type: boolean
        restartCount:
          type: integer
        state:
          $ref: '#/components/schemas/ContainerStateDetail'
    ContainerStateDetail:
      type: object
      description: Exactly one of running / waiting / terminated is present.
      additionalProperties: false
      properties:
        running:
          $ref: '#/components/schemas/RunningState'
        waiting:
          $ref: '#/components/schemas/WaitingState'
        terminated:
          $ref: '#/components/schemas/TerminatedState'
    RunningState:
      type: object
      additionalProperties: false
      required:
        - startedAt
      properties:
        startedAt:
          type: string
          format: date-time
    WaitingState:
      type: object
      additionalProperties: false
      required:
        - reason
      properties:
        reason:
          type: string
          examples:
            - Created
    TerminatedState:
      type: object
      additionalProperties: false
      required:
        - exitCode
        - reason
        - oomKilled
        - startedAt
        - finishedAt
      properties:
        exitCode:
          type: integer
        reason:
          type: string
          examples:
            - Error
        oomKilled:
          type: boolean
        startedAt:
          type: string
          format: date-time
        finishedAt:
          type: string
          format: date-time
    WatchEventPayload:
      type: object
      description: One change on the desired WATCH stream (k8s watch-event shape).
      additionalProperties: false
      required:
        - type
        - object
      properties:
        type:
          type: string
          enum:
            - ADDED
            - MODIFIED
            - DELETED
        object:
          $ref: '#/components/schemas/Container'
    ContainerListPayload:
      type: object
      description: The full desired set for a device (k8s List shape).
      additionalProperties: false
      required:
        - apiVersion
        - kind
        - items
      properties:
        apiVersion:
          type: string
          const: pilet/v1
        kind:
          type: string
          const: ContainerList
        items:
          type: array
          items:
            $ref: '#/components/schemas/Container'
    Container:
      type: object
      description: A desired container object (k8s Pod-spec shape, one Docker container).
      additionalProperties: false
      required:
        - apiVersion
        - kind
        - metadata
        - spec
      properties:
        apiVersion:
          type: string
          const: pilet/v1
        kind:
          type: string
          const: Container
        metadata:
          $ref: '#/components/schemas/Metadata'
        spec:
          $ref: '#/components/schemas/ContainerSpec'
    ContainerSpec:
      type: object
      additionalProperties: false
      required:
        - containers
      properties:
        restartPolicy:
          type: string
          default: Always
          enum:
            - Always
            - OnFailure
            - Never
        containers:
          type: array
          description: Always length 1 — one Docker container per object.
          items:
            $ref: '#/components/schemas/ContainerDef'
    ContainerDef:
      type: object
      description: One container definition (k8s Container shape).
      additionalProperties: false
      required:
        - name
        - image
      properties:
        name:
          type: string
        image:
          type: string
          examples:
            - nginx:alpine
        ports:
          type: array
          items:
            $ref: '#/components/schemas/ContainerPort'
        env:
          type: array
          items:
            $ref: '#/components/schemas/EnvVar'
        resources:
          $ref: '#/components/schemas/ResourceRequirements'
    ContainerPort:
      type: object
      additionalProperties: false
      required:
        - containerPort
      properties:
        containerPort:
          type: integer
          examples:
            - 80
        hostPort:
          type: integer
          description: If set, published to the host (docker -p hostPort:containerPort).
          examples:
            - 8080
    EnvVar:
      type: object
      additionalProperties: false
      required:
        - name
        - value
      properties:
        name:
          type: string
        value:
          type: string
    ResourceRequirements:
      type: object
      additionalProperties: false
      properties:
        limits:
          $ref: '#/components/schemas/ResourceLimits'
    ResourceLimits:
      type: object
      description: Resource ceilings as k8s quantities.
      additionalProperties: false
      properties:
        cpu:
          type: string
          examples:
            - 500m
        memory:
          type: string
          examples:
            - 128Mi
