Skip to main content

MQTT State Reporting API

Kerrigan reports appliance, fleet, and liveness state to AWS IoT Core over MQTT. Cloud systems can consume these reports directly from MQTT, route them through AWS IoT Rules, or read the latest fleet state maintained by Neat CICD Infrastructure.

This reference describes the reporting contract produced by Kerrigan. For profile installation and connector operations, see Neat CICD Infrastructure reporting.

Connection and Identity​

The appliance connects to the AWS IoT Core data endpoint from its imported profile using mutual TLS on port 8883 and MQTT 3.1.1.

The following profile values must contain the same immutable identity:

cluster_id = thing_name = MQTT client_id

The human-readable cluster_name is metadata and does not replace the immutable cluster ID. A cloud integration should use cluster_id as its primary key.

All reports are:

  • UTF-8 JSON objects with schema_version: 1;
  • published at MQTT QoS 1;
  • published with MQTT retain disabled.

QoS 1 is an at-least-once delivery contract. Consumers must tolerate duplicate messages. Do not give a cloud consumer the appliance certificate or private key; use an AWS IAM identity or an AWS IoT Rule for cloud-side access.

Pairing profiles can also contain publish_topics.claims and subscribe_topics.offers entries reserved for compatibility with the cloud pairing protocol. The current Fleet Manager does not publish claims or process offers over MQTT; they are not part of the state-reporting or control APIs.

Report Types and Frequency​

ReportDestinationDefault frequencyAdditional triggersMaximum size
Fleet snapshotpublish_topics.statusEvery 60 secondsMQTT connection and material fleet-state changes120 KiB
Heartbeatpublish_topics.heartbeatsEvery 30 secondsNone120 KiB
Host stateNamed Device Shadow hostEvery 60 secondsMQTT connection8 KiB

Kerrigan checks for material fleet-state changes approximately once per second. Changes to reported_at, sequence, or only a DevKit's last_seen_at do not trigger an extra snapshot. The next periodic snapshot still includes the latest values.

The reporting intervals can be changed on the appliance:

KERRIGAN_VULCAN_REPORTING_INTERVAL_SEC=60
KERRIGAN_VULCAN_HEARTBEAT_INTERVAL_SEC=30

Kerrigan enforces a minimum effective interval of five seconds for both values. If the MQTT connection is unavailable, local fleet operation continues. After reconnection, Kerrigan immediately publishes a fresh fleet snapshot and host state.

Fleet Snapshot​

Topic​

The topic is supplied by publish_topics.status. The standard topic is:

kerrigan/{environment}/clusters/{cluster_id}/status

Example:

kerrigan/staging/clusters/ll2-b887d76b-8989-461d-b42b-8d5b1e93de9a/status

Example payload​

{
"schema_version": 1,
"cluster_id": "ll2-b887d76b-8989-461d-b42b-8d5b1e93de9a",
"cluster_name": "ll2",
"reported_at": "2026-09-03T00:40:00.123456+00:00",
"sequence": 42,
"kerrigan": {
"version": "0.1.0",
"hostname": "jim-NucBox-K10",
"status": "degraded",
"channel": "develop",
"commit": "abcdef1234567890",
"release_status": "update_available",
"update_available": true,
"latest_commit": "fedcba6543210987",
"maintenance_control": {"available": true},
"cleanup_control": {"available": true},
"metrics_window_seconds": 60.0,
"metrics_sample_count": 60,
"network": {
"uplink_interface": "enp4s0",
"link_speed_mbps": 1000.0
},
"metrics": {
"cpu_used_percent": 18.4,
"memory_used_percent": 42.1,
"root_fs_used_percent": 31.7,
"network_rx_bytes_per_second": 1250000.0,
"network_tx_bytes_per_second": 625000.0,
"network_rx_utilization_percent": 1.0,
"network_tx_utilization_percent": 0.5
}
},
"summary": {
"devkit_count": 2,
"online_count": 1,
"offline_count": 1,
"busy_count": 0,
"available_count": 1
},
"devkits": [
{
"id": "modalix-01",
"name": "DevKit 01",
"type": "modalix",
"status": "online",
"state": "idle",
"platform_version": "2.1.2",
"neat_version": "0.4.0+release-3.0.0-prep.b728dc3a138b",
"ip_address": "10.77.77.20",
"last_seen_at": "2026-09-03T00:39:55.000000+00:00",
"architecture": "aarch64",
"capabilities": ["nvme"],
"metrics": {
"cpu_used_percent": 12.5,
"memory_used_percent": 37.2,
"root_fs_used_percent": 48.0,
"nvme_fs_used_percent": 21.4,
"temperature_celsius": 72.9,
"power_watts": 13.3
}
},
{
"id": "modalix-02",
"status": "offline",
"state": "idle",
"last_seen_at": "2026-09-03T00:34:20.000000+00:00"
}
]
}

Top-level fields​

FieldTypeDescription
schema_versionintegerPayload schema version. Currently 1.
cluster_idstringImmutable cluster identity and AWS IoT Thing name.
cluster_namestringHuman-readable appliance or cluster name. It is not guaranteed to be unique.
reported_atRFC 3339 stringUTC time at which Kerrigan assembled the snapshot.
sequenceintegerProcess-local sequence shared with heartbeat reports. It can contain gaps and resets after a Fleet Manager restart.
kerriganobjectKerrigan software and aggregate status.
summaryobjectCounts derived from the devkits array.
devkitsarrayComplete known DevKit inventory and current state. This is a snapshot, not a patch.

kerrigan fields​

FieldTypeDescription
versionstringRunning Kerrigan software version.
hostnamestringHostname reported by the Kerrigan server.
statusstringAggregate state: healthy, degraded, or error.
channelstringInstalled Ubuntu host software channel.
commitstringGit commit recorded by the installed Ubuntu host package.
release_statusstringChannel comparison result: latest, update_available, or unknown.
update_availableboolean or nullWhether the channel catalog identifies a newer commit; null when no reliable comparison is possible.
latest_commitstringLatest Git commit advertised for the installed channel, when available.
metrics_window_secondsnumberRolling host-utilization window, 60 seconds by default.
metrics_sample_countintegerNumber of controller samples represented by the current rolling average.
networkobjectSelected uplink interface and its advertised link speed, when available. Kerrigan selects the active IPv4 default-route interface with the lowest route metric.
metricsobjectRolling controller and uplink utilization averages. Unavailable measurements are omitted.

Controller metric keys are:

MetricUnitDescription
cpu_used_percentpercentHost CPU time that was not idle.
memory_used_percentpercentHost memory in use, calculated using MemAvailable.
root_fs_used_percentpercentUsed space on the root filesystem visible to Kerrigan.
network_rx_bytes_per_secondbytes/secondAverage traffic received through the selected uplink.
network_tx_bytes_per_secondbytes/secondAverage traffic transmitted through the selected uplink.
network_rx_utilization_percentpercentReceive throughput divided by the interface link speed. Omitted when link speed is unavailable.
network_tx_utilization_percentpercentTransmit throughput divided by the interface link speed. Omitted when link speed is unavailable.

Aggregate status is derived in this order:

  1. error when any DevKit has status error;
  2. degraded when no DevKit has an error but one or more are offline;
  3. healthy otherwise.

summary fields​

FieldTypeDescription
devkit_countintegerTotal number of DevKits in the snapshot.
online_countintegerDevKits whose status is not offline, including busy and error states.
offline_countintegerDevKits whose status is offline.
busy_countintegerDevKits whose status is busy.
available_countintegerDevKits with fresh online connectivity, healthy underlying health, idle effective state, and no blocking maintenance. This is basic readiness, not allocation authorization.

DevKit fields​

FieldTypeRequiredDescription
idstringyesStable DevKit target ID.
typestringyesDetected DevKit type, for example modalix.
statusstringyesonline, busy, offline, or error.
statestringyesCurrent operational state. During firmware work this is the detailed update phase, such as ota_updating; otherwise it is the target state, such as idle, running, peer-pairing, or peer-paired.
firmware_update_statestringnoFirmware-job phase or result, when retained on the target. Retained labels are history only. Detailed state comes from a matching active durable firmware job; terminal and orphaned labels do not override it.
namestringnoOperator-assigned display name.
platform_versionstringnoLast reported platform or image version.
neat_versionstringnoInstalled Neat core version reported by neat --json. Omission means the device agent could not read a non-empty version; it does not assert that Neat is absent.
ip_addressstringnoLast reported DevKit IP address.
last_seen_atRFC 3339 stringnoTime of the most recent device-agent heartbeat known to Kerrigan.
architecturestringnoReported machine architecture, for example aarch64.
capabilitiesarray of stringsnoEnabled capability labels with the capability_ prefix removed.
peripheralsarray of stringsnoExplicit peripheral inventory from the device agent. Omission means unknown, not an empty peripheral set.
metricsobjectnoArithmetic mean of device-agent samples in the current reporting window (60 seconds by default). Supported keys are cpu_used_percent, memory_used_percent, root_fs_used_percent, nvme_fs_used_percent, network_rx_bytes_per_second, network_tx_bytes_per_second, network_rx_utilization_percent, network_tx_utilization_percent, temperature_celsius, and power_watts; unavailable measurements are omitted. temperature_celsius is the hottest finite thermal sensor in a fresh Sentinel sample, and power_watts is Sentinel's current total board-rail power. Other DevKit metrics come from a fresh Sentinel sample when available, with local CPU, memory, and filesystem collection as a fallback.
peer_pairingobjectnoActive Peer Pairing state reported by the device agent. Contains only session_id, lifecycle state, selected transport (p2p, routed, or staging-only ice), path (direct or relay), health, expiry, handshake timestamp, and a fixed diagnostic message on error. Unknown keys and device error text are omitted; no arbitrary nested data is forwarded.

A DevKit is classified as:

  • offline when it has no heartbeat, its last heartbeat is more than 30 seconds old, or its internal state is explicitly offline;
  • error when its health is unhealthy or degraded, or it is quarantined;
  • busy while booting, bootstrapping, leased, running, cleaning up, recovering, updating, pairing a peer, or paired with a peer;
  • online otherwise.

Heartbeat​

Topic​

The topic is supplied by publish_topics.heartbeats. The standard topic is:

kerrigan/{environment}/clusters/{cluster_id}/heartbeats

Payload​

{
"schema_version": 1,
"cluster_id": "ll2-b887d76b-8989-461d-b42b-8d5b1e93de9a",
"reported_at": "2026-09-03T00:40:30.123456+00:00",
"sequence": 43,
"status": "online",
"uptime_seconds": 86400
}

The heartbeat confirms that Fleet Manager is running and connected to AWS IoT Core. uptime_seconds is measured from the start of the current Fleet Manager process. A heartbeat does not assert that every DevKit is healthy; use the fleet snapshot for cluster health.

Because the topic is not retained, cloud consumers should determine offline or stale state from their own message-receipt time. reported_at is useful for diagnostics but depends on the appliance clock.

Host Named Device Shadow​

Kerrigan publishes host information to the AWS IoT named Device Shadow host.

Update topic​

$aws/things/{thing_name}/shadow/name/host/update

The corresponding cloud API is the standard AWS IoT Data Plane named-shadow API. Cloud consumers normally read the current document with GetThingShadow(thingName, shadowName="host") instead of subscribing to the appliance's update topic.

Update payload​

{
"state": {
"reported": {
"schema_version": 1,
"observed_at": "2026-09-03T00:40:00.123456+00:00",
"os": {
"name": "Ubuntu",
"version": "24.04",
"kernel": "6.8.0-79-generic",
"architecture": "x86_64"
},
"cpu": {
"logical_cores": 16,
"model": "Intel(R) Core(TM)"
},
"memory": {
"total_bytes": 33554432000,
"available_bytes": 21474836480
},
"disks": [
{
"mount": "/",
"total_bytes": 1000204886016,
"available_bytes": 750153664512
}
],
"network": {
"hostname": "jim-NucBox-K10",
"uplink_interface": "enp4s0",
"link_speed_mbps": 1000.0
},
"metrics_window_seconds": 60.0,
"metrics_sample_count": 60,
"metrics": {
"cpu_used_percent": 18.4,
"memory_used_percent": 42.1,
"root_fs_used_percent": 31.7,
"network_rx_bytes_per_second": 1250000.0,
"network_tx_bytes_per_second": 625000.0,
"network_rx_utilization_percent": 1.0,
"network_tx_utilization_percent": 0.5
},
"kerrigan": {
"version": "0.1.0",
"uptime_seconds": 86400,
"status": "degraded"
},
"fleet_summary": {
"devkits": 2,
"online": 1,
"offline": 1,
"busy": 0,
"available": 1
}
}
},
"clientToken": "3d4ce37bde514501bdf38cf8f95cd91b"
}

CPU model and memory byte values can be absent when the host operating system does not expose them. The shadow service adds its standard metadata, version, and timestamp fields to the stored response document.

The shadow and fleet snapshot carry the same rolling controller metrics. Network rates are derived from Linux interface byte-counter deltas. Link-utilization percentages are included only when the selected uplink exposes a positive link speed through sysfs; throughput rates remain available otherwise.

The host shadow's kerrigan.status is degraded when at least one DevKit is offline and healthy otherwise. Use the fleet snapshot when an integration must distinguish DevKit error state from offline state.

Kerrigan subscribes to the named shadow's accepted and rejected response topics and correlates responses with clientToken. The device policy must allow the exact named-shadow update and response topics.

Sequence and Time Semantics​

Fleet snapshots and heartbeats share one increasing, process-local sequence. Consequently, a consumer can observe gaps in status-topic sequence values when heartbeats were published between snapshots. A Fleet Manager restart resets the sequence, so it must not be treated as a permanent event ID.

For freshness and ordering across reconnects or restarts, cloud systems should record a server-side ingestion timestamp. The Neat CICD Infrastructure AWS IoT Rule adds vulcan_updated_at_ms when it stores a fleet snapshot.

Neat CICD Infrastructure Persistence​

The standard Neat CICD Infrastructure deployment processes reports as follows:

InputCloud destinationBehavior
kerrigan/{environment}/clusters/+/statusDynamoDB vulcan-{environment}-kerrigan-cluster-stateReplaces the previous item for that cluster.
kerrigan/{environment}/clusters/+/+SQS inbound event queuePreserves reports and other appliance events for asynchronous processing.
Named Device Shadow hostAWS IoT Device Shadow serviceMaintains the current host-reported state.

The DynamoDB table uses cluster_id as its string partition key and has no sort key. The JSON snapshot is stored in the state attribute. The IoT Rule also adds:

  • vulcan_cluster_id, derived from the authenticated MQTT topic;
  • vulcan_updated_at_ms, generated by AWS IoT Core at ingestion time.

Use vulcan_updated_at_ms to evaluate cloud-side freshness. Kerrigan certificates publish through AWS IoT Core and do not receive direct DynamoDB access.

Consumer Requirements​

A cloud integration should:

  1. authorize MQTT subscription or IoT Rule access only to the required environment and cluster topics;
  2. validate schema_version before processing a report;
  3. verify that the payload cluster_id matches the cluster ID in the topic;
  4. treat a fleet snapshot as a complete replacement rather than a partial update;
  5. tolerate duplicate QoS 1 deliveries and process-local sequence resets;
  6. use cloud receipt time to detect stale heartbeats and snapshots;
  7. avoid using cluster_name as a unique identifier.

If a fleet snapshot exceeds 120 KiB or a host shadow update exceeds 8 KiB, Kerrigan first omits optional fleet detail and sets details_omitted: true. Every device, its identity, operational state, health, maintenance blocker and control capabilities remain present. If mandatory inventory still exceeds the limit, or a host shadow exceeds its limit, the complete payload is rejected. Reporting stages are failure-isolated and repeated failure logs are rate-limited.

Power-control availability​

The full cluster state includes kerrigan.power_control.available and devkits[].power_control.available booleans. For example:

{
"kerrigan": {"power_control": {"available": true}},
"devkits": [
{"id": "board-with-relay", "power_control": {"available": true}},
{"id": "board-without-relay", "power_control": {"available": false}}
]
}

This example shows only the added fields. Appliance availability means a USB relay was discovered without an access error. Device availability additionally requires an assigned channel within that connected relay's capacity. Availability is independent of DevKit network reachability and does not indicate whether power is on. Discovery does not initialize or change relay outputs.

Relay disconnection, reconnection, and assignment changes update availability in subsequent cluster snapshots. Consumers must treat absent fields from older controllers as unavailable and must not enable controls using stale appliance state. NeatOps shows the Power tab only for a fresh appliance report with both availability flags true. The controller revalidates availability when executing devices.powerctl; reported metadata is not an authorization grant.

Local admin portal​

kerrigan.network.admin_url advertises the local dashboard URL, for example http://192.168.90.222:8001/admin. Kerrigan discovers the IPv4 address of its lowest-metric default-route interface and uses dashboard port 8001. If discovery fails, the field is omitted. Deployments must expose that port on the host.

NeatOps uses this URL for the administrator-only Open admin link. The browser must have network access to the appliance (LAN or VPN); this link does not proxy the portal through the cloud. Older appliances without this field require an update before they can advertise a local portal URL.

Maintenance state​

The appliance capability is described under Maintenance capability.

DevKit entries include a separate maintenance object with enabled, blocked, phase, scopes, services, error, and updated_at. While blocked, effective health and status are in-maintenance. underlying_health and connectivity_status retain health/connectivity independently; offline counts continue to reflect actual connectivity and maintained devices are not available for new work. Intent survives controller restarts and is inherited from appliance, pool, and device scopes.

Cleanup state​

kerrigan.cleanup_control.available: true advertises support for the devices.cleanup operation. A DevKit's latest cleanup is included in its fleet snapshot:

{
"cleanup": {
"job_id": "cleanup-0123456789ab",
"state": "running",
"phase": "deleting_workloads",
"last_progress_at": "2026-10-16T12:00:00+00:00"
}
}

last_progress_at is the controller's UTC observation time of the latest state or phase transition. It is not a device heartbeat, percentage, or proof that a long-running phase is still making progress. Repeated snapshots and receipt-only updates do not advance it. Older jobs without progress records may expose only job_id and state until their next transition.

The latest cleanup remains in the snapshot after completion or failure, even after the device reservation is released. A later cleanup replaces it. Device availability is determined by the device state and reservations, not merely by the presence of the cleanup object. Recovery-required devices remain reserved.

Each state or phase transition also produces a device_cleanup_progress event on the configured aggregate events topic (the status-topic sibling events when no events topic is configured). Events include event_id, job_id, target_id, target_mac, state, phase, and last_progress_at. They are persisted with the job update and published with MQTT QoS 1, without retain. Unacknowledged events are retried after reconnection or controller restart; consumers must deduplicate by event_id. Events are not command acknowledgements and have no command_id. Use the local cleanup job API for per-path results, errors, and retry.

Pool membership​

devkits[].pool_id reports membership in a local Kerrigan pool. Pools divide an appliance's devices into local groups for allocation, policy, and administration. A pool ID is scoped to its Kerrigan instance: the same ID on two appliances does not identify a shared cloud pool. Consumers should use (cluster_id, pool_id) when identifying or grouping pools across appliances.

An unassigned device reports pool_id: "default". Pool membership does not indicate maintenance support or whether a device is currently in maintenance. Local pool maintenance policy can affect member devices, but that effective state is reported separately in devkits[].maintenance.

The wire format remains the flat devkits array in schema version 1. Consumers may group it by local pool for display without changing that contract. Older appliances may omit pool_id; display those devices without assuming a reported pool membership.

Maintenance capability​

kerrigan.maintenance_control.available: true advertises that this Kerrigan instance supports the devices.maintenance control operation. It is independent of pool membership and is not a per-device maintenance state, readiness signal, or authorization grant.

Consumers should offer maintenance controls only when a fresh appliance report advertises this capability. Treat an absent capability on older appliances, or available: false, as unsupported. The field is additive in schema version 1.

After submitting a command, follow each device's maintenance state. See the maintenance window lifecycle for entry, reconciliation, and explicit exit.

Projection and retry semantics​

connectivity_status is online only with a heartbeat no older than 30 seconds, no more than five seconds in the future, and a target state other than offline. Busy and maintenance states do not imply a lost connection. Fleet and host-shadow health use the same aggregate rule; their independent observation times may differ.

available_count describes basic readiness. Allocation still checks reservations, active firmware/agent maintenance, session leases, capability, cloud inventory freshness, and cloud locks at admission. A snapshot does not authorize a lease.

Firmware detail is resolved from active durable jobs by target identity or MAC. The reporter never repairs lifecycle state or releases operation ownership.

Publication signatures are committed only after MQTT accepts the message into its local queue. This is not a broker acknowledgment. Failed reports retry after five seconds without requiring a state change; reconnect publishes fresh state. Sequence and fleet-signature bookkeeping are serialized across reconnect and periodic reporting. The connector's reporting_errors reports failing stages and clears each stage after recovery. Historical last_error remains diagnostic evidence.

Optional strings, lists, maintenance detail and pairing fields are bounded before publication. Maintenance and pairing error text is replaced with fixed diagnostic messages; inspect local diagnostics for details. Pairing timestamps must be bounded nonnegative integers; session identifiers must match the 32-character lowercase hexadecimal transport format.