Read the release manifest: verify, parse, decide, cache - #396
Merged
Conversation
The apply transaction and its trigger are built; nothing picks the target. On an appliance the target comes from a signed release manifest. This is the half of that with no network and no timer in it. New internal/hostagent/relmanifest. The minisign verifier takes both algorithm modes and checks both signatures in the file — the second one covers the signature plus the trusted comment, so skipping it would let anyone rewrite that comment on a genuinely signed file. Keys are a list from day one, because rotation depends on it; zero keys refuses everything, which is the shipping state today and keeps the appliance updater inert rather than credulous. Decide answers four states. "Valid and current, but host-agent too old" is a healthy box waiting on apt, not a manifest to ignore, so it stays distinct. The kill switch is checked before the host-agent gate: a box on a retracted version should be offered the way back even when it is behind, since the gate exists to stop it moving forward. The cache stores the publisher's raw bytes, because a re-marshal drops ignored unknown fields and could never verify again, and Load re-verifies so the local file system is not a way around the signature. Closes #395 Claude-Session: https://claude.ai/code/session_01DpJrjCXiQygMgNsw2AqH25
onel
force-pushed
the
backend/395-release-manifest
branch
from
August 12, 2026 09:41
18d09f5 to
aa8b069
Compare
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| internal/hostagent/relmanifest/cache.go | Stores the manifest and signature in one atomically replaced envelope, resolving the previously reported split-pair failure. |
| internal/hostagent/relmanifest/manifest.go | Implements manifest validation and semver-aware update, rollback, hold, and no-op decisions. |
| internal/hostagent/relmanifest/minisign.go | Implements minisign key parsing and verification of both primary and trusted-comment signatures. |
| internal/hostagent/relmanifest/relmanifest_test.go | Covers signature failures, manifest decisions, cache verification, and whole-pair cache replacement. |
Reviews (2): Last reviewed commit: "Review fixes: one cache file, and compar..." | Re-trigger Greptile
The cache wrote the manifest and its signature as two files, each write atomic on its own. That cannot keep the promise RELEASE_MANIFEST.md # Failure modes makes — "the previous valid manifest stays in effect". Two files means two renames: a power cut between them leaves the new manifest beside the old signature, which fails verification, and the previous good manifest is already gone because the first rename overwrote it. The box would come back with no usable cache at all. Both now live in one file, so saving them is one rename and the box always comes back to a complete pair. The publisher's exact bytes are still stored verbatim, since the signature covers them. Decide also compared the running pair as raw strings while the host-agent gate compared versions. The two sides come from different code paths, so "v1.4.2" and "1.4.2" for the same release would have offered the box an update to what it already runs. Both comparisons now go through semver. Claude-Session: https://claude.ai/code/session_01DpJrjCXiQygMgNsw2AqH25
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #395.
The apply transaction (#385) and its trigger (#386) are both built, and nothing picks the target —
POST /api/v1/system/updatetakes two image refs an admin typed. On an appliance the target comes from a signed release manifest (RELEASE_MANIFEST.md). This is the half of that with no network and no timer in it: verify, parse, decide, cache. The poll, the dashboard prompt and the three-strikes pin are later slices, and each is easier with this part pure and tested.The verifier takes both minisign algorithm modes (
Edover the file,EDover its BLAKE2b-512) and checks both signatures in the file. The second one covers the signature plus the trusted comment — skipping it would let anyone rewrite that comment on a genuinely signed file, and the caller would get attacker-chosen text that looks authenticated.Keys are a list from day one.
RELEASE_MANIFEST.md# Signing builds rotation on it, and one constant would turn a lost key into a synchronized fleet update. Zero keys refuses everything — that is the shipping state today, since no signing key exists yet, so the appliance updater stays inert rather than acting on a file it cannot check.Decideanswers four states, not two. "Valid and current, but this box's host-agent is too old" is a healthy box waiting on its apt update, not a manifest to ignore, so it stays distinct from "not for you". The kill switch is checked before the host-agent gate: a box already running a retracted version should be offered the way back even when it is behind, because the gate exists to stop it moving forward. An unparseable version fails closed.The cache stores the publisher's raw bytes, because a re-marshal drops the unknown fields
Parseignores and could never verify again, andLoadre-verifies so the local file system is not a way around the signature.Testing. Table tests over the failure modes the spec names, driven by a small minisign signer in the test file so the parser meets bytes in minisign's real layout. Four mutation checks, each done by editing the code and restoring it: drop the global-signature check (only the trusted-comment test fails), prehash the wrong mode, treat an unreadable version as new enough, skip the re-verify in
Load.Two things this does not fix, recorded in the progress entry rather than glossed over: no box has a signing key, so this is proven against test-generated keys only; and turning a version into an image digest is still undecided — the manifest names versions, the transaction pulls by digest, and the published ghcr packages are still private per
BUILD.md# 6.Progress entry:
docs/progress/release-manifest-read.md.make test-nopamgreen;gofmtclean; no API surface touched.