Repository navigation
docs: when each job-user refusal fires, and registration is in-tree on purpose (#342, #357) - #371
Merged
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The last two items of the #345 leftovers. Both live on
docs/backends.md, which had no test of any kindbefore this PR -- not incidental: its refusal sentence named seven of the code's eight causes in six
clauses, and omitted
runtime-unreadableentirely.When a refusal fires (#357 item 3)
rootless,userns-remap,worker-is-root,desktop-linux-usernslocalis the default venueruntime-unreadable,root-group,docker-group,any-uid-unsupportedunknown, never cached, never a boot exit, soRestartPreventExitStatus=2is 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.mjsbuilds each list line fromJOB_USER_FIXandjobUserBootRefusaland 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:
-->in ordinary proseThe hardening had also begun failing an honest page: the per-image refusal id
job-image-any-uid-unsupportedcontainsany-uid-unsupportedas 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, nonereachable by a pattern.
podman-doc.test.mjsrecords the same limit and what four rounds of trying topattern it cost.
Registration is in-tree (#342)
## Registering itread as a gap ("there is no published import forstartWorkertoday"). It reads as adecision now. Exporting
startWorkerwould make the worker's entire dependency-injection bag a public API arelease 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 andloses 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
podmanbackend isspecified 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
BACKENDSis frozen atmodule load,
parseBackendListrefuses an unknownPI_BACKENDSname, andvalidateBackendrefuses atrigger naming one. The second is what the in-tree decision deliberately refuses. Nobody has asked for either.
DES-CONTAINER-BACKEND-REGISTRYAMENDED with a bullet, not a revision row alone, because the entry saysthe 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 testhere". In-tree registration is new ground againstthat 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.mdhedged an external name resolved directly from an internal network as "about 10 seconds onthe 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.
EAI_AGAINnode:22-slim(glibc)EAI_AGAINnode:22-alpine(musl)EAI_AGAINThe embedded resolver's upstream is not reachable from an
--internalnetwork, so the forward fails ratherthan 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-REGISTRYAMENDED (above).DES-JOB-USER-INFERRED-READ-BACK-ON-REQUESTUNCHANGED, checked, and cited: its residual already carriesthe 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-APIandDES-WORKER-ON-HOSTUNCHANGED, checked.INT-,REQ-orCONST-entry changes, checked: no contract, file, flag, argv or log line moves.worker/package.json's exports map is byte-unchanged andworker/test/publish.test.mjsstill passes, whichis 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-checkandtemp-dir-check. Every attack above replayed against the generator, plus a sourcemutation 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 andleaves 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.