fix(verification): the runner refuses writes a brief may not perform
/trekreview now runs the commands a BRIEF declares, and a brief is an artifact
that can arrive from outside the repo. Measured 2026-09-18 on 6cafb4c: the
executor denylist stopped a download piped into a shell, but the remote-writing
git subcommand and a recursive delete of a path both RAN. The denylist screens
catastrophe (root deletion, fork bombs, mkfs); it was never meant to screen an
artifact under review.
A second screen, in the runner and ahead of the denylist, refuses four classes:
- a remote-writing git subcommand. The subcommand is found by walking git's own
options (`-C`, `-c`, `--git-dir`, ... take a value), so `git status` and
`git log` still run and `git -C sub push` does not.
- a recursive delete: any `rm` carrying `-r`/`-rf`/`--recursive`. A plain
`rm build/artifact.txt` still runs.
- a download piped straight into a shell (also caught by the denylist; pinned
here so the runner does not depend on another file for it).
- a write outside the working tree. `/dev/null`-class devices are fine, and so
is anything under the working tree; `~/...`, an absolute path elsewhere, and
a target carrying an unexpanded `$VAR` are refused - the runner cannot know
where a variable points, and guessing is how a screen stops screening.
A refusal is its own outcome, REFUSED_BY_POLICY: the command never reaches a
shell, and `summary.ok` is false in both plan and brief mode. For the reviewer,
REFUSED is like NOT RUN - the absence of a measurement, never on its own a
finding - and the rubric and the evidence block both say so.
Chosen deliberately, and it is stricter than today's habit: writing scratch to
/tmp is refused too. The repo's own example plan does `> /tmp/out`. Verification
output belongs in the working tree; exempting the whole system temp dir would
have made the rule unstatable, since a working tree created under /tmp then
contains its own escape hatch.
NOT covered, stated rather than implied:
- other writing git subcommands (tag, remote, config, gc) - only push is listed
- writes through a wrapper: `sh -c '...'`, `xargs`, `find -exec`, a Makefile
target, a script the criterion invokes. The screen reads the command it is
given, not what that command goes on to do.
- `>` inside a quoted string reads as a redirect, so a criterion echoing a
literal `>` is refused. Fail-closed, on purpose.
- the whole surface still runs with the invoking process's permissions; this is
a refusal list, not a sandbox.
The denylist-layer test now uses a stand-in command with a screen double: the
refusal list catches a recursive delete first, so naming one there would have
stopped exercising the denylist layer at all.
Red first: the 6 new tests failed before this change (`refuseCommand` did not
exist), and the fixture brief's four writes ran.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
a7af76e5ff
commit
52b87978cb
6 changed files with 286 additions and 17 deletions
|
|
@ -216,14 +216,17 @@ if [ ! -f "$VOYAGE_ROOT/lib/verification/criteria-runner.mjs" ]; then
|
|||
exit 2
|
||||
fi
|
||||
|
||||
# Every command is screened through the executor denylist before it reaches a
|
||||
# shell; a blocked command is reported BLOCKED, never run. Foreground only.
|
||||
# Every command is screened twice before it reaches a shell: the runner's own
|
||||
# refusal list (writes - push, recursive delete, pipe-to-shell, redirection
|
||||
# outside the working tree) reports REFUSED, and the executor denylist
|
||||
# (catastrophe) reports BLOCKED. Neither is ever run. Foreground only.
|
||||
node "$VOYAGE_ROOT/lib/verification/criteria-runner.mjs" \
|
||||
--brief "{brief_path}" --evidence --cwd "$(git rev-parse --show-toplevel)"
|
||||
```
|
||||
|
||||
Exit 0 means every criterion passed; exit 1 means at least one failed, was
|
||||
blocked, or had no command; exit 2 means the runner could not run. **The exit
|
||||
refused by policy, was blocked by the executor denylist, or had no command;
|
||||
exit 2 means the runner could not run. **The exit
|
||||
code does not stop the review** — a failing criterion is exactly what the review
|
||||
exists to find. Capture stdout as `sc_evidence_block`.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue