UNPKG

@sellable/install

Version:

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

152 lines (144 loc) • 6.55 kB
#!/usr/bin/env node import { spawn, spawnSync } from "node:child_process"; import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs"; import { join } from "node:path"; import { buildRuntimeBoundaryContract, buildRuntimeBoundaryLaunchSpec, buildRuntimeBoundaryNftProgram, expectedRuntimeCgroup, inspectRuntimeBoundaryInputs, runtimeBoundaryNftRulesMatch, } from "../lib/sellable-agent/runtime-boundary.mjs"; const CONFIG_PATH = "/etc/sellable-agent/runtime-boundary.json"; const SECRET = /xox[bap]-|sat_[A-Za-z0-9_-]+|asec\.v1\.|access[_-]?token|refresh[_-]?token/i; function fixedRootConfig() { const link = lstatSync(CONFIG_PATH); if (link.isSymbolicLink() || !link.isFile() || link.uid !== 0 || link.gid !== 0 || (link.mode & 0o777) !== 0o400) { throw new Error("runtime boundary config rejected"); } const bytes = readFileSync(CONFIG_PATH, "utf8"); if (bytes.length > 64 * 1024 || SECRET.test(bytes)) throw new Error("runtime boundary config rejected"); const built = buildRuntimeBoundaryContract(JSON.parse(bytes)); if (!built.ok) throw new Error("runtime boundary contract rejected"); return built.contract; } function provisionCgroup(contract) { const parent = contract.paths.cgroupRoot; const systemRoot = "/sys/fs/cgroup"; const rootInfo = lstatSync(systemRoot); if (rootInfo.isSymbolicLink() || !rootInfo.isDirectory()) throw new Error("cgroup v2 root rejected"); const controllers = readFileSync(join(systemRoot, "cgroup.controllers"), "utf8").trim().split(/\s+/); if (!["cpu", "memory", "pids"].every((name) => controllers.includes(name))) { throw new Error("required cgroup controllers unavailable"); } const requiredControllers = ["cpu", "memory", "pids"]; const enableControllers = (path) => { const controlPath = join(path, "cgroup.subtree_control"); const enabled = new Set(readFileSync(controlPath, "utf8").trim().split(/\s+/).filter(Boolean)); const missing = requiredControllers.filter((name) => !enabled.has(name)); if (missing.length) writeFileSync(controlPath, `${missing.map((name) => `+${name}`).join(" ")}\n`); const observed = new Set(readFileSync(controlPath, "utf8").trim().split(/\s+/).filter(Boolean)); if (requiredControllers.some((name) => !observed.has(name))) throw new Error("cgroup controller enablement failed"); }; enableControllers(systemRoot); if (!existsSync(parent)) mkdirSync(parent, { mode: 0o755 }); const parentInfo = lstatSync(parent); if (parentInfo.isSymbolicLink() || !parentInfo.isDirectory() || parentInfo.uid !== 0 || (parentInfo.mode & 0o022) !== 0) { throw new Error("runtime cgroup parent rejected"); } enableControllers(parent); const expected = expectedRuntimeCgroup(contract); if (!existsSync(expected.path)) mkdirSync(expected.path, { mode: 0o755 }); const groupInfo = lstatSync(expected.path); if (groupInfo.isSymbolicLink() || !groupInfo.isDirectory() || groupInfo.uid !== 0 || (groupInfo.mode & 0o022) !== 0) { throw new Error("runtime cgroup rejected"); } if (readFileSync(join(expected.path, "cgroup.procs"), "utf8").trim()) { throw new Error("runtime cgroup still has processes"); } for (const [name, value] of Object.entries(expected.files)) { writeFileSync(join(expected.path, name), `${value}\n`); if (readFileSync(join(expected.path, name), "utf8").trim() !== value) throw new Error("runtime cgroup limit drift"); } writeFileSync(join(expected.path, "cgroup.procs"), `${process.pid}\n`); } function provisionFirewall(contract) { const policy = buildRuntimeBoundaryNftProgram(contract); if (!policy.ok) throw new Error("runtime firewall contract rejected"); const listed = spawnSync(contract.paths.nftPath, ["-j", "list", "tables"], { encoding: "utf8", env: { PATH: "/usr/sbin:/usr/bin:/sbin:/bin" }, timeout: 10_000, }); if (listed.status !== 0) throw new Error("runtime firewall inventory failed"); let inventory; try { inventory = JSON.parse(listed.stdout); } catch { throw new Error("runtime firewall inventory rejected"); } const exists = Array.isArray(inventory?.nftables) && inventory.nftables.some((entry) => entry?.table?.family === "inet" && entry.table.name === policy.table); const program = `${exists ? `delete table inet ${policy.table}\n` : ""}${policy.program}`; const applied = spawnSync(contract.paths.nftPath, policy.applyArgs, { input: program, encoding: "utf8", env: { PATH: "/usr/sbin:/usr/bin:/sbin:/bin" }, timeout: 10_000, maxBuffer: 64 * 1024, }); if (applied.status !== 0 || SECRET.test(`${applied.stdout}${applied.stderr}`)) { throw new Error("runtime firewall apply failed"); } const observed = spawnSync(contract.paths.nftPath, ["list", "table", "inet", policy.table], { encoding: "utf8", env: { PATH: "/usr/sbin:/usr/bin:/sbin:/bin" }, timeout: 10_000, }); if (observed.status !== 0 || !runtimeBoundaryNftRulesMatch(contract, observed.stdout)) { throw new Error("runtime firewall readback failed"); } } async function run() { if (process.argv.length !== 2 || process.getuid?.() !== 0 || process.getgid?.() !== 0) { throw new Error("runtime launcher identity rejected"); } for (const fd of [8, 9]) { const link = lstatSync(`/proc/self/fd/${fd}`); if (!link.isSymbolicLink()) throw new Error("runtime Slack descriptor rejected"); } const contract = fixedRootConfig(); if (!inspectRuntimeBoundaryInputs(contract).ok) throw new Error("runtime boundary inputs rejected"); provisionCgroup(contract); provisionFirewall(contract); const spec = buildRuntimeBoundaryLaunchSpec(contract); if (!spec.ok) throw new Error("runtime launch spec rejected"); const stdio = Array(10).fill("ignore"); stdio[1] = "inherit"; stdio[2] = "inherit"; stdio[8] = 8; stdio[9] = 9; const child = spawn(spec.command, spec.args, { env: spec.env, shell: false, stdio, }); const forwardTerm = () => { if (!child.killed) child.kill("SIGTERM"); }; const forwardInt = () => { if (!child.killed) child.kill("SIGINT"); }; process.on("SIGTERM", forwardTerm); process.on("SIGINT", forwardInt); const exitCode = await new Promise((resolveExit, rejectExit) => { child.once("error", rejectExit); child.once("exit", (code, signal) => resolveExit(code ?? (signal ? 1 : 0))); }); process.exitCode = exitCode; } try { await run(); } catch { process.stderr.write("sellable-agent runtime boundary failed closed\n"); process.exitCode = 1; }