Skip to content

fix(releases): sync install.sh with hardened bootstrap + serve manifest bundles - #45

Open
thinmintdev wants to merge 3 commits into
masterfrom
fix/web-install-parity-and-manifests
Open

fix(releases): sync install.sh with hardened bootstrap + serve manifest bundles#45
thinmintdev wants to merge 3 commits into
masterfrom
fix/web-install-parity-and-manifests

Conversation

@thinmintdev

Copy link
Copy Markdown
Contributor

Fixes the daily Bootstrap parity CI failure in Hal0ai/hal0 and makes the hardened release channels servable.

DRAFT — needs one re-sync before merge. hal0 PR #1363 changes installer/bootstrap.sh again (digest-pinned cosign fetch). public/install.sh here must be re-copied from that merged commit, since check-bootstrap-parity.sh is a byte-exact diff -u.

1. install.sh drift

Hal0ai/hal0s Bootstrap parity (daily) workflow has failed for days:

DRIFT — installer/bootstrap.sh differs from the live hal0.dev/install.sh

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-line install.sh is byte-identical to hal0s installer/bootstrap.sh at c0d5538b — 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 -u with no normalisation, so a patch would not do.

2. Middleware could not serve the hardened channels

functions/_middleware.ts routed {stable,nightly,dev}.json only — no preview channel (a real channel per hal0 release/policy.py) and no .bundle at all. Hardened clients need both halves, so no external pointer was operational.

  • CHANNEL_RE/^\/(stable|preview|nightly|dev)\.json(\.bundle)?$/
  • Release selection is manifest-driven for both halves, so a tag publishing between a clients two fetches cannot pair a current manifest with an older releases signature. Missing sibling fails closed (no-sibling:<asset>:<tag>) rather than walking back
  • No static backstop for bundles — a placeholder signature reads as tampering, not as not-yet-published
  • per_page 10 → 50: nightlies publish daily carrying only nightly.json, and the newest release carrying stable.json had drifted to position 6 of 10 — stable was days from silently falling back to the static placeholder

Known 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 in functions/, 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)

npm ci        -> 563 packages, exit 0
npm test      -> tests 15, pass 15, fail 0
npx astro check -> 5 errors (all pre-existing, identical to master baseline; 22->23 files checked)
npm run build -> 57 pages, exit 0; dist/install.sh identical to canonical

⚠️ Deploying this alone will break stable installs

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:

LIVE stable.json (v0.9.8)      -> strict policy FAIL (missing release_kind, prerelease_stage)
GH v1.0.0-alpha.2 preview.json -> policy PASS, identity PASS

stable fails three independent ways — no stable.json.bundle on any release, manifest predates two required fields, and signer_identity carries an (?i) prefix the new exact-equality check rejects. None fixable here. Cutting v1.0.0 with the current release.yml clears all three at once.

Merge order: hal0 #1363 → re-sync install.sh here → tag v1.0.0 → deploy.

🤖 Generated with Claude Code

thinmintdev and others added 2 commits July 27, 2026 09:26
…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>
@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hal0-web Ready Ready Preview, Comment Jul 27, 2026 4:07pm

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 27, 2026

Copy link
Copy Markdown

Deploying hal0-web with  Cloudflare Pages  Cloudflare Pages

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

View logs

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>
@thinmintdev
thinmintdev marked this pull request as ready for review July 27, 2026 16:07
@thinmintdev

Copy link
Copy Markdown
Contributor Author

Re-synced against the merged bootstrap. hal0 #1363 landed as f6f92691, taking installer/bootstrap.sh from 362 → 504 lines (digest-pinned cosign fetch).

public/install.sh is now byte-identical to it. Verified from a clean main worktree:

OK: in-tree bootstrap.sh matches the live install.sh — no drift.
parity exit=0

Plus cmp byte-identical and bash -n clean. Marking ready for review.

⚠️ The deploy-order constraint still stands: tag v1.0.0 before deploying this. The canonical bootstrap defaults to releases.hal0.dev/<channel>.json, and the live stable.json (v0.9.8) fails the new client three ways — no stable.json.bundle on any release, manifest missing release_kind/prerelease_stage, and a signer_identity carrying an (?i) prefix the exact-equality check rejects. Cutting v1.0.0 with the current release.yml clears all three.

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.

1 participant