A correctness + security follow-up from a full read-only audit. Smoke coverage grew
from 371 to 429 assertions; the stress harness now fails on an anomaly (was report-only),
and typecheck now also covers test/ and bench/.
Security
- Record-store path traversal closed.
sh_detail/sh_logsreject any id that isn't a
well-formedcmdNbefore it reaches the filesystem, so a crafted id can no longer read a
JSON file outside the per-project store. - Checkpoints are private and transactional. The per-project checkpoint store is now
owner-only (0700, like the record store), and a snapshot is published atomically (built
in a staging sibling, then renamed over the target) so a crash mid-copy can never leave a
partial tree that a later restore would apply. sandbox { network: false }(Linux/bubblewrap) also masks Unix sockets.--unshare-net
isolates TCP/UDP but left/run/docker.sockreachable;/runand/var/runare now
overlaid with tmpfs so a confined command can't reach the host Docker/Podman daemon.
Fixed
background: truewith an invalid cwd no longer crashes the server — the spawn error
listener is wired before the early return, so the async failure is absorbed.sh_logsno longer duplicates output after a background run exits. The durable record
keeps per-stream byte totals so a poll after the live→durable handoff returns only new
output; slices honor the cursor for binary too and never split a UTF-8 codepoint.sh_killreportsterminating(signal sent) instead of claiming the process is dead
with exit 137 before it actually closes.- Children get
/dev/nullstdin — a command that reads stdin gets EOF instead of hanging
to the timeout. files_changedcatches a re-modified already-dirty file (its git status line unchanged),
which also fixesexpect.changedfor that case.- Effects honesty: a syscall trace is bounded by
VEIL_MAX_STREAM_BYTES(with
trace_truncated) and no longer counts a failedopenas a write; a largepreviewdiff
reportspreview_effects_incompleteinstead of collapsing to "nothing changed". BoundedBuffercaps a single chunk larger than the byte cap.- Classifier: a raw newline is now a command separator, and
git config <set>,
branch/tag creation, andgit reflog expire/deleteclassify as writes (were read-only). - ReDoS guard:
stdout_matchesandsh_detail matchrefuse catastrophic-backtracking
patterns instead of running them on the single-threaded event loop.
Changed
veil-guard.shanchors the delete rule to executable position —echo rm -rf xand
grep "rm -rf" fno longer false-block, whilerm -rf,sudo rm -rf, wrapper forms
(timeout/nohup/nice/…), and operator/{/dopositions still block.- Release workflow fails loud (no more silent skip), runs the full gate with
npm ci,
verifies the tag matchespackage.json/package-lock.json, smoke-tests the packed
tarball, and publishes with npm provenance. - Node engines bumped to
>=22(matches CI).