UNPKG

@sellable/install

Version:

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

511 lines (493 loc) • 15.6 kB
import { createHash, randomUUID } from "node:crypto"; import { chmodSync, closeSync, constants, existsSync, lstatSync, mkdirSync, openSync, realpathSync, renameSync, rmSync, writeFileSync, } from "node:fs"; import { basename, dirname, isAbsolute, join, relative } from "node:path"; import { parseFlyCustomerModelCredential } from "./fly-customer-model.mjs"; const SAFE_ID = /^[A-Za-z0-9_-]{1,128}$/; const SHA256 = /^[a-f0-9]{64}$/; function imageBundleIdentity(env) { const rawVersion = env?.SELLABLE_AGENT_DEFAULT_PROFILE_BUNDLE_VERSION; const bundleDigest = env?.SELLABLE_AGENT_DEFAULT_PROFILE_BUNDLE_DIGEST; if (rawVersion === undefined && bundleDigest === undefined) return null; const bundleVersion = Number(rawVersion); if ( !Number.isSafeInteger(bundleVersion) || bundleVersion < 1 || !SHA256.test(bundleDigest ?? "") ) { throw new Error("runtime_serving_observation_rejected"); } return Object.freeze({ bundleVersion, bundleDigest }); } function sha256(value) { return createHash("sha256").update(value).digest("hex"); } function stableJson(value) { if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`; if (value && typeof value === "object") { return `{${Object.keys(value) .sort() .map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`) .join(",")}}`; } return JSON.stringify(value); } /** The only claim that may write an operation-bound snapshot without applying. */ export function isOperationBoundBackupClaim(claim) { return claim?.operation?.stage === "BACKING_UP"; } // CUSTOMER and ADMIN runtimes share this one serving-observation builder. export function buildRuntimeServingObservation(input = {}, env = process.env) { const bundleIdentity = imageBundleIdentity(env); const keys = [ "scope", "kind", "operationId", "workspaceId", "agentId", "runtimeId", "authorityGeneration", "runtimeArtifactId", "machineId", "machineVersion", "bootSessionId", "profileIdentity", "processIdentity", "observedAt", "expiresAt", "slack", "productMcp", "adminMcp", "nativeGateway", "customerRouting", ...(bundleIdentity ? ["bundleVersion", "bundleDigest"] : []), ]; const identity = (value) => typeof value === "string" && SAFE_ID.test(value); const mcp = (value, name) => exactKeys(value, ["serverInfoName", "toolCount"]) && value.serverInfoName === name && Number.isSafeInteger(value.toolCount) && value.toolCount > 0 && value.toolCount <= 10_000; const observedAt = Date.parse(input.observedAt ?? ""); const expiresAt = Date.parse(input.expiresAt ?? ""); const kindExact = input.kind === "CUSTOMER" ? input.adminMcp === null && input.customerRouting === true : input.kind === "ADMIN" && mcp(input.adminMcp, "sellable-admin-mcp") && input.customerRouting === false; if ( !exactKeys({ ...input, ...(bundleIdentity ?? {}) }, keys) || !["ASSIGNED", "CANDIDATE"].includes(input.scope) || !keys .slice(2, 13) .filter((key) => key !== "authorityGeneration") .every((key) => identity(input[key])) || !Number.isSafeInteger(input.authorityGeneration) || input.authorityGeneration < 1 || !Number.isFinite(observedAt) || !Number.isFinite(expiresAt) || expiresAt <= observedAt || expiresAt - observedAt > 5 * 60_000 || !exactKeys(input.slack, [ "authenticated", "socketModeConnected", "apiReachable", ]) || input.slack.authenticated !== true || input.slack.socketModeConnected !== true || input.slack.apiReachable !== true || !mcp(input.productMcp, "sellable-mcp") || input.nativeGateway !== true || !kindExact ) { throw new Error("runtime_serving_observation_rejected"); } return Object.freeze({ schemaVersion: "sellable-agent-serving-observation/v2", ...input, ...(bundleIdentity ?? {}), }); } export function buildAgentServiceEnvironment({ workspaceId, agentId, serviceCredentialGeneration, policyHash, selectedChannelIds, }) { const canonicalChannels = Array.isArray(selectedChannelIds) ? [...new Set(selectedChannelIds)].sort() : []; if ( !SAFE_ID.test(workspaceId ?? "") || !SAFE_ID.test(agentId ?? "") || !Number.isSafeInteger(serviceCredentialGeneration) || serviceCredentialGeneration < 1 || !SHA256.test(policyHash ?? "") || canonicalChannels.length < 1 || canonicalChannels.length > 100 || canonicalChannels.some((channelId) => !SAFE_ID.test(channelId ?? "")) || JSON.stringify(selectedChannelIds) !== JSON.stringify(canonicalChannels) ) { throw new Error("agent_service_environment_rejected"); } return Object.freeze({ SELLABLE_AGENT_WORKSPACE_ID: workspaceId, SELLABLE_AGENT_ID: agentId, SELLABLE_AGENT_CREDENTIAL_GENERATION: String(serviceCredentialGeneration), SELLABLE_AGENT_POLICY_HASH: policyHash, SELLABLE_AGENT_SELECTED_CHANNEL_IDS: JSON.stringify(canonicalChannels), SELLABLE_LOCK_WORKSPACE_ID: workspaceId, SELLABLE_REQUIRE_WORKSPACE_LOCK: "1", }); } export function buildAdminAgentServiceEnvironment({ agentKind, ...input }) { if (agentKind !== "ADMIN") { throw new Error("admin_agent_service_environment_rejected"); } const { SELLABLE_LOCK_WORKSPACE_ID: _workspaceLock, SELLABLE_REQUIRE_WORKSPACE_LOCK: _workspaceLockRequired, ...environment } = buildAgentServiceEnvironment(input); return Object.freeze(environment); } function exactKeys(value, keys) { return Boolean( value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).sort().join("\0") === [...keys].sort().join("\0") ); } function privateDirectory(pathValue) { try { if (!isAbsolute(pathValue) || !existsSync(pathValue)) return null; const link = lstatSync(pathValue); if ( link.isSymbolicLink() || !link.isDirectory() || (link.mode & 0o077) !== 0 ) return null; return realpathSync(pathValue); } catch { return null; } } function directChild(root, candidate) { const path = relative(root, candidate); return ( isAbsolute(candidate) && dirname(candidate) === root && path === basename(candidate) ); } function validateExpected(expected) { if (!expected || typeof expected !== "object" || Array.isArray(expected)) return false; for (const key of [ "workspaceId", "agentId", "runtimeId", "profileId", "revisionId", "installationId", ]) { if (!SAFE_ID.test(expected[key] ?? "")) return false; } if (!/^[1-9][0-9]{0,19}$/.test(expected.fence ?? "")) return false; for (const key of [ "slackGeneration", "slackAppGeneration", "serviceCredentialGeneration", "modelCredentialGeneration", "apiServerKeyGeneration", ]) { if (!Number.isSafeInteger(expected[key]) || expected[key] < 1) return false; } return true; } const ENVELOPE_KEYS = [ "purpose", "provider", "secret", "fingerprint", "generation", "binding", ]; const BINDING_KEYS = [ "workspaceId", "agentId", "runtimeId", "profileId", "revisionId", "fence", "purpose", ]; const PURPOSES = Object.freeze({ slackApp: { purpose: "slack-app", provider: "slack", file: "slack-app", generation: "slackAppGeneration", }, slackBot: { purpose: "slack-bot", provider: "slack", file: "slack-bot", generation: "slackGeneration", }, mcpService: { purpose: "service-credential", provider: "sellable", file: "service-credential", generation: "serviceCredentialGeneration", }, modelCodex: { purpose: "model-codex-auth", provider: "codex", file: "model-codex-auth", generation: "modelCredentialGeneration", }, hermesApiServer: { purpose: "hermes-api-server", provider: "sellable", file: "api-server-key", generation: "apiServerKeyGeneration", }, }); function validEnvelope(envelope, contract, expected) { if ( !exactKeys(envelope, ENVELOPE_KEYS) || !exactKeys(envelope.binding, BINDING_KEYS) ) return false; if ( envelope.purpose !== contract.purpose || envelope.provider !== contract.provider || typeof envelope.secret !== "string" || envelope.secret.length < 1 || envelope.secret.length > 1_048_576 || envelope.fingerprint !== sha256(envelope.secret).slice(0, 16) || envelope.generation !== expected[contract.generation] ) return false; if (contract.purpose === "model-codex-auth") { try { parseFlyCustomerModelCredential(envelope.secret); } catch { return false; } } return BINDING_KEYS.every( (key) => envelope.binding[key] === (key === "purpose" ? contract.purpose : expected[key]) ); } function writeExclusivePrivate(pathValue, bytes) { const fd = openSync( pathValue, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | (constants.O_NOFOLLOW ?? 0), 0o600 ); try { writeFileSync(fd, bytes, "utf8"); } finally { closeSync(fd); } chmodSync(pathValue, 0o600); } export function installCustomerRuntimeSecrets({ expected, secretsRoot, envelopes, }) { if ( !validateExpected(expected) || !exactKeys(envelopes, Object.keys(PURPOSES)) ) { return { ok: false, code: "runtime_secret_envelope_rejected" }; } const root = privateDirectory(secretsRoot); if (!root) return { ok: false, code: "runtime_secret_root_rejected" }; for (const [name, contract] of Object.entries(PURPOSES)) { if (!validEnvelope(envelopes[name], contract, expected)) { return { ok: false, code: "runtime_secret_envelope_rejected" }; } } if ( new Set(Object.values(envelopes).map((envelope) => envelope.fingerprint)) .size !== Object.keys(PURPOSES).length ) { return { ok: false, code: "runtime_secret_credential_alias_rejected" }; } const activeRoot = join(root, expected.profileId); const stagingRoot = join( root, `.${expected.profileId}.${randomUUID()}.staging` ); const backupRoot = join( root, `.${expected.profileId}.${randomUUID()}.backup` ); if (!directChild(root, activeRoot) || !directChild(root, stagingRoot)) { return { ok: false, code: "runtime_secret_path_rejected" }; } try { mkdirSync(stagingRoot, { mode: 0o700 }); chmodSync(stagingRoot, 0o700); for (const [name, contract] of Object.entries(PURPOSES)) { writeExclusivePrivate( join(stagingRoot, contract.file), envelopes[name].secret ); } // Must match SELLABLE_AGENT_TURN_CONSUMED_ROOT exactly: the secret-root // swap below discards the directory customer-runtime made at boot, and // the MCP context proxy fails closed when the nonce directory is absent. mkdirSync(join(stagingRoot, "turn-consumed"), { mode: 0o700 }); chmodSync(join(stagingRoot, "turn-consumed"), 0o700); if (existsSync(activeRoot)) { const active = lstatSync(activeRoot); if (active.isSymbolicLink() || !active.isDirectory()) throw new Error("active secret root rejected"); renameSync(activeRoot, backupRoot); } renameSync(stagingRoot, activeRoot); if (existsSync(backupRoot)) rmSync(backupRoot, { recursive: true, force: true }); return { ok: true, installedCount: Object.keys(PURPOSES).length }; } catch { if (existsSync(stagingRoot) && !lstatSync(stagingRoot).isSymbolicLink()) { rmSync(stagingRoot, { recursive: true, force: true }); } if (!existsSync(activeRoot) && existsSync(backupRoot)) renameSync(backupRoot, activeRoot); return { ok: false, code: "runtime_secret_install_failed" }; } } export function clearCustomerRuntimeSecrets({ secretsRoot, profileId }) { const root = privateDirectory(secretsRoot); if (!root || !SAFE_ID.test(profileId ?? "")) return { ok: false, code: "runtime_secret_path_rejected" }; const activeRoot = join(root, profileId); if (!directChild(root, activeRoot)) return { ok: false, code: "runtime_secret_path_rejected" }; try { if (!existsSync(activeRoot)) return { ok: true, removed: false }; const active = lstatSync(activeRoot); if (active.isSymbolicLink() || !active.isDirectory()) { return { ok: false, code: "runtime_secret_path_rejected" }; } rmSync(activeRoot, { recursive: true, force: true }); return { ok: true, removed: true }; } catch { return { ok: false, code: "runtime_secret_cleanup_failed" }; } } export function buildNativeHermesLaunch({ profileRoot, secretsRoot, profileId, env = {}, apiServerKeyRequired = true, }) { if ( !SAFE_ID.test(profileId ?? "") || !isAbsolute(profileRoot) || !existsSync(profileRoot) || lstatSync(profileRoot).isSymbolicLink() || !lstatSync(profileRoot).isDirectory() || !isAbsolute(secretsRoot) || !env || typeof env !== "object" || Array.isArray(env) || typeof apiServerKeyRequired !== "boolean" || Object.keys(env).some((key) => /SLACK_(?:APP|BOT)_TOKEN|XAPP/i.test(key)) || Object.keys(env).some((key) => key === "API_SERVER_KEY") || /(?:xapp-|xoxb-|npm install|pip install)/i.test(stableJson(env)) ) { throw new Error("native_customer_gateway_policy_rejected"); } const resolvedProfileRoot = realpathSync(profileRoot); const resolvedSecretsRoot = realpathSync(secretsRoot); const configPath = join(resolvedProfileRoot, "config.yaml"); const appTokenPath = join(resolvedSecretsRoot, "slack-app"); const botTokenPath = join(resolvedSecretsRoot, "slack-bot"); const apiServerKeyPath = join(resolvedSecretsRoot, "api-server-key"); for (const pathValue of [ configPath, appTokenPath, botTokenPath, ...(apiServerKeyRequired ? [apiServerKeyPath] : []), ]) { if ( !existsSync(pathValue) || lstatSync(pathValue).isSymbolicLink() || !lstatSync(pathValue).isFile() ) { throw new Error("native_customer_gateway_policy_rejected"); } } return Object.freeze({ command: "hermes", // Lifecycle refreshes can overlap the previous gateway's final teardown // after a machine replacement or a retried operation. Hermes owns the // profile-scoped singleton lock, so let it atomically replace a stale // same-profile process instead of leaving the worker in a hot retry loop. args: Object.freeze([ "--profile", profileId, "gateway", "run", "--replace", // Each Fly Machine owns exactly one profile. Sellable supervises this // foreground child and its credential descriptors; Hermes 0.21.5 must // neither attach it to a host multiplexer nor redirect it to stock s6. "--force", "--no-supervise", "--external-supervisor", ]), cwd: resolvedProfileRoot, env: Object.freeze({ ...env, HERMES_PROFILE: profileId, HERMES_CONFIG_PATH: configPath, SELLABLE_AGENT_RUNTIME: "1", SELLABLE_SLACK_APP_TOKEN_FD: "8", SELLABLE_SLACK_BOT_TOKEN_FD: "9", }), inheritedFileDescriptors: Object.freeze([ Object.freeze({ fd: 8, path: appTokenPath }), Object.freeze({ fd: 9, path: botTokenPath }), ]), apiServerKeyFile: apiServerKeyRequired ? apiServerKeyPath : null, apiServer: apiServerKeyRequired ? Object.freeze({ host: "127.0.0.1", port: 8642, cors: false, }) : null, nativeSlackListener: true, runtimeInstallAllowed: false, }); }