From b618057fb17d08d42f7a6ed89ceb4b1e1ba5fd3b Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 16 Sep 2026 10:27:26 +0200 Subject: [PATCH 1/3] Resolve the SDK by version from the registry, not from a git URL package.json names @novox/mesh-sdk by version and the install is a buildkit-secret-mounted resolve from the mesh's package registry, closing the git-URL half of issue 053. The lock is regenerated against the registry (a follow-up switches install to ci with a committed lock). Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx --- Dockerfile | 2 +- package-lock.json | 118 ---------------------------------------------- package.json | 2 +- 3 files changed, 2 insertions(+), 120 deletions(-) delete mode 100644 package-lock.json diff --git a/Dockerfile b/Dockerfile index c0861cd..e57f236 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ RUN apt-get update \ WORKDIR /app COPY package.json package-lock.json ./ # Development dependencies included: the compiler is one, and so is the toolkit's own. -RUN npm install --no-audit --no-fund +RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm install --no-audit --no-fund # The toolkit arrives compiled. It used to arrive as sources, and this compiled it by hand — the # hook that builds it on install was running all along, and the result was then packed out of the # package, because with no explicit file list npm falls back to .gitignore and that ignores the diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 2d08849..0000000 --- a/package-lock.json +++ /dev/null @@ -1,118 +0,0 @@ -{ - "name": "@novox/mesh-tools", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@novox/mesh-tools", - "version": "0.1.0", - "dependencies": { - "@novox/mesh-sdk": "file:../mesh-sdk", - "amqplib": "^0.10.9" - }, - "bin": { - "mesh-tools": "dist/main.js" - }, - "devDependencies": { - "@types/amqplib": "^0.10.8", - "@types/node": "^22.20.1", - "typescript": "^5.9.3" - } - }, - "../mesh-sdk": { - "name": "@novox/mesh-sdk", - "version": "0.1.0", - "devDependencies": { - "@types/node": "^22.0.0", - "typescript": "^5.6.0" - } - }, - "node_modules/@novox/mesh-sdk": { - "resolved": "../mesh-sdk", - "link": true - }, - "node_modules/@types/amqplib": { - "version": "0.10.8", - "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.10.8.tgz", - "integrity": "sha512-vtDp8Pk1wsE/AuQ8/Rgtm6KUZYqcnTgNvEHwzCkX8rL7AGsC6zqAfKAAJhUZXFhM/Pp++tbnUHiam/8vVpPztA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/node": { - "version": "22.20.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", - "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/amqplib": { - "version": "0.10.9", - "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.9.tgz", - "integrity": "sha512-jwSftI4QjS3mizvnSnOrPGYiUnm1vI2OP1iXeOUz5pb74Ua0nbf6nPyyTzuiCLEE3fMpaJORXh2K/TQ08H5xGA==", - "license": "MIT", - "dependencies": { - "buffer-more-ints": "~1.0.0", - "url-parse": "~1.5.10" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/buffer-more-ints": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", - "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==", - "license": "MIT" - }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "license": "MIT" - }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "license": "MIT" - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "license": "MIT", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - } - } -} diff --git a/package.json b/package.json index 720a4b4..3ed7292 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "test": "node --test --experimental-strip-types 'test/*.test.ts'" }, "dependencies": { - "@novox/mesh-sdk": "git+https://git.novox.be/novox/mesh-sdk.git#9213336", + "@novox/mesh-sdk": "^0.1.0", "amqplib": "^0.10.9" }, "devDependencies": { -- 2.54.0 From b00468d4c089407e5281edcb30ace11db492b187 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 16 Sep 2026 11:48:16 +0200 Subject: [PATCH 2/3] Resolve the SDK in a throwaway deps stage, not with a buildkit secret MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A machine's docker may carry no buildx, so --mount=type=secret cannot be relied on. Instead a deps stage copies in the builder-written .npmrc, resolves node_modules from the mesh's registry, and the toolchain stage copies those node_modules out without the credential — so it is in no published layer. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx --- Dockerfile | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/Dockerfile b/Dockerfile index e57f236..143613b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,28 +1,36 @@ -# Two images from one recipe: the one modules are COMPILED in, and the one they RUN in. +# Three stages, two published images: the one modules are COMPILED in, and the one they RUN in. # # **They were the same image, and that was a mistake.** A module's recipe starts from this and # invokes the compiler out of it, so the compiler had to be here — and because the same image was # also what every module ran in, every running container on every machine carried a TypeScript -# compiler it would never invoke. 23 of the 28 MB of libraries were that compiler. It was defended -# in a comment, which made a workaround look like a decision: the earlier attempt to prune the build -# tools produced a smaller image that nothing could be built on, and the answer to that is two -# images rather than one image that is bad at both jobs. +# compiler it would never invoke. The answer is separate stages rather than one image bad at both +# jobs. `build.artifacts` in module.json names the published stage each — `toolchain` and `runtime`. # -# Kept in one recipe deliberately. They must agree about the operating system, the language version -# and the library, and two files drift. `build.artifacts` in module.json names a stage each. +# The `deps` stage is neither published nor named there: it is where the mesh's package registry is +# reached, so it is where — and only where — the credential to reach it exists. The toolchain copies +# resolved node_modules out of it, so the credential is in no image any machine ever holds +# (novox/hq ADR 0076). This is the buildkit-secret's job done without buildkit, because a machine's +# docker may carry no buildx. -# ---- toolchain: what a module is compiled in ------------------------------------------------- -FROM node:22-bookworm-slim AS toolchain -# git, because a dependency named by a git URL is fetched by git and this image does not carry it. -# Only here: what it is needed for happens at build time, and an image that can clone is an image -# that can be made to clone. +# ---- deps: node_modules resolved from the mesh's registry, credential and all ---------------- +FROM node:22-bookworm-slim AS deps RUN apt-get update \ && apt-get install -y --no-install-recommends git ca-certificates \ && rm -rf /var/lib/apt/lists/* WORKDIR /app -COPY package.json package-lock.json ./ -# Development dependencies included: the compiler is one, and so is the toolkit's own. -RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm install --no-audit --no-fund +COPY package.json ./ +# The builder writes .npmrc into the build context; it authenticates to the mesh's package registry +# for the @novox scope, which is where @novox/mesh-sdk resolves. This stage is not published, so the +# credential travels no further than here. Development dependencies included: the compiler is one. +COPY .npmrc ./.npmrc +RUN npm install --no-audit --no-fund + +# ---- toolchain: what a module is compiled in, WITHOUT the credential -------------------------- +FROM node:22-bookworm-slim AS toolchain +WORKDIR /app +COPY package.json ./ +# The resolved libraries, but not the .npmrc that resolved them. +COPY --from=deps /app/node_modules ./node_modules # The toolkit arrives compiled. It used to arrive as sources, and this compiled it by hand — the # hook that builds it on install was running all along, and the result was then packed out of the # package, because with no explicit file list npm falls back to .gitignore and that ignores the @@ -32,9 +40,7 @@ COPY src ./src RUN npm run build # ---- what the running image needs, and nothing else ------------------------------------------- -# Its own stage so the toolchain image keeps its build tools while the runtime image does not. The -# prune has to happen somewhere, and doing it in the toolchain stage would take the compiler out of -# the image whose whole purpose is to have one. +# Its own stage so the toolchain image keeps its build tools while the runtime image does not. FROM toolchain AS lean RUN npm prune --omit=dev -- 2.54.0 From 813f2e0db3eecfed4c6a2033498b08378da8e76f Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 16 Sep 2026 18:40:40 +0200 Subject: [PATCH 3/3] Rename mesh-control -> mesh-controller, substrate -> foundation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One name per thing, per the HQ glossary: the module/container/image/binary/repo becomes mesh-controller, the seat the-controller, and the store+broker pair the foundation (embedded base bundles, default template and example lock renamed with their go:embed directives). No behaviour change — a pure vocabulary rename. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx --- src/broker-amqp.ts | 6 +++--- src/main.ts | 2 +- test/amqp.test.ts | 2 +- test/events-wire.test.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/broker-amqp.ts b/src/broker-amqp.ts index 7b50768..117d5c4 100644 --- a/src/broker-amqp.ts +++ b/src/broker-amqp.ts @@ -38,7 +38,7 @@ export interface Credential { * Connect to the mesh broker and return a Broker. `close()` tears both channel and connection down. * * A scoped module (novox/hq ADR 0043) passes `assumeExchanges: true`: its account may not declare - * an exchange, and the substrate already owns them, so it declares only its own queue. A credential + * an exchange, and the foundation already owns them, so it declares only its own queue. A credential * carrying a fingerprint is dialled over amqps, pinned to exactly that certificate. */ export async function connectAmqp( @@ -55,9 +55,9 @@ export async function connectAmqp( // at-least-once starts at the emitter, not only the consumer (ADR 0042). const ch = await conn.createConfirmChannel(); - // The substrate owns the exchanges (ADR 0043). A bootstrap/admin connection declares them; a + // The foundation owns the exchanges (ADR 0043). A bootstrap/admin connection declares them; a // scoped module assumes they exist and never tries — its account could not, and the dead-letter - // queue behind the exchange is the substrate's to keep, not a module's. + // queue behind the exchange is the foundation's to keep, not a module's. if (!opts.assumeExchanges) { await ch.assertExchange(RPC_EXCHANGE, "topic", { durable: true }); await ch.assertExchange(EVENTS_EXCHANGE, "topic", { durable: true }); diff --git a/src/main.ts b/src/main.ts index d0d46d1..c32805f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -32,7 +32,7 @@ import { emit } from "@novox/mesh-sdk/events"; /** * Connect the way this process is meant to: with its sealed credential if the mesh gave it one, and - * over the plain bootstrap URL otherwise. A scoped module assumes the substrate's exchanges exist — + * over the plain bootstrap URL otherwise. A scoped module assumes the foundation's exchanges exist — * its account may not declare them (ADR 0043). */ async function connectBroker(): Promise { diff --git a/test/amqp.test.ts b/test/amqp.test.ts index 3d1ece8..b20be06 100644 --- a/test/amqp.test.ts +++ b/test/amqp.test.ts @@ -27,7 +27,7 @@ test("a module's tool serves and is invoked over a real AMQP broker", { skip: !u const serverBroker = await connectAmqp(url!); const stop = await runTools({ broker: serverBroker, moduleEntrypoints: [] }); - // A separate connection — a caller, like mesh-control's command API — invokes over the broker, + // A separate connection — a caller, like mesh-controller's command API — invokes over the broker, // by module and tool (novox/hq ADR 0047: served on serve.demo.greet, invoked as demo.greet). const caller = await connectAmqp(url!); const result = (await invokeTool(caller, "demo", "greet", { who: "mesh" })) as { hello: string }; diff --git a/test/events-wire.test.ts b/test/events-wire.test.ts index 2188aa7..6cceeb4 100644 --- a/test/events-wire.test.ts +++ b/test/events-wire.test.ts @@ -10,7 +10,7 @@ import { emit, on, type Event } from "@novox/mesh-sdk/events"; // headers on the wire, a body that is only the payload, persistent messages, a durable per-consumer // queue, dead-letter, poison handling? This tests the adapter in isolation, against a disposable // broker that stands in for the one the mesh hosts (ADR 0001). It is NOT an event test of the mesh: -// that lives in a full lab scenario where the mesh raises the broker as substrate. Requires +// that lives in a full lab scenario where the mesh raises the broker as foundation. Requires // $MESH_BROKER_URL (a throwaway broker); skipped, never failed, when it is not set. const url = process.env.MESH_BROKER_URL; -- 2.54.0