Repository navigation
Agent v2: local-first board with signed update packages - #1
Conversation
Replaces the v1 offline mode (unsigned bundles pushed through a restricted SSH account) with a local-first architecture; see docs/architecture.md. - Every board is served by its own agent on 127.0.0.1:8080, online or not. The network only changes how fresh the installed content is. - Content, the board app and the agent itself arrive as signed .mbu packages (internal/mbu). Content keys (backend) and release keys (CI) are separate trust roots (internal/trust), so the backend cannot sign code. Anti-rollback marks live in state.json. - One importer (internal/importer) serves every transport: backend sync with device-signed requests and media deltas (internal/boardsync), USB sticks (internal/usbimport), the service-port upload server with a browser page (internal/uploadserver), and `musallahboard-agent import`. - Agent self-update through a root oneshot that re-verifies the package, health-checks the new agent and rolls back (internal/selfupdate). - The update screen (update-anim/index.html) is shown over CDP while a person-delivered update installs (internal/updatescreen). - Clock floor from signed package times; uploader time only alongside a verified package (internal/clock). - Kiosk keeps the hosted board as a fallback until the first app release is installed, so upgrading an online board never blanks it. - mbpack builds, signs and verifies packages. Removed: internal/offline, the gate, `bundle`/`app`/`mode` commands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUxfdFFUkL7SmAEUvUzspy
- systemd: self-update .path/.service (root oneshot), usb-import@ template, kiosk ordered after the agent, agent gets CAP_SYS_TIME and CAP_NET_BIND_SERVICE; StartLimit* moved to [Unit] where systemd reads it - udev rules for USB sticks and RTC access - setup.sh: --service-port (was --offline), v1 push account and offline dirs cleaned up safely, trust.json created and content keys fetched, board-url kept as the hosted fallback until the first app release - Makefile + release workflow: compiled-in release keys, signed agent packages and channel files; CI uses the go.mod Go version - mbpush: multipart HTTP upload to the service port, status, fetch of the latest software packages, --ssh fallback; SSH gate client removed - Root readers and writers of trust.json refuse a file the daemon could have planted (not root-owned, extra links, writable by others) - hwclock uses --noadjfile (/etc/adjtime is read-only for the daemon) - Uploader clock is offset-corrected for the time the upload took - README: v2 operator guide Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUxfdFFUkL7SmAEUvUzspy
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1ac55a4d72
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| for _, f := range groups[src] { | ||
| if _, err := os.Stat(f); err == nil { | ||
| os.Remove(f) | ||
| } |
There was a problem hiding this comment.
Preserve inbox packages queued behind an agent update
When a USB or CLI inbox batch contains a newer agent package followed by an app/content package, requeue marks the later file as queued but does not move it because it is already in Inbox. This cleanup loop then deletes that same file anyway, and the daemon exits to await the self-update, so the supposedly queued package is never processed after restart. Skip deletion for files whose result is ActionQueued (or move them to a distinct pending path) so mixed offline update batches are not silently dropped.
Useful? React with 👍 / 👎.
…s local The backend's live GET /api/musallah/payload is being removed, so nothing is left for a hosted board to load. - Weather overlay now comes from the device-authenticated GET /api/agent/weather instead of the live payload - kiosk-url is always the local server; the board-url hosted fallback, --board-url and MB_BOARD_URL are gone (setup removes a v1 board-url) - config.refresh only calls window.MusallahBoard.refresh(); the page gets its device id from the agent, so nothing is pushed into it - The local payload endpoint drops its v1 ?deviceId= check - docs/architecture.md updated (sections 7, 9.3, 14, 15) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUxfdFFUkL7SmAEUvUzspy
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUxfdFFUkL7SmAEUvUzspy
v1 offline mode and the hosted board never reached production, so nothing needs migrating or tolerating. - config: v1 `mode` key and its mapping to service_port are gone - CLI: no stub messages for the removed bundle/app/mode/gate commands - selfupdate: no "fetch content keys for a pre-v2 board" step - enroll: content keys come from the regenerated OpenAPI client instead of a side-parse for backends that predate them - setup.sh: v1 cleanup (push account, unsigned dirs), --offline alias, stubs for removed flags, pre-v2 agent checks and the pre-v2 trust fetch are gone; --service-port no longer re-installs units for v1 boards - install/update/uninstall scripts: removed-flag stubs, v1 push account removal and pre-v2 warnings are gone; install.sh no longer falls back to `trust fetch` after enrollment - docs: migration notes removed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUxfdFFUkL7SmAEUvUzspy
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUxfdFFUkL7SmAEUvUzspy
Replaces the v1 offline mode with a local-first architecture designed for boards that have no internet after setup. The contract for every component (agent, backend, frontend, mbpush, Android) is
docs/architecture.md; it replacesdocs/offline.mdand opens with the audit of v1.What changes on a board
http://127.0.0.1:8080/on every board. The agent serves the installed app and content; the network only changes how fresh they are. No more online/offline mode..mbupackage (internal/mbu): content, the board app, and the agent binary. Two trust roots (internal/trust): the backend's content key can only sign content; a release key held by CI signs software, so a compromised backend cannot run code on boards.state.json: content by sequence, software by version.internal/importer) for every transport:internal/boardsync,internal/devauth).mbufiles, via udev and a root helper that mounts read-only (internal/usbimport)http://10.77.0.1/ormbpush, and the Android app, both over the service port (internal/uploadserver)sudo musallahboard-agent import <file.mbu>internal/selfupdate).update-anim/index.htmlis embedded and driven over CDP while a person-delivered update installs (internal/updatescreen). Background content sync stays silent.internal/clock).board-urlkeeps loading the hosted site, so upgrading online boards never blanks them.internal/offline, the SSH push account and gate,bundle/app/modecommands.Rollout order (important)
mbpack keygentwice: a release key (seed in secretMB_RELEASE_SIGNING_KEY, public key in repo variableMB_RELEASE_PUBLIC_KEYSin both this repo and MusallahBoard) and a content key (seed in backend envMUSALLAHBOARD_CONTENT_SIGNING_KEY).setup.sh(orupdate.shthensetup.sh --service-portfor offline boards). Pre-v2 boards pin the content key withtrust fetch(setup does it).Testing
go vet ./...andgo test -race ./...pass; arm64 build passes.bash -n, shellcheck; units:systemd-analyze verify.Related: LensBridge/LensBridgeBackend, LensBridge/MusallahBoard and LensBridge/mbpush-android PRs/commits on
claude/awesome-turing-bdgpfd.🤖 Generated with Claude Code
https://claude.ai/code/session_01VUxfdFFUkL7SmAEUvUzspy
Generated by Claude Code