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