@sellable/install
Version:
One-command installer for Sellable MCP in Claude Code, Codex, and Hermes
273 lines (258 loc) • 11.2 kB
JavaScript
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;
}
}