From 64c081d0259c5d89285bc19506f8ff1fb437f199 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 20:29:49 +0200 Subject: [PATCH 1/3] The vault bed installs a consumer that keeps two secrets, and both are delivered and rotated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two files with two values, two holders in the vault's ledger — the identity with the local name after it — and one rotate moves both (novox/hq ADR 0094). --- test/integration/assigned-vault.test.ts | 38 +++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/integration/assigned-vault.test.ts b/test/integration/assigned-vault.test.ts index b3eaf9a..d2e6225 100644 --- a/test/integration/assigned-vault.test.ts +++ b/test/integration/assigned-vault.test.ts @@ -259,9 +259,32 @@ test("redis's own password is a secret the vault provides: it authenticates, and assert.match(issued, /scoped to what it emits and consumes/, issued); await mesh(`assign ${MACHINE} ${name}`); } + // A consumer that needs TWO values from the vault (novox/hq ADR 0094): its `secrets` entry names + // them under local names, and each is a pair of its own. It runs no code — the delivery is what + // is under test. Synthetic, so it wears no catalogue module's name. + const twoSecrets = JSON.stringify({ + module: "two-secrets", version: "1", slug: "two", + requires: ["secret"], + secrets: { secret: { first: "/var/lib/two-secrets/first", second: "/var/lib/two-secrets/second" } }, + resources: [{ id: "state", type: "directory", path: "/var/lib/two-secrets", mode: "0700" }], + }); + await must(`printf %s ${quote(twoSecrets)} > /tmp/two-secrets.json && docker cp /tmp/two-secrets.json mesh-controller:/two-secrets.json`); + await mesh("module add /two-secrets.json"); + await mesh(`assign ${MACHINE} two-secrets`); await mesh(`push ${MACHINE}`); await settled(); + // Two files, two values, and the vault holds two holders for one module — the identity with the + // local name after it. + const first = (await must(`cat /var/lib/two-secrets/first`)).replace(/\n$/, ""); + const second = (await must(`cat /var/lib/two-secrets/second`)).replace(/\n$/, ""); + assert.ok(first.length >= 20 && second.length >= 20, "a two-secrets value is empty or implausibly short"); + assert.notEqual(first, second, "two local names were given one value"); + for (const holderOf of ["mesh_anchor_two_first", "mesh_anchor_two_second"]) { + await until(`the vault holding ${holderOf}`, 90_000, async () => + (await on(`test -s ${LEDGER}/${holderOf}.json`)).ok ? true : undefined); + } + const running = await must(`docker ps --format '{{.Names}}'`); for (const c of ["mesh-vault", "redis", "mesh-redis"]) { assert.match(running, new RegExp(`(^|\\n)${c}(\\n|$)`), @@ -335,6 +358,21 @@ test("rotating the secret moves both ends: the new password works, the old one i }); assert.notEqual(after, before); + // Both of the two-secrets consumer's values moved too, apart from each other (ADR 0094). + const firstAfter = await until("the rotated first secret", 180_000, async () => { + const now = (await must(`cat /var/lib/two-secrets/first`)).replace(/\n$/, ""); + return now !== "" ? now : undefined; + }); + const secondAfter = (await must(`cat /var/lib/two-secrets/second`)).replace(/\n$/, ""); + assert.notEqual(firstAfter, secondAfter, "two local names were given one value after rotation"); + for (const holderOf of ["mesh_anchor_two_first", "mesh_anchor_two_second"]) { + const h = await until(`the vault recording ${holderOf}'s rotation`, 90_000, async () => { + const got = JSON.parse(await must(`cat ${LEDGER}/${holderOf}.json`)) as Held; + return got.rotations >= 1 ? got : undefined; + }); + assert.equal(h.rotations, 1, holderOf); + } + // Three logins. The new one works (redis was restarted on its config — `restart-on`), the old one // does not: that third check is what makes it a rotation rather than an addition. await until("redis accepting the rotated password", 180_000, async () => { -- 2.54.0 From 2f11a29c4d18ced2f4612361f662b64c9e22f5be Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 20:31:47 +0200 Subject: [PATCH 2/3] The runtime build installs the module's SDK itself and shows its compiler's output A module never built on this workstation had no node_modules, tsc failed on the first import behind /dev/null, and the suite reported a build that said nothing. --- scripts/build-module-runtime.sh | 11 ++++++++++- src/rebuild.ts | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/scripts/build-module-runtime.sh b/scripts/build-module-runtime.sh index 17a9192..53f57a5 100755 --- a/scripts/build-module-runtime.sh +++ b/scripts/build-module-runtime.sh @@ -33,7 +33,16 @@ SRCS=(); for f in \ pg.d.ts; do [ -f "$MOD/$f" ] && SRCS+=("$f") done -TSC="$MESH_SDK/node_modules/.bin/tsc"; ( cd "$MOD" && "$TSC" "${SRCS[@]}" --module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist >/dev/null ) +# The module compiles against the SDK, which its package.json names and nothing installs: a module +# never built on this workstation has no node_modules, and tsc fails on the first import. Installed +# as a package copy from the sibling checkout (never a link) when absent — the compile needs only +# the types; the image takes the SDK from MESH_SDK below. +if [ ! -e "$MOD/node_modules/@novox/mesh-sdk" ]; then + ( cd "$MOD" && npm install --no-save --install-links --no-package-lock --ignore-scripts --silent "$MESH_SDK" ) \ + || { echo "cannot install the SDK into $MOD for the compile" >&2; exit 1; } +fi +# Output kept: a compile error hidden behind /dev/null is a build that fails saying nothing. +TSC="$MESH_SDK/node_modules/.bin/tsc"; ( cd "$MOD" && "$TSC" "${SRCS[@]}" --module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist 1>&2 ) STAGE="$(mktemp -d)"; trap 'rm -rf "$STAGE"' EXIT cp -r "$MESH_TOOLS/dist" "$STAGE/dist" diff --git a/src/rebuild.ts b/src/rebuild.ts index e20dda8..d6276f3 100644 --- a/src/rebuild.ts +++ b/src/rebuild.ts @@ -134,7 +134,7 @@ export function rebuild(env: NodeJS.ProcessEnv = process.env, testFiles: string[ // the code in front of you, which is the whole of 005. throw new Error( `could not build the ${build.what}: ${build.argv.join(" ")} in ${build.in}\n\n` + - `${(ran.stderr || ran.stdout || String(ran.error)).trim()}`, + `${(ran.stderr || ran.stdout || (ran.error ? String(ran.error) : `exit status ${ran.status}, and it said nothing`)).trim()}`, ); } built.push(build.what); -- 2.54.0 From 85dc827660205693fa0afdaa05ec5fecd164319c Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 20:34:03 +0200 Subject: [PATCH 3/3] The confluence bed asks a tool through the control plane, and the tool answers No account in the mesh but the control plane's may create a reply queue and publish to a module's request key (novox/hq 04-ISSUES/049, ADR 0095). --- test/integration/assigned-tools-confluence.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/integration/assigned-tools-confluence.test.ts b/test/integration/assigned-tools-confluence.test.ts index faafcf1..1d7e57a 100644 --- a/test/integration/assigned-tools-confluence.test.ts +++ b/test/integration/assigned-tools-confluence.test.ts @@ -204,6 +204,15 @@ test("the mesh assigns confluence: its tools-only runtime comes up and serves th assert.match(served2, /serve\.confluence\.confluence_search/, `confluence's runtime never bound its serve queue:\n${(await on(`docker logs mesh-runtime-confluence 2>&1 | tail -20`)).out}\n---\n${served2}`); + // --- and the control plane is the way to ask it (novox/hq ADR 0095, issue 049) ------------------ + // No account in the mesh but the control plane's may create a reply queue and publish to a + // module's request key. Asked through it, the tool ANSWERS — with an error, since the lab has no + // Confluence and no token, which is an answer: the round trip is what is under test, and a + // timeout would read differently. + const asked = await on(`docker exec mesh-controller /mesh-controller ask confluence confluence_search '{"query":"mesh"}' --wait 60s`, 90_000); + assert.doesNotMatch(asked.out, /did not answer/, `the tool was never reached through the control plane:\n${asked.out}`); + assert.match(asked.out, /"(result|error)"/, `the control plane printed no answer:\n${asked.out}`); + // --- confluence got its own scoped broker account ----------------------------------------------- const users = await must(`docker exec mesh-broker lavinmqctl list_users 2>&1`); assert.match(users, /anchor-confluence/, `the scoped account anchor-confluence is not on the broker:\n${users}`); -- 2.54.0