Satellite OTA updates — signed, canary-gated firmware delivery across both tiers#18
Conversation
Satellites report firmware_version + an `ota` capability at registration; the daemon records them in a new ota_device_state table (auth_db schema v59) and the WebUI admin panel surfaces "Firmware: x.y.z" per device. Foundation for the server→satellite OTA system (docs/OTA_DESIGN.md). - auth_db v59: ota_device_state table; src/core/ota_db.c upsert/get (shared handle) - webui_satellite: parse+record version+ota cap on register; admin list + JS render - Tier 1 RPi (satellite_version.h single-source) + Tier 2 ESP32 send version; ESP32 partitions.csv + README for the future dual-OTA layout Validated on hardware: dawn-kitchen (Tier 1) + Office Speaker (Tier 2) both report 2.0.0 in the WebUI; v59 migration applied on the live daemon.
…-build fixes The daemon build never compiled dawn_satellite, so config-gated breakage shipped silently. Add build coverage + a container to build Pi-target binaries locally. - ci.yml: satellite-build job (headless, apt-only deps — lws/json-c/alsa/opus) - pre-push.hook: build the satellite before push - CMakeLists: move include_directories(src) out of the SDL block (ws_client.h includes ui/music_types.h unconditionally → headless build was broken) - voice_processing: guard vad_silero_cleanup on HAVE_VAD_SILERO; drop dead TTS-off ensure_tts_loaded stub (-Wunused-function) - Dockerfile.pibase/pibuild + build-pi.sh: build Pi-ABI binaries (trixie arm64, libwebsockets .so.19) from the working tree — no commit/checkout-on-Pi Validated: headless + full voice+UI builds clean; full binary runs on a Pi.
Trust-model primitive + operator signing CLI for server→satellite OTA, both
host-validated. Daemon-side serving/apply (release store, download route, push)
comes next.
- src/core/ota_manifest.{c,h}: pure libsodium core — fixed little-endian binary
wire format (not JSON), Ed25519 sign + verify-before-parse against a keyring,
SHA-256, numeric semver compare, anti-rollback/min_version floor.
- tests/test_ota_manifest.c: 17 unit tests (CI label) — sign/verify roundtrip,
tamper/wrong-key/signed-garbage rejection, keyring rotation, semver, rollback.
- tools/ota_keytool.c (make ota-keytool): offline keygen/sign/verify +
pubkey-header; private key never touches the daemon. Validated end-to-end.
Daemon side of server→satellite OTA: it can offer, serve, and track updates (devices apply in Phase 3/4). [ota].enabled=false by default — inert until opt-in. - ota_db: device state machine — set_state, single-flight begin_offer, one-time consume_token, clear_target, reconcile_stale. - src/core/ota.c: release-store scan/resolve, ota_begin_push (mints token + builds offer), ota_authorize_image_download (token consume + realpath guard), server-owned ota_finalize_on_register. No webui dependency. - [ota] config (parser/defaults/validate/env round-trip + dawn.toml.example). - Transport (Layer 4): HTTPS image route (/api/ota/.., TLS + one-time token + path-hardened), WS device handlers (ota_status/ack/reject) + server→device offer push, admin-gated ota_push/ota_list, registration finalize hook. - tests/test_ota.c: 9 integration tests (single-flight, single-use token, path-traversal, finalize, reconcile). Full CI suite 69/69 green.
Add the two operator entry points over the already-shipped OTA engine. dawn-admin: reserve admin opcode band 0xC0-0xCF; admin_socket_ota.c (ota list / push, push delegates to webui_ota_push so it shares the WebUI delivery spine); `dawn-admin ota list` and `ota push --uuid --version [--allow-downgrade]`. WebUI: per-online-device version picker + allow-downgrade + Push button in the satellite panel; fetches ota_list, sends ota_push, renders in-flight ota_state. Four-agent review applied: escapeAttr for attribute-context output (fixes an XSS via attacker-controlled satellite name, incl. the pre-existing delete-button site); clamped snprintf accumulation; mobile touch targets; keep-in-sync note on the cross-layer forward decl. Build clean, test_ota 9/9 + 17/17, CI 69/69. Live round-trip pending a daemon restart (running daemon predates the 0xC0 dispatch).
…erage Device side: ota_apply downloads a signed image over WSS, verifies before parse, and atomically swaps its own ELF; a frozen libc-only launcher (dawn-satellite-launch) owns boot-count rollback so a dead-on-arrival image still recovers. Trust anchor is a runtime operator keyring (/etc/dawn/ota_pubkey, fail-closed) so unsigned prebuilt binaries can ship and each operator signs for their own fleet. Distribution: .deb (package-deb.sh, auto-computed runtime deps + bundled non-apt libs), release.yml CI, and ota-release.sh (offline signing, keys under /etc/dawn/signing). ota-release.sh refuses a --version that doesn't match the binary's embedded marker, and defaults min_version empty (anti-skip floor is opt-in; anti-rollback is automatic). Server: offer reads manifest+sig fresh from disk (re-stage needs no daemon restart, no cached/sig skew); register-time finalize resolves a stuck version-mismatch row to failed; download token is bound to platform (schema v60) so an rpi token can't fetch an esp32 image. Tests: host suites for the offer decision, launcher rollback decision, and marker codec (decision cores for ~10/13 of the device-apply matrix) + a cross-platform token-binding test. Folds in big-three review fixes (shared fsync helper, version-mirror static_assert, input validation). Tested: all OTA unit tests + full CI (72/72) green; live-verified 2.0.0->2.2.0 happy path on dawn-kitchen; v60 migration applied cleanly to the live DB.
The helper pointed at a never-published GitHub "models" release manifest, so every fetch 404'd. Rework it to download directly over HTTPS from the same sources setup_models.sh uses — Silero VAD + the Piper "Alba" voice from the DAWN repo, and the chosen ASR from alphacephei.com (Vosk) or HuggingFace (Whisper) — into /var/lib/dawn-satellite/models. The default set (VAD + TTS + Vosk-small) matches the shipped satellite.toml; --asr whisper switches engines. curl -f makes an HTTP error abort instead of writing an error page over a model. control: Recommends curl/unzip/ca-certificates (what the helper actually needs) instead of the nonexistent dawn-satellite-models package. Tested: all five upstream URLs return 200; a real download yields a valid ONNX (not an HTML error page) and --create-dirs builds the whisper.cpp/ subdir; arg validation + sh -n clean.
Completes server→satellite OTA across both tiers. ESP32-S3 device side: Ed25519 verify-before-parse of the offer, NVS hand-off + reboot-first WiFi-only download (esp_ota_ops + SHA-256 of committed flash + boot switch), NVS boot-count rollback guard, and a verify-boot esp_timer watchdog that force-reboots an image which reaches setup() but can't register — so a hang (not just a reboot loop) still advances the guard to revert. ota_boot_path runs before the PSRAM/buffer allocs so the counter advances even when a later step hangs. Vendored TweetNaCl (verify-only); ota_pubkey.h baked per-operator (gitignored, .example committed). build-esp32.sh scripts the arduino-cli build (PSRAM=enabled — this board is QSPI, not OPI) + a CI compile gate. Reg log reports fw=. Big-three reviewed; fixes folded in (all-zero-key reject, timer-start failure, clear-before-disarm race, next!=running guard). Also fixes find_session_by_uuid_unlocked to prefer a live session over a stale disconnected one (a reboot-churn leftover shadowed the live session and made `ota push` falsely report "Device is offline"). Test: live-verified end-to-end on ESP32-S3 — clean 2.x update through to daemon committed/WebUI success, AND the watchdog rollback (a deliberately-broken push hung in setup() and self-reverted to the prior slot, no reflash). Daemon + firmware compile clean (0 warnings); format --check --changed clean. Daemon VERSION_NUMBER 1.0.0→2.0.0, RPi satellite firmware 2.2.0→2.0.0, to match the ESP32 satellite — one coherent 2.0.0 across the suite.
Avoids restarting the daemon to consume a freshly-staged release. New ota_rescan() reloads the release dir into the in-memory store; exposed as admin opcode 0xC2 + `dawn-admin ota rescan`, and ota-release.sh auto-rescans after staging (best-effort) so a new version is pushable immediately. The release store gains a mutex (find_release copies out under the lock) since rescan is now a runtime writer concurrent with push/list reads. Also adds --allow-downgrade to ota-release.sh, passing through to `dawn-admin ota push --allow-downgrade` (downgrade pushes previously needed the WebUI toggle). Test: live — staged + auto-rescanned + pushed esp32/2.1.0 with no restart (daemon reported 5 releases, device updated + committed). Build + format clean.
The 2.0.0 bump exposed a dual definition — CMake passed -DVERSION_NUMBER="1.0.0" while include/version.h defined "2.0.0", warning on every TU including the header. Drop the CMake set/-D (keep GIT_SHA's) so version.h is the sole source, and add the missing version.h include to tui.c, which had relied on the -D. Build is now warning-free.
`dawn-admin ota push-all --platform <p> --version <v>` rolls a release out to a whole platform/tier safely: one canary first, and the rest fan out only after it re-registers reporting the new version. A canary revert or 600s timeout halts the rollout — the rest are never touched. Adds `ota rollout-status` / `rollout-abort`. New src/core/ota_rollout.c orchestrator: single in-memory rollout, mutex-guarded. Stays out of the webui layer per ota.h's rule — delivery is a callback dawn.c registers (webui_ota_push, under ENABLE_WEBUI). Commit/fail events hooked into ota_finalize_on_register only flip state; the fan-out push is deferred to a once-per-second main-loop tick so offers are never delivered while the registration handler holds session locks. Admin opcodes 0xC3-0xC5. Eligibility = online devices of the tier not already on the target (offline skipped; offer-pending-on-reconnect is a future add). Also updates OTA_DESIGN §8/Phase 4 to reflect the verify-boot watchdog as shipped. master-code-reviewer pass applied (ENABLE_WEBUI build guard, abort/finish state races, overflow accounting). test_ota gets no-op stubs for the new ota.c rollout hooks. tests-ci builds clean, test_ota passes 11/11, format clean. Canary path is testable with one device; the fan-out wave needs a 2nd esp32 to exercise live.
Adds a "Fleet Rollout" panel at the bottom of the satellite management area: pick a satellite type (ESP32/RPi) + version + allow-downgrade and roll a release out to the whole fleet (canary first). A status line polls every 4s during a rollout with an Abort button, and reflects an in-flight rollout on page load. Backend: three admin-gated WS handlers (ota_push_all / ota_rollout_status / ota_rollout_abort) in webui_ota.c -> ota_rollout_*, dispatched in webui_message_dispatch.c, routed in dawn.js. Frontend (satellites.js + satellites.css): reuses the per-device OTA control's layout primitives, confirm-modal pattern, and CSS tokens. ui-design-architect reviewed; applied a11y + UX fixes (aria-live status region, aria-label on the selects incl. the per-device one, danger-styled confirm + double-submit guard on the whole-fleet action, mobile touch-target sizing). Live-verified from the browser: push-all 2.3.0 to esp32 -> canary committed -> status "done". Format clean.
smoke_test.sh's local/full presets failed to link. Make the daemon buildable without WebUI, wire the smoke test into pre-push so it stays that way, and show the Tier-1 firmware version in its settings panel. Build/config: - calendar_service.c hard-called email_db_account_list; guard with #ifdef DAWN_ENABLE_EMAIL_TOOL (calendar can be built with email off). - Move embedding_engine + the DB-layer sources the always-compiled memory/OTA subsystems depend on (auth_db_conv/user/settings/satellite, image_store) out of the ENABLE_WEBUI/ENABLE_AUTH blocks into always-compiled. - Detect + link libsodium unconditionally (crypto_store/calendar always use it). - scheduler.c weak broadcast fallbacks were #ifdef ENABLE_WEBUI (backwards) so the scheduler couldn't build standalone -> flip to #ifndef. - Gate webui_oauth.c, the messaging tool registration, and phone_service's session/SMS calls on ENABLE_WEBUI. Pre-push hook: - After tests-ci + the satellite build, run smoke_test.sh, which links the full dawn binary across all four presets (tests-ci links only the test binaries, so a daemon that won't link without WebUI slipped through). Gated on the four smoke build dirs existing so it never forces a cold build in the hook. Satellite: - Show DAWN_SATELLITE_FIRMWARE_VERSION in the Tier-1 settings pull-down panel. Test: smoke_test.sh passes all four presets (local/full/debug/debug+email); WebUI-on presets unaffected; daemon + satellite build clean; sdl_ui syntax-checked.
OTA_DESIGN.md: add the runtime release rescan (dawn-admin ota rescan / opcode 0xC2 + ota-release.sh auto-rescan -> pushable without a daemon restart) to the operator-surfaces section, and the WebUI Fleet Rollout panel to the Phase 5 shipped notes. Both shipped but were undocumented. dawn_satellite_arduino/README.md: the publish flow said "restart the daemon if it hasn't scanned the new release dir" -- obsolete now that staging auto-rescans. Reflect auto-rescan + --allow-downgrade + the stage-then-Fleet-Rollout path.
Addresses master-code-reviewer + architecture-reviewer findings on the satellite_ota branch. No security invariants changed; correctness/robustness. Rollout (ota_rollout.c): - Close the start TOCTOU: two threads (admin + WebUI) could both pass the busy-check and both start a rollout, the 2nd orphaning the 1st. Claim the single-rollout slot atomically via a new RO_STARTING state; release it on any post-claim failure (RO_FAIL_CLAIMED) and bail the arm step if aborted mid-start. - Render RO_STARTING as "Rollout starting…" instead of an empty summary. Trust boundary: - Reject unknown satellite-reported OTA state tokens (ota_state_is_valid) — an unknown token would dodge the in-flight predicate. Width-clamp the free-text error/reason/version at the DB chokepoint (ota_db_set_state / ota_db_report_version) so every caller gets one enforced bound. Keytool (ota_keytool.c): - pubkey-header now emits the OTA_PUBKEY_HEX[] form the ESP32 sketch #includes (was an uncompilable uint8_t[][32]). - Write the signing secret key with open(O_CREAT|O_EXCL, 0600)+fdopen (no world-readable window, no symlink/pre-created-file reuse). WebUI: - Resume rollout-status polling for a rollout discovered after reload / in another tab (was only ever stopped). - Surface OTA push/rollout failures as a panel toast + re-enable the buttons (routed from dawn.js OTA_ERROR) instead of the chat transcript. Satellite / packaging: - Tier-1: charset-validate the download token (hex) before it's spliced into the URL, mirroring the uuid guard. - postinst: clear the stale ota/pending marker after re-seeding the binary so an apt action can't trip a probation rollback. Tests: - New tests/test_ota_rollout.c (8 cases) — first coverage of ota_rollout.c: slot-release on no-eligible + push-fail (the TOCTOU regression), second-start rejection, canary->fan-out->done, canary-revert-halt, abort, no-transport. - test_ota: ota_state_is_valid; test_ota_apply: bad-token reject. Test: daemon + keytool + Tier-1 satellite build clean; ctest -L ci 73/73; format clean.
Code Review by Qodo
Context used✅ Compliance rules (platform):
27 rules 1.
|
PR Summary by QodoSigned OTA updates with canary-gated rollouts for RPi and ESP32 satellites WalkthroughsDescription• Add signed OTA firmware offers, token-gated downloads, and server-owned commit/rollback. • Support both Tier-1 (RPi/.deb) and Tier-2 (ESP32) apply paths with anti-rollback. • Add canary-first fleet rollout controls in WebUI and dawn-admin, with new unit tests. Diagramgraph TD
A["Operator (WebUI / dawn-admin)"] --> B["DAWN daemon OTA"] --> F[("auth_db (SQLite)")]
C["Offline ota-keytool"] --> E["Release store (disk)"] --> B
B --> H["HTTPS /api/ota image"] --> E
G["Satellites (RPi / ESP32)"] -->|"WS: offer/status/register"| B
G -->|"GET image + token"| H
High-Level AssessmentThe following are alternative approaches to this PR: 1. Adopt TUF/Uptane-style metadata framework
2. Persist rollout state in the DB (restart-survivable)
3. Use tier-specific native update channels (apt repo for RPi)
Recommendation: The PR’s approach (fixed-size signed manifest + token-gated HTTPS fetch + server-owned commit on re-registration) is a strong fit for constrained devices and keeps both tiers on a consistent trust model. Canary-first fleet rollout adds meaningful safety with minimal operational overhead. Consider DB-persisted rollout/audit logging later if restart-survivability and compliance reporting become requirements. File ChangesEnhancement (44)
Bug fix (2)
Refactor (7)
Tests (7)
Documentation (7)
Other (33)
|
There was a problem hiding this comment.
Pull request overview
Adds an end-to-end, signed OTA update pipeline for both satellite tiers (RPi .deb and ESP32-S3), including server-side orchestration (single-device push + canary-gated fleet rollout), device-side apply/rollback logic, and operator surfaces (WebUI + dawn-admin) with CI/build hardening to keep the daemon buildable across presets.
Changes:
- Introduces signed OTA release/offer primitives (manifest + token-gated HTTPS image fetch) plus per-device OTA state tracking and finalize-on-register semantics.
- Adds fleet rollout orchestrator (canary → fan-out) and exposes OTA operations via WebUI +
dawn-admin+ WebSocket protocol updates. - Implements Tier-1 apply/rollback launcher + Debian packaging; adds Tier-2 OTA boot/apply path plus CI compile gates and tooling updates.
Reviewed changes
Copilot reviewed 101 out of 102 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| www/js/dawn.js | Routes OTA WS responses/errors |
| www/css/components/satellites.css | Styles OTA + fleet rollout UI |
| www/js/admin/satellites.js | Admin OTA panel behaviors |
| tests/CMakeLists.txt | Registers new OTA unit tests |
| tests/test_ota_rollout.c | Rollout state machine tests |
| tests/test_ota_marker.c | Marker codec tests |
| tests/test_ota_launch.c | Launcher decision tests |
| src/webui/webui_satellite.c | Records fw version + caps |
| src/webui/webui_message_dispatch.c | Dispatches OTA WS messages |
| src/webui/webui_http.c | Adds OTA image download route |
| src/webui/webui_admin_satellite.c | Exposes OTA state in list |
| src/webui/webui_ota.c | OTA WS transport + push |
| src/ui/tui.c | Version header include |
| src/tools/tools_init.c | WebUI-gates messaging tool |
| src/tools/phone_service.c | WebUI-gates session/messaging hooks |
| src/tools/calendar_service.c | Email-tool-gates OAuth revoke check |
| src/dawn.c | OTA init + rollout tick |
| src/core/session_manager.c | Prefer live session by UUID |
| src/core/scheduler.c | WebUI-off weak fallback hooks |
| src/config/config_validate.c | Validates [ota] config |
| src/config/config_parser.c | Parses [ota] TOML |
| src/config/config_env.c | Serializes [ota] to TOML |
| src/config/config_defaults.c | Adds [ota] defaults |
| src/auth/auth_db_schema.c | Adds OTA device state table/migrations |
| src/auth/admin_socket.c | Adds OTA admin opcodes |
| src/auth/admin_socket_ota.c | Implements OTA admin handlers |
| services/dawn-satellite/dawn-satellite.service | Switches ExecStart to launcher |
| services/dawn-satellite/dawn-satellite.conf | Debian-correct env file notes |
| pre-push.hook | Builds satellite + preset smoke |
| install-git-hooks.sh | Updates hook descriptions |
| include/webui/webui_ota.h | Declares OTA WS transport API |
| include/version.h | Bumps daemon version macro |
| include/core/session_manager.h | Adds capabilities.ota |
| include/core/ota.h | Declares OTA daemon API |
| include/core/ota_rollout.h | Declares rollout orchestrator API |
| include/core/ota_manifest.h | Declares signed manifest core |
| include/core/ota_db.h | Declares OTA DB state API |
| include/config/dawn_config.h | Adds ota_config_t |
| include/auth/auth_db_internal.h | Bumps schema version to v60 |
| include/auth/admin_socket.h | Defines OTA admin opcodes/payloads |
| include/auth/admin_socket_internal.h | Declares OTA admin handlers |
| format_code.sh | Excludes vendored TweetNaCl from formatting |
| docs/WEBSOCKET_PROTOCOL.md | Documents OTA WS messages |
| docs/SATELLITE_OTA_DEB_TEST_PLAN.md | Adds Tier-1 OTA test plan |
| docs/DAP2_SATELLITE.md | Adds .deb + OTA docs |
| DEPENDENCIES.md | Documents TweetNaCl + arduino-cli |
| dawn.toml.example | Adds [ota] example config |
| dawn-admin/socket_client.h | Adds OTA client APIs |
| dawn-admin/socket_client.c | Implements OTA client APIs |
| dawn-admin/main.c | Adds dawn-admin ota … commands |
| dawn_satellite/CMakeLists.txt | Adds OTA apply + launcher build |
| dawn_satellite/src/main.c | Adds version marker + cleanup |
| dawn_satellite/src/ws_client.c | Handles ota_offer + status flush |
| dawn_satellite/src/voice_processing.c | Fixes headless cleanup gating |
| dawn_satellite/src/ui/sdl_ui.c | Displays firmware version |
| dawn_satellite/src/ota_marker.c | Adds marker codec implementation |
| dawn_satellite/src/ota_launch.c | Adds launcher entrypoint |
| dawn_satellite/src/ota_launch_core.c | Adds launcher decision core |
| dawn_satellite/include/ws_client.h | Declares OTA WS sends |
| dawn_satellite/include/satellite_version.h | Centralizes fw version |
| dawn_satellite/include/ota_marker.h | Declares marker + paths |
| dawn_satellite/include/ota_launch.h | Declares launcher API |
| dawn_satellite/include/ota_apply.h | Declares Tier-1 apply API |
| dawn_satellite/include/ota_apply_internal.h | Declares apply decision helpers |
| dawn_satellite/build-pi.sh | Adds Pi build workflow |
| dawn_satellite/package-deb.sh | Builds Tier-1 .deb |
| dawn_satellite/packaging/debian/control | Debian package metadata |
| dawn_satellite/packaging/debian/conffiles | Marks conffiles |
| dawn_satellite/packaging/debian/postinst | Seeds live/rollback binaries |
| dawn_satellite/packaging/debian/prerm | Stops service on remove/upgrade |
| dawn_satellite/packaging/debian/postrm | Cleanup on remove/purge |
| dawn_satellite/packaging/dawn-satellite-fetch-models | Operator model downloader |
| dawn_satellite/Dockerfile.pibuild | Headless Pi build image |
| dawn_satellite/Dockerfile.pibase | Full Pi base image (ML deps) |
| dawn_satellite_arduino/README.md | Adds ESP32 OTA instructions |
| dawn_satellite_arduino/satellite_version.h | Centralizes ESP32 fw version |
| dawn_satellite_arduino/partitions.csv | Dual-OTA partition layout |
| dawn_satellite_arduino/ota_pubkey.h.example | Example OTA pubkey header |
| dawn_satellite_arduino/ota_apply.h | ESP32 OTA public API |
| dawn_satellite_arduino/dawn_satellite_arduino.ino | OTA offer handling + boot path |
| dawn_satellite_arduino/build-esp32.sh | CLI/CI ESP32 build gate |
| CMakeLists.txt | Build refactor + ota-keytool target |
| cmake/DawnTools.cmake | WebUI-gates webui_oauth source |
| .gitignore | Ignores ESP32 OTA pubkey header |
| .github/workflows/ci.yml | Adds satellite + esp32 compile jobs |
| .github/workflows/release.yml | Adds arm64 Tier-1 release build |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /* Require the device online before reserving an offer (avoids an orphaned | ||
| * in-flight row for an unreachable device). */ | ||
| session_t *session = session_find_by_uuid(uuid); | ||
| if (!session || session->disconnected) { | ||
| if (session) { | ||
| session_release(session); | ||
| } | ||
| return AUTH_DB_NOT_FOUND; | ||
| } | ||
|
|
| int handle_ota_push_cmd(int client_fd, const char *payload, uint16_t payload_len) { | ||
| /* Minimum: flags + uuid_len + 1 uuid byte + 1 version byte = 4. */ | ||
| if (!payload || payload_len < 4) { | ||
| return send_text_response(client_fd, ADMIN_RESP_FAILURE, "Invalid ota push payload"); | ||
| } |
| #include "tweetnacl.h" | ||
| #define FOR(i,n) for (i = 0;i < n;++i) | ||
| #define sv static void | ||
|
|
||
| typedef unsigned char u8; | ||
| typedef unsigned long u32; |
There was a problem hiding this comment.
1. tweetnacl.c missing gpl header 📘 Rule violation § Compliance
The newly added files dawn_satellite_arduino/tweetnacl.c and dawn_satellite_arduino/tweetnacl.h start immediately with code (includes/guards/macros) and omit the required GPLv3 header block at the top. This can cause licensing/audit compliance failures for newly added C/C++ source and header files.
Agent Prompt
## Issue description
The newly added files `dawn_satellite_arduino/tweetnacl.c` and `dawn_satellite_arduino/tweetnacl.h` are missing the mandated GPLv3 header block at the top of the file before any includes, macros, or header guards.
## Issue Context
Per PR Compliance ID 278939 and the repository’s standard GPLv3 header template (per CLAUDE.md), every new `.c`/`.h`/`.cpp` file must include the GPLv3 header at the top to avoid licensing/audit compliance issues.
## Fix Focus Areas
- dawn_satellite_arduino/tweetnacl.c[1-20]
- dawn_satellite_arduino/tweetnacl.h[1-25]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
Intentional, not an omission. tweetnacl.{c,h} is vendored unmodified from upstream TweetNaCl (public domain), so we deliberately do not add the GPLv3 header — adding our license to third-party code would misrepresent it. It is attributed in NOTICE and DEPENDENCIES.md per the project's third-party-attribution policy, and excluded from format_code.sh. Leaving as-is.
| /** | ||
| * @brief Register the per-device delivery function (transport layer, Layer 4). | ||
| * | ||
| * Called once at init with webui_ota_push. Until set, ota_rollout_start() fails | ||
| * (no way to deliver an offer). Signature matches webui_ota_push(): | ||
| * returns AUTH_DB_SUCCESS | AUTH_DB_LOCKED | AUTH_DB_NOT_FOUND | AUTH_DB_FAILURE. | ||
| */ | ||
| typedef int (*ota_rollout_push_fn)(const char *uuid, const char *version, bool allow_downgrade); | ||
| void ota_rollout_set_push_fn(ota_rollout_push_fn fn); |
There was a problem hiding this comment.
3. ota_rollout_set_push_fn() missing doxygen 📘 Rule violation ✧ Quality
The public API function ota_rollout_set_push_fn() is declared without an immediately preceding Doxygen-style comment block with @param/@return tags. This breaks the requirement that all public API functions be documented in a Doxygen-recognized format.
Agent Prompt
## Issue description
`ota_rollout_set_push_fn()` is a public header declaration and needs an immediately preceding Doxygen comment block, including `@param` for its parameters and `@return` if non-void.
## Issue Context
The existing Doxygen block is separated from the function declaration by a typedef, so documentation tools may not associate it with the function.
## Fix Focus Areas
- include/core/ota_rollout.h[54-63]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| /* External linkage (not static): ota_apply.cpp references it via `extern char | ||
| * persistentUUID[]` to build the download URL. */ | ||
| char persistentUUID[37] = {0}; |
There was a problem hiding this comment.
4. persistentuuid not snake_case 📘 Rule violation ✧ Quality
The modified global identifier persistentUUID is not snake_case (contains uppercase letters). This violates the naming convention for C/C++ identifiers in modified code.
Agent Prompt
## Issue description
The modified global variable `persistentUUID` violates the required snake_case naming convention.
## Issue Context
This identifier is newly modified (changed linkage) in this PR, so it falls under the rule scope for modified variables.
## Fix Focus Areas
- dawn_satellite_arduino/dawn_satellite_arduino.ino[171-173]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
Skipping. This is the ESP32 Arduino sketch, which follows Arduino-ecosystem camelCase throughout (webSocket, nvsPrefs, loadOrCreateUUID, etc.). The snake_case convention in CLAUDE.md applies to the DAWN daemon C/C++ sources; renaming a single identifier here would make the sketch internally inconsistent. Leaving as-is.
| int ws_client_send_ota_ack(ws_client_t *client) { | ||
| if (!client) { | ||
| return -1; | ||
| } |
There was a problem hiding this comment.
5. ws_client_send_ota_ack() returns -1 📘 Rule violation ≡ Correctness
The new OTA websocket helpers return negative values (e.g., -1) to signal invalid arguments/errors. This violates the project rule disallowing negative return values as an error signaling mechanism.
Agent Prompt
## Issue description
New functions use `return -1;` as an error sentinel (e.g., when `client` is NULL). The project disallows negative return values for error signaling.
## Issue Context
These OTA send helpers are new API surface used by OTA code paths.
## Fix Focus Areas
- dawn_satellite/src/ws_client.c[1217-1247]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
Skipping. The ws_client_send_ota_* helpers follow ws_client.c's established send-API convention — every ws_client_send_* returns -1 on error (documented in ws_client.h). Converting only the new OTA helpers to SUCCESS/FAILURE would make them inconsistent with every other sender in the file, and normalizing the whole API is out of scope for this PR. The no-negative-returns rule targets the daemon, not this localized satellite-client send API; filed as a follow-up if the send API is ever normalized.
The Docker server image excludes dawn_satellite/ (.dockerignore) while add_subdirectory(tests) is unconditional, so `cmake --preset server` in the dawn-builder stage failed its configure step on the missing satellite sources — test_ota_marker/launch/apply compile dawn_satellite/src/*.c directly. Guard those three registrations on the satellite tree being present. The daemon-side OTA tests (test_ota, test_ota_rollout) don't reference the satellite sources and stay enabled everywhere. Test: cmake --preset debug configures clean; the three guarded tests still build and pass locally; no unguarded dawn_satellite/ refs remain in the server configure path, so the Docker (absent-tree) configure now succeeds.
…il, push gate) From the Copilot + Qodo bot reviews. - webui_ota_push now gates on the device's advertised DAP2 capabilities.ota before reserving an offer — offering to a non-OTA device could wedge the single-flight 'offered' row until token expiry (Copilot). - handle_ota_push_cmd gates on ota_enabled() like its sibling commands, so a push under [ota].enabled=false returns a clear "OTA is disabled" instead of a misleading "no matching release" (Copilot). - ota_finalize_on_register only auto-fails a pending target from genuinely in-progress states (downloading/verifying/applying/rebooting). It previously failed any state != 'offered', so a stale offer the startup reconcile rewrote to 'unknown' was falsely marked failed when the device reconnected on the old version (Qodo bug). + regression test test_finalize_unknown_not_failed. - ota_rollout.h: rename typedef ota_rollout_push_fn -> ota_rollout_push_fn_t and give ota_rollout_set_push_fn its own Doxygen block (Qodo nits). Test: ctest -L ci 73/73; daemon + tests build clean; format clean.
Summary
Adds end-to-end over-the-air firmware updates for both satellite tiers — Tier-1
(Raspberry Pi
.deb) and Tier-2 (ESP32-S3) — driven from the DAWN daemon. Anoperator stages a signed release; the daemon offers it to eligible devices over
the existing DAP2 WebSocket; each device verifies, applies, and re-registers; the
server commits (or rolls back) on its own authority. Fleet rollouts go one canary
first and only fan out after it proves the image.
are signed off-box with
ota-keytool; devices verify against a baked-in publickeyring and fail closed if none is provisioned.
SHA-256s the downloaded image against the signed hash before switching to it.
consumed atomically; HTTPS route with path-traversal containment.
min_versionfloor; downgrades require an explicit--allow-downgrade.service's writable paths; Tier-2 via a verify-boot watchdog + dual-partition
swap.
What's included
format +
ota-keytool, the release store + per-device state machine, and theHTTPS / WebSocket / admin transports.
.debpackaging, Pi build container,CI coverage.
NVS state machine + boot-count rollback, verify-boot watchdog.
ota_rollout.c) with aWebUI "Fleet Rollout" panel.
ota-release.sh --allow-downgrade.local/cipresets) — moved the always-needed DB/crypto/memory sources out of the WEBUI/AUTH
blocks, fixed the scheduler weak-symbol fallback, gated webui/messaging/phone
couplings.
smoke_test.shnow covers all four presets and runs in the pre-push hook.Operator surfaces
dawn-admin ota list / push / rescan / push-all / rollout-status / rollout-abort(type + version + allow-downgrade), with live status + abort.
ota-release.sh(sign + stage + optional push),ota-keytool(offline keygen/sign).Testing
test_ota,test_ota_rollout(canary state machine +slot-claim invariants),
test_ota_apply/_marker/_launch,test_ota_manifest.ctest -L ci73/73;smoke_test.sh4/4 presets; builds clean (daemon, keytool,Tier-1 satellite, ESP32 CI stub).
Review
Reviewed pre-merge by master-code-reviewer (whole branch) and architecture-reviewer
(fixes + tests) — no Critical/High findings; the actionable Mediums/Lows were folded
in (rollout-start TOCTOU hardening, trust-boundary validation at the DB chokepoint,
keytool header-format + secret-key O_EXCL, WebUI failure feedback, Tier-1 token
charset, postinst stale-marker cleanup).
Deferred (follow-ups, non-blocking)
Audit log of OTA operations; signed
allow_downgradein the manifest (wire-formatchange); optional ESP32 native-bootloader rollback; plus the minor review Lows
tracked in TODO.