Skip to content

feat(bundle): Take the guest initrd from hull-assets - #30

Merged
ananos merged 2 commits into
mainfrom
feat/initrd-from-assets
Oct 31, 2026
Merged

ananos merged 2 commits into
mainfrom
feat/initrd-from-assets

Conversation

@ananos

@ananos ananos commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

Takes the guest initrd from the signed hull-assets bundle, as the bundle already does for the kernel, for v0.1.0-rc19. Stacked on #29 (hull-assets 0.1.11).

Since hull-assets 0.1.11 its initrd is urunit's single binary (/urunit with /init, /vz-init and /urunit-agent links), built against urunc 3c00b69. That is this bundle's URUNC_REF_DEFAULT, and urunc's urunit.pin there names urunit 48a61bd. So the bundle no longer builds urunit or its own initrd.

Changes

  • scripts/build-bundle.sh: drops the urunit checkout and build and the build-container-initrd.sh run. It keeps hull-assets' container-initrd and still rejects a gzipped one. docker is now needed only for the urunc build. --variant stock is unchanged: it has no urunit, kernel or initrd.
  • scripts/check-assets.sh (new): the coherence guard. It reads sources.urunc.ref from the hull-assets bundle.json (pulled from the cosign-verified digest) and fails the build unless it equals URUNC_REF. hull-assets 0.1.11 does not record a urunit ref. When it does, the guard requires the commit urunc's urunit.pin names; until then it logs that it matched on urunc alone.
  • URUNIT_REF in pins.env comes from urunc's packaging/container-initrd/urunit.pin at URUNC_REF, as before, since hull-assets' bundle.json does not carry it. URUNIT_BRANCH and URUNIT_PINNED are gone. Setting URUNIT_REF, URUNIT_BRANCH or URUNIT_REPO is refused, like HVI_REF. INITRD_SOURCE is the hull-assets digest, and ASSETS_URUNC_REF is what hull-assets recorded.
  • share/guest/bundle.json keeps ref and urunit, sets built_by to hull-assets, adds packed_by: brig-build-bundle and names the digest each file came from. Nothing in install.sh or brig main (4fed3cc) reads this file. SHA256SUMS still covers the kernel and container-initrd.
  • CI: the bundle job checks that the kernel and initrd are the layers of the hull-assets manifest named in pins.env, that bundle.json names it, and that ASSETS_URUNC_REF is the pinned urunc. URUNIT_PINNED is no longer checked.
  • Tests: new tests/check-assets.sh. tests/source-pins.sh now ends at the first download and checks that docker runs only for the urunc build.
  • README, DESIGN.md and docs/variants.md say the initrd comes from hull-assets.

Tested

Locally (macOS, no docker): sh -n and shellcheck 0.11.0 over the CI file list, and every tests/*.sh. The Linux-only install tests skip on macOS. scripts/check-assets.sh against the real hull-assets bundle.json files:

0.1.11-linux-amd64, urunc 3c00b69 -> pass (no urunit commit recorded; matched on urunc alone)
0.1.11-linux-arm64, urunc 3c00b69 -> pass
0.1.10-linux-amd64 (urunc 6c062f3), urunc 3c00b69 -> refused

CI builds the four bundles. Pending: the same release gates as #29 on this PR's artifacts.

Release sequence

Merge #29, rebase this onto main, merge, then tag v0.1.0-rc19 on main.

🤖 Generated with Claude Code

@ananos
ananos changed the base branch from chore/assets-0.1.11 to main October 30, 2026 16:53
@ananos
ananos marked this pull request as ready for review October 30, 2026 17:02
@ananos
ananos force-pushed the feat/initrd-from-assets branch from 9525018 to 8b716e2 Compare October 30, 2026 17:02
@ananos

ananos commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

@claude-fleet review

1 similar comment
@ananos

ananos commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

@claude-fleet review

@claude-fleet

claude-fleet Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Review task queued.

Task: c0c3b0b2-2778-49a0-bd0f-1b42e04d1cf6
Commit: 8b716e2
Model: claude-opus-5 (effort medium)
Executor: rp1
Status: done. Review posted.

@claude-fleet

claude-fleet Bot commented Oct 30, 2026

Copy link
Copy Markdown

Review task c0c3b0b2-2778-49a0-bd0f-1b42e04d1cf6 is already running for this commit.

@claude-fleet claude-fleet Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Takes the kernel and initrd from one signed hull-assets digest and replaces the local urunit/initrd build with a coherence guard. The shape is right, the docs track the diff, and both new test files pass. Two things to fix before merge, both about the urunit half of the guard.

Findings:

  • should_fix, scripts/check-assets.sh:45 — the urunit check fails open on any sources.urunit layout the sed does not model
  • should_fix, scripts/build-bundle.sh:1222 — the shipped bundle.json asserts a urunit commit nothing verified
  • nit, scripts/build-bundle.sh:1015 — URUNIT_REPO from the pin is no longer validated before it lands in pins.env

Verified:

  • assets built from another urunc are refused, with the message the description quotes — scripts/check-assets.sh:38-42; tests/check-assets.sh case 2
  • URUNIT_REF, URUNIT_BRANCH and URUNIT_REPO are refused like HVI_REF — scripts/build-bundle.sh:145-147; tests/source-pins.sh:157-170
  • URUNIT_REF comes from urunc's packaging/container-initrd/urunit.pin at URUNC_REF, full-SHA validated — scripts/build-bundle.sh:1011-1023
  • a gzipped container-initrd is still rejected — scripts/build-bundle.sh:1211-1212
  • docker is now reachable only from the urunc build and the TMP_DIR reclaim — the only docker run sites left are scripts/build-bundle.sh:254 and :993
  • SHA256SUMS still covers the kernel and container-initrd — scripts/build-bundle.sh:1227
  • nothing in install.sh reads share/guest/bundle.json — no hit for bundle.json in install.sh
  • --variant stock is untouched: both new blocks are under [ "$VARIANT" != "stock" ] — scripts/build-bundle.sh:1011, :1188
  • the README now says a URUNC_REF= tip build fails unless ASSETS_VERSION names a matching hull-assets release — README.md:336-338, which matches scripts/check-assets.sh:40

Checks run:

  • sh tests/check-assets.sh -> 4 ok
  • sh tests/source-pins.sh -> 7 ok
  • sh -n over install.sh, build-bundle.sh, check-assets.sh and both new tests -> ok
  • scripts/check-assets.sh against a hand-written bundle.json whose sources.urunit has a nested object before "ref" -> exit 0, "records no urunit commit", despite a urunit ref that is not the pinned one
  • no revert check: this PR changes where a file comes from rather than fixing a bug, so there is no regression test to falsify

Smaller things:

  • tests/source-pins.sh:121: if grep -qv "make static" "$T/docker.log" only catches a wrong docker invocation. It does not assert the urunc build ran, so an empty docker.log passes the step that is meant to prove docker is still used once. Adding grep -q "make static" "$T/docker.log" alongside it closes that.
  • The initrd no longer gets an explicit chmod 0644 (the old cp path had one). If hull-assets' layer carries the exec bit, scripts/build-bundle.sh:1514 promotes container-initrd to 0755 in the tarball. Cosmetic, but it changes the packed mode silently.
  • README.md:286 states the initrd is urunit 48a61bd as fact; that is the same unverified claim as the bundle.json finding below.

Not checked:

  • the real ghcr.io/nofireai/hull-assets manifests: whether the layer titles are exactly bzImage/Image and container-initrd, and whether oras resolve returns an image manifest rather than an index, which the new CI jq '.layers[]' step assumes. No network here.
  • hull-assets 0.1.11's actual bundle.json key layout, and whether its initrd really is urunit 48a61bd.
  • whether brig main reads share/guest/bundle.json; only install.sh and this repo's scripts were searched.
  • .github/workflows/release.yml beyond the two comment edits.

Not checked:

  • The real ghcr.io/nofireai/hull-assets manifests: layer titles (bzImage/Image, container-initrd) and whether oras resolve yields an image manifest rather than an index, both assumed by the new jq '.layers[]' step in .github/workflows/ci.yml:133-141. No network in this environment.
  • hull-assets 0.1.11's actual bundle.json key layout, and whether its initrd is in fact urunit 48a61bd built against urunc 3c00b69.
  • Whether brig main (4fed3cc) reads share/guest/bundle.json; only install.sh and this repo's scripts were searched.
  • .github/workflows/release.yml beyond its two comment edits.

Task c0c3b0b2-2778-49a0-bd0f-1b42e04d1cf6 on rp1, model claude-opus-5, effort medium.

Comment thread scripts/check-assets.sh
release built from urunc $urunc_ref, or move URUNC_REF_DEFAULT to $got."

got_urunit="$(source_ref urunit)"
if [ -n "$got_urunit" ]; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should_fix: An unreadable sources.urunit.ref is treated as an absent one, so the urunit half of the guard silently becomes a no-op instead of failing the build.

Failure: a hull-assets bundle.json whose urunit entry is {"repo": "...", "build": {"arch": "amd64"}, "ref": "deadbeef..."} carries a urunit that is not the one urunc pins, and the build accepts it. I ran scripts/check-assets.sh on exactly that file with urunc-ref matching and urunit-ref = 48a61bd: exit 0, stderr check-assets: ... records no urunit commit; matched on urunc ... alone. The [^}]* in source_ref (line 35) cannot cross the nested }, so the ref is invisible. Any key rename or added sub-object on hull-assets' side has the same effect, and nothing in CI would notice, because the whole point of the branch is that the ref is sometimes legitimately missing.

The urunc branch gets this right (line 39 dies on an empty read). Mirror it: treat "the \"urunit\" key is present in the json" and "its ref parsed" as separate facts, and die when the first holds and the second does not. For example grep -q '"urunit"[[:space:]]*:' "$json" before the if, and die with "records a urunit source whose ref could not be read" when $got_urunit is empty.

Comment thread scripts/build-bundle.sh Outdated
# from. ref and urunit keep their meaning: the urunc the initrd matches and
# the urunit commit that urunc pins.
cat > "$g/bundle.json" <<JSON
{"ref": "$URUNC_REF", "urunit": "$URUNIT_REF", "built_by": "hull-assets", "packed_by": "brig-build-bundle", "kernel": {"file": "$kernel", "source": "$KERNEL_SOURCE"}, "initrd": {"file": "container-initrd", "source": "$KERNEL_SOURCE"}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should_fix: The bundle.json packed into the tarball states "urunit": "$URUNIT_REF" next to "built_by": "hull-assets", but $URUNIT_REF comes from urunc's urunit.pin (line 1018), not from the assets, and against hull-assets 0.1.11 nothing confirms the initrd was built from it.

Failure: hull-assets cuts a 0.1.11-style release from urunc 3c00b69 but a different urunit. check-assets.sh passes on urunc alone (scripts/check-assets.sh:48-50 logs "records no urunit commit"), and the bundle ships claiming urunit: 48a61bd as hull-assets' own record. Someone debugging an agent-protocol mismatch reads that file, sees a matching urunit, and rules out the actual cause. Before this change the field was true by construction, because the build compiled that urunit itself.

Either name the field for what it is (the commit urunc pins, e.g. urunit_expected), or emit it only when check-assets.sh actually read a urunit ref from the signed bundle.json -- it already prints the urunc ref it read, so it could print the urunit one the same way.

Comment thread scripts/build-bundle.sh
[ -d "${src:-}" ] || fatal "the urunc checkout is needed to read its urunit pin (build urunc from source)"
pin="$src/packaging/container-initrd/urunit.pin"
[ -f "$pin" ] || fatal "$URUNC_REPO@$URUNC_REF has no packaging/container-initrd/urunit.pin"
URUNIT_REPO="$(sed -n 's/^URUNIT_REPO=//p' "$pin" | head -1)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: URUNIT_REPO is now read from urunit.pin and written straight to pins.env (line 1476) with the <owner>/<repo> validation dropped along with the checkout.

Failure: a urunc commit whose pin says URUNIT_REPO=git@github.com:NOFireAI/urunit.git yields URUNIT_REPO=git@github.com:NOFireAI/urunit in pins.env. install.sh:638 parses that file with awk -F=, so a value containing = is also truncated on read. No shell sourcing is involved, so this is a record-quality issue rather than an injection one -- the old check and its test (tests/source-pins.sh case 1c, removed here) caught it. URUNIT_REF kept its validation at lines 1019-1022; the repo did not.

@ananos
ananos force-pushed the feat/initrd-from-assets branch from 8b716e2 to e21f1fc Compare October 30, 2026 21:40
@ananos

ananos commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

@claude-fleet review

@claude-fleet

claude-fleet Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Review task queued.

Task: 7d33a9fa-f9dc-4a37-bfa8-233fa0fa478e
Commit: e21f1fc
Model: claude-opus-5 (effort medium)
Executor: rp1
Status: done. Review posted.

@claude-fleet claude-fleet Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previous round at 8b716e2: 3 findings.

Previous finding Classification Cited line
scripts/check-assets.sh:45 (should_fix): An unreadable sources.urunit.ref is treated as an absent one, so the urunit half of the guard silently becomes a no-op instead of failing the build. Failure: a hull-assets bundle.json whose urunit entry is {"repo": "...", "build": {"arch": "amd64"}, "ref": "deadbeef..."} carries a urunit that is not the one urunc pins, and the build accepts it. I ran scripts/check-assets.sh on exactly that file with urunc-ref matching and urunit-ref = 48a61bd: exit 0, stderr check-assets: ... records no urunit commit; matched on urunc ... alone. The [^}]* in source_ref (line 35) cannot cross the nested }, so the ref is invisible. Any key rename or added sub-object on hull-assets' side has the same effect, and nothing in CI would notice, because the whole point of the branch is that the ref is sometimes legitimately missing. The urunc branch gets this right (line 39 dies on an empty read). Mirror it: treat "the \"urunit\" key is present in the json" and "its ref parsed" as separate facts, and die when the first holds and the second does not. For example grep -q '"urunit"[[:space:]]*:' "$json" before the if, and die with "records a urunit source whose ref could not be read" when $got_urunit is empty. fixed scripts/check-assets.sh:50
scripts/build-bundle.sh:1222 (should_fix): The bundle.json packed into the tarball states "urunit": "$URUNIT_REF" next to "built_by": "hull-assets", but $URUNIT_REF comes from urunc's urunit.pin (line 1018), not from the assets, and against hull-assets 0.1.11 nothing confirms the initrd was built from it. Failure: hull-assets cuts a 0.1.11-style release from urunc 3c00b69 but a different urunit. check-assets.sh passes on urunc alone (scripts/check-assets.sh:48-50 logs "records no urunit commit"), and the bundle ships claiming urunit: 48a61bd as hull-assets' own record. Someone debugging an agent-protocol mismatch reads that file, sees a matching urunit, and rules out the actual cause. Before this change the field was true by construction, because the build compiled that urunit itself. Either name the field for what it is (the commit urunc pins, e.g. urunit_expected), or emit it only when check-assets.sh actually read a urunit ref from the signed bundle.json -- it already prints the urunc ref it read, so it could print the urunit one the same way. fixed scripts/build-bundle.sh:1228
scripts/build-bundle.sh:1015 (nit): URUNIT_REPO is now read from urunit.pin and written straight to pins.env (line 1476) with the <owner>/<repo> validation dropped along with the checkout. Failure: a urunc commit whose pin says URUNIT_REPO=git@github.com:NOFireAI/urunit.git yields URUNIT_REPO=git@github.com:NOFireAI/urunit in pins.env. install.sh:638 parses that file with awk -F=, so a value containing = is also truncated on read. No shell sourcing is involved, so this is a record-quality issue rather than an injection one -- the old check and its test (tests/source-pins.sh case 1c, removed here) caught it. URUNIT_REF kept its validation at lines 1019-1022; the repo did not. fixed scripts/build-bundle.sh:1006

All three findings from the last round are fixed, and I proved each fix's test fails without it. Approve.

Verified:

  • Unreadable sources.urunit.ref now refused, not treated as absent — scripts/check-assets.sh:50-51; the "urunit" presence grep is separate from the ref parse, matching the urunc branch's fail-closed shape.
  • bundle.json no longer claims hull-assets recorded a urunit it did not: check-assets.sh returns <urunc> <urunit|-> (check-assets.sh:57) and build-bundle.sh:1227-1231 emits urunit_expected only in the - case, with the comment saying nothing checked it against the initrd.
  • URUNIT_REPO validated before it reaches pins.env — scripts/build-bundle.sh:1005-1010; git@github.com:... hits the *[!A-Za-z0-9._/-]* arm, empty hits the final *).
  • README:286 and README:337 match the code: URUNC_REF is reassigned to the resolved commit at build-bundle.sh:987 before check-assets.sh compares, so a URUNC_REF= tip build does fail unless ASSETS_VERSION was built from that tip.
  • No consumer of the dropped URUNIT_PINNED/URUNIT_BRANCH/INITRD_SOURCE=built: keys: grep over install.sh and the tree finds none.

Checks run:

  • sh tests/check-assets.sh → 5 ok
  • sh tests/source-pins.sh → 8 ok
  • Removed the elif ... grep -q '"urunit"...' branch (check-assets.sh:50-51) → tests/check-assets.sh case 3b fails: FAIL: a urunit source the check cannot read was accepted. Restored.
  • Removed the case "$URUNIT_REPO" block (build-bundle.sh:1005-1010) → tests/source-pins.sh case 1c fails: FAIL: a urunit.pin with an ssh URL was not refused, log shows pins git@github.com:NOFireAI/urunit@3333.... Restored; git status --porcelain clean both times.

Smaller things:

  • No test covers the urunit_expected vs urunit branch in the packed bundle.json (build-bundle.sh:1227-1231). The CI jq -e only asserts built_by and the two source fields, so a swap of the two arms would go unnoticed. Cheap to add and has("urunit_expected") there while hull-assets 0.1.11 is the pin.
  • docs/variants.md: "hull-assets' own build checks that urunit is built for the target architecture" is a claim about another repo; the brig-side arch check is gone and nothing in this tree replaces it. The per-arch tag plus the signed digest keeps it narrow.

Not checked:

  • hull-assets' own bundle.json layout and whether its build checks urunit's target architecture (external repo, not reachable here)
  • That a hull-assets layer digest equals the file's sha256, which the new CI manifest check at .github/workflows/ci.yml:131-139 assumes (needs network and oras)
  • The CI bundle job and release job end to end; only read, not run

Task 7d33a9fa-f9dc-4a37-bfa8-233fa0fa478e on rp1, model claude-opus-5, effort medium.

Comment thread tests/source-pins.sh
# HEAD answers with it, or with $STUB_HEAD, or with $tip for a branch. A
# checkout of the urunc source plants urunit.pin, as the real commit carries
# one; STUB_NO_PIN=1 leaves it out, and STUB_PIN_REPO replaces its repo.
# one; STUB_NO_PIN=1 leaves it out.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: The stub git's comment no longer mentions STUB_PIN_REPO, which the stub still reads and case 1c still sets.

Failure: a reader of tests/source-pins.sh:34-37 sees only STUB_NO_PIN=1 leaves it out and has to find tests/source-pins.sh:62 ("${STUB_PIN_REPO:-https://github.com/NOFireAI/urunit.git}") and tests/source-pins.sh:139 to learn the knob exists.

Evidence: the sentence "and STUB_PIN_REPO replaces its repo" was dropped in the commit that removed case 1c, and the knob came back in e21f1fc without it. Restore that clause.

Since hull-assets 0.1.11 its initrd is urunit's single binary, built
from the urunit commit that urunc 3c00b69 pins. That is the urunc the
bundle builds. The bundle now keeps hull-assets' container-initrd next
to its kernel and stops building urunit and its own initrd.

- scripts/check-assets.sh reads sources.urunc.ref from the signed
  hull-assets bundle.json and fails the build unless it is URUNC_REF.
  hull-assets records no urunit commit yet. When it does, the check
  requires the commit urunc pins in its urunit.pin.
- The build still reads urunit.pin from the urunc checkout and records
  URUNIT_REPO and URUNIT_REF in pins.env. URUNIT_BRANCH and
  URUNIT_PINNED are gone, and setting URUNIT_REF, URUNIT_BRANCH or
  URUNIT_REPO is refused.
- share/guest/bundle.json keeps ref and urunit, sets built_by to
  hull-assets and names the digest each file came from.
  INITRD_SOURCE in pins.env is that digest too.
- CI checks that the kernel and the initrd are the layers of the
  hull-assets manifest pins.env names, and that ASSETS_URUNC_REF is
  the pinned urunc.
- tests/check-assets.sh covers the check. tests/source-pins.sh now
  fails at the first download and checks that docker runs only for
  the urunc build.

Signed-off-by: Anastassios Nanos <ananos@nofire.ai>
The README, DESIGN.md and docs/variants.md said the build assembles its
own initrd from urunit. They now say the kernel and the initrd come from
one signed hull-assets bundle, and that the build refuses a bundle built
from another urunc.

Signed-off-by: Anastassios Nanos <ananos@nofire.ai>
@ananos
ananos force-pushed the feat/initrd-from-assets branch from e21f1fc to 331f478 Compare October 30, 2026 22:14
@ananos
ananos merged commit 6f3ba44 into main Oct 31, 2026
5 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.

2 participants