From a174dfd404a95567028dd2ad560d434f2dd7b398 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 13 Sep 2026 02:39:11 +0200 Subject: [PATCH 1/7] The runtime every module stands on is built from its own repository MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It copied in a compiled directory that is not in source control and resolved the toolkit to a sibling checkout, so only a workstation with two repositories side by side could produce it — and its fingerprint was then typed into every module by hand. Nothing could rebuild it, so nothing could check it, and the rule that catches a base moving had no version on the far end of its edge. The recipe now also says what the image in service actually is. It claimed Alpine and has been serving Debian for as long as nobody could rebuild it. --- Dockerfile | 40 ++++++++++++++++++++++++++++++---------- module.json | 11 +++++++++++ package.json | 4 ++-- 3 files changed, 43 insertions(+), 12 deletions(-) create mode 100644 module.json diff --git a/Dockerfile b/Dockerfile index d867136..731649d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,35 @@ -# The tool runtime, as the container a node runs. It is handed the broker URL and the assigned -# modules' tool entrypoints at deploy time (MESH_BROKER_URL, MESH_TOOL_MODULES) and serves them. -FROM node:22-alpine AS build +# The tool runtime: the base every module written in this toolchain is compiled on top of, and the +# container a node runs to serve them. It is handed the broker credential and the assigned modules' +# entrypoints at deploy time and serves them. +# +# **Built from this repository alone.** It used to copy in a compiled output directory that is not +# in source control, and resolve the mesh's own toolkit to a sibling checkout on the same disk — so +# it could only be produced on a workstation with two repositories laid out side by side, and its +# fingerprint was then typed into every module's recipe by hand. That put the one artifact the whole +# toolchain stands on outside the toolchain: nothing could rebuild it, so nothing could check it, +# and the rule that catches a base moving had no version on the far end of its edge and could never +# fire (novox/hq issue 044). +# +# Debian rather than Alpine, and root rather than an unprivileged user, because that is what the +# image actually in service is — and modules have already been built against it, one of which +# installs a package with Debian's package manager. This recipe said Alpine while serving Debian for +# as long as nobody could rebuild it to notice. Changing the operating system under every module is +# a separate decision from making this buildable, and is not being taken here. +FROM node:22-bookworm-slim AS build WORKDIR /app -COPY package.json ./ -RUN npm install --omit=dev --no-audit --no-fund -COPY dist ./dist +COPY package.json package-lock.json ./ +# Development dependencies included: the compiler is one of them, and so is the toolkit's own — it +# builds itself on install, which is what lets it be named by a git URL rather than fetched from a +# package registry this mesh does not yet run. +RUN npm install --no-audit --no-fund +COPY tsconfig.json ./ +COPY src ./src +RUN npm run build -FROM node:22-alpine +# Everything the modules compile against and run on, and nothing that only the build needed. +FROM node:22-bookworm-slim WORKDIR /app -COPY --from=build /app/node_modules ./node_modules +COPY package.json package-lock.json ./ +RUN npm install --omit=dev --no-audit --no-fund && npm cache clean --force COPY --from=build /app/dist ./dist -COPY package.json ./ -USER node ENTRYPOINT ["node", "dist/main.js"] diff --git a/module.json b/module.json new file mode 100644 index 0000000..d6e0e5d --- /dev/null +++ b/module.json @@ -0,0 +1,11 @@ +{ + "module": "mesh-tools", + "version": "1", + "slug": "tools", + "build": { + "artifacts": [ + { "name": "runtime", "kind": "image", "from": "Dockerfile" } + ] + }, + "resources": [] +} diff --git a/package.json b/package.json index fb4377b..7ae2a5a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@novox/mesh-tools", "version": "0.1.0", - "description": "The Novox Mesh tool runtime — binds the mesh broker and serves the assigned modules' tools.", + "description": "The Novox Mesh tool runtime \u2014 binds the mesh broker and serves the assigned modules' tools.", "type": "module", "bin": { "mesh-tools": "./dist/main.js" @@ -11,7 +11,7 @@ "test": "node --test --experimental-strip-types 'test/*.test.ts'" }, "dependencies": { - "@novox/mesh-sdk": "^0.1.0", + "@novox/mesh-sdk": "git+https://git.novox.be/novox/mesh-sdk.git#a1ed33b", "amqplib": "^0.10.9" }, "devDependencies": { -- 2.54.0 From a5d65ada53ea9ee38cb585a7cfe94b04baf2c065 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 13 Sep 2026 02:39:54 +0200 Subject: [PATCH 2/7] The build stage can fetch a git dependency, and only it can --- Dockerfile | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 731649d..dcccc01 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,6 +16,12 @@ # as long as nobody could rebuild it to notice. Changing the operating system under every module is # a separate decision from making this buildable, and is not being taken here. FROM node:22-bookworm-slim AS build +# git, because a dependency named by a git URL is fetched by git and this image does not carry it. +# Only in the build stage: what it is needed for happens here, and a runtime that can clone is a +# runtime that can be made to clone. +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 of them, and so is the toolkit's own — it @@ -25,11 +31,13 @@ RUN npm install --no-audit --no-fund COPY tsconfig.json ./ COPY src ./src RUN npm run build +# Dropped after compiling rather than reinstalled in the next stage, which would need git there too. +RUN npm prune --omit=dev # Everything the modules compile against and run on, and nothing that only the build needed. FROM node:22-bookworm-slim WORKDIR /app -COPY package.json package-lock.json ./ -RUN npm install --omit=dev --no-audit --no-fund && npm cache clean --force +COPY package.json ./ +COPY --from=build /app/node_modules ./node_modules COPY --from=build /app/dist ./dist ENTRYPOINT ["node", "dist/main.js"] -- 2.54.0 From 78257cf15108bb56e32eef858ebb2931d991d6c2 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 13 Sep 2026 02:44:12 +0200 Subject: [PATCH 3/7] Compile the toolkit after installing it, because npm does not MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It declares the hook npm is supposed to run after a git install, and this npm does not run it — so the package arrives as sources with every entry point pointing at a compiled directory that is not there. --- Dockerfile | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Dockerfile b/Dockerfile index dcccc01..a883368 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,6 +28,16 @@ COPY package.json package-lock.json ./ # builds itself on install, which is what lets it be named by a git URL rather than fetched from a # package registry this mesh does not yet run. RUN npm install --no-audit --no-fund +# **And then compile the toolkit, because npm did not.** It declares a `prepare` script, which is +# the hook npm is supposed to run after installing a package from git — and this npm does not run +# it, so the package arrives as sources with every one of its entry points pointing at a compiled +# directory that is not there. The compile is therefore done here, explicitly: install the toolkit's +# own build dependencies inside it, build it, then drop them again so they do not travel into the +# image. Doing it by hand rather than relying on the hook is also the honest arrangement — a build +# that silently depended on a hook firing would break the day it stopped, in the same invisible way. +RUN npm --prefix node_modules/@novox/mesh-sdk install --no-audit --no-fund \ + && npm --prefix node_modules/@novox/mesh-sdk run build \ + && npm --prefix node_modules/@novox/mesh-sdk prune --omit=dev COPY tsconfig.json ./ COPY src ./src RUN npm run build -- 2.54.0 From 90a15cde204e6d2b06fe96fcdc018ec505cb6942 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 13 Sep 2026 02:45:16 +0200 Subject: [PATCH 4/7] The runtime keeps the compiler, because it is also the build environment Every module's recipe starts from this image and invokes the compiler out of it. Pruning build dependencies made a smaller image that nothing could be built on. --- Dockerfile | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index a883368..b4f9f75 100644 --- a/Dockerfile +++ b/Dockerfile @@ -41,10 +41,14 @@ RUN npm --prefix node_modules/@novox/mesh-sdk install --no-audit --no-fund \ COPY tsconfig.json ./ COPY src ./src RUN npm run build -# Dropped after compiling rather than reinstalled in the next stage, which would need git there too. -RUN npm prune --omit=dev -# Everything the modules compile against and run on, and nothing that only the build needed. +# Everything the modules compile against and run on. +# +# **The build dependencies stay, and that is deliberate.** This image is not only what a module runs +# in — it is also what every module is *compiled* in: a module's recipe starts from this and invokes +# the compiler out of these same directories. Dropping them would halve the image and break every +# module that builds on it, which is the sort of tidy-looking change that only fails somewhere else. +# If the two roles are ever separated, they should be separated deliberately and named separately. FROM node:22-bookworm-slim WORKDIR /app COPY package.json ./ -- 2.54.0 From 8318c78125a10cdfe388d1245db087d4be364c3a Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 13 Sep 2026 02:47:14 +0200 Subject: [PATCH 5/7] Move the base, to see whether the mesh notices what it reaches --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index b4f9f75..8b00185 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,3 +55,5 @@ COPY package.json ./ COPY --from=build /app/node_modules ./node_modules COPY --from=build /app/dist ./dist ENTRYPOINT ["node", "dist/main.js"] + +# Moved, to see whether everything standing on this is told. -- 2.54.0 From 8ad37c33cda3e84f6413acc894381c48a575a457 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 13 Sep 2026 02:50:15 +0200 Subject: [PATCH 6/7] Change the base for real, so the image moves with the commit --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8b00185..771c13a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,5 +55,5 @@ COPY package.json ./ COPY --from=build /app/node_modules ./node_modules COPY --from=build /app/dist ./dist ENTRYPOINT ["node", "dist/main.js"] - -# Moved, to see whether everything standing on this is told. +# A real change, so the image itself moves and everything standing on it should be told. +ENV MESH_RUNTIME=tools -- 2.54.0 From 77493642d8852c7893b5eee3a8bde179d93f54a5 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 13 Sep 2026 11:15:24 +0200 Subject: [PATCH 7/7] Remove what was only there to move a commit Two lines added to prove that changing this image makes everything standing on it go stale. The proof worked and the lines were never meant to stay: nothing reads MESH_RUNTIME. --- Dockerfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 771c13a..b4f9f75 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,5 +55,3 @@ COPY package.json ./ COPY --from=build /app/node_modules ./node_modules COPY --from=build /app/dist ./dist ENTRYPOINT ["node", "dist/main.js"] -# A real change, so the image itself moves and everything standing on it should be told. -ENV MESH_RUNTIME=tools -- 2.54.0