UNPKG

@sellable/install

Version:

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

164 lines (157 loc) • 6.86 kB
#!/usr/bin/env node import { spawn } from "node:child_process"; import { lstatSync, readFileSync } from "node:fs"; import { createInterface } from "node:readline"; const SAFE_ID = /^[A-Za-z0-9_-]{1,128}$/; const profileIndex = process.argv.indexOf("--profile-id"); const profileId = profileIndex >= 0 ? process.argv[profileIndex + 1] : null; function requiredEnv(name) { const value = process.env[name]?.trim(); if (!value) throw new Error(`missing ${name}`); return value; } async function withClient(lockWorkspaceId, run) { const command = requiredEnv("SELLABLE_AGENT_MCP_COMMAND"); if (command !== "/usr/local/lib/sellable-agent/external-runtime/mcp/bin/sellable-mcp") { throw new Error("MCP command pin rejected"); } const env = { PATH: "/usr/local/bin:/usr/bin:/bin", HOME: requiredEnv("HOME"), NODE_ENV: "production", SELLABLE_CONFIG_PATH: requiredEnv("SELLABLE_CONFIG_PATH"), SELLABLE_AGENT_RUNTIME_SECRET_ROOT: requiredEnv("SELLABLE_AGENT_RUNTIME_SECRET_ROOT"), SELLABLE_AGENT_PROFILE_ID: profileId, SELLABLE_AGENT_SERVICE_CREDENTIAL_FILE: requiredEnv("SELLABLE_AGENT_SERVICE_CREDENTIAL_FILE"), SELLABLE_AGENT_WORKSPACE_ID: requiredEnv("SELLABLE_AGENT_WORKSPACE_ID"), SELLABLE_AGENT_ID: requiredEnv("SELLABLE_AGENT_ID"), SELLABLE_AGENT_CREDENTIAL_GENERATION: requiredEnv("SELLABLE_AGENT_CREDENTIAL_GENERATION"), SELLABLE_AGENT_POLICY_HASH: requiredEnv("SELLABLE_AGENT_POLICY_HASH"), SELLABLE_AGENT_SELECTED_CHANNEL_IDS: requiredEnv("SELLABLE_AGENT_SELECTED_CHANNEL_IDS"), SELLABLE_LOCK_WORKSPACE_ID: lockWorkspaceId, SELLABLE_REQUIRE_WORKSPACE_LOCK: "1", }; const child = spawn(command, [], { env, shell: false, stdio: ["pipe", "pipe", "ignore"] }); const pending = new Map(); let nextId = 1; let failed = null; const lines = createInterface({ input: child.stdout, crlfDelay: Infinity }); const failAll = (error) => { failed = error; for (const waiter of pending.values()) waiter.reject(error); pending.clear(); }; lines.on("line", (line) => { try { const message = JSON.parse(line); if (!Number.isSafeInteger(message.id) || !pending.has(message.id)) return; const waiter = pending.get(message.id); pending.delete(message.id); if (message.error) waiter.reject(new Error("MCP request rejected")); else waiter.resolve(message.result); } catch { failAll(new Error("MCP response rejected")); } }); child.once("error", failAll); child.once("exit", () => failAll(new Error("MCP process exited"))); const request = (method, params = {}) => new Promise((resolveRequest, rejectRequest) => { if (failed) return rejectRequest(failed); const id = nextId++; const timer = setTimeout(() => { pending.delete(id); rejectRequest(new Error("MCP request timed out")); }, 10_000); pending.set(id, { resolve(value) { clearTimeout(timer); resolveRequest(value); }, reject(error) { clearTimeout(timer); rejectRequest(error); }, }); child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`); }); const notify = (method, params = {}) => { child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`); }; const initialized = await request("initialize", { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "sellable-agent-runtime-probe", version: "1.0.0" }, }); if (!initialized?.serverInfo) throw new Error("MCP initialization rejected"); notify("notifications/initialized"); const client = { listTools: () => request("tools/list", {}), callTool: (params) => request("tools/call", params), }; try { return await run(client); } finally { lines.close(); child.stdin.end(); child.kill("SIGTERM"); } } async function toolDenied(client, name, channelId, requesterId, providerRequestId) { try { const result = await client.callTool({ name, arguments: {}, _meta: { sellableAgent: { channelId, requesterId, providerRequestId } }, }, undefined, { timeout: 10_000, maxTotalTimeout: 10_000 }); return result?.isError === true; } catch { return true; } } try { if (!SAFE_ID.test(profileId ?? "") || process.argv.length !== 4) throw new Error("probe arguments rejected"); const configPath = requiredEnv("SELLABLE_CONFIG_PATH"); const link = lstatSync(configPath); if (link.isSymbolicLink() || !link.isFile() || (link.mode & 0o077) !== 0) throw new Error("profile config rejected"); const config = JSON.parse(readFileSync(configPath, "utf8")); const workspaceId = requiredEnv("SELLABLE_AGENT_WORKSPACE_ID"); const channelIds = config.requestChannelPolicy?.selectedChannelIds; const requesterIds = config.requestChannelPolicy?.memberAllowlist; const tools = config.tools?.include; if ( config.workspaceLockId !== workspaceId || !Array.isArray(channelIds) || channelIds.length === 0 || !Array.isArray(requesterIds) || requesterIds.length === 0 || !Array.isArray(tools) || !tools.includes("get_company_info") ) throw new Error("profile MCP contract rejected"); const channelId = channelIds[0]; const requesterId = requesterIds[0]; const providerRequestId = `runtime-probe-${profileId}`; const main = await withClient(workspaceId, async (client) => { const listed = (await client.listTools(undefined, { timeout: 10_000, maxTotalTimeout: 10_000 })).tools .map((tool) => tool.name).sort(); const safe = await client.callTool({ name: "get_company_info", arguments: {}, _meta: { sellableAgent: { channelId, requesterId, providerRequestId } }, }, undefined, { timeout: 10_000, maxTotalTimeout: 10_000 }); return { tools: listed, safeRead: safe?.isError !== true, unexpectedToolDenied: await toolDenied(client, "__sellable_unapproved_probe__", channelId, requesterId, providerRequestId), wrongChannelDenied: await toolDenied(client, "get_company_info", "C0000000000", requesterId, providerRequestId), }; }); const wrongWorkspaceDenied = await withClient(`wrong-${workspaceId}`, (client) => toolDenied(client, "get_company_info", channelId, requesterId, providerRequestId)); const ok = main.safeRead && main.unexpectedToolDenied && main.wrongChannelDenied && wrongWorkspaceDenied && JSON.stringify(main.tools) === JSON.stringify([...tools].sort()); process.stdout.write(`${JSON.stringify({ ok, profileId, workspaceId, tools: main.tools, safeRead: main.safeRead, unexpectedToolDenied: main.unexpectedToolDenied, wrongChannelDenied: main.wrongChannelDenied, wrongWorkspaceDenied, })}\n`); if (!ok) process.exitCode = 1; } catch { process.stdout.write(`${JSON.stringify({ ok: false, code: "runtime_probe_rejected" })}\n`); process.exitCode = 1; }