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