From 991fb6faeceaf42e778e0a0beeea7aedecf0810c Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 6 Sep 2026 00:17:13 +0200 Subject: [PATCH] runtime: a 'run' subcommand to run a module entrypoint to completion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The run-once step of ADR 0052 reuses a module's runtime image but must run its bootstrap/seed entrypoint offline and exit, not the broker-bound serve loop. Add 'mesh-tools run ': import the compiled entrypoint, await its top-level work, exit — no broker, so a first-boot seed runs before the module has anything to talk to. Exit code is the step's, which is how the host gates the container that depends on it. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF --- src/main.ts | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index f1aea23..d0d46d1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,10 +1,17 @@ -// The runnable entrypoint. Two modes: +// The runnable entrypoint. Three modes: // // mesh-tools serve — bind the broker and serve the assigned modules until // stopped. A module entrypoint that subscribes to events (on("#")) // starts consuming as it is imported, so this also runs consumers. // mesh-tools emit TYPE [JSON] emit one event onto the mesh and exit — an operable primitive, // and what an events test uses to put a message on the wire. +// mesh-tools run ENTRYPOINT run one compiled module entrypoint to completion and exit — the +// runtime side of a run-once step (novox/hq ADR 0052). It imports +// the given entrypoint, whose top-level code does its work — seed a +// store, migrate, health-gate — and awaits it. It does NOT connect +// to the broker: a first-boot step runs offline, before the module +// has anything to talk to, and the host gates the container that +// depends on it on this process exiting 0. // // The broker, in order of preference: // MESH_BROKER_FILE a sealed {url, fingerprint} the mesh delivered (novox/hq ADR 0043) — an @@ -14,6 +21,7 @@ // MESH_MODULE / MESH_NODE the identity stamped onto emitted events (ADR 0042) import { readFileSync } from "node:fs"; +import { pathToFileURL } from "node:url"; import { connectAmqp } from "./broker-amqp.js"; import type { Credential } from "./broker-amqp.js"; import { runTools } from "./runtime.js"; @@ -109,8 +117,29 @@ async function invokeOnce(module: string, tool: string, argsJson: string): Promi await broker.close(); } +/** + * Run one compiled module entrypoint to completion — the runtime side of a run-once step + * (novox/hq ADR 0052). Importing it runs its top-level code and awaits any top-level await, so this + * returns only once the step's own code has finished; a step that throws rejects here and the + * process exits non-zero, which is how the host knows the step did not complete and must not start + * the container it gates. No broker is connected — a first-boot seed or migration runs offline. + */ +async function runEntry(entrypoint: string): Promise { + if (!entrypoint) { + console.error("mesh-tools run — a compiled module entrypoint path is required"); + process.exit(1); + } + // A file URL, not a bare path: dynamic import of an absolute path is not portable, and the + // entrypoint the manifest names is an absolute path inside the image. + await import(pathToFileURL(entrypoint).href); +} + async function main(): Promise { const [command, ...rest] = process.argv.slice(2); + if (command === "run") { + await runEntry(rest[0] ?? ""); + return; + } if (command === "invoke") { const [module, tool] = rest; if (!module || !tool) { -- 2.54.0