Skip to content

docs: when each job-user refusal fires, and registration is in-tree on purpose (#342, #357) - #371

Merged
edgehero merged 4 commits into
mainfrom
docs/backends-job-user-timing
Sep 21, 2026
Merged

edgehero merged 4 commits into
mainfrom
docs/backends-job-user-timing

Conversation

@edgehero

@edgehero edgehero commented Sep 21, 2026 •

Copy link
Copy Markdown
Owner

The last two items of the #345 leftovers. Both live on docs/backends.md, which had no test of any kind
before this PR
-- not incidental: its refusal sentence named seven of the code's eight causes in six
clauses, and omitted runtime-unreadable entirely.

When a refusal fires (#357 item 3)

causes what happens
Stops the boot rootless, userns-remap, worker-is-root, desktop-linux-userns nothing a job may run as works, so the worker exits rather than picking up work it could only refuse, while local is the default venue
Refuses each job runtime-unreadable, root-group, docker-group, any-uid-unsupported the worker boots; each local job returns a policy refusal naming the cause
Neither a daemon that has not answered yet unknown, never cached, never a boot exit, so RestartPreventExitStatus=2 is not stranded by a daemon still starting. A job picked up meanwhile is an infra retry, not a refusal (CONST-RETRY-INFRA-ONLY)

The bolt, and why it is a generator

worker/test/backends-doc.test.mjs builds each list line from JOB_USER_FIX and
jobUserBootRefusal and requires the page to carry it verbatim, trimmed, at the start of exactly one line.
It does not parse the page.

It started as a parser, and that is the useful part of this PR. An adversarial reviewer with one brief,
"write a version of this paragraph that is wrong and still green", got eleven wrong pages past the
first version and ten past the hardened one. Every hardening bought exactly one round, because each
attack was a new hiding place rather than a new idea:

hiding place what the reader saw
a correct copy inside an HTML comment the inverted visible bullets
a comment closed by a plain --> in ordinary prose the stripper ate the visible text around it
a link reference definition renders as nothing; the extractor read it
non-breaking hyphens in a cause name renders identically to the real name
a decoy marker pair earlier in the file the real block, unread
the block's own end marker moved up the false prose now outside it

The hardening had also begun failing an honest page: the per-image refusal id
job-image-any-uid-unsupported contains any-uid-unsupported as a bare substring.

So the rule was replaced rather than widened a third time. There is now one correct string and the page
either has it or does not, which closes that class by construction; the markers are gone with the extractor.
A second rule counts each cause's backticked form page-wide and requires exactly one, which kills a
contradicting copy elsewhere on the page and cannot collide with the longer id.

The residual is prose, and this is the third attempt at stating it -- the first two overstated the
test's reach, in exactly the way this file exists to prevent. Only the two lines and the cause names are
derived. A third bullet, a claim that the headings were swapped in some release, a redefinition of "the
boot" as the job container's, an invented promise about pi-dispatch doctor: all demonstrated green, none
reachable by a pattern. podman-doc.test.mjs records the same limit and what four rounds of trying to
pattern it cost.

Registration is in-tree (#342)

## Registering it read as a gap ("there is no published import for startWorker today"). It reads as a
decision now. Exporting startWorker would make the worker's entire dependency-injection bag a public API a
release has to keep, because that bag is the second argument. What makes the choice cheap is already on
the page: BACKENDS_TABLE's entry is in-tree by necessity, so an adapter already lands one file here and
loses nothing by landing a registration beside it. Its code can still live anywhere.

#342 asked to be decided alongside the Podman route, and this decides it: #354's podman backend is
specified in-tree
, so it needs no export and nothing here blocks it. Reopening the question needs two
things, not one -- an export and a way to declare a venue out of tree -- because BACKENDS is frozen at
module load, parseBackendList refuses an unknown PI_BACKENDS name, and validateBackend refuses a
trigger naming one. The second is what the in-tree decision deliberately refuses. Nobody has asked for either.

DES-CONTAINER-BACKEND-REGISTRY AMENDED with a bullet, not a revision row alone, because the entry says
the table exists so a venue can be added "without reading the worker's source" and that an adapter is
"written elsewhere by someone who will never run npm test here". In-tree registration is new ground against
that framing, so the reconciliation is written inside the entry: both sentences stay true of the adapter,
and neither was ever true of its declaration.

The last measurement (#357 item 4, second bullet)

docs/egress.md hedged an external name resolved directly from an internal network as "about 10 seconds on
the host this was first measured on". #357 asked for a fresh measurement on Docker Desktop, which is what
this is (Docker Desktop 27.4.0, macOS). The hedge was about the wrong variable: the resolver inside the
container decides it.

client result times (ms)
the job image (Debian, glibc) EAI_AGAIN 14, 6, 10, 9, 23, 12
node:22-slim (glibc) EAI_AGAIN 13, 29, 7
node:22-alpine (musl) EAI_AGAIN 5013, 5025, 5024, 5015, 5017

The embedded resolver's upstream is not reachable from an --internal network, so the forward fails rather
than hanging, and what each client does with that failure is the whole difference. (Six job-image runs: one
in the first batch, five in the second.) The job image is glibc,
so the number describing a job here is milliseconds. The ten-second figure did not reproduce on this host in
any client, and what it was measured against is not known, so it is replaced rather than accounted for.

Specs

  • DES-CONTAINER-BACKEND-REGISTRY AMENDED (above).
  • DES-JOB-USER-INFERRED-READ-BACK-ON-REQUEST UNCHANGED, checked, and cited: its residual already carries
    the conditional this paragraph states, corrected earlier in this round under doctor: the job-user read-back's leftover test gaps and wording after #347 #348, so the page and the entry
    land agreeing rather than one correcting the other.
  • DES-PODMAN-THROUGH-ITS-DOCKER-API and DES-WORKER-ON-HOST UNCHANGED, checked.
  • No INT-, REQ- or CONST- entry changes, checked: no contract, file, flag, argv or log line moves.
  • worker/package.json's exports map is byte-unchanged and worker/test/publish.test.mjs still passes, which
    is the check that would catch an export slipping in beside the prose.

Verification

3993 tests, 0 fail, 1 skipped in the CI posture with a live Valkey, plus dated-fixture-check,
test-count-check and temp-dir-check. Every attack above replayed against the generator, plus a source
mutation the page was not updated for: all red, and the honest page that the previous rule failed is green
again.

The final review then beat the generator three more ways, all from one cause: removing the extractor removed
its comment strip, and a line inside a multi-line comment is still a line. The realistic instance is the
worst: deleting the single --> that closes the page's own instruction comment swallows both bullets and
leaves the suite green. The strip is back with an unclosed-comment refusal beside it. That cannot reopen the
arms race, and the direction is why: a stripper can only remove candidate lines, so its worst outcome is a
false red on an honest page, never a pass on a wrong one. All three replay red.

Two gate rounds confirmed defects, so this is the round cap's one simpler-rule fix, followed by one final
re-review.

Closes #342. Closes #357.

That second one is the whole cluster: item 1 landed in #359, item 2 in #364, item 4's first bullet in #368,
and item 3 with item 4's second bullet here.

…n purpose (#342)

The last two items of the #345 leftovers, and the page they both live on had no test of any kind before
this commit. That is not incidental: its refusal sentence named SIX causes where the code has eight, and
omitted `runtime-unreadable` entirely, which is exactly what an unpinned restatement does given three
rounds to drift.

WHEN A REFUSAL FIRES (#357 item 3). The job-user bullet now names both sets instead of one. Four causes
stop the boot (`rootless`, `userns-remap`, `worker-is-root`, `desktop-linux-userns`), because no uid on
such a host can serve a job and the worker exits rather than picking up work it could only refuse. Four
refuse each job (`runtime-unreadable`, and on the `--user` path `root-group`, `docker-group`,
`any-uid-unsupported`), where the worker boots and every local job returns a policy refusal naming the
cause. What is in NEITHER set is said too: a daemon that does not answer reads `unknown`, never cached and
never a boot exit, so a unit carrying `RestartPreventExitStatus=2` is not stranded by a daemon that is
still starting; and a job picked up while the answer is unknown is an infrastructure RETRY rather than a
refusal (`CONST-RETRY-INFRA-ONLY`: `processor.mjs` throws there, where a policy refusal returns).

THE BOLT, which CLAUDE.md makes non-optional for a hand-written restatement and which this page has never
had. `worker/test/backends-doc.test.mjs` reads the two lists from between markers and bolts them to
`jobUserBootRefusal(decision, defaultBackend)`, NOT to `BOOT_REFUSING_JOB_USER_CAUSES`. The distinction is
the point: the page's claim is not that four names sit in a Set, it is that they stop a boot AND only
while `local` is the default venue, and only the function carries the second half. It is exported for
precisely that reason, so a one-venue build still pins the branch it cannot reach. Four mutations were
applied and all four are red: a member added, removed, renamed, and the venue condition dropped. Two more
properties ride along, each of which a drifting page would break first: every name the page prints is a
real `JOB_USER_FIX` cause, and the two lists together are exactly that key set, so a cause cannot be
dropped from the page or filed under both headings.

The prose around the lists is deliberately unpinned, and `worker/test/podman-doc.test.mjs`'s header now
records why at length: a regex over prose pins a claim's shape and never its truth. That page paid four
review rounds to learn it, and this test bolts the lists to a function instead of widening a regex, which
is the same lesson spent rather than restated.

REGISTRATION IS IN-TREE (#342). `## Registering it` read as a gap: "there is no published import for
`startWorker` today". It reads as a decision now, and the decision is argued rather than asserted. The
alternative was exporting `startWorker`, or a narrower `"./start"`, and both make the worker's entire
dependency-injection bag a public API a release has to keep, because that bag is the second argument.
What makes the choice cheap is on the page already: `BACKENDS_TABLE`'s entry is in-tree BY NECESSITY,
since a venue nobody can read a declaration for is a venue nobody can reason about, so an adapter already
lands one file here and loses nothing by landing a registration beside it. Its code can still live
anywhere. What reopens it is named: an adapter that cannot live in this repository at all, of which
`podman` is the live example, which is why #342 asked to be decided alongside the Podman route. In-tree
it needs no export; out-of-tree it needs exactly this entry point, and #354 is where that lands.

`DES-CONTAINER-BACKEND-REGISTRY` AMENDED with a bullet rather than a revision row alone, because the entry
says the table exists so a venue can be added "without reading the worker's source" and that an adapter is
"written elsewhere by someone who will never run `npm test` here". Declaring registration in-tree is new
ground against that framing, so the reconciliation is written inside the entry: both sentences stay true
of the ADAPTER, and neither was ever true of its declaration.

THE LAST MEASUREMENT (#357 item 4, second bullet). `docs/egress.md` hedged an external name resolved
directly from an internal network as "about 10 seconds on the host this was first measured on". Measured
on docker 27.4.0, and the hedge was about the wrong variable: it is the resolver INSIDE the container that
decides. The job image, Debian and glibc, gives up in about 10 ms (six runs, 6 to 23, all `EAI_AGAIN`)
because it takes docker's embedded resolver's failure as final; a musl image on the same network waits out
musl's own 5 s timeout (five runs, 5013 to 5025 ms). So the number that describes a job here is
milliseconds, and a multi-second reading is a property of the client rather than of this design.

`DES-JOB-USER-INFERRED-READ-BACK-ON-REQUEST` UNCHANGED, checked, and cited: its residual already carries
the conditional this paragraph states, corrected earlier in this round under #348, so the page and the
entry land agreeing rather than one correcting the other. `DES-PODMAN-THROUGH-ITS-DOCKER-API` and
`DES-WORKER-ON-HOST` UNCHANGED, checked. No `INT-`, `REQ-` or `CONST-` entry changes, checked: no
contract, file, flag, argv or log line moves. `worker/package.json`'s exports map is byte-unchanged and
`worker/test/publish.test.mjs` still passes, which is the check that would catch an export slipping in
beside the prose.

3991 tests, 0 fail, 1 skipped in the CI posture with a live Valkey, plus dated-fixture-check,
test-count-check and temp-dir-check.

Closes #342.

Signed-off-by: Rob Boerman <robboerman@live.nl>
…342, #357)

Two reviewers, one checking every sentence against source and one briefed to write a version of the timing
paragraph that is wrong and still green. The second succeeded eleven times out of seventeen, which is the
more useful half of this round.

THE BOLT WAS PINNING THE WRONG THING. The two lists were genuinely derived (a straight cause swap died at
once), but nothing pinned WHICH TEXT the extractor read, and everything after each bullet's first full stop
was free. Six classes of wrong page passed:

- A correct copy of both lists hidden in an HTML COMMENT satisfied every assertion while the rendered
  bullets said the opposite. Markdown hides comments from a reader; `indexOf` reads raw bytes. The same
  trick hid one cause from the reader alone, mid-list.
- A second marker pair pasted higher up the page made the test read a decoy and leave the real block free.
- Prose after the first full stop reassigned a cause outright ("`worker-is-root` no longer stops the boot").
- A cause named without backticks was invisible to the regex and plain to a reader.
- The venue condition, which the header calls the page's real claim, is prose: "that exit is unconditional,
  whatever the default venue is" passed everything.
- Prose after the closing marker contradicted the lists in the next paragraph.

The repair, and the shape of it matters more than the list: slice AFTER the opening marker and strip
comments, so the test reads what a reader reads; require exactly one marker of each kind; require every
cause to be named exactly ONCE anywhere in the block, which is what makes the lists mean something rather
than merely exist and which kills the reassignment, the un-backticked name and the hidden name together;
and widen the markers to enclose the whole passage rather than the two bullets. All seven attacks are red
now, replayed one at a time.

The conditional half gets the weakest assertion in the file: a required phrase with the venue name derived
from `DEFAULT_BACKEND` rather than typed. Behaviour can be derived and a page's SILENCE about it cannot,
and the alternative is a page free to call the boot exit unconditional while its own test proves otherwise.
It is named as the weakest thing here rather than left to look like the rest.

THE RESIDUAL IS WRITTEN INTO THE TEST rather than papered over. A sentence naming NO cause is invisible to
all of this: "`pi-dispatch doctor` clears all four boot-stopping causes, so run it once and the worker is
guaranteed to start" would be false and green. That is the same limit `podman-doc.test.mjs` records, and
this round's whole lesson is that the answer to it is not a bigger pattern.

THE CLAIM CHECKER FOUND ONE FALSE SENTENCE, and it is the one I would least have wanted to ship: the page
cited issue #354 as the live example of an adapter that cannot live in this repository. #354 specifies an
IN-TREE backend in its own words, "It declares its own words in the backend table" and "It implements the
adapter contract in `docs/backends.md`", so by this page's own rule it needs no export. The page cited as
its example of an unhostable adapter the one issue describing a hosted one. It now says what is true: #342
asked to be decided alongside the Podman route and this decides it, #354 needs no export and nothing here
blocks it, and what would reopen the question is an operator installing from npm who wants to register with
no checkout at all.

Three smaller corrections, each verified against source first:

- "named SIX causes where the code has eight" undercounts. The old sentence's sixth clause covered two
  causes, so it named seven of eight in six clauses, and `runtime-unreadable` was the only omission.
- "No uid on such a host can serve a job" over-generalises `worker-is-root`, which is a fact about the
  account the worker runs as and not about the host. That is the exact class this round spent two gate
  rounds unwinding one file away.
- #357 item 4's second bullet asked for the measurement ON DOCKER DESKTOP, and it was: the lab's
  `ExtServers` is Docker Desktop's VM gateway. Said explicitly now. And the old ten-second figure is no
  longer explained by the glibc/musl split, because the job image is glibc on both hosts: it did not
  reproduce here, what it was measured against is not known, and it is replaced rather than accounted for.

One unrelated flake seen while gating, in a file this diff does not touch: `receiver/test/start.test.mjs`'s
triggers-watch poll failed once in two full runs and passes 3/3 in isolation. Added to issue #369, which
already carries the same class from `start-wiring.test.mjs`, because the fix is one decision about what
deadline real-time polls get under full-suite load, not two.

`DES-CONTAINER-BACKEND-REGISTRY` AMENDED further, the same bullet and revision row. No other `DES-`, `INT-`,
`REQ-` or `CONST-` entry changes, checked.

3993 tests, 0 fail, 1 skipped in the CI posture with a live Valkey, plus dated-fixture-check,
test-count-check and temp-dir-check.

Refs #342, #357, #369.

Signed-off-by: Rob Boerman <robboerman@live.nl>
…#357)

Round two of the gate. The adversarial reviewer got ten more wrong pages past the hardened test and, more
usefully, named the reason: every repair so far had been a better PARSER, and a parser is an arms race the
page always wins. The round cap asks for one simpler rule at this point, and this is it.

THE TEST NO LONGER READS THE PAGE'S LISTS. It BUILDS each line from `JOB_USER_FIX` and `jobUserBootRefusal`
and asserts `docs/backends.md` carries it, trimmed, at the start of exactly one line. Nothing is extracted,
so there is nothing to hide anything from: there is one correct string and the page either has it or does
not. That closes the whole class in one move, and I checked each member rather than assuming it. Dead now:
a correct copy inside an HTML comment; a comment closed by a plain arrow in ordinary prose, so the stripper
ate the visible text around it; a link reference definition, which renders as nothing; non-breaking hyphens
that render identically to the real name; a decoy marker pair earlier in the file; the block's own end
marker moved up to narrow what was policed. The markers are gone with the extractor, which also removes a
reported cost of having them: the page could never quote its own marker without failing four tests.

A SECOND RULE, page-wide: each cause's BACKTICKED form must appear exactly once in the whole file. That kills
a contradicting copy of the lists somewhere else on the page, which the block-scoped version allowed, and it
fixes a FALSE RED the hardening had introduced. The per-image refusal id `job-image-any-uid-unsupported`
contains `any-uid-unsupported` as a bare substring, so an honest sentence naming the id turned the suite red;
counted in its backticked form there is no collision. A test that fails honest edits erodes, and this one had
started to.

THE RESIDUAL IS PROSE, and this is the third attempt at stating it, the first two having overstated the
test's reach in exactly the way this file exists to prevent. Only the two lines and the cause names are
derived. A third bullet, a sentence claiming the two headings were swapped in some release, a redefinition
of "the boot" as the job container's rather than the worker's, an invented promise that `pi-dispatch doctor`
clears the boot-stopping causes: all demonstrated green, none reachable by a pattern. Widening again is what
this round has twice proved does not work, and `podman-doc.test.mjs` already records what four rounds of
trying cost. The answer to prose that keeps being wrong is fewer such sentences.

THE CLAIM CHECKER FOUND THREE THINGS, and one of them is a pattern rather than a fact. The `#354` sentence
corrected last round was fixed on the page and in the design ENTRY, and left standing verbatim in the
revision ROW that summarises that same amendment. A sentence fixed where it was pointed out and not at its
second site is the failure mode, so the row now says so about itself.

The replacement for it was also false, which the page refutes eight lines above. I wrote that reopening the
in-tree decision needs "an operator installing from npm who wants to register an adapter with no checkout",
and that an export would serve that case. It would not: `BACKENDS` is frozen at module load, `parseBackendList`
refuses a `PI_BACKENDS` name it does not know, and `validateBackend` refuses a trigger naming one, so such an
operator still cannot register anything. Verified by driving all three. Reopening needs an export AND a way
to declare a venue out of tree, and the second is what the paragraph above deliberately refuses.

And the daemon/account split I added was narrower but still inexact: `rootless` is not always a daemon fact.
`decideJobUser` also infers it from the socket this worker was pointed at (`euid !== 0 && socket.uid === euid`),
which is a systemd `SocketUser=` override on a rootful daemon, fixed by re-pointing the worker rather than by
changing the host. The page says that now.

Two smaller repairs: the egress bullet had lost the clause explaining WHY the embedded resolver fails when I
added the host shape, so it read as predicting success three lines under a bullet saying the gateway is
reachable; and `specs/design.md` recorded the measurement as "docker 27.4.0" without saying Docker Desktop,
which is the one fact that discharges what #357 actually asked for.

`DES-CONTAINER-BACKEND-REGISTRY` AMENDED further, the same bullet and revision row. No other `DES-`, `INT-`,
`REQ-` or `CONST-` entry changes, checked.

3993 tests, 0 fail, 1 skipped in the CI posture with a live Valkey, plus dated-fixture-check,
test-count-check and temp-dir-check.

Refs #342, #357.

Signed-off-by: Rob Boerman <robboerman@live.nl>
The final re-review of the round cap. It found no defect in what `docs/backends.md` says, and one in what
this branch had started saying about its own test, which is the failure mode this file exists to prevent
arriving through the file itself.

REMOVING THE EXTRACTOR REMOVED ITS COMMENT STRIP. The generated line is matched against trimmed lines of the
page, and a line inside a multi-line HTML comment is still a line beginning `- **`. Three wrong pages went
green: both bullets moved into a comment with the visible prose inverted; the whole section deleted with a
copy buried at the end of the file; and the one that matters, deleting the single ` -->` that closes the
page's OWN instruction comment, one token directly above the bullets, which swallows them both and leaves
the suite green. A one-character edit to the comment this branch added silently un-pins both lists.

The strip is back, with an unclosed-comment refusal beside it, and this does not reopen the arms race the
extractor lost. The direction is the argument: a stripper can only REMOVE candidate lines, so its worst
outcome is a false red on an honest page and never a pass on a wrong one. That is the opposite of the
extractor, whose every failure was a wrong page passing. All three replay red and the honest page is green.

THREE SENTENCES ASSERTED THE CLASS WAS CLOSED and were false until that fix: "no hiding place helps because
nothing is extracted" in the test header, "a copy inside an HTML comment is not a line beginning `- **`" at
the match itself, and "Nothing is parsed, so a hiding place buys nothing" in the revision row. All three now
say what is true, which is that the guarantee holds once the comments a reader never sees are removed first.

THE RESIDUAL GAINS ITS OTHER HALF, because a residual that lists only the easy part is the same sin. The
second list is the COMPLEMENT of the first, not an observed behaviour: `stopsBoot` asks about a synthetic
`{ mode: "unmappable", cause }` for every `JOB_USER_FIX` key, and `any-uid-unsupported` never reaches
`decideJobUser` as a decision at all, since `resolveImageUser` returns it as a refusal. So a cause added to
`JOB_USER_FIX` for doctor's use alone would be filed under "Refuses each job" here and this test would then
REQUIRE the page to state something false. What bounds it is `worker/test/job-user.test.mjs` pinning the
exact eight keys, so such an addition cannot be accidental. Said in the test rather than discovered later.

FOUR SMALLER ONES, all from the same review:

- The page had lost the WSL2 caveat, which `origin/main` carried and which an operator otherwise learns by
  triggering the refusal. Restored without naming a cause, since each cause may appear once on the page.
- "What would reopen the question is an two things rather than one" was ungrammatical.
- Two wrapping artefacts in the new block, a 128-character line and a two-word dangle, both from appending
  mid-paragraph, re-wrapped by hand.
- The instruction comment said to paste what the test prints; it prints on failure, and now says so.

One reported finding was already correct and is recorded rather than changed: the measurement is six
job-image runs, one taken in a first batch and five in a second, so "six runs, 6 to 23 ms" is right and it
was the PR body's table that listed five. The table now lists all six.

`DES-CONTAINER-BACKEND-REGISTRY` AMENDED further, the same bullet and revision row. No other `DES-`, `INT-`,
`REQ-` or `CONST-` entry changes, checked.

3993 tests, 0 fail, 1 skipped in the CI posture with a live Valkey, plus dated-fixture-check,
test-count-check and temp-dir-check.

Refs #342, #357.

Signed-off-by: Rob Boerman <robboerman@live.nl>
@edgehero
edgehero merged commit 925bb62 into main Sep 21, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant