Skip to content

node report: a claimed device sends its own signed heartbeat - #426

Merged
com-junkawasaki merged 5 commits into
mainfrom
feat/node-report
Oct 2, 2026
Merged

com-junkawasaki merged 5 commits into
mainfrom
feat/node-report

Conversation

@com-junkawasaki

Copy link
Copy Markdown
Member

Stacked on #424 (→ #423 → #421); base is feat/node-onboard-device. Draft; retarget as the lower PRs merge.

Why

The console accepts device-signed heartbeats (POST /api/devices/:did/heartbeat, devices-http/device-heartbeat) but nothing on a device called it. A claimed box therefore showed never seen however healthy it was; only the operator collector's fleet pushes (shared token, one host that can reach every box) made rows live. A device signing with the key it already holds needs neither.

What

  • murakumo.device-report: builds the body, signs grant.device-attest/heartbeat-signing-input (domain, DID, the origin the request reaches, SHA-256 of the body as sent) with the device's Ed25519 key, posts it, and sorts the answer into outcomes with distinct exit codes.
  • murakumo node report [--site URL] [--once | --interval SECONDS]. The loop prints only when the answer changes, so a healthy box is quiet and an unclaimed one does not fill a log.
  • Metrics are the machine's own health: load1, mem-free-ratio, uptime-s, platform, arch, node. No host name, address, SSID, path or account data; this is tested.
  • NixOS: services.murakumoNode.report.{enable,intervalSeconds} (timer, default 60 s). Exit 4 (not claimed yet) is a success status, so an unclaimed box does not alarm.

Verification

  • kbb -M:test -n murakumo.device-report-test: 8 tests, 34 assertions. The signing input is pinned as a literal; a signature verifies with the DID's key via node:crypto (a different path from the signer) and does not verify for another body hash, endpoint, or the enrolment domain; every server answer maps to an outcome; the posted request carries a signature over the exact body.
  • node test/device_report_test.mjs (added to CI): the packaged CLI against a fake console that re-implements the server's check independently (DID → public key, domain\ndid\norigin\nsha256(raw body)\n, Ed25519 verify). Covers accepted (server-side verification passed), the fixed body shape and no identifying data, not-claimed (exit 4), clock-ahead (5), replayed observation (0), a mismatched identity, unreachable (3), and the site allow-list.
  • Release rebuilt; nixos/node.nix and release/nixos-node.nix are in sync (the CI cmp).

Not verified

  • Not run against production or the real Worker: the fake console mirrors verify-heartbeat from reading its source. The Worker itself (devices_http.cljk) was not run, and /api/devices/* is behind the broken production deploy (cloud-murakumo#253).
  • The NixOS module was not evaluated (no nix here); nixos-node-module CI is the first evaluation.
  • GET /api/devices must return the stored last_metrics for the console to show anything beyond last seen; I read the code but did not run it. The matching console change is a separate PR.
  • Until a device is claimed the console has no row and refuses the heartbeat by design (409 unknown-device); that is exit 4, not an error.
  • The heartbeat is signed with the same Ed25519 identity as the claim. Rotating that key is outside this PR.

🤖 Generated with Claude Code

The console has accepted device-signed heartbeats (POST /api/devices/:did/heartbeat)
for some time but nothing on a device called it, so a claimed box showed 'never
seen'. murakumo node report signs the body as sent (aiueos-device-heartbeat-v1 +
DID + the origin reached + SHA-256 of the body) with the device key and posts it.

Metrics are the machine's own health only; no host name, address, SSID or
account data. Exit codes separate accepted, rejected, unreachable, not-yet-claimed
and clock-ahead. Adds services.murakumoNode.report (timer, default 60 s) and its
smoke assertions. The packaged CLI is tested against an independent
re-implementation of the server's signature check.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
com-junkawasaki and others added 4 commits October 2, 2026 11:14
The site's cljk-mirror refuses a .cljk file with no entry in its repo's cljk-origin.edn
(the Worker build of murakumo.cloud mirrors this repo), so these files must be recorded
or merging would stop that build. They are node-side (js interop), so .cljs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@com-junkawasaki
com-junkawasaki changed the base branch from feat/node-onboard-device to main October 2, 2026 06:46
@com-junkawasaki
com-junkawasaki marked this pull request as ready for review October 2, 2026 06:46
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@com-junkawasaki
com-junkawasaki merged commit 108063e into main Oct 2, 2026
3 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant