From 690596d33bbfafbb75aa76d211ee7f39b46bbe75 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 22:16:15 +0200 Subject: [PATCH 1/9] The whole-mesh bed installs the vault before the modules that keep a secret from it; route-forwarding waits on issue 076 --- test/beds-read-the-catalogue.test.ts | 2 +- test/integration/whole-mesh-novox.test.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/test/beds-read-the-catalogue.test.ts b/test/beds-read-the-catalogue.test.ts index 932326b..720a356 100644 --- a/test/beds-read-the-catalogue.test.ts +++ b/test/beds-read-the-catalogue.test.ts @@ -57,7 +57,7 @@ const STILL_CARRIED: Record = { "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" }, "route-forwarding.test.ts": { modules: ["route-proxy", "hello-web"], - why: "WEARING: route-proxy without its certificate authority, hello-web with the route shape ADR 0066 replaced" }, + why: "WEARING: route-proxy without its certificate authority, hello-web with the route shape ADR 0066 replaced — waits on novox/hq 04-ISSUES/076 (step-ca's root is a minted secret, so the catalogue's authority cannot be raised beside the proxy yet)" }, "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" }, }; diff --git a/test/integration/whole-mesh-novox.test.ts b/test/integration/whole-mesh-novox.test.ts index b44ab47..399fa85 100644 --- a/test/integration/whole-mesh-novox.test.ts +++ b/test/integration/whole-mesh-novox.test.ts @@ -68,6 +68,9 @@ const NODE = "novox"; */ const MODULES: { name: string; containers: string[]; node?: boolean }[] = [ { name: "postgres", containers: ["mesh-store", "mesh-postgres"] }, + // The vault, before everything that keeps a secret from it: redis, gitea, umami, influxdb, + // mailu require one (novox/hq ADRs 0085, 0094). + { name: "mesh-vault", containers: ["mesh-vault"] }, { name: "redis", containers: ["redis", "mesh-redis"] }, { name: "minio", containers: ["minio", "mesh-minio"] }, { name: "mongodb", containers: ["mongo", "mesh-mongodb"] }, -- 2.54.0 From 107257faf48e29f3600894dd4530d8918a9c6118 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 22:27:17 +0200 Subject: [PATCH 2/9] The route-forwarding bed installs the catalogue's authority, proxy and consumer step-ca beside the proxy, the proxy fetching the authority's root through its gate, hello-web routed under the node's public domain by its label (ADR 0066). The last declared mesh test but one reads the catalogue (novox/hq 04-ISSUES/074, 076). --- test/beds-read-the-catalogue.test.ts | 2 - test/integration/route-forwarding.test.ts | 80 +++++------------------ 2 files changed, 16 insertions(+), 66 deletions(-) diff --git a/test/beds-read-the-catalogue.test.ts b/test/beds-read-the-catalogue.test.ts index 720a356..39bfcdf 100644 --- a/test/beds-read-the-catalogue.test.ts +++ b/test/beds-read-the-catalogue.test.ts @@ -56,8 +56,6 @@ const STILL_CARRIED: Record = { 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" }, - "route-forwarding.test.ts": { modules: ["route-proxy", "hello-web"], - why: "WEARING: route-proxy without its certificate authority, hello-web with the route shape ADR 0066 replaced — waits on novox/hq 04-ISSUES/076 (step-ca's root is a minted secret, so the catalogue's authority cannot be raised beside the proxy yet)" }, "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" }, }; diff --git a/test/integration/route-forwarding.test.ts b/test/integration/route-forwarding.test.ts index 9ded2b1..8fb35da 100644 --- a/test/integration/route-forwarding.test.ts +++ b/test/integration/route-forwarding.test.ts @@ -37,7 +37,7 @@ 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 { labIsUsable, destroyAll, foundationBundle, catalogueModule, catalogueIsPresent } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -50,12 +50,14 @@ const skip = !capability.usable ? "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; + : catalogueIsPresent(); const SCENARIO = "route-forwarding"; const MACHINE = "anchor"; const NAME = "hello.example"; const PAGE = "hello from hello-web, routed by the mesh"; +/** The node's public domain; hello-web's label composes under it (ADR 0066). */ +const DOMAIN = "example"; let instanceId = ""; let held: HeldImage[] = []; @@ -85,10 +87,6 @@ async function mesh(command: string, timeoutMs?: number): Promise { } /** The reference a manifest should carry, once this scenario has been raised. */ -/** 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); -} /** The foundation bundle: ours by the ID the machine holds, everything else upstream. */ function bundleFor(images: HeldImage[]): string { @@ -167,64 +165,18 @@ after(async () => { test("the mesh routes a public name through the proxy to the consumer, and withdraws it on unassign", { skip, timeout: 1_500_000, }, async () => { - // The PROVIDER: route-proxy in the plain-HTTP shape — provides `route`, is given every consumer as - // the file at receives.route, forwards by Host. No TLS here (that is certificates.test.ts); the - // image is pinned to what this scenario serves by digest. - const proxyManifest = JSON.stringify({ - module: "route-proxy", - version: "1", - capabilities: ["container-runtime"], - provides: [{ name: "route", scope: "mesh" }], - serves: { route: {} }, - receives: { route: "/var/lib/route-proxy/routes/mesh.json" }, - listens: [{ port: 80, protocol: "tcp", from: "anywhere", why: "public HTTP; the route-forwarding front door" }], - resources: [ - { id: "state", type: "directory", path: "/var/lib/route-proxy", mode: "0700" }, - { id: "routes-dir", type: "directory", path: "/var/lib/route-proxy/routes", mode: "0700" }, - { - id: "server", type: "container", name: "route-proxy", - image: pinned("mesh-route-proxy"), network: "host", - volumes: ["/var/lib/route-proxy/routes:/routes:ro"], - env: { ROUTES: "/routes/mesh.json", LISTEN: ":80" }, - }, - ], - }); - - // The CONSUMER: hello-web requires `route` and contributes the name it wants and the port it - // listens on. It runs no code of the mesh's — a bare alpine serving a fixed page over a busybox nc - // loop stands in for a web service. `contributes` is what makes it *ask*: the grant forms from it. - const webManifest = JSON.stringify({ - module: "hello-web", - slug: "hello", - version: "1", - capabilities: ["container-runtime"], - requires: ["route"], - contributes: { route: { name: NAME, port: 8080 } }, - binds: { route: "/var/lib/hello-web/route.json" }, - listens: [{ port: 8080, protocol: "tcp", from: "mesh", why: "the demo page; only the proxy reaches it" }], - resources: [ - { id: "state", type: "directory", path: "/var/lib/hello-web", mode: "0700" }, - { id: "page", type: "file", path: "/var/lib/hello-web/index.html", mode: "0644", content: `${PAGE}\n` }, - { id: "net", type: "network", name: "hello-web" }, - { - id: "server", type: "container", name: "hello-web", - image: pinned("alpine"), network: "hello-web", ports: ["8080:8080"], - volumes: ["/var/lib/hello-web/index.html:/www/index.html:ro"], - args: ["sh", "-c", - "while true; do { printf 'HTTP/1.1 200 OK\\r\\nContent-Type: text/plain\\r\\nConnection: close\\r\\n\\r\\n'; cat /www/index.html; } | nc -l -p 8080; done"], - }, - ], - }); - - await must(`printf %s ${quote(proxyManifest)} > /tmp/route-proxy.json && docker cp /tmp/route-proxy.json mesh-controller:/route-proxy.json`); - await mesh("module add /route-proxy.json"); - // No `module issue`: route-proxy has no broker account and no own-secret to mint. `assign` resolves - // its plan and the provider is matchable by a consumer's route from that alone. - await mesh(`assign ${MACHINE} route-proxy`); - - await must(`printf %s ${quote(webManifest)} > /tmp/hello-web.json && docker cp /tmp/hello-web.json mesh-controller:/hello-web.json`); - await mesh("module add /hello-web.json"); - await mesh(`assign ${MACHINE} hello-web`); + // All three from the catalogue (novox/hq ADR 0093, issue 074): the authority the proxy requires, + // the proxy, and the consumer. The proxy's manifest names the runtime image the scenario stocks; + // the authority and the consumer's server are pulled upstream by digest. The name the consumer + // is routed under is composed from its label and the node's public domain (ADR 0066), which + // the bed sets first. + await mesh(`node public-domain ${MACHINE} ${DOMAIN}`); + 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}`); + } await mesh(`push ${MACHINE}`); await settled(); -- 2.54.0 From 2530ca762e22baa249cc1afc268f9586618036c0 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 22:33:44 +0200 Subject: [PATCH 3/9] The lab stands in for the builder on an upstream artifact too: the reference the manifest pins hello-web's server is somebody else's image the mesh would copy in (ADR 0096); the loader refused it as an artifact nobody stocked. --- test/catalogue-module.test.ts | 14 ++++++++++++++ test/integration/harness.ts | 14 ++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/test/catalogue-module.test.ts b/test/catalogue-module.test.ts index 45195e3..6ebab3a 100644 --- a/test/catalogue-module.test.ts +++ b/test/catalogue-module.test.ts @@ -85,3 +85,17 @@ test("a manifest the catalogue does not have is refused by name", () => { assert.throws(() => catalogueModule("nothing", held), /no manifest for nothing/); } finally { restore(); } }); + +test("an upstream artifact resolves to the reference the manifest pins, as the machine pulls it", () => { + const web = { + module: "thing", version: "1", + resources: [{ id: "server", type: "container", name: "web", artifact: "server" }], + build: { artifacts: [{ name: "server", kind: "upstream", from: "alpine@" + digest("e") }] }, + }; + const restore = aCatalogueWith(web); + try { + const m = JSON.parse(catalogueModule("thing", held)) as { resources: Record[] }; + assert.equal(m.resources[0]!["image"], "alpine@" + digest("e")); + assert.equal(m.resources[0]!["artifact"], undefined); + } finally { restore(); } +}); diff --git a/test/integration/harness.ts b/test/integration/harness.ts index a85422c..6c3cded 100644 --- a/test/integration/harness.ts +++ b/test/integration/harness.ts @@ -306,12 +306,22 @@ export interface ForTheLab { export function catalogueModule(module: string, held: HeldImage[], lab: ForTheLab = {}): string { const m = JSON.parse(readFileSync(catalogueManifest(module), "utf8")) as { resources?: { id: string; type: string; image?: string; artifact?: string; ports?: string[]; env?: Record }[]; - build?: unknown; + build?: { artifacts?: { name: string; kind: string; from?: string }[] }; }; const artifacts: Record = { runtime: `mesh-runtime-${module}`, ...(lab.artifacts ?? {}) }; + // An upstream artifact is somebody else's image, which the mesh's builder copies into its own + // registry (ADR 0096). The lab stands in for the builder by using the reference the manifest + // pins, which the machine pulls over its uplink — the same bytes, without the copy. + const upstream = new Map(); + for (const a of m.build?.artifacts ?? []) { + if (a.kind === "upstream" && a.from) upstream.set(a.name, a.from); + } for (const r of m.resources ?? []) { if (r.type !== "container") continue; - if (typeof r.artifact === "string") { + if (typeof r.artifact === "string" && upstream.has(r.artifact) && !lab.artifacts?.[r.artifact]) { + r.image = onTheMachine(upstream.get(r.artifact)!, held); + delete r.artifact; + } else if (typeof r.artifact === "string") { const repository = artifacts[r.artifact]; assert.ok(repository, `${module}'s container '${r.id}' names the "${r.artifact}" artifact, which the mesh would ` + -- 2.54.0 From d95174da027f226de9017567b8fb1426fe92eeda Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 22:35:33 +0200 Subject: [PATCH 4/9] The route-forwarding bed places its machine on the overlay: the authority certifies its private-network address --- test/integration/route-forwarding.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/integration/route-forwarding.test.ts b/test/integration/route-forwarding.test.ts index 8fb35da..b6ecce0 100644 --- a/test/integration/route-forwarding.test.ts +++ b/test/integration/route-forwarding.test.ts @@ -171,6 +171,11 @@ test("the mesh routes a public name through the proxy to the consumer, and withd // is routed under is composed from its label and the node's public domain (ADR 0066), which // the bed sets first. await mesh(`node public-domain ${MACHINE} ${DOMAIN}`); + // The authority certifies itself for the machine's private-network address, which is what a + // consumer on any node dials; a machine raised from the bundle has none until it is placed on + // the overlay. Placed as a hub of one, the way a real first node is. + await mesh(`overlay place ${MACHINE} --hub --endpoint 192.0.2.10:51820 --site lab`); + await mesh(`assign ${MACHINE} networking`); 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`); -- 2.54.0 From f785f5892a15d9475441af21f186ff94214e5dab Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 22:41:36 +0200 Subject: [PATCH 5/9] The route-forwarding bed converges the overlay before the modules: the proxy fetches the roots at the private-network address at first start --- test/integration/route-forwarding.test.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/test/integration/route-forwarding.test.ts b/test/integration/route-forwarding.test.ts index b6ecce0..5100447 100644 --- a/test/integration/route-forwarding.test.ts +++ b/test/integration/route-forwarding.test.ts @@ -37,7 +37,7 @@ 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, catalogueModule, catalogueIsPresent } from "./harness.ts"; +import { labIsUsable, destroyAll, foundationBundle, catalogueModule, catalogueIsPresent, deriveTheFilterOn } from "./harness.ts"; import type { HeldImage } from "../../src/pinning.ts"; const capability = await labIsUsable(); @@ -172,10 +172,19 @@ test("the mesh routes a public name through the proxy to the consumer, and withd // the bed sets first. await mesh(`node public-domain ${MACHINE} ${DOMAIN}`); // The authority certifies itself for the machine's private-network address, which is what a - // consumer on any node dials; a machine raised from the bundle has none until it is placed on - // the overlay. Placed as a hub of one, the way a real first node is. + // consumer on any node dials (ADR 0098); a machine raised from the bundle has none until it is + // placed on the overlay. Placed as a hub of one, the way a real first node is, and converged + // BEFORE the modules arrive: the proxy fetches the authority's roots at that address at first + // start, so the interface must exist by then — in one push the order between modules is not + // promised. The derived filter admits the hub's port, as genesis does on a control-node (ADR 0088). await mesh(`overlay place ${MACHINE} --hub --endpoint 192.0.2.10:51820 --site lab`); await mesh(`assign ${MACHINE} networking`); + await mesh(`push ${MACHINE}`); + await settled(); + await deriveTheFilterOn({ machine: MACHINE, node: MACHINE, hubPort: 51820, + must: (_m, c, t) => must(c, t), mesh, on: (_m, c, t) => on(c, t) }); + const overlay = await must(`ip -4 addr show dev mesh0 2>&1 || ip -4 addr 2>&1`); + assert.match(overlay, /inet 10\./, `${MACHINE} has no private-network address after networking converged:\n${overlay}`); 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`); -- 2.54.0 From acb8d3da8852746676eaf8a806569b37d31ff854 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 22:53:28 +0200 Subject: [PATCH 6/9] whole-mesh-full: the vault before the modules that keep secrets in it; no operator root or app secrets delivered (ADRs 0094, 0098) --- test/integration/whole-mesh-full.test.ts | 67 +++--------------------- 1 file changed, 7 insertions(+), 60 deletions(-) diff --git a/test/integration/whole-mesh-full.test.ts b/test/integration/whole-mesh-full.test.ts index 17d1dbf..a73493c 100644 --- a/test/integration/whole-mesh-full.test.ts +++ b/test/integration/whole-mesh-full.test.ts @@ -179,6 +179,9 @@ const NOVOX: Mod[] = [ { name: "mongodb", containers: ["mongo", "mesh-mongodb"] }, { name: "mssql", containers: ["mssql", "mesh-mssql"] }, { name: "lavinmq", containers: ["lavinmq", "mesh-lavinmq"] }, + // The vault, before everything that keeps a secret from it: gitea, umami, influxdb, mailu + // require one (novox/hq ADRs 0085, 0094). + { name: "mesh-vault", containers: ["mesh-vault"] }, // ADR 0066: the proxy now REQUIRES an `acme-ca`, so the bed must assign a provider of one or // route-proxy is unresolvable and takes every routed module down with it. step-ca is that // provider, on the anchor, at mesh scope. @@ -306,14 +309,11 @@ const CREDENTIALS: { node: string; module: string; name: string; crash: string } { node: "ace", module: "home-assistant", name: "token", crash: "no Home Assistant token" }, { node: "ace", module: "nzbget", name: "password", crash: "NZBGet not configured" }, { node: "ace", module: "qbittorrent", name: "password", crash: "qBittorrent not configured" }, - { node: "novox", module: "umami", name: "admin", crash: "admin password is not set" }, ]; -/** Operator secrets for the credential modules that own-secret their whole app (mailu, de-spiegel). */ +/** Operator secrets for the credential modules that own-secret their whole app (de-spiegel, + * amqp-email-forwarder). mailu's and umami's are kept in the vault now (ADR 0094), not delivered. */ const OPERATOR_SECRETS: { node: string; module: string; name: string; value: string }[] = [ - { node: "novox", module: "mailu", name: "secret-key", value: "0123456789abcdef0123456789abcdef" }, - { node: "novox", module: "mailu", name: "admin", value: "MailuAdminFakePass123" }, - { node: "novox", module: "mailu", name: "api-token", value: "mailuapitokenfake0123456789abcd" }, { 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" }, @@ -410,56 +410,8 @@ async function psMapOf(node: string): Promise> { return map; } -/** - * ADR 0066: the internal CA is initialised FROM AN OPERATOR'S ROOT — it does not mint its own. - * - * So the bed has to be an operator. The material is made on the anchor with openssl and handed to - * the mesh through the real `secret accept` path, exactly as a person would: the mesh cannot invent - * a PEM, and the random 32 bytes it makes for an own-secret nobody supplied would leave step-ca - * crash-looping on a root key that is not a key. - */ -async function deliverCaRoot(): Promise { - const made = await on(CONTROL, [ - "set -e", - "mkdir -p /tmp/ca && cd /tmp/ca", - // No trailing newline on a password file: step-ca reads the file as the password itself. - "openssl rand -hex 16 | tr -d '\\n' > key-password", - "openssl ecparam -genkey -name prime256v1 -out root.unenc", - "openssl ec -in root.unenc -aes256 -passout file:key-password -out root.key", - "rm -f root.unenc", - "openssl req -x509 -new -key root.key -passin file:key-password -sha256 -days 3650" + - ` -out root.crt -subj "/CN=Mesh Internal CA/O=Novox Mesh Lab"`, - // Readable by the control plane, which is not root. Its image is FROM scratch and runs as - // 65534, and `docker cp` keeps the ownership and mode a file had outside — openssl writes a - // private key 0600 root-owned, so the copy landed unreadable and `secret accept` failed with - // `open /ca-root-key: permission denied`. The CA then crash-looped on a root it never got. - // Chowning it inside the container is not available: there is no shell in there to do it with. - // - // Safe here and nowhere else: these three exist for the seconds between being written and - // being sealed to the machine, on a lab node, for a CA thrown away with the scenario. - "chmod 0644 /tmp/ca/root.crt /tmp/ca/root.key /tmp/ca/key-password", - "docker cp /tmp/ca/root.crt mesh-controller:/ca-root-cert", - "docker cp /tmp/ca/root.key mesh-controller:/ca-root-key", - "docker cp /tmp/ca/key-password mesh-controller:/ca-root-key-password", - ].join("\n"), 180_000); - if (!made.ok) { - console.log(`CA ROOT NOT MADE on ${CONTROL}:\n${made.out.split("\n").slice(-8).join("\n")}`); - return false; - } - for (const [name, file] of [ - ["root-cert", "/ca-root-cert"], - ["root-key", "/ca-root-key"], - ["root-key-password", "/ca-root-key-password"], - ] as const) { - try { - await mesh(`secret accept ${CONTROL} step-ca ${name} --from ${file}`); - } catch (err) { - console.log(`CA ROOT ACCEPT FAILED (${name}): ${(err as Error).message.split("\n").slice(0, 3).join(" | ")}`); - return false; - } - } - return true; -} +// ADR 0098: the internal authority makes its own root at first start and serves it; the proxy +// fetches it. No operator root is delivered — the mesh mints only the authority's password. /** What a module's manifest says its route label is, or "" if it contributes no route. */ function routeLabelOf(name: string): string { @@ -889,10 +841,6 @@ test("the full mesh forms across the access point and both server sets converge" } } - // ADR 0066: the CA's root, before the push that would otherwise deliver a random 32 bytes for it. - const caRootDelivered = assigned["novox"]!.has("step-ca") ? await deliverCaRoot() : false; - if (!caRootDelivered) console.log("ADR 0066: no operator root delivered; step-ca cannot initialise."); - // ONE push per node (workstations first — cheap — then the heavy service nodes). const pushError: Record = {}; for (const node of ["shanks", "g14", "novox", "ace"]) { @@ -1027,7 +975,6 @@ test("the full mesh forms across the access point and both server sets converge" ? (await on("novox", `curl -s -o /dev/null -w '%{http_code}' --max-time 10 -H 'Host: ${probe.name}' http://127.0.0.1/`)).out.trim() : ""; adr.push(`\n proxy answers for ${probe?.name ?? "(nothing composed)"}: ${servedCode || "no answer"}`); - adr.push(` operator root delivered to step-ca: ${caRootDelivered}`); adr.push(` step-ca container: ${psMaps["novox"]?.get("step-ca") ?? "MISSING"}`); console.log(adr.join("\n")); -- 2.54.0 From e64d296c800c644dbfc47d2a31786867c030fdb6 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 22:54:09 +0200 Subject: [PATCH 7/9] whole-mesh-full: issue a module with an own-secret, not only one with a broker account --- test/integration/whole-mesh-full.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/integration/whole-mesh-full.test.ts b/test/integration/whole-mesh-full.test.ts index a73493c..2d7e75f 100644 --- a/test/integration/whole-mesh-full.test.ts +++ b/test/integration/whole-mesh-full.test.ts @@ -800,7 +800,9 @@ 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); - if (broker) await mesh(`module issue ${name} --node ${node}`); + // 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 || /"secrets":\s*\[/.test(loadManifest(name).manifest)) await mesh(`module issue ${name} --node ${node}`); await mesh(`assign ${node} ${name}`); assigned[node]!.add(name); } catch (err) { -- 2.54.0 From 2ce130c256bdead40f21905df2aad674bc4b8183 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 22:55:00 +0200 Subject: [PATCH 8/9] whole-mesh-full: an own-secret is declared under own-secrets --- 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 2d7e75f..6dfef61 100644 --- a/test/integration/whole-mesh-full.test.ts +++ b/test/integration/whole-mesh-full.test.ts @@ -802,7 +802,7 @@ test("the full mesh forms across the access point and both server sets converge" 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 || /"secrets":\s*\[/.test(loadManifest(name).manifest)) await mesh(`module issue ${name} --node ${node}`); + if (broker || loadManifest(name).manifest.includes('"own-secrets"')) await mesh(`module issue ${name} --node ${node}`); await mesh(`assign ${node} ${name}`); assigned[node]!.add(name); } catch (err) { -- 2.54.0 From b0eddd28d8899b2d580f1eb9a9a4e536767230e1 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 22:56:25 +0200 Subject: [PATCH 9/9] build-route-proxy-image.sh builds FROM the bases the manifest declares --- scripts/build-route-proxy-image.sh | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/build-route-proxy-image.sh b/scripts/build-route-proxy-image.sh index f660349..42e85d6 100755 --- a/scripts/build-route-proxy-image.sh +++ b/scripts/build-route-proxy-image.sh @@ -22,7 +22,18 @@ DOCKERFILE="$MESH_CATALOG/modules/route-proxy/Dockerfile" [ -f "$MESH_CONTROL/examples/route-proxy/main.go" ] || { echo "no proxy source at $MESH_CONTROL/examples/route-proxy" >&2; exit 1; } +# The bases the manifest declares (novox/hq ADR 0097) are what this build starts FROM — the same +# images the mesh's builder would copy and hand the recipe, not the Dockerfile's floating defaults. +BASES=() +while IFS=$'\t' read -r arg image; do + [ -n "$arg" ] && BASES+=(--build-arg "$arg=$image") +done < <(python3 -c ' +import json, sys +for on in json.load(open(sys.argv[1])).get("build", {}).get("on", []): + print(on["arg"], on["image"], sep="\t") +' "$MESH_CATALOG/modules/route-proxy/module.json") + # Context is the mesh-controller repository root: the proxy compiles against that module's go.mod and # its examples/route-proxy package. -docker build -f "$DOCKERFILE" -t "$TAG" "$MESH_CONTROL" +docker build -f "$DOCKERFILE" "${BASES[@]}" -t "$TAG" "$MESH_CONTROL" echo "built $TAG (from $MESH_CONTROL/examples/route-proxy)" -- 2.54.0