Skip to content

Poll the release manifest: fetch, verify, cache, on a timer - #398

Merged
onel merged 2 commits into
devfrom
backend/397-manifest-poll
Aug 12, 2026
Merged

Poll the release manifest: fetch, verify, cache, on a timer#398
onel merged 2 commits into
devfrom
backend/397-manifest-poll

Conversation

@onel

@onel onel commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Closes #397.

#396 built the pure half — verify, parse, decide, cache — and nothing called it. This gives it a clock and a network. After this an appliance box discovers on its own that a newer release exists, which is the first thing in the update design that does not need a human to type something.

A failed poll changes nothing on disk. That is the promise RELEASE_MANIFEST.md # Failure modes makes for an offline box, and it is the thing in this slice most worth getting right: unreachable host, HTTP 500, truncated body, tampered manifest, a signature from a key this box does not accept, wrong schema — seven table cases, each caching a good manifest first and then breaking the CDN one way.

Verify before cache, not after, or anyone who can answer an HTTP request could replace what an offline box acts on.

Keys are stamped at build time (-X ...relmanifest.BakedKeys=...), empty by default, and there is no environment override on purpose: changing which releases a box accepts should take a new binary that arrives through apt and is itself signed, not an edit to a unit file. A build with no key does not poll at all — inert rather than credulous, and it reads in the journal as "this build trusts nobody" instead of "no releases published".

Appliance only. A hosted box's target is held per box by the cloud (UPDATES.md # 8.1), so it gets a no-op twin and main.go carries no build tags. The poll starts last, after the socket is serving and the brain is launched, so a hanging CDN sits in front of nothing.

Two things not in the spec, both now recorded there as "as built": jitter of up to five minutes per tick (boxes cluster after a provider window or a regional power cut), and a 64 KiB body cap.

A mutation check caught a bad test, which is why they are worth doing. The oversized-body case first served junk — which fails signature verification anyway, so it proved nothing about the cap. It now serves a real, correctly signed, schema-valid manifest padded past the limit, and asserts the error names the size: without the explicit check the poll still fails, but as "signature does not verify", which sends whoever reads that log hunting a key problem that does not exist.

The -ldflags symbol path was checked by building a throwaway program against the package (stamped: 1 key, unstamped: 0). A wrong path there fails silently.

docs/architecture.md is also brought up to date for the last four merged slices — it still said the updater had no real-box proof and no release manifest.

Progress entry: docs/progress/release-manifest-poll.md. make test-nopam green, gofmt clean, OpenAPI unchanged (no API surface touched).

Still true after this lands: no signing key, no releases.malmo.network, version → image digest undecided, and the ghcr packages still private — so no appliance box can complete an update yet.

The manifest reader had no clock and no network, so nothing called it. Now an
appliance box discovers a release on its own — the first part of the update
design that needs no human to type anything.

Poll fetches the manifest and its signature, verifies, parses and caches. Run
polls once immediately, then on a jittered hourly tick. The jitter is not in
the spec: boxes cluster after a provider window or a regional power cut, and an
exact hour boundary would turn that into a spike on a static file host.

A failed poll changes nothing on disk. That is the promise the spec makes for
an offline box, and seven table cases hold it. Verification happens before the
cache is written, so anyone who can answer an HTTP request cannot replace what
an offline box acts on.

Keys are stamped at build time like internal/version, empty by default, with no
environment override: changing which releases a box accepts should take a new
apt-signed binary, not an edit to a unit file. A build with no key does not
poll at all, which is every build today.

Appliance only. A hosted box takes its target from the cloud, so it gets a
no-op with the same name. The poll starts last, after the socket is serving and
the brain is launched, so a hanging CDN sits in front of nothing.

Closes #397

Claude-Session: https://claude.ai/code/session_01DpJrjCXiQygMgNsw2AqH25
@greptile-apps

greptile-apps Bot commented Aug 12, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
internal/hostagent/relmanifest/poll.go Adds the timed fetch, verification, state tracking, bounded-body handling, and cache-update workflow; no eligible follow-up finding remains.
internal/hostagent/relmanifest/keys.go Parses build-stamped minisign public keys while safely skipping malformed entries.
cmd/host-agent-real/releasepoll_appliance.go Wires cache loading and background polling into appliance startup with cancellation and HTTP timeouts.
cmd/host-agent-real/releasepoll_hosted.go Keeps release-manifest polling disabled for hosted builds.
cmd/host-agent-real/main.go Starts polling after brain launch and registers cleanup on normal and explicit exit paths.
internal/hostagent/relmanifest/poll_test.go Exercises polling success, failure preservation, size limits, startup cache loading, timing, keyless behavior, and cancellation.
Makefile Adds the release-manifest trust-key value to linker flags with an intentionally empty default.

Reviews (2): Last reviewed commit: "Review notes: Poll refuses without a ver..." | Re-trigger Greptile

…ields

Two notes from review.

Run refuses to start without keys, but Poll is exported and callable on its
own, where a zero-value Poller would panic on a nil verifier. It now returns
ErrNoKeys, the same refusal a keyless build gives.

CLAUDE.md asks for new recurring slog fields to be listed, and this slice added
five: brain, ui, minimum_host_agent, keys, state_dir.

Claude-Session: https://claude.ai/code/session_01DpJrjCXiQygMgNsw2AqH25
@onel
onel merged commit d5de554 into dev Aug 12, 2026
3 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.

Poll the release manifest: fetch, verify, cache, on a timer

1 participant