feat: hash container.watch files into the image tag (#35) - #36
Merged
Conversation
The rebuild-detection hash only covered the Containerfile itself, so editing a lockfile it COPYs in and builds a toolchain from (e.g. flake.nix/flake.lock) silently reused a stale image. `container.watch` lists extra project files whose path+content now fold into the same tag hash, with backward-compatible encoding (an empty list hashes byte-identically to before), lexical-only path validation, hard caps (100 files / 4 MiB), a hard error on a missing or non-regular watch entry, and a hard error when watch is set without a project Containerfile (the shared default image can't depend on per-project files without breaking cross-project image pruning). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Summary
Closes #35. Today,
pall8t run/builddecides whether to rebuild thesandbox image by hashing only the Containerfile's bytes and embedding that
hash in the image tag. If the Containerfile
COPYs in a lockfile(
flake.nix/flake.lock) and builds a toolchain from it, editing thelockfile doesn't change the Containerfile's own bytes, so the stale image
gets silently reused. This adds
[container] watch = [...]: a list ofextra project files whose path + contents fold into the same tag hash.
Design decisions
watchlist produces ahash byte-identical to today's Containerfile-only hash, so upgrading
triggers no fleet-wide rebuild. A non-empty list hashes a length-prefixed
encoding (kills concatenation ambiguity) of the Containerfile plus each
(rel_path, bytes)pair sorted by path (kills TOML list order andcaller-order sensitivity).
~,and any
..component; drops.components; sorts + dedups. Nocanonicalization/symlink chasing — not a privilege boundary here, and
canonicalizing would fight the atomic-save retry.
Containerfile is exempt, as today). Exceeding either is an error, never
truncation.
never hashed as empty. The non-regular-file check (added during review)
also rejects a watch entry that's a symlink to a device/FIFO/socket:
stating something like/dev/zeroreports size 0 and would otherwisesail past the byte cap, and then blocking forever on the actual read.
watch+ built-in default image = hard error. The default imagebuilds from
~/.pall8t, so project files can't affect it, and giving ita project-specific tag would make pruning delete other projects' images
sharing
pall8t-base.repos/home.policy.every project missing the listed files, so it's not advertised in
GLOBAL_SKELETON.Review notes
Went through
/code-reviewand an adversarial/skeptical-reviewpass;both found real issues that are fixed in this branch, notably: the
post-build re-check now treats a byte-cap overflow as a poisoned build
(not a warning) since it can only mean a watched file grew mid-build; and
the symlink-to-device hang described above.
/simplifyfurther collapsedsome duplicated control flow and removed a couple of redundant syscalls.
try_build's post-build poison check isn't unit-tested directly (it shellsout to the real
container buildbinary, consistent with the rest of thecodebase — the pre-existing poison-detection logic wasn't unit-tested for
the same reason); the underlying primitives it's built from
(
try_read_watched,combined_hash) are covered.Test plan
cargo fmt --checkcargo checkcargo test(148 tests passing, including new watch-specific cases)cargo clippy --all-targets -- -D warnings(host target)cargo clippy --all-targets --target aarch64-apple-darwin -- -D warningswith
.pall8t/Containerfile+watch = ["flake.lock"], runpall8t run, editflake.lock, run again — a rebuild shouldtrigger; revert the edit — the original tag should be reused.
🤖 Generated with Claude Code