UNPKG

@sellable/install

Version:

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

994 lines (965 loc) • 34.2 kB
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); } }