feat(agent-fetch): signed binary fetcher + hot-swap (VS-20) #29

Merged
David merged 1 commit from feat/agent-binary-fetcher-vs-20 into main 2026-05-19 10:27:09 +02:00
Owner

Summary

Implements VS-20. The server now pulls signed agent binaries from a remote source (Forgejo Generic Package registry or a plain HTTP host like Dufs) on boot, verifies each with minisign, and atomically promotes verified bytes into the on-disk catalog while hot-swapping the in-memory AgentBinaryCatalog so in-flight /meshagents/:id requests are never interrupted. A periodic refresh task and an admin-gated force-refresh endpoint reuse the same path.

Config (under settings.agentbinaryfetcher)

  • type: "http" or "forgejo"
  • HTTP: baseurl, optional nested auth: { authtype: "bearer"|"basic", ... }
  • Forgejo: baseurl, owner, package, version, optional token
  • Common: publickey (minisign pubkey, bare or two-line block), optional agentids (defaults to the known AgentId set), refreshintervalsecs (0 = boot-only)

Binaries land in settings.agentbinariespath (VS-19) when set; otherwise <datapath>/agents. Filename convention meshagent-<id> matches VS-19's catalog loader, so the existing on-disk read path picks them up.

Failure model

  • Bad pubkey / malformed config / unwritable target dir: warn! and disable the fetcher; server still boots.
  • Boot-time fetch fails on the network: warn! and keep the previous on-disk catalog.
  • Signature mismatch on an individual binary: that binary is dropped (file never written); other binaries in the same cycle are evaluated independently.
  • Atomic write is <file>.tmp.<pid> plus rename(2) in the same directory, so a crash mid-fetch leaves either the old binary or the new one - never a torn write.

Test plan

  • cargo build --workspace clean
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo test --workspace -> 892 passing, 0 failing (including 17 new unit tests in meshcentral-agent-fetch and 5 new boot-level tests)
  • End-to-end: stand up a Dufs source with a signed meshagent-6, point a dev server at it, confirm /meshagents/6 serves the fetched bytes after boot
  • Configure refreshintervalsecs: 60, drop a new binary into the source, confirm hot-swap on next tick
  • POST /agentbinaries/refresh as a site admin: 200 with {installed, failed}; as non-admin: 403; with no fetcher configured: 404

Deferred (not in scope for this PR)

  • Per-domain refresh route (the trigger is server-global today)
  • Sigstore / cosign verification (minisign was chosen for simplicity; documented in the crate's lib.rs)
  • TUF-style mirror metadata
## Summary Implements VS-20. The server now pulls signed agent binaries from a remote source (Forgejo Generic Package registry or a plain HTTP host like Dufs) on boot, verifies each with minisign, and atomically promotes verified bytes into the on-disk catalog while hot-swapping the in-memory `AgentBinaryCatalog` so in-flight `/meshagents/:id` requests are never interrupted. A periodic refresh task and an admin-gated force-refresh endpoint reuse the same path. ## Config (under `settings.agentbinaryfetcher`) - `type`: `"http"` or `"forgejo"` - HTTP: `baseurl`, optional nested `auth: { authtype: "bearer"|"basic", ... }` - Forgejo: `baseurl`, `owner`, `package`, `version`, optional `token` - Common: `publickey` (minisign pubkey, bare or two-line block), optional `agentids` (defaults to the known AgentId set), `refreshintervalsecs` (`0` = boot-only) Binaries land in `settings.agentbinariespath` (VS-19) when set; otherwise `<datapath>/agents`. Filename convention `meshagent-<id>` matches VS-19's catalog loader, so the existing on-disk read path picks them up. ## Failure model - Bad pubkey / malformed config / unwritable target dir: `warn!` and disable the fetcher; server still boots. - Boot-time fetch fails on the network: `warn!` and keep the previous on-disk catalog. - Signature mismatch on an individual binary: that binary is dropped (file never written); other binaries in the same cycle are evaluated independently. - Atomic write is `<file>.tmp.<pid>` plus `rename(2)` in the same directory, so a crash mid-fetch leaves either the old binary or the new one - never a torn write. ## Test plan - [x] `cargo build --workspace` clean - [x] `cargo clippy --workspace --all-targets -- -D warnings` clean - [x] `cargo test --workspace` -> 892 passing, 0 failing (including 17 new unit tests in `meshcentral-agent-fetch` and 5 new boot-level tests) - [ ] End-to-end: stand up a Dufs source with a signed `meshagent-6`, point a dev server at it, confirm `/meshagents/6` serves the fetched bytes after boot - [ ] Configure `refreshintervalsecs: 60`, drop a new binary into the source, confirm hot-swap on next tick - [ ] `POST /agentbinaries/refresh` as a site admin: 200 with `{installed, failed}`; as non-admin: 403; with no fetcher configured: 404 ## Deferred (not in scope for this PR) - Per-domain refresh route (the trigger is server-global today) - Sigstore / cosign verification (minisign was chosen for simplicity; documented in the crate's lib.rs) - TUF-style mirror metadata
feat(agent-fetch): signed binary fetcher + hot-swap (VS-20)
Some checks failed
Check / fmt + clippy + build + tests (pull_request) Failing after 2s
Create release / Create release from merged PR (pull_request) Has been skipped
4ed162d0d9
- New `meshcentral-agent-fetch` crate: `BinarySource` trait with `HttpSource` (Dufs / plain HTTP / S3 / nginx autoindex) and `ForgejoSource` (Generic Package registry) impls, `MinisignVerifier` for mandatory ed25519 signature checking, and a `BinaryFetcher` orchestrator that writes via `<file>.tmp.<pid>` + atomic rename so readers never see a half-written `meshagent-<id>`.
- `AgentBinaryCatalog` now lives behind a thin `AgentBinaryHandle` (`Arc<ArcSwap<...>>`) so the boot-time fetch, the periodic refresh task, and the admin force-refresh trigger all hot-swap the in-memory catalog without interrupting in-flight `/meshagents/:id` requests.
- `AgentBinaryRefresher` (in `meshcentral-web`) wires the fetcher to the handle: each cycle runs `fetch_into` then `AgentBinaryCatalog::load_dir` against the target dir and swaps the result into the live handle.
- Boot path in `meshcentral::boot` parses `settings.agentbinaryfetcher`, ensures the target dir exists (defaults to `settings.agentbinariespath` from VS-19, else `<datapath>/agents`), runs an initial fetch (failures logged, server still starts), and schedules a periodic refresh via the existing `TaskManager` when `refreshintervalsecs > 0`. A bad pubkey, malformed config, or unwritable target dir disables the fetcher cleanly with a `warn!`.
- New `POST /agentbinaries/refresh` route: site-admin-gated (mirrors the `pluginadmin.ashx` auth pattern), returns `{installed, failed}` JSON. 404s when no fetcher is configured.
- Tests cover: URL layout for both sources (HTTP + Forgejo), trailing-slash handling, config (de)serialization for both source types, signature parsing happy/error paths, atomic-replace + temp-file cleanup, partial-failure aggregation, target-missing error path, and three boot-time disabling paths (no config, malformed block, bad pubkey).

#VS-20 State Done

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
David merged commit afd0aef5ec into main 2026-05-19 10:27:09 +02:00
David deleted branch feat/agent-binary-fetcher-vs-20 2026-05-19 10:27:09 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/vervain-server!29
No description provided.