UNPKG

@sellable/install

Version:

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

273 lines (258 loc) • 11.2 kB
import { createHash, randomUUID } from "node:crypto"; import { chmodSync, chownSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync, } from "node:fs"; import { dirname, isAbsolute, join, relative, resolve } from "node:path"; import { HERMES_AGENT_BRIDGE_CONTRACT, installHermesAgentBridge, verifyHermesAgentBridge, } from "./hermes-bridge.mjs"; import { EXTERNAL_HERMES_ENTRYPOINT, EXTERNAL_MCP_ENTRYPOINT, EXTERNAL_RUNTIME_CLOSURE_ROOT, EXTERNAL_RUNTIME_MANIFEST_PATH, observeExternalRuntimeClosure, } from "./runtime-helper.mjs"; const MANIFEST_VERSION = "sellable-agent-external-runtime-closure/v1"; const MCP_PACKAGE = "@sellable/mcp"; const MCP_VERSION = "0.1.957"; const SAFE_COMPONENT = /^[A-Za-z0-9@._+-]+$/; const HERMES_EXCLUDES = new Set([".git", ".playwright", "node_modules"]); const sha256 = (value) => createHash("sha256").update(value).digest("hex"); function mapped(pathRoot, absolutePath) { const root = realpathSync(resolve(pathRoot)); const target = root === "/" ? resolve(absolutePath) : join(root, absolutePath.replace(/^\/+/, "")); const confined = relative(root, target); if (!isAbsolute(absolutePath) || confined.startsWith("..") || isAbsolute(confined)) { throw new Error("external runtime build target rejected"); } return target; } function trustedDirectory(pathValue) { const resolved = realpathSync(resolve(pathValue)); const link = lstatSync(resolved); if ( link.isSymbolicLink() || !link.isDirectory() || (link.mode & 0o022) !== 0 ) throw new Error("external runtime source rejected"); return resolved; } function copyFile(source, target, transform = (bytes) => bytes) { const link = lstatSync(source); if (!link.isFile() || link.isSymbolicLink()) throw new Error("external runtime source file rejected"); const bytes = transform(readFileSync(source)); const mode = (link.mode & 0o111) === 0 ? 0o644 : 0o755; writeFileSync(target, bytes, { flag: "wx", mode }); chmodSync(target, mode); } function hermesScript(bytes) { const source = bytes.toString("utf8"); const newline = source.indexOf("\n"); const first = newline < 0 ? source : source.slice(0, newline); if (!first.startsWith("#!/opt/hermes/.venv/bin/python")) return bytes; const rest = newline < 0 ? "" : source.slice(newline); return Buffer.from(`#!${EXTERNAL_RUNTIME_CLOSURE_ROOT}/hermes/venv/bin/python3${rest}`); } function copyTree({ sourceRoot, targetRoot, kind, logicalRoot = "" }) { for (const name of readdirSync(sourceRoot).sort()) { if (!SAFE_COMPONENT.test(name)) throw new Error("external runtime source name rejected"); if (kind === "hermes" && HERMES_EXCLUDES.has(name)) continue; if (kind === "mcp-dependencies" && [".bin", ".package-lock.json"].includes(name)) continue; if (kind === "mcp-dependencies" && logicalRoot === "" && name === "@sellable") { const scopedSource = join(sourceRoot, name); const scopedTarget = join(targetRoot, name); mkdirSync(scopedTarget, { mode: 0o755 }); for (const scopedName of readdirSync(scopedSource).sort()) { if (scopedName === "mcp" || scopedName === "install") continue; if (!SAFE_COMPONENT.test(scopedName)) throw new Error("external runtime source name rejected"); const nestedSource = join(scopedSource, scopedName); const nestedTarget = join(scopedTarget, scopedName); const nested = lstatSync(nestedSource); if (!nested.isDirectory() || nested.isSymbolicLink()) { throw new Error("external runtime dependency rejected"); } mkdirSync(nestedTarget, { mode: 0o755 }); copyTree({ sourceRoot: nestedSource, targetRoot: nestedTarget, kind, logicalRoot: `${name}/${scopedName}`, }); } if (readdirSync(scopedTarget).length === 0) rmSync(scopedTarget, { recursive: true }); continue; } const outputName = kind === "hermes" && logicalRoot === "" && name === ".venv" ? "venv" : name; const source = join(sourceRoot, name); const target = join(targetRoot, outputName); const logicalPath = logicalRoot ? `${logicalRoot}/${outputName}` : outputName; const link = lstatSync(source); if (link.isSymbolicLink()) { if (kind !== "hermes") throw new Error("external runtime dependency symlink rejected"); if (logicalPath === "venv/lib64") continue; if (!["venv/bin/python", "venv/bin/python3", "venv/bin/python3.13"].includes(logicalPath)) { throw new Error("external Hermes symlink rejected"); } copyFile(realpathSync(source), target); continue; } if (link.isDirectory()) { mkdirSync(target, { mode: 0o755 }); copyTree({ sourceRoot: source, targetRoot: target, kind, logicalRoot: logicalPath }); continue; } if (!link.isFile()) throw new Error("external runtime special source rejected"); copyFile(source, target, kind === "hermes" && logicalPath.startsWith("venv/bin/") ? hermesScript : undefined); } } function normalizeTree(root, ownerUid, ownerGid) { const directories = []; const files = []; const visit = (directory, logicalDirectory) => { for (const name of readdirSync(directory).sort()) { const pathValue = join(directory, name); const logicalPath = logicalDirectory ? `${logicalDirectory}/${name}` : name; const link = lstatSync(pathValue); if (link.isSymbolicLink()) throw new Error("external runtime output symlink rejected"); if (link.isDirectory()) { visit(pathValue, logicalPath); chmodSync(pathValue, 0o555); chownSync(pathValue, ownerUid, ownerGid); directories.push({ path: logicalPath, mode: 0o555 }); } else if (link.isFile()) { const mode = (link.mode & 0o111) === 0 ? 0o444 : 0o555; chmodSync(pathValue, mode); chownSync(pathValue, ownerUid, ownerGid); files.push({ path: logicalPath, mode, digest: sha256(readFileSync(pathValue)) }); } else { throw new Error("external runtime output special file rejected"); } } }; visit(root, ""); chmodSync(root, 0o555); chownSync(root, ownerUid, ownerGid); return { directories: directories.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0), files: files.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0), }; } function packageIdentity(root) { const parsed = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); if (parsed?.name !== MCP_PACKAGE || parsed.version !== MCP_VERSION) { throw new Error("external MCP package identity rejected"); } } function bridgeContract(input) { return input.disposableFixture === true && input.hermesBridgeContract ? input.hermesBridgeContract : HERMES_AGENT_BRIDGE_CONTRACT; } function currentClosure(input, pathRoot, ownerUid, ownerGid) { const observed = observeExternalRuntimeClosure({ pathRoot, manifestPath: EXTERNAL_RUNTIME_MANIFEST_PATH, expectedClosureRoot: EXTERNAL_RUNTIME_CLOSURE_ROOT, trustRoot: dirname(EXTERNAL_RUNTIME_CLOSURE_ROOT), ownerUid, ownerGid, expectedEntrypoints: { mcp: EXTERNAL_MCP_ENTRYPOINT, hermes: EXTERNAL_HERMES_ENTRYPOINT }, }); const root = mapped(pathRoot, EXTERNAL_RUNTIME_CLOSURE_ROOT); packageIdentity(join(root, "mcp")); verifyHermesAgentBridge({ sourceRoot: join(root, "hermes"), contract: bridgeContract(input), }); return { ...observed, reused: true }; } export function buildExternalRuntimeClosure(input = {}) { const pathRoot = resolve(input.pathRoot ?? "/"); if (!isAbsolute(pathRoot) || !existsSync(pathRoot)) throw new Error("external runtime path root rejected"); if (pathRoot !== "/" && input.disposableFixture !== true) { throw new Error("external runtime live root rejected"); } const ownerUid = input.ownerUid ?? process.getuid?.() ?? 0; const ownerGid = input.ownerGid ?? process.getgid?.() ?? 0; if (!Number.isSafeInteger(ownerUid) || ownerUid < 0 || !Number.isSafeInteger(ownerGid) || ownerGid < 0) { throw new Error("external runtime owner rejected"); } const mcpPackageRoot = trustedDirectory(input.mcpPackageRoot); const mcpNodeModulesRoot = trustedDirectory(input.mcpNodeModulesRoot); const hermesSourceRoot = trustedDirectory(input.hermesSourceRoot); packageIdentity(mcpPackageRoot); const target = mapped(pathRoot, EXTERNAL_RUNTIME_CLOSURE_ROOT); if (existsSync(target)) return currentClosure(input, pathRoot, ownerUid, ownerGid); const trustRoot = dirname(target); if (!existsSync(trustRoot)) mkdirSync(trustRoot, { recursive: true, mode: 0o755 }); const staging = join(trustRoot, `.external-runtime-${randomUUID()}`); mkdirSync(staging, { mode: 0o755 }); try { const mcpRoot = join(staging, "mcp"); const dependencyRoot = join(mcpRoot, "node_modules"); const hermesRoot = join(staging, "hermes"); mkdirSync(mcpRoot, { mode: 0o755 }); mkdirSync(dependencyRoot, { mode: 0o755 }); mkdirSync(hermesRoot, { mode: 0o755 }); copyTree({ sourceRoot: mcpPackageRoot, targetRoot: mcpRoot, kind: "mcp" }); copyTree({ sourceRoot: mcpNodeModulesRoot, targetRoot: dependencyRoot, kind: "mcp-dependencies" }); mkdirSync(join(mcpRoot, "bin"), { mode: 0o755 }); writeFileSync( join(mcpRoot, "bin", "sellable-mcp"), `#!/bin/sh\nexec /usr/bin/node '${EXTERNAL_RUNTIME_CLOSURE_ROOT}/mcp/dist/index.js' "$@"\n`, { mode: 0o755, flag: "wx" }, ); copyTree({ sourceRoot: hermesSourceRoot, targetRoot: hermesRoot, kind: "hermes" }); installHermesAgentBridge({ sourceRoot: hermesRoot, contract: bridgeContract(input) }); const mcp = normalizeTree(mcpRoot, ownerUid, ownerGid); const hermes = normalizeTree(hermesRoot, ownerUid, ownerGid); const manifest = { version: MANIFEST_VERSION, closureRoot: EXTERNAL_RUNTIME_CLOSURE_ROOT, roots: [ { kind: "mcp", entrypoint: "bin/sellable-mcp", rootMode: 0o555, ...mcp }, { kind: "hermes", entrypoint: "venv/bin/hermes", rootMode: 0o555, ...hermes }, ], }; writeFileSync(join(staging, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o444, flag: "wx", }); chmodSync(join(staging, "manifest.json"), 0o444); chownSync(join(staging, "manifest.json"), ownerUid, ownerGid); chownSync(staging, ownerUid, ownerGid); renameSync(staging, target); chmodSync(target, 0o555); return { ...currentClosure(input, pathRoot, ownerUid, ownerGid), reused: false }; } catch (error) { if (existsSync(staging)) { const makeWritable = (pathValue) => { const link = lstatSync(pathValue); if (link.isDirectory()) { chmodSync(pathValue, 0o755); for (const name of readdirSync(pathValue)) makeWritable(join(pathValue, name)); } else if (link.isFile()) chmodSync(pathValue, 0o644); }; makeWritable(staging); rmSync(staging, { recursive: true }); } throw error; } }