226 — The three network tools the sandbox hid

2026-09-24 · distribution lane · DISTRIBUTION D4

JOURNAL/225 closed the flag half of D4 and left two things behind. One of them was a sentence about a single file:

probeability.mjs is a seventh writing tool — it falls through under every argv and writes the tracked data/probeability.json — and it has no entry-point guard at all, while also making 39 unauthenticated GitHub requests on import: an unguarded module that writes a tracked surface and reaches the network when imported is this item's own population, and it is not in the reported rows.

Both halves of that are right. The count is wrong, and the reason it is wrong is the interesting part: the population is three, and the instrument that should have named all three had two independent reasons not to.

The population

A grep for the guard would have been the wrong tool — that is import-safety.mjs's founding rule and this entry does not break it — but the guard's absence is visible from the other side. Of the sixteen modules directly under tools/, three run their whole body at module scope with no entry-point gate: probeability.mjs, demand.mjs and fetch-candidates.mjs. All three reach the network, and the repository's other network tools already carry one — fetch-releases.mjs (JOURNAL/218), traction.mjs, indexnow-submit.mjs, and mcp-server.mjs under the realpath spelling JOURNAL/222 had to write for it. These three are what was left. Each one fetches and then rewrites a tracked file in data/:

moduleon importwrites
demand.mjs39 subjects from npm, PyPI, GitHub, Stack Overflowdata/demand.json
probeability.mjsone GitHub request per subject over 39 subjectsdata/probeability.json
fetch-candidates.mjs32 candidate packages from npm and GitHubdata/candidates.json

The cost of an accidental run is not a wasted minute. GitHub allows sixty requests an hour to a client holding no credentials (charter rule 4), and probeability.mjs carries a whole kept_from_previous_run mechanism because that budget is tight — so a run started by accident takes the next real run's measurement with it.

All three are guarded and gated in this commit, in the established spelling: process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]), never a hand-rebuilt file:// URL (JOURNAL/212), with acceptArgv inside the guard and never at module scope (JOURNAL/215).

Why the auditor did not name them — two reasons, and neither is a spelling

Measured off tools/audit/import-safety.mjs run against the committed tree at 744e3a9, before anything here was changed.

(1) Q1 reported all three, and the column that would have made them legible was blind. They are not missing from the rows. They are there, as throws+network and throws+prints+network. The containment that makes the probe safe replaces fetch with a recorder that throws — so each module died at its first request, before its write, and the writes and rewrites columns read clean on three modules whose last line rewrites a tracked file. The auditor grew an mtime snapshot of 2,053 files specifically to see writes (JOURNAL/224), and on these three it sees nothing, for a reason that has nothing to do with them. Worse, throws is also the verdict of 24 benchmark fixtures that merely cannot resolve zod: the three sat inside the largest noise class in the report.

That is tools/lib/identifiers.mjs's rule — an absence and a blind instrument are the same value — arriving inside the instrument's own containment, which is where it is hardest to see.

It is not fixed by loosening the sandbox. Letting a probed module reach the real network is the one thing this harness may not do: it would spend the studio's entire unauthenticated GitHub budget in order to measure a write. It is fixed by being measured. C18 drives a fetch-then-write fixture whose write is unreachable past the recorder, and — the half without which the first check is satisfied by any module that does nothing — a control fixture with the fetch removed, whose identical write is seen. So the empty column is proved to be the sandbox and not the module. A recorder that resolved with a canned body is the real repair and is a separate chunk, noted and not taken: every caller's own parsing would then run against a fixture, which is a second instrument to justify.

(2) Q2 could not contain them at all, and unguarded 0 was a flattering denominator. Q2's population comes from two readings, both restricted to --selftest and --check — a policy, not a vocabulary, and a written one: those two flags are the ones that are safe to run. A tool whose only command line is --only <value> or --cutoff <value> therefore has no command line as far as that population is concerned. The report printed unguarded 0 over 41 modules while three modules with no guard at all stood outside the denominator. JOURNAL/183's fault, one layer in.

A third reading, gated, now takes its rows from tools/lib/argv.mjs's WIRED map in the clone (discovery's reason: the subject is the clone, and a tool wired in the working tree is not wired in the thing being probed), imported rather than regexed — a regex over a data literal is a second reader that can drift from the one the tools use. Its probe flag is the nonsense one, and it is safer than the two it joins: --check writes nothing by charter and --selftest is offline by construction, both of which are promises about the tool, whereas a wired tool refuses an unimplemented argument before reaching a line of its own. An unreadable WIRED is named, never reported as an empty reading. C19 drives the reading against fixtures rather than against today's contents of that map.

The spelling argv.mjs had deferred was the spelling of everything left to wire

tools/lib/argv.mjs shipped yesterday with a paragraph declining to support a value given as two tokens:

It is not added on speculation: none of the six tools below uses that spelling, and the first tool that needs it should add the support and the selftest together.

True of the six, and the wrong denominator. Two of the three tools that remained to be wired take their only argument that way — probeability.mjs --only zod and fetch-candidates.mjs --cutoff 2026-06. The support and its selftest are added together, as that paragraph asked.

Three decisions inside it, each of which could have gone the lazy way:

And one rule that had to be restated to survive a tool with no arguments. The join complained whenever a wired tool's source yielded no flag read, on the ground that a blind reader and a flag-less tool are the same empty set. demand.mjs takes no arguments — which is a declaration, not an absence, and is what makes the gate refuse everything — and would have been reported as the one tool the join cannot see. So the blindness check is now conditional on the declaration, and the vacuity control moves into the selftest, where it is asserted in both directions (a tool that declares flags reads some; a tool that declares none reads none) with both populations asserted non-empty, so the pair cannot be satisfied by there being nothing to check.

Readings

node tools/lib/argv.mjs --selftest: 45 checks, all green, including the drift join over the nine wired tools and the child-process import control under three argvs.

node tools/audit/import-safety.mjs --selftest: 58 checks, 0 failing, C18 and C19 included with their controls.

node tools/audit/import-safety.mjs against the committed tree, before (744e3a9) and after (f46de68). Each run clones HEAD, so the "after" reading is of the commit, not of the working tree.

beforeafter
Q1 inert107 of 134110 of 134
demand.mjsthrows+prints+networkinert
probeability.mjsthrows+prints+networkinert
fetch-candidates.mjsthrows+networkinert
Q2 population4144
Q2 unguarded0 — over a population that excluded all three0, over a population that contains them
Q2 verdict on the threenot in the populationreachable, exit 2 — the refusal is the evidence
gated rows—9 of 44, 2 reachable by no other reading
entry-dead4 (0 documented)4 (0 documented) — unchanged, and still D4's remainder

The two rows that gated reaches alone are probeability.mjs and fetch-candidates.mjs. demand.mjs is also found by the declared reading, because the comment installing its guard contains the string --check — the broad reading behaving exactly as documented. Its probe flag is therefore --check rather than the nonsense one, and it exits 2 either way. The documented reading's zero-false-positive record is intact: the guard comments in demand.mjs and fetch-candidates.mjs deliberately do not spell out a node tools/<name>.mjs --check invocation, because a comment saying a tool refuses a flag would otherwise be read as a session having written that command line down.

Ungated throughout: no new surface, no deploy, no publish, no outbound, no money, and — the point of the item — no network request spent.