Skip to content

Agent v2: local-first board with signed update packages - #1

Merged
IbraTech04 merged 6 commits into
developfrom
claude/awesome-turing-bdgpfd
Sep 24, 2026
Merged

IbraTech04 merged 6 commits into
developfrom
claude/awesome-turing-bdgpfd

Conversation

@IbraTech04

Copy link
Copy Markdown
Contributor

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 replaces docs/offline.md and opens with the audit of v1.

What changes on a board

  • Local-first, always. The kiosk loads 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.
  • Everything is a signed .mbu package (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.
  • Anti-rollback in state.json: content by sequence, software by version.
  • One importer (internal/importer) for every transport:
    • online: device-signed sync from the backend with media deltas, triggered by the existing refresh channel, plus a live weather overlay (internal/boardsync, internal/devauth)
    • online: app and agent auto-update from GitHub release channel files
    • offline: USB stick with .mbu files, via udev and a root helper that mounts read-only (internal/usbimport)
    • offline: laptop via a browser upload page at http://10.77.0.1/ or mbpush, and the Android app, both over the service port (internal/uploadserver)
    • sudo musallahboard-agent import <file.mbu>
  • Agent self-update through a root oneshot that re-verifies from a private copy, health-checks the new agent and rolls back on failure (internal/selfupdate).
  • Update screen: update-anim/index.html is embedded and driven over CDP while a person-delivered update installs (internal/updatescreen). Background content sync stays silent.
  • Clock: floor raised by signed package times; uploader time accepted only alongside a verified package (internal/clock).
  • Migration guard: until the first app release is installed, a board with board-url keeps loading the hosted site, so upgrading online boards never blanks them.
  • Removed: internal/offline, the SSH push account and gate, bundle/app/mode commands.

Rollout order (important)

  1. mbpack keygen twice: a release key (seed in secret MB_RELEASE_SIGNING_KEY, public key in repo variable MB_RELEASE_PUBLIC_KEYS in both this repo and MusallahBoard) and a content key (seed in backend env MUSALLAHBOARD_CONTENT_SIGNING_KEY).
  2. Deploy the backend PR with the content key.
  3. Tag releases of MusallahBoard (app package) and this repo (agent package).
  4. Upgrade boards: setup.sh (or update.sh then setup.sh --service-port for offline boards). Pre-v2 boards pin the content key with trust fetch (setup does it).

Testing

  • go vet ./... and go test -race ./... pass; arm64 build passes.
  • Cross-language checks: packages built by the Java backend and by the frontend's Node packer verify with the Go verifier; a backend delta package installs from the media store end to end.
  • Shell scripts: bash -n, shellcheck; units: systemd-analyze verify.
  • Not run on real Pi hardware (USB mount, NetworkManager, CDP, self-update restart).

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

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +61 to +64
for _, f := range groups[src] {
if _, err := os.Stat(f); err == nil {
os.Remove(f)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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
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
@IbraTech04
IbraTech04 merged commit fb8b35b into develop Sep 24, 2026
1 check failed
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.

2 participants