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:
parent
8a6a09df05
commit
cec9b8fe50
5 changed files with 116 additions and 68 deletions
|
|
@ -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(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue