From 235321ed8697bc0e4108c07bee9bbbb3f1ca22c8 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 24 Oct 2026 00:45:01 +0200 Subject: [PATCH] Entry: derive the daemon's environment in one exported function, and test its argument contract Co-Authored-By: Claude Opus 5.5 --- scripts/idunn-entry.mjs | 22 +++++++++----- scripts/test-vili-daemon.mjs | 57 +++++++++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 8 deletions(-) diff --git a/scripts/idunn-entry.mjs b/scripts/idunn-entry.mjs index 1cbaf8f..257dd5b 100644 --- a/scripts/idunn-entry.mjs +++ b/scripts/idunn-entry.mjs @@ -94,15 +94,23 @@ function parseEntryArgs(argv) { if (!/^[0-9a-f]{40}$/u.test(argv[3])) fail("--kimodo-commit must be 40 lowercase hex characters"); } +// What the daemon reads from its environment, derived from Idunn's bind and the sealed release's own layout: +// the interpreter sits beside the vili tree, and the environment's identity is the hash of the two lock files. +export function daemonEnvironment(bind, entryDir) { + const { host, port } = splitBind(bind); + const locks = ["requirements-build.lock", "requirements.lock"].map((name) => + readFileSync(path.resolve(entryDir, "..", "deployment", "idunn", name))); + return { + VILI_DAEMON_HOST: host, + VILI_DAEMON_PORT: port, + VILI_PYTHON: path.resolve(entryDir, "..", "..", "python", "bin", "python3.12"), + VILI_PYTHON_ENV: `locks-sha256:${createHash("sha256").update(Buffer.concat(locks)).digest("hex")}`, + }; +} + async function main() { parseEntryArgs(process.argv.slice(2)); - const { host, port } = splitBind(process.env.GAMECULT_IDUNN_CANDIDATE_BIND); - const locks = ["requirements-build.lock", "requirements.lock"].map((name) => - readFileSync(path.resolve(here, "..", "deployment", "idunn", name))); - process.env.VILI_DAEMON_HOST = host; - process.env.VILI_DAEMON_PORT = port; - process.env.VILI_PYTHON = path.resolve(here, "..", "..", "python", "bin", "python3.12"); - process.env.VILI_PYTHON_ENV = `locks-sha256:${createHash("sha256").update(Buffer.concat(locks)).digest("hex")}`; + Object.assign(process.env, daemonEnvironment(process.env.GAMECULT_IDUNN_CANDIDATE_BIND, here)); const authority = loadIdunnRuntimeAuthorityFromEnvironment("vili", HEALTH_CONTRACT); const signer = createIdunnRuntimeSigner({ diff --git a/scripts/test-vili-daemon.mjs b/scripts/test-vili-daemon.mjs index 7fca2e9..1e64857 100644 --- a/scripts/test-vili-daemon.mjs +++ b/scripts/test-vili-daemon.mjs @@ -2,7 +2,7 @@ // Runs the real daemon against scripts/fixtures/stub-worker.mjs as its --python/--worker-entry. import assert from "node:assert/strict"; import { spawn } from "node:child_process"; -import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs"; import http from "node:http"; import os from "node:os"; import path from "node:path"; @@ -834,3 +834,58 @@ test("the hand-run daemon has no route challenge", async () => { await daemon.stop(); } }); + +// ---- the Idunn entry's own rules: its argument contract, and what it derives for the daemon + +const entryPath = path.join(here, "idunn-entry.mjs"); +const runEntry = (args, env = {}) => new Promise((resolve) => { + const proc = spawn(process.execPath, [entryPath, ...args], { env: { PATH: process.env.PATH, ...env }, stdio: ["ignore", "pipe", "pipe"] }); + let output = ""; + proc.stdout.on("data", (chunk) => { output += chunk; }); + proc.stderr.on("data", (chunk) => { output += chunk; }); + proc.on("exit", (code) => resolve({ code, output })); +}); + +test("the entry takes exactly --state-root --kimodo-commit <40 hex>, and echoes none of a bad value", async () => { + const commit = "6bb58488037dd65360ff0c5d1692b403a23309f7"; + const good = ["--state-root", "/var/lib/gamecult/vili", "--kimodo-commit", commit]; + const bads = [ + [], + [...good, "--extra", "x"], + ["--kimodo-commit", commit, "--state-root", "/var/lib/gamecult/vili"], + ["--state-root", "canary-relative-path", "--kimodo-commit", commit], + ["--state-root", "/var/lib/gamecult/vili", "--kimodo-commit", "canary-not-a-commit"], + ["--state-root", "/var/lib/gamecult/vili", "--kimodo-commit", commit.toUpperCase()], + ]; + for (const args of bads) { + const answer = await runEntry(args, { GAMECULT_IDUNN_CANDIDATE_BIND: "127.0.0.1:18861" }); + assert.equal(answer.code, 2, JSON.stringify(args)); + assert.ok(!answer.output.includes("canary"), `the refusal repeated a value of ${JSON.stringify(args)}`); + } + const bind = await runEntry(good, { GAMECULT_IDUNN_CANDIDATE_BIND: "canary-not-a-bind" }); + assert.equal(bind.code, 2); + assert.match(bind.output, /CANDIDATE_BIND is not host:port/); + assert.ok(!bind.output.includes("canary")); +}); + +test("the entry derives the daemon's host, port, interpreter and environment identity from the release", async () => { + const { createHash } = await import("node:crypto"); + const { daemonEnvironment } = await import("./idunn-entry.mjs"); + const release = mkdtempSync(path.join(os.tmpdir(), "vili-entry-")); + try { + mkdirSync(path.join(release, "vili", "scripts"), { recursive: true }); + mkdirSync(path.join(release, "vili", "deployment", "idunn"), { recursive: true }); + writeFileSync(path.join(release, "vili", "deployment", "idunn", "requirements-build.lock"), "build lock bytes\n"); + writeFileSync(path.join(release, "vili", "deployment", "idunn", "requirements.lock"), "runtime lock bytes\n"); + const entryDir = path.join(release, "vili", "scripts"); + for (const bind of ["127.0.0.1:18861", "http://127.0.0.1:18861"]) { + const derived = daemonEnvironment(bind, entryDir); + assert.equal(derived.VILI_DAEMON_HOST, "127.0.0.1"); + assert.equal(derived.VILI_DAEMON_PORT, "18861"); + assert.equal(derived.VILI_PYTHON, path.join(release, "python", "bin", "python3.12")); + assert.equal(derived.VILI_PYTHON_ENV, `locks-sha256:${createHash("sha256").update("build lock bytes\nruntime lock bytes\n").digest("hex")}`); + } + } finally { + rmSync(release, { recursive: true, force: true }); + } +});