`--run-id` was guarded as a class; `--approach-id` was not. `write_outbox` composes
`{run_id}-{approach_id}`, so the same escape ran through the unguarded half: measured 20.09,
`--approach-id a/../../../ESCAPE` answered 0 and put the artefacts three levels above the
directory the caller named.
Counted rather than assumed: 11 path compositions in `outbox.py`, two caller-supplied values in
them. Four forms x the one door approach-id reaches, each with the intermediate directory
created first -- without it two of the four were green at HEAD on a missing directory, which is
a reason that is not the guard.
The containment check is asserted where it IS reachable: the writers themselves, called
directly. That is the run path's own way in -- `run.py` hands its `--run-id` straight to
`outbox.write_*`, past the string rule that lives in the door -- so the arms are not vacuous.
10 arms red on ASSERT about behaviour: 4 approach-id (exit 0), 6 writer-level (DID NOT RAISE).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RED. Seven outbox doors compose `<out_dir>/<run_id>-<artefact>.json` and never look at the
run id, so `--outbox-dir <d>/inni --run-id ../../ESCAPE` writes two levels ABOVE the
directory the caller named and answers 0. The guard stood on one half of the path and was
missing on the other. Reproduced on write-prepass before this file existed.
The class is measured, not one case: all seven doors against four path-carrying forms --
relative-up, separator, bare `..`, absolute. Each arm asserts a non-zero exit AND that the
file tree under tmp_path is byte-unchanged afterwards; the first alone would be satisfied
by a door that refused after writing. An rc-0 control per door runs the same argv with a
clean run id, so the refusals measure the run id rather than a malformed call.
The file also carries the guard the `--stop-reason` rule never had. `required=True` is
called load-bearing in the ledger -- "the run finished" and "we never found out" must not
be the same value -- but swapping it for `default=""` passed the whole suite. The arm
fells that mutant: no flag is a usage error, and the empty string stays a value the caller
can supply on purpose.
Measured at this commit: 28 failed, 8 passed (the seven controls and the stop-reason arm).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>