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). * under test (novox/hq SELF-UPGRADE-PLAN, rule 1).
*/ */
const BASE = { module: "mesh-tools", repo: "mesh-tools", path: "" }; 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 MODULE = { module: "amqp-ping", repo: "mesh-catalog", path: "modules/amqp-ping" };
const capability = await labIsUsable(); 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 * 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 * 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`. * 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> { async function registerModule(module: string, manifest: string): Promise<string> {
assert.ok(existsSync(manifest), `no manifest for ${module} at ${manifest}`); assert.ok(existsSync(manifest), `no manifest for ${module} at ${manifest}`);
const onMachine = `/tmp/${module}.json`; const onMachine = `/tmp/${module}.json`;
const inContainer = `/${module}.json`;
await push(instanceId, CONTROL, manifest, onMachine); await push(instanceId, CONTROL, manifest, onMachine);
await must(CONTROL, `docker cp ${onMachine} mesh-control:${onMachine}`); await must(CONTROL, `docker cp ${onMachine} mesh-control:${inContainer}`);
return mesh(`module add ${onMachine}`); return mesh(`module add ${inContainer}`);
} }
function tokenFrom(said: string): string { function tokenFrom(said: string): string {
const found = said.split("\n").map((l) => l.trim()).find((l) => l.length > 100 && !l.includes(" ")); 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}`; 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 ------------------------------------------------------------------ // ---- 5. THE ANCHOR RUNS IT ------------------------------------------------------------------
// //
// The machine that built it. This is the case every earlier proof covered, and it is here as the // 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. // 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", await step("the anchor runs the module the mesh built", NEEDS, async () => {
"the mesh builds a module standing on that base", async () => {
await mesh(`module issue ${MODULE.module} --node ${CONTROL}`).catch(() => {}); await mesh(`module issue ${MODULE.module} --node ${CONTROL}`).catch(() => {});
await mesh(`assign ${CONTROL} ${MODULE.module}`); await mesh(`assign ${CONTROL} ${MODULE.module}`);
await mesh(`push ${CONTROL}`, 600_000); await mesh(`push ${CONTROL}`, 600_000);
@@ -362,6 +400,7 @@ for (const name of [
"three machines join it across the household gateway", "three machines join it across the household gateway",
"the mesh builds the shared base from source", "the mesh builds the shared base from source",
"the mesh builds a module standing on that base", "the mesh builds a module standing on that base",
NEEDS,
"the anchor runs the module the mesh built", "the anchor runs the module the mesh built",
"a joined machine runs the module the mesh built", "a joined machine runs the module the mesh built",
]) { ]) {