@sellable/install
Version:
One-command installer for Sellable MCP in Claude Code, Codex, and Hermes
786 lines (752 loc) • 27 kB
JavaScript
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import {
closeSync,
constants,
existsSync,
fstatSync,
lstatSync,
mkdirSync,
openSync,
readFileSync,
realpathSync,
renameSync,
rmSync,
writeFileSync,
} from "node:fs";
import { isAbsolute, join } from "node:path";
import {
SOUL_BRIDGE_DATA_ROOT,
discoverLiveHermesGateway,
revalidateLiveHermesGateway,
soulProfilePaths,
withProfileSoulLock,
} from "./fly-soul-bridge.mjs";
import { markHermesSnapshotDirty } from "./hermes-memory-snapshot.mjs";
export const CAPABILITIES_BRIDGE_SCHEMA_VERSION =
"sellable-agent-capabilities-bridge/v1";
export const CAPABILITIES_PROOF_SCHEMA_VERSION =
"sellable-agent-capabilities-proof/v1";
const CAPABILITIES_CONFIG_AUTHORITY_SCHEMA_VERSION =
"sellable-agent-capabilities-config-authority/v1";
export const CAPABILITIES_REQUEST_MAX_BYTES = 16 * 1024;
export const SELLABLE_BROWSER_TOOLSET_DEFAULT_MARKER =
".sellable-agent/browser-toolset-default-v1";
export const SELLABLE_DEFAULT_SLACK_TOOLSETS = Object.freeze([
"browser",
"clarify",
"memory",
"terminal",
"todo",
]);
// Deliberately excludes credential-bearing, external-account, platform-admin,
// media-generation, and default-off toolsets. These are the already-bundled
// core capabilities a Sellable Agent operator may narrow or restore for Slack.
export const SELLABLE_ALLOWED_TOOLSETS = Object.freeze([
"browser",
"clarify",
"code_execution",
"context_engine",
"cronjob",
"delegation",
"file",
"memory",
"session_search",
"skills",
"terminal",
"todo",
]);
export function sellableSlackToolsetsForMaterialization(
config,
{ browserDefaultApplied = false } = {}
) {
const configured = config?.platform_toolsets?.slack;
const source = Array.isArray(configured)
? configured
: SELLABLE_DEFAULT_SLACK_TOOLSETS;
const allowed = new Set(SELLABLE_ALLOWED_TOOLSETS);
const selected = new Set(
source.filter((name) => typeof name === "string" && allowed.has(name))
);
// Legacy profiles predate a browser toggle, so absence cannot represent an
// operator decision. Enable it exactly once; the marker then makes later
// explicit disablement durable across image/profile refreshes.
if (!browserDefaultApplied) selected.add("browser");
return [...selected].sort();
}
const ALLOWED_TOOLSETS = new Set(SELLABLE_ALLOWED_TOOLSETS);
const PROFILE_ID = /^[a-z0-9][a-z0-9_-]{0,127}$/;
const BOUND_ID = /^[A-Za-z0-9_-]{1,128}$/;
const TOOLSET_NAME = /^[a-z][a-z0-9_]{0,127}$/;
const TOOL_NAME = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
const SHA256 = /^[a-f0-9]{64}$/;
const VERSION = /^[0-9A-Za-z][0-9A-Za-z.+-]{0,127}$/;
const PYTHON = "/opt/hermes/.venv/bin/python3";
const EXPECTED_PACKAGES = Object.freeze({
sellable: "@sellable/mcp",
"sellable-admin": "@sellable/admin-mcp",
});
/**
* @typedef {{
* enumerate: (input: any) => any,
* toggle: (input: any) => any,
* }} CapabilitiesNativeAdapter
*/
/**
* @typedef {{
* profileId?: string,
* dataRoot?: string,
* nativeAdapter?: CapabilitiesNativeAdapter | null,
* packageSpecs?: Record<string, string> | null,
* markDirty?: (profileRoot: string) => void,
* lockHeld?: boolean,
* }} CapabilitiesBridgeOptions
*/
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
const 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);
};
export class FlyCapabilitiesBridgeError extends Error {
constructor(code) {
super(code);
this.name = "FlyCapabilitiesBridgeError";
this.code = code;
}
}
function fail(code) {
throw new FlyCapabilitiesBridgeError(code);
}
function regularFile(path, maxBytes = 2 * 1024 * 1024) {
const link = lstatSync(path);
if (
link.isSymbolicLink() ||
!link.isFile() ||
link.nlink !== 1 ||
link.size > maxBytes
) {
fail("capabilities_config_rejected");
}
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
try {
const opened = fstatSync(fd);
if (
!opened.isFile() ||
opened.dev !== link.dev ||
opened.ino !== link.ino ||
opened.nlink !== 1 ||
opened.size !== link.size
) {
fail("capabilities_config_changed");
}
return readFileSync(fd);
} finally {
closeSync(fd);
}
}
function assertExactKeys(value, allowed) {
if (!value || typeof value !== "object" || Array.isArray(value))
fail("capabilities_request_rejected");
if (Object.keys(value).some((key) => !allowed.has(key)))
fail("capabilities_request_rejected");
}
function expectedAliases(kind) {
return kind === "ADMIN" ? ["sellable", "sellable-admin"] : ["sellable"];
}
function assertRequest(request) {
const common = ["operation", "expectedRuntimeKind", "expectedWorkspaceId"];
const shapes = {
list: common,
toggle_tool: [...common, "toolsetName", "enabled", "configSha256"],
test_mcp: [...common, "serverName"],
};
const keys = shapes[request?.operation];
if (!keys) fail("capabilities_operation_rejected");
assertExactKeys(request, new Set(keys));
if (!["CUSTOMER", "ADMIN"].includes(request.expectedRuntimeKind))
fail("capabilities_runtime_kind_rejected");
if (!BOUND_ID.test(request.expectedWorkspaceId ?? ""))
fail("capabilities_workspace_rejected");
if (request.operation === "toggle_tool") {
if (
!TOOLSET_NAME.test(request.toolsetName ?? "") ||
!ALLOWED_TOOLSETS.has(request.toolsetName)
)
fail("capabilities_toolset_rejected");
if (
typeof request.enabled !== "boolean" ||
!SHA256.test(request.configSha256 ?? "")
)
fail("capabilities_cas_rejected");
}
if (
request.operation === "test_mcp" &&
!expectedAliases(request.expectedRuntimeKind).includes(request.serverName)
)
fail("capabilities_mcp_server_rejected");
return request;
}
function parsePackageSpec(spec, expectedName) {
if (typeof spec !== "string") fail("capabilities_package_identity_rejected");
const delimiter = spec.lastIndexOf("@");
const name = spec.slice(0, delimiter);
const version = spec.slice(delimiter + 1);
if (name !== expectedName || !VERSION.test(version))
fail("capabilities_package_identity_rejected");
return { packageName: name, version };
}
function boundedText(value, max) {
return typeof value === "string" && Buffer.byteLength(value, "utf8") <= max;
}
function normalizeNative(native, request, packageSpecs) {
if (!native || !Array.isArray(native.tools) || !Array.isArray(native.servers))
fail("capabilities_native_output_rejected");
const tools = native.tools.map((row) => {
if (
!row ||
!ALLOWED_TOOLSETS.has(row.name) ||
!boundedText(row.label, 256) ||
!boundedText(row.description, 2048) ||
row.platform !== "slack" ||
typeof row.enabled !== "boolean" ||
typeof row.configured !== "boolean" ||
!Array.isArray(row.tools) ||
row.tools.length > 256 ||
row.tools.some((name) => !TOOL_NAME.test(name))
)
fail("capabilities_tool_inventory_rejected");
const names = [...new Set(row.tools)].sort();
if (names.length !== row.tools.length)
fail("capabilities_tool_inventory_rejected");
return Object.freeze({
name: row.name,
label: row.label,
description: row.description,
platform: "slack",
enabled: row.enabled,
available: row.enabled && row.configured,
configured: row.configured,
policy: "allowed",
tools: Object.freeze(names),
});
});
tools.sort((left, right) => left.name.localeCompare(right.name));
if (
stableJson(tools.map((tool) => tool.name)) !==
stableJson([...SELLABLE_ALLOWED_TOOLSETS].sort())
)
fail("capabilities_tool_inventory_rejected");
const aliases = expectedAliases(request.expectedRuntimeKind);
const serverConfigAuthority = new Map();
const mcpServers = native.servers.map((row) => {
if (
!row ||
!aliases.includes(row.name) ||
row.enabled !== true ||
row.transport !== "stdio" ||
typeof row.workspaceLocked !== "boolean" ||
typeof row.requiresWorkspaceLock !== "boolean" ||
!SHA256.test(row.configAuthoritySha256 ?? "") ||
!["healthy", "unhealthy", "unknown"].includes(row.health) ||
!Array.isArray(row.tools) ||
row.tools.length > 1024 ||
row.tools.some((name) => !TOOL_NAME.test(name)) ||
!Array.isArray(row.configuredTools) ||
row.configuredTools.length > 1024 ||
row.configuredTools.some((name) => !TOOL_NAME.test(name)) ||
!Number.isSafeInteger(row.prompts) ||
row.prompts < 0 ||
!Number.isSafeInteger(row.resources) ||
row.resources < 0 ||
![null, "probe_failed", "not_tested"].includes(row.errorCode)
)
fail("capabilities_mcp_inventory_rejected");
const customerProductServer =
request.expectedRuntimeKind === "CUSTOMER" && row.name === "sellable";
if (
(customerProductServer &&
(!row.workspaceLocked || !row.requiresWorkspaceLock)) ||
(request.expectedRuntimeKind === "ADMIN" &&
(row.workspaceLocked || row.requiresWorkspaceLock))
)
fail("capabilities_workspace_lock_rejected");
const probedNames = [...new Set(row.tools)].sort();
const configuredNames = [...new Set(row.configuredTools)].sort();
if (
probedNames.length !== row.tools.length ||
configuredNames.length !== row.configuredTools.length ||
(customerProductServer && configuredNames.length === 0) ||
(customerProductServer &&
probedNames.length > 0 &&
stableJson(probedNames) !== stableJson(configuredNames)) ||
(!customerProductServer && configuredNames.length > 0)
)
fail("capabilities_mcp_inventory_rejected");
// A CUSTOMER Product MCP is policy-filtered from its live config. Outside
// a signed turn Hermes can successfully probe that server while receiving
// an empty tools/list, so the sealed, workspace-bound include list is the
// authoritative inventory. ADMIN has no customer workspace lock and its
// two direct live probes remain authoritative.
const names = customerProductServer ? configuredNames : probedNames;
serverConfigAuthority.set(row.name, row.configAuthoritySha256);
return Object.freeze({
name: row.name,
label: row.name === "sellable" ? "Sellable" : "Sellable Admin",
...parsePackageSpec(packageSpecs[row.name], EXPECTED_PACKAGES[row.name]),
enabled: true,
managed: true,
readOnly: true,
transport: "stdio",
workspaceLocked: row.workspaceLocked,
requiresWorkspaceLock: row.requiresWorkspaceLock,
health: row.health,
tools: Object.freeze(names),
prompts: row.prompts,
resources: row.resources,
errorCode: row.errorCode,
});
});
mcpServers.sort((left, right) => left.name.localeCompare(right.name));
if (
stableJson(mcpServers.map((server) => server.name)) !== stableJson(aliases)
)
fail("capabilities_mcp_inventory_rejected");
// The CAS token describes effective Capabilities authority, not YAML bytes.
// Hermes' native saver may normalize formatting or materialize defaults, so
// hashing the file made an off -> on round trip semantically exact but hash-
// unstable. Raw bytes are still fenced around enumeration in readInventory.
const configSha256 = sha256(
stableJson({
schemaVersion: CAPABILITIES_CONFIG_AUTHORITY_SCHEMA_VERSION,
platform: "slack",
tools: tools.map(({ name, enabled, configured }) => ({
name,
enabled,
configured,
})),
mcpServers: mcpServers.map(
({
name,
enabled,
transport,
workspaceLocked,
requiresWorkspaceLock,
}) => ({
name,
enabled,
transport,
workspaceLocked,
requiresWorkspaceLock,
configAuthoritySha256: serverConfigAuthority.get(name),
})
),
})
);
const inventory = Object.freeze({
complete: true,
configSha256,
tools: Object.freeze(tools),
mcpServers: Object.freeze(mcpServers),
});
return Object.freeze({
...inventory,
inventorySha256: sha256(stableJson(inventory)),
});
}
const NATIVE_SCRIPT = String.raw`
import hashlib, json, os, stat, sys
request = json.loads(sys.stdin.read())
from hermes_cli.config import load_config
from hermes_cli.mcp_config import _get_mcp_servers, _probe_single_server
from hermes_cli.tools_config import CONFIGURABLE_TOOLSETS, _get_platform_tools, _save_platform_tools, _toolset_has_keys
from toolsets import resolve_toolset
def configured_customer_tools(name, server):
if request["expectedRuntimeKind"] != "CUSTOMER" or name != "sellable":
return []
env = server.get("env") if isinstance(server.get("env"), dict) else {}
config_path = env.get("SELLABLE_CONFIG_PATH")
expected_path = os.path.join(request["profileRoot"], "sellable-mcp.json")
if config_path != expected_path:
raise RuntimeError("managed_server_config_path")
linked = os.lstat(config_path)
if (not stat.S_ISREG(linked.st_mode) or linked.st_nlink != 1 or
linked.st_size < 2 or linked.st_size > 2 * 1024 * 1024 or
linked.st_mode & 0o077):
raise RuntimeError("managed_server_config_file")
descriptor = os.open(config_path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
try:
opened = os.fstat(descriptor)
if (not stat.S_ISREG(opened.st_mode) or opened.st_dev != linked.st_dev or
opened.st_ino != linked.st_ino or opened.st_nlink != 1 or
opened.st_size != linked.st_size):
raise RuntimeError("managed_server_config_changed")
content = os.read(descriptor, opened.st_size + 1)
finally:
os.close(descriptor)
product = json.loads(content.decode("utf-8"))
include = product.get("tools", {}).get("include")
if (product.get("workspaceId") != request["expectedWorkspaceId"] or
product.get("activeWorkspaceId") != request["expectedWorkspaceId"] or
not isinstance(include, list) or not include or
any(not isinstance(tool_name, str) for tool_name in include) or
len(set(include)) != len(include)):
raise RuntimeError("managed_server_config_identity")
return sorted(include)
def enumerate_capabilities():
allowed = request["allowedToolsets"]
config = load_config()
catalog = {name: (label, description) for name, label, description in CONFIGURABLE_TOOLSETS}
enabled = _get_platform_tools(config, "slack", include_default_mcp_servers=False)
tool_rows = []
for name in allowed:
if name not in catalog:
raise RuntimeError("missing_policy_toolset")
label, description = catalog[name]
label_parts = str(label).strip().split(None, 1)
if len(label_parts) == 2 and not any(ch.isascii() and ch.isalnum() for ch in label_parts[0]):
label = label_parts[1]
tool_rows.append({
"name": name,
"label": str(label),
"description": str(description),
"platform": "slack",
"enabled": name in enabled,
"configured": bool(_toolset_has_keys(name, config)),
"tools": sorted(set(resolve_toolset(name, include_registry=False))),
})
servers = _get_mcp_servers(config)
expected_aliases = request["expectedAliases"]
if sorted(servers.keys()) != sorted(expected_aliases):
raise RuntimeError("managed_server_inventory")
server_rows = []
for name in expected_aliases:
server = servers[name]
env = server.get("env") if isinstance(server.get("env"), dict) else {}
details = {}
try:
found = _probe_single_server(name, server, connect_timeout=12, details=details)
health = "healthy"
tools = sorted(set(str(tool_name) for tool_name, _description in found))
prompts = int(details.get("prompts", 0) or 0)
resources = int(details.get("resources", 0) or 0)
error_code = None
except BaseException:
health, tools, prompts, resources, error_code = "unhealthy", [], 0, 0, "probe_failed"
server_rows.append({
"name": name,
"enabled": server.get("enabled", True) is not False,
"transport": "http" if server.get("url") else "stdio" if server.get("command") else "unknown",
"workspaceLocked": env.get("SELLABLE_LOCK_WORKSPACE_ID") == request["expectedWorkspaceId"],
"requiresWorkspaceLock": str(env.get("SELLABLE_REQUIRE_WORKSPACE_LOCK", "")) == "1",
"configAuthoritySha256": hashlib.sha256(json.dumps(server, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")).hexdigest(),
"health": health,
"tools": tools,
"configuredTools": configured_customer_tools(name, server),
"prompts": prompts,
"resources": resources,
"errorCode": error_code,
})
return {"tools": tool_rows, "servers": server_rows}
if request["operation"] == "enumerate":
output = enumerate_capabilities()
elif request["operation"] == "toggle":
config = load_config()
enabled = set(_get_platform_tools(config, "slack", include_default_mcp_servers=False))
if request["enabled"]: enabled.add(request["name"])
else: enabled.discard(request["name"])
_save_platform_tools(config, "slack", enabled)
output = {"ok": True}
else:
raise RuntimeError("operation")
sys.stdout.write(json.dumps(output, separators=(",", ":")))
`;
function runNative(payload, paths, { python = PYTHON } = {}) {
const child = spawnSync(python, ["-c", NATIVE_SCRIPT], {
input: JSON.stringify(payload),
encoding: "utf8",
timeout: 30_000,
maxBuffer: 4 * 1024 * 1024,
env: {
HOME: paths.dataRoot,
HERMES_HOME: paths.profileRoot,
HERMES_PROFILE: paths.profileId,
SELLABLE_HERMES_PROFILE_ID: paths.profileId,
SELLABLE_AGENT_RUNTIME: "1",
PYTHONDONTWRITEBYTECODE: "1",
PATH: "/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin",
},
});
if (child.status !== 0 || child.signal || child.error)
fail("capabilities_native_call_failed");
try {
return JSON.parse(child.stdout);
} catch {
fail("capabilities_native_output_rejected");
}
}
function defaultAdapter(paths) {
return {
enumerate: ({
expectedAliases: aliases,
expectedWorkspaceId,
expectedRuntimeKind,
probeServerName,
}) =>
runNative(
{
operation: "enumerate",
allowedToolsets: SELLABLE_ALLOWED_TOOLSETS,
expectedAliases: aliases,
expectedWorkspaceId,
expectedRuntimeKind,
profileRoot: paths.profileRoot,
...(probeServerName ? { probeServerName } : {}),
},
paths
),
toggle: ({ name, enabled }) =>
runNative({ operation: "toggle", name, enabled }, paths),
};
}
function runtimePackageSpecs(kind, provided) {
if (provided) return provided;
return kind === "ADMIN"
? {
sellable: process.env.SELLABLE_AGENT_PRODUCT_MCP_PACKAGE,
"sellable-admin": process.env.SELLABLE_AGENT_ADMIN_MCP_PACKAGE,
}
: { sellable: process.env.SELLABLE_AGENT_MCP_PACKAGE };
}
function atomicProof(paths, inventory) {
const root = join(paths.profileRoot, ".sellable-agent");
mkdirSync(root, { recursive: true, mode: 0o700 });
const path = join(root, "capabilities-proof.json");
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
writeFileSync(
temporary,
`${JSON.stringify({
schemaVersion: CAPABILITIES_PROOF_SCHEMA_VERSION,
inventorySha256: inventory.inventorySha256,
configSha256: inventory.configSha256,
})}\n`,
{ mode: 0o600 }
);
renameSync(temporary, path);
}
function readInventory(request, paths, adapter, packageSpecs) {
const configPath = join(paths.profileRoot, "config.yaml");
const rawConfigSha256 = sha256(regularFile(configPath));
const aliases = expectedAliases(request.expectedRuntimeKind);
const native = adapter.enumerate({
expectedAliases: aliases,
expectedWorkspaceId: request.expectedWorkspaceId,
expectedRuntimeKind: request.expectedRuntimeKind,
...(request.operation === "test_mcp"
? { probeServerName: request.serverName }
: {}),
});
if (sha256(regularFile(configPath)) !== rawConfigSha256)
fail("capabilities_config_changed");
return normalizeNative(native, request, packageSpecs);
}
function executeLocked(request, paths, adapter, options) {
const packageSpecs = runtimePackageSpecs(
request.expectedRuntimeKind,
options.packageSpecs
);
const before = readInventory(request, paths, adapter, packageSpecs);
if (request.operation === "list" || request.operation === "test_mcp") {
atomicProof(paths, before);
return {
ok: true,
schemaVersion: CAPABILITIES_BRIDGE_SCHEMA_VERSION,
status: "proven",
operation: request.operation,
...(request.operation === "test_mcp"
? { testedServer: request.serverName }
: {}),
...before,
};
}
if (request.configSha256 !== before.configSha256) {
return {
ok: false,
code: "capabilities_config_conflict",
configSha256: before.configSha256,
inventorySha256: before.inventorySha256,
};
}
const detail = before.tools.find((tool) => tool.name === request.toolsetName);
if (!detail) fail("capabilities_toolset_rejected");
if (detail.enabled === request.enabled) {
atomicProof(paths, before);
return {
ok: true,
schemaVersion: CAPABILITIES_BRIDGE_SCHEMA_VERSION,
status: "noop",
operation: "toggle_tool",
...before,
detail,
};
}
const configPath = join(paths.profileRoot, "config.yaml");
const configBackup = regularFile(configPath);
const proofPath = join(
paths.profileRoot,
".sellable-agent",
"capabilities-proof.json"
);
const proofBackup = existsSync(proofPath)
? regularFile(proofPath, 4096)
: null;
const rollback = () => {
writeFileSync(configPath, configBackup, { mode: 0o600 });
if (proofBackup) writeFileSync(proofPath, proofBackup, { mode: 0o600 });
else rmSync(proofPath, { force: true });
};
try {
const result = adapter.toggle({
name: request.toolsetName,
enabled: request.enabled,
platform: "slack",
});
if (result?.ok !== true) fail("capabilities_native_refused");
const after = readInventory(request, paths, adapter, packageSpecs);
const afterDetail = after.tools.find(
(tool) => tool.name === request.toolsetName
);
if (
!afterDetail ||
afterDetail.enabled !== request.enabled ||
after.configSha256 === before.configSha256 ||
stableJson(after.mcpServers.map((server) => server.name)) !==
stableJson(before.mcpServers.map((server) => server.name))
)
fail("capabilities_readback_failed");
atomicProof(paths, after);
options.markDirty(paths.profileRoot);
return {
ok: true,
schemaVersion: CAPABILITIES_BRIDGE_SCHEMA_VERSION,
status: "applied",
operation: "toggle_tool",
effectId: randomUUID(),
...after,
detail: afterDetail,
};
} catch (error) {
rollback();
try {
const restored = readInventory(request, paths, adapter, packageSpecs);
if (restored.configSha256 !== before.configSha256)
return { ok: false, code: "capabilities_rollback_unproven" };
} catch {
return { ok: false, code: "capabilities_rollback_unproven" };
}
return {
ok: false,
code: "capabilities_apply_failed_rolled_back",
reason:
error instanceof FlyCapabilitiesBridgeError
? error.code
: "capabilities_bridge_failed",
};
}
}
/**
* @param {any} request
* @param {CapabilitiesBridgeOptions} [options]
*/
export function executeCapabilitiesBridgeOperation(
request,
{
profileId = process.env.SELLABLE_HERMES_PROFILE_ID,
dataRoot = SOUL_BRIDGE_DATA_ROOT,
nativeAdapter = null,
packageSpecs = null,
markDirty = markHermesSnapshotDirty,
lockHeld = false,
} = {}
) {
assertRequest(request);
if (typeof profileId !== "string" || !PROFILE_ID.test(profileId))
fail("capabilities_profile_id_rejected");
const paths = soulProfilePaths(profileId, { dataRoot });
const adapter = nativeAdapter ?? defaultAdapter(paths);
if (
!adapter ||
typeof adapter.enumerate !== "function" ||
typeof adapter.toggle !== "function"
)
fail("capabilities_native_adapter_rejected");
if (typeof markDirty !== "function")
fail("capabilities_dirty_marker_rejected");
const run = () =>
executeLocked(request, paths, adapter, { packageSpecs, markDirty });
return lockHeld ? run() : withProfileSoulLock(profileId, run, { dataRoot });
}
export function executeExternalCapabilitiesBridgeOperation(
request,
{
procRoot = "/proc",
dataRoot = SOUL_BRIDGE_DATA_ROOT,
nativeAdapter = null,
packageSpecs = null,
markDirty = markHermesSnapshotDirty,
callerProfileId = process.env.SELLABLE_HERMES_PROFILE_ID,
} = {}
) {
const gateway = discoverLiveHermesGateway({ procRoot, dataRoot });
if (callerProfileId && callerProfileId !== gateway.profileId)
fail("capabilities_gateway_caller_profile_mismatch");
revalidateLiveHermesGateway(gateway, { procRoot, dataRoot });
const result = executeCapabilitiesBridgeOperation(request, {
profileId: gateway.profileId,
dataRoot,
nativeAdapter,
packageSpecs,
markDirty,
});
revalidateLiveHermesGateway(gateway, { procRoot, dataRoot });
return Object.freeze({
...result,
profileId: gateway.profileId,
gatewayPid: gateway.pid,
gatewayStartTicks: gateway.startTicks,
});
}
export function readCapabilitiesProofObservation(profileRoot) {
try {
if (!isAbsolute(profileRoot)) return null;
const value = JSON.parse(
regularFile(
join(
realpathSync(profileRoot),
".sellable-agent",
"capabilities-proof.json"
),
4096
).toString("utf8")
);
if (
value.schemaVersion !== CAPABILITIES_PROOF_SCHEMA_VERSION ||
!SHA256.test(value.inventorySha256 ?? "") ||
!SHA256.test(value.configSha256 ?? "") ||
Object.keys(value).length !== 3
)
return null;
return Object.freeze({
inventorySha256: value.inventorySha256,
configSha256: value.configSha256,
});
} catch {
return null;
}
}