UNPKG

@sellable/install

Version:

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

682 lines (648 loc) • 24.6 kB
import { createHash } from "node:crypto"; import { existsSync, lstatSync, readFileSync, readlinkSync, realpathSync, } from "node:fs"; import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; export const RUNTIME_BOUNDARY_VERSION = "sellable-agent-runtime-boundary/v1"; export const RUNTIME_SANDBOX_INIT_PATH = "/usr/local/bin/sellable-agent-sandbox-init"; export const RUNTIME_EGRESS_HOSTS = Object.freeze([ "api.openai.com", "app.sellable.dev", "slack.com", ".slack.com", ]); export const RUNTIME_SYSTEM_READ_ONLY_PATHS = Object.freeze([ "/usr", "/etc/ssl", "/etc/resolv.conf", "/etc/hosts", "/etc/nsswitch.conf", "/etc/passwd", "/etc/group", ]); const SAFE_ID = /^[A-Za-z0-9_-]{1,128}$/; const SLACK_ID = /^[CG][A-Z0-9]{8,31}$/; const SLACK_MEMBER_ID = /^[UW][A-Z0-9]{8,31}$/; const SHA256 = /^[a-f0-9]{64}$/; const INPUT_KEYS = new Set([ "profileId", "workspaceId", "agentId", "runtimeUid", "runtimeGid", "proxyUid", "proxyGid", "proxyPort", "proxyExecutablePath", "proxyExecutableDigest", "proxyConfigPath", "proxyConfigDigest", "activeProfileRoot", "runtimeStateRoot", "mcpCredentialRoot", "hermesSourceRoot", "hermesExecutablePath", "hermesExecutableDigest", "mcpExecutablePath", "bubblewrapPath", "setprivPath", "nftPath", "cgroupRoot", "serviceCredentialGeneration", "policyHash", "selectedChannelIds", "defaultChannelId", "memberAllowlist", ]); const sha256 = (value) => createHash("sha256").update(value).digest("hex"); function canonical(value) { if (Array.isArray(value)) return value.map(canonical); if (value && typeof value === "object") { return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])])); } return value; } function stableJson(value) { return JSON.stringify(canonical(value)); } function safeAbsolute(value) { return ( typeof value === "string" && isAbsolute(value) && resolve(value) === value && !/[\0\n\r,]/.test(value) ); } function inside(root, candidate) { const rel = relative(root, candidate); return rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel)); } function exactKeys(value, expected) { return ( value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === expected.size && Object.keys(value).every((key) => expected.has(key)) ); } function runtimeProfilePath(contract) { return `/runtime/profiles/${contract.profileId}`; } export function buildRuntimeIdentityLaunchArgs({ uid, gid, profileId, hermesExecutablePath, }) { if ( !/^[1-9][0-9]{0,9}$/.test(uid ?? "") || !/^[1-9][0-9]{0,9}$/.test(gid ?? "") || !SAFE_ID.test(profileId ?? "") || !safeAbsolute(hermesExecutablePath) ) { throw new Error("runtime identity launch rejected"); } return [ "--reuid", uid, "--regid", gid, "--clear-groups", "--no-new-privs", "--bounding-set=-all", "--inh-caps=-all", "--ambient-caps=-all", "/usr/bin/env", "-C", `/runtime/profiles/${profileId}`, hermesExecutablePath, "--profile", profileId, "gateway", "run", ]; } function boundaryTableName(profileId, runtimeUid) { return `sellable_agent_${sha256(`${profileId}\0${runtimeUid}`).slice(0, 16)}`; } export function buildRuntimeBoundaryContract(input) { try { if (!exactKeys(input, INPUT_KEYS)) throw new Error("boundary schema rejected"); const identityValues = [input.runtimeUid, input.proxyUid]; if ( !SAFE_ID.test(input.profileId) || !SAFE_ID.test(input.workspaceId) || !SAFE_ID.test(input.agentId) || !identityValues.every((value) => Number.isSafeInteger(value) && value > 0) || ![input.runtimeGid, input.proxyGid].every((value) => Number.isSafeInteger(value) && value > 0) || input.runtimeUid === input.proxyUid || !Number.isSafeInteger(input.proxyPort) || input.proxyPort < 1024 || input.proxyPort > 65535 || !Number.isSafeInteger(input.serviceCredentialGeneration) || input.serviceCredentialGeneration < 1 || !SHA256.test(input.policyHash) || !SHA256.test(input.proxyExecutableDigest) || !SHA256.test(input.proxyConfigDigest) || !SHA256.test(input.hermesExecutableDigest) ) throw new Error("boundary identity rejected"); const pathKeys = [ "proxyExecutablePath", "proxyConfigPath", "activeProfileRoot", "runtimeStateRoot", "mcpCredentialRoot", "hermesSourceRoot", "hermesExecutablePath", "mcpExecutablePath", "bubblewrapPath", "setprivPath", "nftPath", "cgroupRoot", ]; if (pathKeys.some((key) => !safeAbsolute(input[key]))) throw new Error("boundary path rejected"); if ( input.bubblewrapPath !== "/usr/bin/bwrap" || input.setprivPath !== "/usr/bin/setpriv" || !["/usr/sbin/nft", "/usr/bin/nft"].includes(input.nftPath) || input.cgroupRoot !== "/sys/fs/cgroup/sellable-agent" || input.mcpExecutablePath !== "/usr/local/lib/sellable-agent/external-runtime/mcp/bin/sellable-mcp" || !inside(input.hermesSourceRoot, input.hermesExecutablePath) ) throw new Error("boundary executable pin rejected"); if ( !Array.isArray(input.selectedChannelIds) || input.selectedChannelIds.length < 1 || input.selectedChannelIds.length > 100 || input.selectedChannelIds.some((value) => !SLACK_ID.test(value)) || new Set(input.selectedChannelIds).size !== input.selectedChannelIds.length || !input.selectedChannelIds.includes(input.defaultChannelId) ) throw new Error("boundary channel policy rejected"); if ( !Array.isArray(input.memberAllowlist) || input.memberAllowlist.length < 1 || input.memberAllowlist.length > 1000 || input.memberAllowlist.some((value) => !SLACK_MEMBER_ID.test(value)) || new Set(input.memberAllowlist).size !== input.memberAllowlist.length ) throw new Error("boundary member policy rejected"); const body = { boundaryVersion: RUNTIME_BOUNDARY_VERSION, profileId: input.profileId, workspaceId: input.workspaceId, agentId: input.agentId, runtime: { uid: input.runtimeUid, gid: input.runtimeGid }, proxy: { uid: input.proxyUid, gid: input.proxyGid, host: "127.0.0.1", port: input.proxyPort, executablePath: input.proxyExecutablePath, executableDigest: input.proxyExecutableDigest, configPath: input.proxyConfigPath, configDigest: input.proxyConfigDigest, allowedHosts: [...RUNTIME_EGRESS_HOSTS], }, paths: Object.fromEntries(pathKeys.filter((key) => !["proxyExecutablePath", "proxyConfigPath"].includes(key)).map((key) => [key, input[key]])), hermesExecutableDigest: input.hermesExecutableDigest, serviceCredentialGeneration: input.serviceCredentialGeneration, policyHash: input.policyHash, selectedChannelIds: [...input.selectedChannelIds], defaultChannelId: input.defaultChannelId, memberAllowlist: [...input.memberAllowlist], systemReadOnlyPaths: [...RUNTIME_SYSTEM_READ_ONLY_PATHS], limits: { pidsMax: 128, memoryMax: 1024 * 1024 * 1024, cpuMax: "100000 100000", }, firewall: { family: "inet", table: boundaryTableName(input.profileId, input.runtimeUid), runtimeUid: input.runtimeUid, proxyHost: "127.0.0.1", proxyPort: input.proxyPort, defaultRuntimeAction: "reject", }, }; return { ok: true, contract: Object.freeze({ ...body, contractDigest: sha256(stableJson(body)) }) }; } catch { return { ok: false, code: "runtime_boundary_contract_rejected" }; } } function addParentDirs(args, targets) { const seen = new Set(); for (const target of targets) { let cursor = dirname(target); const parents = []; while (cursor !== "/" && cursor !== ".") { parents.push(cursor); cursor = dirname(cursor); } for (const parent of parents.reverse()) { if (!seen.has(parent)) { args.push("--dir", parent); seen.add(parent); } } } } function runtimeEnvironment(contract) { const profileRoot = runtimeProfilePath(contract); return { PATH: `${dirname(contract.paths.hermesExecutablePath)}:/usr/local/bin:/usr/bin:/bin`, HOME: `${profileRoot}/home`, HERMES_HOME: "/runtime", NODE_ENV: "production", HTTPS_PROXY: `http://${contract.proxy.host}:${contract.proxy.port}`, HTTP_PROXY: `http://${contract.proxy.host}:${contract.proxy.port}`, NO_PROXY: "127.0.0.1,localhost", SLACK_HOME_CHANNEL: contract.defaultChannelId, SLACK_ALLOWED_CHANNELS: contract.selectedChannelIds.join(","), SLACK_ALLOWED_USERS: contract.memberAllowlist.join(","), SLACK_REQUIRE_MENTION: "true", SELLABLE_AGENT_RUNTIME: "1", SELLABLE_AGENT_RUNTIME_UID: String(contract.runtime.uid), SELLABLE_AGENT_RUNTIME_GID: String(contract.runtime.gid), SELLABLE_AGENT_HERMES_EXECUTABLE: contract.paths.hermesExecutablePath, SELLABLE_AGENT_SETPRIV_EXECUTABLE: contract.paths.setprivPath, SELLABLE_SLACK_APP_TOKEN_FD: "8", SELLABLE_SLACK_BOT_TOKEN_FD: "9", SELLABLE_AGENT_PROFILE_ID: contract.profileId, SELLABLE_AGENT_WORKSPACE_ID: contract.workspaceId, SELLABLE_AGENT_ID: contract.agentId, SELLABLE_AGENT_CREDENTIAL_GENERATION: String(contract.serviceCredentialGeneration), SELLABLE_AGENT_POLICY_HASH: contract.policyHash, SELLABLE_AGENT_SELECTED_CHANNEL_IDS: JSON.stringify(contract.selectedChannelIds), SELLABLE_LOCK_WORKSPACE_ID: contract.workspaceId, SELLABLE_REQUIRE_WORKSPACE_LOCK: "1", SELLABLE_CONFIG_PATH: `${profileRoot}/sellable/mcp.json`, SELLABLE_AGENT_RUNTIME_SECRET_ROOT: "/run/sellable-agent", SELLABLE_AGENT_SERVICE_CREDENTIAL_FILE: "/run/sellable-agent/service-credential", }; } export function buildRuntimeBoundaryLaunchSpec(contract) { try { if (!contract || contract.boundaryVersion !== RUNTIME_BOUNDARY_VERSION || !SHA256.test(contract.contractDigest)) { throw new Error("boundary contract missing"); } const active = contract.paths.activeProfileRoot; const state = contract.paths.runtimeStateRoot; const credential = join(contract.paths.mcpCredentialRoot, contract.profileId); const profileTarget = runtimeProfilePath(contract); const readOnlyProfileBinds = [ [join(active, "agent-profile.json"), `${profileTarget}/agent-profile.json`], [join(active, "config.yaml"), `${profileTarget}/config.yaml`], [join(active, "SOUL.md"), `${profileTarget}/SOUL.md`], [join(active, "skills"), `${profileTarget}/skills`], [join(active, "sellable"), `${profileTarget}/sellable`], ]; const args = [ "--die-with-parent", "--new-session", "--unshare-pid", "--unshare-ipc", "--unshare-uts", "--unshare-cgroup", "--clearenv", "--unsetenv", "PWD", "--cap-drop", "ALL", "--cap-add", "CAP_SETUID", "--cap-add", "CAP_SETGID", "--cap-add", "CAP_SETPCAP", ]; const bindTargets = [ ...contract.systemReadOnlyPaths, contract.paths.hermesSourceRoot, "/runtime", "/run/sellable-agent", "/run/bootstrap", ...readOnlyProfileBinds.map(([, target]) => target), "/proc", "/dev", "/tmp", ]; addParentDirs(args, bindTargets); for (const path of contract.systemReadOnlyPaths) args.push("--ro-bind", path, path); args.push( "--symlink", "usr/bin", "/bin", "--symlink", "usr/lib", "/lib", "--symlink", "usr/lib64", "/lib64", "--symlink", "usr/sbin", "/sbin", ); args.push("--ro-bind", contract.paths.hermesSourceRoot, contract.paths.hermesSourceRoot); args.push("--bind", state, "/runtime"); for (const [source, target] of readOnlyProfileBinds) args.push("--ro-bind", source, target); args.push("--ro-bind", credential, "/run/sellable-agent"); args.push("--tmpfs", "/run/bootstrap"); args.push( "--perms", "0600", "--file", "8", "/run/bootstrap/slack-app", "--perms", "0600", "--file", "9", "/run/bootstrap/slack-bot", ); args.push( "--proc", "/proc", "--dev", "/dev", "--tmpfs", "/tmp", "--remount-ro", "/", ); const env = runtimeEnvironment(contract); for (const [name, value] of Object.entries(env)) args.push("--setenv", name, value); args.push( "--chdir", "/", RUNTIME_SANDBOX_INIT_PATH, ); return { ok: true, command: contract.paths.bubblewrapPath, args, env: { PATH: "/usr/sbin:/usr/bin:/sbin:/bin" }, inheritedFileDescriptors: [8, 9], runtimeEnvironment: env, profileTarget, cgroupPath: join(contract.paths.cgroupRoot, contract.profileId), shell: false, }; } catch { return { ok: false, code: "runtime_boundary_launch_rejected" }; } } export function buildRuntimeBoundaryNftProgram(contract) { if (!contract?.firewall || contract.firewall.defaultRuntimeAction !== "reject") { return { ok: false, code: "runtime_boundary_firewall_rejected" }; } const { table, runtimeUid, proxyHost, proxyPort } = contract.firewall; if (!/^sellable_agent_[a-f0-9]{16}$/.test(table) || proxyHost !== "127.0.0.1") { return { ok: false, code: "runtime_boundary_firewall_rejected" }; } const program = [ `table inet ${table} {`, " chain output {", " type filter hook output priority -20; policy accept;", ` meta skuid ${runtimeUid} ip daddr 127.0.0.1 tcp dport ${proxyPort} accept`, ` meta skuid ${runtimeUid} ip6 daddr ::1 tcp dport ${proxyPort} accept`, ` meta skuid ${runtimeUid} reject with icmpx type admin-prohibited`, " }", "}", "", ].join("\n"); return { ok: true, table, deleteArgs: ["delete", "table", "inet", table], applyArgs: ["-f", "-"], program, programDigest: sha256(program), }; } export function expectedRuntimeCgroup(contract) { const path = join(contract.paths.cgroupRoot, contract.profileId); return { path, files: { "pids.max": String(contract.limits.pidsMax), "memory.max": String(contract.limits.memoryMax), "cpu.max": contract.limits.cpuMax, }, }; } function parseStatus(source) { return Object.fromEntries( source.split(/\r?\n/).filter(Boolean).map((line) => { const index = line.indexOf(":"); return [line.slice(0, index), line.slice(index + 1).trim()]; }), ); } function parseMountInfo(source) { const entries = []; for (const line of source.split(/\r?\n/).filter(Boolean)) { const separator = line.indexOf(" - "); if (separator < 0) continue; const left = line.slice(0, separator).split(" "); entries.push({ mountPoint: left[4], options: new Set(left[5].split(",")) }); } return entries; } function requiredMount(entries, mountPoint, option) { return entries.some((entry) => entry.mountPoint === mountPoint && entry.options.has(option)); } function expectedHermesCommand(contract) { const executable = safeRead(contract.paths.hermesExecutablePath, null); if (sha256(executable) !== contract.hermesExecutableDigest) throw new Error("Hermes executable drift rejected"); const firstLine = executable.subarray(0, 4096).toString("utf8").split(/\r?\n/, 1)[0]; if (!firstLine.startsWith("#!")) return [contract.paths.hermesExecutablePath]; const interpreter = firstLine.slice(2).trim(); if (!safeAbsolute(interpreter) || /\s/.test(interpreter)) throw new Error("Hermes shebang rejected"); return [interpreter, contract.paths.hermesExecutablePath]; } export function runtimeBoundaryNftRulesMatch(contract, source) { if (typeof source !== "string" || source.length > 128 * 1024) return false; const expected = buildRuntimeBoundaryNftProgram(contract); if (!expected.ok) return false; const tokens = (value) => { const parsed = value .replace(/#.*$/gm, "") .match(/[{};]|[^\s{};]+/g) ?? []; const normalized = []; for (let index = 0; index < parsed.length; index += 1) { if ( parsed[index] === "icmpx" && parsed[index + 1] === "type" && parsed[index + 2] === "admin-prohibited" ) { normalized.push("icmpx", "admin-prohibited"); index += 2; continue; } normalized.push(parsed[index]); } return normalized; }; return stableJson(tokens(source)) === stableJson(tokens(expected.program)); } function safeRead(path, encoding = "utf8") { const link = lstatSync(path); if (link.isSymbolicLink() || !link.isFile()) throw new Error("boundary observation path rejected"); return readFileSync(path, encoding); } export function observeRuntimeBoundary({ contract, pid, procRoot = "/proc", cgroupRoot = contract?.paths?.cgroupRoot, nftRuleset, proxyObservation, }) { try { if (!Number.isSafeInteger(pid) || pid < 2 || !safeAbsolute(procRoot) || !safeAbsolute(cgroupRoot)) { throw new Error("boundary observation identity rejected"); } const processRoot = join(procRoot, String(pid)); const status = parseStatus(safeRead(join(processRoot, "status"))); const uid = Number(status.Uid?.split(/\s+/)[0]); const gid = Number(status.Gid?.split(/\s+/)[0]); if ( uid !== contract.runtime.uid || gid !== contract.runtime.gid || status.NoNewPrivs !== "1" || !/^0+$/.test(status.CapEff ?? "") ) throw new Error("runtime privilege observation rejected"); const namespaceChecks = ["mnt", "pid", "ipc", "uts"].map((name) => { const runtimeNs = readlinkSync(join(processRoot, "ns", name)); const hostNs = readlinkSync(join(procRoot, "1", "ns", name)); return runtimeNs !== hostNs; }); if (namespaceChecks.some((value) => !value)) throw new Error("runtime namespace observation rejected"); const mounts = parseMountInfo(safeRead(join(processRoot, "mountinfo"))); const profileTarget = runtimeProfilePath(contract); const requiredReadOnlyMounts = [ "/", contract.paths.hermesSourceRoot, `${profileTarget}/agent-profile.json`, `${profileTarget}/config.yaml`, `${profileTarget}/SOUL.md`, `${profileTarget}/skills`, `${profileTarget}/sellable`, "/run/sellable-agent", ]; if ( requiredReadOnlyMounts.some((path) => !requiredMount(mounts, path, "ro")) || !requiredMount(mounts, "/runtime", "rw") ) throw new Error("runtime mount observation rejected"); const cgroupText = safeRead(join(processRoot, "cgroup")).trim(); const expectedGroup = `/sellable-agent/${contract.profileId}`; if (!cgroupText.split(/\r?\n/).some((line) => line === `0::${expectedGroup}`)) { throw new Error("runtime cgroup membership rejected"); } const cgroup = expectedRuntimeCgroup(contract); const observedCgroupPath = join(cgroupRoot, contract.profileId); for (const [name, value] of Object.entries(cgroup.files)) { if (safeRead(join(observedCgroupPath, name)).trim() !== value) throw new Error("runtime cgroup limit rejected"); } const environment = Object.fromEntries( safeRead(join(processRoot, "environ"), null) .toString("utf8") .split("\0") .filter(Boolean) .map((entry) => [entry.slice(0, entry.indexOf("=")), entry.slice(entry.indexOf("=") + 1)]), ); const expectedEnv = runtimeEnvironment(contract); if ( stableJson(environment) !== stableJson(expectedEnv) || Object.keys(environment).some((name) => /SLACK_(?:APP|BOT)_TOKEN$|HERMES_SESSION_/.test(name)) ) throw new Error("runtime environment observation rejected"); const cmdline = safeRead(join(processRoot, "cmdline"), null).toString("utf8").split("\0").filter(Boolean); const expectedCommand = [ ...expectedHermesCommand(contract), "--profile", contract.profileId, "gateway", "run", ]; if (stableJson(cmdline) !== stableJson(expectedCommand)) throw new Error("runtime executable observation rejected"); for (const [fd, name] of [[8, "slack-app"], [9, "slack-bot"]]) { const descriptor = lstatSync(join(processRoot, "fd", String(fd))); if (!descriptor.isSymbolicLink()) throw new Error("runtime inherited descriptor rejected"); const target = readlinkSync(join(processRoot, "fd", String(fd))); if (target !== `/run/bootstrap/${name} (deleted)`) { throw new Error("runtime inherited descriptor source rejected"); } } if (!runtimeBoundaryNftRulesMatch(contract, nftRuleset)) throw new Error("runtime firewall observation rejected"); if ( !proxyObservation?.ok || proxyObservation.uid !== contract.proxy.uid || proxyObservation.gid !== contract.proxy.gid || proxyObservation.host !== contract.proxy.host || proxyObservation.port !== contract.proxy.port || proxyObservation.executableDigest !== contract.proxy.executableDigest || proxyObservation.configDigest !== contract.proxy.configDigest || stableJson(proxyObservation.allowedHosts) !== stableJson(contract.proxy.allowedHosts) ) throw new Error("runtime egress proxy observation rejected"); return { ok: true, boundaryVersion: contract.boundaryVersion, contractDigest: contract.contractDigest, pid, runtimeUid: uid, cgroup: expectedGroup, firewallTable: contract.firewall.table, proxy: { uid: contract.proxy.uid, host: contract.proxy.host, port: contract.proxy.port, executableDigest: contract.proxy.executableDigest, configDigest: contract.proxy.configDigest, allowedHosts: [...contract.proxy.allowedHosts], }, checks: { namespacesIsolated: true, rootReadOnly: true, revisionReadOnly: true, runtimeStateWritable: true, noNewPrivileges: true, capabilitiesDropped: true, resourceLimits: true, directEgressDenied: true, allowlistedProxyOnly: true, inheritedSlackDescriptors: true, }, }; } catch { return { ok: false, code: "runtime_boundary_observation_rejected" }; } } export function inspectRuntimeBoundaryInputs(contract) { try { const regularFiles = [ contract.paths.bubblewrapPath, contract.paths.setprivPath, contract.paths.nftPath, contract.paths.hermesExecutablePath, contract.paths.mcpExecutablePath, RUNTIME_SANDBOX_INIT_PATH, contract.proxy.executablePath, contract.proxy.configPath, join(contract.paths.activeProfileRoot, "agent-profile.json"), join(contract.paths.activeProfileRoot, "config.yaml"), join(contract.paths.activeProfileRoot, "SOUL.md"), join(contract.paths.activeProfileRoot, "sellable", "mcp.json"), join(contract.paths.mcpCredentialRoot, contract.profileId, "service-credential"), ]; const directories = [ contract.paths.activeProfileRoot, contract.paths.runtimeStateRoot, contract.paths.mcpCredentialRoot, contract.paths.hermesSourceRoot, join(contract.paths.activeProfileRoot, "skills"), join(contract.paths.activeProfileRoot, "sellable"), ]; for (const path of regularFiles) { const link = lstatSync(path); if (link.isSymbolicLink() || !link.isFile() || (link.mode & 0o022) !== 0) throw new Error("unsafe boundary file"); } for (const path of directories) { const link = lstatSync(path); if (link.isSymbolicLink() || !link.isDirectory() || (link.mode & 0o022) !== 0) throw new Error("unsafe boundary directory"); } if ( sha256(readFileSync(contract.proxy.executablePath)) !== contract.proxy.executableDigest || sha256(readFileSync(contract.proxy.configPath)) !== contract.proxy.configDigest || sha256(readFileSync(contract.paths.hermesExecutablePath)) !== contract.hermesExecutableDigest || realpathSync(contract.paths.hermesSourceRoot) !== contract.paths.hermesSourceRoot || !inside(contract.paths.hermesSourceRoot, realpathSync(contract.paths.hermesExecutablePath)) ) throw new Error("boundary byte identity rejected"); return { ok: true, contractDigest: contract.contractDigest, proxyExecutableDigest: contract.proxy.executableDigest, proxyConfigDigest: contract.proxy.configDigest, }; } catch { return { ok: false, code: "runtime_boundary_inputs_rejected" }; } }