UNPKG

@sellable/install

Version:

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

1,192 lines (1,150 loc) • 37.3 kB
#!/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, readdirSync, realpathSync, renameSync, rmSync, writeFileSync, } from "node:fs"; import { dirname, isAbsolute, join, relative, sep } 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 SKILLS_BRIDGE_SCHEMA_VERSION = "sellable-agent-skills-bridge/v1"; export const SKILLS_PROOF_SCHEMA_VERSION = "sellable-agent-skills-proof/v1"; export const SKILLS_MAX_COUNT = 256; export const SKILL_MAX_FILES = 256; export const SKILL_MAX_TOTAL_BYTES = 2 * 1024 * 1024; export const SKILL_MAX_TEXT_BYTES = 256 * 1024; export const SKILLS_REQUEST_MAX_BYTES = 512 * 1024; const PROFILE_ID = /^[a-z0-9][a-z0-9_-]{0,127}$/; const SHA256 = /^[a-f0-9]{64}$/; const SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/; const PYTHON = "/opt/hermes/.venv/bin/python3"; const SUPPORT_ROOTS = new Set(["references", "templates"]); const SOURCE_MANAGED = new Set(["bundled", "hub", "plugin", "org", "external"]); 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 FlySkillsBridgeError extends Error { constructor(code) { super(code); this.name = "FlySkillsBridgeError"; this.code = code; } } function fail(code) { throw new FlySkillsBridgeError(code); } function validProfileId(value) { if (typeof value !== "string" || !PROFILE_ID.test(value)) { fail("skills_profile_id_rejected"); } return value; } function utf8(bytes, code = "skills_utf8_rejected") { try { return new TextDecoder("utf-8", { fatal: true }).decode(bytes); } catch { fail(code); } } function safeRelativePath(value, { supporting = false } = {}) { if ( typeof value !== "string" || value.length < 1 || value.length > 256 || value.includes("\\") || value.includes("\0") || isAbsolute(value) ) { fail("skills_file_path_rejected"); } const parts = value.split("/"); if (parts.some((part) => !part || part === "." || part === "..")) { fail("skills_file_path_rejected"); } if (supporting && (!SUPPORT_ROOTS.has(parts[0]) || parts.length < 2)) { fail("skills_support_path_rejected"); } return parts.join("/"); } function regularFile(path, maxBytes = SKILL_MAX_TOTAL_BYTES) { const link = lstatSync(path); if ( link.isSymbolicLink() || !link.isFile() || link.nlink !== 1 || link.size > maxBytes ) { fail("skills_file_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("skills_file_changed"); } return readFileSync(fd); } finally { closeSync(fd); } } function contained(root, candidate) { const rel = relative(root, candidate); return ( rel !== "" && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel) ); } function inspectTree(skillRoot, limits) { const entries = []; let totalBytes = 0; let incompleteReason = null; const walk = (directory) => { for (const name of readdirSync(directory).sort()) { const path = join(directory, name); const link = lstatSync(path); if (link.isSymbolicLink()) { incompleteReason ??= "symlink"; continue; } if (link.isDirectory()) { walk(path); continue; } if (!link.isFile() || link.nlink !== 1) { incompleteReason ??= "non_regular"; continue; } if (entries.length >= limits.maxFiles) { incompleteReason ??= "file_count"; continue; } if (totalBytes + link.size > limits.maxTotalBytes) { incompleteReason ??= "total_bytes"; continue; } const bytes = regularFile(path, limits.maxTotalBytes); totalBytes += bytes.byteLength; entries.push({ relativePath: relative(skillRoot, path).split(sep).join("/"), byteLength: bytes.byteLength, fileSha256: sha256(bytes), }); } }; walk(skillRoot); return { entries, totalBytes, incompleteReason }; } function normalizedProvenance(item, editable) { const upstream = String( item.provenance ?? item.source ?? "unknown" ).toLowerCase(); if (!editable) { if (String(item.name).toLowerCase() === "sellable") return "sellable"; return SOURCE_MANAGED.has(upstream) ? upstream : "external"; } return "profile"; } function skillId(item, provenance, relativeDirectory) { return sha256( stableJson({ name: String(item.name), provenance, relativeDirectory, }) ); } function disabledBytes(native) { if (typeof native.disabledConfig === "string") { return Buffer.from(native.disabledConfig, "utf8"); } return Buffer.from( `${stableJson([...new Set(native.disabledNames ?? [])].sort())}\n`, "utf8" ); } function normalizeEnumeration(native, paths, limits = {}) { const maxSkills = limits.maxSkills ?? SKILLS_MAX_COUNT; const treeLimits = { maxFiles: limits.maxFiles ?? SKILL_MAX_FILES, maxTotalBytes: limits.maxTotalBytes ?? SKILL_MAX_TOTAL_BYTES, }; if ( !native || !Array.isArray(native.skills) || !Array.isArray(native.disabledNames) ) { fail("skills_native_enumeration_rejected"); } const disabled = new Set(native.disabledNames.map(String)); const profileSkillsRoot = join(paths.profileRoot, "skills"); const canonicalProfileSkillsRoot = existsSync(profileSkillsRoot) ? realpathSync(profileSkillsRoot) : profileSkillsRoot; const skills = []; let incompleteReason = native.complete === false ? String(native.reason ?? "native") : null; for (const item of native.skills.slice(0, maxSkills)) { if ( !item || !SAFE_NAME.test(String(item.name ?? "")) || typeof item.path !== "string" ) { incompleteReason ??= "native_entry"; continue; } let canonicalRoot; try { const link = lstatSync(item.path); if (link.isSymbolicLink() || !link.isDirectory()) { incompleteReason ??= "symlink"; continue; } canonicalRoot = realpathSync(item.path); } catch { incompleteReason ??= "missing"; continue; } const relativeDirectory = contained( canonicalProfileSkillsRoot, canonicalRoot ) ? relative(canonicalProfileSkillsRoot, canonicalRoot).split(sep).join("/") : null; const upstream = String( item.provenance ?? item.source ?? "unknown" ).toLowerCase(); const editable = Boolean( relativeDirectory && !relativeDirectory.split("/").includes(".org") && !SOURCE_MANAGED.has(upstream) && String(item.name).toLowerCase() !== "sellable" ); const provenance = normalizedProvenance(item, editable); const tree = inspectTree(canonicalRoot, treeLimits); incompleteReason ??= tree.incompleteReason; const skillMd = tree.entries.find( (entry) => entry.relativePath === "SKILL.md" ); if (!skillMd) { incompleteReason ??= "manifest_missing"; continue; } let manifestContent = null; if (skillMd.byteLength <= SKILL_MAX_TEXT_BYTES) { try { manifestContent = utf8( regularFile(join(canonicalRoot, "SKILL.md"), SKILL_MAX_TEXT_BYTES) ); } catch { incompleteReason ??= "manifest_utf8"; } } else { incompleteReason ??= "manifest_bytes"; } const manifestIdentity = { provenance, relativeDirectory, entries: tree.entries, }; skills.push({ skillId: skillId(item, provenance, relativeDirectory), name: String(item.name), description: String(item.description ?? "").slice(0, 2000), category: item.category == null ? null : String(item.category).slice(0, 128), provenance, editable, enabled: !disabled.has(String(item.name)), usage: Number.isSafeInteger(item.usage) && item.usage >= 0 ? item.usage : 0, manifestContent, fileSha256: skillMd.fileSha256, skillManifestSha256: sha256(stableJson(manifestIdentity)), manifest: tree.entries, _canonicalRoot: canonicalRoot, }); } if (native.skills.length > maxSkills) incompleteReason ??= "skill_count"; skills.sort((left, right) => left.skillId.localeCompare(right.skillId)); const disabledConfigSha256 = sha256(disabledBytes(native)); const inventorySha256 = sha256( stableJson( skills.map( ({ _canonicalRoot: _private, manifestContent: _content, ...skill }) => skill ) ) ); return { complete: incompleteReason === null, ...(incompleteReason === null ? {} : { code: "inventory_incomplete", truncation: { reason: incompleteReason, observedSkillCount: native.skills.length, returnedSkillCount: skills.length, }, }), disabledConfigSha256, inventorySha256, skills, }; } function publicInventory(inventory) { return { ...inventory, skills: inventory.skills.map(({ _canonicalRoot, ...skill }) => skill), }; } const NATIVE_SCRIPT = String.raw` import hashlib, json, os, sys from pathlib import Path request = json.loads(sys.stdin.read()) from tools.skills_tool import _find_all_skills, skill_view from tools.skill_manager_tool import _find_skill, _create_skill, _edit_skill, _delete_skill, _write_file, _remove_file from hermes_cli.config import load_config from hermes_cli.skills_config import get_disabled_skills, save_disabled_skills def native_view(read_name, read_file=None): viewed = skill_view(read_name, file_path=read_file, preprocess=False) if read_file else skill_view(read_name, preprocess=False) return {"requestedName": read_name, "raw": viewed} def inventory(read_name=None, read_file=None): rows = _find_all_skills(skip_disabled=True) config = load_config() disabled = sorted(get_disabled_skills(config)) result = [] try: from tools.skill_usage import _read_bundled_manifest_names, _read_hub_installed_names, activity_count, load_usage bundled, hub, usage = _read_bundled_manifest_names(), _read_hub_installed_names(), load_usage() except Exception: bundled, hub, usage = set(), set(), {} for row in rows: name = str(row.get("name") or "") found = _find_skill(name) if not found: continue provenance = "hub" if name in hub else "bundled" if name in bundled else "agent" item = dict(row) item.update({"path": str(found["path"]), "provenance": provenance, "usage": activity_count(usage.get(name, {})) if usage else 0}) result.append(item) viewed = None if read_name: viewed = native_view(read_name, read_file) return {"complete": True, "skills": result, "disabledNames": disabled, "disabledConfig": json.dumps(disabled, separators=(",", ":")), "nativeView": viewed} op = request.get("operation") if op == "enumerate": output = inventory(request.get("readName"), request.get("readFile")) if "readNames" in request: output["nativeViews"] = [native_view(name) for name in request["readNames"]] elif op == "read": output = {"nativeView": native_view(request["readName"], request.get("readFile"))} elif op == "scan": from tools.skills_guard import scan_skill, should_allow_install # Phase 170 production receipt skills_apply_failed_rolled_back proved that # Hermes 0.20.0 rejects a string here; keep the native boundary path-typed. path = Path(request["path"]) scan = scan_skill(path, source="agent-created") allowed, reason = should_allow_install(scan) output = {"ok": allowed is True, "reason": str(reason)} elif op == "mutate": action = request["action"] name = request["name"] if action == "create": output = _create_skill(name, request["content"], request.get("category")) elif action == "edit": output = _edit_skill(name, request["content"]) elif action == "delete": output = _delete_skill(name, "") elif action == "write_file": output = _write_file(name, request["filePath"], request["content"]) elif action == "remove_file": output = _remove_file(name, request["filePath"]) elif action == "toggle": config = load_config(); disabled = get_disabled_skills(config) if request["enabled"]: disabled.discard(name) else: disabled.add(name) save_disabled_skills(config, disabled) output = {"success": True} else: raise RuntimeError("operation") else: raise RuntimeError("operation") sys.stdout.write(json.dumps(output, separators=(",", ":"))) `; function runNative( payload, paths, { python = PYTHON, timeoutMs = 20_000 } = {} ) { const child = spawnSync(python, ["-c", NATIVE_SCRIPT], { input: JSON.stringify(payload), encoding: "utf8", timeout: timeoutMs, 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.error?.code === "ETIMEDOUT") fail("skills_native_call_timeout"); if (child.status !== 0 || child.signal || child.error) fail("skills_native_call_failed"); try { return JSON.parse(child.stdout); } catch { fail("skills_native_output_rejected"); } } export function normalizeHermesNativeSkillView(value) { const requestedName = typeof value?.requestedName === "string" ? value.requestedName : ""; let viewed = value?.raw; if (typeof viewed === "string") { try { const decoded = JSON.parse(viewed); viewed = decoded; } catch { return { success: true, name: requestedName, content: viewed, contentSha256: sha256(Buffer.from(viewed, "utf8")), }; } if (typeof viewed === "string") { return { success: true, name: requestedName, content: viewed, contentSha256: sha256(Buffer.from(viewed, "utf8")), }; } } const content = viewed && typeof viewed === "object" && !Array.isArray(viewed) ? viewed.content : null; return { success: Boolean(viewed) && typeof viewed === "object" && !Array.isArray(viewed) && viewed.success === true, name: viewed && typeof viewed === "object" && !Array.isArray(viewed) ? String(viewed.name ?? "") : "", content, contentSha256: typeof content === "string" ? sha256(Buffer.from(content, "utf8")) : null, }; } function normalizeNativeResult(result) { return { ...result, ...(result?.nativeView ? { nativeView: normalizeHermesNativeSkillView(result.nativeView) } : {}), ...(Array.isArray(result?.nativeViews) ? { nativeViews: result.nativeViews.map(normalizeHermesNativeSkillView) } : {}), }; } function defaultAdapter(paths, timeoutMs) { return { enumerate: ({ readName, readFile, readNames } = {}) => normalizeNativeResult( runNative( { operation: "enumerate", readName, readFile, readNames }, paths, { timeoutMs, } ) ), read: ({ readName, readFile } = {}) => normalizeNativeResult( runNative({ operation: "read", readName, readFile }, paths, { timeoutMs, }) ), scan: (path) => runNative({ operation: "scan", path }, paths, { timeoutMs }), mutate: (mutation) => runNative({ operation: "mutate", ...mutation }, paths, { timeoutMs }), }; } function assertExactKeys(value, allowed) { if (!value || typeof value !== "object" || Array.isArray(value)) fail("skills_request_rejected"); if (Object.keys(value).some((key) => !allowed.has(key))) fail("skills_request_rejected"); } function assertRequest(request) { const common = ["operation"]; const shapes = { list: common, verify: [...common, "names"], read: [...common, "skillId", "filePath"], edit: [...common, "skillId", "content", "expectedFileSha256"], toggle: [...common, "skillId", "enabled", "disabledConfigSha256"], create: [ ...common, "name", "content", "category", "expectedFileAbsent", "inventorySha256", ], delete: [...common, "skillId", "skillManifestSha256"], write_file: [ ...common, "skillId", "filePath", "content", "expectedFileSha256", "expectedFileAbsent", "skillManifestSha256", ], remove_file: [ ...common, "skillId", "filePath", "expectedFileSha256", "skillManifestSha256", ], }; const keys = shapes[request?.operation]; if (!keys) fail("skills_operation_rejected"); assertExactKeys(request, new Set(keys)); if ( request.operation === "verify" && (!Array.isArray(request.names) || request.names.length < 1 || request.names.length > SKILLS_MAX_COUNT || request.names.some( (name) => typeof name !== "string" || !SAFE_NAME.test(name) ) || new Set(request.names).size !== request.names.length) ) fail("skills_names_rejected"); if ( request.operation === "create" && (!SAFE_NAME.test(request.name ?? "") || (request.category != null && !SAFE_NAME.test(request.category))) ) { fail("skills_category_rejected"); } if (request.skillId !== undefined && !SHA256.test(request.skillId)) fail("skills_identity_rejected"); if (request.content !== undefined) { if ( typeof request.content !== "string" || Buffer.byteLength(request.content) > SKILL_MAX_TEXT_BYTES ) { fail("skills_content_rejected"); } } if (request.filePath !== undefined) safeRelativePath(request.filePath, { supporting: request.operation !== "read", }); for (const field of [ "expectedFileSha256", "disabledConfigSha256", "inventorySha256", "skillManifestSha256", ]) { if (request[field] !== undefined && !SHA256.test(request[field])) fail("skills_cas_rejected"); } return request; } function findSkill(inventory, id, { editable = false } = {}) { const skill = inventory.skills.find((entry) => entry.skillId === id); if (!skill) fail("skills_identity_not_found"); if (editable && (!inventory.complete || !skill.editable)) fail("skills_mutation_refused"); return skill; } function assertNativeView(proof, name, content, code) { if ( proof?.success !== true || proof.name !== name || typeof proof.content !== "string" || proof.content !== content || proof.contentSha256 !== sha256(Buffer.from(content, "utf8")) ) { fail(code); } } function backupTree(skillRoot) { const snapshot = []; const walk = (directory) => { for (const name of readdirSync(directory).sort()) { const path = join(directory, name); const stat = lstatSync(path); if (stat.isSymbolicLink()) fail("skills_backup_rejected"); if (stat.isDirectory()) walk(path); else if (stat.isFile()) snapshot.push({ relativePath: relative(skillRoot, path), bytes: regularFile(path), mode: stat.mode & 0o777, }); else fail("skills_backup_rejected"); } }; walk(skillRoot); return snapshot; } function restoreTree(skillRoot, snapshot) { rmSync(skillRoot, { recursive: true, force: true }); mkdirSync(skillRoot, { recursive: true, mode: 0o700 }); for (const entry of snapshot) { const path = join(skillRoot, entry.relativePath); mkdirSync(dirname(path), { recursive: true, mode: 0o700 }); writeFileSync(path, entry.bytes, { mode: entry.mode }); } } function atomicProof(paths, inventory) { const root = join(paths.profileRoot, ".sellable-agent"); mkdirSync(root, { recursive: true, mode: 0o700 }); const path = join(root, "skills-proof.json"); const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`; const value = { schemaVersion: SKILLS_PROOF_SCHEMA_VERSION, inventorySha256: inventory.inventorySha256, disabledConfigSha256: inventory.disabledConfigSha256, }; writeFileSync(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600 }); renameSync(temporary, path); } function executeLocked(request, paths, adapter, options) { // Startup proves the entire managed bundle in one Python invocation and one // profile lock. Re-importing Hermes for list + each read made the second live // serving pass exceed its deadline even after every duplicate was retired. const beforeNative = request.operation === "verify" ? adapter.enumerate({ readNames: request.names }) : adapter.enumerate(); const before = normalizeEnumeration(beforeNative, paths, options.limits); if (request.operation === "verify") { if ( !Array.isArray(beforeNative.nativeViews) || beforeNative.nativeViews.length !== request.names.length ) fail("skills_native_readback_failed"); const proofs = request.names.map((name, index) => { const matches = before.skills.filter((skill) => skill.name === name); if (matches.length !== 1 || !matches[0].enabled) fail("skills_native_readback_failed"); const skill = matches[0]; assertNativeView( beforeNative.nativeViews[index], name, skill.manifestContent, "skills_native_readback_failed" ); return { name, fileSha256: beforeNative.nativeViews[index].contentSha256, }; }); atomicProof(paths, before); return { ok: true, proofs }; } if (request.operation === "list") { atomicProof(paths, before); return { ok: true, schemaVersion: SKILLS_BRIDGE_SCHEMA_VERSION, status: "proven", ...publicInventory(before), }; } if (request.operation === "read") { const skill = findSkill(before, request.skillId); const relativePath = request.filePath ? safeRelativePath(request.filePath) : "SKILL.md"; const manifest = skill.manifest.find( (entry) => entry.relativePath === relativePath ); if (!manifest) return { ok: false, code: before.complete ? "skills_file_not_found" : "inventory_incomplete", }; if (manifest.byteLength > SKILL_MAX_TEXT_BYTES) return { ok: false, code: "skills_file_too_large" }; const content = utf8( regularFile( join(skill._canonicalRoot, relativePath), SKILL_MAX_TEXT_BYTES ) ); const proof = adapter.read({ readName: skill.name, readFile: relativePath === "SKILL.md" ? undefined : relativePath, }); assertNativeView( proof.nativeView, skill.name, content, "skills_native_readback_failed" ); atomicProof(paths, before); return { ok: true, schemaVersion: SKILLS_BRIDGE_SCHEMA_VERSION, status: "proven", skill: publicInventory({ ...before, skills: [skill] }).skills[0], filePath: relativePath, content, fileSha256: sha256(Buffer.from(content, "utf8")), inventorySha256: before.inventorySha256, disabledConfigSha256: before.disabledConfigSha256, }; } if (!before.complete) return { ok: false, code: "inventory_incomplete", inventorySha256: before.inventorySha256, }; let skill = request.operation === "create" ? null : findSkill(before, request.skillId, { editable: true }); if (request.operation === "create") { if ( !SAFE_NAME.test(request.name ?? "") || request.expectedFileAbsent !== true || request.inventorySha256 !== before.inventorySha256 ) { return { ok: false, code: "skills_conflict", inventorySha256: before.inventorySha256, }; } if (before.skills.some((entry) => entry.name === request.name)) return { ok: false, code: "skills_absent_conflict" }; } if ( request.operation === "edit" && request.expectedFileSha256 !== skill.fileSha256 ) return { ok: false, code: "skills_file_conflict", fileSha256: skill.fileSha256, }; if ( request.operation === "toggle" && request.disabledConfigSha256 !== before.disabledConfigSha256 ) return { ok: false, code: "skills_disabled_config_conflict", disabledConfigSha256: before.disabledConfigSha256, }; if ( ["delete", "write_file", "remove_file"].includes(request.operation) && request.skillManifestSha256 !== skill.skillManifestSha256 ) return { ok: false, code: "skills_manifest_conflict", skillManifestSha256: skill.skillManifestSha256, }; let supportingExisting = null; if (["write_file", "remove_file"].includes(request.operation)) { const relativePath = safeRelativePath(request.filePath, { supporting: true, }); supportingExisting = skill.manifest.find((entry) => entry.relativePath === relativePath) ?? null; if (request.operation === "remove_file" || supportingExisting) { if ( !supportingExisting || request.expectedFileSha256 !== supportingExisting.fileSha256 ) return { ok: false, code: "skills_file_conflict", fileSha256: supportingExisting?.fileSha256 ?? null, }; } else if (request.expectedFileAbsent !== true) return { ok: false, code: "skills_absent_conflict" }; } const skillRoot = skill?._canonicalRoot ?? join( paths.profileRoot, "skills", request.category ? join(request.category, request.name) : request.name ); const skillExisted = existsSync(skillRoot); const treeBackup = skillExisted ? backupTree(skillRoot) : []; const disabledPath = join(paths.profileRoot, "config.yaml"); const disabledBackup = existsSync(disabledPath) ? regularFile(disabledPath) : null; const proofPath = join( paths.profileRoot, ".sellable-agent", "skills-proof.json" ); const proofBackup = existsSync(proofPath) ? regularFile(proofPath, 4096) : null; const rollback = () => { if (skillExisted) restoreTree(skillRoot, treeBackup); else rmSync(skillRoot, { recursive: true, force: true }); if (disabledBackup !== null) writeFileSync(disabledPath, disabledBackup, { mode: 0o600 }); if (proofBackup !== null) writeFileSync(proofPath, proofBackup, { mode: 0o600 }); else rmSync(proofPath, { force: true }); }; try { const scanTarget = skillRoot; if (skillExisted) { const scan = adapter.scan(scanTarget); if (scan?.ok !== true) fail("skills_scan_refused"); } const noEffect = (request.operation === "edit" && sha256(Buffer.from(request.content, "utf8")) === skill.fileSha256) || (request.operation === "toggle" && request.enabled === skill.enabled) || (request.operation === "write_file" && supportingExisting !== null && sha256(Buffer.from(request.content, "utf8")) === supportingExisting.fileSha256); if (noEffect) { atomicProof(paths, before); return { ok: true, schemaVersion: SKILLS_BRIDGE_SCHEMA_VERSION, status: "noop", operation: request.operation, inventory: publicInventory(before), detail: publicInventory({ ...before, skills: [skill] }).skills[0], }; } const mutation = { action: request.operation, name: skill?.name ?? request.name, ...(request.content !== undefined ? { content: request.content } : {}), ...(request.category !== undefined ? { category: request.category } : {}), ...(request.filePath !== undefined ? { filePath: request.filePath } : {}), ...(request.enabled !== undefined ? { enabled: request.enabled } : {}), }; const result = adapter.mutate(mutation); if (result?.success !== true) return { ok: false, code: "skills_native_refused" }; if (existsSync(skillRoot)) { const scan = adapter.scan(skillRoot); if (scan?.ok !== true) fail("skills_scan_refused"); } const afterNative = adapter.enumerate({ readName: request.operation === "delete" ? undefined : mutation.name, }); const after = normalizeEnumeration(afterNative, paths, options.limits); if ( !after.complete || (request.operation !== "delete" && !afterNative.nativeView) ) fail("skills_readback_failed"); const afterSkill = after.skills.find((entry) => entry.name === mutation.name) ?? null; if ( request.operation === "delete" ? afterSkill !== null : afterSkill === null ) fail("skills_readback_failed"); if (afterSkill) { assertNativeView( afterNative.nativeView, afterSkill.name, afterSkill.manifestContent, "skills_readback_failed" ); } if ( (request.operation === "edit" && afterSkill.fileSha256 !== sha256(Buffer.from(request.content, "utf8"))) || (request.operation === "toggle" && (afterSkill.enabled !== request.enabled || after.disabledConfigSha256 === before.disabledConfigSha256)) || (request.operation === "create" && afterSkill.fileSha256 !== sha256(Buffer.from(request.content, "utf8"))) || (request.operation === "write_file" && !afterSkill.manifest.some( (entry) => entry.relativePath === request.filePath && entry.fileSha256 === sha256(Buffer.from(request.content, "utf8")) )) || (request.operation === "remove_file" && afterSkill.manifest.some( (entry) => entry.relativePath === request.filePath )) ) { fail("skills_readback_failed"); } atomicProof(paths, after); options.markDirty(paths.profileRoot); return { ok: true, schemaVersion: SKILLS_BRIDGE_SCHEMA_VERSION, status: "applied", operation: request.operation, effectId: randomUUID(), inventory: publicInventory(after), detail: afterSkill ? publicInventory({ ...after, skills: [afterSkill] }).skills[0] : null, }; } catch (error) { rollback(); try { adapter.enumerate(); } catch { return { ok: false, code: "skills_rollback_unproven" }; } return { ok: false, code: "skills_apply_failed_rolled_back", reason: error instanceof FlySkillsBridgeError ? error.code : "skills_bridge_failed", }; } } /** * @param {any} request * @param {{ * profileId?: string, * dataRoot?: string, * nativeAdapter?: {enumerate: Function, read: Function, scan: Function, mutate: Function} | null, * markDirty?: (profileRoot: string) => void, * lockHeld?: boolean, * nativeCallTimeoutMs?: number, * limits?: any, * }} [options] */ export function executeSkillsBridgeOperation( request, { profileId = process.env.SELLABLE_HERMES_PROFILE_ID, dataRoot = SOUL_BRIDGE_DATA_ROOT, nativeAdapter = null, markDirty = markHermesSnapshotDirty, lockHeld = false, nativeCallTimeoutMs = 20_000, limits = {}, } = {} ) { assertRequest(request); if ( !Number.isInteger(nativeCallTimeoutMs) || nativeCallTimeoutMs < 1 || nativeCallTimeoutMs > 60_000 ) fail("skills_native_timeout_rejected"); const id = validProfileId(profileId); const paths = soulProfilePaths(id, { dataRoot }); const adapter = nativeAdapter ?? defaultAdapter(paths, nativeCallTimeoutMs); if ( !adapter || !["enumerate", "read", "scan", "mutate"].every( (name) => typeof adapter[name] === "function" ) ) fail("skills_native_adapter_rejected"); if (typeof markDirty !== "function") fail("skills_dirty_marker_rejected"); const run = () => executeLocked(request, paths, adapter, { markDirty, limits }); return lockHeld ? run() : withProfileSoulLock(id, run, { dataRoot }); } /** * @param {any} request * @param {{ * procRoot?: string, * dataRoot?: string, * nativeAdapter?: {enumerate: Function, read: Function, scan: Function, mutate: Function} | null, * markDirty?: (profileRoot: string) => void, * callerProfileId?: string, * nativeCallTimeoutMs?: number, * limits?: any, * }} [options] */ export function executeExternalSkillsBridgeOperation( request, { procRoot = "/proc", dataRoot = SOUL_BRIDGE_DATA_ROOT, nativeAdapter = null, markDirty = markHermesSnapshotDirty, callerProfileId = process.env.SELLABLE_HERMES_PROFILE_ID, nativeCallTimeoutMs, limits = {}, } = {} ) { const gateway = discoverLiveHermesGateway({ procRoot, dataRoot }); if (callerProfileId && callerProfileId !== gateway.profileId) fail("skills_gateway_caller_profile_mismatch"); revalidateLiveHermesGateway(gateway, { procRoot, dataRoot }); const result = executeSkillsBridgeOperation(request, { profileId: gateway.profileId, dataRoot, nativeAdapter, nativeCallTimeoutMs, markDirty, limits, }); revalidateLiveHermesGateway(gateway, { procRoot, dataRoot }); return Object.freeze({ ...result, profileId: gateway.profileId, gatewayPid: gateway.pid, gatewayStartTicks: gateway.startTicks, }); } /** * @param {{ * actions?: Array<{type: string, id: string, nativeName?: string}>, * profileId?: string, * dataRoot?: string, * bridge?: typeof executeExternalSkillsBridgeOperation, * nativeCallTimeoutMs?: number, * }} [options] */ export function proveDefaultProfileNativeSkills({ actions, profileId, dataRoot = SOUL_BRIDGE_DATA_ROOT, bridge = executeExternalSkillsBridgeOperation, nativeCallTimeoutMs, } = {}) { const managed = (actions ?? []).filter((action) => action.type === "skill"); if (managed.length === 0) return { status: "EMPTY", digest: sha256("[]") }; const options = { dataRoot, callerProfileId: profileId, ...(nativeCallTimeoutMs === undefined ? {} : { nativeCallTimeoutMs }), }; for (const action of managed) { if (!SAFE_NAME.test(action.nativeName ?? "")) fail("default_profile_native_skill_name_missing"); } const verified = bridge( { operation: "verify", names: managed.map((action) => action.nativeName) }, options ); if ( !verified.ok || !Array.isArray(verified.proofs) || verified.proofs.length !== managed.length ) fail("default_profile_native_skill_readback_rejected"); const proofs = managed.map((action, index) => { const viewed = verified.proofs[index]; if ( viewed?.name !== action.nativeName || !SHA256.test(viewed.fileSha256 ?? "") ) fail("default_profile_native_skill_readback_rejected"); return { id: action.id, nativeName: action.nativeName, fileSha256: viewed.fileSha256, }; }); return { status: "PROVEN", digest: sha256(stableJson(proofs)) }; } export function computeSkillsConfigIdentity(profileRoot) { if (!isAbsolute(profileRoot) || !existsSync(profileRoot)) fail("skills_profile_root_rejected"); const canonical = realpathSync(profileRoot); const skillsRoot = join(canonical, "skills"); const tree = existsSync(skillsRoot) ? inspectTree(skillsRoot, { maxFiles: 4096, maxTotalBytes: 64 * 1024 * 1024, }) : { entries: [], incompleteReason: null }; if (tree.incompleteReason) fail("skills_profile_identity_incomplete"); const configPath = join(canonical, "config.yaml"); const configSha256 = existsSync(configPath) ? sha256(regularFile(configPath, 2 * 1024 * 1024)) : sha256(""); return Object.freeze({ inventorySha256: sha256(stableJson(tree.entries)), disabledConfigSha256: configSha256, }); } export function readSkillsProofObservation(profileRoot) { try { const path = join( realpathSync(profileRoot), ".sellable-agent", "skills-proof.json" ); const value = JSON.parse(regularFile(path, 4096).toString("utf8")); if ( value.schemaVersion !== SKILLS_PROOF_SCHEMA_VERSION || !SHA256.test(value.inventorySha256 ?? "") || !SHA256.test(value.disabledConfigSha256 ?? "") || Object.keys(value).length !== 3 ) return null; return Object.freeze({ inventorySha256: value.inventorySha256, disabledConfigSha256: value.disabledConfigSha256, }); } catch { return null; } }