diff --git a/modules/anthropic-consumer/Dockerfile b/modules/anthropic-consumer/Dockerfile deleted file mode 100644 index 8445007..0000000 --- a/modules/anthropic-consumer/Dockerfile +++ /dev/null @@ -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. diff --git a/modules/anthropic-consumer/module.json b/modules/anthropic-consumer/module.json index 7b4c3ad..b2379cc 100644 --- a/modules/anthropic-consumer/module.json +++ b/modules/anthropic-consumer/module.json @@ -14,19 +14,10 @@ "secrets": { "model-access": "${dir:state}/access-token" }, - "own-secrets": { - "broker": "${dir:mesh-state}/broker" - }, "emits": [ "usage.session" ], "resources": [ - { - "id": "mesh-state", - "type": "directory", - "mode": "0700", - "place": "mesh" - }, { "id": "state", "type": "directory", @@ -46,66 +37,39 @@ }, { "id": "apply", - "type": "container", - "name": "mesh-anthropic-consumer-apply", - "network": "host", + "type": "process", + "name": "anthropic-consumer-apply", + "artifact": "code", + "run": [ + "node", + "apply/index.js" + ], "schedule": "*/5 * * * *", - "args": [ - "run", - "/app/modules/anthropic-consumer/dist/apply/index.js" - ], - "volumes": [ - "${dir:state}:/run/state" - ], "env": { - "MESH_MODEL_ACCESS_SECRET_FILE": "/run/state/access-token", - "MESH_MODEL_ACCESS_BIND_FILE": "/run/state/model.json", - "MESH_CLAUDE_CREDENTIALS_FILE": "/run/state/claude/.credentials.json", - "MESH_CLAUDE_IDENTITY_FILE": "/run/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" + "MESH_MODEL_ACCESS_SECRET_FILE": "${dir:state}/access-token", + "MESH_MODEL_ACCESS_BIND_FILE": "${dir:state}/model.json", + "MESH_CLAUDE_CREDENTIALS_FILE": "${dir:state}/claude/.credentials.json", + "MESH_CLAUDE_IDENTITY_FILE": "${dir:state}/claude/.claude.json" + } } ], "build": { - "on": [ - { - "arg": "BUILD_BASE", - "module": "mesh-tools", - "artifact": "build" - }, - { - "arg": "RUNTIME_BASE", - "module": "mesh-tools", - "artifact": "runtime" - } - ], "artifacts": [ { - "name": "runtime", - "kind": "image", - "from": "Dockerfile" + "name": "code", + "kind": "bundle", + "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" + } } ] } diff --git a/modules/anthropic-consumer/usage/index.ts b/modules/anthropic-consumer/usage/index.ts index 9dae62d..71a9662 100644 --- a/modules/anthropic-consumer/usage/index.ts +++ b/modules/anthropic-consumer/usage/index.ts @@ -3,12 +3,15 @@ // 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). // -// Runs as `mesh-tools run` (no broker), so events are emitted best-effort via the sibling mesh-tools -// `emit` primitive; the totals are also written to a file so the reading is observable without one. +// Runs in the node's runtime (novox/hq ADR 0198), every five minutes, so events are emitted through +// 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 { join, dirname } from "node:path"; +import { emit } from "@novox/mesh-sdk/events"; + import { readSessionFile, type SessionUsage } from "../transcript.js"; /** 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); } -/** 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): Promise { - const main = process.env.MESH_TOOLS_MAIN ?? "/app/dist/main.js"; - const { spawn } = await import("node:child_process"); - await new Promise((resolve) => { - 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}`); - resolve(); - }); - }); + try { + await emit("usage.session", body); + } catch (err) { + console.error(`[anthropic-consumer] could not emit usage: ${err}`); + } } -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); diff --git a/modules/audit-logger/Dockerfile b/modules/audit-logger/Dockerfile deleted file mode 100644 index f95c9ed..0000000 --- a/modules/audit-logger/Dockerfile +++ /dev/null @@ -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 diff --git a/modules/audit-logger/module.json b/modules/audit-logger/module.json index 358dbcd..f8c5e74 100644 --- a/modules/audit-logger/module.json +++ b/modules/audit-logger/module.json @@ -5,27 +5,21 @@ "consumes": [ "**" ], - "own-secrets": { - "broker": "${dir:state}/broker" - }, "build": { - "on": [ - { - "arg": "BUILD_BASE", - "module": "mesh-tools", - "artifact": "build" - }, - { - "arg": "RUNTIME_BASE", - "module": "mesh-tools", - "artifact": "runtime" - } - ], "artifacts": [ { - "name": "runtime", - "kind": "image", - "from": "Dockerfile" + "name": "code", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "index.js" + ], + "loads": [ + "index.js" + ], + "env": { + "AUDIT_LOG": "${dir:trail}/audit.log" + } } ] }, @@ -40,21 +34,6 @@ "id": "trail", "type": "directory", "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": [ diff --git a/modules/gitea/Dockerfile b/modules/gitea/Dockerfile deleted file mode 100644 index 59f134d..0000000 --- a/modules/gitea/Dockerfile +++ /dev/null @@ -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 diff --git a/modules/gitea/module.json b/modules/gitea/module.json index fe6e671..c329469 100644 --- a/modules/gitea/module.json +++ b/modules/gitea/module.json @@ -85,9 +85,6 @@ "scope": "mesh" } ], - "own-secrets": { - "broker": "${dir:mesh-state}/broker" - }, "resources": [ { "id": "mesh-state", @@ -180,32 +177,6 @@ "mode": "0600", "content": "{}\n", "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": [ @@ -219,23 +190,29 @@ } ], "build": { - "on": [ - { - "arg": "BUILD_BASE", - "module": "mesh-tools", - "artifact": "build" - }, - { - "arg": "RUNTIME_BASE", - "module": "mesh-tools", - "artifact": "runtime" - } - ], "artifacts": [ { - "name": "runtime", - "kind": "image", - "from": "Dockerfile" + "name": "code", + "kind": "bundle", + "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" + } } ] }, diff --git a/modules/lab/Dockerfile b/modules/lab/Dockerfile deleted file mode 100644 index d43ec04..0000000 --- a/modules/lab/Dockerfile +++ /dev/null @@ -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 diff --git a/modules/lab/module.json b/modules/lab/module.json index df85bc9..5cdc40b 100644 --- a/modules/lab/module.json +++ b/modules/lab/module.json @@ -5,16 +5,7 @@ "container-runtime", "virtualisation" ], - "own-secrets": { - "broker": "${dir:mesh-state}/broker" - }, "resources": [ - { - "id": "mesh-state", - "type": "directory", - "mode": "0700", - "place": "mesh" - }, { "id": "state", "type": "directory", @@ -35,47 +26,67 @@ "content": "MESH_LAB_FORGE=${setting:forge}\n" }, { - "id": "runtime", - "type": "container", - "name": "mesh-lab", - "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" - ], - "artifact": "runtime" + "id": "git", + "type": "package", + "package": "git" + }, + { + "id": "make", + "type": "package", + "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": { - "on": [ - { - "arg": "BUILD_BASE", - "module": "mesh-tools", - "artifact": "build" - }, - { - "arg": "RUNTIME_BASE", - "module": "mesh-tools", - "artifact": "runtime" - } - ], "artifacts": [ { - "name": "runtime", - "kind": "image", - "from": "Dockerfile" + "name": "code", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "tools/index.js" + ], + "loads": [ + "tools/index.js" + ], + "env": { + "MESH_LAB_WORK": "${dir:work}", + "MESH_LAB_ENV_FILE": "${dir:state}/lab.env" + } } ] } diff --git a/modules/lab/tools/index.ts b/modules/lab/tools/index.ts index 097b022..53dbd75 100644 --- a/modules/lab/tools/index.ts +++ b/modules/lab/tools/index.ts @@ -2,18 +2,23 @@ // lab is assigned to, and only there: a bed raises virtual machines on that machine's virtualisation. import { spawnSync } from "node:child_process"; +import { readFileSync } from "node:fs"; import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools"; import { listRuns, readStatus, REPOSITORIES, running, start, stop, tail } from "./runs.js"; export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] { 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 [ { name: "lab_check", description: "Whether this machine can run the lab's beds: the lab's own check, against the forge's main branch.", input: {}, run: async () => { + const forge = forgeOf(); if (!forge) return { ok: false, output: "the lab's forge is not set: settings for lab, {\"forge\": \"\"}" }; const dir = `${work}/check`; spawnSync("rm", ["-rf", dir]); @@ -38,6 +43,7 @@ export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] { }, }, run: async (args) => { + const forge = forgeOf(); if (!forge) return { started: false, reason: "the lab's forge is not set: settings for lab, {\"forge\": \"\"}" }; 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" }; @@ -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)); diff --git a/modules/mailu/Dockerfile b/modules/mailu/Dockerfile deleted file mode 100644 index 2462f57..0000000 --- a/modules/mailu/Dockerfile +++ /dev/null @@ -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 diff --git a/modules/mailu/module.json b/modules/mailu/module.json index 9d7dd75..4a1cd66 100644 --- a/modules/mailu/module.json +++ b/modules/mailu/module.json @@ -135,11 +135,15 @@ "protocol": "tcp", "from": "mesh", "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": [ { "id": "mesh-state", @@ -305,6 +309,9 @@ "name": "mailu-admin", "image": "ghcr.io/mailu/admin@sha256:6dbfdadc4a9590dcb7652357b505200115b689b74008653bbf369e4599a3be5a", "network": "mailu", + "ports": [ + "8080" + ], "env-file": [ "${dir:state}/mailu.env", "${dir:state}/secret.env", @@ -473,31 +480,6 @@ "content": "{}\n", "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", "type": "container", @@ -517,16 +499,6 @@ ], "build": { "on": [ - { - "arg": "BUILD_BASE", - "module": "mesh-tools", - "artifact": "build" - }, - { - "arg": "RUNTIME_BASE", - "module": "mesh-tools", - "artifact": "runtime" - }, { "arg": "PYTHON_BASE", "image": "python@sha256:25f3cfeaceca14921366af4d1240b56457ef46273bdb508c7b0e8f469f6fd228" @@ -534,9 +506,26 @@ ], "artifacts": [ { - "name": "runtime", - "kind": "image", - "from": "Dockerfile" + "name": "code", + "kind": "bundle", + "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", diff --git a/modules/mesh-vault/Dockerfile b/modules/mesh-vault/Dockerfile deleted file mode 100644 index ffe09b0..0000000 --- a/modules/mesh-vault/Dockerfile +++ /dev/null @@ -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 diff --git a/modules/mesh-vault/module.json b/modules/mesh-vault/module.json index 29e17a3..31c3092 100644 --- a/modules/mesh-vault/module.json +++ b/modules/mesh-vault/module.json @@ -27,16 +27,7 @@ "secret": "${dir:grants}" }, "keeps": "/var/lib/mesh-vault/root", - "own-secrets": { - "broker": "${dir:mesh-state}/broker" - }, "resources": [ - { - "id": "mesh-state", - "type": "directory", - "mode": "0700", - "place": "mesh" - }, { "id": "state", "type": "directory", @@ -57,45 +48,29 @@ "id": "root", "type": "directory", "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": { - "on": [ - { - "arg": "BUILD_BASE", - "module": "mesh-tools", - "artifact": "build" - }, - { - "arg": "RUNTIME_BASE", - "module": "mesh-tools", - "artifact": "runtime" - } - ], "artifacts": [ { - "name": "runtime", - "kind": "image", - "from": "Dockerfile" + "name": "code", + "kind": "bundle", + "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}" + } } ] }, diff --git a/modules/openai-consumer/Dockerfile b/modules/openai-consumer/Dockerfile deleted file mode 100644 index ff58bd2..0000000 --- a/modules/openai-consumer/Dockerfile +++ /dev/null @@ -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. diff --git a/modules/openai-consumer/module.json b/modules/openai-consumer/module.json index 6d4a0b0..90457d9 100644 --- a/modules/openai-consumer/module.json +++ b/modules/openai-consumer/module.json @@ -28,44 +28,31 @@ }, { "id": "apply", - "type": "container", - "name": "mesh-openai-consumer-apply", - "network": "host", + "type": "process", + "name": "openai-consumer-apply", + "artifact": "code", + "run": [ + "node", + "apply/index.js" + ], "schedule": "*/5 * * * *", - "args": [ - "run", - "/app/modules/openai-consumer/dist/apply/index.js" - ], - "volumes": [ - "${dir:state}:/run/state" - ], "env": { - "MESH_MODEL_ACCESS_SECRET_FILE": "/run/state/api-key", - "MESH_MODEL_ACCESS_BIND_FILE": "/run/state/model.json", - "MESH_OPENAI_ENV_FILE": "/run/state/config/openai.env", - "MESH_OPENAI_CREDENTIALS_FILE": "/run/state/config/auth.json" - }, - "artifact": "runtime" + "MESH_MODEL_ACCESS_SECRET_FILE": "${dir:state}/api-key", + "MESH_MODEL_ACCESS_BIND_FILE": "${dir:state}/model.json", + "MESH_OPENAI_ENV_FILE": "${dir:state}/config/openai.env", + "MESH_OPENAI_CREDENTIALS_FILE": "${dir:state}/config/auth.json" + } } ], "build": { - "on": [ - { - "arg": "BUILD_BASE", - "module": "mesh-tools", - "artifact": "build" - }, - { - "arg": "RUNTIME_BASE", - "module": "mesh-tools", - "artifact": "runtime" - } - ], "artifacts": [ { - "name": "runtime", - "kind": "image", - "from": "Dockerfile" + "name": "code", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "apply/index.js" + ] } ] } diff --git a/modules/records/Dockerfile b/modules/records/Dockerfile deleted file mode 100644 index 83a10c6..0000000 --- a/modules/records/Dockerfile +++ /dev/null @@ -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 diff --git a/modules/records/module.json b/modules/records/module.json index bf45d2f..f1f3c4e 100644 --- a/modules/records/module.json +++ b/modules/records/module.json @@ -11,9 +11,6 @@ "binds": { "git": "${dir:mesh-state}/git.json" }, - "own-secrets": { - "broker": "${dir:mesh-state}/broker" - }, "consumes": [ "gitea.pull.merged" ], @@ -53,47 +50,30 @@ "content": "${bound:git:scheme}://${bound:git:at}:${bound:git:port}\n" }, { - "id": "runtime", - "type": "container", - "name": "records", - "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" - ] + "id": "git", + "type": "package", + "package": "git" } ], "build": { - "on": [ - { - "arg": "BUILD_BASE", - "module": "mesh-tools", - "artifact": "build" - }, - { - "arg": "RUNTIME_BASE", - "module": "mesh-tools", - "artifact": "runtime" - } - ], "artifacts": [ { - "name": "runtime", - "kind": "image", - "from": "Dockerfile" + "name": "code", + "kind": "bundle", + "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}" + } } ] } diff --git a/modules/route-adapter/Dockerfile b/modules/route-adapter/Dockerfile deleted file mode 100644 index d65de80..0000000 --- a/modules/route-adapter/Dockerfile +++ /dev/null @@ -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. diff --git a/modules/route-adapter/module.json b/modules/route-adapter/module.json index 658b8db..e7501b2 100644 --- a/modules/route-adapter/module.json +++ b/modules/route-adapter/module.json @@ -47,23 +47,18 @@ }, { "id": "adapt", - "type": "container", - "name": "mesh-route-adapter", - "artifact": "runtime", - "run-once": true, - "volumes": [ - "${dir:routes-dir}/mesh.json:${dir:routes-dir}/mesh.json:ro", - "${dir:state}/config.json:/run/config/config.json:ro", - "${access:dynamic}:/services/traefik/dynamic" + "type": "process", + "name": "route-adapter", + "artifact": "code", + "run": [ + "node", + "index.js" ], + "run-once": true, "env": { "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": [ "received-route", "config" @@ -71,23 +66,14 @@ } ], "build": { - "on": [ - { - "arg": "BUILD_BASE", - "module": "mesh-tools", - "artifact": "build" - }, - { - "arg": "RUNTIME_BASE", - "module": "mesh-tools", - "artifact": "runtime" - } - ], "artifacts": [ { - "name": "runtime", - "kind": "image", - "from": "Dockerfile" + "name": "code", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "index.js" + ] } ] }