poll.rs: one exec per tick that tests existence then hashes+stats the
file (sha256sum/stat), so the 2 s poll costs one exec instead of
re-downloading up to 1 MiB of archive per window per tick.
write.rs: sha256_hex/is_sha256_hex, MAX_WRITE_BYTES, and the save
script. Saving stages the payload in /tmp via the existing
write_file_to_container (owned by the container user, since the
Docker archive API writes as root), then an exec as `claude` checks
the base hash, swaps the file in with a same-directory rename when
the directory is writable (falling back to an in-place `cat >` when
it is not), and always cleans up the staged temp file via `trap`.
classify_write maps exit 0/3/4 to Saved/Conflict/Gone.
Applies three pre-flight rulings against the brief's literal text:
- P8: pulled the write script's argv shape and the size/hash checks
into pure `write_command`/`check_write_input` helpers with their
own unit tests, since both lived only inside the untested async
`write_file` otherwise.
- P9: the brief's manual Docker smoke-test invocation
(`sh w.sh save target tmp hash`) makes `$1` become "save" instead
of the target, which is not what the script or the Rust caller
expect. Verified in a throwaway container that invoking the file
directly without the dummy "save" arg reproduces the Rust
convention's `$1/$2/$3` correctly: exit 0 with the new hash and a
removed payload on a clean save, exit 3 with the file untouched on
a stale base hash, and exit 4 when the target is gone.
- P15: GNU sha256sum prefixes its output line with `\` when the
path contains a backslash or newline. Without a fix that turns
into a permanent false conflict (write.rs) and a blinded poll
(poll.rs, hash: null forever). Both parsers now strip a leading
`\`, and the script itself strips it from $actual before comparing
to $expect. Verified against real sha256sum output in a container
with a backslash-named file: the save no longer false-conflicts
and the reported hash matches.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>