From dd93cfd6139d96823beffd75932c91200924e741 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 4 Oct 2026 00:32:59 +0200 Subject: [PATCH 1/9] audit-logger: its handler runs in the node's runtime (hq ADR 0198) The mesh-audit-logger container goes with its Dockerfile, build bases and bus credential: its one entrypoint is a load of one bundle, which subscribes to every event through the runtime and writes the trail at the host path the container used to mount. --- modules/audit-logger/Dockerfile | 33 ----------------------- modules/audit-logger/module.json | 45 +++++++++----------------------- 2 files changed, 12 insertions(+), 66 deletions(-) delete mode 100644 modules/audit-logger/Dockerfile 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": [ -- 2.54.0 From 944f086ec71058aa9df20e4654caaef6fcb1c9b6 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 4 Oct 2026 00:34:00 +0200 Subject: [PATCH 2/9] gitea: its watcher, tools and provisioner run in the node's runtime (hq ADR 0198) The mesh-gitea container goes with its Dockerfile, build bases and bus credential; its env becomes the bundle's words with mount targets folded back to host paths: the config file, the admin password and the kept-token state directory are read where the mesh writes them. --- modules/gitea/Dockerfile | 37 ---------------------- modules/gitea/module.json | 65 +++++++++++++-------------------------- 2 files changed, 21 insertions(+), 81 deletions(-) delete mode 100644 modules/gitea/Dockerfile 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" + } } ] }, -- 2.54.0 From 043ae17fbff28a11f601e8816d2725b377eb97b7 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 4 Oct 2026 00:34:25 +0200 Subject: [PATCH 3/9] mesh-vault: its handlers, tools and provisioner run in the node's runtime (hq ADR 0198) The mesh-vault container goes with its Dockerfile, build bases, bus credential and state directory; its env was already host paths, so it becomes the bundle's words unchanged. --- modules/mesh-vault/Dockerfile | 22 ------------ modules/mesh-vault/module.json | 61 ++++++++++------------------------ 2 files changed, 18 insertions(+), 65 deletions(-) delete mode 100644 modules/mesh-vault/Dockerfile 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}" + } } ] }, -- 2.54.0 From 8877f893e5b6419962008058cc52f1848a22da61 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 4 Oct 2026 00:34:57 +0200 Subject: [PATCH 4/9] records: its consumer and tools run in the node's runtime (hq ADR 0198) The records container goes with its Dockerfile, build bases and bus credential. The checkout, the config file and the origin file are read where the mesh writes them, and git comes from the machine's git package instead of the image's apt layer. --- modules/records/Dockerfile | 26 ----------------- modules/records/module.json | 58 ++++++++++++------------------------- 2 files changed, 19 insertions(+), 65 deletions(-) delete mode 100644 modules/records/Dockerfile 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}" + } } ] } -- 2.54.0 From 3b8164f1c098fbc6e5da0045027670f5a627c336 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 4 Oct 2026 00:35:30 +0200 Subject: [PATCH 5/9] mailu: its handlers, tools and provisioner run in the node's runtime (hq ADR 0198) The mesh-mailu container goes with its Dockerfile, the mesh-tools build bases and its bus credential; automx keeps its own image. The code reached the admin API by its name on the mailu network, which a process on the machine cannot, so the admin container publishes 8080 to this machine only and the bundle reaches it on loopback at that port. Mail is still read through docker exec into mailu-imap, so the runtime's account needs the docker socket as nextcloud's does. --- modules/mailu/Dockerfile | 30 ----------------- modules/mailu/module.json | 71 +++++++++++++++++---------------------- 2 files changed, 30 insertions(+), 71 deletions(-) delete mode 100644 modules/mailu/Dockerfile 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", -- 2.54.0 From 59c42b20860662ffa0a6be0ca804a929dc3551f1 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 4 Oct 2026 00:36:17 +0200 Subject: [PATCH 6/9] lab: its tools run in the node's runtime (hq ADR 0198) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mesh-lab container goes with its Dockerfile, build bases, bus credential and state directory. What the image installed — git, make, python, file, iproute2, sudo, npm, go and the incus client — are packages of the machine, and docker and incus are reached through their sockets as the runtime's account. The forge is an operator's setting, which reaches a file and never a bundle's words, so the tools read it from the env-file the mesh already fills, at each call; that is the one code change. --- modules/lab/Dockerfile | 31 ------------ modules/lab/module.json | 101 ++++++++++++++++++++----------------- modules/lab/tools/index.ts | 24 ++++++++- 3 files changed, 79 insertions(+), 77 deletions(-) delete mode 100644 modules/lab/Dockerfile 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)); -- 2.54.0 From 35ef72081fb6bc06961a826fe54f57436af7c6db Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 4 Oct 2026 00:36:40 +0200 Subject: [PATCH 7/9] route-adapter: its step is a run-once process (hq ADR 0198) The mesh-route-adapter container goes with its Dockerfile and build bases. The step runs node on the bundle as a run-once process, reading what the mesh contributed and its config where the mesh writes them and writing the proxy's dynamic directory at the path the container used to mount; it still runs again when a route or its config changes. --- modules/route-adapter/Dockerfile | 24 ------------------ modules/route-adapter/module.json | 42 +++++++++++-------------------- 2 files changed, 14 insertions(+), 52 deletions(-) delete mode 100644 modules/route-adapter/Dockerfile 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" + ] } ] } -- 2.54.0 From 3c6b70845c15fb35c5b530f31638879ee36cf68f Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 4 Oct 2026 00:36:58 +0200 Subject: [PATCH 8/9] openai-consumer: its apply is a scheduled process (hq ADR 0198) The mesh-openai-consumer-apply container goes with its Dockerfile and build bases. The same entrypoint runs every five minutes as a process on the machine, reading the binding and writing the credentials at the host paths the container used to mount. --- modules/openai-consumer/Dockerfile | 22 ------------- modules/openai-consumer/module.json | 49 +++++++++++------------------ 2 files changed, 18 insertions(+), 53 deletions(-) delete mode 100644 modules/openai-consumer/Dockerfile 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" + ] } ] } -- 2.54.0 From 568674fef7a92409c3fcc01c9b3e07ec46650319 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 4 Oct 2026 00:37:49 +0200 Subject: [PATCH 9/9] anthropic-consumer: its usage runs in the node's runtime, and its apply is a scheduled process (hq ADR 0198) Both containers go with the Dockerfile, build bases, bus credential and state directory. apply needs no bus and runs every five minutes as a process on the machine at the host paths the container mounted. usage emitted by spawning the runtime image's own emit command with the module's credential, which exists nowhere now, so it is loaded by the node's runtime instead: it emits through the SDK as this module and reads on the cadence the schedule gave it, once at start and every five minutes. That is the one code change. --- modules/anthropic-consumer/Dockerfile | 22 ------ modules/anthropic-consumer/module.json | 88 +++++++---------------- modules/anthropic-consumer/usage/index.ts | 37 +++++----- 3 files changed, 45 insertions(+), 102 deletions(-) delete mode 100644 modules/anthropic-consumer/Dockerfile 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); -- 2.54.0