Skip to content

feat: hash container.watch files into the image tag (#35) - #36

Merged
TakiTake merged 1 commit into
mainfrom
feat/container-watch
Jul 25, 2026
Merged

feat: hash container.watch files into the image tag (#35)#36
TakiTake merged 1 commit into
mainfrom
feat/container-watch

Conversation

@TakiTake

@TakiTake TakiTake commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #35. Today, pall8t run/build decides whether to rebuild the
sandbox 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 the
lockfile doesn't change the Containerfile's own bytes, so the stale image
gets silently reused. This adds [container] watch = [...]: a list of
extra project files whose path + contents fold into the same tag hash.

Design decisions

  • Backward-compatible hash. An empty (or unset) watch list produces a
    hash 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 and
    caller-order sensitivity).
  • Lexical-only path validation. Rejects empty, absolute, leading ~,
    and any .. component; drops . components; sorts + dedups. No
    canonicalization/symlink chasing — not a privilege boundary here, and
    canonicalizing would fight the atomic-save retry.
  • Hard caps (v1): 100 files, 4 MiB combined (watch files only; the
    Containerfile is exempt, as today). Exceeding either is an error, never
    truncation.
  • Missing or non-regular listed file = hard error, naming the file —
    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/zero reports size 0 and would otherwise
    sail past the byte cap, and then blocking forever on the actual read.
  • watch + built-in default image = hard error. The default image
    builds from ~/.pall8t, so project files can't affect it, and giving it
    a project-specific tag would make pruning delete other projects' images
    sharing pall8t-base.
  • Merge semantics: replace, not append — same as repos/home.policy.
  • Docs: project skeleton only. A global watch list would hard-error
    every project missing the listed files, so it's not advertised in
    GLOBAL_SKELETON.

Review notes

Went through /code-review and an adversarial /skeptical-review pass;
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. /simplify further collapsed
some duplicated control flow and removed a couple of redundant syscalls.

try_build's post-build poison check isn't unit-tested directly (it shells
out to the real container build binary, consistent with the rest of the
codebase — 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 --check
  • cargo check
  • cargo 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 warnings
  • End-to-end (needs a real apple/container host): in a scratch project
    with .pall8t/Containerfile + watch = ["flake.lock"], run
    pall8t run, edit flake.lock, run again — a rebuild should
    trigger; revert the edit — the original tag should be reused.

🤖 Generated with Claude Code

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>
@TakiTake
TakiTake merged commit 4b64d4c into main Jul 25, 2026
2 checks passed
@TakiTake TakiTake mentioned this pull request Jul 25, 2026
7 tasks
@TakiTake
TakiTake deleted the feat/container-watch branch July 25, 2026 13:54
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.

Let the image hash cover files beyond the Containerfile ([container] watch)

1 participant