fix(releases): sync install.sh with hardened bootstrap + serve manifest bundles - #45
fix(releases): sync install.sh with hardened bootstrap + serve manifest bundles#45thinmintdev wants to merge 3 commits into
Conversation
…st bundles Two live symptoms of one cause — this repo's serving layer is behind the hal0 release contract. 1. public/install.sh was 95 lines behind hal0:installer/bootstrap.sh. Provenance, since this is the second sync attempt: #44 (e1cef95, parent of this commit) was CORRECT when it landed. Its 267 bytes-for-bytes match hal0's installer/bootstrap.sh at c0d5538b (2026-07-11) exactly — verified with cmp. hal0 then re-hardened the installer on 2026-07-22 across six commits (c3466c98, 8d1afefd, 65611a69, 3f7145e0, c41db8e5, ba644c68, +208/-113), which is the drift the daily parity job now reports. This is staleness, not a deliberately reduced installer. What the 2026-07-22 hardening reinstates, all of it absent from the currently-served copy: - cosign is a REQUIRED bootstrap dependency again. The 2026-07-04 commit (3ef73d59) had made it optional because cosign isn't in apt; HAL0_INSTALL_REQUIRE_COSIGN is now gone entirely and a missing cosign is fatal. - The channel manifest is authenticated before it is parsed: cosign verify-blob against a sibling .bundle with a client-pinned, per-channel release-workflow OIDC identity. - A fail-closed jq policy pass over the authenticated bytes (_schema/channel/release_kind/prerelease_stage/version consistency) and exact signer_identity equality against a client-derived value. - jq becomes a new hard dependency; ambiguous manifest input is rejected; workdir cleanup is trap-guarded. - Default manifest URL moves from GitHub /latest/download/<channel>.json to https://releases.hal0.dev/<channel>.json — which is precisely why item 2 below is a launch blocker and not a nicety. Replaced wholesale with the canonical bytes. hal0's `Bootstrap parity (daily)` workflow diffs the two with no normalisation, so this is exit 0. 2. functions/_middleware.ts only proxied `{stable,nightly,dev}.json`. It knew nothing about `preview` (a real channel per hal0's src/hal0/release/policy.py manifest_targets) and nothing about the sibling `<channel>.json.bundle` that release.yml uploads alongside every manifest. Hardened clients require BOTH halves, so no external pointer was operational: stable.json 200, everything else 404. Route `/{stable,preview,nightly}.json` and `/….json.bundle`, resolving both from ONE release selection driven by the manifest asset — never by the bundle — so a manifest request and a bundle request land on the same release even if a tag publishes between a client's two fetches. If the selected release carries a manifest but not its bundle (broken publish), fail closed with `no-sibling:<asset>:<tag>` instead of walking back to an older release and handing out a mismatched pair. Bundles get no static backstop: a placeholder signature reads as tampering, not as not-yet-published. Also bump the releases-list window 10 → 50. Nightlies publish daily and carry only nightly.json, so at per_page=10 the newest release carrying stable.json scrolled out of the window in about a week and the stable pointer silently degraded to the static placeholder. `dev.json` stays routed as a legacy alias with byte-for-byte identical behaviour (no release publishes it → annotated fallthrough). README: the release-hosting section still described the retired sig_url/cert_url verification path and omitted preview and bundles entirely. Rewritten around the bundle contract, plus the channel matrix, the both-halves-must-be-200 probe, the install.sh mirror rule, and the two reasons a channel can be non-operational upstream. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
functions/_middleware.ts decides which GitHub Release a hardened client's manifest AND its signature bundle come from. If those two ever come from different releases, cosign verify-blob fails and the client reads a routing bug as tampering. That makes the same-release pairing rule and the fail-closed-on-missing-bundle rule security invariants, not style choices — and exactly the kind of thing a later well-meaning refactor drops silently. This repo had no test coverage at all, so nothing would have caught it. 15 cases across 5 groups, driving the real exported onRequest against a stubbed GitHub Releases API: pairing (manifest-driven selection, sibling bundle from the same release, fail-closed when the bundle is absent), channel semantics (preview from newest prerelease, preview falling back to a final tag, nightly, stable not served by a nightly, drafts skipped), response contract (content-type, CORS, cache, nosniff), degradation (annotated static fallthrough, rate-limit fallthrough, legacy dev alias) and host-conditional routing (rewrite, no double-prefix, other hosts untouched and no upstream call). Node's built-in runner and type stripping — no dev dependencies, no build step, no framework for 15 cases. Lives in test/ rather than functions/ because Cloudflare Pages deploys everything under functions/ as routes. Confirmed load-bearing by mutation: flipping the release-selection predicate from the manifest asset to the requested asset turns "selects the release by the MANIFEST asset, never by the bundle" red and npm test exits 1. Reverted after. Deliberately NOT wired into CI — hal0-web's only workflow is mirror-docs.yml and adding a job is a separate call. README: document npm test, and record that astro check's 5 errors (src/content.config.ts, src/pages/changelog.astro) are a pre-existing baseline so the next person doesn't think they caused them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Deploying hal0-web with
|
| Latest commit: |
020578e
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://cd688396.hal0-web.pages.dev |
| Branch Preview URL: | https://fix-web-install-parity-and-m.hal0-web.pages.dev |
hal0 #1363 (f6f92691) added the digest-pinned cosign fetch to installer/bootstrap.sh, taking it 362 -> 504 lines. check-bootstrap-parity.sh is a byte-exact diff, so the served copy has to follow. Verified: cmp against hal0 f6f92691:installer/bootstrap.sh is byte-identical, bash -n clean, and check-bootstrap-parity.sh run from a clean main worktree reports 'OK: in-tree bootstrap.sh matches the live install.sh — no drift.' Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Re-synced against the merged bootstrap. hal0 #1363 landed as
Plus
|
Fixes the daily
Bootstrap parityCI failure inHal0ai/hal0and makes the hardened release channels servable.1. install.sh drift
Hal0ai/hal0sBootstrap parity (daily)workflow has failed for days:The served copy is missing the manifest-authentication layer entirely —
release manifest signature verification FAILED,authenticated release manifest failed strict policy validation, and the field-extraction guard are all absent. The one-line installer everyone runs does not verify the release manifest.Was #44 wrong, or is upstream deliberately reduced? Neither.
Proven by
cmp: upstreams 267-lineinstall.shis byte-identical to hal0sinstaller/bootstrap.shatc0d5538b— the day #44 landed. "cosign optional" was hal0s own policy (3ef73d59, 2026-07-04, because cosign is not in apt). hal0 then reversed it on 2026-07-22 across six commits. So this is ~2 weeks of staleness, not divergence, and overriding is correct.Fixed by wholesale copy — the parity check does a byte-exact
diff -uwith no normalisation, so a patch would not do.2. Middleware could not serve the hardened channels
functions/_middleware.tsrouted{stable,nightly,dev}.jsononly — nopreviewchannel (a real channel per hal0release/policy.py) and no.bundleat all. Hardened clients need both halves, so no external pointer was operational.CHANNEL_RE→/^\/(stable|preview|nightly|dev)\.json(\.bundle)?$/no-sibling:<asset>:<tag>) rather than walking backper_page10 → 50: nightlies publish daily carrying onlynightly.json, and the newest release carryingstable.jsonhad drifted to position 6 of 10 — stable was days from silently falling back to the static placeholderKnown limitation, stated for the record
Same-release pairing is a narrowed race, not a guarantee. Within one request it holds; across the clients two requests the middleware holds no state, so a release publishing in that gap yields manifest from N and bundle from N+1 and verification fails. It is fail-closed and not attacker-inducible, but the error message reads as tampering. Closing it properly needs a hal0-side change (the client derives the bundle URL by appending
.bundle, so it cannot pin a tag). Documented in the README.3. Tests
test/middleware.test.mjs— 15 cases,node --test, zero new dependencies (package.json+1 line). Deliberately not infunctions/, which Cloudflare Pages deploys as routes.Mutation-checked: flipping the release-selection predicate from the manifest asset to the requested asset turns exactly one test red and nothing else.
Verification (clean-room
npm ci)The canonical bootstrap defaults to
releases.hal0.dev/<channel>.json; the old served copy used GitHub/latest/download/, which works today. Replaying the canonical bootstraps own jq policy against the live manifests:stablefails three independent ways — nostable.json.bundleon any release, manifest predates two required fields, andsigner_identitycarries an(?i)prefix the new exact-equality check rejects. None fixable here. Cutting v1.0.0 with the currentrelease.ymlclears all three at once.Merge order: hal0 #1363 → re-sync install.sh here → tag v1.0.0 → deploy.
🤖 Generated with Claude Code