@sellable/install
Version:
One-command installer for Sellable MCP in Claude Code, Codex, and Hermes
994 lines (965 loc) • 34.2 kB
JavaScript
import { createHash } from "node:crypto";
export const RUNTIME_DRIFT_ACTIONS = Object.freeze({
NONE: "NONE",
MCP: "RELOAD_MCP",
SKILLS: "RELOAD_SKILLS",
ENVIRONMENT: "RELOAD_ENVIRONMENT_OR_RESTART",
SLACK: "RESTART_GATEWAY",
PACKAGE: "REPROVISION_PROFILE",
});
export const LIFECYCLE_TRANSITION_ORDER = Object.freeze({
DISABLE_GATEWAY: [
"FENCE_DISPATCH",
"STOP_GATEWAY",
"PROVE_NO_DISPATCH",
"OBSERVE_OFF",
],
ENABLE_GATEWAY: [
"VERIFY_DESIRED",
"START_GATEWAY",
"PROVE_GATEWAY",
"OBSERVE_ON",
],
ARCHIVE: [
"FENCE_ROUTE_OUTPUT",
"DRAIN",
"STOP_GATEWAY",
"CLEAN_SECRETS",
"ACK_REVOKE",
"OBSERVE_ARCHIVED",
],
RESTORE: [
"VERIFY_SUCCESSOR",
"REHYDRATE",
"START_GATEWAY",
"PROVE_GATEWAY",
"OBSERVE_ON",
],
});
const REPAIR_ID = /^[A-Za-z0-9][A-Za-z0-9_./:-]{0,255}$/;
// The control plane owns the durable repair budget. This runtime seam only
// executes the single action returned by that authority.
export async function applyBoundedRuntimeRepair(input = {}) {
const {
workspaceId,
agentId,
agentKind,
assignedRuntimeArtifactId,
readinessIdentity,
ports,
} = input;
if (
![workspaceId, agentId, assignedRuntimeArtifactId, readinessIdentity].every(
(value) => REPAIR_ID.test(value ?? "")
) ||
!["CUSTOMER", "ADMIN"].includes(agentKind) ||
!ports ||
typeof ports.recordFailure !== "function" ||
typeof ports.restartLocal !== "function" ||
typeof ports.requestMachineRestart !== "function"
) {
return { ok: false, status: "REPAIR_INPUT_REJECTED" };
}
const authority = Object.freeze({
workspaceId,
agentId,
agentKind,
runtimeArtifactId: assignedRuntimeArtifactId,
readinessIdentity,
});
const decision = await ports.recordFailure(authority);
if (decision?.action === "LOCAL_RESTART") {
await ports.restartLocal(authority);
return { ok: true, status: "LOCAL_RESTART", exhausted: false };
}
if (decision?.action === "MACHINE_RESTART") {
await ports.requestMachineRestart(authority);
return { ok: true, status: "MACHINE_RESTART", exhausted: false };
}
if (decision?.action === "EXHAUSTED") {
return { ok: true, status: "EXHAUSTED", exhausted: true };
}
return { ok: false, status: "REPAIR_DECISION_REJECTED" };
}
const DRIFT_GROUPS = [
{
action: RUNTIME_DRIFT_ACTIONS.PACKAGE,
fields: [
"packageHash",
"revisionId",
"revisionSequence",
"nonSoulContentHash",
],
},
{
action: RUNTIME_DRIFT_ACTIONS.SLACK,
fields: [
"slackManifestHash",
"slackAppHash",
"slackTeamHash",
"slackBotHash",
"slackChannelHash",
"slackDefaultChannelHash",
"slackMemberAllowlistHash",
"slackChannelIds",
"slackDefaultChannelId",
"slackMemberAllowlist",
"slackGeneration",
"lifecycle",
],
},
{
action: RUNTIME_DRIFT_ACTIONS.ENVIRONMENT,
fields: ["providerFingerprint", "serviceCredentialGeneration"],
},
{
action: RUNTIME_DRIFT_ACTIONS.MCP,
fields: ["configHash", "mcpHash", "policyHash", "toolsHash"],
},
{ action: RUNTIME_DRIFT_ACTIONS.SKILLS, fields: ["skillsHash"] },
];
const RUNTIME_PROJECTION_FIELDS = [
"revisionId",
"revisionSequence",
"lifecycle",
"packageHash",
"configHash",
"mcpHash",
"policyHash",
"toolsHash",
"skillsHash",
"providerFingerprint",
"serviceCredentialGeneration",
"slackManifestHash",
"slackAppHash",
"slackTeamHash",
"slackBotHash",
"slackChannelHash",
"slackDefaultChannelHash",
"slackMemberAllowlistHash",
"slackChannelIds",
"slackDefaultChannelId",
"slackMemberAllowlist",
"slackGeneration",
"contentHash",
"nonSoulContentHash",
"soulHash",
];
const RESTORE_TARGET_FIELDS = RUNTIME_PROJECTION_FIELDS.filter(
(field) => !["packageHash", "soulHash", "contentHash"].includes(field)
);
function projectRestoreTarget(value) {
return Object.fromEntries(
RESTORE_TARGET_FIELDS.map((field) => [field, value[field]])
);
}
function restoreTargetsMatch(expected, actual) {
if (
stableJson(projectRestoreTarget(expected)) !==
stableJson(projectRestoreTarget(actual))
) {
return false;
}
const liveSoulDrift = expected.soulHash !== actual.soulHash;
return liveSoulDrift || expected.contentHash === actual.contentHash;
}
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");
}
export function projectRuntimeDriftTuple(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
const projection = Object.fromEntries(
RUNTIME_PROJECTION_FIELDS.map((field) => [field, value[field]])
);
if (
typeof projection.revisionId !== "string" ||
!projection.revisionId ||
!Number.isSafeInteger(projection.revisionSequence) ||
projection.revisionSequence < 1 ||
!["ABSENT", "APPLYING", "ON", "OFF", "ARCHIVED", "DEGRADED"].includes(
projection.lifecycle
) ||
!Number.isSafeInteger(projection.serviceCredentialGeneration) ||
projection.serviceCredentialGeneration < 1 ||
!Number.isSafeInteger(projection.slackGeneration) ||
projection.slackGeneration < 1
)
return null;
for (const field of RUNTIME_PROJECTION_FIELDS.filter(
(field) => field.endsWith("Hash") || field === "providerFingerprint"
)) {
if (!/^[a-f0-9]{64}$/.test(projection[field] ?? "")) return null;
}
if (
!Array.isArray(projection.slackChannelIds) ||
projection.slackChannelIds.length < 1 ||
projection.slackChannelIds.length > 100 ||
projection.slackChannelIds.some(
(value) => !/^C[A-Z0-9]{8,20}$/.test(value)
) ||
new Set(projection.slackChannelIds).size !==
projection.slackChannelIds.length ||
!/^C[A-Z0-9]{8,20}$/.test(projection.slackDefaultChannelId ?? "") ||
!Array.isArray(projection.slackMemberAllowlist) ||
projection.slackMemberAllowlist.length < 1 ||
projection.slackMemberAllowlist.length > 1_000 ||
projection.slackMemberAllowlist.some(
(value) => !/^[UW][A-Z0-9]{8,20}$/.test(value)
) ||
new Set(projection.slackMemberAllowlist).size !==
projection.slackMemberAllowlist.length
)
return null;
return projection;
}
export function classifyRuntimeDrift(desired, observed) {
const desiredProjection = projectRuntimeDriftTuple(desired);
const observedProjection = projectRuntimeDriftTuple(observed);
if (!desiredProjection || !observedProjection) {
return {
action: RUNTIME_DRIFT_ACTIONS.PACKAGE,
drift: ["invalidObservation"],
};
}
const liveSoulDrift =
stableJson(desiredProjection.soulHash) !==
stableJson(observedProjection.soulHash);
// SOUL is live Machine authority. Its raw hash and the legacy aggregate
// content hash that includes it are observation/mirror input, never
// lifecycle work. nonSoulContentHash independently binds every other
// server-owned file-hash input and remains lifecycle-enforced.
const aggregateContentDrift =
!liveSoulDrift &&
stableJson(desiredProjection.contentHash) !==
stableJson(observedProjection.contentHash);
let groups = DRIFT_GROUPS.map((group) => ({
action: group.action,
drift: group.fields.filter(
(field) =>
stableJson(desiredProjection[field]) !==
stableJson(observedProjection[field])
),
})).filter((group) => group.drift.length > 0);
const semanticConfigOwners = groups.filter((group) =>
[RUNTIME_DRIFT_ACTIONS.SLACK, RUNTIME_DRIFT_ACTIONS.ENVIRONMENT].includes(
group.action
)
);
const mcpFiles = groups.find(
(group) => group.action === RUNTIME_DRIFT_ACTIONS.MCP
);
if (
semanticConfigOwners.length === 1 &&
mcpFiles &&
mcpFiles.drift.every((field) => ["configHash", "mcpHash"].includes(field))
) {
semanticConfigOwners[0].drift.push(...mcpFiles.drift);
groups = groups.filter((group) => group !== mcpFiles);
}
if (groups.length > 1 || (aggregateContentDrift && groups.length === 0)) {
return {
action: RUNTIME_DRIFT_ACTIONS.PACKAGE,
drift: [
...groups.flatMap((group) => group.drift),
...(aggregateContentDrift ? ["contentHash"] : []),
],
};
}
if (groups.length === 1) {
return {
...groups[0],
drift: [
...groups[0].drift,
...(aggregateContentDrift ? ["contentHash"] : []),
],
};
}
return { action: RUNTIME_DRIFT_ACTIONS.NONE, drift: [] };
}
const REFRESH_PORTS = [
"reloadMcp",
"reloadSkills",
"reloadEnvironmentOrRestart",
"restartGateway",
"reprovisionProfile",
"observe",
"rollback",
];
function portsPresent(ports, names) {
return ports && names.every((name) => typeof ports[name] === "function");
}
const REFRESH_ACTION_PORT = {
[RUNTIME_DRIFT_ACTIONS.MCP]: "reloadMcp",
[RUNTIME_DRIFT_ACTIONS.SKILLS]: "reloadSkills",
[RUNTIME_DRIFT_ACTIONS.ENVIRONMENT]: "reloadEnvironmentOrRestart",
[RUNTIME_DRIFT_ACTIONS.SLACK]: "restartGateway",
[RUNTIME_DRIFT_ACTIONS.PACKAGE]: "reprovisionProfile",
};
export async function reconcileRuntimeRefresh(input = {}) {
const {
desired,
approvedDesired,
observed,
previousKnownGood,
operation,
ports,
} = input;
if (!portsPresent(ports, REFRESH_PORTS))
return { ok: false, status: "PORT_REJECTED" };
const desiredProjection = projectRuntimeDriftTuple(desired);
const approvedProjection = projectRuntimeDriftTuple(approvedDesired);
const observedProjection = projectRuntimeDriftTuple(observed);
const knownGoodProjection = projectRuntimeDriftTuple(previousKnownGood);
if (
!desiredProjection ||
!approvedProjection ||
!observedProjection ||
!knownGoodProjection
) {
return { ok: false, status: "OBSERVATION_REJECTED", observed };
}
if (stableJson(desiredProjection) !== stableJson(approvedProjection)) {
return { ok: false, status: "UNAPPROVED_DESIRED", observed };
}
if (
!operation ||
typeof operation.id !== "string" ||
!/^[1-9][0-9]{0,19}$/.test(operation.fence ?? "")
) {
return { ok: false, status: "OPERATION_REJECTED", observed };
}
const classification = classifyRuntimeDrift(desired, observed);
if (classification.action === RUNTIME_DRIFT_ACTIONS.NONE) {
const current = await ports.observe({ operation, desired });
if (
classifyRuntimeDrift(desired, current).action ===
RUNTIME_DRIFT_ACTIONS.NONE
) {
return {
ok: true,
status: "READY",
action: "NONE",
drift: [],
observed: current,
};
}
return proveRefreshRollback({
ports,
operation,
previousKnownGood,
observed,
action: "NONE",
drift: [],
});
}
const method = REFRESH_ACTION_PORT[classification.action];
try {
await ports[method]({
operation,
desired,
observed,
previousKnownGood,
drift: classification.drift,
});
const current = await ports.observe({ operation, desired });
if (
classifyRuntimeDrift(desired, current).action !==
RUNTIME_DRIFT_ACTIONS.NONE
) {
throw new Error("readback mismatch");
}
return {
ok: true,
status: "READY",
action: classification.action,
drift: classification.drift,
observed: current,
observationDigest: digest(current),
};
} catch {
return proveRefreshRollback({
ports,
operation,
previousKnownGood,
observed,
action: classification.action,
drift: classification.drift,
});
}
}
async function proveRefreshRollback({
ports,
operation,
previousKnownGood,
observed,
action,
drift,
}) {
try {
await ports.rollback({ operation, previousKnownGood });
const rollbackObserved = await ports.observe({
operation,
rollback: true,
desired: previousKnownGood,
target: previousKnownGood,
});
const expected = projectRuntimeDriftTuple(previousKnownGood);
const actual = projectRuntimeDriftTuple(rollbackObserved);
if (
expected &&
actual &&
classifyRuntimeDrift(expected, actual).action ===
RUNTIME_DRIFT_ACTIONS.NONE
) {
return {
ok: false,
status: "ROLLED_BACK",
action,
drift,
observed,
rollbackObservation: { lifecycle: rollbackObserved.lifecycle },
};
}
return {
ok: false,
status: "ROLLBACK_UNPROVEN",
action,
drift,
observed: actual ? rollbackObserved : observed,
};
} catch {
return { ok: false, status: "ROLLBACK_UNPROVEN", action, drift, observed };
}
}
const LIFECYCLE_PORTS = [
"fenceDispatch",
"fenceRouteOutput",
"drain",
"startGateway",
"stopGateway",
"proveNoDispatch",
"observeGateway",
"cleanSecrets",
"acknowledgeRevocation",
"rehydrate",
"rollback",
"reconcileArchive",
"persistProgress",
];
function validLifecycleState(state) {
return Boolean(
state &&
typeof state === "object" &&
/^[0-9]+$/.test(state.acceptedFence ?? "") &&
typeof state.receipts === "object" &&
state.receipts !== null
);
}
function validOperation(operation) {
return Boolean(
operation &&
typeof operation.id === "string" &&
operation.id &&
typeof operation.eventId === "string" &&
operation.eventId &&
Object.hasOwn(LIFECYCLE_TRANSITION_ORDER, operation.action) &&
/^[1-9][0-9]{0,19}$/.test(operation.fence ?? "") &&
typeof operation.targetRevisionId === "string" &&
operation.targetRevisionId &&
Number.isSafeInteger(operation.targetRevisionSequence) &&
operation.targetRevisionSequence > 0 &&
Number.isSafeInteger(operation.credentialGeneration) &&
operation.credentialGeneration > 0 &&
/^[a-f0-9]{64}$/.test(operation.profileDigest ?? "")
);
}
function clone(value) {
return JSON.parse(JSON.stringify(value));
}
function operationSemantic(operation) {
return {
id: operation.id,
eventId: operation.eventId,
action: operation.action,
fence: operation.fence,
targetRevisionId: operation.targetRevisionId,
targetRevisionSequence: operation.targetRevisionSequence,
credentialGeneration: operation.credentialGeneration,
profileDigest: operation.profileDigest,
targetRuntimeTuple: operation.targetRuntimeTuple ?? null,
};
}
function storedResult(state, operation) {
const receipt = state.receipts[operation.eventId];
if (!receipt) return null;
if (receipt.operationDigest !== digest(operationSemantic(operation))) {
return { ok: false, status: "DUPLICATE_CONFLICT", state };
}
return { ok: receipt.ok, status: receipt.status, state, receipt };
}
function finalizeSuccess(state, operation, status) {
const receipt = {
producer: "sellable-agent-runtime-reconciler",
subject: `agent-operation:${operation.id}`,
operationId: operation.id,
eventId: operation.eventId,
fence: operation.fence,
ok: true,
status,
operationDigest: digest(operationSemantic(operation)),
stateDigest: digest({ ...state, receipts: undefined }),
};
state.receipts[operation.eventId] = receipt;
return { ok: true, status, state, receipt };
}
function observationMatches(observation, operation, lifecycle) {
return Boolean(
observation &&
observation.lifecycle === lifecycle &&
observation.revisionId === operation.targetRevisionId &&
observation.credentialGeneration === operation.credentialGeneration &&
observation.profileDigest === operation.profileDigest &&
observation.secretsPresent === (lifecycle !== "ARCHIVED") &&
observation.process &&
observation.process.ready === (lifecycle === "ON") &&
(lifecycle === "ON"
? Number.isSafeInteger(observation.process.pid) &&
observation.process.pid > 1 &&
/^[1-9][0-9]{0,31}$/.test(observation.process.startTicks ?? "") &&
typeof observation.process.bootId === "string" &&
observation.process.bootId.length > 0
: observation.process.pid === null &&
observation.process.startTicks === null)
);
}
async function rollbackLifecycle(ports, operation, priorState) {
try {
await ports.rollback({ operation, priorState });
const expected = {
targetRevisionId: priorState.observedRevisionId,
credentialGeneration: priorState.credentialGeneration,
profileDigest: priorState.profileDigest,
};
const observed = await ports.observeGateway({
...expected,
rollback: true,
lifecycle: priorState.observedLifecycle,
});
if (observationMatches(observed, expected, priorState.observedLifecycle)) {
return {
ok: false,
status: "ROLLED_BACK",
state: priorState,
rollbackObservation: {
observedLifecycle: observed.lifecycle,
observedRevisionId: observed.revisionId,
acceptedFence: priorState.acceptedFence,
},
};
}
return { ok: false, status: "ROLLBACK_UNPROVEN", state: priorState };
} catch {
return { ok: false, status: "ROLLBACK_UNPROVEN", state: priorState };
}
}
export async function applyRuntimeLifecycle(input = {}) {
const { state, operation, ports } = input;
if (!validLifecycleState(state))
return { ok: false, status: "STATE_REJECTED", state };
if (!validOperation(operation))
return { ok: false, status: "OPERATION_REJECTED", state };
if (!portsPresent(ports, LIFECYCLE_PORTS))
return { ok: false, status: "PORT_REJECTED", state };
if (state.archiveProgress && operation.action !== "ARCHIVE") {
return { ok: false, status: "ARCHIVE_IN_PROGRESS", state };
}
const duplicate = storedResult(state, operation);
if (duplicate) return duplicate;
const operationDigest = digest(operationSemantic(operation));
const resumesArchive = Boolean(
operation.action === "ARCHIVE" &&
state.archiveProgress?.eventId === operation.eventId &&
state.archiveProgress?.operationDigest === operationDigest &&
state.archiveProgress?.fence === operation.fence
);
if (
!resumesArchive &&
BigInt(operation.fence) <= BigInt(state.acceptedFence)
) {
return { ok: false, status: "STALE_FENCE", state };
}
const priorState = clone(state);
const next = clone(state);
let archiveProgressState = null;
let archiveAttemptedStep = null;
try {
switch (operation.action) {
case "DISABLE_GATEWAY": {
if (
operation.targetRevisionId !== state.desiredRevisionId ||
operation.targetRevisionSequence < state.desiredRevisionSequence ||
operation.credentialGeneration !== state.credentialGeneration ||
operation.profileDigest !== state.profileDigest
)
return { ok: false, status: "DESIRED_MISMATCH", state };
await ports.fenceDispatch({ operation });
await ports.stopGateway({ operation });
const proof = await ports.proveNoDispatch({ operation });
if (!proof?.noDispatch) throw new Error("dispatch remained enabled");
const observed = await ports.observeGateway(operation);
if (!observationMatches(observed, operation, "OFF"))
throw new Error("off readback mismatch");
next.requestedLifecycle = "OFF";
next.observedLifecycle = "OFF";
next.desiredRevisionSequence = operation.targetRevisionSequence;
next.dispatchEnabled = false;
next.acceptedFence = operation.fence;
return finalizeSuccess(next, operation, "OFF");
}
case "ENABLE_GATEWAY": {
if (
operation.targetRevisionId !== state.desiredRevisionId ||
operation.targetRevisionSequence < state.desiredRevisionSequence ||
operation.credentialGeneration !== state.credentialGeneration ||
operation.profileDigest !== state.profileDigest ||
state.observedLifecycle === "ARCHIVED"
)
return { ok: false, status: "DESIRED_MISMATCH", state };
await ports.startGateway({ operation });
const observed = await ports.observeGateway(operation);
if (!observationMatches(observed, operation, "ON"))
throw new Error("on readback mismatch");
const tupleAware =
state.desiredTuple !== undefined ||
observed.runtimeTuple !== undefined;
const expectedTuple =
state.desiredTuple === undefined
? null
: projectRuntimeDriftTuple({
...state.desiredTuple,
lifecycle: "ON",
});
const observedTuple = projectRuntimeDriftTuple(observed.runtimeTuple);
if (
tupleAware &&
(!expectedTuple ||
!observedTuple ||
classifyRuntimeDrift(expectedTuple, observedTuple).action !==
RUNTIME_DRIFT_ACTIONS.NONE ||
!/^[a-f0-9]{64}$/.test(observed.runtimeTuple?.processHash ?? ""))
)
throw new Error("on runtime tuple mismatch");
next.requestedLifecycle = "ON";
next.observedLifecycle = "ON";
next.desiredRevisionSequence = operation.targetRevisionSequence;
next.dispatchEnabled = true;
if (observedTuple) {
next.desiredTuple = clone(observed.runtimeTuple);
next.observedTuple = clone(observed.runtimeTuple);
next.previousKnownGoodTuple = clone(observed.runtimeTuple);
next.runtimeHashes = {
package: observed.runtimeTuple.packageHash,
config: observed.runtimeTuple.configHash,
content: observed.runtimeTuple.contentHash,
process: observed.runtimeTuple.processHash,
};
}
next.acceptedFence = operation.fence;
return finalizeSuccess(next, operation, "ON");
}
case "ARCHIVE": {
if (
operation.targetRevisionId !== state.desiredRevisionId ||
operation.targetRevisionSequence < state.desiredRevisionSequence ||
operation.credentialGeneration !== state.credentialGeneration ||
operation.profileDigest !== state.profileDigest
)
return { ok: false, status: "DESIRED_MISMATCH", state };
const progress = resumesArchive
? clone(state.archiveProgress)
: {
eventId: operation.eventId,
operationDigest,
fence: operation.fence,
steps: [],
attemptedStep: null,
resumePredicate: null,
};
if (resumesArchive) {
const reconciliation = await ports.reconcileArchive({
operation,
state,
progress,
});
if (
!reconciliation?.ok ||
typeof reconciliation.observationId !== "string" ||
!reconciliation.observationId ||
!Array.isArray(reconciliation.confirmedSteps)
) {
return {
ok: false,
status: "ARCHIVE_RECONCILIATION_REQUIRED",
state,
};
}
const allowedSteps = [
"FENCE_ROUTE_OUTPUT",
"DRAIN",
"STOP_GATEWAY",
"CLEAN_SECRETS",
"ACK_REVOKE",
"OBSERVE_ARCHIVED",
];
const confirmedSteps = reconciliation.confirmedSteps;
const exactPrefix =
confirmedSteps.length <= allowedSteps.length &&
confirmedSteps.every(
(step, index) => step === allowedSteps[index]
) &&
progress.steps.every(
(step, index) => confirmedSteps[index] === step
);
if (!exactPrefix) {
return {
ok: false,
status: "ARCHIVE_RECONCILIATION_REJECTED",
state,
};
}
progress.steps = [...confirmedSteps];
if (confirmedSteps.includes("FENCE_ROUTE_OUTPUT")) {
next.routeFenced = true;
next.outputFenced = true;
next.dispatchEnabled = false;
next.routeFenceStatus = "CONFIRMED";
next.outputFenceStatus = "CONFIRMED";
next.dispatchStatus = "DISABLED";
}
if (confirmedSteps.includes("STOP_GATEWAY"))
next.gatewayStatus = "STOPPED";
if (confirmedSteps.includes("CLEAN_SECRETS")) {
next.secretsPresent = false;
next.secretDisposition = "REMOVED";
}
if (confirmedSteps.includes("ACK_REVOKE"))
next.revocationStatus = "ACKNOWLEDGED";
progress.reconciledObservationId = reconciliation.observationId;
progress.attemptedStep = null;
progress.resumePredicate = null;
}
next.requestedLifecycle = "ARCHIVED";
next.desiredRevisionSequence = operation.targetRevisionSequence;
next.acceptedFence = operation.fence;
const saveProgress = async (phase, step) => {
next.archiveProgress = clone(progress);
archiveProgressState = clone(next);
await ports.persistProgress({
operation,
phase,
step,
state: clone(next),
progress: clone(progress),
});
};
if (!progress.steps.includes("FENCE_ROUTE_OUTPUT")) {
archiveAttemptedStep = "FENCE_ROUTE_OUTPUT";
progress.attemptedStep = archiveAttemptedStep;
next.routeFenceStatus = "UNKNOWN";
next.outputFenceStatus = "UNKNOWN";
next.dispatchStatus = "UNKNOWN";
await saveProgress("BEFORE", archiveAttemptedStep);
await ports.fenceRouteOutput({ operation });
progress.steps.push("FENCE_ROUTE_OUTPUT");
progress.attemptedStep = null;
next.routeFenced = true;
next.outputFenced = true;
next.dispatchEnabled = false;
next.routeFenceStatus = "CONFIRMED";
next.outputFenceStatus = "CONFIRMED";
next.dispatchStatus = "DISABLED";
await saveProgress("AFTER", archiveAttemptedStep);
}
if (!progress.steps.includes("DRAIN")) {
archiveAttemptedStep = "DRAIN";
progress.attemptedStep = archiveAttemptedStep;
await saveProgress("BEFORE", archiveAttemptedStep);
const drained = await ports.drain({ operation });
if (!drained?.drained || drained.remaining !== 0)
throw new Error("drain incomplete");
progress.steps.push("DRAIN");
progress.attemptedStep = null;
await saveProgress("AFTER", archiveAttemptedStep);
}
if (!progress.steps.includes("STOP_GATEWAY")) {
archiveAttemptedStep = "STOP_GATEWAY";
progress.attemptedStep = archiveAttemptedStep;
next.gatewayStatus = "UNKNOWN";
await saveProgress("BEFORE", archiveAttemptedStep);
await ports.stopGateway({ operation });
progress.steps.push("STOP_GATEWAY");
progress.attemptedStep = null;
next.gatewayStatus = "STOPPED";
await saveProgress("AFTER", archiveAttemptedStep);
}
if (!progress.steps.includes("CLEAN_SECRETS")) {
archiveAttemptedStep = "CLEAN_SECRETS";
progress.attemptedStep = archiveAttemptedStep;
next.secretDisposition = "UNKNOWN";
next.secretsPresent = null;
await saveProgress("BEFORE", archiveAttemptedStep);
await ports.cleanSecrets({ operation });
progress.steps.push("CLEAN_SECRETS");
progress.attemptedStep = null;
next.secretDisposition = "REMOVED";
next.secretsPresent = false;
await saveProgress("AFTER", archiveAttemptedStep);
}
if (!progress.steps.includes("ACK_REVOKE")) {
archiveAttemptedStep = "ACK_REVOKE";
progress.attemptedStep = archiveAttemptedStep;
next.revocationStatus = "UNKNOWN";
await saveProgress("BEFORE", archiveAttemptedStep);
const revoke = await ports.acknowledgeRevocation({ operation });
if (!revoke?.acknowledged)
throw new Error("revocation not acknowledged");
progress.steps.push("ACK_REVOKE");
progress.attemptedStep = null;
next.revocationStatus = "ACKNOWLEDGED";
await saveProgress("AFTER", archiveAttemptedStep);
}
archiveAttemptedStep = "OBSERVE_ARCHIVED";
progress.attemptedStep = archiveAttemptedStep;
await saveProgress("BEFORE", archiveAttemptedStep);
const observed = await ports.observeGateway(operation);
if (!observationMatches(observed, operation, "ARCHIVED"))
throw new Error("archive readback mismatch");
progress.steps.push("OBSERVE_ARCHIVED");
progress.attemptedStep = null;
next.requestedLifecycle = "ARCHIVED";
next.observedLifecycle = "ARCHIVED";
next.routeFenced = true;
next.outputFenced = true;
next.dispatchEnabled = false;
next.secretsPresent = false;
next.archivedCredentialGeneration = state.credentialGeneration;
next.archivedProfileDigest = state.profileDigest;
next.acceptedFence = operation.fence;
await saveProgress("AFTER", archiveAttemptedStep);
delete next.archiveProgress;
await ports.persistProgress({
operation,
phase: "COMPLETED",
step: "OBSERVE_ARCHIVED",
state: clone(next),
progress: { ...clone(progress), completed: true },
});
return finalizeSuccess(next, operation, "ARCHIVED");
}
case "RESTORE": {
const requestedTuple = projectRuntimeDriftTuple(
operation.targetRuntimeTuple
);
if (
state.observedLifecycle !== "ARCHIVED" ||
!Number.isSafeInteger(state.archivedCredentialGeneration) ||
operation.credentialGeneration <=
state.archivedCredentialGeneration ||
operation.targetRevisionSequence <= state.desiredRevisionSequence ||
operation.targetRevisionId === state.desiredRevisionId ||
operation.profileDigest === state.archivedProfileDigest ||
!requestedTuple ||
requestedTuple.revisionId !== operation.targetRevisionId ||
requestedTuple.revisionSequence !==
operation.targetRevisionSequence ||
requestedTuple.serviceCredentialGeneration !==
operation.credentialGeneration ||
requestedTuple.lifecycle !== "ON"
)
return { ok: false, status: "SUCCESSOR_REQUIRED", state };
const rehydrated = await ports.rehydrate({ operation });
const rehydratedTuple = projectRuntimeDriftTuple(
rehydrated?.runtimeTuple
);
if (
rehydratedTuple &&
(rehydratedTuple.revisionId !== operation.targetRevisionId ||
rehydratedTuple.revisionSequence !==
operation.targetRevisionSequence ||
rehydratedTuple.serviceCredentialGeneration !==
operation.credentialGeneration ||
rehydratedTuple.lifecycle !== "ON" ||
!restoreTargetsMatch(requestedTuple, rehydratedTuple))
)
throw new Error("rehydrated successor tuple rejected");
await ports.startGateway({ operation });
const observed = await ports.observeGateway(operation);
const observedTuple = projectRuntimeDriftTuple(observed?.runtimeTuple);
if (
!observationMatches(observed, operation, "ON") ||
!observedTuple ||
!restoreTargetsMatch(requestedTuple, observedTuple) ||
(rehydratedTuple &&
observedTuple.packageHash !== rehydratedTuple.packageHash)
)
throw new Error("restore readback mismatch");
next.desiredRevisionId = operation.targetRevisionId;
next.desiredRevisionSequence = operation.targetRevisionSequence;
next.observedRevisionId = operation.targetRevisionId;
next.requestedLifecycle = "ON";
next.observedLifecycle = "ON";
next.credentialGeneration = operation.credentialGeneration;
next.profileDigest = operation.profileDigest;
next.routeFenced = false;
next.outputFenced = false;
next.dispatchEnabled = true;
next.secretsPresent = true;
next.desiredTuple = clone(observed.runtimeTuple);
next.observedTuple = clone(observed.runtimeTuple);
next.previousKnownGoodTuple = clone(observed.runtimeTuple);
next.runtimeHashes = {
package: observed.runtimeTuple.packageHash,
config: observed.runtimeTuple.configHash,
content: observed.runtimeTuple.contentHash,
process: observed.runtimeTuple.processHash ?? null,
};
next.acceptedFence = operation.fence;
return finalizeSuccess(next, operation, "ON");
}
default:
return { ok: false, status: "ACTION_REJECTED", state };
}
} catch {
if (operation.action === "ARCHIVE" && archiveProgressState) {
archiveProgressState.archiveProgress = {
...archiveProgressState.archiveProgress,
attemptedStep: archiveAttemptedStep,
resumePredicate: "independent_archive_reconciliation_required",
};
await ports.persistProgress({
operation,
phase: "UNCERTAIN",
step: archiveAttemptedStep,
state: clone(archiveProgressState),
progress: clone(archiveProgressState.archiveProgress),
});
return {
ok: false,
status: "UNCERTAIN",
state: archiveProgressState,
resumePredicate: "independent_archive_reconciliation_required",
};
}
return rollbackLifecycle(ports, operation, priorState);
}
}