fix(outbox,toolbox): the class was every caller-supplied name, and containment was not unreachable

Session 146 fixed `--run-id` across the seven doors and called it a class. It was not: the class
is every caller-supplied value that reaches a file name, and `write_outbox` composes
`{run_id}-{approach_id}`. 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`, 2 such values, both now through one `_checked_name`.

The ledger sentence that said containment was UNREACHABLE after the string rule was untrue, and
the approach-id escape is the disproof -- the removed check would have caught it. It is back, but
in `outbox._artefact_path`, where the composition is, not in the door. That is the difference
that makes it reachable: the string rule lives in the door, while `run.py` hands its own
`--run-id` straight to the writers and goes past it. Checked before the directory is created, so
a refusal leaves nothing behind, and it covers the next flag someone interpolates into a name.

The judge's exact call now answers 3 with 0 files outside. Suite 2291/0/5/5 (746 s), ruff clean,
mypy 0. Both gates re-run after `git add`: v1 exit 1 (0/3, 0/3, 3/8, no report, 3/8, NOT
MEASURED, 1/20), B exit 1 (15/17, 0/2, 15/15 over 516 files, 0/3, 4/5, NOT MEASURED) -- no row
moved, and row 3's denominator held because the probe grew in place rather than as a new file.

Also: the presentation deck said 1.1.0 was the current version in two places. 1.2.0 now stands in
every tracked place that claims the repo's version. No bump, no tag, no new capability.

[skip-docs]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-20 13:12:15 +02:00
commit cec9b8fe50
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
5 changed files with 116 additions and 68 deletions

View file

@ -46,6 +46,31 @@ def _dump(payload: dict[str, Any]) -> str:
return json.dumps(payload, sort_keys=True, indent=2) + "\n"
def _artefact_path(directory: Path, name: str) -> Path:
"""Compose ONE artefact path, and refuse one that leaves the directory — the last link.
Every writer below builds ``<directory>/<name>``, where ``name`` interpolates caller-supplied
values. The doors (``toolbox._checked_name``) reject a value that carries a path before it
ever gets here, and that string rule is the first link. This is the last one, and it is not
the same check twice: it looks at the FINISHED path, so it holds for the NEXT value someone
interpolates into a file name without remembering the door and for the callers that never
pass a door at all. ``run.py`` hands its ``--run-id`` straight to these writers, so the
property is reachable here in a way it was not inside the door, where mutating it away in
session 146 left all 36 arms green and it was removed as dead code.
Checked BEFORE the directory is created, so a refusal leaves nothing behind not the
artefact, and not a folder the caller would have to clean up."""
path = directory / name
root = directory.resolve()
if root not in path.resolve().parents:
raise ValueError(
f"{name!r} resolves outside {directory}: an artefact belongs in the output directory "
"the caller named — utkatalogen — and this path leaves it."
)
directory.mkdir(parents=True, exist_ok=True)
return path
def write_outbox(
outbox_dir: str,
run_id: str,
@ -70,14 +95,16 @@ def write_outbox(
rather than written as null, since these artefacts are byte-deterministic by contract and a run
nobody commissioned has no approach to name."""
directory = Path(outbox_dir)
directory.mkdir(parents=True, exist_ok=True)
stem = run_id if approach_id is None else f"{run_id}-{approach_id}"
keys: dict[str, Any] = {"run_id": run_id}
if approach_id is not None:
keys["approach_id"] = approach_id
proposal_path = directory / f"{stem}-proposal.json"
# Both paths BEFORE either write: a pair where the second escapes must not leave the first
# on disk, which is what composing them one at a time would do.
proposal_path = _artefact_path(directory, f"{stem}-proposal.json")
outcome_path = _artefact_path(directory, f"{stem}-outcome.json")
proposal_path.write_text(
_dump(
{
@ -89,7 +116,6 @@ def write_outbox(
encoding="utf-8",
)
outcome_path = directory / f"{stem}-outcome.json"
outcome_path.write_text(
_dump(
{
@ -170,8 +196,7 @@ def write_parse_failures(
consistency of reading, not to pin bytes. The caller writes it only when there is at least one
failure, so the file's PRESENCE is itself the signal that something did not parse."""
directory = Path(outbox_dir)
directory.mkdir(parents=True, exist_ok=True)
path = directory / f"{run_id}-parse-failures.json"
path = _artefact_path(directory, f"{run_id}-parse-failures.json")
path.write_text(
_dump({"run_id": run_id, "parse_failures": [dict(f) for f in failures]}),
encoding="utf-8",
@ -197,8 +222,7 @@ def write_exploration(
written even when the exploration RAISED the caller writes it from a ``finally``, because a
capped exploration is precisely the one whose per-round ledger a reader needs."""
directory = Path(outbox_dir)
directory.mkdir(parents=True, exist_ok=True)
path = directory / f"{run_id}-exploration.json"
path = _artefact_path(directory, f"{run_id}-exploration.json")
path.write_text(_dump({"run_id": run_id, **dict(payload)}), encoding="utf-8")
return path
@ -226,8 +250,7 @@ def write_debate_tools(
Takes already-rendered plain mappings (``explore.tool_call_payload``) so the RAW output layer
stays MAF-free ``write_exploration``'s own rule, same reason."""
directory = Path(outbox_dir)
directory.mkdir(parents=True, exist_ok=True)
path = directory / f"{run_id}-debate.json"
path = _artefact_path(directory, f"{run_id}-debate.json")
path.write_text(
_dump(
{
@ -270,8 +293,7 @@ def write_multibase(
Plain data only, so the RAW output layer stays MAF-free (``write_debate_tools``' own rule)."""
directory = Path(outbox_dir)
directory.mkdir(parents=True, exist_ok=True)
path = directory / f"{run_id}-multibase.json"
path = _artefact_path(directory, f"{run_id}-multibase.json")
path.write_text(
_dump(
{
@ -312,8 +334,7 @@ def write_prepass(
Takes an already-rendered plain mapping (``prepass.declaration_payload``) so the RAW output
layer stays framework-free ``write_exploration``'s rule, same reason."""
directory = Path(outbox_dir)
directory.mkdir(parents=True, exist_ok=True)
path = directory / f"{run_id}-prepass.json"
path = _artefact_path(directory, f"{run_id}-prepass.json")
path.write_text(_dump({"run_id": run_id, "prepass": dict(declaration)}), encoding="utf-8")
return path
@ -341,8 +362,7 @@ def write_plan_review(
the loop has already moved past. Staleness is caught anyway the answer names the
``request_id`` it answers but the file should not invite it."""
directory = Path(outbox_dir)
directory.mkdir(parents=True, exist_ok=True)
path = directory / f"{run_id}-plan-review.json"
path = _artefact_path(directory, f"{run_id}-plan-review.json")
path.write_text(_dump({"run_id": run_id, **dict(payload)}), encoding="utf-8")
return path
@ -369,8 +389,7 @@ def write_proposal_reviews(
validated is a fact this artefact must be able to STATE, not something an operator has to
infer from a missing file. "Iff a reviewer was given" is the only rule that keeps both."""
directory = Path(outbox_dir)
directory.mkdir(parents=True, exist_ok=True)
path = directory / f"{run_id}-proposal-reviews.json"
path = _artefact_path(directory, f"{run_id}-proposal-reviews.json")
path.write_text(_dump({"run_id": run_id, **dict(payload)}), encoding="utf-8")
return path
@ -397,8 +416,7 @@ def write_coverage(
Byte-deterministic and wall-clock-free, mirroring ``write_run_config``; plain data only, so the
RAW output layer stays MAF-free."""
directory = Path(outbox_dir)
directory.mkdir(parents=True, exist_ok=True)
path = directory / f"{run_id}-coverage.json"
path = _artefact_path(directory, f"{run_id}-coverage.json")
path.write_text(
_dump(
{
@ -429,8 +447,7 @@ def write_run_config(
NO wall-clock / date (that lives in the S11 report envelope, not the deterministic artefact), so
two runs with identical config produce byte-identical files (mirrors ``write_outbox``)."""
directory = Path(config_dir)
directory.mkdir(parents=True, exist_ok=True)
path = directory / f"{run_id}-runconfig.json"
path = _artefact_path(directory, f"{run_id}-runconfig.json")
path.write_text(
_dump(
{

View file

@ -257,38 +257,49 @@ def capture_verdict_command(args: argparse.Namespace) -> Mapping[str, Any]:
return verdict_to_dict(verdict)
def _checked_run_id(run_id: str) -> str:
"""A run id NAMES a run; it never carries a path — and the doors enforce that here.
def _checked_name(flag: str, value: str) -> str:
"""A caller-supplied NAME names a thing; it never carries a path — enforced here, as a class.
Every writer composes ``<out_dir>/<run_id>-<artefact>.json``. The caller owns the directory
(its own rule), but the run id went into that composition untouched, so
Every writer composes ``<out_dir>/<name>-<artefact>.json``. The caller owns the directory
(its own rule), but the names went into that composition untouched, so
``--out-dir <d>/inni --run-id ../../X`` wrote two levels ABOVE the directory the caller named
and answered 0. Measured on ``write-prepass`` 20.09; it was never one door's bug — the
composition is the same in all seven, so the guard belongs to the class.
and answered 0. Measured on ``write-prepass`` 20.09.
ONE check, and that is a measurement rather than a preference. The first draft had two this
string rule, plus a second that required the composed path to resolve INSIDE the resolved
output directory. Mutating the second one away left all 36 arms green: after a run id with no
separator, no ``..`` and no absolute prefix, there is no composition that can leave the
directory, so the containment check was unreachable and only looked like defence. It is gone,
and the property it claimed is asserted where it IS reachable the accepting arm of the probe
checks that what the door wrote resolves inside the directory the caller named.
``--run-id`` was fixed as a class across all seven doors and ``--approach-id`` was not, which
missed the point twice over: ``write_outbox`` composes ``{run_id}-{approach_id}``, so the
OTHER half of the same stem was still open ``--approach-id a/../../../ESCAPE`` answered 0
and wrote three levels up (measured 20.09). The class is not "the run id in seven doors", it
is every caller-supplied value that reaches a file name. Counted in ``outbox.py``: 11 path
compositions, two such values, both routed through here.
A refusal (exit 3), never a usage error: the call parsed, and the answer is no."""
separators = {"/", "\\", os.sep} | ({os.altsep} if os.altsep else set())
carries_path = (
not run_id
or run_id in {".", ".."}
or "\x00" in run_id
or any(sep in run_id for sep in separators)
or Path(run_id).is_absolute()
not value
or value in {".", ".."}
or "\x00" in value
or any(sep in value for sep in separators)
or Path(value).is_absolute()
)
if carries_path:
raise ValueError(
f"--run-id {run_id!r}: a run id names a run, not a path — no path separator, "
f"{flag} {value!r}: this names a run, not a path — no path separator, "
"no '..', no absolute path. Name the directory with the output flag instead."
)
return run_id
return value
def _checked_run_id(run_id: str) -> str:
"""``--run-id`` through the shared rule. Kept as a name because seven doors read better for
it, but it computes nothing of its own a second copy of the rule is the one thing this
module must not grow."""
return _checked_name("--run-id", run_id)
def _checked_approach_id(approach_id: str | None) -> str | None:
"""``--approach-id`` through the SAME rule. ``None`` is the flag's honest default (a run
nobody commissioned has no approach to name) and is not a name to check."""
return None if approach_id is None else _checked_name("--approach-id", approach_id)
def _payload_file(path: str, expected: type) -> Any:
@ -369,7 +380,7 @@ def write_outbox_command(args: argparse.Namespace) -> Mapping[str, Any] | Refuse
provenance=stamp,
checker_verdict=args.checker_verdict,
verdict_id=args.verdict_id,
approach_id=args.approach_id,
approach_id=_checked_approach_id(args.approach_id),
)
validated = isinstance(outcome, ValidatedProposal)
payload = {