Give the module the broker it asks for, rather than a module that asks for nothing

The mesh refused to place amqp-ping: nothing provides amqp. That refusal is
right. The substrate raises a broker, but as a bundle resource — plumbing, not a
module the mesh has a record of — so it offers nothing to anything, and a module
wanting a broker wants one in the graph.

lavinmq is that module and needs no building, its image being upstream, so this
is a register and an assign. The alternative was to pick a module with no
requires, which would have passed by testing less.

Also: the control plane's image has no /tmp to copy a manifest into. Root does.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
This commit is contained in:
2026-09-14 20:56:35 +02:00
parent 4ef0a19053
commit f04911a763
+43 -4
View File
@@ -67,6 +67,16 @@ const REGISTRY = `${ANCHOR}:5000`;
* under test (novox/hq SELF-UPGRADE-PLAN, rule 1).
*/
const BASE = { module: "mesh-tools", repo: "mesh-tools", path: "" };
/**
* What `amqp-ping` requires, and what the substrate does not supply.
*
* The installer raises a broker, but as a bundle resource — plumbing, not a module the mesh has a
* record of, so it provides nothing to anything. A module asking for `amqp` is asking for a
* provider in the graph, and this is it. No build: its image is upstream.
*/
const PROVIDER = { module: "lavinmq" };
/** Named once, because the step title is also how later steps say what they waited on. */
const NEEDS = "the mesh runs a broker for that module to talk to";
const MODULE = { module: "amqp-ping", repo: "mesh-catalog", path: "modules/amqp-ping" };
const capability = await labIsUsable();
@@ -146,13 +156,17 @@ async function mesh(command: string, timeoutMs?: number): Promise<string> {
* Pushing the manifest to the machine and naming that path got `no such file or directory` from
* inside mesh-control, which is correct and was briefly mistaken for a missing manifest. It is
* copied the last step of the way with `docker cp`.
*
* **Into the root, not into /tmp.** The control plane's image is a minimal one and has no `/tmp`
* to copy into — `docker cp` says so in those words. `/` is the one directory every image has.
*/
async function registerModule(module: string, manifest: string): Promise<string> {
assert.ok(existsSync(manifest), `no manifest for ${module} at ${manifest}`);
const onMachine = `/tmp/${module}.json`;
const inContainer = `/${module}.json`;
await push(instanceId, CONTROL, manifest, onMachine);
await must(CONTROL, `docker cp ${onMachine} mesh-control:${onMachine}`);
return mesh(`module add ${onMachine}`);
await must(CONTROL, `docker cp ${onMachine} mesh-control:${inContainer}`);
return mesh(`module add ${inContainer}`);
}
function tokenFrom(said: string): string {
const found = said.split("\n").map((l) => l.trim()).find((l) => l.length > 100 && !l.includes(" "));
@@ -297,12 +311,36 @@ before(async () => {
return `${built}\n${builds}`;
});
// ---- 4b. WHAT THE MODULE NEEDS --------------------------------------------------------------
//
// `amqp-ping` requires the `amqp` provision, and the mesh refused to place it: "nothing provides
// amqp, wanted by amqp-ping". That refusal is correct and is the reason this step exists rather
// than the reason to pick an easier module. **The substrate's broker is not a provider.** It is
// raised by the installer as part of the bundle, so it is a running container and not a module
// with something to offer — the mesh's own plumbing, not an entry in its graph. A module that
// wants a broker wants one the mesh knows about.
//
// `lavinmq` is that module, and it needs no building: its image is upstream, so this is a
// register and an assign, and it is a fair test of provisioning rather than of building.
await step(NEEDS, "the mesh builds a module standing on that base", async () => {
await registerModule(PROVIDER.module, resolve(catalogDir, PROVIDER.module, "module.json"));
await mesh(`assign ${CONTROL} ${PROVIDER.module}`);
await mesh(`push ${CONTROL}`, 600_000);
for (let i = 0; i < 60; i++) {
const ps = (await on(CONTROL, `docker ps --format '{{.Names}}\t{{.Status}}'`)).out;
const line = ps.split("\n").find((l) => l.includes(PROVIDER.module));
if (line && /Up /.test(line)) return ps;
await new Promise((r) => setTimeout(r, 5_000));
}
throw new Error(`${PROVIDER.module} never came up on ${CONTROL}:\n` +
(await on(CONTROL, `docker ps -a --format '{{.Names}}\t{{.Status}}'`)).out);
});
// ---- 5. THE ANCHOR RUNS IT ------------------------------------------------------------------
//
// The machine that built it. This is the case every earlier proof covered, and it is here as the
// control for step 6: if this fails, step 6's failure says nothing about fetching.
await step("the anchor runs the module the mesh built",
"the mesh builds a module standing on that base", async () => {
await step("the anchor runs the module the mesh built", NEEDS, async () => {
await mesh(`module issue ${MODULE.module} --node ${CONTROL}`).catch(() => {});
await mesh(`assign ${CONTROL} ${MODULE.module}`);
await mesh(`push ${CONTROL}`, 600_000);
@@ -362,6 +400,7 @@ for (const name of [
"three machines join it across the household gateway",
"the mesh builds the shared base from source",
"the mesh builds a module standing on that base",
NEEDS,
"the anchor runs the module the mesh built",
"a joined machine runs the module the mesh built",
]) {