Symptom. mesh-catalog #240's build (plan-1791809737454479380, build-1791810262889408363) failed: archive … betterbird-153.4.0esr-bb10.en-US.linux-x86_64.tar.xz: it is compressed with xz, and this build seat has no xz to read it.
Cause.#255 read xz by running the xz tool and added xz to cmd/mesh-builder/Dockerfile. The build seat does not run that image: its image is built from the catalogue's modules/build-agent/Dockerfile (with this repository as context), which installs docker-cli git alone. #255's rollout did rebuild build-agent, from that recipe, so the seat never had xz. My #255 report was wrong to say xz was in the seat's image.
Fix. The xz stream is read in Go with github.com/therootcompany/xz v1.0.1 (vendored; a port of xz-embedded, CC0), whose NewReader(r, dictMax) refuses a dictionary over 64 MiB (xz's largest preset, -9) before allocating it (review 65). What it unpacks to is bounded as before (maxUpstream 512 MiB, maxEntries 100,000). No tool is run, so the exec path, the drain and the wait go. The xz line in the unused cmd/mesh-builder/Dockerfile is taken back out.
Block limit (review 68). The reader keeps one delta or BCJ decoder per block and frees them only at the end of a stream, so a file of many tiny blocks grows the heap with its length (about 67 MiB live for a 6 MB xz --block-size=1 --delta file). Before decoding, xzWithinLimits (internal/builder/xz_framing.go) now walks the file's framing without decompressing: stream header, each block header, the LZMA2 chunk headers (each carries its compressed size, so a chunk is skipped unread), padding, check, index, footer, stream padding. It refuses a file of more than 1,024 blocks or more than 16 concatenated streams, and an index whose block count is not the count walked.
Why the framing and not the index alone: the reader compares the index with the blocks only after decoding them all, so a file whose index lies would still be decoded. The walk counts the blocks the reader will decode.
Why not refuse delta and BCJ outright: it needs the same walk to see each block header, and it would refuse sound releases filtered with BCJ.
Why 1,024: about 340 bytes of decoder per block, so at most about 0.35 MiB live. Betterbird's archive is 1 stream with 1 block; multi-threaded xz cuts blocks of 3 times the dictionary, so 512 MiB in blocks of 512 KiB still fits. 16 streams: a release is one stream, and the decoders are reused across streams, so that limit only bounds the walk's work.
The library stays unforked.
Tests.
TestAnXzReleaseIsReadWithoutAnyXzToolOnTheBuildSeat: a committed 252-byte .tar.xz fixture unpacked with PATH empty. Fails on main with the exact message above.
TestAnXzFileOfManyDeltaBlocksIsRefusedBeforeItIsDecoded: a 4.0 MB stream of one-byte delta blocks (about 202,000 blocks) whose index lists 1 block, built in Go by the test; refused with "more than 1024 xz blocks", and TotalAlloc grows less than 4 MiB. With the walk disabled it is decoded without error; the same file through the reader alone keeps 57 MiB live.
TestAnXzFileOfAFewDeltaBlocksAndTwoStreamsIsRead: the test's own builder makes streams that xz -t accepts and that unpack to the right bytes (delta blocks; two streams with padding), so the walk follows the same framing the reader does.
The real Betterbird archive is 1 stream with 1 block, within the limit; its result is unchanged (review 68 checked 55 files, packed sha256 74dd6b3b…bd7c).
go vet ./... clean; sh merge-check.sh with a throwaway Postgres 17 (MESH_TEST_POSTGRES) passes with exit 0: gofmt, go test -race on every package, every store test.
After delivery. build-agent rebuilds from the catalogue's recipe with this source; then re-run the Betterbird build of mesh-catalog #240's merge.
A controller and build-agent rollout: merge only while mesh-controller.plans says no plan is mid-gate. Part of issue 345. Not merged.
**Symptom.** mesh-catalog #240's build (plan-1791809737454479380, build-1791810262889408363) failed: `archive … betterbird-153.4.0esr-bb10.en-US.linux-x86_64.tar.xz: it is compressed with xz, and this build seat has no xz to read it`.
**Cause.** #255 read xz by running the `xz` tool and added `xz` to `cmd/mesh-builder/Dockerfile`. The build seat does not run that image: its image is built from the catalogue's `modules/build-agent/Dockerfile` (with this repository as context), which installs `docker-cli git` alone. #255's rollout did rebuild build-agent, from that recipe, so the seat never had xz. My #255 report was wrong to say xz was in the seat's image.
**Fix.** The xz stream is read in Go with `github.com/therootcompany/xz` v1.0.1 (vendored; a port of xz-embedded, CC0), whose `NewReader(r, dictMax)` refuses a dictionary over 64 MiB (xz's largest preset, `-9`) before allocating it (review 65). What it unpacks to is bounded as before (`maxUpstream` 512 MiB, `maxEntries` 100,000). No tool is run, so the exec path, the drain and the `wait` go. The xz line in the unused `cmd/mesh-builder/Dockerfile` is taken back out.
**Block limit (review 68).** The reader keeps one delta or BCJ decoder per block and frees them only at the end of a stream, so a file of many tiny blocks grows the heap with its length (about 67 MiB live for a 6 MB `xz --block-size=1 --delta` file). Before decoding, `xzWithinLimits` (`internal/builder/xz_framing.go`) now walks the file's framing without decompressing: stream header, each block header, the LZMA2 chunk headers (each carries its compressed size, so a chunk is skipped unread), padding, check, index, footer, stream padding. It refuses a file of more than **1,024 blocks** or more than **16 concatenated streams**, and an index whose block count is not the count walked.
- Why the framing and not the index alone: the reader compares the index with the blocks only after decoding them all, so a file whose index lies would still be decoded. The walk counts the blocks the reader will decode.
- Why not refuse delta and BCJ outright: it needs the same walk to see each block header, and it would refuse sound releases filtered with BCJ.
- Why 1,024: about 340 bytes of decoder per block, so at most about 0.35 MiB live. Betterbird's archive is 1 stream with 1 block; multi-threaded xz cuts blocks of 3 times the dictionary, so 512 MiB in blocks of 512 KiB still fits. 16 streams: a release is one stream, and the decoders are reused across streams, so that limit only bounds the walk's work.
- The library stays unforked.
**Tests.**
- `TestAnXzReleaseIsReadWithoutAnyXzToolOnTheBuildSeat`: a committed 252-byte `.tar.xz` fixture unpacked with `PATH` empty. Fails on main with the exact message above.
- `TestAnXzStreamAskingForTooLargeADictionaryIsRefused` (review 65).
- `TestAnXzFileOfManyDeltaBlocksIsRefusedBeforeItIsDecoded`: a 4.0 MB stream of one-byte delta blocks (about 202,000 blocks) whose index lists 1 block, built in Go by the test; refused with "more than 1024 xz blocks", and TotalAlloc grows less than 4 MiB. With the walk disabled it is decoded without error; the same file through the reader alone keeps 57 MiB live.
- `TestTheXzBlockLimitIsExact` (1,024 blocks pass, 1,025 refused), `TestAnXzFileOfTooManyStreamsIsRefused` (17 streams), `TestAnXzIndexThatDoesNotListItsBlocksIsRefused`.
- `TestAnXzFileOfAFewDeltaBlocksAndTwoStreamsIsRead`: the test's own builder makes streams that `xz -t` accepts and that unpack to the right bytes (delta blocks; two streams with padding), so the walk follows the same framing the reader does.
- The real Betterbird archive is 1 stream with 1 block, within the limit; its result is unchanged (review 68 checked 55 files, packed sha256 74dd6b3b…bd7c).
- `go vet ./...` clean; `sh merge-check.sh` with a throwaway Postgres 17 (`MESH_TEST_POSTGRES`) passes with exit 0: gofmt, `go test -race` on every package, every store test.
**After delivery.** build-agent rebuilds from the catalogue's recipe with this source; then re-run the Betterbird build of mesh-catalog #240's merge.
A controller and build-agent rollout: merge only while `mesh-controller.plans` says no plan is mid-gate. Part of issue 345. Not merged.
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
Betterbird's pinned .tar.xz failed on the build seat with "this build seat has
no xz to read it": the seat's image is the catalogue's build-agent recipe,
which installs git and docker-cli alone, and xz had been added only to
cmd/mesh-builder/Dockerfile, which the seat does not build from. The stream
is now read with github.com/ulikunitz/xz (vendored), its dictionary capped
at xz's own largest preset, and what it unpacks to bounded as before; the
xz line in the unused Dockerfile is taken back out.
build-agent keeps data (ADR 0233): what it holds is backed up before it moves
route-proxy provides route: its consumers are sent again after it
route-proxy keeps data (ADR 0233): what it holds is backed up before it moves
grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before build-agent, route-proxy is judged on its first machine
Machines
mesh-controller on novox: passed — healthy 3 times over 4m8s
mesh-controller on the rest running it: sent
build-agent on ace: passed — healthy 1 times over 1m45s
build-agent on the rest running it: sent
route-proxy on ace: passed — healthy 1 times over 1m25s
route-proxy on the rest running it: sent
Transitions
2026-10-12 14:08 (new) → proposed (announced): novox/mesh-controller#264's head announced
2026-10-12 14:15 proposed → checked (checked): the verdict names this commit
2026-10-12 14:15 checked → ready (accepted): the gate passed or warned, and the repository's own check did not fail
2026-10-12 17:36 ready → published (merged): on the trunk its modules follow: merged there, and its walk opened — or nothing for a walk to move
2026-10-12 17:36 published → delivering (go): its walk started: let go by this owner in its group's order, by a person, or on the controller's own path — not a failed walk that carried a later commit for it, after which it waits for a walk of its own commit
2026-10-12 17:50 delivering → delivered (done): its walk is done, carrying its own commit: every machine of its deploy plan passed, or was left as its policy says
The commit's note under refs/notes/mesh-plan keeps every transition: git log --notes=mesh-plan.
<!-- mesh-delivery:view -->
**Delivery** `novox/mesh-controller@539a1c16cd4e` — **delivered** since 2026-10-12T17:50:24Z
**Delivery plan** — builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step
- tier 0: mesh-controller
- tier 1: build-agent
- tier 2: route-proxy
- ace: receives build-agent, route-proxy
- g14: receives build-agent
- novox: receives mesh-controller, build-agent, route-proxy
- shanks: receives build-agent
- build-agent keeps data (ADR 0233): what it holds is backed up before it moves
- route-proxy provides route: its consumers are sent again after it
- route-proxy keeps data (ADR 0233): what it holds is backed up before it moves
- grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before build-agent, route-proxy is judged on its first machine
**Machines**
- mesh-controller on novox: passed — healthy 3 times over 4m8s
- mesh-controller on the rest running it: sent
- build-agent on ace: passed — healthy 1 times over 1m45s
- build-agent on the rest running it: sent
- route-proxy on ace: passed — healthy 1 times over 1m25s
- route-proxy on the rest running it: sent
**Transitions**
- 2026-10-12 14:08 (new) → proposed (announced): novox/mesh-controller#264's head announced
- 2026-10-12 14:15 proposed → checked (checked): the verdict names this commit
- 2026-10-12 14:15 checked → ready (accepted): the gate passed or warned, and the repository's own check did not fail
- 2026-10-12 17:36 ready → published (merged): on the trunk its modules follow: merged there, and its walk opened — or nothing for a walk to move
- 2026-10-12 17:36 published → delivering (go): its walk started: let go by this owner in its group's order, by a person, or on the controller's own path — not a failed walk that carried a later commit for it, after which it waits for a walk of its own commit
- 2026-10-12 17:50 delivering → delivered (done): its walk is done, carrying its own commit: every machine of its deploy plan passed, or was left as its policy says
The commit's note under `refs/notes/mesh-plan` keeps every transition: `git log --notes=mesh-plan`.
build-agent keeps data (ADR 0233): what it holds is backed up before it moves
route-proxy provides route: its consumers are sent again after it
route-proxy keeps data (ADR 0233): what it holds is backed up before it moves
grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before build-agent, route-proxy is judged on its first machine
Run by the build seat on novox as build-1791810667206442075 (builds --log build-1791810667206442075).
--- the gate: build-agent, mesh-controller, route-proxy
mesh-controller: ok, 58 tool(s); WARNING: merge-window ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339); WARNING: controller stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8)
long-running resources without health: 1
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-12T13:06:42Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-12T13:06:42Z
modules the change moves:
- mesh-controller (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 3 module(s) in 3 tier(s): build-agent, mesh-controller, route-proxy
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-controller/cmd/mesh-builder 1.042s
ok github.com/novox/mesh-controller/cmd/mesh-controller 321.859s
ok github.com/novox/mesh-controller/examples/objectstore-provisioner 1.023s
ok github.com/novox/mesh-controller/examples/postgres-provisioner 1.027s
ok github.com/novox/mesh-controller/examples/redis-provisioner 1.026s
ok github.com/novox/mesh-controller/examples/route-proxy 1.145s
ok github.com/novox/mesh-controller/internal/artifacts 1.064s
? github.com/novox/mesh-controller/internal/beside [no test files]
ok github.com/novox/mesh-controller/internal/broker 4.975s
ok github.com/novox/mesh-controller/internal/builder 46.768s
ok github.com/novox/mesh-controller/internal/catalogue 9.395s
ok github.com/novox/mesh-controller/internal/conditions 2.722s
ok github.com/novox/mesh-controller/internal/envfile 1.037s
ok github.com/novox/mesh-controller/internal/facts 1.029s
ok github.com/novox/mesh-controller/internal/identity 11.786s
ok github.com/novox/mesh-controller/internal/inventory 119.912s
ok github.com/novox/mesh-controller/internal/lease 19.217s
ok github.com/novox/mesh-controller/internal/licences 8.034s
ok github.com/novox/mesh-controller/internal/licences/adapters 1.017s
ok github.com/novox/mesh-controller/internal/link 72.863s
ok github.com/novox/mesh-controller/internal/lint 2.547s
ok github.com/novox/mesh-controller/internal/outward 1.056s
ok github.com/novox/mesh-controller/internal/overlay 1.033s
ok github.com/novox/mesh-controller/internal/secrets 1.542s
ok github.com/novox/mesh-controller/internal/store 4.159s
ok github.com/novox/mesh-controller/internal/testbus 1.225s
ok github.com/novox/mesh-controller/internal/token 1.029s
store tests, each run against the store:
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeWalksTheOthersAgain (0.54s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeEndsTheMergesThatContainIt (0.58s)
./cmd/mesh-controller --- PASS: TestAStopWalksTheOthersAgainInTheOpenBatch (0.73s)
./cmd/mesh-controller --- PASS: TestAStopNamesWhatStaysOnTheMachinesAndWhatStillCarriesIt (0.59s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeLeavesItOutOfABatch (0.63s)
./cmd/mesh-controller --- PASS: TestSavingPlansAndMovingMergesIsOneAct (0.53s)
./cmd/mesh-controller --- PASS: TestNodeShowAndStatusReadTheLastSendFromTheStore (0.68s)
./cmd/mesh-controller --- PASS: TestTheConditionsVerbReadsTheLooksTheBucketNoLongerHolds (0.36s)
./cmd/mesh-controller --- PASS: TestAWalkLetGoAndStoppedByVerbsIsSaidStartedAndEnded (0.61s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedOrClosedByAPersonIsSaidEndedStopped (0.87s)
./cmd/mesh-controller --- PASS: TestAWalkRetriedByAPersonIsSaidStartedAgain (0.67s)
./cmd/mesh-controller --- PASS: TestABacklogReleasedByAPersonIsSaidStarted (0.87s)
./cmd/mesh-controller --- PASS: TestAWalkEndedByCancelOrClearIsSaidEnded (0.83s)
./cmd/mesh-controller --- PASS: TestAWalkRestartedByARebuildIsSaidStarted (0.67s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedByHandAndRetriedEndsFailedWhenItFailsAgain (0.74s)
./internal/inventory --- PASS: TestTheLastSendSaysWhatTheMachineAnswered (0.29s)
./internal/inventory --- PASS: TestAReportIsTheLastSendsAnswerOnlyWhenItIsAboutThatSend (0.34s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedToEveryHolderOnceAndNotAgain (0.29s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedAgainAfterAKeyChangeOrAnAcceptance (0.29s)
./internal/inventory --- PASS: TestAConditionsLooksAreKeptBesideTheBucket (0.30s)
./internal/inventory --- PASS: TestAWalksStartIsKeptAndARefusedSaveLeavesItUnsaid (0.29s)
**Merge check** at `10f76caf`
- `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: build-agent, mesh-controller, route-proxy)
- `mesh/repo-check`: **PASS** — its merge-check.sh passed
**Delivery plan** — builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step
- tier 0: mesh-controller
- tier 1: build-agent
- tier 2: route-proxy
- ace: receives build-agent, route-proxy
- g14: receives build-agent
- novox: receives mesh-controller, build-agent, route-proxy
- shanks: receives build-agent
- build-agent keeps data (ADR 0233): what it holds is backed up before it moves
- route-proxy provides route: its consumers are sent again after it
- route-proxy keeps data (ADR 0233): what it holds is backed up before it moves
- grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before build-agent, route-proxy is judged on its first machine
- read by no module's build: internal/builder/testdata/release.tar.xz, internal/builder/upstream_file_test.go, vendor/github.com/ulikunitz/xz/.gitignore, vendor/github.com/ulikunitz/xz/LICENSE, vendor/github.com/ulikunitz/xz/README.md, vendor/github.com/ulikunitz/xz/SECURITY.md, vendor/github.com/ulikunitz/xz/TODO.md, vendor/github.com/ulikunitz/xz/bits.go, vendor/github.com/ulikunitz/xz/crc.go, vendor/github.com/ulikunitz/xz/format.go, vendor/github.com/ulikunitz/xz/fox-check-none.xz, vendor/github.com/ulikunitz/xz/fox.xz, vendor/github.com/ulikunitz/xz/internal/hash/cyclic_poly.go, vendor/github.com/ulikunitz/xz/internal/hash/doc.go, vendor/github.com/ulikunitz/xz/internal/hash/rabin_karp.go, vendor/github.com/ulikunitz/xz/internal/hash/roller.go, vendor/github.com/ulikunitz/xz/internal/xlog/xlog.go, vendor/github.com/ulikunitz/xz/lzma/bintree.go, vendor/github.com/ulikunitz/xz/lzma/bitops.go, vendor/github.com/ulikunitz/xz/lzma/breader.go, vendor/github.com/ulikunitz/xz/lzma/buffer.go, vendor/github.com/ulikunitz/xz/lzma/bytewriter.go, vendor/github.com/ulikunitz/xz/lzma/decoder.go, vendor/github.com/ulikunitz/xz/lzma/decoderdict.go, vendor/github.com/ulikunitz/xz/lzma/directcodec.go, vendor/github.com/ulikunitz/xz/lzma/distcodec.go, vendor/github.com/ulikunitz/xz/lzma/encoder.go, vendor/github.com/ulikunitz/xz/lzma/encoderdict.go, vendor/github.com/ulikunitz/xz/lzma/fox.lzma, vendor/github.com/ulikunitz/xz/lzma/hashtable.go, vendor/github.com/ulikunitz/xz/lzma/header.go, vendor/github.com/ulikunitz/xz/lzma/header2.go, vendor/github.com/ulikunitz/xz/lzma/lengthcodec.go, vendor/github.com/ulikunitz/xz/lzma/literalcodec.go, vendor/github.com/ulikunitz/xz/lzma/matchalgorithm.go, vendor/github.com/ulikunitz/xz/lzma/operation.go, vendor/github.com/ulikunitz/xz/lzma/prob.go, vendor/github.com/ulikunitz/xz/lzma/properties.go, vendor/github.com/ulikunitz/xz/lzma/rangecodec.go, vendor/github.com/ulikunitz/xz/lzma/reader.go, vendor/github.com/ulikunitz/xz/lzma/reader2.go, vendor/github.com/ulikunitz/xz/lzma/state.go, vendor/github.com/ulikunitz/xz/lzma/treecodecs.go, vendor/github.com/ulikunitz/xz/lzma/writer.go, vendor/github.com/ulikunitz/xz/lzma/writer2.go, vendor/github.com/ulikunitz/xz/lzmafilter.go, vendor/github.com/ulikunitz/xz/make-docs, vendor/github.com/ulikunitz/xz/none-check.go, vendor/github.com/ulikunitz/xz/reader.go, vendor/github.com/ulikunitz/xz/writer.go
Run by the build seat on novox as `build-1791810667206442075` (`builds --log build-1791810667206442075`).
```
--- the gate: build-agent, mesh-controller, route-proxy
mesh-controller: ok, 58 tool(s); WARNING: merge-window ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339); WARNING: controller stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8)
long-running resources without health: 1
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-12T13:06:42Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-12T13:06:42Z
modules the change moves:
- mesh-controller (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 3 module(s) in 3 tier(s): build-agent, mesh-controller, route-proxy
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-controller/cmd/mesh-builder 1.042s
ok github.com/novox/mesh-controller/cmd/mesh-controller 321.859s
ok github.com/novox/mesh-controller/examples/objectstore-provisioner 1.023s
ok github.com/novox/mesh-controller/examples/postgres-provisioner 1.027s
ok github.com/novox/mesh-controller/examples/redis-provisioner 1.026s
ok github.com/novox/mesh-controller/examples/route-proxy 1.145s
ok github.com/novox/mesh-controller/internal/artifacts 1.064s
? github.com/novox/mesh-controller/internal/beside [no test files]
ok github.com/novox/mesh-controller/internal/broker 4.975s
ok github.com/novox/mesh-controller/internal/builder 46.768s
ok github.com/novox/mesh-controller/internal/catalogue 9.395s
ok github.com/novox/mesh-controller/internal/conditions 2.722s
ok github.com/novox/mesh-controller/internal/envfile 1.037s
ok github.com/novox/mesh-controller/internal/facts 1.029s
ok github.com/novox/mesh-controller/internal/identity 11.786s
ok github.com/novox/mesh-controller/internal/inventory 119.912s
ok github.com/novox/mesh-controller/internal/lease 19.217s
ok github.com/novox/mesh-controller/internal/licences 8.034s
ok github.com/novox/mesh-controller/internal/licences/adapters 1.017s
ok github.com/novox/mesh-controller/internal/link 72.863s
ok github.com/novox/mesh-controller/internal/lint 2.547s
ok github.com/novox/mesh-controller/internal/outward 1.056s
ok github.com/novox/mesh-controller/internal/overlay 1.033s
ok github.com/novox/mesh-controller/internal/secrets 1.542s
ok github.com/novox/mesh-controller/internal/store 4.159s
ok github.com/novox/mesh-controller/internal/testbus 1.225s
ok github.com/novox/mesh-controller/internal/token 1.029s
store tests, each run against the store:
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeWalksTheOthersAgain (0.54s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeEndsTheMergesThatContainIt (0.58s)
./cmd/mesh-controller --- PASS: TestAStopWalksTheOthersAgainInTheOpenBatch (0.73s)
./cmd/mesh-controller --- PASS: TestAStopNamesWhatStaysOnTheMachinesAndWhatStillCarriesIt (0.59s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeLeavesItOutOfABatch (0.63s)
./cmd/mesh-controller --- PASS: TestSavingPlansAndMovingMergesIsOneAct (0.53s)
./cmd/mesh-controller --- PASS: TestNodeShowAndStatusReadTheLastSendFromTheStore (0.68s)
./cmd/mesh-controller --- PASS: TestTheConditionsVerbReadsTheLooksTheBucketNoLongerHolds (0.36s)
./cmd/mesh-controller --- PASS: TestAWalkLetGoAndStoppedByVerbsIsSaidStartedAndEnded (0.61s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedOrClosedByAPersonIsSaidEndedStopped (0.87s)
./cmd/mesh-controller --- PASS: TestAWalkRetriedByAPersonIsSaidStartedAgain (0.67s)
./cmd/mesh-controller --- PASS: TestABacklogReleasedByAPersonIsSaidStarted (0.87s)
./cmd/mesh-controller --- PASS: TestAWalkEndedByCancelOrClearIsSaidEnded (0.83s)
./cmd/mesh-controller --- PASS: TestAWalkRestartedByARebuildIsSaidStarted (0.67s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedByHandAndRetriedEndsFailedWhenItFailsAgain (0.74s)
./internal/inventory --- PASS: TestTheLastSendSaysWhatTheMachineAnswered (0.29s)
./internal/inventory --- PASS: TestAReportIsTheLastSendsAnswerOnlyWhenItIsAboutThatSend (0.34s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedToEveryHolderOnceAndNotAgain (0.29s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedAgainAfterAKeyChangeOrAnAcceptance (0.29s)
./internal/inventory --- PASS: TestAConditionsLooksAreKeptBesideTheBucket (0.30s)
./internal/inventory --- PASS: TestAWalksStartIsKeptAndARefusedSaveLeavesItUnsaid (0.29s)
```
xz.ReaderConfig.DictCap is a floor, not a cap, so the 64 MiB limit does not bound memory. In the vendored lzmafilter.go (lzmaFilter.reader), the stream's own dictionary size replaces the configured one whenever it is larger: if dc > config.DictCap { config.DictCap = dc }. I proved it with a probe test (not committed). I made a real .tar.xz with xz --lzma2=dict=1MiB, patched the dictionary code in its block header, recomputed the header CRC32 and passed it through unpackUpstream:
dictionary code 36 (1 GiB): no error, and TotalAlloc grew by 1024 MiB;
dictionary code 40 (4 GiB - 1): no error, and TotalAlloc grew by 4096 MiB.
The stream is accepted and the dictionary is allocated in full, so the comment on xzDictCap ("a stream asking for more is refused") is not true. The digest pin limits who can supply such a file, but the code claims a limit it does not enforce, and a 4 GiB allocation on the build node is the failure the cap exists to prevent.
Two ways to fix it:
Read each block header's LZMA2 dictionary size before decoding and refuse a size above xzDictCap. A stream can hold several blocks, so either walk the index at the end of the stream, or wrap the reader so each block header is checked as it arrives.
Use a reader that takes a real maximum, for example github.com/therootcompany/xz (a port of xz-embedded, whose NewReader(r, dictMax) refuses a larger dictionary).
Either way, add a test that builds a stream asking for a dictionary above the cap (patched dictionary byte and recomputed CRC, as above) and expects a refusal that names the cap.
Checked and fine
Vendored copy: the go.sum lines match what go mod download fetched through the checksum database (h1:flR0y/x1hgM8EGV1AW3Xll6T413G0glV8UfBwR617V4=). GOFLAGS=-mod=mod go mod verify says "all modules verified". All 48 vendored files are byte-identical to the module cache copy of v0.5.17.
Licence: BSD-3-Clause, fine to vendor.
Known vulnerabilities: OSV lists none for v0.5.17. The two known advisories are fixed in this version: CVE-2021-29482 (fixed in 0.5.8) and CVE-2025-58058 (fixed in 0.5.15).
Size caps on the xz path: the decompressed stream still goes through untar, so the 512 MiB unpacked limit and the 100,000-entry limit still apply. Removing the exec, drain and wait code is right.
The new test fails on main: with main's upstream_file.go, TestAnXzReleaseIsReadWithoutAnyXzToolOnTheBuildSeat fails with "it is compressed with xz, and this build seat has no xz to read it".
Local run: I ran merge-check.sh with a throwaway Postgres 17. gofmt, go vet and go test -race on everything else passed. One unrelated test failed once: TestAMachineJoiningIsAPeerOfTheHubUntilItsTokenExpires with "no live token to issue for the tunnel key". It uses a 2-second token, so I think it is a timing flake under the full parallel race run. It passed 3 of 3 times run alone, and this pull request does not touch it.
Forge checks: mesh/merge-gate passed (builds build-agent, mesh-controller and route-proxy; every machine composes), and mesh/repo-check passed, both at 13:22 UTC.
Verdict: request changes at 10f76ca
## Blocking
**`xz.ReaderConfig.DictCap` is a floor, not a cap, so the 64 MiB limit does not bound memory.** In the vendored `lzmafilter.go` (`lzmaFilter.reader`), the stream's own dictionary size replaces the configured one whenever it is larger: `if dc > config.DictCap { config.DictCap = dc }`. I proved it with a probe test (not committed). I made a real .tar.xz with `xz --lzma2=dict=1MiB`, patched the dictionary code in its block header, recomputed the header CRC32 and passed it through `unpackUpstream`:
- dictionary code 36 (1 GiB): no error, and TotalAlloc grew by 1024 MiB;
- dictionary code 40 (4 GiB - 1): no error, and TotalAlloc grew by 4096 MiB.
The stream is accepted and the dictionary is allocated in full, so the comment on `xzDictCap` ("a stream asking for more is refused") is not true. The digest pin limits who can supply such a file, but the code claims a limit it does not enforce, and a 4 GiB allocation on the build node is the failure the cap exists to prevent.
Two ways to fix it:
- Read each block header's LZMA2 dictionary size before decoding and refuse a size above `xzDictCap`. A stream can hold several blocks, so either walk the index at the end of the stream, or wrap the reader so each block header is checked as it arrives.
- Use a reader that takes a real maximum, for example `github.com/therootcompany/xz` (a port of xz-embedded, whose `NewReader(r, dictMax)` refuses a larger dictionary).
Either way, add a test that builds a stream asking for a dictionary above the cap (patched dictionary byte and recomputed CRC, as above) and expects a refusal that names the cap.
## Checked and fine
- **Vendored copy:** the go.sum lines match what `go mod download` fetched through the checksum database (`h1:flR0y/x1hgM8EGV1AW3Xll6T413G0glV8UfBwR617V4=`). `GOFLAGS=-mod=mod go mod verify` says "all modules verified". All 48 vendored files are byte-identical to the module cache copy of v0.5.17.
- **Licence:** BSD-3-Clause, fine to vendor.
- **Known vulnerabilities:** OSV lists none for v0.5.17. The two known advisories are fixed in this version: CVE-2021-29482 (fixed in 0.5.8) and CVE-2025-58058 (fixed in 0.5.15).
- **Size caps on the xz path:** the decompressed stream still goes through `untar`, so the 512 MiB unpacked limit and the 100,000-entry limit still apply. Removing the exec, drain and wait code is right.
- **The new test fails on main:** with main's `upstream_file.go`, `TestAnXzReleaseIsReadWithoutAnyXzToolOnTheBuildSeat` fails with "it is compressed with xz, and this build seat has no xz to read it".
- **Local run:** I ran `merge-check.sh` with a throwaway Postgres 17. gofmt, go vet and `go test -race` on everything else passed. One unrelated test failed once: `TestAMachineJoiningIsAPeerOfTheHubUntilItsTokenExpires` with "no live token to issue for the tunnel key". It uses a 2-second token, so I think it is a timing flake under the full parallel race run. It passed 3 of 3 times run alone, and this pull request does not touch it.
- **Forge checks:** mesh/merge-gate passed (builds build-agent, mesh-controller and route-proxy; every machine composes), and mesh/repo-check passed, both at 13:22 UTC.
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
ulikunitz/xz's ReaderConfig.DictCap is a minimum: a stream asking for a
1 GiB or 4 GiB dictionary was given it. github.com/therootcompany/xz, a port
of xz-embedded, takes a maximum and refuses a block whose LZMA2 dictionary is
larger before allocating it. 64 MiB, xz's largest preset.
build-agent keeps data (ADR 0233): what it holds is backed up before it moves
route-proxy provides route: its consumers are sent again after it
route-proxy keeps data (ADR 0233): what it holds is backed up before it moves
grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before build-agent, route-proxy is judged on its first machine
read by no module's build: internal/builder/testdata/release.tar.xz, internal/builder/upstream_file_test.go, vendor/github.com/therootcompany/xz/AUTHORS, vendor/github.com/therootcompany/xz/LICENSE, vendor/github.com/therootcompany/xz/README.md, vendor/github.com/therootcompany/xz/dec_bcj.go, vendor/github.com/therootcompany/xz/dec_delta.go, vendor/github.com/therootcompany/xz/dec_lzma2.go, vendor/github.com/therootcompany/xz/dec_stream.go, vendor/github.com/therootcompany/xz/dec_util.go, vendor/github.com/therootcompany/xz/dec_xz.go, vendor/github.com/therootcompany/xz/doc.go, vendor/github.com/therootcompany/xz/reader.go
Run by the build seat on ace as build-1791812389482187478 (builds --log build-1791812389482187478).
--- the gate: build-agent, mesh-controller, route-proxy
mesh-controller: ok, 58 tool(s); WARNING: merge-window ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339); WARNING: controller stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8)
long-running resources without health: 1
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-12T13:33:41Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-12T13:33:41Z
modules the change moves:
- mesh-controller (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 3 module(s) in 3 tier(s): build-agent, mesh-controller, route-proxy
notes:
- internal/builder/testdata/release.tar.xz, internal/builder/upstream_file_test.go, vendor/github.com/therootcompany/xz/AUTHORS, vendor/github.com/therootcompany/xz/LICENSE, vendor/github.com/therootcompany/xz/README.md, vendor/github.com/therootcompany/xz/dec_bcj.go, vendor/github.com/therootcompany/xz/dec_delta.go, vendor/github.com/therootcompany/xz/dec_lzma2.go, vendor/github.com/therootcompany/xz/dec_stream.go, vendor/github.com/therootcompany/xz/dec_util.go, vendor/github.com/therootcompany/xz/dec_xz.go, vendor/github.com/therootcompany/xz/doc.go and vendor/github.com/therootcompany/xz/reader.go read by no module's build, and rebuild nothing (issue 280)
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-controller/cmd/mesh-builder 1.043s
ok github.com/novox/mesh-controller/cmd/mesh-controller 257.378s
ok github.com/novox/mesh-controller/examples/objectstore-provisioner 1.018s
ok github.com/novox/mesh-controller/examples/postgres-provisioner 1.026s
ok github.com/novox/mesh-controller/examples/redis-provisioner 1.017s
ok github.com/novox/mesh-controller/examples/route-proxy 1.160s
ok github.com/novox/mesh-controller/internal/artifacts 1.062s
? github.com/novox/mesh-controller/internal/beside [no test files]
ok github.com/novox/mesh-controller/internal/broker 4.254s
ok github.com/novox/mesh-controller/internal/builder 36.469s
ok github.com/novox/mesh-controller/internal/catalogue 8.391s
ok github.com/novox/mesh-controller/internal/conditions 2.778s
ok github.com/novox/mesh-controller/internal/envfile 1.073s
ok github.com/novox/mesh-controller/internal/facts 1.085s
ok github.com/novox/mesh-controller/internal/identity 4.917s
ok github.com/novox/mesh-controller/internal/inventory 96.511s
ok github.com/novox/mesh-controller/internal/lease 17.784s
ok github.com/novox/mesh-controller/internal/licences 4.426s
ok github.com/novox/mesh-controller/internal/licences/adapters 1.067s
ok github.com/novox/mesh-controller/internal/link 60.898s
ok github.com/novox/mesh-controller/internal/lint 2.453s
ok github.com/novox/mesh-controller/internal/outward 1.127s
ok github.com/novox/mesh-controller/internal/overlay 1.099s
ok github.com/novox/mesh-controller/internal/secrets 1.501s
ok github.com/novox/mesh-controller/internal/store 3.631s
ok github.com/novox/mesh-controller/internal/testbus 1.271s
ok github.com/novox/mesh-controller/internal/token 1.023s
store tests, each run against the store:
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeWalksTheOthersAgain (0.46s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeEndsTheMergesThatContainIt (0.50s)
./cmd/mesh-controller --- PASS: TestAStopWalksTheOthersAgainInTheOpenBatch (0.49s)
./cmd/mesh-controller --- PASS: TestAStopNamesWhatStaysOnTheMachinesAndWhatStillCarriesIt (0.46s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeLeavesItOutOfABatch (0.43s)
./cmd/mesh-controller --- PASS: TestSavingPlansAndMovingMergesIsOneAct (0.41s)
./cmd/mesh-controller --- PASS: TestNodeShowAndStatusReadTheLastSendFromTheStore (0.49s)
./cmd/mesh-controller --- PASS: TestTheConditionsVerbReadsTheLooksTheBucketNoLongerHolds (0.32s)
./cmd/mesh-controller --- PASS: TestAWalkLetGoAndStoppedByVerbsIsSaidStartedAndEnded (0.51s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedOrClosedByAPersonIsSaidEndedStopped (0.52s)
./cmd/mesh-controller --- PASS: TestAWalkRetriedByAPersonIsSaidStartedAgain (0.44s)
./cmd/mesh-controller --- PASS: TestABacklogReleasedByAPersonIsSaidStarted (0.46s)
./cmd/mesh-controller --- PASS: TestAWalkEndedByCancelOrClearIsSaidEnded (0.47s)
./cmd/mesh-controller --- PASS: TestAWalkRestartedByARebuildIsSaidStarted (0.45s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedByHandAndRetriedEndsFailedWhenItFailsAgain (0.47s)
./internal/inventory --- PASS: TestTheLastSendSaysWhatTheMachineAnswered (0.26s)
./internal/inventory --- PASS: TestAReportIsTheLastSendsAnswerOnlyWhenItIsAboutThatSend (0.27s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedToEveryHolderOnceAndNotAgain (0.29s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedAgainAfterAKeyChangeOrAnAcceptance (0.27s)
./internal/inventory --- PASS: TestAConditionsLooksAreKeptBesideTheBucket (0.25s)
./internal/inventory --- PASS: TestAWalksStartIsKeptAndARefusedSaveLeavesItUnsaid (0.26s)
**Merge check** at `01fc9e1d`
- `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: build-agent, mesh-controller, route-proxy)
- `mesh/repo-check`: **PASS** — its merge-check.sh passed
**Delivery plan** — builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step
- tier 0: mesh-controller
- tier 1: build-agent
- tier 2: route-proxy
- ace: receives build-agent, route-proxy
- g14: receives build-agent
- novox: receives mesh-controller, build-agent, route-proxy
- shanks: receives build-agent
- build-agent keeps data (ADR 0233): what it holds is backed up before it moves
- route-proxy provides route: its consumers are sent again after it
- route-proxy keeps data (ADR 0233): what it holds is backed up before it moves
- grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before build-agent, route-proxy is judged on its first machine
- read by no module's build: internal/builder/testdata/release.tar.xz, internal/builder/upstream_file_test.go, vendor/github.com/therootcompany/xz/AUTHORS, vendor/github.com/therootcompany/xz/LICENSE, vendor/github.com/therootcompany/xz/README.md, vendor/github.com/therootcompany/xz/dec_bcj.go, vendor/github.com/therootcompany/xz/dec_delta.go, vendor/github.com/therootcompany/xz/dec_lzma2.go, vendor/github.com/therootcompany/xz/dec_stream.go, vendor/github.com/therootcompany/xz/dec_util.go, vendor/github.com/therootcompany/xz/dec_xz.go, vendor/github.com/therootcompany/xz/doc.go, vendor/github.com/therootcompany/xz/reader.go
Run by the build seat on ace as `build-1791812389482187478` (`builds --log build-1791812389482187478`).
```
--- the gate: build-agent, mesh-controller, route-proxy
mesh-controller: ok, 58 tool(s); WARNING: merge-window ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339); WARNING: controller stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8)
long-running resources without health: 1
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-12T13:33:41Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-12T13:33:41Z
modules the change moves:
- mesh-controller (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 3 module(s) in 3 tier(s): build-agent, mesh-controller, route-proxy
notes:
- internal/builder/testdata/release.tar.xz, internal/builder/upstream_file_test.go, vendor/github.com/therootcompany/xz/AUTHORS, vendor/github.com/therootcompany/xz/LICENSE, vendor/github.com/therootcompany/xz/README.md, vendor/github.com/therootcompany/xz/dec_bcj.go, vendor/github.com/therootcompany/xz/dec_delta.go, vendor/github.com/therootcompany/xz/dec_lzma2.go, vendor/github.com/therootcompany/xz/dec_stream.go, vendor/github.com/therootcompany/xz/dec_util.go, vendor/github.com/therootcompany/xz/dec_xz.go, vendor/github.com/therootcompany/xz/doc.go and vendor/github.com/therootcompany/xz/reader.go read by no module's build, and rebuild nothing (issue 280)
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-controller/cmd/mesh-builder 1.043s
ok github.com/novox/mesh-controller/cmd/mesh-controller 257.378s
ok github.com/novox/mesh-controller/examples/objectstore-provisioner 1.018s
ok github.com/novox/mesh-controller/examples/postgres-provisioner 1.026s
ok github.com/novox/mesh-controller/examples/redis-provisioner 1.017s
ok github.com/novox/mesh-controller/examples/route-proxy 1.160s
ok github.com/novox/mesh-controller/internal/artifacts 1.062s
? github.com/novox/mesh-controller/internal/beside [no test files]
ok github.com/novox/mesh-controller/internal/broker 4.254s
ok github.com/novox/mesh-controller/internal/builder 36.469s
ok github.com/novox/mesh-controller/internal/catalogue 8.391s
ok github.com/novox/mesh-controller/internal/conditions 2.778s
ok github.com/novox/mesh-controller/internal/envfile 1.073s
ok github.com/novox/mesh-controller/internal/facts 1.085s
ok github.com/novox/mesh-controller/internal/identity 4.917s
ok github.com/novox/mesh-controller/internal/inventory 96.511s
ok github.com/novox/mesh-controller/internal/lease 17.784s
ok github.com/novox/mesh-controller/internal/licences 4.426s
ok github.com/novox/mesh-controller/internal/licences/adapters 1.067s
ok github.com/novox/mesh-controller/internal/link 60.898s
ok github.com/novox/mesh-controller/internal/lint 2.453s
ok github.com/novox/mesh-controller/internal/outward 1.127s
ok github.com/novox/mesh-controller/internal/overlay 1.099s
ok github.com/novox/mesh-controller/internal/secrets 1.501s
ok github.com/novox/mesh-controller/internal/store 3.631s
ok github.com/novox/mesh-controller/internal/testbus 1.271s
ok github.com/novox/mesh-controller/internal/token 1.023s
store tests, each run against the store:
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeWalksTheOthersAgain (0.46s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeEndsTheMergesThatContainIt (0.50s)
./cmd/mesh-controller --- PASS: TestAStopWalksTheOthersAgainInTheOpenBatch (0.49s)
./cmd/mesh-controller --- PASS: TestAStopNamesWhatStaysOnTheMachinesAndWhatStillCarriesIt (0.46s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeLeavesItOutOfABatch (0.43s)
./cmd/mesh-controller --- PASS: TestSavingPlansAndMovingMergesIsOneAct (0.41s)
./cmd/mesh-controller --- PASS: TestNodeShowAndStatusReadTheLastSendFromTheStore (0.49s)
./cmd/mesh-controller --- PASS: TestTheConditionsVerbReadsTheLooksTheBucketNoLongerHolds (0.32s)
./cmd/mesh-controller --- PASS: TestAWalkLetGoAndStoppedByVerbsIsSaidStartedAndEnded (0.51s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedOrClosedByAPersonIsSaidEndedStopped (0.52s)
./cmd/mesh-controller --- PASS: TestAWalkRetriedByAPersonIsSaidStartedAgain (0.44s)
./cmd/mesh-controller --- PASS: TestABacklogReleasedByAPersonIsSaidStarted (0.46s)
./cmd/mesh-controller --- PASS: TestAWalkEndedByCancelOrClearIsSaidEnded (0.47s)
./cmd/mesh-controller --- PASS: TestAWalkRestartedByARebuildIsSaidStarted (0.45s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedByHandAndRetriedEndsFailedWhenItFailsAgain (0.47s)
./internal/inventory --- PASS: TestTheLastSendSaysWhatTheMachineAnswered (0.26s)
./internal/inventory --- PASS: TestAReportIsTheLastSendsAnswerOnlyWhenItIsAboutThatSend (0.27s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedToEveryHolderOnceAndNotAgain (0.29s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedAgainAfterAKeyChangeOrAnAcceptance (0.27s)
./internal/inventory --- PASS: TestAConditionsLooksAreKeptBesideTheBucket (0.25s)
./internal/inventory --- PASS: TestAWalksStartIsKeptAndARefusedSaveLeavesItUnsaid (0.26s)
```
The dictionary problem from review 65 is fixed. One allocation in the new library still grows with the input, and that is the reason for this verdict.
Blocking
The library keeps one filter decoder per block, so memory grows with the number of blocks. In the vendored dec_stream.go, each block header that names a delta or BCJ filter takes s.deltas[s.deltasUsed] / s.bcjs[s.bcjsUsed], or appends a new decoder, and then increments deltasUsed / bcjsUsed. Those counters are reset only in xzDecReset, which runs once per stream, not once per block. A single stream with many small blocks therefore appends one decoder per block. xz-embedded in C reuses one decoder; this is a bug in the port.
I measured it with a probe test (not committed). I made a 200 kB tar with real xz --block-size=1:
plain LZMA2, 5.3 MB file: about 2.4 MiB of heap still live after reading;
--delta=dist=1, 6.1 MB file: about 67 MiB live;
--x86 (BCJ), 6.1 MB file: about 28 MiB live.
That is roughly 11 bytes of heap per compressed byte for delta. At the 512 MiB download cap it comes to about 5.6 GiB. The 512 MiB unpacked limit and the 100,000-entry limit do not catch it, because the unpacked output is tiny. It is the same failure as the dictionary one (a pinned file exhausting the build node's memory), so I hold it to the same bar.
Two ways to fix it:
Patch the port so bcjsUsed and deltasUsed reset at each block header, or so a chain holds one decoder of each kind. Carry it as a replace to a fork on the mesh's forge (vendored, with go.sum to match) and report it upstream.
Or, before decoding, read the stream's index (at the end of the file, for every concatenated stream) and refuse more than N blocks. For example N = 10,000: the Betterbird release has 1, and multi-threaded xz makes blocks of 3 × the dictionary size.
Add a test with a many-block delta stream that checks live heap stays under a bound.
Checked and fine
Crafted dictionary probe, rerun: dictionary codes 32, 33, 36 and 40 (256 MiB, 384 MiB, 1 GiB, 4 GiB - 1) are refused with "xz: LZMA2 dictionary size exceeds max", and TotalAlloc grew by 0 MiB. The check runs in xzDecLZMA2Reset, before make, and the dictionary buffer is reused across blocks and streams up to the maximum.
The new test fails on the old code:TestAnXzStreamAskingForTooLargeADictionaryIsRefused, run on 10f76ca, fails (props 36 allocates 1,075,139,288 bytes, props 40 allocates 4,296,329,792 bytes, and neither is refused).
Vendored copy: go.sum matches go mod download through the checksum database (h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=). GOFLAGS=-mod=mod go mod verify says "all modules verified". All 11 vendored files are byte-identical to the module cache copy of v1.0.1.
Licence: CC0 1.0, fine.
Known vulnerabilities: OSV lists none for github.com/therootcompany/xz.
Other allocations are bounded: the block header goes into a fixed 1024-byte array, there are at most 4 filters, each check (CRC32, CRC64, SHA-256) is a fixed-size hash, the index is hashed rather than stored, and stream padding and multistream reuse the same decoder. The per-block filter decoders above are the only exception I found.
Real Betterbird archive: I fetched it from its pinned URL. Its sha256 is still 38dd2489...29e2, as pinned. It is 1 stream with 1 block. unpackUpstream with within: betterbird gives the same 55 files as tar -xJ, and the packed archive is byte-identical to the one 10f76ca's decoder produced (sha256 74dd6b3b...bd7c).
Local run:merge-check.sh with a throwaway Postgres 17 (gofmt, go vet, go test -race on every package, every store test) passes with exit 0.
Forge checks: mesh/merge-gate passed (builds build-agent, mesh-controller and route-proxy; every machine composes) and mesh/repo-check passed, both at 13:46 UTC.
Verdict: request changes at 01fc9e1
The dictionary problem from review 65 is fixed. One allocation in the new library still grows with the input, and that is the reason for this verdict.
## Blocking
**The library keeps one filter decoder per block, so memory grows with the number of blocks.** In the vendored `dec_stream.go`, each block header that names a delta or BCJ filter takes `s.deltas[s.deltasUsed]` / `s.bcjs[s.bcjsUsed]`, or appends a new decoder, and then increments `deltasUsed` / `bcjsUsed`. Those counters are reset only in `xzDecReset`, which runs once per stream, not once per block. A single stream with many small blocks therefore appends one decoder per block. xz-embedded in C reuses one decoder; this is a bug in the port.
I measured it with a probe test (not committed). I made a 200 kB tar with real `xz --block-size=1`:
- plain LZMA2, 5.3 MB file: about 2.4 MiB of heap still live after reading;
- `--delta=dist=1`, 6.1 MB file: about 67 MiB live;
- `--x86` (BCJ), 6.1 MB file: about 28 MiB live.
That is roughly 11 bytes of heap per compressed byte for delta. At the 512 MiB download cap it comes to about 5.6 GiB. The 512 MiB unpacked limit and the 100,000-entry limit do not catch it, because the unpacked output is tiny. It is the same failure as the dictionary one (a pinned file exhausting the build node's memory), so I hold it to the same bar.
Two ways to fix it:
- Patch the port so `bcjsUsed` and `deltasUsed` reset at each block header, or so a chain holds one decoder of each kind. Carry it as a `replace` to a fork on the mesh's forge (vendored, with go.sum to match) and report it upstream.
- Or, before decoding, read the stream's index (at the end of the file, for every concatenated stream) and refuse more than N blocks. For example N = 10,000: the Betterbird release has 1, and multi-threaded xz makes blocks of 3 × the dictionary size.
Add a test with a many-block delta stream that checks live heap stays under a bound.
## Checked and fine
- **Crafted dictionary probe, rerun:** dictionary codes 32, 33, 36 and 40 (256 MiB, 384 MiB, 1 GiB, 4 GiB - 1) are refused with "xz: LZMA2 dictionary size exceeds max", and TotalAlloc grew by 0 MiB. The check runs in `xzDecLZMA2Reset`, before `make`, and the dictionary buffer is reused across blocks and streams up to the maximum.
- **The new test fails on the old code:** `TestAnXzStreamAskingForTooLargeADictionaryIsRefused`, run on 10f76ca, fails (props 36 allocates 1,075,139,288 bytes, props 40 allocates 4,296,329,792 bytes, and neither is refused).
- **Vendored copy:** go.sum matches `go mod download` through the checksum database (`h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=`). `GOFLAGS=-mod=mod go mod verify` says "all modules verified". All 11 vendored files are byte-identical to the module cache copy of v1.0.1.
- **Licence:** CC0 1.0, fine.
- **Known vulnerabilities:** OSV lists none for github.com/therootcompany/xz.
- **Other allocations are bounded:** the block header goes into a fixed 1024-byte array, there are at most 4 filters, each check (CRC32, CRC64, SHA-256) is a fixed-size hash, the index is hashed rather than stored, and stream padding and multistream reuse the same decoder. The per-block filter decoders above are the only exception I found.
- **Real Betterbird archive:** I fetched it from its pinned URL. Its sha256 is still 38dd2489...29e2, as pinned. It is 1 stream with 1 block. `unpackUpstream` with `within: betterbird` gives the same 55 files as `tar -xJ`, and the packed archive is byte-identical to the one 10f76ca's decoder produced (sha256 74dd6b3b...bd7c).
- **Local run:** `merge-check.sh` with a throwaway Postgres 17 (gofmt, go vet, `go test -race` on every package, every store test) passes with exit 0.
- **Forge checks:** mesh/merge-gate passed (builds build-agent, mesh-controller and route-proxy; every machine composes) and mesh/repo-check passed, both at 13:46 UTC.
The xz reader keeps one delta or BCJ decoder for every block of a stream and frees them only when the
stream ends, so a file of many tiny blocks grows the heap with its length: about 67 MiB live for a 6 MB
`xz --block-size=1 --delta` file, about 5.6 GiB at the 512 MiB download cap.
The file's framing is now walked first, without decompressing: stream header, each block header, the
LZMA2 chunk headers (each carries its compressed size, so a chunk is skipped unread), padding, check,
index and footer. A file of more than 1,024 blocks or 16 streams is refused, as is an index whose block
count is not the count walked. The index alone is not trusted because the reader compares it with the
blocks only after decoding them all, so a lying index would still be decoded. Refusing delta and BCJ
outright was not chosen: it needs the same walk to see each block header, and would refuse sound
releases filtered with BCJ.
1,024 blocks keep at most about 0.35 MiB of decoders. Betterbird's archive is 1 stream with 1 block;
multi-threaded xz cuts blocks of 3 times the dictionary, so 512 MiB in blocks of 512 KiB still fits.
The library stays unforked.
build-agent keeps data (ADR 0233): what it holds is backed up before it moves
route-proxy provides route: its consumers are sent again after it
route-proxy keeps data (ADR 0233): what it holds is backed up before it moves
grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before build-agent, route-proxy is judged on its first machine
read by no module's build: internal/builder/testdata/release.tar.xz, internal/builder/upstream_file_test.go, internal/builder/xz_framing_test.go, vendor/github.com/therootcompany/xz/AUTHORS, vendor/github.com/therootcompany/xz/LICENSE, vendor/github.com/therootcompany/xz/README.md, vendor/github.com/therootcompany/xz/dec_bcj.go, vendor/github.com/therootcompany/xz/dec_delta.go, vendor/github.com/therootcompany/xz/dec_lzma2.go, vendor/github.com/therootcompany/xz/dec_stream.go, vendor/github.com/therootcompany/xz/dec_util.go, vendor/github.com/therootcompany/xz/dec_xz.go, vendor/github.com/therootcompany/xz/doc.go, vendor/github.com/therootcompany/xz/reader.go
Run by the build seat on ace as build-1791814136071167014 (builds --log build-1791814136071167014).
--- the gate: build-agent, mesh-controller, route-proxy
mesh-controller: ok, 58 tool(s); WARNING: merge-window ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339); WARNING: controller stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8)
long-running resources without health: 1
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-12T14:02:23Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-12T14:02:23Z
modules the change moves:
- mesh-controller (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 3 module(s) in 3 tier(s): build-agent, mesh-controller, route-proxy
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-controller/cmd/mesh-builder 1.037s
ok github.com/novox/mesh-controller/cmd/mesh-controller 247.882s
ok github.com/novox/mesh-controller/examples/objectstore-provisioner 1.027s
ok github.com/novox/mesh-controller/examples/postgres-provisioner 1.036s
ok github.com/novox/mesh-controller/examples/redis-provisioner 1.024s
ok github.com/novox/mesh-controller/examples/route-proxy 1.249s
ok github.com/novox/mesh-controller/internal/artifacts 1.105s
? github.com/novox/mesh-controller/internal/beside [no test files]
ok github.com/novox/mesh-controller/internal/broker 4.291s
ok github.com/novox/mesh-controller/internal/builder 34.548s
ok github.com/novox/mesh-controller/internal/catalogue 8.309s
ok github.com/novox/mesh-controller/internal/conditions 2.882s
ok github.com/novox/mesh-controller/internal/envfile 1.127s
ok github.com/novox/mesh-controller/internal/facts 1.104s
ok github.com/novox/mesh-controller/internal/identity 4.979s
ok github.com/novox/mesh-controller/internal/inventory 92.721s
ok github.com/novox/mesh-controller/internal/lease 17.800s
ok github.com/novox/mesh-controller/internal/licences 4.568s
ok github.com/novox/mesh-controller/internal/licences/adapters 1.090s
ok github.com/novox/mesh-controller/internal/link 60.240s
ok github.com/novox/mesh-controller/internal/lint 2.529s
ok github.com/novox/mesh-controller/internal/outward 1.131s
ok github.com/novox/mesh-controller/internal/overlay 1.118s
ok github.com/novox/mesh-controller/internal/secrets 1.544s
ok github.com/novox/mesh-controller/internal/store 3.703s
ok github.com/novox/mesh-controller/internal/testbus 1.303s
ok github.com/novox/mesh-controller/internal/token 1.027s
store tests, each run against the store:
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeWalksTheOthersAgain (0.42s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeEndsTheMergesThatContainIt (0.45s)
./cmd/mesh-controller --- PASS: TestAStopWalksTheOthersAgainInTheOpenBatch (0.43s)
./cmd/mesh-controller --- PASS: TestAStopNamesWhatStaysOnTheMachinesAndWhatStillCarriesIt (0.41s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeLeavesItOutOfABatch (0.41s)
./cmd/mesh-controller --- PASS: TestSavingPlansAndMovingMergesIsOneAct (0.37s)
./cmd/mesh-controller --- PASS: TestNodeShowAndStatusReadTheLastSendFromTheStore (0.44s)
./cmd/mesh-controller --- PASS: TestTheConditionsVerbReadsTheLooksTheBucketNoLongerHolds (0.32s)
./cmd/mesh-controller --- PASS: TestAWalkLetGoAndStoppedByVerbsIsSaidStartedAndEnded (0.45s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedOrClosedByAPersonIsSaidEndedStopped (0.44s)
./cmd/mesh-controller --- PASS: TestAWalkRetriedByAPersonIsSaidStartedAgain (0.37s)
./cmd/mesh-controller --- PASS: TestABacklogReleasedByAPersonIsSaidStarted (0.44s)
./cmd/mesh-controller --- PASS: TestAWalkEndedByCancelOrClearIsSaidEnded (0.50s)
./cmd/mesh-controller --- PASS: TestAWalkRestartedByARebuildIsSaidStarted (0.45s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedByHandAndRetriedEndsFailedWhenItFailsAgain (0.58s)
./internal/inventory --- PASS: TestTheLastSendSaysWhatTheMachineAnswered (0.26s)
./internal/inventory --- PASS: TestAReportIsTheLastSendsAnswerOnlyWhenItIsAboutThatSend (0.27s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedToEveryHolderOnceAndNotAgain (0.27s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedAgainAfterAKeyChangeOrAnAcceptance (0.27s)
./internal/inventory --- PASS: TestAConditionsLooksAreKeptBesideTheBucket (0.26s)
./internal/inventory --- PASS: TestAWalksStartIsKeptAndARefusedSaveLeavesItUnsaid (0.25s)
**Merge check** at `539a1c16`
- `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: build-agent, mesh-controller, route-proxy)
- `mesh/repo-check`: **PASS** — its merge-check.sh passed
**Delivery plan** — builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step
- tier 0: mesh-controller
- tier 1: build-agent
- tier 2: route-proxy
- ace: receives build-agent, route-proxy
- g14: receives build-agent
- novox: receives mesh-controller, build-agent, route-proxy
- shanks: receives build-agent
- build-agent keeps data (ADR 0233): what it holds is backed up before it moves
- route-proxy provides route: its consumers are sent again after it
- route-proxy keeps data (ADR 0233): what it holds is backed up before it moves
- grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before build-agent, route-proxy is judged on its first machine
- read by no module's build: internal/builder/testdata/release.tar.xz, internal/builder/upstream_file_test.go, internal/builder/xz_framing_test.go, vendor/github.com/therootcompany/xz/AUTHORS, vendor/github.com/therootcompany/xz/LICENSE, vendor/github.com/therootcompany/xz/README.md, vendor/github.com/therootcompany/xz/dec_bcj.go, vendor/github.com/therootcompany/xz/dec_delta.go, vendor/github.com/therootcompany/xz/dec_lzma2.go, vendor/github.com/therootcompany/xz/dec_stream.go, vendor/github.com/therootcompany/xz/dec_util.go, vendor/github.com/therootcompany/xz/dec_xz.go, vendor/github.com/therootcompany/xz/doc.go, vendor/github.com/therootcompany/xz/reader.go
Run by the build seat on ace as `build-1791814136071167014` (`builds --log build-1791814136071167014`).
```
--- the gate: build-agent, mesh-controller, route-proxy
mesh-controller: ok, 58 tool(s); WARNING: merge-window ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339); WARNING: controller stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8)
long-running resources without health: 1
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-12T14:02:23Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-12T14:02:23Z
modules the change moves:
- mesh-controller (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 3 module(s) in 3 tier(s): build-agent, mesh-controller, route-proxy
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-controller/cmd/mesh-builder 1.037s
ok github.com/novox/mesh-controller/cmd/mesh-controller 247.882s
ok github.com/novox/mesh-controller/examples/objectstore-provisioner 1.027s
ok github.com/novox/mesh-controller/examples/postgres-provisioner 1.036s
ok github.com/novox/mesh-controller/examples/redis-provisioner 1.024s
ok github.com/novox/mesh-controller/examples/route-proxy 1.249s
ok github.com/novox/mesh-controller/internal/artifacts 1.105s
? github.com/novox/mesh-controller/internal/beside [no test files]
ok github.com/novox/mesh-controller/internal/broker 4.291s
ok github.com/novox/mesh-controller/internal/builder 34.548s
ok github.com/novox/mesh-controller/internal/catalogue 8.309s
ok github.com/novox/mesh-controller/internal/conditions 2.882s
ok github.com/novox/mesh-controller/internal/envfile 1.127s
ok github.com/novox/mesh-controller/internal/facts 1.104s
ok github.com/novox/mesh-controller/internal/identity 4.979s
ok github.com/novox/mesh-controller/internal/inventory 92.721s
ok github.com/novox/mesh-controller/internal/lease 17.800s
ok github.com/novox/mesh-controller/internal/licences 4.568s
ok github.com/novox/mesh-controller/internal/licences/adapters 1.090s
ok github.com/novox/mesh-controller/internal/link 60.240s
ok github.com/novox/mesh-controller/internal/lint 2.529s
ok github.com/novox/mesh-controller/internal/outward 1.131s
ok github.com/novox/mesh-controller/internal/overlay 1.118s
ok github.com/novox/mesh-controller/internal/secrets 1.544s
ok github.com/novox/mesh-controller/internal/store 3.703s
ok github.com/novox/mesh-controller/internal/testbus 1.303s
ok github.com/novox/mesh-controller/internal/token 1.027s
store tests, each run against the store:
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeWalksTheOthersAgain (0.42s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeEndsTheMergesThatContainIt (0.45s)
./cmd/mesh-controller --- PASS: TestAStopWalksTheOthersAgainInTheOpenBatch (0.43s)
./cmd/mesh-controller --- PASS: TestAStopNamesWhatStaysOnTheMachinesAndWhatStillCarriesIt (0.41s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeLeavesItOutOfABatch (0.41s)
./cmd/mesh-controller --- PASS: TestSavingPlansAndMovingMergesIsOneAct (0.37s)
./cmd/mesh-controller --- PASS: TestNodeShowAndStatusReadTheLastSendFromTheStore (0.44s)
./cmd/mesh-controller --- PASS: TestTheConditionsVerbReadsTheLooksTheBucketNoLongerHolds (0.32s)
./cmd/mesh-controller --- PASS: TestAWalkLetGoAndStoppedByVerbsIsSaidStartedAndEnded (0.45s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedOrClosedByAPersonIsSaidEndedStopped (0.44s)
./cmd/mesh-controller --- PASS: TestAWalkRetriedByAPersonIsSaidStartedAgain (0.37s)
./cmd/mesh-controller --- PASS: TestABacklogReleasedByAPersonIsSaidStarted (0.44s)
./cmd/mesh-controller --- PASS: TestAWalkEndedByCancelOrClearIsSaidEnded (0.50s)
./cmd/mesh-controller --- PASS: TestAWalkRestartedByARebuildIsSaidStarted (0.45s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedByHandAndRetriedEndsFailedWhenItFailsAgain (0.58s)
./internal/inventory --- PASS: TestTheLastSendSaysWhatTheMachineAnswered (0.26s)
./internal/inventory --- PASS: TestAReportIsTheLastSendsAnswerOnlyWhenItIsAboutThatSend (0.27s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedToEveryHolderOnceAndNotAgain (0.27s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedAgainAfterAKeyChangeOrAnAcceptance (0.27s)
./internal/inventory --- PASS: TestAConditionsLooksAreKeptBesideTheBucket (0.26s)
./internal/inventory --- PASS: TestAWalksStartIsKeptAndARefusedSaveLeavesItUnsaid (0.25s)
```
Re-review against review 68. The per-block decoder growth is now refused before anything is decoded, and the walk that refuses it is itself bounded.
My probes. I reran my files through xzWithinLimits:
xz --block-size=1 files (6.1 MB delta, 6.1 MB BCJ, 5.3 MB plain) are each refused with "more than 1024 xz blocks" in about 1 ms, with about 90 KiB allocated.
unpackUpstream on the delta file refuses it the same way.
1,024 one-byte blocks pass and 1,025 are refused; 16 concatenated streams pass and 17 are refused.
The walk cannot allocate or loop without bound:
Allocations are fixed. Its only allocations are full(n) with n at most 1,023 (the block header), 12, 4, 2 or 3 bytes; chunks are skipped with Discard, which copies nothing.
Every loop consumes input. Each pass reads at least one byte: an LZMA2 chunk declares at most 65,536 bytes and at least 1, a variable-length number is at most 9 bytes, and the index loop runs at most 2 × 1,024 times. The walk is therefore linear in the file, which is at most 512 MiB.
Truncation is safe. A truncated file, or a skip past EOF, ends in io.ErrUnexpectedEOF.
Fuzzing found nothing. I ran xzFraming on fuzzed input for 90 s (532,905 runs, seeded with real multi-block, multi-stream and delta files) with no panic and no hang.
The walk follows the reader's framing:
Block headers: size is (b + 1) × 4 and the CRC32 is checked.
LZMA2 chunks: control 0x00 ends the block; 0x01 and 0x02 are stored chunks of a 16-bit size + 1; 0x80 and above are LZMA chunks with 4 size bytes, plus the properties byte from 0xC0 up (state reset with new properties, and dictionary reset); 0x03 to 0x7F are refused. The reader (xz-embedded) consumes exactly the declared compressed bytes of each chunk and ends a block only at 0x00, so the walk is at the same byte offset as the reader at every block boundary. That means no file that passes the walk gives the reader more blocks than were counted.
Padding, check, index, streams: block padding must be zeros, the check size is 4 << ((id - 1) / 3), and the index count must equal the walked count. Stream padding must be zeros in multiples of 4, and the next non-zero byte must start a valid stream header.
What I tried against it: trailing junk after a valid stream, an index that lies, and a 17th stream are all refused. I found no file the walk passes with fewer blocks than it holds.
The new tests fail without the walk. With only the xzWithinLimits call removed, three tests fail:
TestAnXzFileOfManyDeltaBlocksIsRefusedBeforeItIsDecoded (the 4 MB one-byte-block file is not refused);
TestAnXzFileOfTooManyStreamsIsRefused;
TestAnXzIndexThatDoesNotListItsBlocksIsRefused. This one passes untar without the walk, because untar stops at the tar end and never reads the index, so the walk is also the only index check this path has.
Real Betterbird archive. Its sha256 is still 38dd24890a66b4cf9a45bc24e15192e00c26b929e1bb8df7974c16a4539129e2. The walk passes it in 84 ms (1 stream, 1 block). The unpacked files match tar -xJ (55 files), and the packed archive is byte-identical to the one from 01fc9e1 and 10f76ca (sha256 74dd6b3be3a8ff65a38fe483a410920b5715079f17455a208a2e076ff124bd7c).
Why the gate builds route-proxy. route-proxy's manifest in the catalogue compiles examples/route-proxy from novox/mesh-controller. This pull request changes go.mod, go.sum and vendor/modules.txt, which are build source for every Go program compiled from the repository. go list -deps ./examples/route-proxy shows it does not import internal/builder or the xz package. The rebuild is expected and harmless: it was already in the gate at 10f76ca, the first head that touched go.mod, and the program's code does not change.
Checks. My local merge-check.sh with a throwaway Postgres 17 (gofmt, go vet, go test -race on every package, every store test) exited 0. On the forge, mesh/merge-gate passed (builds build-agent, mesh-controller and route-proxy for ace, g14, novox and shanks) and mesh/repo-check passed, both at 14:15 UTC.
Not blocking.
The walk reads the whole file once more before decoding, which takes about 84 ms for 78 MB.
The limit belongs in the library: worth reporting upstream that bcjsUsed and deltasUsed are not reset per block.
Verdict: approve at 539a1c16cd4e11feb49511e1b8b859c03d478076
Re-review against review 68. The per-block decoder growth is now refused before anything is decoded, and the walk that refuses it is itself bounded.
**My probes.** I reran my files through `xzWithinLimits`:
- `xz --block-size=1` files (6.1 MB delta, 6.1 MB BCJ, 5.3 MB plain) are each refused with "more than 1024 xz blocks" in about 1 ms, with about 90 KiB allocated.
- `unpackUpstream` on the delta file refuses it the same way.
- 1,024 one-byte blocks pass and 1,025 are refused; 16 concatenated streams pass and 17 are refused.
**The walk cannot allocate or loop without bound:**
- **Allocations are fixed.** Its only allocations are `full(n)` with n at most 1,023 (the block header), 12, 4, 2 or 3 bytes; chunks are skipped with `Discard`, which copies nothing.
- **Every loop consumes input.** Each pass reads at least one byte: an LZMA2 chunk declares at most 65,536 bytes and at least 1, a variable-length number is at most 9 bytes, and the index loop runs at most 2 × 1,024 times. The walk is therefore linear in the file, which is at most 512 MiB.
- **Truncation is safe.** A truncated file, or a skip past EOF, ends in `io.ErrUnexpectedEOF`.
- **Fuzzing found nothing.** I ran `xzFraming` on fuzzed input for 90 s (532,905 runs, seeded with real multi-block, multi-stream and delta files) with no panic and no hang.
**The walk follows the reader's framing:**
- **Block headers:** size is (b + 1) × 4 and the CRC32 is checked.
- **LZMA2 chunks:** control 0x00 ends the block; 0x01 and 0x02 are stored chunks of a 16-bit size + 1; 0x80 and above are LZMA chunks with 4 size bytes, plus the properties byte from 0xC0 up (state reset with new properties, and dictionary reset); 0x03 to 0x7F are refused. The reader (xz-embedded) consumes exactly the declared compressed bytes of each chunk and ends a block only at 0x00, so the walk is at the same byte offset as the reader at every block boundary. That means no file that passes the walk gives the reader more blocks than were counted.
- **Padding, check, index, streams:** block padding must be zeros, the check size is 4 << ((id - 1) / 3), and the index count must equal the walked count. Stream padding must be zeros in multiples of 4, and the next non-zero byte must start a valid stream header.
- **What I tried against it:** trailing junk after a valid stream, an index that lies, and a 17th stream are all refused. I found no file the walk passes with fewer blocks than it holds.
**The new tests fail without the walk.** With only the `xzWithinLimits` call removed, three tests fail:
- `TestAnXzFileOfManyDeltaBlocksIsRefusedBeforeItIsDecoded` (the 4 MB one-byte-block file is not refused);
- `TestAnXzFileOfTooManyStreamsIsRefused`;
- `TestAnXzIndexThatDoesNotListItsBlocksIsRefused`. This one passes untar without the walk, because untar stops at the tar end and never reads the index, so the walk is also the only index check this path has.
**Real Betterbird archive.** Its sha256 is still 38dd24890a66b4cf9a45bc24e15192e00c26b929e1bb8df7974c16a4539129e2. The walk passes it in 84 ms (1 stream, 1 block). The unpacked files match `tar -xJ` (55 files), and the packed archive is byte-identical to the one from 01fc9e1 and 10f76ca (sha256 74dd6b3be3a8ff65a38fe483a410920b5715079f17455a208a2e076ff124bd7c).
**Why the gate builds route-proxy.** route-proxy's manifest in the catalogue compiles `examples/route-proxy` from novox/mesh-controller. This pull request changes go.mod, go.sum and vendor/modules.txt, which are build source for every Go program compiled from the repository. `go list -deps ./examples/route-proxy` shows it does not import internal/builder or the xz package. The rebuild is expected and harmless: it was already in the gate at 10f76ca, the first head that touched go.mod, and the program's code does not change.
**Checks.** My local `merge-check.sh` with a throwaway Postgres 17 (gofmt, go vet, `go test -race` on every package, every store test) exited 0. On the forge, mesh/merge-gate passed (builds build-agent, mesh-controller and route-proxy for ace, g14, novox and shanks) and mesh/repo-check passed, both at 14:15 UTC.
**Not blocking.**
- The walk reads the whole file once more before decoding, which takes about 84 ms for 78 MB.
- The limit belongs in the library: worth reporting upstream that `bcjsUsed` and `deltasUsed` are not reset per block.
build-agent keeps data (ADR 0233): what it holds is backed up before it moves
route-proxy provides route: its consumers are sent again after it
route-proxy keeps data (ADR 0233): what it holds is backed up before it moves
grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before build-agent, route-proxy is judged on its first machine
read by no module's build: internal/builder/testdata/release.tar.xz, internal/builder/upstream_file_test.go, internal/builder/xz_framing_test.go, vendor/github.com/therootcompany/xz/AUTHORS, vendor/github.com/therootcompany/xz/LICENSE, vendor/github.com/therootcompany/xz/README.md, vendor/github.com/therootcompany/xz/dec_bcj.go, vendor/github.com/therootcompany/xz/dec_delta.go, vendor/github.com/therootcompany/xz/dec_lzma2.go, vendor/github.com/therootcompany/xz/dec_stream.go, vendor/github.com/therootcompany/xz/dec_util.go, vendor/github.com/therootcompany/xz/dec_xz.go, vendor/github.com/therootcompany/xz/doc.go, vendor/github.com/therootcompany/xz/reader.go
Run by the build seat on g14 as build-1791825663577192956 (builds --log build-1791825663577192956).
--- the gate: build-agent, mesh-controller, route-proxy
mesh-controller: ok, 58 tool(s); WARNING: merge-window ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339); WARNING: controller stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8)
long-running resources without health: 1
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-12T17:18:24Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-12T17:18:24Z
modules the change moves:
- mesh-controller (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 3 module(s) in 3 tier(s): build-agent, mesh-controller, route-proxy
notes:
- internal/builder/testdata/release.tar.xz, internal/builder/upstream_file_test.go, internal/builder/xz_framing_test.go, vendor/github.com/therootcompany/xz/AUTHORS, vendor/github.com/therootcompany/xz/LICENSE, vendor/github.com/therootcompany/xz/README.md, vendor/github.com/therootcompany/xz/dec_bcj.go, vendor/github.com/therootcompany/xz/dec_delta.go, vendor/github.com/therootcompany/xz/dec_lzma2.go, vendor/github.com/therootcompany/xz/dec_stream.go, vendor/github.com/therootcompany/xz/dec_util.go, vendor/github.com/therootcompany/xz/dec_xz.go, vendor/github.com/therootcompany/xz/doc.go and vendor/github.com/therootcompany/xz/reader.go read by no module's build, and rebuild nothing (issue 280)
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-controller/cmd/mesh-builder 1.038s
ok github.com/novox/mesh-controller/cmd/mesh-controller 255.834s
ok github.com/novox/mesh-controller/examples/objectstore-provisioner 1.012s
ok github.com/novox/mesh-controller/examples/postgres-provisioner 1.018s
ok github.com/novox/mesh-controller/examples/redis-provisioner 1.015s
ok github.com/novox/mesh-controller/examples/route-proxy 1.175s
ok github.com/novox/mesh-controller/internal/artifacts 1.043s
? github.com/novox/mesh-controller/internal/beside [no test files]
ok github.com/novox/mesh-controller/internal/broker 3.951s
ok github.com/novox/mesh-controller/internal/builder 31.596s
ok github.com/novox/mesh-controller/internal/catalogue 7.092s
ok github.com/novox/mesh-controller/internal/conditions 2.420s
ok github.com/novox/mesh-controller/internal/envfile 1.027s
ok github.com/novox/mesh-controller/internal/facts 1.030s
ok github.com/novox/mesh-controller/internal/identity 4.104s
ok github.com/novox/mesh-controller/internal/inventory 85.020s
ok github.com/novox/mesh-controller/internal/lease 17.706s
ok github.com/novox/mesh-controller/internal/licences 3.757s
ok github.com/novox/mesh-controller/internal/licences/adapters 1.008s
ok github.com/novox/mesh-controller/internal/link 60.881s
ok github.com/novox/mesh-controller/internal/lint 2.169s
ok github.com/novox/mesh-controller/internal/outward 1.036s
ok github.com/novox/mesh-controller/internal/overlay 1.027s
ok github.com/novox/mesh-controller/internal/secrets 1.423s
ok github.com/novox/mesh-controller/internal/store 3.216s
ok github.com/novox/mesh-controller/internal/testbus 1.174s
ok github.com/novox/mesh-controller/internal/token 1.020s
store tests, each run against the store:
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeWalksTheOthersAgain (0.35s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeEndsTheMergesThatContainIt (0.37s)
./cmd/mesh-controller --- PASS: TestAStopWalksTheOthersAgainInTheOpenBatch (1.74s)
./cmd/mesh-controller --- PASS: TestAStopNamesWhatStaysOnTheMachinesAndWhatStillCarriesIt (0.65s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeLeavesItOutOfABatch (0.32s)
./cmd/mesh-controller --- PASS: TestSavingPlansAndMovingMergesIsOneAct (0.35s)
./cmd/mesh-controller --- PASS: TestAPlanFollowedFromTheStoreAnswersWhenItMoves (4.36s)
./cmd/mesh-controller --- PASS: TestNodeShowAndStatusReadTheLastSendFromTheStore (0.36s)
./cmd/mesh-controller --- PASS: TestTheConditionsVerbReadsTheLooksTheBucketNoLongerHolds (0.26s)
./cmd/mesh-controller --- PASS: TestAWalkLetGoAndStoppedByVerbsIsSaidStartedAndEnded (0.40s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedOrClosedByAPersonIsSaidEndedStopped (0.40s)
./cmd/mesh-controller --- PASS: TestAWalkRetriedByAPersonIsSaidStartedAgain (0.37s)
./cmd/mesh-controller --- PASS: TestABacklogReleasedByAPersonIsSaidStarted (0.38s)
./cmd/mesh-controller --- PASS: TestAWalkEndedByCancelOrClearIsSaidEnded (0.48s)
./cmd/mesh-controller --- PASS: TestAWalkRestartedByARebuildIsSaidStarted (0.38s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedByHandAndRetriedEndsFailedWhenItFailsAgain (0.42s)
./cmd/mesh-controller --- PASS: TestReplay237 (0.36s)
./cmd/mesh-controller --- PASS: TestAnUnassignThatLeavesItsNodeUnresolvedSaysSoOnceInItsAnswer (0.32s)
./cmd/mesh-controller --- PASS: TestAnUnassignThatLeavesItsNodeResolvedSaysNothingOfIt (0.35s)
./cmd/mesh-controller --- PASS: TestAnAssignThatLeavesItsNodeUnresolvedSaysOnlyThePlansRefusal (0.35s)
./cmd/mesh-controller --- PASS: TestADirectoryOwnersKeyIsRefusedThroughAVerb (0.39s)
./cmd/mesh-controller --- PASS: TestTheSendMakesNoValueForAnOutsideSecretNobodyGave (0.40s)
./cmd/mesh-controller --- PASS: TestAQuestionComposesWithoutAnOutsideSecretNobodyGave (0.39s)
./internal/inventory --- PASS: TestTheLastSendSaysWhatTheMachineAnswered (0.24s)
./internal/inventory --- PASS: TestAReportIsTheLastSendsAnswerOnlyWhenItIsAboutThatSend (0.23s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedToEveryHolderOnceAndNotAgain (0.23s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedAgainAfterAKeyChangeOrAnAcceptance (0.23s)
./internal/inventory --- PASS: TestAConditionsLooksAreKeptBesideTheBucket (0.24s)
./internal/inventory --- PASS: TestAPlanIdHoldingAQuoteIsReadAsAPlainId (0.22s)
./internal/inventory --- PASS: TestAWalksStartIsKeptAndARefusedSaveLeavesItUnsaid (0.22s)
**Merge check** at `539a1c16`
Judged: head `539a1c16cd4e` merged into trunk `ba685b17f5e1`; read beside it: mesh-catalog `23ea0f431faf`, mesh-controller-main `ba685b17f5e1`, mesh-host `a1f9856b3957`, mesh-tools `954e1a53eef9`
- `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: build-agent, mesh-controller, route-proxy)
- `mesh/repo-check`: **PASS** — its merge-check.sh passed
**Delivery plan** — builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step
- tier 0: mesh-controller
- tier 1: build-agent
- tier 2: route-proxy
- ace: receives build-agent, route-proxy
- g14: receives build-agent
- novox: receives mesh-controller, build-agent, route-proxy
- shanks: receives build-agent
- build-agent keeps data (ADR 0233): what it holds is backed up before it moves
- route-proxy provides route: its consumers are sent again after it
- route-proxy keeps data (ADR 0233): what it holds is backed up before it moves
- grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before build-agent, route-proxy is judged on its first machine
- read by no module's build: internal/builder/testdata/release.tar.xz, internal/builder/upstream_file_test.go, internal/builder/xz_framing_test.go, vendor/github.com/therootcompany/xz/AUTHORS, vendor/github.com/therootcompany/xz/LICENSE, vendor/github.com/therootcompany/xz/README.md, vendor/github.com/therootcompany/xz/dec_bcj.go, vendor/github.com/therootcompany/xz/dec_delta.go, vendor/github.com/therootcompany/xz/dec_lzma2.go, vendor/github.com/therootcompany/xz/dec_stream.go, vendor/github.com/therootcompany/xz/dec_util.go, vendor/github.com/therootcompany/xz/dec_xz.go, vendor/github.com/therootcompany/xz/doc.go, vendor/github.com/therootcompany/xz/reader.go
Run by the build seat on g14 as `build-1791825663577192956` (`builds --log build-1791825663577192956`).
```
--- the gate: build-agent, mesh-controller, route-proxy
mesh-controller: ok, 58 tool(s); WARNING: merge-window ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted: set at the terminal alone; say "trusted" false where nothing trusts it (novox/hq issue 339); WARNING: controller stay(s) up and say(s) not how it is ready — judged by liveness alone, refused from 2026-11-18 (ADR 0240 rule 8)
long-running resources without health: 1
files asking for a setting without saying whether it is trusted: 1
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 1 module(s) changed against 4 machine(s), as the snapshot of 2026-10-12T17:18:24Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-12T17:18:24Z
modules the change moves:
- mesh-controller (changed)
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 3 module(s) in 3 tier(s): build-agent, mesh-controller, route-proxy
notes:
- internal/builder/testdata/release.tar.xz, internal/builder/upstream_file_test.go, internal/builder/xz_framing_test.go, vendor/github.com/therootcompany/xz/AUTHORS, vendor/github.com/therootcompany/xz/LICENSE, vendor/github.com/therootcompany/xz/README.md, vendor/github.com/therootcompany/xz/dec_bcj.go, vendor/github.com/therootcompany/xz/dec_delta.go, vendor/github.com/therootcompany/xz/dec_lzma2.go, vendor/github.com/therootcompany/xz/dec_stream.go, vendor/github.com/therootcompany/xz/dec_util.go, vendor/github.com/therootcompany/xz/dec_xz.go, vendor/github.com/therootcompany/xz/doc.go and vendor/github.com/therootcompany/xz/reader.go read by no module's build, and rebuild nothing (issue 280)
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-controller/cmd/mesh-builder 1.038s
ok github.com/novox/mesh-controller/cmd/mesh-controller 255.834s
ok github.com/novox/mesh-controller/examples/objectstore-provisioner 1.012s
ok github.com/novox/mesh-controller/examples/postgres-provisioner 1.018s
ok github.com/novox/mesh-controller/examples/redis-provisioner 1.015s
ok github.com/novox/mesh-controller/examples/route-proxy 1.175s
ok github.com/novox/mesh-controller/internal/artifacts 1.043s
? github.com/novox/mesh-controller/internal/beside [no test files]
ok github.com/novox/mesh-controller/internal/broker 3.951s
ok github.com/novox/mesh-controller/internal/builder 31.596s
ok github.com/novox/mesh-controller/internal/catalogue 7.092s
ok github.com/novox/mesh-controller/internal/conditions 2.420s
ok github.com/novox/mesh-controller/internal/envfile 1.027s
ok github.com/novox/mesh-controller/internal/facts 1.030s
ok github.com/novox/mesh-controller/internal/identity 4.104s
ok github.com/novox/mesh-controller/internal/inventory 85.020s
ok github.com/novox/mesh-controller/internal/lease 17.706s
ok github.com/novox/mesh-controller/internal/licences 3.757s
ok github.com/novox/mesh-controller/internal/licences/adapters 1.008s
ok github.com/novox/mesh-controller/internal/link 60.881s
ok github.com/novox/mesh-controller/internal/lint 2.169s
ok github.com/novox/mesh-controller/internal/outward 1.036s
ok github.com/novox/mesh-controller/internal/overlay 1.027s
ok github.com/novox/mesh-controller/internal/secrets 1.423s
ok github.com/novox/mesh-controller/internal/store 3.216s
ok github.com/novox/mesh-controller/internal/testbus 1.174s
ok github.com/novox/mesh-controller/internal/token 1.020s
store tests, each run against the store:
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeWalksTheOthersAgain (0.35s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeEndsTheMergesThatContainIt (0.37s)
./cmd/mesh-controller --- PASS: TestAStopWalksTheOthersAgainInTheOpenBatch (1.74s)
./cmd/mesh-controller --- PASS: TestAStopNamesWhatStaysOnTheMachinesAndWhatStillCarriesIt (0.65s)
./cmd/mesh-controller --- PASS: TestAStopWithoutAMergeLeavesItOutOfABatch (0.32s)
./cmd/mesh-controller --- PASS: TestSavingPlansAndMovingMergesIsOneAct (0.35s)
./cmd/mesh-controller --- PASS: TestAPlanFollowedFromTheStoreAnswersWhenItMoves (4.36s)
./cmd/mesh-controller --- PASS: TestNodeShowAndStatusReadTheLastSendFromTheStore (0.36s)
./cmd/mesh-controller --- PASS: TestTheConditionsVerbReadsTheLooksTheBucketNoLongerHolds (0.26s)
./cmd/mesh-controller --- PASS: TestAWalkLetGoAndStoppedByVerbsIsSaidStartedAndEnded (0.40s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedOrClosedByAPersonIsSaidEndedStopped (0.40s)
./cmd/mesh-controller --- PASS: TestAWalkRetriedByAPersonIsSaidStartedAgain (0.37s)
./cmd/mesh-controller --- PASS: TestABacklogReleasedByAPersonIsSaidStarted (0.38s)
./cmd/mesh-controller --- PASS: TestAWalkEndedByCancelOrClearIsSaidEnded (0.48s)
./cmd/mesh-controller --- PASS: TestAWalkRestartedByARebuildIsSaidStarted (0.38s)
./cmd/mesh-controller --- PASS: TestAWalkStoppedByHandAndRetriedEndsFailedWhenItFailsAgain (0.42s)
./cmd/mesh-controller --- PASS: TestReplay237 (0.36s)
./cmd/mesh-controller --- PASS: TestAnUnassignThatLeavesItsNodeUnresolvedSaysSoOnceInItsAnswer (0.32s)
./cmd/mesh-controller --- PASS: TestAnUnassignThatLeavesItsNodeResolvedSaysNothingOfIt (0.35s)
./cmd/mesh-controller --- PASS: TestAnAssignThatLeavesItsNodeUnresolvedSaysOnlyThePlansRefusal (0.35s)
./cmd/mesh-controller --- PASS: TestADirectoryOwnersKeyIsRefusedThroughAVerb (0.39s)
./cmd/mesh-controller --- PASS: TestTheSendMakesNoValueForAnOutsideSecretNobodyGave (0.40s)
./cmd/mesh-controller --- PASS: TestAQuestionComposesWithoutAnOutsideSecretNobodyGave (0.39s)
./internal/inventory --- PASS: TestTheLastSendSaysWhatTheMachineAnswered (0.24s)
./internal/inventory --- PASS: TestAReportIsTheLastSendsAnswerOnlyWhenItIsAboutThatSend (0.23s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedToEveryHolderOnceAndNotAgain (0.23s)
./internal/inventory --- PASS: TestAnIssuedValueIsSealedAgainAfterAKeyChangeOrAnAcceptance (0.23s)
./internal/inventory --- PASS: TestAConditionsLooksAreKeptBesideTheBucket (0.24s)
./internal/inventory --- PASS: TestAPlanIdHoldingAQuoteIsReadAsAPlainId (0.22s)
./internal/inventory --- PASS: TestAWalksStartIsKeptAndARefusedSaveLeavesItUnsaid (0.22s)
```
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Symptom. mesh-catalog #240's build (plan-1791809737454479380, build-1791810262889408363) failed:
archive … betterbird-153.4.0esr-bb10.en-US.linux-x86_64.tar.xz: it is compressed with xz, and this build seat has no xz to read it.Cause. #255 read xz by running the
xztool and addedxztocmd/mesh-builder/Dockerfile. The build seat does not run that image: its image is built from the catalogue'smodules/build-agent/Dockerfile(with this repository as context), which installsdocker-cli gitalone. #255's rollout did rebuild build-agent, from that recipe, so the seat never had xz. My #255 report was wrong to say xz was in the seat's image.Fix. The xz stream is read in Go with
github.com/therootcompany/xzv1.0.1 (vendored; a port of xz-embedded, CC0), whoseNewReader(r, dictMax)refuses a dictionary over 64 MiB (xz's largest preset,-9) before allocating it (review 65). What it unpacks to is bounded as before (maxUpstream512 MiB,maxEntries100,000). No tool is run, so the exec path, the drain and thewaitgo. The xz line in the unusedcmd/mesh-builder/Dockerfileis taken back out.Block limit (review 68). The reader keeps one delta or BCJ decoder per block and frees them only at the end of a stream, so a file of many tiny blocks grows the heap with its length (about 67 MiB live for a 6 MB
xz --block-size=1 --deltafile). Before decoding,xzWithinLimits(internal/builder/xz_framing.go) now walks the file's framing without decompressing: stream header, each block header, the LZMA2 chunk headers (each carries its compressed size, so a chunk is skipped unread), padding, check, index, footer, stream padding. It refuses a file of more than 1,024 blocks or more than 16 concatenated streams, and an index whose block count is not the count walked.Tests.
TestAnXzReleaseIsReadWithoutAnyXzToolOnTheBuildSeat: a committed 252-byte.tar.xzfixture unpacked withPATHempty. Fails on main with the exact message above.TestAnXzStreamAskingForTooLargeADictionaryIsRefused(review 65).TestAnXzFileOfManyDeltaBlocksIsRefusedBeforeItIsDecoded: a 4.0 MB stream of one-byte delta blocks (about 202,000 blocks) whose index lists 1 block, built in Go by the test; refused with "more than 1024 xz blocks", and TotalAlloc grows less than 4 MiB. With the walk disabled it is decoded without error; the same file through the reader alone keeps 57 MiB live.TestTheXzBlockLimitIsExact(1,024 blocks pass, 1,025 refused),TestAnXzFileOfTooManyStreamsIsRefused(17 streams),TestAnXzIndexThatDoesNotListItsBlocksIsRefused.TestAnXzFileOfAFewDeltaBlocksAndTwoStreamsIsRead: the test's own builder makes streams thatxz -taccepts and that unpack to the right bytes (delta blocks; two streams with padding), so the walk follows the same framing the reader does.go vet ./...clean;sh merge-check.shwith a throwaway Postgres 17 (MESH_TEST_POSTGRES) passes with exit 0: gofmt,go test -raceon every package, every store test.After delivery. build-agent rebuilds from the catalogue's recipe with this source; then re-run the Betterbird build of mesh-catalog #240's merge.
A controller and build-agent rollout: merge only while
mesh-controller.planssays no plan is mid-gate. Part of issue 345. Not merged.Delivery
novox/mesh-controller@539a1c16cd4e— delivered since 2026-10-12T17:50:24ZDelivery plan — builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step
Machines
Transitions
The commit's note under
refs/notes/mesh-plankeeps every transition:git log --notes=mesh-plan.Merge check at
10f76cafmesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: build-agent, mesh-controller, route-proxy)mesh/repo-check: PASS — its merge-check.sh passedDelivery plan — builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step
Run by the build seat on novox as
build-1791810667206442075(builds --log build-1791810667206442075).Verdict: request changes at
10f76caBlocking
xz.ReaderConfig.DictCapis a floor, not a cap, so the 64 MiB limit does not bound memory. In the vendoredlzmafilter.go(lzmaFilter.reader), the stream's own dictionary size replaces the configured one whenever it is larger:if dc > config.DictCap { config.DictCap = dc }. I proved it with a probe test (not committed). I made a real .tar.xz withxz --lzma2=dict=1MiB, patched the dictionary code in its block header, recomputed the header CRC32 and passed it throughunpackUpstream:The stream is accepted and the dictionary is allocated in full, so the comment on
xzDictCap("a stream asking for more is refused") is not true. The digest pin limits who can supply such a file, but the code claims a limit it does not enforce, and a 4 GiB allocation on the build node is the failure the cap exists to prevent.Two ways to fix it:
xzDictCap. A stream can hold several blocks, so either walk the index at the end of the stream, or wrap the reader so each block header is checked as it arrives.github.com/therootcompany/xz(a port of xz-embedded, whoseNewReader(r, dictMax)refuses a larger dictionary).Either way, add a test that builds a stream asking for a dictionary above the cap (patched dictionary byte and recomputed CRC, as above) and expects a refusal that names the cap.
Checked and fine
go mod downloadfetched through the checksum database (h1:flR0y/x1hgM8EGV1AW3Xll6T413G0glV8UfBwR617V4=).GOFLAGS=-mod=mod go mod verifysays "all modules verified". All 48 vendored files are byte-identical to the module cache copy of v0.5.17.untar, so the 512 MiB unpacked limit and the 100,000-entry limit still apply. Removing the exec, drain and wait code is right.upstream_file.go,TestAnXzReleaseIsReadWithoutAnyXzToolOnTheBuildSeatfails with "it is compressed with xz, and this build seat has no xz to read it".merge-check.shwith a throwaway Postgres 17. gofmt, go vet andgo test -raceon everything else passed. One unrelated test failed once:TestAMachineJoiningIsAPeerOfTheHubUntilItsTokenExpireswith "no live token to issue for the tunnel key". It uses a 2-second token, so I think it is a timing flake under the full parallel race run. It passed 3 of 3 times run alone, and this pull request does not touch it.Merge check at
01fc9e1dmesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: build-agent, mesh-controller, route-proxy)mesh/repo-check: PASS — its merge-check.sh passedDelivery plan — builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step
Run by the build seat on ace as
build-1791812389482187478(builds --log build-1791812389482187478).Verdict: request changes at
01fc9e1The dictionary problem from review 65 is fixed. One allocation in the new library still grows with the input, and that is the reason for this verdict.
Blocking
The library keeps one filter decoder per block, so memory grows with the number of blocks. In the vendored
dec_stream.go, each block header that names a delta or BCJ filter takess.deltas[s.deltasUsed]/s.bcjs[s.bcjsUsed], or appends a new decoder, and then incrementsdeltasUsed/bcjsUsed. Those counters are reset only inxzDecReset, which runs once per stream, not once per block. A single stream with many small blocks therefore appends one decoder per block. xz-embedded in C reuses one decoder; this is a bug in the port.I measured it with a probe test (not committed). I made a 200 kB tar with real
xz --block-size=1:--delta=dist=1, 6.1 MB file: about 67 MiB live;--x86(BCJ), 6.1 MB file: about 28 MiB live.That is roughly 11 bytes of heap per compressed byte for delta. At the 512 MiB download cap it comes to about 5.6 GiB. The 512 MiB unpacked limit and the 100,000-entry limit do not catch it, because the unpacked output is tiny. It is the same failure as the dictionary one (a pinned file exhausting the build node's memory), so I hold it to the same bar.
Two ways to fix it:
bcjsUsedanddeltasUsedreset at each block header, or so a chain holds one decoder of each kind. Carry it as areplaceto a fork on the mesh's forge (vendored, with go.sum to match) and report it upstream.Add a test with a many-block delta stream that checks live heap stays under a bound.
Checked and fine
xzDecLZMA2Reset, beforemake, and the dictionary buffer is reused across blocks and streams up to the maximum.TestAnXzStreamAskingForTooLargeADictionaryIsRefused, run on10f76ca, fails (props 36 allocates 1,075,139,288 bytes, props 40 allocates 4,296,329,792 bytes, and neither is refused).go mod downloadthrough the checksum database (h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=).GOFLAGS=-mod=mod go mod verifysays "all modules verified". All 11 vendored files are byte-identical to the module cache copy of v1.0.1.unpackUpstreamwithwithin: betterbirdgives the same 55 files astar -xJ, and the packed archive is byte-identical to the one 10f76ca's decoder produced (sha256 74dd6b3b...bd7c).merge-check.shwith a throwaway Postgres 17 (gofmt, go vet,go test -raceon every package, every store test) passes with exit 0.Merge check at
539a1c16mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: build-agent, mesh-controller, route-proxy)mesh/repo-check: PASS — its merge-check.sh passedDelivery plan — builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step
Run by the build seat on ace as
build-1791814136071167014(builds --log build-1791814136071167014).Verdict: approve at
539a1c16cdRe-review against review 68. The per-block decoder growth is now refused before anything is decoded, and the walk that refuses it is itself bounded.
My probes. I reran my files through
xzWithinLimits:xz --block-size=1files (6.1 MB delta, 6.1 MB BCJ, 5.3 MB plain) are each refused with "more than 1024 xz blocks" in about 1 ms, with about 90 KiB allocated.unpackUpstreamon the delta file refuses it the same way.The walk cannot allocate or loop without bound:
full(n)with n at most 1,023 (the block header), 12, 4, 2 or 3 bytes; chunks are skipped withDiscard, which copies nothing.io.ErrUnexpectedEOF.xzFramingon fuzzed input for 90 s (532,905 runs, seeded with real multi-block, multi-stream and delta files) with no panic and no hang.The walk follows the reader's framing:
The new tests fail without the walk. With only the
xzWithinLimitscall removed, three tests fail:TestAnXzFileOfManyDeltaBlocksIsRefusedBeforeItIsDecoded(the 4 MB one-byte-block file is not refused);TestAnXzFileOfTooManyStreamsIsRefused;TestAnXzIndexThatDoesNotListItsBlocksIsRefused. This one passes untar without the walk, because untar stops at the tar end and never reads the index, so the walk is also the only index check this path has.Real Betterbird archive. Its sha256 is still 38dd24890a66b4cf9a45bc24e15192e00c26b929e1bb8df7974c16a4539129e2. The walk passes it in 84 ms (1 stream, 1 block). The unpacked files match
tar -xJ(55 files), and the packed archive is byte-identical to the one from01fc9e1and10f76ca(sha256 74dd6b3be3a8ff65a38fe483a410920b5715079f17455a208a2e076ff124bd7c).Why the gate builds route-proxy. route-proxy's manifest in the catalogue compiles
examples/route-proxyfrom novox/mesh-controller. This pull request changes go.mod, go.sum and vendor/modules.txt, which are build source for every Go program compiled from the repository.go list -deps ./examples/route-proxyshows it does not import internal/builder or the xz package. The rebuild is expected and harmless: it was already in the gate at10f76ca, the first head that touched go.mod, and the program's code does not change.Checks. My local
merge-check.shwith a throwaway Postgres 17 (gofmt, go vet,go test -raceon every package, every store test) exited 0. On the forge, mesh/merge-gate passed (builds build-agent, mesh-controller and route-proxy for ace, g14, novox and shanks) and mesh/repo-check passed, both at 14:15 UTC.Not blocking.
bcjsUsedanddeltasUsedare not reset per block.Merge check at
539a1c16Judged: head
539a1c16cd4emerged into trunkba685b17f5e1; read beside it: mesh-catalog23ea0f431faf, mesh-controller-mainba685b17f5e1, mesh-hosta1f9856b3957, mesh-tools954e1a53eef9mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: build-agent, mesh-controller, route-proxy)mesh/repo-check: PASS — its merge-check.sh passedDelivery plan — builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step
Run by the build seat on g14 as
build-1791825663577192956(builds --log build-1791825663577192956).