UNPKG

@sellable/install

Version:

One-command installer for Sellable MCP in Claude Code, Codex, and Hermes

1,519 lines (1,457 loc) • 46.3 kB
#!/usr/bin/env node import { spawnSync } from "node:child_process"; import { createHash, randomUUID } from "node:crypto"; import { chmodSync, chownSync, closeSync, constants, existsSync, fchmodSync, fchownSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, unlinkSync, writeFileSync, } from "node:fs"; import { dirname, isAbsolute, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { SOUL_ARTIFACT_MAX_BYTES, SoulArtifactValidationError, assertSoulArtifact, validateSoulArtifact, } from "./soul-artifact-validator.mjs"; export const SOUL_BRIDGE_DATA_ROOT = "/opt/data"; export const SOUL_LOCK_DIRECTORY = ".sellable-agent/soul-locks"; export const SOUL_JOURNAL_DIRECTORY = ".sellable-agent/soul-journals"; export const SOUL_PROOF_DIRECTORY = ".sellable-agent/soul-proofs"; export const SOUL_PROOF_SCHEMA_VERSION = "sellable-agent-soul-proof/v1"; export const SOUL_PROOF_STATE_PROVEN = "PROVEN"; export const SOUL_PROOF_STATE_UNPROVEN = "UNPROVEN"; export const SOUL_JOURNAL_SCHEMA_VERSION = "sellable-agent-soul-journal/v1"; const PROFILE_ID = /^[a-z0-9][a-z0-9_-]{0,127}$/; // Runtime replacement must never disappear behind an unbounded synchronous // flock wait. Ten seconds stays below the operation heartbeat interval and is // long enough for the short profile metadata critical sections used here. const SOUL_LOCK_ACQUIRE_TIMEOUT_SECONDS = 10; const SHA256 = /^[a-f0-9]{64}$/; const JOURNAL_LIMIT = 256 * 1024; // JSON.stringify may encode one accepted content byte as a six-byte `\uXXXX` // escape. Bound the closed stdin envelope without rejecting any SOUL that the // portable validator can admit. export const SOUL_BRIDGE_REQUEST_LIMIT = SOUL_ARTIFACT_MAX_BYTES * 6 + 4 * 1024; const LOCK_MODE = 0o660; const STATE_FILE_MODE = 0o660; const PROC_ENTRY_LIMIT = 8_192; const PROC_CMDLINE_LIMIT = 16 * 1024; const PROC_ENVIRON_LIMIT = 128 * 1024; const PROC_STAT_LIMIT = 8 * 1024; const PINNED_HERMES_LAUNCHER = "/opt/hermes/.venv/bin/hermes"; const PINNED_HERMES_PYTHON = "/opt/hermes/.venv/bin/python3"; // Fly images provide util-linux at the immutable system path. Local macOS // verification uses Homebrew's equivalent because Apple does not ship flock. const FLOCK_EXECUTABLE = existsSync("/usr/bin/flock") ? "/usr/bin/flock" : existsSync("/opt/homebrew/bin/flock") ? "/opt/homebrew/bin/flock" : existsSync("/usr/local/bin/flock") ? "/usr/local/bin/flock" : "/usr/bin/flock"; const sha256 = (value) => createHash("sha256").update(value).digest("hex"); export class FlySoulBridgeError extends Error { constructor(code) { super(code); this.name = "FlySoulBridgeError"; this.code = code; } } function fail(code) { throw new FlySoulBridgeError(code); } function assertProfileId(profileId) { if (typeof profileId !== "string" || !PROFILE_ID.test(profileId)) { fail("soul_profile_id_rejected"); } return profileId; } function assertDataRoot(dataRoot) { if ( typeof dataRoot !== "string" || !isAbsolute(dataRoot) || resolve(dataRoot) !== dataRoot || !existsSync(dataRoot) ) { fail("soul_data_root_rejected"); } const info = lstatSync(dataRoot); if (info.isSymbolicLink() || !info.isDirectory()) fail("soul_data_root_rejected"); return realpathSync(dataRoot); } export function soulProfilePaths( profileId, { dataRoot = SOUL_BRIDGE_DATA_ROOT } = {} ) { const id = assertProfileId(profileId); const root = assertDataRoot(dataRoot); const profileRoot = join(root, "profiles", id); return Object.freeze({ dataRoot: root, profileId: id, profileRoot, soulPath: join(profileRoot, "SOUL.md"), lockRoot: join(root, SOUL_LOCK_DIRECTORY), lockPath: join(root, SOUL_LOCK_DIRECTORY, `${id}.lock`), journalRoot: join(root, SOUL_JOURNAL_DIRECTORY), journalPath: join(root, SOUL_JOURNAL_DIRECTORY, `${id}.json`), proofRoot: join(root, SOUL_PROOF_DIRECTORY), proofPath: join(root, SOUL_PROOF_DIRECTORY, `${id}.json`), legacyRootSoulPath: join(root, "SOUL.md"), }); } function readBoundedProcFile(path, limit) { const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW); try { const chunks = []; let total = 0; while (true) { const chunk = Buffer.allocUnsafe(Math.min(8 * 1024, limit + 1 - total)); const count = readSync(fd, chunk, 0, chunk.byteLength, null); if (count === 0) break; chunks.push(chunk.subarray(0, count)); total += count; if (total > limit) fail("soul_gateway_process_metadata_exceeded"); } return Buffer.concat(chunks, total); } finally { closeSync(fd); } } function decodeProcNulList(bytes, code) { if (bytes.byteLength < 2 || bytes.at(-1) !== 0) fail(code); let decoded; try { decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes); } catch { fail(code); } const entries = decoded.split("\0"); entries.pop(); if ( entries.length < 1 || entries.some((entry) => !entry || entry.includes("\0")) ) { fail(code); } return entries; } function parseProcStartTicks(bytes, pid) { const value = bytes.toString("utf8").trim(); const close = value.lastIndexOf(")"); if (!value.startsWith(`${pid} (`) || close < 4) fail("soul_gateway_stat_rejected"); const fields = value.slice(close + 2).split(" "); const startTicks = fields[19]; if (fields.length < 20 || !/^[1-9][0-9]*$/.test(startTicks ?? "")) { fail("soul_gateway_stat_rejected"); } return startTicks; } function parseProcEnvironment(entries) { const environment = new Map(); for (const entry of entries) { const separator = entry.indexOf("="); if (separator < 1) fail("soul_gateway_environment_rejected"); const name = entry.slice(0, separator); const value = entry.slice(separator + 1); if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) || environment.has(name)) { fail("soul_gateway_environment_rejected"); } environment.set(name, value); } return environment; } function stableGatewayIdentity(gateway) { return JSON.stringify({ pid: gateway.pid, startTicks: gateway.startTicks, profileId: gateway.profileId, profileRoot: gateway.profileRoot, executable: gateway.executable, launcher: gateway.launcher, cwd: gateway.cwd, hermesHome: gateway.environment.HERMES_HOME, configPath: gateway.environment.HERMES_CONFIG_PATH, }); } function realpathOrFail(path, code) { try { return realpathSync(path); } catch { fail(code); } } function inspectGatewayPid( pid, { procRoot, dataRoot, pinnedHermesLauncher, pinnedHermesPython } ) { const pidRoot = join(procRoot, pid); let startBefore; let argv; try { startBefore = parseProcStartTicks( readBoundedProcFile(join(pidRoot, "stat"), PROC_STAT_LIMIT), pid ); const cmdline = readBoundedProcFile( join(pidRoot, "cmdline"), PROC_CMDLINE_LIMIT ); // Fly Machines expose kernel threads in the guest PID namespace. They // have a valid /proc/<pid>/stat entry but an intentionally empty cmdline // and therefore cannot be a userspace Hermes gateway. if (cmdline.byteLength === 0) return null; argv = decodeProcNulList(cmdline, "soul_gateway_argv_rejected"); } catch (error) { if (["ENOENT", "ESRCH", "EACCES"].includes(error?.code)) return null; throw error; } const gatewayRunIndex = argv.findIndex( (entry, index) => entry === "gateway" && argv[index + 1] === "run" ); const looksLikeGateway = argv.includes(pinnedHermesLauncher) && gatewayRunIndex >= 0; if (!looksLikeGateway) return null; // Hermes 0.21.5 runs as Sellable's foreground child with this exact supervisor // suffix. Keep the prior bare/--replace forms for rolling upgrades, but do not // admit partial, reordered, or arbitrary extra gateway flags. const supervisorSuffix = [ "--replace", "--force", "--no-supervise", "--external-supervisor", ]; const suffixLength = supervisorSuffix.every( (entry, index) => argv.at(index - supervisorSuffix.length) === entry ) ? supervisorSuffix.length : argv.at(-1) === "--replace" ? 1 : 0; const profileId = argv.at(-3 - suffixLength); const prefix = argv.slice(0, -4 - suffixLength); if ( argv.at(-4 - suffixLength) !== "--profile" || argv.at(-2 - suffixLength) !== "gateway" || argv.at(-1 - suffixLength) !== "run" || !PROFILE_ID.test(profileId ?? "") || prefix.length !== 2 || prefix[1] !== pinnedHermesLauncher ) { fail("soul_gateway_argv_rejected"); } const expectedPython = realpathOrFail( pinnedHermesPython, "soul_gateway_executable_rejected" ); if ( !isAbsolute(prefix[0]) || realpathOrFail(prefix[0], "soul_gateway_executable_rejected") !== expectedPython ) { fail("soul_gateway_executable_rejected"); } const executable = realpathOrFail( join(pidRoot, "exe"), "soul_gateway_identity_changed" ); if (executable !== expectedPython) fail("soul_gateway_executable_rejected"); const paths = soulProfilePaths(profileId, { dataRoot }); const cwd = realpathOrFail( join(pidRoot, "cwd"), "soul_gateway_identity_changed" ); if (cwd !== paths.profileRoot) fail("soul_gateway_cwd_rejected"); let environment; try { environment = parseProcEnvironment( decodeProcNulList( readBoundedProcFile(join(pidRoot, "environ"), PROC_ENVIRON_LIMIT), "soul_gateway_environment_rejected" ) ); } catch (error) { if (["ENOENT", "ESRCH"].includes(error?.code)) { fail("soul_gateway_identity_changed"); } throw error; } const expectedEnvironment = { SELLABLE_AGENT_RUNTIME: "1", HERMES_PROFILE: profileId, SELLABLE_HERMES_PROFILE_ID: profileId, HERMES_HOME: paths.dataRoot, HERMES_CONFIG_PATH: join(paths.profileRoot, "config.yaml"), }; for (const [name, value] of Object.entries(expectedEnvironment)) { if (environment.get(name) !== value) fail("soul_gateway_environment_rejected"); } let startAfter; try { startAfter = parseProcStartTicks( readBoundedProcFile(join(pidRoot, "stat"), PROC_STAT_LIMIT), pid ); } catch { fail("soul_gateway_identity_changed"); } if (startAfter !== startBefore) fail("soul_gateway_identity_changed"); return Object.freeze({ pid: Number(pid), startTicks: startBefore, profileId, profileRoot: paths.profileRoot, executable, launcher: pinnedHermesLauncher, cwd, environment: Object.freeze(expectedEnvironment), }); } export function discoverLiveHermesGateway({ procRoot = "/proc", dataRoot = SOUL_BRIDGE_DATA_ROOT, pinnedHermesLauncher = PINNED_HERMES_LAUNCHER, pinnedHermesPython = PINNED_HERMES_PYTHON, } = {}) { const root = assertDataRoot(dataRoot); if ( !isAbsolute(procRoot) || resolve(procRoot) !== procRoot || !isAbsolute(pinnedHermesLauncher) || !isAbsolute(pinnedHermesPython) ) { fail("soul_gateway_discovery_configuration_rejected"); } const pids = readdirSync(procRoot) .filter((entry) => /^[1-9][0-9]*$/.test(entry)) .sort((left, right) => Number(left) - Number(right)); if (pids.length > PROC_ENTRY_LIMIT) fail("soul_gateway_process_inventory_exceeded"); const matches = []; for (const pid of pids) { const match = inspectGatewayPid(pid, { procRoot, dataRoot: root, pinnedHermesLauncher, pinnedHermesPython, }); if (match) matches.push(match); if (matches.length > 1) fail("soul_gateway_ambiguous"); } if (matches.length === 0) fail("soul_gateway_not_running"); return matches[0]; } export function revalidateLiveHermesGateway( gateway, { procRoot = "/proc", dataRoot = SOUL_BRIDGE_DATA_ROOT, pinnedHermesLauncher = PINNED_HERMES_LAUNCHER, pinnedHermesPython = PINNED_HERMES_PYTHON, } = {} ) { if (!gateway || !Number.isSafeInteger(gateway.pid) || gateway.pid < 1) { fail("soul_gateway_identity_rejected"); } const current = inspectGatewayPid(String(gateway.pid), { procRoot, dataRoot: assertDataRoot(dataRoot), pinnedHermesLauncher, pinnedHermesPython, }); if ( !current || stableGatewayIdentity(current) !== stableGatewayIdentity(gateway) ) { fail("soul_gateway_identity_changed"); } return current; } /** Prepare the stable, setgid lock root before a runtime drops privileges. */ export function prepareSoulLockRoot({ dataRoot = SOUL_BRIDGE_DATA_ROOT, gid = process.getgid?.(), } = {}) { const root = assertDataRoot(dataRoot); const stateRoot = join(root, ".sellable-agent"); const lockRoot = join(root, SOUL_LOCK_DIRECTORY); const journalRoot = join(root, SOUL_JOURNAL_DIRECTORY); const proofRoot = join(root, SOUL_PROOF_DIRECTORY); mkdirSync(stateRoot, { recursive: true, mode: 0o2770 }); for (const path of [stateRoot, lockRoot, journalRoot, proofRoot]) { mkdirSync(path, { recursive: true, mode: 0o2770, }); const info = lstatSync(path); if (info.isSymbolicLink() || !info.isDirectory()) fail("soul_state_root_rejected"); } if (process.getuid?.() === 0 && Number.isSafeInteger(gid) && gid >= 0) { for (const path of [stateRoot, lockRoot, journalRoot, proofRoot]) { chownSync(path, 0, gid); } } for (const path of [stateRoot, lockRoot, journalRoot, proofRoot]) chmodSync(path, 0o2770); return Object.freeze({ lockRoot, journalRoot, proofRoot }); } export function prepareProfileSoulLock( profileId, { dataRoot = SOUL_BRIDGE_DATA_ROOT, gid = process.getgid?.() } = {} ) { const paths = soulProfilePaths(profileId, { dataRoot }); prepareSoulLockRoot({ dataRoot, gid }); const fd = openSync( paths.lockPath, constants.O_RDWR | constants.O_CREAT | constants.O_NOFOLLOW, LOCK_MODE ); try { fchmodSync(fd, LOCK_MODE); if (process.getuid?.() === 0 && Number.isSafeInteger(gid) && gid >= 0) { fchownSync(fd, 0, gid); } fsyncSync(fd); } finally { closeSync(fd); } fsyncDirectory(paths.lockRoot); return Object.freeze({ lockPath: paths.lockPath }); } function safeSharedOwner(uid) { const current = process.getuid?.(); return ( uid === 0 || uid === current || (current === 0 && Number.isSafeInteger(uid) && uid > 0) ); } function openAndLock(paths) { if (!existsSync(paths.lockRoot)) { prepareSoulLockRoot({ dataRoot: paths.dataRoot }); } const before = lstatSync(paths.lockRoot); if ( before.isSymbolicLink() || !before.isDirectory() || (before.mode & 0o7777) !== 0o2770 ) fail("soul_lock_root_rejected"); const existed = existsSync(paths.lockPath); const fd = openSync( paths.lockPath, constants.O_RDWR | constants.O_CREAT | constants.O_NOFOLLOW, LOCK_MODE ); try { if (!existed) fchmodSync(fd, LOCK_MODE); const link = lstatSync(paths.lockPath); const opened = fstatSync(fd); if ( link.isSymbolicLink() || !link.isFile() || !opened.isFile() || link.dev !== opened.dev || link.ino !== opened.ino || opened.nlink !== 1 || (opened.mode & 0o777) !== LOCK_MODE || opened.gid !== before.gid || !safeSharedOwner(opened.uid) ) { fail("soul_lock_file_rejected"); } const locked = spawnSync( FLOCK_EXECUTABLE, [ "--exclusive", "--timeout", String(SOUL_LOCK_ACQUIRE_TIMEOUT_SECONDS), "3", ], { stdio: ["ignore", "ignore", "pipe", fd], encoding: "utf8", timeout: (SOUL_LOCK_ACQUIRE_TIMEOUT_SECONDS + 2) * 1_000, } ); if (locked.status === 1) fail("soul_lock_busy"); if (locked.status !== 0) fail("soul_lock_failed"); return fd; } catch (error) { closeSync(fd); throw error; } } /** * Hold the one Machine-external POSIX flock for a synchronous or async * callback. The child `flock(1)` applies LOCK_EX to an inherited duplicate of * the parent's open-file description, so the lock remains held until this * function closes the parent descriptor after the callback settles. */ export function withProfileSoulLock( profileId, fn, { dataRoot = SOUL_BRIDGE_DATA_ROOT } = {} ) { if (typeof fn !== "function") fail("soul_lock_callback_rejected"); const paths = soulProfilePaths(profileId, { dataRoot }); const fd = openAndLock(paths); let value; try { value = fn(paths); } catch (error) { closeSync(fd); throw error; } if (value && typeof value.then === "function") { return Promise.resolve(value).finally(() => closeSync(fd)); } closeSync(fd); return value; } function fsyncDirectory(path) { const fd = openSync(path, constants.O_RDONLY | constants.O_DIRECTORY); try { fsyncSync(fd); } finally { closeSync(fd); } } function assertSafeDirectory(path, expectedParent) { const info = lstatSync(path); if (info.isSymbolicLink() || !info.isDirectory()) fail("soul_parent_rejected"); if (realpathSync(path) !== path || dirname(path) !== expectedParent) { fail("soul_parent_rejected"); } return info; } function readSafeRegular(path, { maxBytes = JOURNAL_LIMIT } = {}) { const link = lstatSync(path); if (link.isSymbolicLink() || !link.isFile() || link.nlink !== 1) { fail("soul_file_type_rejected"); } const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW); try { const opened = fstatSync(fd); if ( !opened.isFile() || opened.dev !== link.dev || opened.ino !== link.ino || opened.nlink !== 1 || opened.size > maxBytes ) { fail("soul_file_descriptor_rejected"); } return { bytes: readFileSync(fd), stat: opened }; } finally { closeSync(fd); } } function readSharedStateFile(path, parent, maxBytes = JOURNAL_LIMIT) { const parentInfo = assertSharedStateDirectory(parent); const result = readSafeRegular(path, { maxBytes }); if ( (result.stat.mode & 0o777) !== STATE_FILE_MODE || result.stat.gid !== parentInfo.gid || !safeSharedOwner(result.stat.uid) ) { fail("soul_state_file_metadata_rejected"); } return result; } function assertSharedStateDirectory(path) { const info = lstatSync(path); const lockRoot = join(dirname(path), "soul-locks"); const lockInfo = lstatSync(lockRoot); if ( info.isSymbolicLink() || !info.isDirectory() || realpathSync(path) !== path || (info.mode & 0o7777) !== 0o2770 || lockInfo.isSymbolicLink() || !lockInfo.isDirectory() || realpathSync(lockRoot) !== lockRoot || (lockInfo.mode & 0o7777) !== 0o2770 || info.gid !== lockInfo.gid ) { fail("soul_state_root_rejected"); } return info; } function readCanonical(paths) { const profilesRoot = join(paths.dataRoot, "profiles"); const profileParent = assertSafeDirectory(profilesRoot, paths.dataRoot); const profile = assertSafeDirectory(paths.profileRoot, profilesRoot); if (profile.dev !== profileParent.dev) fail("soul_profile_volume_rejected"); if (!existsSync(paths.soulPath)) fail("canonical_file_absent"); const result = readSafeRegular(paths.soulPath, { maxBytes: 64 * 1024 }); const mode = result.stat.mode & 0o777; if (![0o600, 0o640].includes(mode) || !safeSharedOwner(result.stat.uid)) fail("soul_file_mode_rejected"); return result; } function atomicReplace(path, bytes, { mode, uid, gid }) { const parent = dirname(path); const parentBefore = lstatSync(parent); const temporary = join(parent, `.SOUL.${process.pid}.${randomUUID()}.tmp`); let fd; try { fd = openSync( temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, mode ); writeFileSync(fd, bytes); fchmodSync(fd, mode); if ( process.getuid?.() === 0 && Number.isSafeInteger(uid) && Number.isSafeInteger(gid) ) { fchownSync(fd, uid, gid); } fsyncSync(fd); closeSync(fd); fd = undefined; renameSync(temporary, path); fsyncDirectory(parent); const parentAfter = lstatSync(parent); if ( parentBefore.dev !== parentAfter.dev || parentBefore.ino !== parentAfter.ino || realpathSync(parent) !== parent ) { fail("soul_parent_changed_during_replace"); } const readback = readSafeRegular(path, { maxBytes: 64 * 1024 }); if ( sha256(readback.bytes) !== sha256(bytes) || (readback.stat.mode & 0o777) !== mode || readback.stat.uid !== uid || readback.stat.gid !== gid ) fail("soul_write_readback_mismatch"); return readback; } finally { if (fd !== undefined) closeSync(fd); if (existsSync(temporary)) unlinkSync(temporary); } } function atomicJson(path, value, mode = STATE_FILE_MODE) { const bytes = Buffer.from(`${JSON.stringify(value)}\n`, "utf8"); const parent = dirname(path); assertSharedStateDirectory(parent); const temporary = join(parent, `.${process.pid}.${randomUUID()}.tmp`); let fd; try { fd = openSync( temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, mode ); writeFileSync(fd, bytes); fchmodSync(fd, mode); fsyncSync(fd); closeSync(fd); fd = undefined; renameSync(temporary, path); fsyncDirectory(parent); const readback = readSharedStateFile(path, parent, JOURNAL_LIMIT); if (sha256(readback.bytes) !== sha256(bytes)) fail("soul_state_write_readback_mismatch"); } finally { if (fd !== undefined) closeSync(fd); if (existsSync(temporary)) unlinkSync(temporary); } } function readJson(path, maxBytes = JOURNAL_LIMIT) { if (!existsSync(path)) return null; try { return JSON.parse( readSharedStateFile(path, dirname(path), maxBytes).bytes.toString("utf8") ); } catch (error) { if (error instanceof FlySoulBridgeError) throw error; fail("soul_metadata_rejected"); } } function validProofMarker(marker, profileId) { return ( marker && Object.keys(marker).sort().join(",") === "effectiveSha256,generation,profileId,rawFileSha256,schemaVersion,state" && marker.schemaVersion === SOUL_PROOF_SCHEMA_VERSION && marker.state === SOUL_PROOF_STATE_PROVEN && marker.profileId === profileId && SHA256.test(marker.rawFileSha256 ?? "") && SHA256.test(marker.effectiveSha256 ?? "") && typeof marker.generation === "string" && marker.generation.length >= 8 && marker.generation.length <= 128 ); } export function readSoulProofMarker( profileId, { dataRoot = SOUL_BRIDGE_DATA_ROOT } = {} ) { const paths = soulProfilePaths(profileId, { dataRoot }); const marker = readJson(paths.proofPath, 8 * 1024); return validProofMarker(marker, profileId) ? Object.freeze({ ...marker }) : null; } function writeProofMarker( paths, { rawFileSha256, effectiveSha256, generation } ) { const marker = { schemaVersion: SOUL_PROOF_SCHEMA_VERSION, state: SOUL_PROOF_STATE_PROVEN, profileId: paths.profileId, rawFileSha256, effectiveSha256, generation, }; atomicJson(paths.proofPath, marker); return marker; } function markUnproven(paths, prior = null) { atomicJson(paths.proofPath, { schemaVersion: SOUL_PROOF_SCHEMA_VERSION, state: SOUL_PROOF_STATE_UNPROVEN, profileId: paths.profileId, rawFileSha256: SHA256.test(prior?.rawFileSha256 ?? "") ? prior.rawFileSha256 : null, effectiveSha256: SHA256.test(prior?.effectiveSha256 ?? "") ? prior.effectiveSha256 : null, generation: typeof prior?.generation === "string" ? prior.generation : null, }); } function runInstalledSoulLoader( paths, { python = PINNED_HERMES_PYTHON, environment } ) { const source = [ "import hashlib, json, sys", "from hermes_constants import get_hermes_home", "from agent.prompt_builder import load_soul_md", "value = load_soul_md()", "if not isinstance(value, str) or not value: raise RuntimeError('soul_loader_empty')", "raw = value.encode('utf-8')", "print(json.dumps({'effectiveSha256': hashlib.sha256(raw).hexdigest(), 'byteLength': len(raw), 'path': str(get_hermes_home() / 'SOUL.md')}))", ].join("\n"); const result = spawnSync(python, ["-c", source], { encoding: "utf8", env: environment, timeout: 25_000, maxBuffer: 16 * 1024, }); if (result.status !== 0) fail("soul_loader_proof_failed"); let parsed; try { parsed = JSON.parse(result.stdout); } catch { fail("soul_loader_proof_failed"); } if ( parsed.path !== paths.soulPath || !SHA256.test(parsed.effectiveSha256 ?? "") || !Number.isSafeInteger(parsed.byteLength) || parsed.byteLength < 1 ) { fail("soul_loader_proof_failed"); } return parsed; } function offlineEffectiveLoader(paths) { return runInstalledSoulLoader(paths, { environment: { HOME: paths.dataRoot, HERMES_HOME: paths.profileRoot, SELLABLE_HERMES_PROFILE_ID: paths.profileId, PYTHONDONTWRITEBYTECODE: "1", PATH: "/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin", }, }); } function gatewayEffectiveLoader( gateway, { procRoot, dataRoot, pinnedHermesLauncher, pinnedHermesPython } ) { return (paths) => { const result = runInstalledSoulLoader(paths, { python: pinnedHermesPython, environment: { HOME: paths.dataRoot, // The attested Gateway argv is `hermes --profile <profileId>`. Hermes' // CLI pre-parser turns that into this exact profile home before its // prompt modules load. Supply the already-proven result directly so a // SOUL read executes the installed `load_soul_md()` without importing // the entire CLI command graph on every dashboard refresh. HERMES_HOME: paths.profileRoot, HERMES_PROFILE: gateway.environment.HERMES_PROFILE, SELLABLE_HERMES_PROFILE_ID: gateway.environment.SELLABLE_HERMES_PROFILE_ID, HERMES_CONFIG_PATH: gateway.environment.HERMES_CONFIG_PATH, SELLABLE_AGENT_RUNTIME: gateway.environment.SELLABLE_AGENT_RUNTIME, PYTHONDONTWRITEBYTECODE: "1", PATH: "/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin", }, }); revalidateLiveHermesGateway(gateway, { procRoot, dataRoot, pinnedHermesLauncher, pinnedHermesPython, }); return result; }; } function proveCanonical(paths, raw, effectiveLoader) { const effective = effectiveLoader(paths); const validated = assertSoulArtifact(raw.bytes, { enforceEffectiveLimit: false, }); if ( effective.path !== paths.soulPath || effective.effectiveSha256 !== validated.effectiveSha256 || effective.byteLength !== validated.effectiveByteLength ) { fail("soul_loader_effective_mismatch"); } return { rawFileSha256: validated.rawFileSha256, effectiveSha256: effective.effectiveSha256, }; } function safelyRemoveLegacyRoot(paths) { if (!existsSync(paths.legacyRootSoulPath)) return null; let legacy; try { legacy = readSafeRegular(paths.legacyRootSoulPath, { maxBytes: 64 * 1024, }); } catch (error) { if (error?.code !== "EACCES" && error?.code !== "EPERM") throw error; const blocked = lstatSync(paths.legacyRootSoulPath); if ( blocked.isSymbolicLink() || !blocked.isFile() || blocked.nlink !== 1 || blocked.size > 64 * 1024 || (blocked.mode & 0o022) !== 0 || !safeSharedOwner(blocked.uid) ) { fail("soul_legacy_root_rejected"); } // The exact live Gateway and canonical profile SOUL were already proven // before this cleanup runs. A root-owned 0600 legacy copy cannot be read // by the deliberately de-privileged bridge, but the Hermes-owned data // directory still permits removing that non-authoritative entry safely. try { unlinkSync(paths.legacyRootSoulPath); fsyncDirectory(paths.dataRoot); } catch (unlinkError) { if (unlinkError?.code !== "EACCES" && unlinkError?.code !== "EPERM") { throw unlinkError; } } return null; } if ((legacy.stat.mode & 0o022) !== 0 || !safeSharedOwner(legacy.stat.uid)) fail("soul_legacy_root_rejected"); const digest = sha256(legacy.bytes); try { unlinkSync(paths.legacyRootSoulPath); fsyncDirectory(paths.dataRoot); return { legacyRootSha256: digest, legacyRootRemovalPending: false }; } catch (error) { if (error?.code === "EACCES" || error?.code === "EPERM") { return { legacyRootSha256: digest, legacyRootRemovalPending: true }; } throw error; } } function writeJournal(paths, journal) { atomicJson(paths.journalPath, journal, STATE_FILE_MODE); } function canonicalBase64(value) { if ( typeof value !== "string" || value.length < 1 || value.length > 88 * 1024 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test( value ) ) { return null; } const bytes = Buffer.from(value, "base64"); return bytes.toString("base64") === value ? bytes : null; } function validJournal(journal, paths) { const prior = canonicalBase64(journal?.priorBase64); const next = canonicalBase64(journal?.nextBase64); return ( journal && journal.schemaVersion === SOUL_JOURNAL_SCHEMA_VERSION && journal.profileId === paths.profileId && ["prepared", "renamed", "proven", "acked"].includes(journal.phase) && typeof journal.generation === "string" && journal.generation.length >= 8 && journal.generation.length <= 128 && SHA256.test(journal.priorRawFileSha256 ?? "") && SHA256.test(journal.nextRawFileSha256 ?? "") && prior && next && prior.byteLength > 0 && prior.byteLength <= 64 * 1024 && next.byteLength > 0 && next.byteLength <= 64 * 1024 && sha256(prior) === journal.priorRawFileSha256 && sha256(next) === journal.nextRawFileSha256 && [0o600, 0o640].includes(journal.priorMode) && Number.isSafeInteger(journal.priorUid) && journal.priorUid >= 0 && Number.isSafeInteger(journal.priorGid) && journal.priorGid >= 0 && (journal.priorMarker === null || validProofMarker(journal.priorMarker, paths.profileId)) && (journal.phase === "prepared" || journal.phase === "renamed" || SHA256.test(journal.effectiveSha256 ?? "")) ); } function removeJournal(paths) { if (!existsSync(paths.journalPath)) return; assertSharedStateDirectory(paths.journalRoot); unlinkSync(paths.journalPath); fsyncDirectory(paths.journalRoot); } function rollback(paths, journal, effectiveLoader) { try { const restored = atomicReplace( paths.soulPath, Buffer.from(journal.priorBase64, "base64"), { mode: journal.priorMode, uid: journal.priorUid, gid: journal.priorGid, } ); const proof = proveCanonical(paths, restored, effectiveLoader); const priorMarker = journal.priorMarker; if ( validProofMarker(priorMarker, paths.profileId) && priorMarker.rawFileSha256 === proof.rawFileSha256 && priorMarker.effectiveSha256 === proof.effectiveSha256 ) { atomicJson(paths.proofPath, priorMarker); } else { writeProofMarker(paths, { ...proof, generation: `rollback-${journal.generation}`, }); } removeJournal(paths); return { ok: false, code: "apply_failed_rolled_back", rawFileSha256: proof.rawFileSha256, }; } catch { markUnproven(paths, journal.priorMarker); let observedRawFileSha256 = null; try { observedRawFileSha256 = sha256(readCanonical(paths).bytes); } catch { // The unproven marker is the durable terminal when even readback fails. } return { ok: false, code: "rollback_unproven", observedRawFileSha256, }; } } function recoverJournal(paths, acknowledgedGeneration, effectiveLoader) { const journal = readJson(paths.journalPath); if (!journal) return { pending: null }; if (!validJournal(journal, paths)) fail("soul_journal_rejected"); let current = readCanonical(paths); let currentHash = sha256(current.bytes); if (journal.phase === "acked") { if (currentHash !== journal.nextRawFileSha256) fail("soul_recovery_conflict"); removeJournal(paths); return { pending: null }; } if (journal.phase === "prepared") { if (currentHash === journal.priorRawFileSha256) { removeJournal(paths); return { pending: null }; } if (currentHash !== journal.nextRawFileSha256) fail("soul_recovery_conflict"); journal.phase = "renamed"; writeJournal(paths, journal); } if (journal.phase === "renamed") { if (currentHash === journal.priorRawFileSha256) { removeJournal(paths); return { pending: null }; } if (currentHash !== journal.nextRawFileSha256) fail("soul_recovery_conflict"); try { const proof = proveCanonical(paths, current, effectiveLoader); journal.phase = "proven"; journal.effectiveSha256 = proof.effectiveSha256; writeProofMarker(paths, { ...proof, generation: journal.generation }); writeJournal(paths, journal); } catch { return { terminal: rollback(paths, journal, effectiveLoader) }; } } current = readCanonical(paths); currentHash = sha256(current.bytes); if (currentHash !== journal.nextRawFileSha256) fail("soul_recovery_conflict"); const marker = readJson(paths.proofPath, 8 * 1024); if ( !validProofMarker(marker, paths.profileId) || marker.generation !== journal.generation || marker.rawFileSha256 !== journal.nextRawFileSha256 ) { // Phase 207 execution 2f07cd28 retained a proven save journal after a // later loader failure invalidated the marker. Recover from the actual // canonical file and installed loader, never from journal claims alone. const proof = proveCanonical(paths, current, effectiveLoader); if (proof.effectiveSha256 !== journal.effectiveSha256) fail("soul_recovery_conflict"); writeProofMarker(paths, { ...proof, generation: journal.generation }); } if (acknowledgedGeneration === journal.generation) { journal.phase = "acked"; writeJournal(paths, journal); removeJournal(paths); return { pending: null }; } return { pending: journal }; } function assertRequest(request) { if (!request || typeof request !== "object" || Array.isArray(request)) { fail("soul_request_rejected"); } if (request.operation === "read") { const allowed = new Set(["operation", "acknowledgedGeneration"]); if (Object.keys(request).some((key) => !allowed.has(key))) fail("soul_request_rejected"); } else if (request.operation === "save") { const allowed = new Set([ "operation", "content", "expectedRawFileSha256", "acknowledgedGeneration", ]); if ( Object.keys(request).some((key) => !allowed.has(key)) || typeof request.content !== "string" || !SHA256.test(request.expectedRawFileSha256 ?? "") ) { fail("soul_request_rejected"); } } else { fail("soul_operation_rejected"); } if ( request.acknowledgedGeneration !== undefined && (typeof request.acknowledgedGeneration !== "string" || request.acknowledgedGeneration.length < 8 || request.acknowledgedGeneration.length > 128) ) { fail("soul_request_rejected"); } return request; } function executeLocked( request, paths, effectiveLoader, { gatewayProven = false, allowLegacyRemoval = false } = {} ) { const recovery = recoverJournal( paths, request.acknowledgedGeneration, effectiveLoader ); if (recovery.terminal) return { ...recovery.terminal, gatewayProven }; if (request.operation === "read") { const raw = readCanonical(paths); const validated = validateSoulArtifact(raw.bytes, { enforceEffectiveLimit: false, }); if (!validated.ok) { markUnproven(paths); return { ok: false, code: "live_invalid_unmirrorable", validationCode: validated.code, rawFileSha256: sha256(raw.bytes), byteLength: raw.bytes.byteLength, proofStatus: SOUL_PROOF_STATE_UNPROVEN, gatewayProven, }; } let proof; try { proof = proveCanonical(paths, raw, effectiveLoader); } catch (error) { if ( error instanceof FlySoulBridgeError && error.code === "soul_loader_effective_mismatch" ) { markUnproven(paths); return { ok: false, code: "live_invalid_unmirrorable", validationCode: error.code, rawFileSha256: validated.rawFileSha256, byteLength: validated.byteLength, proofStatus: SOUL_PROOF_STATE_UNPROVEN, gatewayProven, }; } // An unavailable loader supplies no new integrity evidence. Return its // failure without erasing an earlier proof; the gateway still checks // the canonical file hash, and the next read must prove the loader again. throw error; } const marker = readJson(paths.proofPath, 8 * 1024); const generation = recovery.pending?.generation ?? (validProofMarker(marker, paths.profileId) && marker.rawFileSha256 === proof.rawFileSha256 ? marker.generation : `observe-${randomUUID()}`); writeProofMarker(paths, { ...proof, generation }); const legacyRoot = allowLegacyRemoval ? safelyRemoveLegacyRoot(paths) : null; return { ok: true, status: recovery.pending ? "proven_pending_mirror" : "proven", content: validated.content, rawFileSha256: proof.rawFileSha256, effectiveSha256: proof.effectiveSha256, byteLength: validated.byteLength, characterCount: validated.characterCount, generation, pendingMirror: Boolean(recovery.pending), gatewayProven, observedAt: new Date().toISOString(), ...(legacyRoot ?? {}), }; } const next = assertSoulArtifact(request.content); const current = readCanonical(paths); const currentValidation = validateSoulArtifact(current.bytes, { enforceEffectiveLimit: false, }); if (!currentValidation.ok) { markUnproven(paths); return { ok: false, code: "live_invalid_unmirrorable", validationCode: currentValidation.code, rawFileSha256: sha256(current.bytes), byteLength: current.bytes.byteLength, proofStatus: SOUL_PROOF_STATE_UNPROVEN, gatewayProven, }; } if (recovery.pending) { if (next.rawFileSha256 !== recovery.pending.nextRawFileSha256) { return { ok: false, code: "soul_mirror_backlog", observedRawFileSha256: currentValidation.rawFileSha256, generation: recovery.pending.generation, gatewayProven, }; } return { ok: true, status: "applied_pending_mirror", rawFileSha256: currentValidation.rawFileSha256, effectiveSha256: recovery.pending.effectiveSha256, generation: recovery.pending.generation, pendingMirror: true, gatewayProven, }; } if (currentValidation.rawFileSha256 !== request.expectedRawFileSha256) { return { ok: false, code: "soul_conflict", observedRawFileSha256: currentValidation.rawFileSha256, gatewayProven, }; } if (next.rawFileSha256 === currentValidation.rawFileSha256) { const proof = proveCanonical(paths, current, effectiveLoader); const marker = readJson(paths.proofPath, 8 * 1024); const generation = validProofMarker(marker, paths.profileId) && marker.rawFileSha256 === proof.rawFileSha256 ? marker.generation : `noop-${randomUUID()}`; writeProofMarker(paths, { ...proof, generation }); return { ok: true, status: "noop", ...proof, generation, pendingMirror: false, gatewayProven, }; } const generation = randomUUID(); const priorMarker = readJson(paths.proofPath, 8 * 1024); const journal = { schemaVersion: SOUL_JOURNAL_SCHEMA_VERSION, profileId: paths.profileId, generation, phase: "prepared", priorRawFileSha256: currentValidation.rawFileSha256, nextRawFileSha256: next.rawFileSha256, priorBase64: current.bytes.toString("base64"), nextBase64: next.bytes.toString("base64"), priorMode: current.stat.mode & 0o777, priorUid: current.stat.uid, priorGid: current.stat.gid, priorMarker: validProofMarker(priorMarker, paths.profileId) ? priorMarker : null, effectiveSha256: null, }; writeJournal(paths, journal); const replaced = atomicReplace(paths.soulPath, next.bytes, { mode: current.stat.mode & 0o777, uid: current.stat.uid, gid: current.stat.gid, }); journal.phase = "renamed"; writeJournal(paths, journal); let proof; try { proof = proveCanonical(paths, replaced, effectiveLoader); } catch { return { ...rollback(paths, journal, effectiveLoader), gatewayProven }; } journal.phase = "proven"; journal.effectiveSha256 = proof.effectiveSha256; writeProofMarker(paths, { ...proof, generation }); writeJournal(paths, journal); const legacyRoot = allowLegacyRemoval ? safelyRemoveLegacyRoot(paths) : null; return { ok: true, status: "applied_pending_mirror", ...proof, generation, pendingMirror: true, gatewayProven, ...(legacyRoot ?? {}), }; } /** Internal/test seam. Browser/MCP callers only reach the fixed read/save CLI. */ function executeSoulBridgeOperationWithPolicy( request, { profileId = process.env.SELLABLE_HERMES_PROFILE_ID, dataRoot = SOUL_BRIDGE_DATA_ROOT, effectiveLoader = offlineEffectiveLoader, lockHeld = false, } = {}, { gatewayProven = false, allowLegacyRemoval = false, beforeOperation = null, } = {} ) { assertRequest(request); const id = assertProfileId(profileId); if (beforeOperation !== null && typeof beforeOperation !== "function") { fail("soul_gateway_preflight_rejected"); } const run = (paths) => { beforeOperation?.(paths); return executeLocked(request, paths, effectiveLoader, { gatewayProven: gatewayProven === true, allowLegacyRemoval: gatewayProven === true && allowLegacyRemoval === true, }); }; if (lockHeld) return run(soulProfilePaths(id, { dataRoot })); return withProfileSoulLock(id, run, { dataRoot }); } export function executeSoulBridgeOperation(request, options = {}) { return executeSoulBridgeOperationWithPolicy(request, options, { gatewayProven: false, allowLegacyRemoval: false, }); } export function executeExternalSoulBridgeOperation( request, { procRoot = "/proc", dataRoot = SOUL_BRIDGE_DATA_ROOT, pinnedHermesLauncher = PINNED_HERMES_LAUNCHER, pinnedHermesPython = PINNED_HERMES_PYTHON, callerProfileId = /** @type {string | null | undefined} */ ( process.env.SELLABLE_HERMES_PROFILE_ID ), effectiveLoader: injectedEffectiveLoader = /** @type {((paths: any, gateway?: any) => any) | null} */ ( null ), } = {} ) { const gateway = discoverLiveHermesGateway({ procRoot, dataRoot, pinnedHermesLauncher, pinnedHermesPython, }); if (callerProfileId && callerProfileId !== gateway.profileId) { fail("soul_gateway_caller_profile_mismatch"); } const revalidate = () => revalidateLiveHermesGateway(gateway, { procRoot, dataRoot, pinnedHermesLauncher, pinnedHermesPython, }); let effectiveLoader; if (injectedEffectiveLoader === null) { effectiveLoader = gatewayEffectiveLoader(gateway, { procRoot, dataRoot, pinnedHermesLauncher, pinnedHermesPython, }); } else { if (typeof injectedEffectiveLoader !== "function") { fail("soul_gateway_loader_rejected"); } effectiveLoader = (paths) => { const result = injectedEffectiveLoader(paths, gateway); revalidate(); return result; }; } const result = executeSoulBridgeOperationWithPolicy( request, { profileId: gateway.profileId, dataRoot, effectiveLoader, }, { gatewayProven: true, allowLegacyRemoval: true, beforeOperation: revalidate, } ); return Object.freeze({ ...result, profileId: gateway.profileId, gatewayPid: gateway.pid, gatewayStartTicks: gateway.startTicks, }); } /** Lifecycle-only proof initializer; deliberately omits SOUL content. */ export function observeProfileSoul( profileId, { dataRoot = SOUL_BRIDGE_DATA_ROOT, effectiveLoader = offlineEffectiveLoader, } = {} ) { const result = executeSoulBridgeOperationWithPolicy( { operation: "read" }, { profileId, dataRoot, effectiveLoader, }, { gatewayProven: false, allowLegacyRemoval: false, } ); const { content: _content, ...receipt } = result; return Object.freeze(receipt); } export async function runSoulBridgeCli() { try { if (process.argv.length !== 2) fail("soul_request_rejected"); const chunks = []; let size = 0; for await (const chunk of process.stdin) { size += chunk.length; if (size > SOUL_BRIDGE_REQUEST_LIMIT) fail("soul_request_too_large"); chunks.push(chunk); } let request; try { request = JSON.parse(Buffer.concat(chunks).toString("utf8")); } catch { fail("soul_request_rejected"); } const result = executeExternalSoulBridgeOperation(request); process.stdout.write(`${JSON.stringify(result)}\n`); process.exitCode = result.ok ? 0 : 2; } catch (error) { const code = error instanceof FlySoulBridgeError || error instanceof SoulArtifactValidationError ? error.code : "soul_bridge_failed"; process.stdout.write(`${JSON.stringify({ ok: false, code })}\n`); process.exitCode = 2; } } const invokedPath = process.argv[1] ? resolve(process.argv[1]) : ""; if (invokedPath === resolve(fileURLToPath(import.meta.url))) { await runSoulBridgeCli(); }