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

@ -32,12 +32,20 @@ at all, and this release gives each of them a command of its own.
report share one module entry point without a subcommand that tells them apart.
### Security
- A `--run-id` that carries a path is refused by all seven outbox doors before anything is written.
Each writer composes `<out_dir>/<run_id>-<artefact>.json`; the directory was guarded and the run
id was not, so `--outbox-dir <d>/inni --run-id ../../X` wrote two levels above the directory the
caller named and answered `0`. Separators, a bare `.` or `..`, an absolute prefix, an embedded
NUL and the empty string are now refused with exit `3`, the reason named, and no directory left
behind. Fixed as a class, because the composition is identical in all seven.
- A caller-supplied **name** that carries a path is refused by all seven outbox doors before
anything is written. Each writer composes `<out_dir>/<name>-<artefact>.json`; the directory was
guarded and the names were not, so `--outbox-dir <d>/inni --run-id ../../X` wrote two levels
above the directory the caller named and answered `0`. Separators, a bare `.` or `..`, an
absolute prefix, an embedded NUL and the empty string are now refused with exit `3`, the reason
named, and no directory left behind. The class is every caller-supplied value that reaches a
file name, not one flag: `write-outbox` composes `<run_id>-<approach_id>`, and
`--approach-id a/../../../X` escaped the same way through the other half of that stem. Both go
through one rule.
- Containment as the last link: a composed artefact path that does not resolve **under** the
output directory is refused, in `outbox` itself, where all eleven compositions are built and
before the directory is created. The string rule above lives in the door, so this one covers the
callers that never pass a door - the run path hands its own `--run-id` straight to these writers
- and the next value someone interpolates into a file name.
### Fixed
- `--stop-reason` being required is now guarded by a test rather than only by prose. The rule is

View file

@ -3576,25 +3576,37 @@
vei til å produsere rapporten — ikke en dør over et eksisterende steg. Å gi proben et navn i
argv ved å døpe en katalog «rapport» ville vært å tøye proben for å få tallet. Rad 1 er derfor
**15 av 17**, uttalt, ikke 17.
- **`--run-id` navngir en kjøring, aldri en sti (20.09):** de sju utboks-dørene bygger filnavnet
som `<utkatalog>/<run_id>-<artefakt>.json`. Utkatalogen var voktet (den er alltid kallerens å
navngi); run-id-en gikk urørt inn i den samme sammensetningen. MÅLT på `write-prepass`:
- **Et kaller-oppgitt NAVN bærer aldri en sti inn i et filnavn (20.09):** de sju utboks-dørene
bygger filnavnet som `<utkatalog>/<navn>-<artefakt>.json`. Utkatalogen var voktet (den er alltid
kallerens å navngi); navnene gikk urørt inn i den samme sammensetningen. MÅLT på `write-prepass`:
`--outbox-dir <kat>/inni --run-id ../../ESCAPE` ga **exit 0** og la artefaktet TO nivåer OVER
katalogen kalleren navnga. Vakten sto på den ene halvdelen av stien og manglet på den andre.
`_checked_run_id` avviser nå en run-id som BÆRER en sti — skilletegn (`/`, `\`, `os.sep`,
`os.altsep`), bar `.` eller `..`, absolutt prefiks, innleiret NUL, tom streng — FØR noen dør
rører filsystemet: exit **3** med grunnen navngitt og ingen katalog lagt igjen. Rettet som
KLASSE i alle sju, fordi sammensetningen er den samme i alle sju.
**ÉN sjekk, ikke to, og det er en MÅLING.** Første utkast krevde i tillegg at den sammensatte
stien resolverte INNI den resolverte utkatalogen. Den mutanten overlevde alle 36 armer: etter
strengregelen finnes det ingen sammensetning som KAN forlate katalogen, så inneslutnings-
sjekken var uoppnåelig og lignet bare på forsvar. Den er borte, og egenskapen den påsto
asserteres der den ER oppnåelig — i probens aksepterende arm.
katalogen kalleren navnga. `_checked_name` avviser nå et navn som BÆRER en sti — skilletegn
(`/`, `\`, `os.sep`, `os.altsep`), bar `.` eller `..`, absolutt prefiks, innleiret NUL, tom
streng — FØR noen dør rører filsystemet: exit **3** med grunnen navngitt og ingen katalog lagt
igjen.
**KLASSEN ble først satt for smalt, og det er rundens funn.** Første rettelse kalte klassen
«run-id-en i sju dører» og var da fortsatt usann: `write_outbox` bygger stammen
`{run_id}-{approach_id}`, så den ANDRE halvdelen av samme sammensetning sto åpen. MÅLT 20.09:
`--approach-id a/../../../ESCAPE` (med `<ut>/<run_id>-a` på plass) ga **exit 0** og la
artefaktene TRE nivåer over kallerens katalog. Klassen er hver kaller-oppgitt verdi som når et
filnavn — TELT i `outbox.py`: **11 stisammensetninger, 2 slike verdier**, begge gjennom samme
regel.
**Inneslutningen STÅR, som siste ledd, og prosaen om at den var uoppnåelig var USANN.** Den ble
fjernet i første rettelse fordi mutanten overlevde alle 36 armer, og begrunnelsen var at ingen
sammensetning KAN forlate katalogen etter strengregelen. Approach-id-rømningen er motbeviset:
den ville tatt akkurat dette. Sjekken bor nå der sammensetningen skjer — `outbox._artefact_path`,
som alle 11 går gjennom — og ikke i døren. Dét er forskjellen som gjør den oppnåelig: strengregelen
bor i døren, mens `run.py` sender sin egen `--run-id` rett inn i skriverne og går utenom den. Den
verner også mot det NESTE flagget noen legger inn i et filnavn uten å huske døren. Sjekket FØR
katalogen opprettes, så en nekt ikke legger igjen noe.
**Load-bearing test:** `tests/test_toolbox_run_id_containment.py` — sju dører × fire stibærende
former, hver arm asserterer ikke-null exit OG at filtreet under `tmp_path` er byte-uendret
etterpå (den første alene ville vært tilfreds med en dør som avviste ETTER å ha skrevet), med
en rc-0-kontroll per dør. Mutanter: vakten fjernet fra én dør → 4 armer faller; hele vakten av
→ alle 28 rømningsarmer faller.
run-id-former, `--approach-id` × fire former mot den ene døren den når, og seks skrivere kalt
DIREKTE for inneslutningen. Hver arm asserterer ikke-null exit (eller `ValueError`) OG at
filtreet under `tmp_path` er byte-uendret etterpå (den første alene ville vært tilfreds med en
dør som avviste ETTER å ha skrevet), med en rc-0-kontroll per dør og per skriver. To av
approach-id-armene var grønne ved HEAD på en manglende mellomkatalog før hver form fikk sin
opprettet — en grunn som ikke er vakten. Mutanter: vakten fjernet fra én dør → 4 armer faller;
hele vakten av → alle 28 rømningsarmer faller.
- **`--stop-reason` er PÅKREVD, og regelen har nå en vakt (20.09):** «kjøringen ble ferdig» og «vi
fikk aldri vite» må ikke være samme verdi. Koden var riktig, men `required=True` byttet til
`default=""` passerte HELE suiten — regelen var bærende i prosa og uvoktet i test.

View file

@ -163,7 +163,7 @@
<!-- tittel -->
<section class="slide divider">
<div class="inner">
<p class="kicker"><span class="part">portfolio-optimiser · v1.1.0 · teknisk gjennomgang</span></p>
<p class="kicker"><span class="part">portfolio-optimiser · v1.2.0 · teknisk gjennomgang</span></p>
<h1>Agentene foreslår. En beregning avgjør. Fagfolk dømmer.</h1>
<p class="lede">Et åpent Python-rammeverk på Microsoft Agent Framework som leter etter kostnadsbesparelser <b>inni</b> hvert prosjekt i en portefølje: arkitekturen, tilbakemeldingssløyfene og harnessen, fra formålet ned til hver enkelt søm.</p>
<p class="small">For AI-arkitekter og utviklere · inntil 60 minutter · alle påstander har kildelinje til repoet (<code>git.fromaitochitta.com/open/portfolio-optimiser</code>, HEAD 50c9763, 2026-09-18)</p>
@ -3147,7 +3147,7 @@ Produce a REVISED SavingsProposal that resolves this.</code></pre>
<div class="inner">
<p class="kicker"><span class="part">Del 7 · Etter v1.1</span></p>
<h1>Etter v1.1</h1>
<p class="lede">v1.1.0 ble sluppet 14.08.2026, og det er den siste utgivelsen. Alt etter den datoen er måling. Denne delen sier hva planen kaller destinasjonen, hvor langt unna vi er, og hva som med vilje ikke skal bygges.</p>
<p class="lede">v1.2.0 er repoets versjon, satt 20.09.2026; v1.1.0 ble sluppet 14.08.2026 og er den siste TAGGEDE utgivelsen. Alt etter den datoen er måling. Denne delen sier hva planen kaller destinasjonen, hvor langt unna vi er, og hva som med vilje ikke skal bygges.</p>
</div>
</section>
@ -3168,7 +3168,7 @@ Produce a REVISED SavingsProposal that resolves this.</code></pre>
</div>
<div class="col soft">
<span class="step">Hva som endret seg i planen</span>
<p>«v1» ble omdefinert fra en tag til en produkttilstand — repoet står allerede på 1.1.0. Kriteriet ble skrevet så det kan FELLES, og en ny syntetisk stressrunde 7 er eksplisitt forbudt.</p>
<p>«v1» ble omdefinert fra en tag til en produkttilstand — repoet står allerede på 1.2.0. Kriteriet ble skrevet så det kan FELLES, og en ny syntetisk stressrunde 7 er eksplisitt forbudt.</p>
</div>
</div>
<p class="callout warn"><b>En ramme planen ber om at ikke skjules:</b> én person er bestiller, fagperson og operatør. «Det er en kjent begrensning.» Loggen i <code>PLAN.md</code> har åtte rader, og seks av dem sier «ingen bevegelse på kriteriet». En serie av dem er i seg selv et funn.</p>

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 = {