diff --git a/src/ump-recall-mcp.js b/src/ump-recall-mcp.js index add8c05..7419085 100644 --- a/src/ump-recall-mcp.js +++ b/src/ump-recall-mcp.js @@ -116,17 +116,64 @@ async function umpCapable() { let umpChild = null; let umpChildInitPromise = null; +// Resolve the canonical UMP_DIR published by ump-memory.service via +// /run/ump-memory/ump-dir.txt. This is the ONLY source of truth that +// matters. We trust it before env, before config, before fallbacks. +// +// Why: every previous fix relied on a profile config or in-process env +// being correct. They drift. Disk-config inherits wrong values on +// every restart. The systemd unit publishes the canonical path on every +// (re)start, so reading it here makes the child process self-healing. +import fs from "node:fs"; + +const CANONICAL_UMP_DIR_FILE = "/run/ump-memory/ump-dir.txt"; +const CANONICAL_UMP_PORT_FILE = "/run/ump-memory/ump-port.txt"; +const CANONICAL_UMP_STORE_FILE = "/run/ump-memory/ump-store.txt"; + +function readCanonical(key, fallback) { + try { + return fs.readFileSync(key, "utf8").trim() || fallback; + } catch { + return fallback; + } +} + +const CANONICAL_UMP_DIR = readCanonical(CANONICAL_UMP_DIR_FILE, "/root/.hermes/state/ump-local"); +const CANONICAL_UMP_PORT = readCanonical(CANONICAL_UMP_PORT_FILE, "4317"); +const CANONICAL_UMP_STORE = readCanonical(CANONICAL_UMP_STORE_FILE, "json"); + function spawnUmpChild() { if (umpChild && !umpChild.killed) return umpChild; LOG("spawning canonical UMP subprocess"); + LOG(`canonical path from ${CANONICAL_UMP_DIR_FILE}: ${CANONICAL_UMP_DIR}`); umpChild = spawn("npx", ["-y", "-p", "@universalmemoryprotocol/core", "ump", "memory"], { stdio: ["pipe", "pipe", "inherit"], env: { - ...process.env, - UMP_DIR: process.env.UMP_DIR || "/root/.hermes/state/ump-local", - UMP_STORE: process.env.UMP_STORE || "json", + // DISCARD the parent's env for the UMP_* trio: anything the parent + // (krystie gateway, default gateway, etc.) might have stale is + // overridden with the canonical published values. This is the + // mechanism that breaks the recurring silent-drop pattern. + ...omitUmpEnv(process.env), + UMP_DIR: CANONICAL_UMP_DIR, + UMP_HTTP: CANONICAL_UMP_PORT, + UMP_STORE: CANONICAL_UMP_STORE, }, }); + // Self-heal: verify the spawned child actually inherited the canonical + // UMP_DIR. If somehow the env was filtered or the path was changed, + // fail loudly so the operator sees it (better than silent drop). + setImmediate(() => { + try { + const childEnv = fs.readFileSync(`/proc/${umpChild.pid}/environ`, "utf8") + .split("\0") + .find((s) => s.startsWith("UMP_DIR=")); + if (!childEnv || childEnv.split("=")[1] !== CANONICAL_UMP_DIR) { + LOG(`FATAL: spawned child inherited wrong UMP_DIR: ${childEnv} (wanted ${CANONICAL_UMP_DIR})`); + } + } catch (e) { + LOG(`could not audit child env: ${e.message}`); + } + }); umpChild.on("exit", (code) => { LOG(`UMP subprocess exited code=${code}`); umpChild = null; @@ -135,6 +182,20 @@ function spawnUmpChild() { return umpChild; } +function omitUmpEnv(env) { + // Strip UMP_DIR / UMP_HTTP / UMP_STORE from a spread'd env so the + // canonical values always take precedence. We only return the safe + // subset of process.env for the child. + const out = {}; + for (const [k, v] of Object.entries(env)) { + if (!["UMP_DIR", "UMP_HTTP", "UMP_STORE", "UMP_NAMESPACE", "SIDECAR_URL"].includes(k)) { + out[k] = v; + } + } + return out; +} + + let nextReqId = 1; const pendingRequests = new Map(); let umpServerInfo = null; // populated by initialize handshake