UNPKG

@sellable/install

Version:

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

669 lines (652 loc) • 22.1 kB
import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, randomUUID, } from "node:crypto"; import { chmodSync, chownSync, closeSync, constants, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, writeFileSync, } from "node:fs"; import { dirname, isAbsolute, join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { buildExternalRuntimeClosure } from "./external-runtime-builder.mjs"; import { canonicalHostWorkerDigest } from "./host-worker.mjs"; import { RUNTIME_EGRESS_HOSTS } from "./runtime-boundary.mjs"; import { RUNTIME_EGRESS_PROXY_VERSION } from "./runtime-egress-proxy.mjs"; import { EXTERNAL_HERMES_ENTRYPOINT, EXTERNAL_MCP_ENTRYPOINT, EXTERNAL_RUNTIME_CLOSURE_ROOT, EXTERNAL_RUNTIME_MANIFEST_PATH, } from "./runtime-helper.mjs"; import { installAgentHostServices } from "./service-installer.mjs"; const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); const VERSION = "sellable-agent-host-bootstrap/v1"; const RECEIPT_VERSION = "sellable-agent-host-registration/v1"; const INSTALLER_PACKAGE = "@sellable/install@0.1.804"; const MCP_PACKAGE = "@sellable/mcp@0.1.957"; const HERMES_VERSION = "0.18.0"; const SHA256 = /^[a-f0-9]{64}$/; const SHA256_16 = /^[a-f0-9]{16}$/; const SHA512_INTEGRITY = /^sha512-[A-Za-z0-9+/]+={0,2}$/; const SAFE_ID = /^[A-Za-z0-9_-]{1,128}$/; const SAFE_TOOL = /^[a-z][a-z0-9_]{0,63}$/; const SECRET = /xox[bap]-|sat_[A-Za-z0-9_-]+|asec\.v1\.|access[_-]?token|refresh[_-]?token|private[_-]?key/i; const PATHS = Object.freeze({ providerSource: "/var/lib/sellable-agent-bootstrap/provider-auth", xappSource: "/var/lib/sellable-agent-bootstrap/slack-app", hostProbe: "/var/lib/sellable-agent-bootstrap/host-probe", receipt: "/var/lib/sellable-agent-bootstrap/registration-receipt.json", releaseIdentity: "/opt/sellable-agent/release.json", workerRoot: "/var/lib/sellable-agent-worker", workerRequests: "/var/lib/sellable-agent-worker/requests", helperRequests: "/var/lib/sellable-agent-worker/helper-requests", workerState: "/var/lib/sellable-agent-worker/state", workerSlackSecrets: "/var/lib/sellable-agent-worker/runtime-secrets", workerHostSecrets: "/var/lib/sellable-agent-worker/host-secrets", xappRuntime: "/var/lib/sellable-agent-worker/host-secrets/slack-app", signingRoot: "/var/lib/sellable-agent-worker/enrollment", signingKey: "/var/lib/sellable-agent-worker/enrollment/private-key.pem", runtimeRoot: "/var/lib/sellable-agent-runtime", runtimeState: "/var/lib/sellable-agent-runtime/state", runtimeMcpSecrets: "/var/lib/sellable-agent-runtime/mcp-secrets", profiles: "/var/lib/sellable-agent-profiles", }); const exactKeys = (value, keys) => Boolean( value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).sort().join("\0") === [...keys].sort().join("\0") ); const sha256 = (value) => createHash("sha256").update(value).digest("hex"); function stable(value) { if (Array.isArray(value)) return value.map(stable); if (value && typeof value === "object") { return Object.fromEntries( Object.keys(value) .sort() .map((key) => [key, stable(value[key])]) ); } return value; } function targetPath(targetRoot, absolutePath) { const root = realpathSync(resolve(targetRoot)); const value = root === "/" ? resolve(absolutePath) : join(root, absolutePath.replace(/^\/+/, "")); const rel = relative(root, value); if (!isAbsolute(absolutePath) || rel.startsWith("..") || isAbsolute(rel)) { throw new Error("host bootstrap target rejected"); } return value; } function atomicFile(pathValue, bytes, mode, { uid, gid, disposableFixture }) { const temp = join(dirname(pathValue), `.${randomUUID()}.tmp`); writeFileSync(temp, bytes, { flag: "wx", mode }); chmodSync(temp, mode); if (!disposableFixture) chownSync(temp, uid, gid); const descriptor = openSync(temp, "r"); try { fsyncSync(descriptor); } finally { closeSync(descriptor); } renameSync(temp, pathValue); const parent = openSync(dirname(pathValue), "r"); try { fsyncSync(parent); } finally { closeSync(parent); } } function ensureDirectory( targetRoot, absolutePath, mode, uid, gid, disposableFixture ) { const value = targetPath(targetRoot, absolutePath); if (!existsSync(value)) mkdirSync(value, { recursive: true, mode }); const link = lstatSync(value); if (link.isSymbolicLink() || !link.isDirectory()) throw new Error("host bootstrap directory rejected"); chmodSync(value, mode); if (!disposableFixture) chownSync(value, uid, gid); const observed = lstatSync(value); if ( (observed.mode & 0o777) !== mode || (!disposableFixture && (observed.uid !== uid || observed.gid !== gid)) ) throw new Error("host bootstrap directory readback rejected"); return value; } function readPrivateSource(targetRoot, absolutePath, disposableFixture) { const value = targetPath(targetRoot, absolutePath); const parent = lstatSync(dirname(value)); const link = lstatSync(value); const expectedUid = disposableFixture ? (process.getuid?.() ?? link.uid) : 0; const expectedGid = disposableFixture ? (process.getgid?.() ?? link.gid) : 0; if ( parent.isSymbolicLink() || !parent.isDirectory() || (parent.mode & 0o777) !== 0o700 || parent.uid !== expectedUid || parent.gid !== expectedGid || link.isSymbolicLink() || !link.isFile() || (link.mode & 0o777) !== 0o600 || link.uid !== expectedUid || link.gid !== expectedGid || realpathSync(dirname(value)) !== dirname(value) ) throw new Error("host bootstrap private source rejected"); const descriptor = openSync( value, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) ); try { const opened = fstatSync(descriptor); if ( !opened.isFile() || opened.dev !== link.dev || opened.ino !== link.ino ) { throw new Error("host bootstrap private source changed"); } return readFileSync(descriptor, "utf8").trim(); } finally { closeSync(descriptor); } } function readReleaseIdentity(targetRoot, expected, disposableFixture) { const value = targetPath(targetRoot, PATHS.releaseIdentity); const link = lstatSync(value); const expectedUid = disposableFixture ? (process.getuid?.() ?? link.uid) : 0; const expectedGid = disposableFixture ? (process.getgid?.() ?? link.gid) : 0; if ( link.isSymbolicLink() || !link.isFile() || (link.mode & 0o777) !== 0o444 || link.uid !== expectedUid || link.gid !== expectedGid ) throw new Error("host bootstrap image release rejected"); const observed = JSON.parse(readFileSync(value, "utf8")); if ( !exactKeys(observed, [ "installerPackage", "installerIntegrity", "mcpPackage", "mcpIntegrity", ]) || JSON.stringify(stable(observed)) !== JSON.stringify(stable(expected)) ) { throw new Error("host bootstrap image release mismatch"); } return observed; } function validateConfig(config) { if ( !exactKeys(config, [ "version", "target", "host", "enrollment", "release", "controlPlaneUrl", "runtimeContract", "identities", ]) || config.version !== VERSION || SECRET.test(JSON.stringify(config)) ) { throw new Error("host bootstrap config rejected"); } if ( !exactKeys(config.target, ["vpsId", "hostname", "containerName"]) || Object.values(config.target).some((value) => !SAFE_ID.test(value ?? "")) || !exactKeys(config.host, [ "id", "stableKey", "compatibilityHash", "capacity", ]) || !SAFE_ID.test(config.host.id ?? "") || !SAFE_ID.test(config.host.stableKey ?? "") || !SHA256.test(config.host.compatibilityHash ?? "") || !Number.isSafeInteger(config.host.capacity) || config.host.capacity < 1 || config.host.capacity > 100 || !exactKeys(config.enrollment, [ "id", "workerId", "keyGeneration", "expiresAt", ]) || !SAFE_ID.test(config.enrollment.id ?? "") || !SAFE_ID.test(config.enrollment.workerId ?? "") || !Number.isSafeInteger(config.enrollment.keyGeneration) || config.enrollment.keyGeneration < 1 || !Number.isFinite(Date.parse(config.enrollment.expiresAt ?? "")) || Date.parse(config.enrollment.expiresAt) <= Date.now() || !exactKeys(config.release, [ "installerPackage", "installerIntegrity", "mcpPackage", "mcpIntegrity", ]) || config.release.installerPackage !== INSTALLER_PACKAGE || config.release.mcpPackage !== MCP_PACKAGE || !SHA512_INTEGRITY.test(config.release.installerIntegrity ?? "") || !SHA512_INTEGRITY.test(config.release.mcpIntegrity ?? "") || !/^https:\/\/[A-Za-z0-9.-]+(?::[0-9]+)?$/.test(config.controlPlaneUrl ?? "") ) throw new Error("host bootstrap identity rejected"); const runtime = config.runtimeContract; if ( !exactKeys(runtime, [ "profileId", "providerReference", "providerFingerprint", "xappReference", "xappFingerprint", "policyHash", "toolInclude", ]) || !SAFE_ID.test(runtime.profileId ?? "") || typeof runtime.providerReference !== "string" || !runtime.providerReference || runtime.providerReference.length > 256 || !SHA256.test(runtime.providerFingerprint ?? "") || typeof runtime.xappReference !== "string" || !runtime.xappReference || runtime.xappReference.length > 256 || !SHA256_16.test(runtime.xappFingerprint ?? "") || !SHA256.test(runtime.policyHash ?? "") || !Array.isArray(runtime.toolInclude) || runtime.toolInclude.length < 1 || runtime.toolInclude.length > 64 || runtime.toolInclude.some((tool) => !SAFE_TOOL.test(tool)) || JSON.stringify(runtime.toolInclude) !== JSON.stringify([...new Set(runtime.toolInclude)].sort()) ) throw new Error("host bootstrap runtime contract rejected"); const identities = config.identities; if ( !exactKeys(identities, [ "workerUid", "workerGid", "proxyUid", "proxyGid", "runtimeUid", "runtimeGid", "siblingUid", "siblingGid", "adminUid", "adminGid", ]) || Object.values(identities).some( (value) => !Number.isSafeInteger(value) || value < 1 ) || new Set([ identities.workerUid, identities.proxyUid, identities.runtimeUid, identities.siblingUid, identities.adminUid, ]).size !== 5 ) throw new Error("host bootstrap runtime identities rejected"); return config; } function signingIdentity(targetRoot, config, disposableFixture) { const keyPath = targetPath(targetRoot, PATHS.signingKey); if (!existsSync(keyPath)) { const { privateKey } = generateKeyPairSync("ed25519"); atomicFile( keyPath, privateKey.export({ type: "pkcs8", format: "pem" }).toString(), 0o600, { uid: config.identities.workerUid, gid: config.identities.workerGid, disposableFixture, } ); } const link = lstatSync(keyPath); if ( link.isSymbolicLink() || !link.isFile() || (link.mode & 0o777) !== 0o600 || (!disposableFixture && (link.uid !== config.identities.workerUid || link.gid !== config.identities.workerGid)) ) throw new Error("host bootstrap signing key rejected"); const privateKey = createPrivateKey(readFileSync(keyPath, "utf8")); if (privateKey.asymmetricKeyType !== "ed25519") throw new Error("host bootstrap signing key type rejected"); const publicKeyPem = createPublicKey(privateKey) .export({ type: "spki", format: "pem" }) .toString(); const publicKeyHash = sha256(publicKeyPem); const enrollmentFingerprint = canonicalHostWorkerDigest({ audience: "sellable-agent-worker/v1", enrollmentId: config.enrollment.id, hostId: config.host.id, keyGeneration: config.enrollment.keyGeneration, publicKeyHash, workerId: config.enrollment.workerId, }); return { publicKeyHash, enrollmentFingerprint }; } function prepareLayout(targetRoot, config, disposableFixture) { const ids = config.identities; for (const pathValue of [ PATHS.workerRoot, PATHS.workerRequests, PATHS.helperRequests, PATHS.workerState, PATHS.workerSlackSecrets, PATHS.workerHostSecrets, PATHS.signingRoot, ]) ensureDirectory( targetRoot, pathValue, 0o700, ids.workerUid, ids.workerGid, disposableFixture ); for (const pathValue of [ PATHS.runtimeRoot, PATHS.runtimeState, PATHS.runtimeMcpSecrets, ]) { ensureDirectory( targetRoot, pathValue, 0o700, ids.runtimeUid, ids.runtimeGid, disposableFixture ); } ensureDirectory( targetRoot, PATHS.profiles, 0o711, ids.workerUid, ids.workerGid, disposableFixture ); const probePath = targetPath(targetRoot, PATHS.hostProbe); if (!existsSync(probePath)) { atomicFile(probePath, "host-only\n", 0o600, { uid: 0, gid: 0, disposableFixture, }); } } function installHostAppSecret(targetRoot, config, disposableFixture) { const source = readPrivateSource( targetRoot, PATHS.xappSource, disposableFixture ); if ( !source || source.length > 4096 || sha256(source).slice(0, 16) !== config.runtimeContract.xappFingerprint ) { throw new Error("host bootstrap Slack app source rejected"); } const target = targetPath(targetRoot, PATHS.xappRuntime); atomicFile(target, `${source}\n`, 0o600, { uid: config.identities.workerUid, gid: config.identities.workerGid, disposableFixture, }); return target; } function configs(config, signing, ownerUid, ownerGid) { const ids = config.identities; const workerConfig = { enrollmentId: config.enrollment.id, hostId: config.host.id, workerId: config.enrollment.workerId, signerGeneration: config.enrollment.keyGeneration, enrollmentFingerprint: signing.enrollmentFingerprint, publicKeyHash: signing.publicKeyHash, privateKeyPath: PATHS.signingKey, controlPlaneUrl: config.controlPlaneUrl, requestRoot: PATHS.workerRequests, helperRequestRoot: PATHS.helperRequests, stateRoot: PATHS.workerState, profilesRoot: PATHS.profiles, runtimeSecretsRoot: PATHS.workerSlackSecrets, runtimeMcpSecretsRoot: PATHS.runtimeMcpSecrets, workerUid: ids.workerUid, workerGid: ids.workerGid, runtimeUid: ids.runtimeUid, runtimeGid: ids.runtimeGid, leaseSeconds: 30, poll: { minDelayMs: 1_000, maxDelayMs: 30_000, jitterRatio: 0.2 }, profileRuntimeContract: { hermesCli: "hermes", hermesVersion: HERMES_VERSION, installerPackage: INSTALLER_PACKAGE, mcpPackage: MCP_PACKAGE, providerReference: config.runtimeContract.providerReference, providerFingerprint: config.runtimeContract.providerFingerprint, xappReference: config.runtimeContract.xappReference, xappFingerprint: config.runtimeContract.xappFingerprint, xappSecretFile: PATHS.xappRuntime, policies: { [config.runtimeContract.policyHash]: { toolInclude: config.runtimeContract.toolInclude, }, }, }, }; const helperProfile = { workerUid: ids.workerUid, workerGid: ids.workerGid, runtimeUid: ids.runtimeUid, runtimeGid: ids.runtimeGid, siblingUid: ids.siblingUid, siblingGid: ids.siblingGid, adminUid: ids.adminUid, adminGid: ids.adminGid, proxyUid: ids.proxyUid, proxyGid: ids.proxyGid, profilesRoot: PATHS.profiles, workerRuntimeSecretRoot: PATHS.workerSlackSecrets, mcpCredentialRoot: PATHS.runtimeMcpSecrets, runtimeStateRoot: PATHS.runtimeState, proxyExecutablePath: "/usr/local/lib/sellable-agent/package/bin/sellable-agent-egress-proxy.mjs", installedPackageManifestPath: "/usr/local/lib/sellable-agent/package/manifest.json", externalRuntimeManifestPath: EXTERNAL_RUNTIME_MANIFEST_PATH, externalRuntimeClosureRoot: EXTERNAL_RUNTIME_CLOSURE_ROOT, externalRuntimeTrustRoot: "/usr/local/lib/sellable-agent", externalRuntimePathRoot: "/", externalRuntimeOwnerUid: ownerUid, externalRuntimeOwnerGid: ownerGid, mcpCommand: EXTERNAL_MCP_ENTRYPOINT, hermesExecutablePath: EXTERNAL_HERMES_ENTRYPOINT, hermesSourceRoot: `${EXTERNAL_RUNTIME_CLOSURE_ROOT}/hermes`, proxyConfigPath: "/etc/sellable-agent/egress-proxy.json", runtimeBoundaryConfigPath: "/etc/sellable-agent/runtime-boundary.json", providerCredentialSourcePath: PATHS.providerSource, providerCredentialFingerprint: config.runtimeContract.providerFingerprint, runtimeServicePath: "/etc/services.d/sellable-agent-runtime", runtimeServiceEnvRoot: "/etc/services.d/sellable-agent-runtime/env", runtimeProbePath: "/usr/local/bin/sellable-agent-runtime-probe", proxyPidPath: "/etc/services.d/sellable-agent-egress-proxy/supervise/pid", proxyPort: 19081, procRoot: "/proc", bootIdPath: "/proc/sys/kernel/random/boot_id", bubblewrapPath: "/usr/bin/bwrap", setprivPath: "/usr/bin/setpriv", nftPath: "/usr/sbin/nft", cgroupRoot: "/sys/fs/cgroup/sellable-agent", hostProbePath: PATHS.hostProbe, dockerSocketPath: "/var/run/docker.sock", }; const helperConfig = { helperRequestRoot: PATHS.helperRequests, workerRequestRoot: PATHS.workerRequests, s6SvcPath: "/command/s6-svc", profiles: { [config.runtimeContract.profileId]: helperProfile }, }; const egressProxyConfig = { version: RUNTIME_EGRESS_PROXY_VERSION, listenHost: "127.0.0.1", listenPort: 19081, uid: ids.proxyUid, gid: ids.proxyGid, allowedHosts: [...RUNTIME_EGRESS_HOSTS], }; return { workerConfig, helperConfig, egressProxyConfig }; } export function bootstrapAgentHost(input) { const config = validateConfig(input.config); const targetRoot = resolve(input.targetRoot ?? "/"); const disposableFixture = input.disposableFixture === true; if ( !isAbsolute(targetRoot) || !existsSync(targetRoot) || (targetRoot !== "/" && !disposableFixture) ) { throw new Error("host bootstrap root rejected"); } if ( targetRoot === "/" && (process.getuid?.() !== 0 || process.getgid?.() !== 0) ) { throw new Error("host bootstrap requires root"); } readReleaseIdentity(targetRoot, config.release, disposableFixture); const provider = readPrivateSource( targetRoot, PATHS.providerSource, disposableFixture ); if ( !provider || provider.length > 1024 * 1024 || sha256(provider) !== config.runtimeContract.providerFingerprint ) { throw new Error("host bootstrap provider source rejected"); } const providerJson = JSON.parse(provider); if ( !providerJson || typeof providerJson !== "object" || Array.isArray(providerJson) ) { throw new Error("host bootstrap provider JSON rejected"); } prepareLayout(targetRoot, config, disposableFixture); installHostAppSecret(targetRoot, config, disposableFixture); const signing = signingIdentity(targetRoot, config, disposableFixture); const sourceRoots = disposableFixture ? input.sourceRoots : { mcpPackageRoot: "/opt/sellable-agent/mcp-root/node_modules/@sellable/mcp", mcpNodeModulesRoot: "/opt/sellable-agent/mcp-root/node_modules", hermesSourceRoot: "/opt/hermes", }; const externalRuntime = buildExternalRuntimeClosure({ pathRoot: targetRoot, ownerUid: disposableFixture ? (process.getuid?.() ?? 0) : 0, ownerGid: disposableFixture ? (process.getgid?.() ?? 0) : 0, disposableFixture, ...(sourceRoots ?? {}), ...(disposableFixture && input.hermesBridgeContract ? { hermesBridgeContract: input.hermesBridgeContract } : {}), }); const ownerUid = disposableFixture ? (process.getuid?.() ?? 0) : 0; const ownerGid = disposableFixture ? (process.getgid?.() ?? 0) : 0; const generated = configs(config, signing, ownerUid, ownerGid); const installed = installAgentHostServices({ targetRoot, packageRoot: input.packageRoot ?? PACKAGE_ROOT, ownerUid, ownerGid, disposableFixture, ...(disposableFixture && input.accountProvisioner ? { accountProvisioner: input.accountProvisioner } : {}), ...generated, }); if (!installed.ok) throw new Error(installed.code); const configurationDigest = sha256(JSON.stringify(stable(generated))); const receipt = { version: RECEIPT_VERSION, producer: "sellable-agent-host-bootstrap", target: config.target, host: config.host, enrollment: { id: config.enrollment.id, workerId: config.enrollment.workerId, audience: "sellable-agent-worker/v1", keyGeneration: config.enrollment.keyGeneration, publicKeyHash: signing.publicKeyHash, enrollmentFingerprint: signing.enrollmentFingerprint, capabilities: ["claim", "heartbeat", "secret-consume", "complete"], expiresAt: new Date(config.enrollment.expiresAt).toISOString(), }, release: { ...config.release, serviceManifestDigest: installed.manifestDigest, externalRuntimeClosureDigest: externalRuntime.closureDigest, configurationDigest, }, secretScan: { unexpectedMatches: 0 }, observedAt: (input.now ?? new Date()).toISOString(), }; if (SECRET.test(JSON.stringify(receipt))) throw new Error("host bootstrap receipt rejected"); atomicFile( targetPath(targetRoot, PATHS.receipt), `${JSON.stringify(receipt, null, 2)}\n`, 0o600, { uid: 0, gid: 0, disposableFixture } ); return { ok: true, receipt, workerConfig: generated.workerConfig, helperConfig: generated.helperConfig, }; } export const SELLABLE_AGENT_HOST_BOOTSTRAP_VERSION = VERSION; export const SELLABLE_AGENT_HOST_BOOTSTRAP_PATHS = PATHS;