From 2e0f11dfc2106b56980e8cedb062dd2472e90412 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 23:25:36 +0200 Subject: [PATCH 1/8] Three beds give their fixtures names that are no catalogue module's (issue 074) --- test/beds-read-the-catalogue.test.ts | 4 ---- test/integration/mesh.test.ts | 16 ++++++++-------- .../provider-uses-mesh-credential.test.ts | 10 +++++----- .../runtime-restart-on-config.test.ts | 12 ++++++------ 4 files changed, 19 insertions(+), 23 deletions(-) diff --git a/test/beds-read-the-catalogue.test.ts b/test/beds-read-the-catalogue.test.ts index 39bfcdf..d1d7cfd 100644 --- a/test/beds-read-the-catalogue.test.ts +++ b/test/beds-read-the-catalogue.test.ts @@ -54,10 +54,6 @@ const STILL_CARRIED: Record = { why: "DIFFERS: redis mints its own secret, baserow drops its route requirement, letta drops its ports" }, "lavinmq-bed.test.ts": { modules: ["lavinmq", "amqp-ping"], why: "BESIDE (lavinmq, with a bootstrap step and a data directory the catalogue has not got); DIFFERS (amqp-ping names its entrypoint)" }, - "provider-uses-mesh-credential.test.ts": { modules: ["redis"], why: "WEARING: a credential mechanism test" }, - "runtime-restart-on-config.test.ts": { modules: ["grafana"], why: "WEARING: a restart mechanism test" }, - "mesh.test.ts": { modules: ["postgres", "builder", "umami"], - why: "WEARING: a postgres with no resources, a builder that builds itself, an umami that is another module of that name" }, }; const beds = resolve(import.meta.dirname, "integration"); diff --git a/test/integration/mesh.test.ts b/test/integration/mesh.test.ts index 46487cd..c78b762 100644 --- a/test/integration/mesh.test.ts +++ b/test/integration/mesh.test.ts @@ -318,7 +318,7 @@ test("both machines join it, and the token is all they need", { skip, timeout: 9 test("a credential reaches both ends and the mesh holds neither", { skip, timeout: 900_000 }, async () => { // The whole argument, on real machines: the two ends must hold the SAME password, and it must // appear nowhere the mesh or the broker could read it. - await must("anchor", `printf %s '{"module":"postgres","version":"1",` + + await must("anchor", `printf %s '{"module":"a-store","version":"1",` + `"provides":[{"name":"postgres-database","scope":"mesh"}],"serves":{"postgres-database":{"port":5432}},` + `"grants":{"postgres-database":"/var/lib/mesh-host/grants"},` + `"receives":{"postgres-database":"/var/lib/mesh-host/grants/mesh.json"},"resources":[]}' > /tmp/pg.json`); @@ -342,7 +342,7 @@ test("a credential reaches both ends and the mesh holds neither", { skip, timeou await mesh("overlay place anchor --hub --endpoint 192.0.2.10:51820 --site lab"); await mesh("overlay place laptop --site lab"); for (const node of ["anchor", "laptop"]) await mesh(`assign ${node} networking`); - await mesh("assign anchor postgres"); + await mesh("assign anchor a-store"); await mesh("assign laptop meshboard"); for (const machine of ["anchor", "laptop"]) { @@ -824,7 +824,7 @@ test("the builder is a module the mesh assigns, with a credential the mesh deliv // // So: the mesh issues a scoped account, seals it to the machine, and delivers it with the // declaration. Nobody types it and the mesh cannot read it back. - await must("anchor", `mkdir -p /root/builder && printf %s '{"module":"builder","version":"1",` + + await must("anchor", `mkdir -p /root/builder && printf %s '{"module":"self-builder","version":"1",` + `"requires":["artifact-store"],"capabilities":["container-runtime"],` + `"claims":[{"name":"the-build-machine","scope":"node"}],` + `"binds":{"artifact-store":"/var/lib/mesh/builder/artifact-store.json"},` + @@ -865,7 +865,7 @@ test("the builder is a module the mesh assigns, with a credential the mesh deliv assert.ok(!(await on("anchor", `pgrep -x mesh-builder`)).ok, "the hand-started builder is still running, so this would test that one"); - await mesh("assign anchor builder"); + await mesh("assign anchor self-builder"); await mesh("push anchor"); await new Promise((r) => setTimeout(r, 20_000)); @@ -1671,7 +1671,7 @@ test("a third-party workload is adopted, with the credential it already had", { const password = "the-password-it-already-had"; await must("anchor", `printf %s ${quote(JSON.stringify({ - module: "umami", + module: "adopted-analytics", version: "1", capabilities: ["container-runtime"], "own-secrets": { @@ -1707,13 +1707,13 @@ test("a third-party workload is adopted, with the credential it already had", { // seals it and cannot read it again. Given whole, as the environment lines the containers read. await must("anchor", `printf %s ${quote(`POSTGRES_PASSWORD=${password}`)} | ` + - `docker exec -i mesh-controller /mesh-controller secret accept anchor umami database --from -`); + `docker exec -i mesh-controller /mesh-controller secret accept anchor adopted-analytics database --from -`); await must("anchor", `printf %s ${quote( `DATABASE_URL=postgresql://umami:${password}@umami-db:5432/umami`)} | ` + - `docker exec -i mesh-controller /mesh-controller secret accept anchor umami app --from -`); + `docker exec -i mesh-controller /mesh-controller secret accept anchor adopted-analytics app --from -`); - await mesh("assign anchor umami"); + await mesh("assign anchor adopted-analytics"); await mesh("push anchor", 300_000); // Both containers, and the network they share. diff --git a/test/integration/provider-uses-mesh-credential.test.ts b/test/integration/provider-uses-mesh-credential.test.ts index c5bffb8..6bad589 100644 --- a/test/integration/provider-uses-mesh-credential.test.ts +++ b/test/integration/provider-uses-mesh-credential.test.ts @@ -145,7 +145,7 @@ test("redis creates a consumer's login with the password the mesh minted, sealin // (MESH_RECEIVES). There is NO MESH_SEAL_KEY — the whole point of ADR 0048 is that a provider // needs none. const manifest = JSON.stringify({ - module: "redis", + module: "a-cache", version: "1", emits: ["module.redis.cache.provisioned", "module.redis.cache.deprovisioned"], consumes: ["module.redis.cache.provisioned", "module.redis.cache.deprovisioned"], @@ -184,10 +184,10 @@ test("redis creates a consumer's login with the password the mesh minted, sealin }, ], }); - await must(`printf %s ${quote(manifest)} > /tmp/redis.json && docker cp /tmp/redis.json mesh-controller:/redis.json`); - await mesh("module add /redis.json"); - await mesh(`module issue redis --node ${MACHINE}`); - await mesh(`assign ${MACHINE} redis`); + await must(`printf %s ${quote(manifest)} > /tmp/a-cache.json && docker cp /tmp/a-cache.json mesh-controller:/a-cache.json`); + await mesh("module add /a-cache.json"); + await mesh(`module issue a-cache --node ${MACHINE}`); + await mesh(`assign ${MACHINE} a-cache`); await mesh(`push ${MACHINE}`); await settled(); diff --git a/test/integration/runtime-restart-on-config.test.ts b/test/integration/runtime-restart-on-config.test.ts index 5dc1bcb..1b5659f 100644 --- a/test/integration/runtime-restart-on-config.test.ts +++ b/test/integration/runtime-restart-on-config.test.ts @@ -113,7 +113,7 @@ async function settled(withinMs = 480_000): Promise { async function setToken(token: string): Promise { const settings = JSON.stringify({ url: "http://127.0.0.1:3000", token }); await must(`printf %s ${quote(settings)} > /tmp/s.json && docker cp /tmp/s.json mesh-controller:/s.json`); - await mesh(`settings set grafana /s.json --node ${MACHINE}`); + await mesh(`settings set a-runtime /s.json --node ${MACHINE}`); } async function containerId(): Promise { @@ -151,7 +151,7 @@ test("a running runtime is recreated when its settings change, and reads the new skip, timeout: 900_000, }, async () => { const manifest = JSON.stringify({ - module: "grafana", + module: "a-runtime", version: "1", emits: ["module.grafana.alert.firing"], "own-secrets": { broker: "/var/lib/mesh/grafana/broker" }, @@ -170,12 +170,12 @@ test("a running runtime is recreated when its settings change, and reads the new }, ], }); - await must(`printf %s ${quote(manifest)} > /tmp/grafana.json && docker cp /tmp/grafana.json mesh-controller:/grafana.json`); - await mesh("module add /grafana.json"); + await must(`printf %s ${quote(manifest)} > /tmp/a-runtime.json && docker cp /tmp/a-runtime.json mesh-controller:/a-runtime.json`); + await mesh("module add /a-runtime.json"); await setToken("token-alpha"); - await mesh(`module issue grafana --node ${MACHINE}`); - await mesh(`assign ${MACHINE} grafana`); + await mesh(`module issue a-runtime --node ${MACHINE}`); + await mesh(`assign ${MACHINE} a-runtime`); await mesh(`push ${MACHINE}`); await settled(); From 0d8eac88c38a3abad2a94920dfb0fa5f38c78aac Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 23:31:34 +0200 Subject: [PATCH 2/8] whole-mesh-full delivers amqp-email-forwarder's smtp-password under the name the module declares (found by issue 078's refusal) --- test/integration/whole-mesh-full.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/whole-mesh-full.test.ts b/test/integration/whole-mesh-full.test.ts index 6dfef61..5c9a0d8 100644 --- a/test/integration/whole-mesh-full.test.ts +++ b/test/integration/whole-mesh-full.test.ts @@ -317,7 +317,7 @@ const OPERATOR_SECRETS: { node: string; module: string; name: string; value: str { node: "novox", module: "de-spiegel", name: "smtp-user", value: "despiegel-smtp-fake" }, { node: "novox", module: "de-spiegel", name: "smtp-pass", value: "despiegel-pass-fake" }, { node: "novox", module: "amqp-email-forwarder", name: "smtp-user", value: "eef-smtp-fake" }, - { node: "novox", module: "amqp-email-forwarder", name: "smtp-pass", value: "eef-pass-fake" }, + { node: "novox", module: "amqp-email-forwarder", name: "smtp-password", value: "eef-pass-fake" }, ]; let instanceId = ""; From fd1e5499d01ffb39d2dc158b80d8509fa0cfe01b Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 23:35:01 +0200 Subject: [PATCH 3/8] Retire provider-uses-mesh-credential: the grant end-to-end bed proves it against the catalogue's redis, with the mesh writing the contributions; its no-seal-key assertion moves there (issue 074) --- node_modules | 1 + .../integration/mesh-grant-end-to-end.test.ts | 7 + .../provider-uses-mesh-credential.test.ts | 236 ------------------ 3 files changed, 8 insertions(+), 236 deletions(-) create mode 120000 node_modules delete mode 100644 test/integration/provider-uses-mesh-credential.test.ts diff --git a/node_modules b/node_modules new file mode 120000 index 0000000..34fd1f4 --- /dev/null +++ b/node_modules @@ -0,0 +1 @@ +/home/jochen/projects/novox/mesh-lab/node_modules \ No newline at end of file diff --git a/test/integration/mesh-grant-end-to-end.test.ts b/test/integration/mesh-grant-end-to-end.test.ts index 509325b..496d1c2 100644 --- a/test/integration/mesh-grant-end-to-end.test.ts +++ b/test/integration/mesh-grant-end-to-end.test.ts @@ -220,4 +220,11 @@ test("the mesh grants a consumer redis's cache, and the credential it delivers a assert.doesNotMatch(authed.out, /WRONGPASS|NOPERM|no password/i, `the consumer's mesh-delivered credential did not authenticate — the two ends do not agree:\n${authed.out}`); assert.match(authed.out, /PONG/, `expected PONG authenticating as the granted consumer:\n${authed.out}`); + + // And the provider needed no seal key to do it: the password reached it as a file the host left + // after unsealing, so MESH_SEAL_KEY is set nowhere (novox/hq ADR 0048). Carried over from the + // retired provider-uses-mesh-credential bed, whose other proofs this bed makes with the mesh + // writing the contributions rather than the bed. + const runtimeEnv = await must(`docker inspect mesh-redis --format '{{json .Config.Env}}'`); + assert.doesNotMatch(runtimeEnv, /MESH_SEAL_KEY/, `a seal key was set after all — ADR 0048 is not what ran:\n${runtimeEnv}`); }); diff --git a/test/integration/provider-uses-mesh-credential.test.ts b/test/integration/provider-uses-mesh-credential.test.ts deleted file mode 100644 index 6bad589..0000000 --- a/test/integration/provider-uses-mesh-credential.test.ts +++ /dev/null @@ -1,236 +0,0 @@ -/** - * A provider creates the resource with the credential the mesh minted — novox/hq ADR 0048. - * - * The old provisioner generated its own password, sealed it with a key nothing delivered, and - * handed it back. This proves the corrected contract: redis's provisioner reads the mesh's - * contributions file and, for each consumer, the password the mesh minted and the host unsealed, and - * creates the ACL user under the login the mesh derived, with that exact password. No $MESH_SEAL_KEY - * is set anywhere. The proof is authentication: a client logging in as that consumer with the mesh's - * password gets PONG — where a provisioner that invented its own password would answer WRONGPASS. - * - * A hand-written contributions file and secret stand in for the control plane here (a full grant - * from a second module is a heavier bed); their SHAPE is exactly what mesh-controller writes — a - * `receives` doc with `as`/`secret`, and the secret file the host leaves after unsealing. - * - * MESH_LAB_HOST_BINARY=.../mesh-host MESH_LAB_BUNDLE=.../examples/foundation-first-node.lock - * scripts/build-module-runtime.sh redis builds mesh-runtime-redis:development, which - * scenarios/redis-node.yml stocks. - */ - -import { test, before, after } from "node:test"; -import assert from "node:assert/strict"; -import { existsSync, readFileSync } from "node:fs"; -import { loadScenario } from "../../src/declaration/parse.ts"; -import { raise } from "../../src/lifecycle/raise.ts"; -import { destroy, exec } from "../../src/lifecycle/operate.ts"; -import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts"; -import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts"; -import type { HeldImage } from "../../src/pinning.ts"; - -const capability = await labIsUsable(); -const binary = hostBinaryPath(); -const bundle = process.env["MESH_LAB_BUNDLE"] ?? ""; - -const skip = !capability.usable - ? `lab not usable: ${capability.why}` - : !binary || !existsSync(binary) - ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host" - : !bundle || !existsSync(bundle) - ? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)" - : false; - -const SCENARIO = "redis-node"; -const MACHINE = "anchor"; - -let instanceId = ""; -let held: HeldImage[] = []; - -function quote(s: string): string { - return `'${s.replaceAll("'", `'\\''`)}'`; -} - -async function on(command: string, timeoutMs?: number): Promise<{ out: string; ok: boolean }> { - const { stdout } = await exec(instanceId, MACHINE, [ - "sh", "-c", `exec 2>&1\n${command}\necho "__exit=$?"`, - ], timeoutMs); - const marker = stdout.lastIndexOf("__exit="); - if (marker < 0) return { out: stdout, ok: false }; - return { out: stdout.slice(0, marker), ok: stdout.slice(marker + 7).trim() === "0" }; -} - -async function must(command: string, timeoutMs?: number): Promise { - const { out, ok } = await on(command, timeoutMs); - if (!ok) throw new Error(`${MACHINE}: ${command}\n${out}`); - return out; -} - -async function mesh(command: string, timeoutMs?: number): Promise { - return must(`docker exec mesh-controller /mesh-controller ${command}`, timeoutMs); -} - -/** What a manifest's image reference becomes on the machine — ours by ID, everything else as written. */ -function pinned(reference: string): string { - return onTheMachine(reference, held); -} - -function bundleFor(images: HeldImage[]): string { - return foundationBundle(bundle, images); -} - -function tokenFrom(said: string): string { - const found = said.split("\n").map((l) => l.trim()).find((l) => l.length > 100 && !l.includes(" ")); - assert.ok(found, `no token in:\n${said}`); - return found; -} - -async function settled(withinMs = 480_000): Promise { - const until = Date.now() + withinMs; - let last = ""; - while (Date.now() < until) { - const asked = await on(`docker exec mesh-controller /mesh-controller status --json`); - if (asked.ok) { - try { - const state = JSON.parse(asked.out) as { - wrong: { node: string; outcome: string }[]; - waiting: { node: string }[]; - reported: { node: string; outcome: string; current: boolean }[]; - }; - const bad = state.wrong.find((w) => w.node === MACHINE); - if (bad) throw new Error(`${MACHINE} did not apply what it was sent: ${bad.outcome}\n${asked.out}`); - const word = state.reported.find((r) => r.node === MACHINE); - if (!state.waiting.some((w) => w.node === MACHINE) && word?.outcome === "applied" && word.current) return; - last = asked.out; - } catch (err) { - if (err instanceof Error && err.message.includes("did not apply")) throw err; - last = asked.out; - } - } - await new Promise((r) => setTimeout(r, 5000)); - } - throw new Error(`${MACHINE} never caught up within ${Math.round(withinMs / 1000)}s. Last:\n${last}`); -} - -before(async () => { - if (skip) return; - - const raised = await raise(loadScenario(`scenarios/${SCENARIO}.yml`), { - onProgress: (m) => console.log(`raise: ${m}`), - }); - instanceId = raised.instanceId; - held = raised.images; - - await must(`cat > /tmp/foundation.lock <<'MESHBUNDLE'\n${bundleFor(raised.images)}\nMESHBUNDLE`); - await must(`${HOST_PATH} apply /tmp/foundation.lock`, 600_000); - const up = await must(`docker ps --format '{{.Names}}'`); - for (const c of ["mesh-store", "mesh-broker", "mesh-controller"]) { - assert.match(up, new RegExp(c), `the foundation did not raise ${c}:\n${up}`); - } - - await mesh(`node add ${MACHINE}`); - const token = tokenFrom(await mesh(`token issue --node ${MACHINE}`)); - await must(`${HOST_PATH} enrol --token ${quote(token)}`); - await must(`nohup ${HOST_PATH} run > /var/log/mesh-host.log 2>&1 & sleep 3`); -}, { timeout: 1_800_000 }); - -after(async () => { - if (instanceId) await destroy(instanceId); - await destroyAll(`${SCENARIO}-`); -}, { timeout: 600_000 }); - -test("redis creates a consumer's login with the password the mesh minted, sealing nothing", { - skip, timeout: 900_000, -}, async () => { - // redis as a provider: the server, and a broker-bound runtime that serves its tools AND runs its - // provisioner. The provisioner is pointed at the contributions file the mesh would write - // (MESH_RECEIVES). There is NO MESH_SEAL_KEY — the whole point of ADR 0048 is that a provider - // needs none. - const manifest = JSON.stringify({ - module: "a-cache", - version: "1", - emits: ["module.redis.cache.provisioned", "module.redis.cache.deprovisioned"], - consumes: ["module.redis.cache.provisioned", "module.redis.cache.deprovisioned"], - "own-secrets": { default: "/var/lib/redis-module/default.secret", broker: "/var/lib/mesh/redis/broker" }, - resources: [ - { id: "mesh-state", type: "directory", path: "/var/lib/mesh/redis", mode: "0700" }, - { id: "state", type: "directory", path: "/var/lib/redis-module", mode: "0700" }, - { id: "grants-dir", type: "directory", path: "/var/lib/redis-module/grants", mode: "0700" }, - { id: "data", type: "directory", path: "/services/redis/data", mode: "0700", owner: "999:999" }, - { - id: "server-conf", type: "file", path: "/var/lib/redis-module/redis.conf", mode: "0644", - content: "requirepass ${secret:default}\nappendonly no\ndir /data\n", - }, - { - id: "server", type: "container", name: "redis", image: pinned("redis"), network: "host", - volumes: [ - "/services/redis/data:/data", - "/var/lib/redis-module/redis.conf:/etc/redis/redis.conf:ro", - ], - args: ["/etc/redis/redis.conf"], - }, - { - id: "runtime", type: "container", name: "mesh-redis", image: pinned("mesh-runtime-redis"), - network: "host", - volumes: [ - "/var/lib/mesh/redis/broker:/run/secrets/broker:ro", - "/var/lib/redis-module/grants:/var/lib/redis-module/grants", - "/var/lib/redis-module/default.secret:/run/secrets/default:ro", - ], - env: { - MESH_BROKER_FILE: "/run/secrets/broker", - MESH_RECEIVES: "/var/lib/redis-module/grants/redis-cache.json", - MESH_PROVISION_REDIS: "127.0.0.1:6379", - MESH_PROVISION_PASSWORD_FILE: "/run/secrets/default", - }, - }, - ], - }); - await must(`printf %s ${quote(manifest)} > /tmp/a-cache.json && docker cp /tmp/a-cache.json mesh-controller:/a-cache.json`); - await mesh("module add /a-cache.json"); - await mesh(`module issue a-cache --node ${MACHINE}`); - await mesh(`assign ${MACHINE} a-cache`); - await mesh(`push ${MACHINE}`); - await settled(); - - const running = await must(`docker ps --format '{{.Names}}'`); - assert.match(running, /mesh-redis/, `redis's runtime is not running:\n${(await on(`docker logs mesh-redis 2>&1 | tail -20`)).out}`); - - // What the mesh delivers to the provider: a contributions file naming the consumer's login and - // where its password is, and the password itself as the file the host leaves after unsealing. - const password = "mesh-minted-9f3c2a"; - await must(`printf %s ${quote(password)} > /var/lib/redis-module/grants/app.secret`); - const contributions = JSON.stringify({ - contributions: 1, - requirement: "redis-cache", - generated: "by the mesh — do not edit", - given: [ - { from: "app", node: "app-node", at: "192.0.2.20:6379", as: "app-one", secret: "/var/lib/redis-module/grants/app.secret", values: {} }, - ], - }); - await must(`printf %s ${quote(contributions)} > /var/lib/redis-module/grants/redis-cache.json`); - - // Within a reconcile tick the provisioner creates the ACL user. It exists on the server. - let acl = ""; - const until = Date.now() + 60_000; - while (Date.now() < until) { - acl = (await on(`docker exec redis redis-cli -a ${quote(await must(`cat /var/lib/redis-module/default.secret`))} --no-auth-warning ACL LIST 2>/dev/null`)).out; - if (/app-one/.test(acl)) break; - await new Promise((r) => setTimeout(r, 3000)); - } - assert.match(acl, /app-one/, `the provisioner never created the consumer's login:\n${(await on(`docker logs mesh-redis 2>&1 | tail -30`)).out}\n---\n${acl}`); - - // The proof: authenticate as that consumer with the password the MESH minted. PONG means the - // provisioner created the login with exactly that password. A provisioner that invented its own - // (the old behaviour) would answer WRONGPASS here. - const authed = await on(`docker exec redis redis-cli --user app-one --pass ${quote(password)} --no-auth-warning PING 2>&1`); - assert.doesNotMatch(authed.out, /WRONGPASS/, - `the consumer could not authenticate with the mesh's password — the provider used a different one:\n${authed.out}`); - assert.match(authed.out, /PONG/, `expected PONG authenticating as the consumer:\n${authed.out}`); - - // And it needed no seal key: the runtime came up and provisioned with MESH_SEAL_KEY set nowhere. - const env = await must(`docker inspect mesh-redis --format '{{json .Config.Env}}'`); - assert.doesNotMatch(env, /MESH_SEAL_KEY/, `a seal key was set after all — ADR 0048 is not what ran:\n${env}`); - - // The provisioner emitted its lifecycle event under the bound account, and no emit was refused. - const log = (await on(`docker logs mesh-redis 2>&1`)).out; - assert.doesNotMatch(log, /emit .*failed/, `the provisioned event was refused:\n${log}`); -}); From 080150addb52c3f613da767084d5edef7336ac1a Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 23:35:17 +0200 Subject: [PATCH 4/8] provider-on-backend-network: no longer names a retired bed --- test/integration/provider-on-backend-network.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/provider-on-backend-network.test.ts b/test/integration/provider-on-backend-network.test.ts index 2903356..7590d0b 100644 --- a/test/integration/provider-on-backend-network.test.ts +++ b/test/integration/provider-on-backend-network.test.ts @@ -9,7 +9,7 @@ * is on the broker — none of which happens if it could not reach the broker from the bridge. * * This mirrors the redis committed manifest exactly (server on `redis` with a published port, runtime - * on `redis`), where provider-uses-mesh-credential used host networking. If this is green, the + * on `redis`), where the earlier host-networked bed (since retired) took the shortcut. If this is green, the * private-network shape is the one to roll out to every provider. */ From 7c8190257195c570bb523fbb0608be5e1e721fec Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 23:38:58 +0200 Subject: [PATCH 5/8] mesh.test.ts: meshboard gets a slug (ADR 0049); the builder-as-module test is retired, genesis proves it --- test/integration/mesh.test.ts | 119 ++-------------------------------- 1 file changed, 7 insertions(+), 112 deletions(-) diff --git a/test/integration/mesh.test.ts b/test/integration/mesh.test.ts index c78b762..498d450 100644 --- a/test/integration/mesh.test.ts +++ b/test/integration/mesh.test.ts @@ -326,7 +326,8 @@ test("a credential reaches both ends and the mesh holds neither", { skip, timeou // real program takes a credential: a sealed file is a password alone, and almost nothing reads // one. The mesh cannot compose the document — it discarded the value — so the module supplies it // with `${secret:...}` in it and the host, the only thing that sees both halves, fills it in. - await must("anchor", `printf %s '{"module":"meshboard","version":"1",` + + // A slug, so its identity on a backend stays within an S3 access key's 20 characters (ADR 0049). + await must("anchor", `printf %s '{"module":"meshboard","version":"1","slug":"board",` + `"requires":["postgres-database"],"contributes":{"postgres-database":{"name":"meshboard"}},` + `"binds":{"postgres-database":"/etc/meshboard/database.json"},` + `"secrets":{"postgres-database":"/etc/meshboard/database.password"},` + @@ -814,117 +815,11 @@ test("a machine filters exactly what its modules declared, and nothing else", { "the port stayed open after the module that wanted it was removed"); }); -test("the builder is a module the mesh assigns, with a credential the mesh delivered", { - skip: skip || (!builder ? "set MESH_LAB_BUILDER to a built mesh-builder" : false), - timeout: 900_000, -}, async () => { - // Until this, the builder was a program somebody started on a machine with whatever credential - // they had to hand — in practice the broker's administrative one. A program documented as - // holding its own credential and given somebody else's is worse than one with no story at all. - // - // So: the mesh issues a scoped account, seals it to the machine, and delivers it with the - // declaration. Nobody types it and the mesh cannot read it back. - await must("anchor", `mkdir -p /root/builder && printf %s '{"module":"self-builder","version":"1",` + - `"requires":["artifact-store"],"capabilities":["container-runtime"],` + - `"claims":[{"name":"the-build-machine","scope":"node"}],` + - `"binds":{"artifact-store":"/var/lib/mesh/builder/artifact-store.json"},` + - `"own-secrets":{"broker":"/var/lib/mesh/builder/broker"},` + - `"build":{"artifacts":[{"name":"builder","kind":"upstream",` + - `"from":"${pinned("mesh-builder")}"}]},` + - `"resources":[` + - `{"id":"state","type":"directory","path":"/var/lib/mesh/builder","mode":"0700"},` + - `{"id":"workspace","type":"directory","path":"/var/lib/mesh/builder/workspace","mode":"0700"},` + - `{"id":"run","type":"container","name":"mesh-builder","artifact":"builder",` + - `"network":"host",` + - `"volumes":["/var/lib/mesh/builder:/var/lib/mesh/builder",` + - `"/var/run/docker.sock:/var/run/docker.sock"],` + - `"env":{"MESH_BROKER_FILE":"/var/lib/mesh/builder/broker",` + - `"MESH_BINDING":"/var/lib/mesh/builder/artifact-store.json",` + - `"MESH_WORKSPACE":"/var/lib/mesh/builder/workspace"}}]}' > /root/builder/module.json`); - await must("anchor", `cd /root/builder && git init -q . && git add -A && ` + - `git -c user.email=lab -c user.name=lab commit -qm builder`); - - // The builder's own image is built by the builder that is already running — the same - // chicken-and-egg as the registry, resolved the same way. The one started by hand does this - // last piece of work and is then replaced by the module it just built. - await mesh("build /root/builder --wait 300s", 420_000); - - // The mesh makes the account and seals the URL to this machine. Nothing is printed that would - // work if it were pasted somewhere else. - const issued = await mesh("builder issue lab-builder --node anchor"); - assert.match(issued, /sealed to anchor/, issued); - assert.doesNotMatch(issued, /amqps:\/\/lab-builder:/, - "the credential was printed, so the one copy that matters is on a terminal"); - - // Now the hand-started one goes, or two builders race for the same queue and whichever answers - // proves nothing. By process name: `pkill -f` matches the shell running it too, which kills the - // connection carrying the command and hangs the caller waiting for a reply that will never - // come. Cost an hour once, in this file. - await on("anchor", `pkill -x mesh-builder`); - await new Promise((r) => setTimeout(r, 2000)); - assert.ok(!(await on("anchor", `pgrep -x mesh-builder`)).ok, - "the hand-started builder is still running, so this would test that one"); - - await mesh("assign anchor self-builder"); - await mesh("push anchor"); - await new Promise((r) => setTimeout(r, 20_000)); - - const running = await must("anchor", `docker ps --format '{{.Names}}'`); - assert.match(running, /mesh-builder/, - `the builder was assigned and is not running:\n${running}\n` + - `${(await on("anchor", `tail -30 /var/log/mesh-host.log`)).out}`); - - // Running is not connected. A builder that cannot reach the broker sits there, and every - // outward sign — the container is up, the credential is on disk — says it is working. - await new Promise((r) => setTimeout(r, 5000)); - const said = await on("anchor", `docker logs mesh-builder 2>&1 | tail -20`); - assert.doesNotMatch(said.out, /cannot reach the broker/, - `the builder is running and cannot reach the broker:\n${said.out}`); - - // The credential arrived, is readable only by the machine, and is the scoped account rather - // than the broker's own. - assert.match(await must("anchor", `stat -c %a /var/lib/mesh/builder/broker`), /^600/); - const credential = await must("anchor", `cat /var/lib/mesh/builder/broker`); - assert.match(credential, /"url":"amqps:\/\/lab-builder:/, - "the builder is using an account that is not its own"); - assert.doesNotMatch(credential, /guest:guest/, "the builder holds the broker's own account"); - // And what to check the broker against. A mesh's broker presents a certificate of the mesh's - // own, so a URL alone reaches only a broker some public authority vouches for — which is no - // mesh broker at all, and fails at TLS with an error about an unknown authority. - assert.match(credential, /"fingerprint":"(sha256:)?[0-9a-f]{64}"/, - `the builder was given nothing to verify the broker with:\n${credential}`); - - // And it works: the mesh asks this builder to build something, and it does. Answering is the - // only proof that the delivered credential authenticates — a container that is up with a - // credential it cannot use looks identical from outside. - // Somewhere the builder can actually see. A builder that is a module runs in a container, so - // the machine's filesystem is not its own — a path like /root only works for a builder somebody - // started on the host, which is what the first build above used. In a real mesh a module is - // cloned from the forge over a URL; here it goes in the directory the module already mounts, - // which is the same fact wearing different clothes. - const repo = "/var/lib/mesh/builder/repositories/built"; - await must("anchor", `mkdir -p ${repo} && printf %s '{"module":"built","version":"1",` + - `"resources":[{"id":"marker","type":"file","path":"/etc/built","content":"yes","mode":"0644"}]}' ` + - `> ${repo}/module.json`); - await must("anchor", `cd ${repo} && git init -q . && git add -A && ` + - `git -c user.email=lab -c user.name=lab commit -qm built`); - try { - await mesh(`build ${repo} --wait 300s`, 420_000); - } catch (why) { - // The builder's own account of itself. Without it the failure is "nothing consumed the - // queue", which names no cause and is the same sentence whether the credential was refused, - // the queue was never declared, or the process died three seconds in. - const said = (await on("anchor", `docker logs mesh-builder 2>&1 | tail -40`)).out; - throw new Error(`${(why as Error).message}\n\nwhat the builder said:\n${said}`); - } - - // Naming the module, and not merely containing its name: `builds` says "nothing has been built - // yet" when there is nothing, and that sentence contains the word this was matching on. - const recorded = await mesh("builds built"); - assert.doesNotMatch(recorded, /nothing has been built/, - `the build was accepted and no build was recorded against the module:\n${recorded}`); - assert.match(recorded, /built/, recorded); -}); +// The builder as a module the mesh assigns, with a credential the mesh delivered, is what genesis +// proves now (genesis-single installs the catalogue's builder through the installer, novox/hq ADR +// 0069). The test that lived here declared the builder's image as an upstream artifact by the bare +// image ID the lab holds, which is not a reference a registry copy can fetch (ADR 0096); retired +// 2026-09-21 rather than rewritten into a second genesis. test("rotating a credential moves both ends, and the old one stops working", { skip, timeout: 900_000, From 484fafe7992a7110d079d5dc3cb06f71e30ea039 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 23:43:01 +0200 Subject: [PATCH 6/8] mesh.test.ts: the consumer's login carries its slug --- test/integration/mesh.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/mesh.test.ts b/test/integration/mesh.test.ts index 498d450..c8e5eeb 100644 --- a/test/integration/mesh.test.ts +++ b/test/integration/mesh.test.ts @@ -372,7 +372,7 @@ test("a credential reaches both ends and the mesh holds neither", { skip, timeou assert.match(filled, /^PGPASSWORD=.+$/m, `the password was never put in:\n${filled}`); assert.ok(filled.includes(`PGPASSWORD=${onConsumer}`), `the file holds a different password from the credential file:\n${filled}`); - assert.match(filled, /^PGUSER=mesh_laptop_meshboard$/m, + assert.match(filled, /^PGUSER=mesh_laptop_board$/m, `the consumer was not told what name to present:\n${filled}`); assert.match(filled, /^PGPORT=5432$/m, `the port did not arrive as a port:\n${filled}`); assert.doesNotMatch(filled, /\$\{/, From 03bbc3757231991259631e7b4bfbe2cab8982eef Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 23:46:27 +0200 Subject: [PATCH 7/8] Beds issue only modules with a broker secret: an own secret is minted at resolve --- test/integration/route-forwarding.test.ts | 1 - test/integration/whole-mesh-full.test.ts | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/integration/route-forwarding.test.ts b/test/integration/route-forwarding.test.ts index 5100447..8371395 100644 --- a/test/integration/route-forwarding.test.ts +++ b/test/integration/route-forwarding.test.ts @@ -188,7 +188,6 @@ test("the mesh routes a public name through the proxy to the consumer, and withd for (const name of ["step-ca", "route-proxy", "hello-web"]) { await must(`printf %s ${quote(catalogueModule(name, held))} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-controller:/${name}.json`); await mesh(`module add /${name}.json`); - if (name === "step-ca") await mesh(`module issue ${name} --node ${MACHINE}`); await mesh(`assign ${MACHINE} ${name}`); } diff --git a/test/integration/whole-mesh-full.test.ts b/test/integration/whole-mesh-full.test.ts index 5c9a0d8..48b008b 100644 --- a/test/integration/whole-mesh-full.test.ts +++ b/test/integration/whole-mesh-full.test.ts @@ -800,9 +800,10 @@ test("the full mesh forms across the access point and both server sets converge" for (const { name } of mods) { try { const broker = await ensureAdded(name); - // Issued when the module holds a broker account or an own-secret the mesh mints for it - // (step-ca's password, ADR 0098); a requirement kept in the vault needs no issue. - if (broker || loadManifest(name).manifest.includes('"own-secrets"')) await mesh(`module issue ${name} --node ${node}`); + // Issued only when the module holds a broker account: an own secret the mesh mints + // (step-ca's password) is minted at resolve, and `module issue` refuses a module with no + // broker secret to deliver into (issue 078). + if (broker) await mesh(`module issue ${name} --node ${node}`); await mesh(`assign ${node} ${name}`); assigned[node]!.add(name); } catch (err) { From 2da442729ff235b6c4f87a4bbbdb2d5e6f65c5db Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 23:56:02 +0200 Subject: [PATCH 8/8] Review: the node_modules link is not committed; two beds no longer issue modules with no broker secret; a stale header --- test/integration/local-model-bed.test.ts | 3 ++- test/integration/openai-bed.test.ts | 3 ++- test/integration/runtime-restart-on-config.test.ts | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/test/integration/local-model-bed.test.ts b/test/integration/local-model-bed.test.ts index 7263ff9..b4d1667 100644 --- a/test/integration/local-model-bed.test.ts +++ b/test/integration/local-model-bed.test.ts @@ -119,7 +119,8 @@ async function settled(withinMs = 600_000): Promise { async function addAssign(name: string, manifest: string): Promise { await must(`printf %s ${quote(manifest)} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-controller:/${name}.json`); await mesh(`module add /${name}.json`); - await mesh(`module issue ${name} --node ${MACHINE}`); + // No `module issue`: neither module speaks on the bus, and issuing a module with no broker + // secret to deliver into is refused (novox/hq issue 078). await mesh(`assign ${MACHINE} ${name}`); } diff --git a/test/integration/openai-bed.test.ts b/test/integration/openai-bed.test.ts index 885640c..3caed9c 100644 --- a/test/integration/openai-bed.test.ts +++ b/test/integration/openai-bed.test.ts @@ -178,7 +178,8 @@ test("a static-key model-access licence delivers the operator's API key to the c const consumerManifest = catalogueModule("openai-consumer", held); await must(`printf %s ${quote(consumerManifest)} > /tmp/openai-consumer.json && docker cp /tmp/openai-consumer.json mesh-controller:/openai-consumer.json`); await mesh(`module add /openai-consumer.json`); - await mesh(`module issue openai-consumer --node ${MACHINE}`); + // No `module issue`: the consumer speaks on no bus, and issuing a module with no broker secret + // to deliver into is refused (novox/hq issue 078). await mesh(`assign ${MACHINE} openai-consumer`); await mesh(`push ${MACHINE}`); await settled(); diff --git a/test/integration/runtime-restart-on-config.test.ts b/test/integration/runtime-restart-on-config.test.ts index 1b5659f..087ccf6 100644 --- a/test/integration/runtime-restart-on-config.test.ts +++ b/test/integration/runtime-restart-on-config.test.ts @@ -8,7 +8,7 @@ * service has: the runtime names its config resource, and the host recreates the container when that * resource changed this pass. * - * This assigns grafana configured by settings, then changes the token and pushes again, and asserts + * This assigns a settings-configured runtime (the fixture wears no catalogue name; its image is grafana's tool runtime), then changes the token and pushes again, and asserts * the container was replaced (a new container id) and the config on disk carries the new value. * It builds the host from source (no --no-build), because the behaviour under test is the host's. *