Merge pull request 'Waves 2-3: nine modules' code moves into bundles the node's runtime serves (hq ADR 0198, to-be 38 WP4c)' (#248) from feat/0198-waves-2-3-module-code-moves into main

This commit was merged in pull request #248.
This commit is contained in:
2026-10-03 23:01:18 +00:00
20 changed files with 256 additions and 632 deletions
-22
View File
@@ -1,22 +0,0 @@
# anthropic-consumer's runtime: the tool runtime, carrying this module's compiled code.
#
# **Built from this module's own directory and nothing else.** The sdk and the tool runtime are in
# the base images, published like any other artifact — which is what makes this buildable by the
# mesh from a repository and a path (novox/hq ADR 0069) rather than only on a workstation that
# happens to have the siblings.
#
# Two bases, named rather than pinned (novox/hq issue 044): the image this is COMPILED in and the
# image it RUNS in — the second must not carry a compiler. Declared in module.json's `build.on`.
ARG BUILD_BASE
ARG RUNTIME_BASE
FROM ${BUILD_BASE} AS build
WORKDIR /app/modules/anthropic-consumer
COPY . .
RUN node /app/node_modules/typescript/bin/tsc apply/index.ts usage/index.ts \
--module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist
FROM ${RUNTIME_BASE}
COPY --from=build /app/modules/anthropic-consumer/dist /app/modules/anthropic-consumer/dist
# No serve-time entrypoints: every container of this module names its command (`run` on a
# schedule), so nothing here serves — deliberately no MESH_TOOL_MODULES.
+26 -62
View File
@@ -14,19 +14,10 @@
"secrets": { "secrets": {
"model-access": "${dir:state}/access-token" "model-access": "${dir:state}/access-token"
}, },
"own-secrets": {
"broker": "${dir:mesh-state}/broker"
},
"emits": [ "emits": [
"usage.session" "usage.session"
], ],
"resources": [ "resources": [
{
"id": "mesh-state",
"type": "directory",
"mode": "0700",
"place": "mesh"
},
{ {
"id": "state", "id": "state",
"type": "directory", "type": "directory",
@@ -46,66 +37,39 @@
}, },
{ {
"id": "apply", "id": "apply",
"type": "container", "type": "process",
"name": "mesh-anthropic-consumer-apply", "name": "anthropic-consumer-apply",
"network": "host", "artifact": "code",
"run": [
"node",
"apply/index.js"
],
"schedule": "*/5 * * * *", "schedule": "*/5 * * * *",
"args": [
"run",
"/app/modules/anthropic-consumer/dist/apply/index.js"
],
"volumes": [
"${dir:state}:/run/state"
],
"env": { "env": {
"MESH_MODEL_ACCESS_SECRET_FILE": "/run/state/access-token", "MESH_MODEL_ACCESS_SECRET_FILE": "${dir:state}/access-token",
"MESH_MODEL_ACCESS_BIND_FILE": "/run/state/model.json", "MESH_MODEL_ACCESS_BIND_FILE": "${dir:state}/model.json",
"MESH_CLAUDE_CREDENTIALS_FILE": "/run/state/claude/.credentials.json", "MESH_CLAUDE_CREDENTIALS_FILE": "${dir:state}/claude/.credentials.json",
"MESH_CLAUDE_IDENTITY_FILE": "/run/state/claude/.claude.json" "MESH_CLAUDE_IDENTITY_FILE": "${dir:state}/claude/.claude.json"
}, }
"artifact": "runtime"
},
{
"id": "usage",
"type": "container",
"name": "mesh-anthropic-consumer-usage",
"network": "host",
"schedule": "*/5 * * * *",
"args": [
"run",
"/app/modules/anthropic-consumer/dist/usage/index.js"
],
"volumes": [
"${dir:mesh-state}/broker:/run/secrets/broker:ro",
"${dir:state}:/run/state"
],
"env": {
"MESH_BROKER_FILE": "/run/secrets/broker",
"MESH_CLAUDE_PROJECTS_DIR": "/run/state/claude/projects",
"MESH_ANTHROPIC_USAGE_OUT": "/run/state/out/session-usage.json",
"MESH_TOOLS_MAIN": "/app/dist/main.js"
},
"artifact": "runtime"
} }
], ],
"build": { "build": {
"on": [
{
"arg": "BUILD_BASE",
"module": "mesh-tools",
"artifact": "build"
},
{
"arg": "RUNTIME_BASE",
"module": "mesh-tools",
"artifact": "runtime"
}
],
"artifacts": [ "artifacts": [
{ {
"name": "runtime", "name": "code",
"kind": "image", "kind": "bundle",
"from": "Dockerfile" "language": "typescript",
"entrypoints": [
"apply/index.js",
"usage/index.js"
],
"loads": [
"usage/index.js"
],
"env": {
"MESH_CLAUDE_PROJECTS_DIR": "${dir:state}/claude/projects",
"MESH_ANTHROPIC_USAGE_OUT": "${dir:state}/out/session-usage.json"
}
} }
] ]
} }
+18 -17
View File
@@ -3,12 +3,15 @@
// per session. The consumer IS the (node,module) session's fixed binding, so no per-message account // per session. The consumer IS the (node,module) session's fixed binding, so no per-message account
// attribution is done — just the totals (port map "don't-map" #3). // attribution is done — just the totals (port map "don't-map" #3).
// //
// Runs as `mesh-tools run` (no broker), so events are emitted best-effort via the sibling mesh-tools // Runs in the node's runtime (novox/hq ADR 0198), every five minutes, so events are emitted through
// `emit` primitive; the totals are also written to a file so the reading is observable without one. // the runtime as this module; the totals are also written to a file so the reading is observable
// without one.
import { readdirSync, statSync, readFileSync, writeFileSync, renameSync, mkdirSync } from "node:fs"; import { readdirSync, statSync, readFileSync, writeFileSync, renameSync, mkdirSync } from "node:fs";
import { join, dirname } from "node:path"; import { join, dirname } from "node:path";
import { emit } from "@novox/mesh-sdk/events";
import { readSessionFile, type SessionUsage } from "../transcript.js"; import { readSessionFile, type SessionUsage } from "../transcript.js";
/** The vendor-neutral usage row ADR 0054 fixes — the shape the model-usage store upserts. Kept local /** The vendor-neutral usage row ADR 0054 fixes — the shape the model-usage store upserts. Kept local
@@ -116,22 +119,20 @@ function atomicWrite(path: string, content: string): void {
renameSync(tmp, path); renameSync(tmp, path);
} }
/** Emit best-effort via the sibling mesh-tools `emit`, which wires a broker a run step has none. */ /** Emit best-effort through the runtime: a reading that could not be announced is still in the file. */
async function emitUsage(body: Record<string, unknown>): Promise<void> { async function emitUsage(body: Record<string, unknown>): Promise<void> {
const main = process.env.MESH_TOOLS_MAIN ?? "/app/dist/main.js"; try {
const { spawn } = await import("node:child_process"); await emit("usage.session", body);
await new Promise<void>((resolve) => { } catch (err) {
const child = spawn(
process.execPath,
[main, "emit", "usage.session", JSON.stringify(body)],
{ stdio: "inherit" },
);
child.on("exit", () => resolve());
child.on("error", (err) => {
console.error(`[anthropic-consumer] could not emit usage: ${err}`); console.error(`[anthropic-consumer] could not emit usage: ${err}`);
resolve(); }
});
});
} }
await main(); // The cadence the scheduled container had: once at start, then every five minutes. Not awaited, so the
// runtime's handshake is answered while a long first reading is still under way.
const EVERY_MS = 5 * 60 * 1000;
const tick = (): void => {
void main().catch((err) => console.error(`[anthropic-consumer] usage reading failed: ${err}`));
};
tick();
setInterval(tick, EVERY_MS);
-33
View File
@@ -1,33 +0,0 @@
# audit-logger's runtime: the shared runtime image, carrying this module's compiled code.
#
# **Built from this module's own directory and nothing else.** The toolkit is in the base image, so
# nothing is copied out of a neighbouring checkout — which is what lets the mesh build this from a
# repository and a path (novox/hq ADR 0069) rather than only on a workstation that happens to have
# the siblings laid out beside it.
# Two bases, named rather than pinned: the image this is COMPILED in, and the image it RUNS in.
# They are different images on purpose — the first carries a compiler and the second must not, or
# every running container would carry one it never invokes. The mesh answers both with the copies it
# holds, because a fingerprint written here would name one particular copy and no other mesh has it
# (novox/hq issue 044). Declared in module.json's `build.on`; deliberately no defaults, so a build
# nobody told stops here and says which module to build first.
ARG BUILD_BASE
ARG RUNTIME_BASE
FROM ${BUILD_BASE} AS build
# Compiled under /app/modules so `@novox/mesh-sdk` resolves upward into the base's own
# node_modules — the module is compiled against exactly the toolkit it will run against.
WORKDIR /app/modules/audit-logger
COPY . .
# The compiler is invoked by its real path rather than through node_modules/.bin, whose entries are
# symlinks to a launcher that requires its library relatively — resolved away when the base image
# was assembled.
RUN node /app/node_modules/typescript/bin/tsc audit.ts index.ts \
--module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist
FROM ${RUNTIME_BASE}
COPY --from=build /app/modules/audit-logger/dist /app/modules/audit-logger/dist
# **Served, not run.** This subscribes on import, and the serve mode binds the broker before it
# imports anything — `run` exists for a step that works offline and exits, and would leave this
# with nothing to subscribe to.
ENV MESH_TOOL_MODULES=/app/modules/audit-logger/dist/index.js
+12 -33
View File
@@ -5,27 +5,21 @@
"consumes": [ "consumes": [
"**" "**"
], ],
"own-secrets": {
"broker": "${dir:state}/broker"
},
"build": { "build": {
"on": [
{
"arg": "BUILD_BASE",
"module": "mesh-tools",
"artifact": "build"
},
{
"arg": "RUNTIME_BASE",
"module": "mesh-tools",
"artifact": "runtime"
}
],
"artifacts": [ "artifacts": [
{ {
"name": "runtime", "name": "code",
"kind": "image", "kind": "bundle",
"from": "Dockerfile" "language": "typescript",
"entrypoints": [
"index.js"
],
"loads": [
"index.js"
],
"env": {
"AUDIT_LOG": "${dir:trail}/audit.log"
}
} }
] ]
}, },
@@ -40,21 +34,6 @@
"id": "trail", "id": "trail",
"type": "directory", "type": "directory",
"mode": "0700" "mode": "0700"
},
{
"id": "run",
"type": "container",
"name": "mesh-audit-logger",
"network": "host",
"volumes": [
"${dir:state}/broker:/run/secrets/broker:ro",
"${dir:trail}:/trail"
],
"env": {
"MESH_BROKER_FILE": "/run/secrets/broker",
"AUDIT_LOG": "/trail/audit.log"
},
"artifact": "runtime"
} }
], ],
"capabilities": [ "capabilities": [
-37
View File
@@ -1,37 +0,0 @@
# gitea's runtime: the tool runtime, carrying this module's compiled provisioner, tools and event
# consumer.
#
# **Built from this module's own directory and nothing else.** The sdk is in the base image, so
# nothing is copied out of a neighbouring checkout — which is what lets the mesh build this from a
# repository and a path (novox/hq ADR 0069) rather than only on a workstation that happens to have
# the siblings.
#
# Two bases, named rather than pinned: the image this is COMPILED in, and the image it RUNS in.
# They are different images on purpose — the first carries a compiler and the second must not, or
# every running container would carry one it never invokes. The mesh answers both with the copies it
# holds, because a fingerprint written here would name one particular copy and no other mesh has it
# (novox/hq issue 044). Declared in module.json's `build.on`; deliberately no defaults, so a build
# nobody told stops here and says which module to build first.
ARG BUILD_BASE
ARG RUNTIME_BASE
FROM ${BUILD_BASE} AS build
# Compiled under /app/modules so `@novox/mesh-sdk` resolves upward into the base's own
# node_modules — the module is compiled against exactly the sdk it will run against.
WORKDIR /app/modules/gitea
COPY . .
# The compiler is invoked by its real path rather than through node_modules/.bin, whose entries are
# symlinks to a launcher that requires its library relatively — resolved away when the base image
# was assembled.
RUN node /app/node_modules/typescript/bin/tsc client.ts token.ts index.ts provisioner/index.ts tools/index.ts \
--module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist
FROM ${RUNTIME_BASE}
# **No apt packages.** gitea's provisioner talks to the forge over HTTP (the gitea REST API), not
# through a CLI the way postgres drives psql — so the runtime base holds everything this needs.
COPY --from=build /app/modules/gitea/dist /app/modules/gitea/dist
# What a tool host should load from this module: its event consumer and its tools, which are
# separate entrypoints because they are loaded by different things. The provisioner is the third,
# and is not listed here — the declaration names it in the container's `args`, because it is what
# this module's own container runs. One image, because they are one module and share a client.
ENV MESH_TOOL_MODULES=/app/modules/gitea/dist/index.js,/app/modules/gitea/dist/tools/index.js,/app/modules/gitea/dist/provisioner/index.js
+21 -44
View File
@@ -85,9 +85,6 @@
"scope": "mesh" "scope": "mesh"
} }
], ],
"own-secrets": {
"broker": "${dir:mesh-state}/broker"
},
"resources": [ "resources": [
{ {
"id": "mesh-state", "id": "mesh-state",
@@ -180,32 +177,6 @@
"mode": "0600", "mode": "0600",
"content": "{}\n", "content": "{}\n",
"merge": "json" "merge": "json"
},
{
"id": "runtime",
"type": "container",
"name": "mesh-gitea",
"network": "host",
"volumes": [
"${dir:mesh-state}/broker:/run/secrets/broker:ro",
"${dir:mesh-state}/config.json:/run/config/config.json:ro",
"${dir:grants}:${dir:grants}:ro",
"${dir:state}/admin.secret:/run/secrets/admin:ro",
"${dir:runtime-state}:/run/state"
],
"env": {
"MESH_BROKER_FILE": "/run/secrets/broker",
"MESH_GITEA_URL": "http://127.0.0.1:${port:3000}",
"MESH_GITEA_CONFIG_FILE": "/run/config/config.json",
"MESH_GITEA_ADMIN_USER": "mesh-admin",
"MESH_GITEA_ADMIN_PASSWORD_FILE": "/run/secrets/admin",
"MESH_GITEA_STATE_DIR": "/run/state",
"MESH_RECEIVES": "${dir:grants}/npm.json"
},
"artifact": "runtime",
"restart-on": [
"runtime-config"
]
} }
], ],
"provides": [ "provides": [
@@ -219,23 +190,29 @@
} }
], ],
"build": { "build": {
"on": [
{
"arg": "BUILD_BASE",
"module": "mesh-tools",
"artifact": "build"
},
{
"arg": "RUNTIME_BASE",
"module": "mesh-tools",
"artifact": "runtime"
}
],
"artifacts": [ "artifacts": [
{ {
"name": "runtime", "name": "code",
"kind": "image", "kind": "bundle",
"from": "Dockerfile" "language": "typescript",
"entrypoints": [
"index.js",
"tools/index.js",
"provisioner/index.js"
],
"loads": [
"index.js",
"tools/index.js",
"provisioner/index.js"
],
"env": {
"MESH_GITEA_URL": "http://127.0.0.1:${port:3000}",
"MESH_GITEA_CONFIG_FILE": "${dir:mesh-state}/config.json",
"MESH_GITEA_ADMIN_USER": "mesh-admin",
"MESH_GITEA_ADMIN_PASSWORD_FILE": "${dir:state}/admin.secret",
"MESH_GITEA_STATE_DIR": "${dir:runtime-state}",
"MESH_RECEIVES": "${dir:grants}/npm.json"
}
} }
] ]
}, },
-31
View File
@@ -1,31 +0,0 @@
# lab's runtime: the tool runtime, carrying this module's code, and the toolchain the lab's suite
# builds the mesh with (novox/hq ADR 0172). It reaches the machine's virtualisation and container
# runtime through their sockets, so what it raises is what a hand run on this machine raises.
#
# Every download is pinned by its checksum: an image that builds the mesh is the last place to take
# whatever an upstream serves today.
ARG BUILD_BASE
ARG RUNTIME_BASE
FROM ${BUILD_BASE} AS build
WORKDIR /app/modules/lab
COPY . .
RUN node /app/node_modules/typescript/bin/tsc tools/index.ts tools/runs.ts --rootDir . \
--module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist
FROM ${RUNTIME_BASE}
RUN apt-get update \
&& apt-get install -y --no-install-recommends git make ca-certificates curl python3 file iproute2 sudo \
&& rm -rf /var/lib/apt/lists/*
RUN curl -fsSL -o /tmp/go.tgz https://go.dev/dl/go1.26.8.linux-amd64.tar.gz \
&& echo "d0f743b33e8d8945e6b1f432edd15785c70507121d6e2a723b21285eddf8b57b /tmp/go.tgz" | sha256sum -c - \
&& tar -C /usr/local -xzf /tmp/go.tgz && rm /tmp/go.tgz
RUN curl -fsSL -o /usr/local/bin/incus https://github.com/lxc/incus/releases/download/v7.5.1/bin.linux.incus.x86_64 \
&& echo "7bd6223b369f4d693fcde695bd8549a73b5b3d403735329212483702aa22c179 /usr/local/bin/incus" | sha256sum -c - \
&& chmod 0755 /usr/local/bin/incus
RUN curl -fsSL -o /tmp/docker.tgz https://download.docker.com/linux/static/stable/x86_64/docker-28.5.2.tgz \
&& echo "ea90cfd12e1eeb12aa1c971741adb8bd4ed88e2a574eaac13f5029a1dbc6300d /tmp/docker.tgz" | sha256sum -c - \
&& tar -C /tmp -xzf /tmp/docker.tgz docker/docker && mv /tmp/docker/docker /usr/local/bin/docker && rm -rf /tmp/docker /tmp/docker.tgz
ENV PATH=/usr/local/go/bin:$PATH
COPY --from=build /app/modules/lab/dist /app/modules/lab/dist
ENV MESH_TOOL_MODULES=/app/modules/lab/dist/tools/index.js
+55 -44
View File
@@ -5,16 +5,7 @@
"container-runtime", "container-runtime",
"virtualisation" "virtualisation"
], ],
"own-secrets": {
"broker": "${dir:mesh-state}/broker"
},
"resources": [ "resources": [
{
"id": "mesh-state",
"type": "directory",
"mode": "0700",
"place": "mesh"
},
{ {
"id": "state", "id": "state",
"type": "directory", "type": "directory",
@@ -35,47 +26,67 @@
"content": "MESH_LAB_FORGE=${setting:forge}\n" "content": "MESH_LAB_FORGE=${setting:forge}\n"
}, },
{ {
"id": "runtime", "id": "git",
"type": "container", "type": "package",
"name": "mesh-lab", "package": "git"
"network": "host",
"env-file": [
"${dir:state}/lab.env"
],
"volumes": [
"${dir:mesh-state}/broker:/run/secrets/broker:ro",
"${dir:work}:${dir:work}",
"/var/run/docker.sock:/var/run/docker.sock",
"/var/lib/incus/unix.socket:/var/lib/incus/unix.socket"
],
"env": {
"MESH_BROKER_FILE": "/run/secrets/broker",
"MESH_LAB_WORK": "${dir:work}"
}, },
"restart-on": [ {
"runtime-env" "id": "make",
], "type": "package",
"artifact": "runtime" "package": "make"
},
{
"id": "python",
"type": "package",
"package": "python"
},
{
"id": "file",
"type": "package",
"package": "file"
},
{
"id": "iproute2",
"type": "package",
"package": "iproute2"
},
{
"id": "sudo",
"type": "package",
"package": "sudo"
},
{
"id": "npm",
"type": "package",
"package": "npm"
},
{
"id": "go",
"type": "package",
"package": "go"
},
{
"id": "incus",
"type": "package",
"package": "incus"
} }
], ],
"build": { "build": {
"on": [
{
"arg": "BUILD_BASE",
"module": "mesh-tools",
"artifact": "build"
},
{
"arg": "RUNTIME_BASE",
"module": "mesh-tools",
"artifact": "runtime"
}
],
"artifacts": [ "artifacts": [
{ {
"name": "runtime", "name": "code",
"kind": "image", "kind": "bundle",
"from": "Dockerfile" "language": "typescript",
"entrypoints": [
"tools/index.js"
],
"loads": [
"tools/index.js"
],
"env": {
"MESH_LAB_WORK": "${dir:work}",
"MESH_LAB_ENV_FILE": "${dir:state}/lab.env"
}
} }
] ]
} }
+23 -1
View File
@@ -2,18 +2,23 @@
// lab is assigned to, and only there: a bed raises virtual machines on that machine's virtualisation. // lab is assigned to, and only there: a bed raises virtual machines on that machine's virtualisation.
import { spawnSync } from "node:child_process"; import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools"; import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools";
import { listRuns, readStatus, REPOSITORIES, running, start, stop, tail } from "./runs.js"; import { listRuns, readStatus, REPOSITORIES, running, start, stop, tail } from "./runs.js";
export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] { export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] {
const work = env.MESH_LAB_WORK ?? "/var/lib/mesh-lab-runs"; const work = env.MESH_LAB_WORK ?? "/var/lib/mesh-lab-runs";
const forge = (env.MESH_LAB_FORGE ?? "").replace(/\/+$/, ""); // The forge is an operator's setting, which reaches a file and never a bundle's words (novox/hq
// ADR 0192): read from the env-file the mesh fills, at each call, so a changed setting is used
// without restarting the runtime. MESH_LAB_FORGE itself still wins, for a hand-run instance.
const forgeOf = (): string => (env.MESH_LAB_FORGE ?? wordIn(env.MESH_LAB_ENV_FILE, "MESH_LAB_FORGE")).replace(/\/+$/, "");
return [ return [
{ {
name: "lab_check", name: "lab_check",
description: "Whether this machine can run the lab's beds: the lab's own check, against the forge's main branch.", description: "Whether this machine can run the lab's beds: the lab's own check, against the forge's main branch.",
input: {}, input: {},
run: async () => { run: async () => {
const forge = forgeOf();
if (!forge) return { ok: false, output: "the lab's forge is not set: settings for lab, {\"forge\": \"<url>\"}" }; if (!forge) return { ok: false, output: "the lab's forge is not set: settings for lab, {\"forge\": \"<url>\"}" };
const dir = `${work}/check`; const dir = `${work}/check`;
spawnSync("rm", ["-rf", dir]); spawnSync("rm", ["-rf", dir]);
@@ -38,6 +43,7 @@ export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] {
}, },
}, },
run: async (args) => { run: async (args) => {
const forge = forgeOf();
if (!forge) return { started: false, reason: "the lab's forge is not set: settings for lab, {\"forge\": \"<url>\"}" }; if (!forge) return { started: false, reason: "the lab's forge is not set: settings for lab, {\"forge\": \"<url>\"}" };
const tests = String(args.tests ?? "").split(",").map((s) => s.trim()).filter(Boolean); const tests = String(args.tests ?? "").split(",").map((s) => s.trim()).filter(Boolean);
if (tests.length === 0) return { started: false, reason: "name at least one bed test file" }; if (tests.length === 0) return { started: false, reason: "name at least one bed test file" };
@@ -83,4 +89,20 @@ export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] {
]; ];
} }
/** One word from an env-file (`KEY=value` lines), or "" when the file or the word is absent. */
export function wordIn(file: string | undefined, word: string): string {
if (!file) return "";
let text: string;
try {
text = readFileSync(file, "utf8");
} catch {
return "";
}
for (const line of text.split("\n")) {
const at = line.indexOf("=");
if (at > 0 && line.slice(0, at).trim() === word) return line.slice(at + 1).trim();
}
return "";
}
registerModuleTools("lab", (env) => getLabTools(env)); registerModuleTools("lab", (env) => getLabTools(env));
-30
View File
@@ -1,30 +0,0 @@
# mailu's runtime: the tool runtime, carrying this module's compiled code.
#
# **Built from this module's own directory and nothing else.** The sdk and the tool runtime are in
# the base images, published like any other artifact — which is what makes this buildable by the
# mesh from a repository and a path (novox/hq ADR 0069) rather than only on a workstation that
# happens to have the siblings.
#
# Two bases, named rather than pinned (novox/hq issue 044): the image this is COMPILED in and the
# image it RUNS in — the second must not carry a compiler. Declared in module.json's `build.on`.
ARG BUILD_BASE
ARG RUNTIME_BASE
FROM ${BUILD_BASE} AS build
# Compiled under /app/modules so `@novox/mesh-sdk` resolves upward into the base's own
# node_modules — the module is compiled against exactly the sdk it will run against. The compiler
# is invoked by its real path: node_modules/.bin entries are launcher symlinks the base image
# resolved away.
WORKDIR /app/modules/mailu
COPY . .
RUN node /app/node_modules/typescript/bin/tsc client.ts index.ts tools/index.ts provisioner/index.ts \
--module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist
FROM ${RUNTIME_BASE}
COPY --from=build /app/modules/mailu/dist /app/modules/mailu/dist
# Every serve-time entrypoint, loaded by the runtime in serve mode: tools and events serve, and a
# provider's provisioner runs its reconcile loop in the same process, with the broker connected —
# the convention novox/hq issues 060/061 settled. A container that instead ran only its
# provisioner (`run`) served no tools and emitted no events; a container that named no command
# ran no provisioner at all.
ENV MESH_TOOL_MODULES=/app/modules/mailu/dist/index.js,/app/modules/mailu/dist/tools/index.js,/app/modules/mailu/dist/provisioner/index.js
+30 -41
View File
@@ -135,11 +135,15 @@
"protocol": "tcp", "protocol": "tcp",
"from": "mesh", "from": "mesh",
"why": "automx: mail client autoconfiguration; the autoconfig, autodiscover and automx names are route grants reaching it here" "why": "automx: mail client autoconfiguration; the autoconfig, autodiscover and automx names are route grants reaching it here"
},
{
"name": "admin-api",
"port": 8080,
"protocol": "tcp",
"from": "machine",
"why": "the admin API, which this module's own code reaches on loopback from the node's runtime now that it runs outside the mailu network"
} }
], ],
"own-secrets": {
"broker": "${dir:mesh-state}/broker"
},
"resources": [ "resources": [
{ {
"id": "mesh-state", "id": "mesh-state",
@@ -305,6 +309,9 @@
"name": "mailu-admin", "name": "mailu-admin",
"image": "ghcr.io/mailu/admin@sha256:6dbfdadc4a9590dcb7652357b505200115b689b74008653bbf369e4599a3be5a", "image": "ghcr.io/mailu/admin@sha256:6dbfdadc4a9590dcb7652357b505200115b689b74008653bbf369e4599a3be5a",
"network": "mailu", "network": "mailu",
"ports": [
"8080"
],
"env-file": [ "env-file": [
"${dir:state}/mailu.env", "${dir:state}/mailu.env",
"${dir:state}/secret.env", "${dir:state}/secret.env",
@@ -473,31 +480,6 @@
"content": "{}\n", "content": "{}\n",
"merge": "json" "merge": "json"
}, },
{
"id": "runtime",
"type": "container",
"name": "mesh-mailu",
"network": "mailu",
"volumes": [
"${dir:mesh-state}/broker:/run/secrets/broker:ro",
"${dir:state}/api-token.secret:/run/secrets/api-token:ro",
"${dir:grants}:${dir:grants}:ro",
"${dir:mesh-state}/config.json:/run/config/config.json:ro",
"/var/run/docker.sock:/var/run/docker.sock"
],
"env": {
"MESH_BROKER_FILE": "/run/secrets/broker",
"MESH_MAILU_URL": "http://mailu-admin:8080/api/v1",
"MESH_MAILU_API_KEY_FILE": "/run/secrets/api-token",
"MESH_MAILU_IMAP_CONTAINER": "mailu-imap",
"MESH_MAILU_CONFIG_FILE": "/run/config/config.json",
"MESH_RECEIVES": "${dir:grants}/mesh.json"
},
"restart-on": [
"runtime-config"
],
"artifact": "runtime"
},
{ {
"id": "automx", "id": "automx",
"type": "container", "type": "container",
@@ -517,16 +499,6 @@
], ],
"build": { "build": {
"on": [ "on": [
{
"arg": "BUILD_BASE",
"module": "mesh-tools",
"artifact": "build"
},
{
"arg": "RUNTIME_BASE",
"module": "mesh-tools",
"artifact": "runtime"
},
{ {
"arg": "PYTHON_BASE", "arg": "PYTHON_BASE",
"image": "python@sha256:25f3cfeaceca14921366af4d1240b56457ef46273bdb508c7b0e8f469f6fd228" "image": "python@sha256:25f3cfeaceca14921366af4d1240b56457ef46273bdb508c7b0e8f469f6fd228"
@@ -534,9 +506,26 @@
], ],
"artifacts": [ "artifacts": [
{ {
"name": "runtime", "name": "code",
"kind": "image", "kind": "bundle",
"from": "Dockerfile" "language": "typescript",
"entrypoints": [
"index.js",
"tools/index.js",
"provisioner/index.js"
],
"loads": [
"index.js",
"tools/index.js",
"provisioner/index.js"
],
"env": {
"MESH_MAILU_URL": "http://127.0.0.1:${port:8080}/api/v1",
"MESH_MAILU_API_KEY_FILE": "${dir:state}/api-token.secret",
"MESH_MAILU_IMAP_CONTAINER": "mailu-imap",
"MESH_MAILU_CONFIG_FILE": "${dir:mesh-state}/config.json",
"MESH_RECEIVES": "${dir:grants}/mesh.json"
}
}, },
{ {
"name": "automx", "name": "automx",
-22
View File
@@ -1,22 +0,0 @@
# mesh-vault's runtime: the tool runtime, carrying this module's compiled provisioner, tools and event
# consumer. The same shape as postgres's, minus the client the database needs: mesh-vault reaches no
# server, because what it provides is a value the mesh already delivered to its node.
#
# **Built from this module's own directory and nothing else.** The sdk is in the base image, so
# nothing is copied out of a neighbouring checkout (novox/hq ADR 0069). Two bases, named rather than
# pinned — the image this is COMPILED in and the image it RUNS in — answered by the mesh from
# `build.on` in module.json (novox/hq issue 044).
ARG BUILD_BASE
ARG RUNTIME_BASE
FROM ${BUILD_BASE} AS build
WORKDIR /app/modules/vault
COPY . .
RUN node /app/node_modules/typescript/bin/tsc client.ts index.ts provisioner/index.ts tools/index.ts \
--module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist
FROM ${RUNTIME_BASE}
COPY --from=build /app/modules/vault/dist /app/modules/vault/dist
# The entrypoints a tool host loads from this module: its event consumer, its tools and its
# provisioner — one image, one process, one broker account (novox/hq ADR 0052).
ENV MESH_TOOL_MODULES=/app/modules/vault/dist/index.js,/app/modules/vault/dist/tools/index.js,/app/modules/vault/dist/provisioner/index.js
+18 -43
View File
@@ -27,16 +27,7 @@
"secret": "${dir:grants}" "secret": "${dir:grants}"
}, },
"keeps": "/var/lib/mesh-vault/root", "keeps": "/var/lib/mesh-vault/root",
"own-secrets": {
"broker": "${dir:mesh-state}/broker"
},
"resources": [ "resources": [
{
"id": "mesh-state",
"type": "directory",
"mode": "0700",
"place": "mesh"
},
{ {
"id": "state", "id": "state",
"type": "directory", "type": "directory",
@@ -57,45 +48,29 @@
"id": "root", "id": "root",
"type": "directory", "type": "directory",
"mode": "0700" "mode": "0700"
},
{
"id": "runtime",
"type": "container",
"name": "mesh-vault",
"network": "host",
"volumes": [
"${dir:mesh-state}/broker:/run/secrets/broker:ro",
"${dir:grants}:${dir:grants}:ro",
"${dir:ledger}:${dir:ledger}",
"${dir:root}:${dir:root}:ro"
],
"env": {
"MESH_BROKER_FILE": "/run/secrets/broker",
"MESH_RECEIVES": "${dir:grants}/mesh.json",
"MESH_VAULT_LEDGER": "${dir:ledger}",
"MESH_VAULT_ROOT": "${dir:root}"
},
"artifact": "runtime"
} }
], ],
"build": { "build": {
"on": [
{
"arg": "BUILD_BASE",
"module": "mesh-tools",
"artifact": "build"
},
{
"arg": "RUNTIME_BASE",
"module": "mesh-tools",
"artifact": "runtime"
}
],
"artifacts": [ "artifacts": [
{ {
"name": "runtime", "name": "code",
"kind": "image", "kind": "bundle",
"from": "Dockerfile" "language": "typescript",
"entrypoints": [
"index.js",
"tools/index.js",
"provisioner/index.js"
],
"loads": [
"index.js",
"tools/index.js",
"provisioner/index.js"
],
"env": {
"MESH_RECEIVES": "${dir:grants}/mesh.json",
"MESH_VAULT_LEDGER": "${dir:ledger}",
"MESH_VAULT_ROOT": "${dir:root}"
}
} }
] ]
}, },
-22
View File
@@ -1,22 +0,0 @@
# openai-consumer's runtime: the tool runtime, carrying this module's compiled code.
#
# **Built from this module's own directory and nothing else.** The sdk and the tool runtime are in
# the base images, published like any other artifact — which is what makes this buildable by the
# mesh from a repository and a path (novox/hq ADR 0069) rather than only on a workstation that
# happens to have the siblings.
#
# Two bases, named rather than pinned (novox/hq issue 044): the image this is COMPILED in and the
# image it RUNS in — the second must not carry a compiler. Declared in module.json's `build.on`.
ARG BUILD_BASE
ARG RUNTIME_BASE
FROM ${BUILD_BASE} AS build
WORKDIR /app/modules/openai-consumer
COPY . .
RUN node /app/node_modules/typescript/bin/tsc apply/index.ts \
--module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist
FROM ${RUNTIME_BASE}
COPY --from=build /app/modules/openai-consumer/dist /app/modules/openai-consumer/dist
# No serve-time entrypoints: every container of this module names its command (`run` on a
# schedule), so nothing here serves — deliberately no MESH_TOOL_MODULES.
+18 -31
View File
@@ -28,44 +28,31 @@
}, },
{ {
"id": "apply", "id": "apply",
"type": "container", "type": "process",
"name": "mesh-openai-consumer-apply", "name": "openai-consumer-apply",
"network": "host", "artifact": "code",
"run": [
"node",
"apply/index.js"
],
"schedule": "*/5 * * * *", "schedule": "*/5 * * * *",
"args": [
"run",
"/app/modules/openai-consumer/dist/apply/index.js"
],
"volumes": [
"${dir:state}:/run/state"
],
"env": { "env": {
"MESH_MODEL_ACCESS_SECRET_FILE": "/run/state/api-key", "MESH_MODEL_ACCESS_SECRET_FILE": "${dir:state}/api-key",
"MESH_MODEL_ACCESS_BIND_FILE": "/run/state/model.json", "MESH_MODEL_ACCESS_BIND_FILE": "${dir:state}/model.json",
"MESH_OPENAI_ENV_FILE": "/run/state/config/openai.env", "MESH_OPENAI_ENV_FILE": "${dir:state}/config/openai.env",
"MESH_OPENAI_CREDENTIALS_FILE": "/run/state/config/auth.json" "MESH_OPENAI_CREDENTIALS_FILE": "${dir:state}/config/auth.json"
}, }
"artifact": "runtime"
} }
], ],
"build": { "build": {
"on": [
{
"arg": "BUILD_BASE",
"module": "mesh-tools",
"artifact": "build"
},
{
"arg": "RUNTIME_BASE",
"module": "mesh-tools",
"artifact": "runtime"
}
],
"artifacts": [ "artifacts": [
{ {
"name": "runtime", "name": "code",
"kind": "image", "kind": "bundle",
"from": "Dockerfile" "language": "typescript",
"entrypoints": [
"apply/index.js"
]
} }
] ]
} }
-26
View File
@@ -1,26 +0,0 @@
# records' runtime: the tool runtime, carrying this module's compiled reader and its tools.
#
# **Built from this module's own directory and nothing else.** The sdk is in the base image, so
# nothing is copied out of a neighbouring checkout (novox/hq ADR 0069).
#
# Two bases, named rather than pinned: the image this is COMPILED in, and the image it RUNS in
# (novox/hq issue 044). Declared in module.json's `build.on`; deliberately no defaults.
ARG BUILD_BASE
ARG RUNTIME_BASE
FROM ${BUILD_BASE} AS build
WORKDIR /app/modules/records
COPY . .
RUN node /app/node_modules/typescript/bin/tsc records.ts index.ts tools/index.ts \
--module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist
FROM ${RUNTIME_BASE}
# **A module may need something the base image does not carry.** The reader keeps a checkout of the
# repository it reads (novox/hq ADR 0153) — a git working copy, kept current, not a derived copy — and
# the base image has no git. Certificates too, because the origin may be reached over TLS.
RUN apt-get update \
&& apt-get install -y --no-install-recommends git ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=build /app/modules/records/dist /app/modules/records/dist
# Both entrypoints, loaded in serve mode: the consumer that pulls on a merge, and the tools.
ENV MESH_TOOL_MODULES=/app/modules/records/dist/index.js,/app/modules/records/dist/tools/index.js
+19 -39
View File
@@ -11,9 +11,6 @@
"binds": { "binds": {
"git": "${dir:mesh-state}/git.json" "git": "${dir:mesh-state}/git.json"
}, },
"own-secrets": {
"broker": "${dir:mesh-state}/broker"
},
"consumes": [ "consumes": [
"gitea.pull.merged" "gitea.pull.merged"
], ],
@@ -53,47 +50,30 @@
"content": "${bound:git:scheme}://${bound:git:at}:${bound:git:port}\n" "content": "${bound:git:scheme}://${bound:git:at}:${bound:git:port}\n"
}, },
{ {
"id": "runtime", "id": "git",
"type": "container", "type": "package",
"name": "records", "package": "git"
"network": "host",
"volumes": [
"${dir:mesh-state}/broker:/run/secrets/broker:ro",
"${dir:mesh-state}/config.json:/run/config/config.json:ro",
"${dir:mesh-state}/origin:/run/config/origin:ro",
"${dir:checkout}:${dir:checkout}"
],
"env": {
"MESH_BROKER_FILE": "/run/secrets/broker",
"MESH_RECORDS_CONFIG_FILE": "/run/config/config.json",
"MESH_RECORDS_ORIGIN_FILE": "/run/config/origin",
"MESH_RECORDS_DIR": "${dir:checkout}"
},
"artifact": "runtime",
"restart-on": [
"config",
"origin"
]
} }
], ],
"build": { "build": {
"on": [
{
"arg": "BUILD_BASE",
"module": "mesh-tools",
"artifact": "build"
},
{
"arg": "RUNTIME_BASE",
"module": "mesh-tools",
"artifact": "runtime"
}
],
"artifacts": [ "artifacts": [
{ {
"name": "runtime", "name": "code",
"kind": "image", "kind": "bundle",
"from": "Dockerfile" "language": "typescript",
"entrypoints": [
"index.js",
"tools/index.js"
],
"loads": [
"index.js",
"tools/index.js"
],
"env": {
"MESH_RECORDS_CONFIG_FILE": "${dir:mesh-state}/config.json",
"MESH_RECORDS_ORIGIN_FILE": "${dir:mesh-state}/origin",
"MESH_RECORDS_DIR": "${dir:checkout}"
}
} }
] ]
} }
-24
View File
@@ -1,24 +0,0 @@
# route-adapter's runtime: the tool runtime, carrying this module's compiled code.
#
# **Built from this module's own directory and nothing else.** The sdk and the tool runtime are in
# the base images, published like any other artifact — which is what makes this buildable by the
# mesh from a repository and a path (novox/hq ADR 0069) rather than only on a workstation that
# happens to have the siblings.
#
# Two bases, named rather than pinned (novox/hq issue 044): the image this is COMPILED in and the
# image it RUNS in — the second must not carry a compiler. Declared in module.json's `build.on`.
ARG BUILD_BASE
ARG RUNTIME_BASE
FROM ${BUILD_BASE} AS build
WORKDIR /app/modules/route-adapter
COPY . .
RUN node /app/node_modules/typescript/bin/tsc adapter.ts index.ts \
--module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist
FROM ${RUNTIME_BASE}
COPY --from=build /app/modules/route-adapter/dist /app/modules/route-adapter/dist
# **No MESH_TOOL_MODULES, deliberately.** This module serves no tool and consumes no event: it is a
# step the host runs to completion, named by the container's `args` as `mesh-tools run …`. Setting a
# serve-time entrypoint here would give the image a second way to be started — one that connects to
# the broker and never exits.
+14 -28
View File
@@ -47,23 +47,18 @@
}, },
{ {
"id": "adapt", "id": "adapt",
"type": "container", "type": "process",
"name": "mesh-route-adapter", "name": "route-adapter",
"artifact": "runtime", "artifact": "code",
"run-once": true, "run": [
"volumes": [ "node",
"${dir:routes-dir}/mesh.json:${dir:routes-dir}/mesh.json:ro", "index.js"
"${dir:state}/config.json:/run/config/config.json:ro",
"${access:dynamic}:/services/traefik/dynamic"
], ],
"run-once": true,
"env": { "env": {
"MESH_RECEIVES": "${dir:routes-dir}/mesh.json", "MESH_RECEIVES": "${dir:routes-dir}/mesh.json",
"MESH_ROUTE_ADAPTER_CONFIG": "/run/config/config.json" "MESH_ROUTE_ADAPTER_CONFIG": "${dir:state}/config.json"
}, },
"args": [
"run",
"/app/modules/route-adapter/dist/index.js"
],
"restart-on": [ "restart-on": [
"received-route", "received-route",
"config" "config"
@@ -71,23 +66,14 @@
} }
], ],
"build": { "build": {
"on": [
{
"arg": "BUILD_BASE",
"module": "mesh-tools",
"artifact": "build"
},
{
"arg": "RUNTIME_BASE",
"module": "mesh-tools",
"artifact": "runtime"
}
],
"artifacts": [ "artifacts": [
{ {
"name": "runtime", "name": "code",
"kind": "image", "kind": "bundle",
"from": "Dockerfile" "language": "typescript",
"entrypoints": [
"index.js"
]
} }
] ]
} }