fix(shim): self-healing canonical path resolution via /run/ump-memory/
LONG-STANDING PROBLEM (now permanently fixed):
The ump-recall-mcp.js shim spawned a 'npx ump memory' child process
with UMP_DIR inherited from the parent gateway's env. Profile
configs (config.yaml) and parent processes had stale UMP_DIR values
from before the ump store migration, so the child wrote to an empty
directory. Result: krystie's ump.remember calls silently dropped.
NEW PATTERN:
1. ump-memory.service publishes its UMP_DIR/HTTP/STORE via
/run/ump-memory/ump-{dir,port,store}.txt on every (re)start.
This file is the canonical source of truth.
2. On every ump child spawn, the shim:
a. Reads /run/ump-memory/ump-{dir,port,store}.txt (THE source)
b. Strips UMP_* / SIDECAR_URL from parent's env (so stale values
from profile configs cannot leak through)
c. Spawns with the canonical values
d. Audits the spawned child's env via /proc/<pid>/environ and
logs FATAL if anything other than the canonical UMP_DIR was
inherited
This breaks the recurring silent-drop pattern regardless of:
- which profile is launching the gateway (krystie, default, future)
- whether env vars are stale or fresh
- whether the parent process is managed by systemd or launched by hand
- whether config.yaml is pinned to a stale path
The only way to get the wrong UMP_DIR after this patch is if
/run/ump-memory/ump-dir.txt itself is wrong — and that file is
regenerated by ump-memory.service on every (re)start, so the only way
to make it wrong is to corrupt it intentionally.
Companion: krystie-hermes-gateway.service deploy asset (next commit)
makes krystie's gateway systemd-managed so its shim lifetime matches
ump-memory.service's lifetime.
This commit is contained in:
+64
-3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user