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