UNPKG

@sellable/install

Version:

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

274 lines (261 loc) • 9 kB
/** * Phase 158 Plan 03 public contract. * * The implementation is intentionally introduced as a behavioral stub before * the RED matrix. The backend owns mutable-tag resolution; this module accepts * only the resulting frozen four-member tuple and one generation fence. */ export const ADMIN_RUNTIME_TUPLE_VERSION = "sellable-agent-admin-runtime-tuple/v1"; export const ADMIN_RUNTIME_GENERATION_VERSION = "sellable-agent-admin-runtime-generation/v1"; export const ADMIN_RUNTIME_TUPLE_MEMBERS = Object.freeze([ "@sellable/mcp", "@sellable/install", "@sellable/admin-mcp", "@sellable/admin-install", ]); import { createHash } from "node:crypto"; const SHA256 = /^[a-f0-9]{64}$/; const EXACT_VERSION = /^\d+\.\d+\.\d+(?:[-.A-Za-z0-9]+)?$/; const PACKAGE_DISPOSITIONS = new Set(["changed", "reused"]); const PUBLIC_ACTIONS = new Set(["INSTANTIATE", "REFRESH"]); const RESTORATION_ACTIONS = new Set([ "START", "RESTORE", "PROVIDER_START", "PROCESS_CRASH", "MACHINE_RESTART", "REPLACEMENT", ]); 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); } function digest(value) { return createHash("sha256").update(stableJson(value)).digest("hex"); } function byteDigest(value) { if (!(value instanceof Uint8Array)) throw new Error("admin_runtime_restore_ciphertext_invalid"); return createHash("sha256").update(value).digest("hex"); } export function validateAdminRuntimeTuple(tuple) { if ( !tuple || tuple.version !== ADMIN_RUNTIME_TUPLE_VERSION || tuple.registry !== "https://registry.npmjs.org" || !tuple.packages || stableJson(Object.keys(tuple.packages).sort()) !== stableJson([...ADMIN_RUNTIME_TUPLE_MEMBERS].sort()) ) { return { ok: false, code: "admin_runtime_tuple_members_invalid" }; } for (const name of ADMIN_RUNTIME_TUPLE_MEMBERS) { const item = tuple.packages[name]; if ( !item || item.name !== name || !EXACT_VERSION.test(item.version ?? "") || typeof item.integrity !== "string" || !/^sha512-[A-Za-z0-9+/]{80,}={0,2}$/.test(item.integrity) || !PACKAGE_DISPOSITIONS.has(item.disposition) ) { return { ok: false, code: "admin_runtime_tuple_package_invalid", packageName: name, }; } } if ( !SHA256.test(tuple.lockedTreeDigest ?? "") || !SHA256.test(tuple.artifactDigest ?? "") ) { return { ok: false, code: "admin_runtime_tuple_proof_invalid" }; } return { ok: true, tupleDigest: digest(tuple) }; } export async function resolveAdminTupleForPublicAction({ action, currentGeneration, resolveTuple, }) { if (PUBLIC_ACTIONS.has(action)) { if (typeof resolveTuple !== "function") throw new Error("admin_runtime_tuple_resolver_required"); const tuple = await resolveTuple(); const checked = validateAdminRuntimeTuple(tuple); if (!checked.ok) throw new Error(checked.code); return { resolution: "FROZEN", tuple: structuredClone(tuple), tupleDigest: checked.tupleDigest, }; } if (!RESTORATION_ACTIONS.has(action)) throw new Error("admin_runtime_action_rejected"); const checked = validateAdminRuntimeTuple(currentGeneration?.tuple); if (!checked.ok || checked.tupleDigest !== currentGeneration?.tupleDigest) { throw new Error("frozen_admin_runtime_generation_required"); } return { resolution: "REUSED", tuple: structuredClone(currentGeneration.tuple), tupleDigest: checked.tupleDigest, }; } export async function reconcileAdminRuntimeGeneration(input) { const checked = validateAdminRuntimeTuple(input.tuple); if (!checked.ok) return checked; if ( input.agentKind !== "ADMIN" || !Number.isSafeInteger(input.expectedFence) || input.expectedFence < 0 ) return { ok: false, code: "admin_runtime_generation_authority_invalid" }; const current = await input.store.load(); if ((current?.fence ?? 0) !== input.expectedFence) return { ok: false, code: "admin_runtime_generation_stale_fence" }; const nextFence = input.expectedFence + 1; const prepared = { ...input.generation, version: ADMIN_RUNTIME_GENERATION_VERSION, state: "PREPARING", fence: nextFence, tuple: structuredClone(input.tuple), tupleDigest: checked.tupleDigest, }; if (!(await input.store.cas(input.expectedFence, prepared))) return { ok: false, code: "admin_runtime_generation_stale_fence" }; let staged; try { staged = await input.ports.stage(prepared); const proof = await input.ports.proveBoth(staged, prepared); if ( proof?.ok !== true || stableJson(proof.servers) !== stableJson(["sellable", "sellable-admin"]) ) throw new Error("admin_runtime_dual_mcp_proof_failed"); const promoted = await input.ports.promote(staged, prepared, proof); const active = { ...prepared, state: "ACTIVE", profileDigest: promoted.profileDigest, proofDigest: digest(proof), }; if (!(await input.store.cas(nextFence, active))) throw new Error("admin_runtime_generation_stale_fence"); await input.ports.receipt(active); return { ok: true, status: "ACTIVE", generation: active }; } catch (error) { await input.ports.rollback?.(staged, current); return { ok: false, code: error instanceof Error ? error.message : "admin_runtime_reconcile_failed", priorGeneration: current ?? null, }; } } export async function restoreAdminRuntimeGeneration(input) { const generation = input.generation; const checked = validateAdminRuntimeTuple(generation?.tuple); if ( generation?.state !== "ACTIVE" || !checked.ok || checked.tupleDigest !== generation.tupleDigest || generation.agentKind !== "ADMIN" || input.machineVersion !== generation.machineVersion ) return { ok: false, code: "admin_runtime_restore_generation_invalid" }; if ("resolveTuple" in input || "registry" in input) return { ok: false, code: "admin_runtime_restore_registry_forbidden" }; try { const [artifact, ciphertext, recoveryKey] = await Promise.all([ input.ports.fetchArtifact(generation.artifactLocator), input.ports.fetchCiphertext(generation.recoveryLocator), input.ports.readRecoveryKey(generation.recoveryKeyId), ]); if ( digest(artifact) !== generation.artifactContentDigest || byteDigest(ciphertext) !== generation.recoveryCiphertextDigest ) throw new Error("admin_runtime_restore_digest_mismatch"); const settings = await input.ports.decrypt(ciphertext, recoveryKey); const staged = await input.ports.stage({ generation, artifact, settings }); const proof = await input.ports.proveBoth(staged, generation); if ( proof?.ok !== true || stableJson(proof.servers) !== stableJson(["sellable", "sellable-admin"]) ) throw new Error("admin_runtime_dual_mcp_proof_failed"); await input.ports.promote(staged, generation, proof); return { ok: true, status: "READY", registryCalls: 0 }; } catch (error) { return { ok: false, code: error instanceof Error ? error.message : "admin_runtime_restore_failed", }; } } export const ADMIN_DEMOTION_STAGES = Object.freeze([ "GATE", "REVOKE_DELIVERY", "REVOKE_KEYS", "STOP_ADMIN", "DELETE_RECOVERY", "DELETE_ADMIN_APP", "PROVISION_CUSTOMER", "PROVE_CUSTOMER", "COMMIT_CUSTOMER", ]); export function planAdminDemotion({ generation, expectedFence }) { if ( generation?.state !== "ACTIVE" || generation.agentKind !== "ADMIN" || generation.fence !== expectedFence ) throw new Error("admin_demotion_fence_rejected"); return { version: "sellable-agent-admin-demotion/v1", agentId: generation.agentId, workspaceId: generation.workspaceId, fence: expectedFence + 1, state: "DEMOTING", completedStages: [], }; } export async function resumeAdminDemotion({ plan, fence, ports }) { if (plan.state !== "DEMOTING" || plan.fence !== fence) return { ok: false, code: "admin_demotion_fence_rejected" }; const completed = new Set(plan.completedStages); for (const stage of ADMIN_DEMOTION_STAGES) { if (completed.has(stage)) continue; const result = await ports[stage](plan); if (result?.ok !== true) return { ok: false, status: "DEMOTING", stage, plan: { ...plan, completedStages: [...completed] }, }; if (stage === "PROVE_CUSTOMER" && result.byteIdentity !== true) return { ok: false, code: "customer_baseline_proof_required", stage }; completed.add(stage); } return { ok: true, status: "CUSTOMER", plan: { ...plan, state: "CLOSED", completedStages: [...completed] }, }; }