Pair: mesh-controller #254 updates the three gitea tests that compose the forge from this catalogue. Merge them together. Rebased onto main after wave 1 (#245) merged; the diff is these nine commits only.
Waves 2 and 3 of hq to-be 38 WP4c, under ADR 0198, 0188, 0192 and 0193. There is one commit per module. Each module's own long-running code leaves its runtime container and becomes one TypeScript bundle named code. The node's runtime (node-tools) launches its loads; steps and schedules become process resources. The container, build.on mesh-tools, the Dockerfile and the broker own-secret are removed. mesh-state is removed only where nothing else names it.
Steps carry their words in the process's env. mesh-controller #250 fills ${port:…} there and mesh-host #85 runs a run-once process as its own oneshot unit, so the 0600 env-file workaround from wave 1 is not used here. Wave 1's mosquitto (bootstrap-env), home-assistant (provisions-env) and nodered (mqtt-env) could now drop theirs: each holds only ${port:} and ${dir:} values.
Per module
Module
Runtime loads
Steps / schedules / packages
Notes
audit-logger
index.js (subscribes #)
trail at ${dir:trail}/audit.log
gitea
handlers, tools, provisioner
words are host paths
mesh-vault
handlers, tools, provisioner
mesh-state removed
records
handlers, tools
package git
mailu
handlers, tools, provisioner
mailu-admin now publishes 8080 to from: machine (new listens entry admin-api); MESH_MAILU_URL=http://127.0.0.1:${port:8080}/api/v1. automx keeps its image. Mail is still read through docker exec mailu-imap
code change: the forge setting is read from MESH_LAB_ENV_FILE (lab.env) at each call, because a bundle's words may not carry ${setting:…}. mesh-state removed
route-adapter
none
run-once process route-adapter (node index.js), restart-on kept
openai-consumer
none
process openai-consumer-apply, schedule */5 * * * *
anthropic-consumer
usage/index.js
process anthropic-consumer-apply, schedule */5 * * * *
code change: usage used to spawn the runtime image's emit with the module's credential. It now emits through the SDK, so the runtime runs it once at start and then every 5 min. mesh-state removed
Left as containers
These three are not touched. Each needs a mesh change first.
mesh-catalog imports pg.
mongodb and mssql shell out to mongosh and sqlcmd, which no machine's system package provides. ADR 0198 says they need a driver in the bundle.
The TypeScript toolchain resolves imports only from the toolchain image's own node_modules, and external only re-copies that directory. So the builder cannot install a module's own npm dependencies today. I checked locally that pg@8, mongodb@6 and mssql@11 each esbuild into one ESM file with no external and run outside any node_modules. Once the builder installs a bundle's declared dependencies before compiling (or the toolchain carries them), they move like the others. mesh-catalog's prepares: true then becomes a run-once process running prepare/index.js, since prepares needs a container of the module's own artifact.
Validation
Builder-style build per module. Each module was compiled with tsc (--rootDir ., NodeNext) against @novox/mesh-sdk@0.1.6 from the Gitea registry. Launchers were written, then every entrypoint and launcher was esbuilt with the builder's flags. All nine bundle cleanly with no external.
Every load launched like node-tools does. Each load was started with its words, and every one answered initialize and tools/list. The expected mesh/subscribe and mesh/publish asks were seen. The route-adapter step ran locally to completion.
Module tests. gitea 13/13, mesh-vault 5/5, records 3/3, route-adapter 11/11, anthropic-consumer 4/4. audit-logger 0/1 fails the same way on main: its in-memory broker matches only # and the test subscribes **.
Controller catalogue tests.go test ./internal/catalogue/... ./cmd/mesh-controller/ was run with this branch as the sibling catalogue (with #254), store-backed. All pass except the known unrelated TestTheResolverIsToldEveryMachineOnTheNetworkAndToldAgainWhenOneLeaves. Re-run after the rebase onto main: pass.
Throwaway declaration test (deleted after use). Each module was composed beside the node's runtime with every need, secret and setting supplied. Each composes. No container left runs module code; what remains is third-party images. No ${…} is left except ${secret:…}. Every word and step env resolves to host paths and ports. Every load is in MESH_TOOL_MODULES. Files a word names exactly are in the runtime's restart-on.
Needs live proof
gitea's pull.merged / repo.created from the runtime. The builder's merge-driven plans depend on it, so check this first on the build node.
audit-logger receives every event, which proves the runtime's grant for **.
Every provisioner acts: gitea npm, mesh-vault, mailu smtp.
The runtime account can reach:
the docker socket, for mailu's doveadm and lab;
the incus socket, for lab;
the operator-owned state that the composer chowns.
mailu's admin API answers on the published loopback port.
records clones into ${dir:checkout} as the operator account. A checkout left root-owned by the container may need re-cloning.
lab: the system go is not the image's pinned 1.26.8, and incus / npm come from packages.
The scheduled processes fire every 5 min and write the credentials as before.
anthropic usage events reach model-usage.
route-adapter re-runs on a route change and the proxy picks the files up.
Nothing live was touched. Not merged.
**Pair:** mesh-controller #254 updates the three gitea tests that compose the forge from this catalogue. Merge them together. Rebased onto main after wave 1 (#245) merged; the diff is these nine commits only.
Waves 2 and 3 of hq to-be 38 WP4c, under ADR 0198, 0188, 0192 and 0193. There is one commit per module. Each module's own long-running code leaves its runtime container and becomes one TypeScript bundle named `code`. The node's runtime (node-tools) launches its `loads`; steps and schedules become `process` resources. The container, `build.on` mesh-tools, the Dockerfile and the `broker` own-secret are removed. `mesh-state` is removed only where nothing else names it.
**Steps carry their words in the process's `env`.** mesh-controller #250 fills `${port:…}` there and mesh-host #85 runs a run-once process as its own oneshot unit, so the 0600 env-file workaround from wave 1 is not used here. Wave 1's mosquitto (`bootstrap-env`), home-assistant (`provisions-env`) and nodered (`mqtt-env`) could now drop theirs: each holds only `${port:}` and `${dir:}` values.
## Per module
| Module | Runtime loads | Steps / schedules / packages | Notes |
|---|---|---|---|
| audit-logger | `index.js` (subscribes `#`) | | trail at `${dir:trail}/audit.log` |
| gitea | handlers, tools, provisioner | | words are host paths |
| mesh-vault | handlers, tools, provisioner | | `mesh-state` removed |
| records | handlers, tools | package `git` | |
| mailu | handlers, tools, provisioner | | `mailu-admin` now publishes 8080 to `from: machine` (new `listens` entry `admin-api`); `MESH_MAILU_URL=http://127.0.0.1:${port:8080}/api/v1`. automx keeps its image. Mail is still read through `docker exec mailu-imap` |
| lab | tools | packages git, make, python, file, iproute2, sudo, npm, go, incus | **code change:** the forge setting is read from `MESH_LAB_ENV_FILE` (lab.env) at each call, because a bundle's words may not carry `${setting:…}`. `mesh-state` removed |
| route-adapter | none | run-once process `route-adapter` (`node index.js`), restart-on kept | |
| openai-consumer | none | process `openai-consumer-apply`, schedule `*/5 * * * *` | |
| anthropic-consumer | `usage/index.js` | process `anthropic-consumer-apply`, schedule `*/5 * * * *` | **code change:** usage used to spawn the runtime image's `emit` with the module's credential. It now emits through the SDK, so the runtime runs it once at start and then every 5 min. `mesh-state` removed |
## Left as containers
These three are not touched. Each needs a mesh change first.
- **mesh-catalog** imports `pg`.
- **mongodb** and **mssql** shell out to `mongosh` and `sqlcmd`, which no machine's system package provides. ADR 0198 says they need a driver in the bundle.
The TypeScript toolchain resolves imports only from the toolchain image's own `node_modules`, and `external` only re-copies that directory. So the builder cannot install a module's own npm dependencies today. I checked locally that `pg@8`, `mongodb@6` and `mssql@11` each esbuild into one ESM file with no `external` and run outside any node_modules. Once the builder installs a bundle's declared `dependencies` before compiling (or the toolchain carries them), they move like the others. mesh-catalog's `prepares: true` then becomes a run-once process running `prepare/index.js`, since `prepares` needs a container of the module's own artifact.
## Validation
- **Builder-style build per module.** Each module was compiled with tsc (`--rootDir .`, NodeNext) against `@novox/mesh-sdk@0.1.6` from the Gitea registry. Launchers were written, then every entrypoint and launcher was esbuilt with the builder's flags. All nine bundle cleanly with no `external`.
- **Every load launched like node-tools does.** Each load was started with its words, and every one answered `initialize` and `tools/list`. The expected `mesh/subscribe` and `mesh/publish` asks were seen. The route-adapter step ran locally to completion.
- **Module tests.** gitea 13/13, mesh-vault 5/5, records 3/3, route-adapter 11/11, anthropic-consumer 4/4. audit-logger 0/1 fails the same way on main: its in-memory broker matches only `#` and the test subscribes `**`.
- **Controller catalogue tests.** `go test ./internal/catalogue/... ./cmd/mesh-controller/` was run with this branch as the sibling catalogue (with #254), store-backed. All pass except the known unrelated `TestTheResolverIsToldEveryMachineOnTheNetworkAndToldAgainWhenOneLeaves`. Re-run after the rebase onto main: pass.
- **Throwaway declaration test** (deleted after use). Each module was composed beside the node's runtime with every need, secret and setting supplied. Each composes. No container left runs module code; what remains is third-party images. No `${…}` is left except `${secret:…}`. Every word and step env resolves to host paths and ports. Every load is in `MESH_TOOL_MODULES`. Files a word names exactly are in the runtime's restart-on.
## Needs live proof
- gitea's `pull.merged` / `repo.created` from the runtime. The builder's merge-driven plans depend on it, so check this first on the build node.
- audit-logger receives every event, which proves the runtime's grant for `**`.
- Every provisioner acts: gitea npm, mesh-vault, mailu smtp.
- The runtime account can reach:
- the docker socket, for mailu's doveadm and lab;
- the incus socket, for lab;
- the operator-owned state that the composer chowns.
- mailu's admin API answers on the published loopback port.
- records clones into `${dir:checkout}` as the operator account. A checkout left root-owned by the container may need re-cloning.
- lab: the system `go` is not the image's pinned 1.26.8, and `incus` / `npm` come from packages.
- The scheduled processes fire every 5 min and write the credentials as before.
- anthropic usage events reach model-usage.
- route-adapter re-runs on a route change and the proxy picks the files up.
Nothing live was touched. Not merged.
The mesh-audit-logger container goes with its Dockerfile, build bases and bus credential: its one entrypoint is a load of one bundle, which subscribes to every event through the runtime and writes the trail at the host path the container used to mount.
The mesh-gitea container goes with its Dockerfile, build bases and bus credential; its env becomes the bundle's words with mount targets folded back to host paths: the config file, the admin password and the kept-token state directory are read where the mesh writes them.
The mesh-vault container goes with its Dockerfile, build bases, bus credential and state directory; its env was already host paths, so it becomes the bundle's words unchanged.
The records container goes with its Dockerfile, build bases and bus credential. The checkout, the config file and the origin file are read where the mesh writes them, and git comes from the machine's git package instead of the image's apt layer.
The mesh-mailu container goes with its Dockerfile, the mesh-tools build bases and its bus credential; automx keeps its own image. The code reached the admin API by its name on the mailu network, which a process on the machine cannot, so the admin container publishes 8080 to this machine only and the bundle reaches it on loopback at that port. Mail is still read through docker exec into mailu-imap, so the runtime's account needs the docker socket as nextcloud's does.
The mesh-lab container goes with its Dockerfile, build bases, bus credential and state directory. What the image installed — git, make, python, file, iproute2, sudo, npm, go and the incus client — are packages of the machine, and docker and incus are reached through their sockets as the runtime's account. The forge is an operator's setting, which reaches a file and never a bundle's words, so the tools read it from the env-file the mesh already fills, at each call; that is the one code change.
The mesh-route-adapter container goes with its Dockerfile and build bases. The step runs node on the bundle as a run-once process, reading what the mesh contributed and its config where the mesh writes them and writing the proxy's dynamic directory at the path the container used to mount; it still runs again when a route or its config changes.
The mesh-openai-consumer-apply container goes with its Dockerfile and build bases. The same entrypoint runs every five minutes as a process on the machine, reading the binding and writing the credentials at the host paths the container used to mount.
Both containers go with the Dockerfile, build bases, bus credential and state directory. apply needs no bus and runs every five minutes as a process on the machine at the host paths the container mounted. usage emitted by spawning the runtime image's own emit command with the module's credential, which exists nowhere now, so it is loaded by the node's runtime instead: it emits through the SDK as this module and reads on the cadence the schedule gave it, once at start and every five minutes. That is the one code change.
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.
Pair: mesh-controller #254 updates the three gitea tests that compose the forge from this catalogue. Merge them together. Rebased onto main after wave 1 (#245) merged; the diff is these nine commits only.
Waves 2 and 3 of hq to-be 38 WP4c, under ADR 0198, 0188, 0192 and 0193. There is one commit per module. Each module's own long-running code leaves its runtime container and becomes one TypeScript bundle named
code. The node's runtime (node-tools) launches itsloads; steps and schedules becomeprocessresources. The container,build.onmesh-tools, the Dockerfile and thebrokerown-secret are removed.mesh-stateis removed only where nothing else names it.Steps carry their words in the process's
env. mesh-controller #250 fills${port:…}there and mesh-host #85 runs a run-once process as its own oneshot unit, so the 0600 env-file workaround from wave 1 is not used here. Wave 1's mosquitto (bootstrap-env), home-assistant (provisions-env) and nodered (mqtt-env) could now drop theirs: each holds only${port:}and${dir:}values.Per module
index.js(subscribes#)${dir:trail}/audit.logmesh-stateremovedgitmailu-adminnow publishes 8080 tofrom: machine(newlistensentryadmin-api);MESH_MAILU_URL=http://127.0.0.1:${port:8080}/api/v1. automx keeps its image. Mail is still read throughdocker exec mailu-imapMESH_LAB_ENV_FILE(lab.env) at each call, because a bundle's words may not carry${setting:…}.mesh-stateremovedroute-adapter(node index.js), restart-on keptopenai-consumer-apply, schedule*/5 * * * *usage/index.jsanthropic-consumer-apply, schedule*/5 * * * *emitwith the module's credential. It now emits through the SDK, so the runtime runs it once at start and then every 5 min.mesh-stateremovedLeft as containers
These three are not touched. Each needs a mesh change first.
pg.mongoshandsqlcmd, which no machine's system package provides. ADR 0198 says they need a driver in the bundle.The TypeScript toolchain resolves imports only from the toolchain image's own
node_modules, andexternalonly re-copies that directory. So the builder cannot install a module's own npm dependencies today. I checked locally thatpg@8,mongodb@6andmssql@11each esbuild into one ESM file with noexternaland run outside any node_modules. Once the builder installs a bundle's declareddependenciesbefore compiling (or the toolchain carries them), they move like the others. mesh-catalog'sprepares: truethen becomes a run-once process runningprepare/index.js, sincepreparesneeds a container of the module's own artifact.Validation
--rootDir ., NodeNext) against@novox/mesh-sdk@0.1.6from the Gitea registry. Launchers were written, then every entrypoint and launcher was esbuilt with the builder's flags. All nine bundle cleanly with noexternal.initializeandtools/list. The expectedmesh/subscribeandmesh/publishasks were seen. The route-adapter step ran locally to completion.#and the test subscribes**.go test ./internal/catalogue/... ./cmd/mesh-controller/was run with this branch as the sibling catalogue (with #254), store-backed. All pass except the known unrelatedTestTheResolverIsToldEveryMachineOnTheNetworkAndToldAgainWhenOneLeaves. Re-run after the rebase onto main: pass.${…}is left except${secret:…}. Every word and step env resolves to host paths and ports. Every load is inMESH_TOOL_MODULES. Files a word names exactly are in the runtime's restart-on.Needs live proof
pull.merged/repo.createdfrom the runtime. The builder's merge-driven plans depend on it, so check this first on the build node.**.${dir:checkout}as the operator account. A checkout left root-owned by the container may need re-cloning.gois not the image's pinned 1.26.8, andincus/npmcome from packages.Nothing live was touched. Not merged.
4aacea5453to568674fef7