@sellable/install 0.1.358 → 0.1.359-phase111.20260720052618

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/bin/sellable-install.mjs +0 -2
  2. package/package.json +2 -11
  3. package/skill-templates/refill-sends-evergreen.md +5 -0
  4. package/skill-templates/refill-sends-v2.md +23 -1
  5. package/skill-templates/refill-sends.md +14 -1
  6. package/bin/sellable-agent-egress-proxy.mjs +0 -31
  7. package/bin/sellable-agent-host-bootstrap.mjs +0 -26
  8. package/bin/sellable-agent-host-worker.mjs +0 -9
  9. package/bin/sellable-agent-runtime-helper.mjs +0 -37
  10. package/bin/sellable-agent-runtime-launcher.mjs +0 -151
  11. package/bin/sellable-agent-runtime-probe.mjs +0 -163
  12. package/bin/sellable-agent-sandbox-init.mjs +0 -93
  13. package/container/Dockerfile +0 -37
  14. package/container/README.md +0 -15
  15. package/container/compose.yaml +0 -22
  16. package/container/entrypoint.sh +0 -13
  17. package/lib/sellable-agent/containment-contract.mjs +0 -472
  18. package/lib/sellable-agent/external-runtime-builder.mjs +0 -272
  19. package/lib/sellable-agent/hermes-bridge.mjs +0 -497
  20. package/lib/sellable-agent/host-bootstrap.mjs +0 -458
  21. package/lib/sellable-agent/host-worker.mjs +0 -2067
  22. package/lib/sellable-agent/profile-materializer.mjs +0 -1410
  23. package/lib/sellable-agent/provisioning-adapter.mjs +0 -992
  24. package/lib/sellable-agent/runtime-boundary.mjs +0 -635
  25. package/lib/sellable-agent/runtime-egress-proxy.mjs +0 -241
  26. package/lib/sellable-agent/runtime-helper.mjs +0 -1428
  27. package/lib/sellable-agent/runtime-reconciler.mjs +0 -683
  28. package/lib/sellable-agent/service-installer.mjs +0 -441
  29. package/services/s6/sellable-agent-egress-proxy/run +0 -15
  30. package/services/s6/sellable-agent-host-worker/run +0 -4
  31. package/services/s6/sellable-agent-runtime/run +0 -19
@@ -1,683 +0,0 @@
1
- import { createHash } from "node:crypto";
2
-
3
- export const RUNTIME_DRIFT_ACTIONS = Object.freeze({
4
- NONE: "NONE",
5
- MCP: "RELOAD_MCP",
6
- SKILLS: "RELOAD_SKILLS",
7
- ENVIRONMENT: "RELOAD_ENVIRONMENT_OR_RESTART",
8
- SLACK: "RESTART_GATEWAY",
9
- CONTENT: "RESTART_PROFILE_SESSION",
10
- PACKAGE: "REPROVISION_PROFILE",
11
- });
12
-
13
- export const LIFECYCLE_TRANSITION_ORDER = Object.freeze({
14
- DISABLE_GATEWAY: ["FENCE_DISPATCH", "STOP_GATEWAY", "PROVE_NO_DISPATCH", "OBSERVE_OFF"],
15
- ENABLE_GATEWAY: ["VERIFY_DESIRED", "START_GATEWAY", "PROVE_GATEWAY", "OBSERVE_ON"],
16
- ARCHIVE: ["FENCE_ROUTE_OUTPUT", "DRAIN", "STOP_GATEWAY", "CLEAN_SECRETS", "ACK_REVOKE", "OBSERVE_ARCHIVED"],
17
- RESTORE: ["VERIFY_SUCCESSOR", "REHYDRATE", "START_GATEWAY", "PROVE_GATEWAY", "OBSERVE_ON"],
18
- });
19
-
20
- const DRIFT_GROUPS = [
21
- {
22
- action: RUNTIME_DRIFT_ACTIONS.PACKAGE,
23
- fields: ["packageHash", "revisionId", "revisionSequence"],
24
- },
25
- {
26
- action: RUNTIME_DRIFT_ACTIONS.SLACK,
27
- fields: [
28
- "slackManifestHash", "slackAppHash", "slackTeamHash", "slackBotHash",
29
- "slackChannelHash", "slackDefaultChannelHash", "slackMemberAllowlistHash",
30
- "slackChannelIds", "slackDefaultChannelId", "slackMemberAllowlist",
31
- "slackGeneration", "lifecycle",
32
- ],
33
- },
34
- {
35
- action: RUNTIME_DRIFT_ACTIONS.ENVIRONMENT,
36
- fields: ["providerFingerprint", "serviceCredentialGeneration"],
37
- },
38
- {
39
- action: RUNTIME_DRIFT_ACTIONS.MCP,
40
- fields: ["configHash", "mcpHash", "policyHash", "toolsHash"],
41
- },
42
- { action: RUNTIME_DRIFT_ACTIONS.SKILLS, fields: ["skillsHash"] },
43
- { action: RUNTIME_DRIFT_ACTIONS.CONTENT, fields: ["soulHash"] },
44
- ];
45
-
46
- const RUNTIME_PROJECTION_FIELDS = [
47
- "revisionId", "revisionSequence", "lifecycle", "packageHash", "configHash", "mcpHash",
48
- "policyHash", "toolsHash", "skillsHash", "providerFingerprint",
49
- "serviceCredentialGeneration", "slackManifestHash", "slackAppHash", "slackTeamHash",
50
- "slackBotHash", "slackChannelHash", "slackDefaultChannelHash", "slackMemberAllowlistHash",
51
- "slackChannelIds", "slackDefaultChannelId", "slackMemberAllowlist", "slackGeneration",
52
- "contentHash", "soulHash",
53
- ];
54
-
55
- const RESTORE_TARGET_FIELDS = RUNTIME_PROJECTION_FIELDS.filter(
56
- (field) => field !== "packageHash",
57
- );
58
-
59
- function projectRestoreTarget(value) {
60
- return Object.fromEntries(RESTORE_TARGET_FIELDS.map((field) => [field, value[field]]));
61
- }
62
-
63
- function stableJson(value) {
64
- if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
65
- if (value && typeof value === "object") {
66
- return `{${Object.keys(value)
67
- .sort()
68
- .map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
69
- .join(",")}}`;
70
- }
71
- return JSON.stringify(value);
72
- }
73
-
74
- function digest(value) {
75
- return createHash("sha256").update(stableJson(value)).digest("hex");
76
- }
77
-
78
- export function projectRuntimeDriftTuple(value) {
79
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
80
- const projection = Object.fromEntries(
81
- RUNTIME_PROJECTION_FIELDS.map((field) => [field, value[field]])
82
- );
83
- if (
84
- typeof projection.revisionId !== "string" || !projection.revisionId ||
85
- !Number.isSafeInteger(projection.revisionSequence) || projection.revisionSequence < 1 ||
86
- !["ABSENT", "APPLYING", "ON", "OFF", "ARCHIVED", "DEGRADED"].includes(projection.lifecycle) ||
87
- !Number.isSafeInteger(projection.serviceCredentialGeneration) || projection.serviceCredentialGeneration < 1 ||
88
- !Number.isSafeInteger(projection.slackGeneration) || projection.slackGeneration < 1
89
- ) return null;
90
- for (const field of RUNTIME_PROJECTION_FIELDS.filter((field) => field.endsWith("Hash") || field === "providerFingerprint")) {
91
- if (!/^[a-f0-9]{64}$/.test(projection[field] ?? "")) return null;
92
- }
93
- if (
94
- !Array.isArray(projection.slackChannelIds) || projection.slackChannelIds.length < 1 ||
95
- projection.slackChannelIds.length > 100 ||
96
- projection.slackChannelIds.some((value) => !/^C[A-Z0-9]{8,20}$/.test(value)) ||
97
- new Set(projection.slackChannelIds).size !== projection.slackChannelIds.length ||
98
- !/^C[A-Z0-9]{8,20}$/.test(projection.slackDefaultChannelId ?? "") ||
99
- !Array.isArray(projection.slackMemberAllowlist) || projection.slackMemberAllowlist.length < 1 ||
100
- projection.slackMemberAllowlist.length > 1_000 ||
101
- projection.slackMemberAllowlist.some((value) => !/^[UW][A-Z0-9]{8,20}$/.test(value)) ||
102
- new Set(projection.slackMemberAllowlist).size !== projection.slackMemberAllowlist.length
103
- ) return null;
104
- return projection;
105
- }
106
-
107
- export function classifyRuntimeDrift(desired, observed) {
108
- const desiredProjection = projectRuntimeDriftTuple(desired);
109
- const observedProjection = projectRuntimeDriftTuple(observed);
110
- if (!desiredProjection || !observedProjection) {
111
- return { action: RUNTIME_DRIFT_ACTIONS.PACKAGE, drift: ["invalidObservation"] };
112
- }
113
- const aggregateContentDrift =
114
- stableJson(desiredProjection.contentHash) !== stableJson(observedProjection.contentHash);
115
- let groups = DRIFT_GROUPS.map((group) => ({
116
- action: group.action,
117
- drift: group.fields.filter(
118
- (field) => stableJson(desiredProjection[field]) !== stableJson(observedProjection[field])
119
- ),
120
- })).filter((group) => group.drift.length > 0);
121
- const semanticConfigOwners = groups.filter((group) =>
122
- [RUNTIME_DRIFT_ACTIONS.SLACK, RUNTIME_DRIFT_ACTIONS.ENVIRONMENT].includes(group.action)
123
- );
124
- const mcpFiles = groups.find((group) => group.action === RUNTIME_DRIFT_ACTIONS.MCP);
125
- if (
126
- semanticConfigOwners.length === 1 && mcpFiles &&
127
- mcpFiles.drift.every((field) => ["configHash", "mcpHash"].includes(field))
128
- ) {
129
- semanticConfigOwners[0].drift.push(...mcpFiles.drift);
130
- groups = groups.filter((group) => group !== mcpFiles);
131
- }
132
- if (groups.length > 1 || (aggregateContentDrift && groups.length === 0)) {
133
- return {
134
- action: RUNTIME_DRIFT_ACTIONS.PACKAGE,
135
- drift: [
136
- ...groups.flatMap((group) => group.drift),
137
- ...(aggregateContentDrift ? ["contentHash"] : []),
138
- ],
139
- };
140
- }
141
- if (groups.length === 1) {
142
- return {
143
- ...groups[0],
144
- drift: [...groups[0].drift, ...(aggregateContentDrift ? ["contentHash"] : [])],
145
- };
146
- }
147
- return { action: RUNTIME_DRIFT_ACTIONS.NONE, drift: [] };
148
- }
149
-
150
- const REFRESH_PORTS = [
151
- "reloadMcp", "reloadSkills", "reloadEnvironmentOrRestart", "restartGateway",
152
- "restartProfileSession", "reprovisionProfile", "observe", "rollback",
153
- ];
154
-
155
- function portsPresent(ports, names) {
156
- return ports && names.every((name) => typeof ports[name] === "function");
157
- }
158
-
159
- const REFRESH_ACTION_PORT = {
160
- [RUNTIME_DRIFT_ACTIONS.MCP]: "reloadMcp",
161
- [RUNTIME_DRIFT_ACTIONS.SKILLS]: "reloadSkills",
162
- [RUNTIME_DRIFT_ACTIONS.ENVIRONMENT]: "reloadEnvironmentOrRestart",
163
- [RUNTIME_DRIFT_ACTIONS.SLACK]: "restartGateway",
164
- [RUNTIME_DRIFT_ACTIONS.CONTENT]: "restartProfileSession",
165
- [RUNTIME_DRIFT_ACTIONS.PACKAGE]: "reprovisionProfile",
166
- };
167
-
168
- export async function reconcileRuntimeRefresh(input = {}) {
169
- const { desired, approvedDesired, observed, previousKnownGood, operation, ports } = input;
170
- if (!portsPresent(ports, REFRESH_PORTS)) return { ok: false, status: "PORT_REJECTED" };
171
- const desiredProjection = projectRuntimeDriftTuple(desired);
172
- const approvedProjection = projectRuntimeDriftTuple(approvedDesired);
173
- const observedProjection = projectRuntimeDriftTuple(observed);
174
- const knownGoodProjection = projectRuntimeDriftTuple(previousKnownGood);
175
- if (!desiredProjection || !approvedProjection || !observedProjection || !knownGoodProjection) {
176
- return { ok: false, status: "OBSERVATION_REJECTED", observed };
177
- }
178
- if (stableJson(desiredProjection) !== stableJson(approvedProjection)) {
179
- return { ok: false, status: "UNAPPROVED_DESIRED", observed };
180
- }
181
- if (!operation || typeof operation.id !== "string" || !/^[1-9][0-9]{0,19}$/.test(operation.fence ?? "")) {
182
- return { ok: false, status: "OPERATION_REJECTED", observed };
183
- }
184
- const classification = classifyRuntimeDrift(desired, observed);
185
- if (classification.action === RUNTIME_DRIFT_ACTIONS.NONE) {
186
- const current = await ports.observe({ operation, desired });
187
- if (stableJson(projectRuntimeDriftTuple(current)) === stableJson(desiredProjection)) {
188
- return { ok: true, status: "READY", action: "NONE", drift: [], observed: current };
189
- }
190
- return proveRefreshRollback({ ports, operation, previousKnownGood, observed, action: "NONE", drift: [] });
191
- }
192
- const method = REFRESH_ACTION_PORT[classification.action];
193
- try {
194
- await ports[method]({
195
- operation, desired, observed, previousKnownGood, drift: classification.drift,
196
- });
197
- const current = await ports.observe({ operation, desired });
198
- if (stableJson(projectRuntimeDriftTuple(current)) !== stableJson(desiredProjection)) {
199
- throw new Error("readback mismatch");
200
- }
201
- return {
202
- ok: true,
203
- status: "READY",
204
- action: classification.action,
205
- drift: classification.drift,
206
- observed: current,
207
- observationDigest: digest(current),
208
- };
209
- } catch {
210
- return proveRefreshRollback({
211
- ports,
212
- operation,
213
- previousKnownGood,
214
- observed,
215
- action: classification.action,
216
- drift: classification.drift,
217
- });
218
- }
219
- }
220
-
221
- async function proveRefreshRollback({ ports, operation, previousKnownGood, observed, action, drift }) {
222
- try {
223
- await ports.rollback({ operation, previousKnownGood });
224
- const rollbackObserved = await ports.observe({
225
- operation, rollback: true, desired: previousKnownGood, target: previousKnownGood,
226
- });
227
- const expected = projectRuntimeDriftTuple(previousKnownGood);
228
- const actual = projectRuntimeDriftTuple(rollbackObserved);
229
- if (expected && actual && stableJson(actual) === stableJson(expected)) {
230
- return {
231
- ok: false,
232
- status: "ROLLED_BACK",
233
- action,
234
- drift,
235
- observed,
236
- rollbackObservation: { lifecycle: rollbackObserved.lifecycle },
237
- };
238
- }
239
- return {
240
- ok: false,
241
- status: "ROLLBACK_UNPROVEN",
242
- action,
243
- drift,
244
- observed: actual ? rollbackObserved : observed,
245
- };
246
- } catch {
247
- return { ok: false, status: "ROLLBACK_UNPROVEN", action, drift, observed };
248
- }
249
- }
250
-
251
- const LIFECYCLE_PORTS = [
252
- "fenceDispatch", "fenceRouteOutput", "drain", "startGateway", "stopGateway",
253
- "proveNoDispatch", "observeGateway", "cleanSecrets", "acknowledgeRevocation",
254
- "rehydrate", "rollback", "reconcileArchive", "persistProgress",
255
- ];
256
-
257
- function validLifecycleState(state) {
258
- return Boolean(
259
- state &&
260
- typeof state === "object" &&
261
- /^[0-9]+$/.test(state.acceptedFence ?? "") &&
262
- typeof state.receipts === "object" &&
263
- state.receipts !== null
264
- );
265
- }
266
-
267
- function validOperation(operation) {
268
- return Boolean(
269
- operation &&
270
- typeof operation.id === "string" && operation.id &&
271
- typeof operation.eventId === "string" && operation.eventId &&
272
- Object.hasOwn(LIFECYCLE_TRANSITION_ORDER, operation.action) &&
273
- /^[1-9][0-9]{0,19}$/.test(operation.fence ?? "") &&
274
- typeof operation.targetRevisionId === "string" && operation.targetRevisionId &&
275
- Number.isSafeInteger(operation.targetRevisionSequence) && operation.targetRevisionSequence > 0 &&
276
- Number.isSafeInteger(operation.credentialGeneration) && operation.credentialGeneration > 0 &&
277
- /^[a-f0-9]{64}$/.test(operation.profileDigest ?? "")
278
- );
279
- }
280
-
281
- function clone(value) {
282
- return JSON.parse(JSON.stringify(value));
283
- }
284
-
285
- function operationSemantic(operation) {
286
- return {
287
- id: operation.id,
288
- eventId: operation.eventId,
289
- action: operation.action,
290
- fence: operation.fence,
291
- targetRevisionId: operation.targetRevisionId,
292
- targetRevisionSequence: operation.targetRevisionSequence,
293
- credentialGeneration: operation.credentialGeneration,
294
- profileDigest: operation.profileDigest,
295
- targetRuntimeTuple: operation.targetRuntimeTuple ?? null,
296
- };
297
- }
298
-
299
- function storedResult(state, operation) {
300
- const receipt = state.receipts[operation.eventId];
301
- if (!receipt) return null;
302
- if (receipt.operationDigest !== digest(operationSemantic(operation))) {
303
- return { ok: false, status: "DUPLICATE_CONFLICT", state };
304
- }
305
- return { ok: receipt.ok, status: receipt.status, state, receipt };
306
- }
307
-
308
- function finalizeSuccess(state, operation, status) {
309
- const receipt = {
310
- producer: "sellable-agent-runtime-reconciler",
311
- subject: `agent-operation:${operation.id}`,
312
- operationId: operation.id,
313
- eventId: operation.eventId,
314
- fence: operation.fence,
315
- ok: true,
316
- status,
317
- operationDigest: digest(operationSemantic(operation)),
318
- stateDigest: digest({ ...state, receipts: undefined }),
319
- };
320
- state.receipts[operation.eventId] = receipt;
321
- return { ok: true, status, state, receipt };
322
- }
323
-
324
- function observationMatches(observation, operation, lifecycle) {
325
- const expectedGates = lifecycle === "ON"
326
- ? { lifecycle: "ON", dispatch: "ENABLED", route: "OPEN", output: "OPEN" }
327
- : lifecycle === "OFF"
328
- ? { lifecycle: "OFF", dispatch: "FENCED", route: "OPEN", output: "OPEN" }
329
- : { lifecycle: "ARCHIVED", dispatch: "FENCED", route: "FENCED", output: "FENCED" };
330
- return Boolean(
331
- observation &&
332
- observation.lifecycle === lifecycle &&
333
- observation.revisionId === operation.targetRevisionId &&
334
- observation.credentialGeneration === operation.credentialGeneration &&
335
- observation.profileDigest === operation.profileDigest &&
336
- stableJson(observation.gates) === stableJson(expectedGates) &&
337
- observation.secretsPresent === (lifecycle !== "ARCHIVED") &&
338
- observation.process && observation.process.ready === (lifecycle === "ON") &&
339
- (lifecycle === "ON"
340
- ? Number.isSafeInteger(observation.process.pid) && observation.process.pid > 1 &&
341
- /^[1-9][0-9]{0,31}$/.test(observation.process.startTicks ?? "") &&
342
- typeof observation.process.bootId === "string" && observation.process.bootId.length > 0
343
- : observation.process.pid === null && observation.process.startTicks === null)
344
- );
345
- }
346
-
347
- async function rollbackLifecycle(ports, operation, priorState) {
348
- try {
349
- await ports.rollback({ operation, priorState });
350
- const expected = {
351
- targetRevisionId: priorState.observedRevisionId,
352
- credentialGeneration: priorState.credentialGeneration,
353
- profileDigest: priorState.profileDigest,
354
- };
355
- const observed = await ports.observeGateway({
356
- ...expected, rollback: true, lifecycle: priorState.observedLifecycle,
357
- });
358
- if (observationMatches(observed, expected, priorState.observedLifecycle)) {
359
- return {
360
- ok: false,
361
- status: "ROLLED_BACK",
362
- state: priorState,
363
- rollbackObservation: {
364
- observedLifecycle: observed.lifecycle,
365
- observedRevisionId: observed.revisionId,
366
- acceptedFence: priorState.acceptedFence,
367
- },
368
- };
369
- }
370
- return { ok: false, status: "ROLLBACK_UNPROVEN", state: priorState };
371
- } catch {
372
- return { ok: false, status: "ROLLBACK_UNPROVEN", state: priorState };
373
- }
374
- }
375
-
376
- export async function applyRuntimeLifecycle(input = {}) {
377
- const { state, operation, ports } = input;
378
- if (!validLifecycleState(state)) return { ok: false, status: "STATE_REJECTED", state };
379
- if (!validOperation(operation)) return { ok: false, status: "OPERATION_REJECTED", state };
380
- if (!portsPresent(ports, LIFECYCLE_PORTS)) return { ok: false, status: "PORT_REJECTED", state };
381
- if (state.archiveProgress && operation.action !== "ARCHIVE") {
382
- return { ok: false, status: "ARCHIVE_IN_PROGRESS", state };
383
- }
384
- const duplicate = storedResult(state, operation);
385
- if (duplicate) return duplicate;
386
- const operationDigest = digest(operationSemantic(operation));
387
- const resumesArchive = Boolean(
388
- operation.action === "ARCHIVE" &&
389
- state.archiveProgress?.eventId === operation.eventId &&
390
- state.archiveProgress?.operationDigest === operationDigest &&
391
- state.archiveProgress?.fence === operation.fence
392
- );
393
- if (!resumesArchive && BigInt(operation.fence) <= BigInt(state.acceptedFence)) {
394
- return { ok: false, status: "STALE_FENCE", state };
395
- }
396
- const priorState = clone(state);
397
- const next = clone(state);
398
- let archiveProgressState = null;
399
- let archiveAttemptedStep = null;
400
- try {
401
- switch (operation.action) {
402
- case "DISABLE_GATEWAY": {
403
- if (
404
- operation.targetRevisionId !== state.desiredRevisionId ||
405
- operation.targetRevisionSequence < state.desiredRevisionSequence ||
406
- operation.credentialGeneration !== state.credentialGeneration ||
407
- operation.profileDigest !== state.profileDigest
408
- ) return { ok: false, status: "DESIRED_MISMATCH", state };
409
- await ports.fenceDispatch({ operation });
410
- await ports.stopGateway({ operation });
411
- const proof = await ports.proveNoDispatch({ operation });
412
- if (!proof?.noDispatch) throw new Error("dispatch remained enabled");
413
- const observed = await ports.observeGateway(operation);
414
- if (!observationMatches(observed, operation, "OFF")) throw new Error("off readback mismatch");
415
- next.requestedLifecycle = "OFF";
416
- next.observedLifecycle = "OFF";
417
- next.desiredRevisionSequence = operation.targetRevisionSequence;
418
- next.dispatchEnabled = false;
419
- next.acceptedFence = operation.fence;
420
- return finalizeSuccess(next, operation, "OFF");
421
- }
422
- case "ENABLE_GATEWAY": {
423
- if (
424
- operation.targetRevisionId !== state.desiredRevisionId ||
425
- operation.targetRevisionSequence < state.desiredRevisionSequence ||
426
- operation.credentialGeneration !== state.credentialGeneration ||
427
- operation.profileDigest !== state.profileDigest ||
428
- state.observedLifecycle === "ARCHIVED"
429
- ) return { ok: false, status: "DESIRED_MISMATCH", state };
430
- await ports.startGateway({ operation });
431
- const observed = await ports.observeGateway(operation);
432
- if (!observationMatches(observed, operation, "ON")) throw new Error("on readback mismatch");
433
- next.requestedLifecycle = "ON";
434
- next.observedLifecycle = "ON";
435
- next.desiredRevisionSequence = operation.targetRevisionSequence;
436
- next.dispatchEnabled = true;
437
- next.acceptedFence = operation.fence;
438
- return finalizeSuccess(next, operation, "ON");
439
- }
440
- case "ARCHIVE": {
441
- if (
442
- operation.targetRevisionId !== state.desiredRevisionId ||
443
- operation.targetRevisionSequence < state.desiredRevisionSequence ||
444
- operation.credentialGeneration !== state.credentialGeneration ||
445
- operation.profileDigest !== state.profileDigest
446
- ) return { ok: false, status: "DESIRED_MISMATCH", state };
447
- const progress = resumesArchive
448
- ? clone(state.archiveProgress)
449
- : {
450
- eventId: operation.eventId,
451
- operationDigest,
452
- fence: operation.fence,
453
- steps: [],
454
- attemptedStep: null,
455
- resumePredicate: null,
456
- };
457
- if (resumesArchive) {
458
- const reconciliation = await ports.reconcileArchive({ operation, state, progress });
459
- if (
460
- !reconciliation?.ok ||
461
- typeof reconciliation.observationId !== "string" ||
462
- !reconciliation.observationId ||
463
- !Array.isArray(reconciliation.confirmedSteps)
464
- ) {
465
- return { ok: false, status: "ARCHIVE_RECONCILIATION_REQUIRED", state };
466
- }
467
- const allowedSteps = [
468
- "FENCE_ROUTE_OUTPUT", "DRAIN", "STOP_GATEWAY", "CLEAN_SECRETS",
469
- "ACK_REVOKE", "OBSERVE_ARCHIVED",
470
- ];
471
- const confirmedSteps = reconciliation.confirmedSteps;
472
- const exactPrefix =
473
- confirmedSteps.length <= allowedSteps.length &&
474
- confirmedSteps.every((step, index) => step === allowedSteps[index]) &&
475
- progress.steps.every((step, index) => confirmedSteps[index] === step);
476
- if (!exactPrefix) {
477
- return { ok: false, status: "ARCHIVE_RECONCILIATION_REJECTED", state };
478
- }
479
- progress.steps = [...confirmedSteps];
480
- if (confirmedSteps.includes("FENCE_ROUTE_OUTPUT")) {
481
- next.routeFenced = true;
482
- next.outputFenced = true;
483
- next.dispatchEnabled = false;
484
- next.routeFenceStatus = "CONFIRMED";
485
- next.outputFenceStatus = "CONFIRMED";
486
- next.dispatchStatus = "DISABLED";
487
- }
488
- if (confirmedSteps.includes("STOP_GATEWAY")) next.gatewayStatus = "STOPPED";
489
- if (confirmedSteps.includes("CLEAN_SECRETS")) {
490
- next.secretsPresent = false;
491
- next.secretDisposition = "REMOVED";
492
- }
493
- if (confirmedSteps.includes("ACK_REVOKE")) next.revocationStatus = "ACKNOWLEDGED";
494
- progress.reconciledObservationId = reconciliation.observationId;
495
- progress.attemptedStep = null;
496
- progress.resumePredicate = null;
497
- }
498
- next.requestedLifecycle = "ARCHIVED";
499
- next.desiredRevisionSequence = operation.targetRevisionSequence;
500
- next.acceptedFence = operation.fence;
501
- const saveProgress = async (phase, step) => {
502
- next.archiveProgress = clone(progress);
503
- archiveProgressState = clone(next);
504
- await ports.persistProgress({
505
- operation,
506
- phase,
507
- step,
508
- state: clone(next),
509
- progress: clone(progress),
510
- });
511
- };
512
- if (!progress.steps.includes("FENCE_ROUTE_OUTPUT")) {
513
- archiveAttemptedStep = "FENCE_ROUTE_OUTPUT";
514
- progress.attemptedStep = archiveAttemptedStep;
515
- next.routeFenceStatus = "UNKNOWN";
516
- next.outputFenceStatus = "UNKNOWN";
517
- next.dispatchStatus = "UNKNOWN";
518
- await saveProgress("BEFORE", archiveAttemptedStep);
519
- await ports.fenceRouteOutput({ operation });
520
- progress.steps.push("FENCE_ROUTE_OUTPUT");
521
- progress.attemptedStep = null;
522
- next.routeFenced = true;
523
- next.outputFenced = true;
524
- next.dispatchEnabled = false;
525
- next.routeFenceStatus = "CONFIRMED";
526
- next.outputFenceStatus = "CONFIRMED";
527
- next.dispatchStatus = "DISABLED";
528
- await saveProgress("AFTER", archiveAttemptedStep);
529
- }
530
- if (!progress.steps.includes("DRAIN")) {
531
- archiveAttemptedStep = "DRAIN";
532
- progress.attemptedStep = archiveAttemptedStep;
533
- await saveProgress("BEFORE", archiveAttemptedStep);
534
- const drained = await ports.drain({ operation });
535
- if (!drained?.drained || drained.remaining !== 0) throw new Error("drain incomplete");
536
- progress.steps.push("DRAIN");
537
- progress.attemptedStep = null;
538
- await saveProgress("AFTER", archiveAttemptedStep);
539
- }
540
- if (!progress.steps.includes("STOP_GATEWAY")) {
541
- archiveAttemptedStep = "STOP_GATEWAY";
542
- progress.attemptedStep = archiveAttemptedStep;
543
- next.gatewayStatus = "UNKNOWN";
544
- await saveProgress("BEFORE", archiveAttemptedStep);
545
- await ports.stopGateway({ operation });
546
- progress.steps.push("STOP_GATEWAY");
547
- progress.attemptedStep = null;
548
- next.gatewayStatus = "STOPPED";
549
- await saveProgress("AFTER", archiveAttemptedStep);
550
- }
551
- if (!progress.steps.includes("CLEAN_SECRETS")) {
552
- archiveAttemptedStep = "CLEAN_SECRETS";
553
- progress.attemptedStep = archiveAttemptedStep;
554
- next.secretDisposition = "UNKNOWN";
555
- next.secretsPresent = null;
556
- await saveProgress("BEFORE", archiveAttemptedStep);
557
- await ports.cleanSecrets({ operation });
558
- progress.steps.push("CLEAN_SECRETS");
559
- progress.attemptedStep = null;
560
- next.secretDisposition = "REMOVED";
561
- next.secretsPresent = false;
562
- await saveProgress("AFTER", archiveAttemptedStep);
563
- }
564
- if (!progress.steps.includes("ACK_REVOKE")) {
565
- archiveAttemptedStep = "ACK_REVOKE";
566
- progress.attemptedStep = archiveAttemptedStep;
567
- next.revocationStatus = "UNKNOWN";
568
- await saveProgress("BEFORE", archiveAttemptedStep);
569
- const revoke = await ports.acknowledgeRevocation({ operation });
570
- if (!revoke?.acknowledged) throw new Error("revocation not acknowledged");
571
- progress.steps.push("ACK_REVOKE");
572
- progress.attemptedStep = null;
573
- next.revocationStatus = "ACKNOWLEDGED";
574
- await saveProgress("AFTER", archiveAttemptedStep);
575
- }
576
- archiveAttemptedStep = "OBSERVE_ARCHIVED";
577
- progress.attemptedStep = archiveAttemptedStep;
578
- await saveProgress("BEFORE", archiveAttemptedStep);
579
- const observed = await ports.observeGateway(operation);
580
- if (!observationMatches(observed, operation, "ARCHIVED")) throw new Error("archive readback mismatch");
581
- progress.steps.push("OBSERVE_ARCHIVED");
582
- progress.attemptedStep = null;
583
- next.requestedLifecycle = "ARCHIVED";
584
- next.observedLifecycle = "ARCHIVED";
585
- next.routeFenced = true;
586
- next.outputFenced = true;
587
- next.dispatchEnabled = false;
588
- next.secretsPresent = false;
589
- next.archivedCredentialGeneration = state.credentialGeneration;
590
- next.archivedProfileDigest = state.profileDigest;
591
- next.acceptedFence = operation.fence;
592
- await saveProgress("AFTER", archiveAttemptedStep);
593
- delete next.archiveProgress;
594
- await ports.persistProgress({
595
- operation,
596
- phase: "COMPLETED",
597
- step: "OBSERVE_ARCHIVED",
598
- state: clone(next),
599
- progress: { ...clone(progress), completed: true },
600
- });
601
- return finalizeSuccess(next, operation, "ARCHIVED");
602
- }
603
- case "RESTORE": {
604
- const requestedTuple = projectRuntimeDriftTuple(operation.targetRuntimeTuple);
605
- if (
606
- state.observedLifecycle !== "ARCHIVED" ||
607
- !Number.isSafeInteger(state.archivedCredentialGeneration) ||
608
- operation.credentialGeneration <= state.archivedCredentialGeneration ||
609
- operation.targetRevisionSequence <= state.desiredRevisionSequence ||
610
- operation.targetRevisionId === state.desiredRevisionId ||
611
- operation.profileDigest === state.archivedProfileDigest || !requestedTuple ||
612
- requestedTuple.revisionId !== operation.targetRevisionId ||
613
- requestedTuple.revisionSequence !== operation.targetRevisionSequence ||
614
- requestedTuple.serviceCredentialGeneration !== operation.credentialGeneration ||
615
- requestedTuple.lifecycle !== "ON"
616
- ) return { ok: false, status: "SUCCESSOR_REQUIRED", state };
617
- const rehydrated = await ports.rehydrate({ operation });
618
- const rehydratedTuple = projectRuntimeDriftTuple(rehydrated?.runtimeTuple);
619
- if (rehydratedTuple && (
620
- rehydratedTuple.revisionId !== operation.targetRevisionId ||
621
- rehydratedTuple.revisionSequence !== operation.targetRevisionSequence ||
622
- rehydratedTuple.serviceCredentialGeneration !== operation.credentialGeneration ||
623
- rehydratedTuple.lifecycle !== "ON" || rehydratedTuple.contentHash !== operation.profileDigest ||
624
- stableJson(projectRestoreTarget(rehydratedTuple)) !== stableJson(projectRestoreTarget(requestedTuple))
625
- )) throw new Error("rehydrated successor tuple rejected");
626
- await ports.startGateway({ operation });
627
- const observed = await ports.observeGateway(operation);
628
- const observedTuple = projectRuntimeDriftTuple(observed?.runtimeTuple);
629
- if (
630
- !observationMatches(observed, operation, "ON") || !observedTuple ||
631
- stableJson(projectRestoreTarget(observedTuple)) !== stableJson(projectRestoreTarget(requestedTuple)) ||
632
- (rehydratedTuple && observedTuple.packageHash !== rehydratedTuple.packageHash)
633
- ) throw new Error("restore readback mismatch");
634
- next.desiredRevisionId = operation.targetRevisionId;
635
- next.desiredRevisionSequence = operation.targetRevisionSequence;
636
- next.observedRevisionId = operation.targetRevisionId;
637
- next.requestedLifecycle = "ON";
638
- next.observedLifecycle = "ON";
639
- next.credentialGeneration = operation.credentialGeneration;
640
- next.profileDigest = operation.profileDigest;
641
- next.routeFenced = false;
642
- next.outputFenced = false;
643
- next.dispatchEnabled = true;
644
- next.secretsPresent = true;
645
- next.desiredTuple = clone(observed.runtimeTuple);
646
- next.observedTuple = clone(observed.runtimeTuple);
647
- next.previousKnownGoodTuple = clone(observed.runtimeTuple);
648
- next.runtimeHashes = {
649
- package: observed.runtimeTuple.packageHash,
650
- config: observed.runtimeTuple.configHash,
651
- content: observed.runtimeTuple.contentHash,
652
- process: observed.runtimeTuple.processHash ?? null,
653
- };
654
- next.acceptedFence = operation.fence;
655
- return finalizeSuccess(next, operation, "ON");
656
- }
657
- default:
658
- return { ok: false, status: "ACTION_REJECTED", state };
659
- }
660
- } catch {
661
- if (operation.action === "ARCHIVE" && archiveProgressState) {
662
- archiveProgressState.archiveProgress = {
663
- ...archiveProgressState.archiveProgress,
664
- attemptedStep: archiveAttemptedStep,
665
- resumePredicate: "independent_archive_reconciliation_required",
666
- };
667
- await ports.persistProgress({
668
- operation,
669
- phase: "UNCERTAIN",
670
- step: archiveAttemptedStep,
671
- state: clone(archiveProgressState),
672
- progress: clone(archiveProgressState.archiveProgress),
673
- });
674
- return {
675
- ok: false,
676
- status: "UNCERTAIN",
677
- state: archiveProgressState,
678
- resumePredicate: "independent_archive_reconciliation_required",
679
- };
680
- }
681
- return rollbackLifecycle(ports, operation, priorState);
682
- }
683
- }