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