@serve.zone/interfaces 32.14.0 → 32.16.0

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.
@@ -6,9 +6,20 @@ import type {
6
6
  IRuntimeAuthorityReference,
7
7
  } from './runtimeassignment.js';
8
8
  import type { IRuntimeSessionBinding } from './runtimesession.js';
9
+ import type { IRuntimeStorageClaim, IRuntimeStorageClaimIntent } from './runtimestorageclaim.js';
10
+ import {
11
+ bindRuntimeStorageClaimToIntent,
12
+ snapshotRuntimeStorageClaim,
13
+ snapshotRuntimeStorageClaimIntent,
14
+ validateRuntimeStorageClaim,
15
+ validateRuntimeStorageClaimIntent,
16
+ } from './runtimestorageclaim.js';
17
+ import type { IAppStoreFilesystemStorageRequest } from '../appstore/types.js';
18
+ import { normalizeStorageRequest } from '../platform/storage.js';
9
19
  import type { ISecretVersionReference } from './secret.js';
10
20
  import {
11
21
  canonicalizeStrictJson,
22
+ compareWireTokens,
12
23
  createCanonicalJsonSha256Hex,
13
24
  } from '../private/canonicaljson.js';
14
25
  import {
@@ -72,6 +83,37 @@ export interface IFleetCutoverMountTargetIdentityInput {
72
83
  target: string;
73
84
  }
74
85
 
86
+ export interface IFleetCutoverDockerMountInput {
87
+ /** Raw Docker mount type. Unsupported values produce an explicit unknown descriptor. */
88
+ type: string;
89
+ /** Exact source bytes from the Docker object being described; never substitute another field. */
90
+ source: string;
91
+ /** Exact container target bytes from the Docker object being described. */
92
+ target: string;
93
+ readOnly: boolean;
94
+ }
95
+
96
+ /**
97
+ * Value-free correlation between one local physical mount and its service-spec mount identity.
98
+ * The two identities stay distinct: a Docker volume's local Source is a host path while its
99
+ * service Source and local Name are the logical volume name.
100
+ */
101
+ export interface IFleetCutoverPhysicalMountCorrelation {
102
+ physicalMountIdentityDigest: TSha256Digest;
103
+ serviceMountIdentityDigest: TSha256Digest;
104
+ correlationIdentityDigest: TSha256Digest;
105
+ }
106
+
107
+ export interface IFleetCutoverPhysicalMountEvidence {
108
+ mount: IFleetCutoverMountDescriptor;
109
+ correlation: IFleetCutoverPhysicalMountCorrelation;
110
+ }
111
+
112
+ export interface IFleetCutoverDockerConstraintSetIdentity {
113
+ constraintCount: number;
114
+ constraintSetDigest: TSha256Digest;
115
+ }
116
+
75
117
  export type TFleetCutoverDockerTaskState =
76
118
  | 'new'
77
119
  | 'allocated'
@@ -138,6 +180,11 @@ export interface IFleetCutoverPhysicalContainer {
138
180
  imageId: TSha256Digest;
139
181
  serviceIdLabel: string | null;
140
182
  mounts: IFleetCutoverMountDescriptor[];
183
+ /**
184
+ * Optional only for compatibility with observations predating correlation evidence. When
185
+ * present it is a complete one-to-one mapping of mounts. Absence establishes no mount join.
186
+ */
187
+ mountCorrelations?: IFleetCutoverPhysicalMountCorrelation[];
141
188
  }
142
189
 
143
190
  export interface IFleetCutoverContainerSetIdentity {
@@ -281,6 +328,101 @@ export interface IFleetCutoverVolumeHandoffIntent {
281
328
  digest: TSha256Digest;
282
329
  }
283
330
 
331
+ export interface IFleetCutoverPersistentVolumeMountDescriptor
332
+ extends IFleetCutoverMountDescriptor {
333
+ type: 'volume';
334
+ classification: 'persistent-volume';
335
+ readOnly: false;
336
+ }
337
+
338
+ /**
339
+ * Cloudly-owned desired handoff prepared from authenticated legacy observations. Observed Docker
340
+ * source identity is not physical storage lineage; Corestore proves lineage later under the held
341
+ * native attachment fence.
342
+ */
343
+ export interface IFleetCutoverPreparedVolumeHandoffIntent {
344
+ id: string;
345
+ generation: number;
346
+ clusterId: string;
347
+ nodeId: string;
348
+ serviceId: string;
349
+ volumeId: string;
350
+ observedMount: IFleetCutoverPersistentVolumeMountDescriptor;
351
+ claimIntent: IRuntimeStorageClaimIntent;
352
+ runtime: IClusterRuntimeReference & { phase: 'draining' };
353
+ scope: IFleetCutoverAcceptedEvidenceReference;
354
+ targetSet: IFleetCutoverTargetSetReference;
355
+ legacyStorageEvidence: IFleetCutoverAcceptedEvidenceReference;
356
+ sourceNodeObservation: IFleetCutoverAcceptedEvidenceReference;
357
+ acceptedAt: number;
358
+ digest: TSha256Digest;
359
+ }
360
+
361
+ export interface IFleetCutoverPreparedVolumeHandoffUpsert {
362
+ nodeId: string;
363
+ serviceId: string;
364
+ mountIdentityDigest: TSha256Digest;
365
+ request: IAppStoreFilesystemStorageRequest & {
366
+ accessMode: 'ReadWriteOnce';
367
+ reclaimPolicy: 'retain';
368
+ };
369
+ }
370
+
371
+ /** One bounded CAS mutation; the durable aggregate may contain many such 32-entry batches. */
372
+ export interface IFleetCutoverPreparedVolumeHandoffMutation {
373
+ mutationId: string;
374
+ clusterId: string;
375
+ currentRuntime: IClusterRuntimeReference & { phase: 'draining' };
376
+ scope: IFleetCutoverAcceptedEvidenceReference;
377
+ targetSet: IFleetCutoverTargetSetReference;
378
+ expectedAggregate: IFleetCutoverAcceptedEvidenceReference | null;
379
+ upserts: IFleetCutoverPreparedVolumeHandoffUpsert[];
380
+ removals: IFleetCutoverAcceptedEvidenceReference[];
381
+ }
382
+
383
+ /**
384
+ * Revisioned header for one complete expected set. It intentionally contains no page references:
385
+ * immutable pages bind back to this exact revision, avoiding an aggregate/page digest cycle.
386
+ */
387
+ export interface IFleetCutoverPreparedVolumeHandoffAggregate {
388
+ id: string;
389
+ generation: number;
390
+ previous: IFleetCutoverAcceptedEvidenceReference | null;
391
+ clusterId: string;
392
+ runtime: IClusterRuntimeReference & { phase: 'draining' };
393
+ scope: IFleetCutoverAcceptedEvidenceReference;
394
+ targetSet: IFleetCutoverTargetSetReference;
395
+ state: 'incomplete' | 'complete';
396
+ requiredHandoffCount: number;
397
+ handoffCount: number;
398
+ pageCount: number;
399
+ handoffSetDigest: TSha256Digest;
400
+ acceptedAt: number;
401
+ digest: TSha256Digest;
402
+ }
403
+
404
+ /** One immutable 1..32-entry page pinned to one aggregate revision. */
405
+ export interface IFleetCutoverPreparedVolumeHandoffPage {
406
+ id: string;
407
+ generation: 1;
408
+ aggregate: IFleetCutoverAcceptedEvidenceReference;
409
+ pageIndex: number;
410
+ handoffs: IFleetCutoverPreparedVolumeHandoffPageReference[];
411
+ handoffSetPageDigest: TSha256Digest;
412
+ digest: TSha256Digest;
413
+ }
414
+
415
+ /** Immutable page membership commits both the handoff revision and its owning physical node. */
416
+ export interface IFleetCutoverPreparedVolumeHandoffPageReference
417
+ extends IFleetCutoverAcceptedEvidenceReference {
418
+ nodeId: string;
419
+ }
420
+
421
+ export interface IFleetCutoverPreparedVolumeHandoffPageSetEntry {
422
+ pageIndex: number;
423
+ handoffSetPageDigest: TSha256Digest;
424
+ }
425
+
284
426
  interface IFleetCutoverVolumeFenceBase {
285
427
  id: string;
286
428
  generation: number;
@@ -328,6 +470,40 @@ export interface IFleetCutoverCorestoreAdoptionReceipt {
328
470
  digest: TSha256Digest;
329
471
  }
330
472
 
473
+ /** Current-session Cloudly grant for one post-Pallet adoption attempt. */
474
+ export interface IFleetCutoverStorageAdoptionGrant {
475
+ id: string;
476
+ generation: number;
477
+ previous: IFleetCutoverAcceptedEvidenceReference | null;
478
+ clusterId: string;
479
+ nodeId: string;
480
+ serviceId: string;
481
+ volumeId: string;
482
+ observedMount: IFleetCutoverPersistentVolumeMountDescriptor;
483
+ runtime: IClusterRuntimeReference & { phase: 'pallet' };
484
+ session: IRuntimeSessionBinding;
485
+ handoffIntent: IFleetCutoverAcceptedEvidenceReference;
486
+ palletFence: IFleetCutoverAcceptedEvidenceReference;
487
+ acceptedAt: number;
488
+ digest: TSha256Digest;
489
+ }
490
+
491
+ /** Exact result Pallet persists only after revalidating the current grant following adoption. */
492
+ export interface IFleetCutoverStorageAdoptionBundle {
493
+ grant: IFleetCutoverStorageAdoptionGrant;
494
+ fence: Extract<TFleetCutoverVolumeFence, { state: 'consumed' }>;
495
+ adoption: IFleetCutoverCorestoreAdoptionReceipt;
496
+ claim: IRuntimeStorageClaim;
497
+ }
498
+
499
+ /** One authenticated Spark reporter's complete bounded subset of a full immutable aggregate page. */
500
+ export interface IFleetCutoverHeldVolumeFenceEvidencePage {
501
+ aggregate: IFleetCutoverAcceptedEvidenceReference;
502
+ page: IFleetCutoverPreparedVolumeHandoffPage;
503
+ nodeId: string;
504
+ fences: Extract<TFleetCutoverVolumeFence, { state: 'held' }>[];
505
+ }
506
+
331
507
  export interface IFleetCutoverCoreflowRetirementEvidence {
332
508
  id: string;
333
509
  generation: number;
@@ -497,19 +673,38 @@ export const fleetCutoverContract = Object.freeze({
497
673
  maximumNodes: 1024,
498
674
  maximumContainersPerNode: 512,
499
675
  maximumMountsPerOwner: 32,
676
+ maximumConstraintsPerService: 256,
677
+ maximumDockerTextBytes: 4096,
678
+ maximumDockerServiceSpecCanonicalBytes: 1_048_576,
679
+ maximumPreparedVolumeHandoffMutationEntries: 32,
680
+ /** Every admitted task has one node placement and at most 32 mounts: 4096 * 32. */
681
+ maximumPreparedVolumeHandoffs: 4096 * 32,
682
+ maximumPreparedVolumeHandoffPages: (4096 * 32) / 32,
683
+ maximumHeldVolumeFenceEvidenceBytes: 131_072,
500
684
  maximumClassifications: 512,
501
685
  observationFreshnessMs: 300_000,
502
686
  mountSourceIdentityDigestDomain: 'serve.zone/fleet-cutover-mount-source-identity',
503
687
  mountTargetIdentityDigestDomain: 'serve.zone/fleet-cutover-mount-target-identity',
504
688
  mountIdentityDigestDomain: 'serve.zone/fleet-cutover-mount-identity',
689
+ mountCorrelationIdentityDigestDomain: 'serve.zone/fleet-cutover-mount-correlation-identity',
690
+ dockerServiceSpecDigestDomain: 'serve.zone/fleet-cutover-docker-service-spec',
691
+ dockerConstraintSetDigestDomain: 'serve.zone/fleet-cutover-docker-constraint-set',
505
692
  containerSetIdentityDigestDomain: 'serve.zone/fleet-cutover-container-set-identity',
506
693
  localNodeSnapshotDigestDomain: 'serve.zone/fleet-cutover-local-node-snapshot',
507
694
  managerSnapshotDigestDomain: 'serve.zone/fleet-cutover-manager-snapshot',
508
695
  censusDigestDomain: 'serve.zone/fleet-cutover-census',
509
696
  scopeDigestDomain: 'serve.zone/fleet-cutover-scope',
510
697
  volumeHandoffDigestDomain: 'serve.zone/fleet-cutover-volume-handoff',
698
+ preparedVolumeHandoffDigestDomain: 'serve.zone/fleet-cutover-prepared-volume-handoff',
699
+ preparedVolumeHandoffAggregateDigestDomain:
700
+ 'serve.zone/fleet-cutover-prepared-volume-handoff-aggregate',
701
+ preparedVolumeHandoffPageContentDigestDomain:
702
+ 'serve.zone/fleet-cutover-prepared-volume-handoff-page-content',
703
+ preparedVolumeHandoffPageDigestDomain: 'serve.zone/fleet-cutover-prepared-volume-handoff-page',
704
+ preparedVolumeHandoffSetDigestDomain: 'serve.zone/fleet-cutover-prepared-volume-handoff-set',
511
705
  volumeFenceDigestDomain: 'serve.zone/fleet-cutover-volume-fence',
512
706
  corestoreAdoptionDigestDomain: 'serve.zone/fleet-cutover-corestore-adoption',
707
+ storageAdoptionGrantDigestDomain: 'serve.zone/fleet-cutover-storage-adoption-grant',
513
708
  coreflowRetirementDigestDomain: 'serve.zone/fleet-cutover-coreflow-retirement',
514
709
  ingressDesignationDigestDomain: 'serve.zone/fleet-cutover-ingress-designation',
515
710
  ingressAuthorityDigestDomain: 'serve.zone/fleet-cutover-ingress-authority',
@@ -521,6 +716,11 @@ export const fleetCutoverContract = Object.freeze({
521
716
  } satisfies Record<TClusterRuntimePhase, TClusterRuntimePhase | null>),
522
717
  } as const);
523
718
 
719
+ /** Docker labels with established historical producer semantics. */
720
+ export const fleetCutoverDockerLabelNames = Object.freeze({
721
+ serviceId: 'serve.zone.serviceId',
722
+ } as const);
723
+
524
724
  const fail = (): never => { throw new Error('Fleet cutover contract rejected.'); };
525
725
  const safe = <T>(operationArg: () => T): T => {
526
726
  try { return operationArg(); } catch { return fail(); }
@@ -612,6 +812,28 @@ const sortedMountDescriptors = (valueArg: unknown): IFleetCutoverMountDescriptor
612
812
  networkList(valueArg, fleetCutoverContract.maximumMountsPerOwner, mountDescriptor),
613
813
  itemArg => itemArg.mountIdentityDigest,
614
814
  );
815
+ const physicalMountCorrelation = (
816
+ valueArg: unknown,
817
+ ): IFleetCutoverPhysicalMountCorrelation => {
818
+ const value = networkRecord(valueArg, [
819
+ 'physicalMountIdentityDigest', 'serviceMountIdentityDigest', 'correlationIdentityDigest',
820
+ ]);
821
+ return {
822
+ physicalMountIdentityDigest: networkDigest(value.physicalMountIdentityDigest),
823
+ serviceMountIdentityDigest: networkDigest(value.serviceMountIdentityDigest),
824
+ correlationIdentityDigest: networkDigest(value.correlationIdentityDigest),
825
+ };
826
+ };
827
+ const sortedPhysicalMountCorrelations = (
828
+ valueArg: unknown,
829
+ ): IFleetCutoverPhysicalMountCorrelation[] => networkSorted(
830
+ networkList(
831
+ valueArg,
832
+ fleetCutoverContract.maximumMountsPerOwner,
833
+ physicalMountCorrelation,
834
+ ),
835
+ itemArg => itemArg.physicalMountIdentityDigest,
836
+ );
615
837
  const dockerMode = (valueArg: unknown): TFleetCutoverDockerServiceMode => {
616
838
  requireNetwork(valueArg !== null && typeof valueArg === 'object');
617
839
  const kind = (valueArg as Record<string, unknown>).kind;
@@ -636,6 +858,8 @@ const dockerService = (valueArg: unknown): IFleetCutoverDockerService => {
636
858
  'dockerServiceId', 'dockerServiceName', 'specVersion', 'specDigest', 'imageDigest', 'mode',
637
859
  'desiredTaskCount', 'constraintCount', 'constraintSetDigest', 'mounts', 'ownershipLabels',
638
860
  ]);
861
+ const constraintCount = nonNegative(value.constraintCount);
862
+ requireNetwork(constraintCount <= fleetCutoverContract.maximumConstraintsPerService);
639
863
  return {
640
864
  dockerServiceId: networkId(value.dockerServiceId),
641
865
  dockerServiceName: networkId(value.dockerServiceName),
@@ -644,7 +868,7 @@ const dockerService = (valueArg: unknown): IFleetCutoverDockerService => {
644
868
  imageDigest: networkDigest(value.imageDigest),
645
869
  mode: dockerMode(value.mode),
646
870
  desiredTaskCount: nonNegative(value.desiredTaskCount),
647
- constraintCount: nonNegative(value.constraintCount),
871
+ constraintCount,
648
872
  constraintSetDigest: networkDigest(value.constraintSetDigest),
649
873
  mounts: sortedMountDescriptors(value.mounts),
650
874
  ownershipLabels: ownershipLabels(value.ownershipLabels),
@@ -681,8 +905,8 @@ const dockerTask = (valueArg: unknown): IFleetCutoverDockerTask => {
681
905
  const physicalContainer = (valueArg: unknown): IFleetCutoverPhysicalContainer => {
682
906
  const value = networkRecord(valueArg, [
683
907
  'dockerContainerId', 'dockerServiceId', 'dockerTaskId', 'imageId', 'serviceIdLabel', 'mounts',
684
- ]);
685
- return {
908
+ ], ['mountCorrelations']);
909
+ const result: IFleetCutoverPhysicalContainer = {
686
910
  dockerContainerId: networkId(value.dockerContainerId),
687
911
  dockerServiceId: value.dockerServiceId === null ? null : networkId(value.dockerServiceId),
688
912
  dockerTaskId: value.dockerTaskId === null ? null : networkId(value.dockerTaskId),
@@ -690,6 +914,10 @@ const physicalContainer = (valueArg: unknown): IFleetCutoverPhysicalContainer =>
690
914
  serviceIdLabel: value.serviceIdLabel === null ? null : networkId(value.serviceIdLabel),
691
915
  mounts: sortedMountDescriptors(value.mounts),
692
916
  };
917
+ if (Object.hasOwn(value, 'mountCorrelations')) {
918
+ result.mountCorrelations = sortedPhysicalMountCorrelations(value.mountCorrelations);
919
+ }
920
+ return result;
693
921
  };
694
922
  const inventory = (valueArg: unknown): IFleetCutoverDockerInventory => {
695
923
  const value = networkRecord(valueArg, [
@@ -773,11 +1001,17 @@ const canonicalDigest = async (domainArg: string, valueArg: object): Promise<TSh
773
1001
 
774
1002
  const exactDockerText = (valueArg: unknown, allowEmptyArg: boolean): string => {
775
1003
  requireNetwork(typeof valueArg === 'string'
776
- && new TextEncoder().encode(valueArg).length <= 4096
1004
+ && new TextEncoder().encode(valueArg).length <= fleetCutoverContract.maximumDockerTextBytes
777
1005
  && (allowEmptyArg || valueArg.length > 0));
778
1006
  return valueArg;
779
1007
  };
780
1008
 
1009
+ const openDockerRecord = (valueArg: unknown): Record<string, unknown> => {
1010
+ const value = captureNetwork(valueArg);
1011
+ requireNetwork(value !== null && typeof value === 'object' && !Array.isArray(value));
1012
+ return value as Record<string, unknown>;
1013
+ };
1014
+
781
1015
  const computeExactIdentityDigest = async (
782
1016
  domainArg: string,
783
1017
  inputArg: object,
@@ -786,6 +1020,160 @@ const computeExactIdentityDigest = async (
786
1020
  input: inputArg,
787
1021
  }, fail), fail)}`;
788
1022
 
1023
+ /**
1024
+ * Hash the complete open-schema Docker Service.Spec object. Unknown response properties are
1025
+ * intentionally included: this helper returns only the digest and never a retained projection.
1026
+ */
1027
+ export const computeFleetCutoverDockerServiceSpecDigest = async (
1028
+ valueArg: unknown,
1029
+ ): Promise<TSha256Digest> => {
1030
+ try {
1031
+ const spec = openDockerRecord(valueArg);
1032
+ const canonicalSpec = canonicalizeStrictJson(spec, fail, 'Docker Service.Spec');
1033
+ requireNetwork(new TextEncoder().encode(canonicalSpec).byteLength
1034
+ <= fleetCutoverContract.maximumDockerServiceSpecCanonicalBytes);
1035
+ return await computeExactIdentityDigest(
1036
+ fleetCutoverContract.dockerServiceSpecDigestDomain,
1037
+ { spec },
1038
+ );
1039
+ } catch {
1040
+ return fail();
1041
+ }
1042
+ };
1043
+
1044
+ /** Sort exact constraint strings while preserving duplicate entries, then return value-free identity. */
1045
+ export const computeFleetCutoverDockerConstraintSetIdentity = async (
1046
+ valueArg: unknown,
1047
+ ): Promise<IFleetCutoverDockerConstraintSetIdentity> => {
1048
+ try {
1049
+ const captured = captureNetwork(valueArg);
1050
+ requireNetwork(Array.isArray(captured)
1051
+ && captured.length <= fleetCutoverContract.maximumConstraintsPerService);
1052
+ const constraints = captured.map(itemArg => exactDockerText(itemArg, false))
1053
+ .sort(compareWireTokens);
1054
+ return {
1055
+ constraintCount: constraints.length,
1056
+ constraintSetDigest: await computeExactIdentityDigest(
1057
+ fleetCutoverContract.dockerConstraintSetDigestDomain,
1058
+ { constraints },
1059
+ ),
1060
+ };
1061
+ } catch {
1062
+ return fail();
1063
+ }
1064
+ };
1065
+
1066
+ export const snapshotFleetCutoverDockerDesiredTaskCount = (valueArg: unknown): number =>
1067
+ safe(() => nonNegative(valueArg));
1068
+
1069
+ export const validateFleetCutoverDockerDesiredTaskCount = (valueArg: unknown): string[] => (
1070
+ typeof valueArg === 'number' && Number.isSafeInteger(valueArg) && valueArg >= 0
1071
+ ? []
1072
+ : ['Docker ServiceStatus.DesiredTasks must be a non-negative safe integer']
1073
+ );
1074
+
1075
+ /** Apply only the documented exact cutover mount classifications; no path normalization occurs. */
1076
+ export const classifyFleetCutoverDockerMount = (
1077
+ valueArg: IFleetCutoverDockerMountInput,
1078
+ ): Pick<IFleetCutoverMountDescriptor, 'type' | 'classification'> => safe(() => {
1079
+ const value = networkRecord(valueArg, ['type', 'source', 'target', 'readOnly']);
1080
+ const rawType = exactDockerText(value.type, false);
1081
+ const source = exactDockerText(value.source, true);
1082
+ const target = exactDockerText(value.target, false);
1083
+ requireNetwork(typeof value.readOnly === 'boolean');
1084
+ if (rawType === 'volume') return { type: 'volume', classification: 'persistent-volume' };
1085
+ if (rawType === 'tmpfs') return { type: 'tmpfs', classification: 'tmpfs' };
1086
+ if (rawType !== 'bind') return { type: 'unknown', classification: 'unknown' };
1087
+ if (source === '/var/run/docker.sock'
1088
+ && target === '/var/run/docker.sock'
1089
+ && value.readOnly === false) {
1090
+ return { type: 'bind', classification: 'docker-socket' };
1091
+ }
1092
+ if (source === '/sys/fs/cgroup'
1093
+ && target === '/host/cgroup'
1094
+ && value.readOnly === true) {
1095
+ return { type: 'bind', classification: 'cgroup' };
1096
+ }
1097
+ return { type: 'bind', classification: 'bind' };
1098
+ });
1099
+
1100
+ export const createFleetCutoverMountDescriptor = async (
1101
+ valueArg: IFleetCutoverDockerMountInput,
1102
+ ): Promise<IFleetCutoverMountDescriptor> => {
1103
+ try {
1104
+ const value = networkRecord(valueArg, ['type', 'source', 'target', 'readOnly']);
1105
+ const source = exactDockerText(value.source, true);
1106
+ const target = exactDockerText(value.target, false);
1107
+ requireNetwork(typeof value.readOnly === 'boolean');
1108
+ const shape = classifyFleetCutoverDockerMount({
1109
+ type: exactDockerText(value.type, false),
1110
+ source,
1111
+ target,
1112
+ readOnly: value.readOnly,
1113
+ });
1114
+ requireNetwork(shape.type === 'tmpfs' ? source.length === 0 : source.length > 0);
1115
+ const identityInput: Omit<IFleetCutoverMountDescriptor, 'mountIdentityDigest'> = {
1116
+ ...shape,
1117
+ sourceIdentityDigest: await computeFleetCutoverMountSourceIdentityDigest({
1118
+ type: shape.type,
1119
+ source,
1120
+ }),
1121
+ targetIdentityDigest: await computeFleetCutoverMountTargetIdentityDigest({ target }),
1122
+ readOnly: value.readOnly,
1123
+ };
1124
+ return {
1125
+ ...identityInput,
1126
+ mountIdentityDigest: await computeFleetCutoverMountIdentityDigest(identityInput),
1127
+ };
1128
+ } catch {
1129
+ return fail();
1130
+ }
1131
+ };
1132
+
1133
+ const dockerServiceMountInput = (valueArg: unknown): IFleetCutoverDockerMountInput => {
1134
+ const value = openDockerRecord(valueArg);
1135
+ const type = exactDockerText(value.Type, false);
1136
+ const source = value.Source === undefined ? '' : exactDockerText(value.Source, true);
1137
+ const target = exactDockerText(value.Target, false);
1138
+ const readOnly = value.ReadOnly === undefined ? false : value.ReadOnly;
1139
+ requireNetwork(typeof readOnly === 'boolean');
1140
+ return { type, source, target, readOnly };
1141
+ };
1142
+
1143
+ interface IFleetCutoverDockerLocalMountInput {
1144
+ descriptorInput: IFleetCutoverDockerMountInput;
1145
+ logicalSource: string;
1146
+ }
1147
+
1148
+ const dockerLocalMountInput = (valueArg: unknown): IFleetCutoverDockerLocalMountInput => {
1149
+ const value = openDockerRecord(valueArg);
1150
+ const type = exactDockerText(value.Type, false);
1151
+ const source = exactDockerText(value.Source, true);
1152
+ const target = exactDockerText(value.Destination, false);
1153
+ requireNetwork(typeof value.RW === 'boolean');
1154
+ const logicalSource = type === 'volume'
1155
+ ? exactDockerText(value.Name, false)
1156
+ : source;
1157
+ return {
1158
+ descriptorInput: { type, source, target, readOnly: !value.RW },
1159
+ logicalSource,
1160
+ };
1161
+ };
1162
+
1163
+ /** Read only consumed Service.Spec Mount fields; future Docker response properties are ignored. */
1164
+ export const createFleetCutoverDockerServiceMountDescriptor = async (
1165
+ valueArg: unknown,
1166
+ ): Promise<IFleetCutoverMountDescriptor> => createFleetCutoverMountDescriptor(
1167
+ dockerServiceMountInput(valueArg),
1168
+ );
1169
+
1170
+ /** Read only consumed local MountPoint fields and retain the exact physical Source identity. */
1171
+ export const createFleetCutoverDockerLocalMountDescriptor = async (
1172
+ valueArg: unknown,
1173
+ ): Promise<IFleetCutoverMountDescriptor> => createFleetCutoverMountDescriptor(
1174
+ dockerLocalMountInput(valueArg).descriptorInput,
1175
+ );
1176
+
789
1177
  export const computeFleetCutoverMountSourceIdentityDigest = async (
790
1178
  valueArg: IFleetCutoverMountSourceIdentityInput,
791
1179
  ): Promise<TSha256Digest> => safe(async () => {
@@ -821,6 +1209,54 @@ export const computeFleetCutoverMountIdentityDigest = async (
821
1209
  });
822
1210
  });
823
1211
 
1212
+ export const computeFleetCutoverMountCorrelationIdentityDigest = async (
1213
+ valueArg: Omit<IFleetCutoverPhysicalMountCorrelation, 'correlationIdentityDigest'>,
1214
+ ): Promise<TSha256Digest> => safe(async () => {
1215
+ const value = networkRecord(valueArg, [
1216
+ 'physicalMountIdentityDigest', 'serviceMountIdentityDigest',
1217
+ ]);
1218
+ return computeExactIdentityDigest(fleetCutoverContract.mountCorrelationIdentityDigestDomain, {
1219
+ physicalMountIdentityDigest: networkDigest(value.physicalMountIdentityDigest),
1220
+ serviceMountIdentityDigest: networkDigest(value.serviceMountIdentityDigest),
1221
+ });
1222
+ });
1223
+
1224
+ /**
1225
+ * Produce both the exact local physical descriptor and its logical service-mount correlation.
1226
+ * For a volume only MountPoint.Name supplies the logical source. Unknown types and a missing or
1227
+ * empty volume Name are refused because they cannot establish a truthful service join.
1228
+ */
1229
+ export const createFleetCutoverDockerPhysicalMountEvidence = async (
1230
+ valueArg: unknown,
1231
+ ): Promise<IFleetCutoverPhysicalMountEvidence> => {
1232
+ try {
1233
+ const value = dockerLocalMountInput(valueArg);
1234
+ requireNetwork(['volume', 'bind', 'tmpfs'].includes(value.descriptorInput.type));
1235
+ const mount = await createFleetCutoverMountDescriptor(value.descriptorInput);
1236
+ const serviceMount = await createFleetCutoverMountDescriptor({
1237
+ ...value.descriptorInput,
1238
+ source: value.logicalSource,
1239
+ });
1240
+ const correlationInput: Omit<
1241
+ IFleetCutoverPhysicalMountCorrelation,
1242
+ 'correlationIdentityDigest'
1243
+ > = {
1244
+ physicalMountIdentityDigest: mount.mountIdentityDigest,
1245
+ serviceMountIdentityDigest: serviceMount.mountIdentityDigest,
1246
+ };
1247
+ return {
1248
+ mount,
1249
+ correlation: {
1250
+ ...correlationInput,
1251
+ correlationIdentityDigest:
1252
+ await computeFleetCutoverMountCorrelationIdentityDigest(correlationInput),
1253
+ },
1254
+ };
1255
+ } catch {
1256
+ return fail();
1257
+ }
1258
+ };
1259
+
824
1260
  const validateMountDescriptors = async (
825
1261
  mountsArg: IFleetCutoverMountDescriptor[],
826
1262
  ): Promise<void> => {
@@ -830,6 +1266,28 @@ const validateMountDescriptors = async (
830
1266
  }
831
1267
  };
832
1268
 
1269
+ const validatePhysicalMountCorrelations = async (
1270
+ containerArg: IFleetCutoverPhysicalContainer,
1271
+ ): Promise<void> => {
1272
+ if (containerArg.mountCorrelations === undefined) return;
1273
+ requireNetwork(containerArg.mountCorrelations.length === containerArg.mounts.length);
1274
+ const physicalMountIdentities = new Set(
1275
+ containerArg.mounts.map(itemArg => itemArg.mountIdentityDigest),
1276
+ );
1277
+ const serviceMountIdentities = new Set(
1278
+ containerArg.mountCorrelations.map(itemArg => itemArg.serviceMountIdentityDigest),
1279
+ );
1280
+ requireNetwork(physicalMountIdentities.size === containerArg.mounts.length
1281
+ && serviceMountIdentities.size === containerArg.mountCorrelations.length
1282
+ && containerArg.mountCorrelations.every(itemArg =>
1283
+ physicalMountIdentities.has(itemArg.physicalMountIdentityDigest)));
1284
+ for (const correlation of containerArg.mountCorrelations) {
1285
+ const { correlationIdentityDigest, ...input } = correlation;
1286
+ requireNetwork(correlationIdentityDigest
1287
+ === await computeFleetCutoverMountCorrelationIdentityDigest(input));
1288
+ }
1289
+ };
1290
+
833
1291
  export const computeFleetCutoverContainerSetIdentityDigest = async (
834
1292
  containerIdsArg: string[],
835
1293
  ): Promise<TSha256Digest> => safe(async () => {
@@ -894,7 +1352,10 @@ export const validateFleetCutoverLocalNodeSnapshot = async (
894
1352
  );
895
1353
  requireNetwork(value.before.containerIdsDigest === containerIdsDigest
896
1354
  && value.after.containerIdsDigest === containerIdsDigest);
897
- for (const container of value.containers) await validateMountDescriptors(container.mounts);
1355
+ for (const container of value.containers) {
1356
+ await validateMountDescriptors(container.mounts);
1357
+ await validatePhysicalMountCorrelations(container);
1358
+ }
898
1359
  return value.snapshotDigest === await computeFleetCutoverLocalNodeSnapshotDigest(value)
899
1360
  ? [] : ['fleet cutover local node snapshot digest does not match'];
900
1361
  } catch {
@@ -1053,6 +1514,13 @@ export const fleetCutoverEvidenceReferenceOf = (
1053
1514
  digest: valueArg.digest,
1054
1515
  });
1055
1516
 
1517
+ export const fleetCutoverPreparedVolumeHandoffPageReferenceOf = (
1518
+ valueArg: { id: string; generation: number; digest: TSha256Digest; nodeId: string },
1519
+ ): IFleetCutoverPreparedVolumeHandoffPageReference => ({
1520
+ ...fleetCutoverEvidenceReferenceOf(valueArg),
1521
+ nodeId: networkNodeId(valueArg.nodeId),
1522
+ });
1523
+
1056
1524
  export const snapshotFleetCutoverAcceptedEvidenceReference = (
1057
1525
  valueArg: unknown,
1058
1526
  ): IFleetCutoverAcceptedEvidenceReference => safe(() => reference(captureNetwork(valueArg)));
@@ -1209,6 +1677,383 @@ export const computeFleetCutoverVolumeHandoffDigest = async (
1209
1677
  snapshotFleetCutoverVolumeHandoffIntent(valueArg),
1210
1678
  );
1211
1679
 
1680
+ const persistentVolumeMountDescriptor = (
1681
+ valueArg: unknown,
1682
+ ): IFleetCutoverPersistentVolumeMountDescriptor => {
1683
+ const value = mountDescriptor(valueArg);
1684
+ requireNetwork(value.type === 'volume'
1685
+ && value.classification === 'persistent-volume'
1686
+ && value.readOnly === false);
1687
+ return value as IFleetCutoverPersistentVolumeMountDescriptor;
1688
+ };
1689
+
1690
+ const preparedVolumeHandoff = (
1691
+ valueArg: unknown,
1692
+ ): IFleetCutoverPreparedVolumeHandoffIntent => {
1693
+ const value = networkRecord(valueArg, [
1694
+ 'id', 'generation', 'clusterId', 'nodeId', 'serviceId', 'volumeId', 'observedMount',
1695
+ 'claimIntent', 'runtime', 'scope', 'targetSet', 'legacyStorageEvidence',
1696
+ 'sourceNodeObservation', 'acceptedAt', 'digest',
1697
+ ]);
1698
+ const clusterId = networkId(value.clusterId);
1699
+ const nodeId = networkNodeId(value.nodeId);
1700
+ const serviceId = networkId(value.serviceId);
1701
+ const observedMount = persistentVolumeMountDescriptor(value.observedMount);
1702
+ const claimIntent = snapshotRuntimeStorageClaimIntent(value.claimIntent);
1703
+ const runtime = runtimeReference(value.runtime);
1704
+ const targetSet = targetSetReference(value.targetSet);
1705
+ requireNetwork(runtime.id === clusterId && runtime.phase === 'draining'
1706
+ && targetSet.cloudlyClusterId === clusterId
1707
+ && claimIntent.clusterId === clusterId
1708
+ && claimIntent.nodeId === nodeId
1709
+ && claimIntent.serviceId === serviceId);
1710
+ return {
1711
+ id: networkId(value.id),
1712
+ generation: networkCounter(value.generation),
1713
+ clusterId,
1714
+ nodeId,
1715
+ serviceId,
1716
+ volumeId: networkId(value.volumeId),
1717
+ observedMount,
1718
+ claimIntent,
1719
+ runtime: runtime as IFleetCutoverPreparedVolumeHandoffIntent['runtime'],
1720
+ scope: reference(value.scope),
1721
+ targetSet,
1722
+ legacyStorageEvidence: reference(value.legacyStorageEvidence),
1723
+ sourceNodeObservation: reference(value.sourceNodeObservation),
1724
+ acceptedAt: networkCounter(value.acceptedAt),
1725
+ digest: networkDigest(value.digest),
1726
+ };
1727
+ };
1728
+
1729
+ export const snapshotFleetCutoverPreparedVolumeHandoffIntent = (
1730
+ valueArg: unknown,
1731
+ ): IFleetCutoverPreparedVolumeHandoffIntent => safe(
1732
+ () => preparedVolumeHandoff(captureNetwork(valueArg)),
1733
+ );
1734
+
1735
+ export const computeFleetCutoverPreparedVolumeHandoffDigest = async (
1736
+ valueArg: unknown,
1737
+ ): Promise<TSha256Digest> => canonicalDigest(
1738
+ fleetCutoverContract.preparedVolumeHandoffDigestDomain,
1739
+ snapshotFleetCutoverPreparedVolumeHandoffIntent(valueArg),
1740
+ );
1741
+
1742
+ export const validateFleetCutoverPreparedVolumeHandoffIntent = async (
1743
+ valueArg: unknown,
1744
+ ): Promise<string[]> => {
1745
+ try {
1746
+ const value = snapshotFleetCutoverPreparedVolumeHandoffIntent(valueArg);
1747
+ if ((await validateRuntimeStorageClaimIntent(value.claimIntent)).length) {
1748
+ return ['fleet cutover prepared volume handoff claim intent is invalid'];
1749
+ }
1750
+ return value.digest === await computeFleetCutoverPreparedVolumeHandoffDigest(value)
1751
+ ? [] : ['fleet cutover prepared volume handoff digest does not match'];
1752
+ } catch {
1753
+ return ['fleet cutover prepared volume handoff must use its exact canonical schema'];
1754
+ }
1755
+ };
1756
+
1757
+ export const snapshotFleetCutoverPreparedVolumeHandoffMutation = (
1758
+ valueArg: unknown,
1759
+ ): IFleetCutoverPreparedVolumeHandoffMutation => safe(() => {
1760
+ const value = networkRecord(captureNetwork(valueArg), [
1761
+ 'mutationId', 'clusterId', 'currentRuntime', 'scope', 'targetSet', 'expectedAggregate',
1762
+ 'upserts', 'removals',
1763
+ ]);
1764
+ const clusterId = networkId(value.clusterId);
1765
+ const currentRuntime = runtimeReference(value.currentRuntime);
1766
+ const targetSet = targetSetReference(value.targetSet);
1767
+ requireNetwork(currentRuntime.id === clusterId && currentRuntime.phase === 'draining'
1768
+ && targetSet.cloudlyClusterId === clusterId);
1769
+ const requestKeys = new Set<string>();
1770
+ const placementKeys = new Set<string>();
1771
+ const upserts = networkSorted(networkList(
1772
+ value.upserts,
1773
+ fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries,
1774
+ itemArg => {
1775
+ const item = networkRecord(itemArg, [
1776
+ 'nodeId', 'serviceId', 'mountIdentityDigest', 'request',
1777
+ ]);
1778
+ const nodeId = networkNodeId(item.nodeId);
1779
+ const serviceId = networkId(item.serviceId);
1780
+ const mountIdentityDigest = networkDigest(item.mountIdentityDigest);
1781
+ const request = normalizeStorageRequest(item.request);
1782
+ requireNetwork(request.kind === 'filesystem'
1783
+ && request.accessMode === 'ReadWriteOnce'
1784
+ && request.reclaimPolicy === 'retain');
1785
+ const requestKey = `${nodeId}\u0000${serviceId}\u0000${request.id}`;
1786
+ const placementKey = `${nodeId}\u0000${serviceId}\u0000${mountIdentityDigest}`;
1787
+ requireNetwork(!requestKeys.has(requestKey) && !placementKeys.has(placementKey));
1788
+ requestKeys.add(requestKey);
1789
+ placementKeys.add(placementKey);
1790
+ return {
1791
+ nodeId,
1792
+ serviceId,
1793
+ mountIdentityDigest,
1794
+ request: request as IFleetCutoverPreparedVolumeHandoffUpsert['request'],
1795
+ };
1796
+ },
1797
+ ), itemArg => `${itemArg.nodeId}\u0000${itemArg.serviceId}\u0000${itemArg.mountIdentityDigest}`);
1798
+ const removals = networkSorted(networkList(
1799
+ value.removals,
1800
+ fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries,
1801
+ reference,
1802
+ ), itemArg => `${itemArg.id}\u0000${String(itemArg.generation).padStart(16, '0')}\u0000${itemArg.digest}`);
1803
+ requireNetwork(upserts.length + removals.length > 0
1804
+ && upserts.length + removals.length
1805
+ <= fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries);
1806
+ return {
1807
+ mutationId: networkId(value.mutationId),
1808
+ clusterId,
1809
+ currentRuntime: currentRuntime as IFleetCutoverPreparedVolumeHandoffMutation['currentRuntime'],
1810
+ scope: reference(value.scope),
1811
+ targetSet,
1812
+ expectedAggregate: value.expectedAggregate === null ? null : reference(value.expectedAggregate),
1813
+ upserts,
1814
+ removals,
1815
+ };
1816
+ });
1817
+
1818
+ const preparedVolumeHandoffAggregate = (
1819
+ valueArg: unknown,
1820
+ ): IFleetCutoverPreparedVolumeHandoffAggregate => {
1821
+ const value = networkRecord(valueArg, [
1822
+ 'id', 'generation', 'previous', 'clusterId', 'runtime', 'scope', 'targetSet', 'state',
1823
+ 'requiredHandoffCount', 'handoffCount', 'pageCount', 'handoffSetDigest', 'acceptedAt',
1824
+ 'digest',
1825
+ ]);
1826
+ const id = networkId(value.id);
1827
+ const generation = networkCounter(value.generation);
1828
+ const previous = value.previous === null ? null : reference(value.previous);
1829
+ requireNetwork(generation === 1
1830
+ ? previous === null
1831
+ : previous?.id === id && previous.generation === generation - 1);
1832
+ const clusterId = networkId(value.clusterId);
1833
+ const runtime = runtimeReference(value.runtime);
1834
+ const targetSet = targetSetReference(value.targetSet);
1835
+ const state = literal(['incomplete', 'complete'] as const, value.state);
1836
+ const requiredHandoffCount = nonNegative(value.requiredHandoffCount);
1837
+ const handoffCount = nonNegative(value.handoffCount);
1838
+ const pageCount = nonNegative(value.pageCount);
1839
+ requireNetwork(runtime.id === clusterId && runtime.phase === 'draining'
1840
+ && targetSet.cloudlyClusterId === clusterId
1841
+ && requiredHandoffCount <= fleetCutoverContract.maximumPreparedVolumeHandoffs
1842
+ && handoffCount <= fleetCutoverContract.maximumPreparedVolumeHandoffs
1843
+ && pageCount <= fleetCutoverContract.maximumPreparedVolumeHandoffPages
1844
+ && pageCount === Math.ceil(handoffCount
1845
+ / fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries)
1846
+ && (state === 'complete') === (requiredHandoffCount === handoffCount));
1847
+ return {
1848
+ id,
1849
+ generation,
1850
+ previous,
1851
+ clusterId,
1852
+ runtime: runtime as IFleetCutoverPreparedVolumeHandoffAggregate['runtime'],
1853
+ scope: reference(value.scope),
1854
+ targetSet,
1855
+ state,
1856
+ requiredHandoffCount,
1857
+ handoffCount,
1858
+ pageCount,
1859
+ handoffSetDigest: networkDigest(value.handoffSetDigest),
1860
+ acceptedAt: networkCounter(value.acceptedAt),
1861
+ digest: networkDigest(value.digest),
1862
+ };
1863
+ };
1864
+
1865
+ export const snapshotFleetCutoverPreparedVolumeHandoffAggregate = (
1866
+ valueArg: unknown,
1867
+ ): IFleetCutoverPreparedVolumeHandoffAggregate => safe(
1868
+ () => preparedVolumeHandoffAggregate(captureNetwork(valueArg)),
1869
+ );
1870
+
1871
+ export const computeFleetCutoverPreparedVolumeHandoffAggregateDigest = async (
1872
+ valueArg: unknown,
1873
+ ): Promise<TSha256Digest> => canonicalDigest(
1874
+ fleetCutoverContract.preparedVolumeHandoffAggregateDigestDomain,
1875
+ snapshotFleetCutoverPreparedVolumeHandoffAggregate(valueArg),
1876
+ );
1877
+
1878
+ export const validateFleetCutoverPreparedVolumeHandoffAggregate = async (
1879
+ valueArg: unknown,
1880
+ ): Promise<string[]> => {
1881
+ try {
1882
+ const value = snapshotFleetCutoverPreparedVolumeHandoffAggregate(valueArg);
1883
+ return value.digest === await computeFleetCutoverPreparedVolumeHandoffAggregateDigest(value)
1884
+ ? [] : ['fleet cutover prepared volume handoff aggregate digest does not match'];
1885
+ } catch {
1886
+ return ['fleet cutover prepared volume handoff aggregate must use its exact canonical schema'];
1887
+ }
1888
+ };
1889
+
1890
+ const preparedVolumeHandoffPage = (
1891
+ valueArg: unknown,
1892
+ ): IFleetCutoverPreparedVolumeHandoffPage => {
1893
+ const value = networkRecord(valueArg, [
1894
+ 'id', 'generation', 'aggregate', 'pageIndex', 'handoffs', 'handoffSetPageDigest', 'digest',
1895
+ ]);
1896
+ requireNetwork(value.generation === 1);
1897
+ const handoffs = networkSorted(networkList(
1898
+ value.handoffs,
1899
+ fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries,
1900
+ itemArg => {
1901
+ const item = networkRecord(itemArg, ['id', 'generation', 'digest', 'nodeId']);
1902
+ return fleetCutoverPreparedVolumeHandoffPageReferenceOf({
1903
+ ...reference({ id: item.id, generation: item.generation, digest: item.digest }),
1904
+ nodeId: networkNodeId(item.nodeId),
1905
+ });
1906
+ },
1907
+ ), itemArg => `${itemArg.id}\u0000${String(itemArg.generation).padStart(16, '0')}\u0000${itemArg.digest}`);
1908
+ const pageIndex = nonNegative(value.pageIndex);
1909
+ requireNetwork(handoffs.length > 0
1910
+ && pageIndex < fleetCutoverContract.maximumPreparedVolumeHandoffPages);
1911
+ return {
1912
+ id: networkId(value.id),
1913
+ generation: 1,
1914
+ aggregate: reference(value.aggregate),
1915
+ pageIndex,
1916
+ handoffs,
1917
+ handoffSetPageDigest: networkDigest(value.handoffSetPageDigest),
1918
+ digest: networkDigest(value.digest),
1919
+ };
1920
+ };
1921
+
1922
+ export const snapshotFleetCutoverPreparedVolumeHandoffPage = (
1923
+ valueArg: unknown,
1924
+ ): IFleetCutoverPreparedVolumeHandoffPage => safe(
1925
+ () => preparedVolumeHandoffPage(captureNetwork(valueArg)),
1926
+ );
1927
+
1928
+ export const computeFleetCutoverPreparedVolumeHandoffPageContentDigest = async (
1929
+ pageIndexArg: unknown,
1930
+ handoffsArg: unknown,
1931
+ ): Promise<TSha256Digest> => {
1932
+ const page = preparedVolumeHandoffPage({
1933
+ id: 'FleetCutoverPreparedVolumeHandoffPage:content',
1934
+ generation: 1,
1935
+ aggregate: { id: 'FleetCutoverPreparedVolumeHandoffAggregate:content', generation: 1,
1936
+ digest: `sha256:${'0'.repeat(64)}` },
1937
+ pageIndex: pageIndexArg,
1938
+ handoffs: handoffsArg,
1939
+ handoffSetPageDigest: `sha256:${'0'.repeat(64)}`,
1940
+ digest: `sha256:${'0'.repeat(64)}`,
1941
+ });
1942
+ return canonicalDigest(fleetCutoverContract.preparedVolumeHandoffPageContentDigestDomain, {
1943
+ pageIndex: page.pageIndex,
1944
+ handoffs: page.handoffs,
1945
+ digest: `sha256:${'0'.repeat(64)}`,
1946
+ });
1947
+ };
1948
+
1949
+ export const computeFleetCutoverPreparedVolumeHandoffPageDigest = async (
1950
+ valueArg: unknown,
1951
+ ): Promise<TSha256Digest> => canonicalDigest(
1952
+ fleetCutoverContract.preparedVolumeHandoffPageDigestDomain,
1953
+ snapshotFleetCutoverPreparedVolumeHandoffPage(valueArg),
1954
+ );
1955
+
1956
+ export const validateFleetCutoverPreparedVolumeHandoffPage = async (
1957
+ valueArg: unknown,
1958
+ ): Promise<string[]> => {
1959
+ try {
1960
+ const value = snapshotFleetCutoverPreparedVolumeHandoffPage(valueArg);
1961
+ if (value.handoffSetPageDigest
1962
+ !== await computeFleetCutoverPreparedVolumeHandoffPageContentDigest(
1963
+ value.pageIndex,
1964
+ value.handoffs,
1965
+ )) {
1966
+ return ['fleet cutover prepared volume handoff page content digest does not match'];
1967
+ }
1968
+ return value.digest === await computeFleetCutoverPreparedVolumeHandoffPageDigest(value)
1969
+ ? [] : ['fleet cutover prepared volume handoff page digest does not match'];
1970
+ } catch {
1971
+ return ['fleet cutover prepared volume handoff page must use its exact canonical schema'];
1972
+ }
1973
+ };
1974
+
1975
+ export const computeFleetCutoverPreparedVolumeHandoffSetDigest = async (
1976
+ entriesArg: unknown,
1977
+ ): Promise<TSha256Digest> => safe(async () => {
1978
+ const entries = networkSorted(networkList(
1979
+ captureNetwork(entriesArg),
1980
+ fleetCutoverContract.maximumPreparedVolumeHandoffPages,
1981
+ itemArg => {
1982
+ const item = networkRecord(itemArg, ['pageIndex', 'handoffSetPageDigest']);
1983
+ return {
1984
+ pageIndex: nonNegative(item.pageIndex),
1985
+ handoffSetPageDigest: networkDigest(item.handoffSetPageDigest),
1986
+ };
1987
+ },
1988
+ ), itemArg => String(itemArg.pageIndex).padStart(8, '0'));
1989
+ requireNetwork(entries.every((entryArg, indexArg) => entryArg.pageIndex === indexArg));
1990
+ return canonicalDigest(fleetCutoverContract.preparedVolumeHandoffSetDigestDomain, {
1991
+ entries,
1992
+ digest: `sha256:${'0'.repeat(64)}`,
1993
+ });
1994
+ });
1995
+
1996
+ /**
1997
+ * Full pinned-set proof. The caller must supply every page for the exact aggregate revision; a
1998
+ * node-local subset or a mixed revision can never satisfy this binder.
1999
+ */
2000
+ export const bindFleetCutoverPreparedVolumeHandoffPages = async (
2001
+ aggregateArg: unknown,
2002
+ pagesArg: unknown,
2003
+ ): Promise<{
2004
+ aggregate: IFleetCutoverPreparedVolumeHandoffAggregate;
2005
+ pages: IFleetCutoverPreparedVolumeHandoffPage[];
2006
+ }> => {
2007
+ const aggregate = snapshotFleetCutoverPreparedVolumeHandoffAggregate(aggregateArg);
2008
+ if ((await validateFleetCutoverPreparedVolumeHandoffAggregate(aggregate)).length) fail();
2009
+ const pages = safe(() => {
2010
+ requireNetwork(Array.isArray(pagesArg)
2011
+ && Object.getPrototypeOf(pagesArg) === Array.prototype
2012
+ && pagesArg.length <= fleetCutoverContract.maximumPreparedVolumeHandoffPages);
2013
+ const keys = Reflect.ownKeys(pagesArg);
2014
+ requireNetwork(keys.length === pagesArg.length + 1);
2015
+ const result: IFleetCutoverPreparedVolumeHandoffPage[] = [];
2016
+ for (let index = 0; index < pagesArg.length; index++) {
2017
+ const descriptor = Object.getOwnPropertyDescriptor(pagesArg, String(index));
2018
+ requireNetwork(descriptor?.enumerable && Object.hasOwn(descriptor, 'value'));
2019
+ result.push(snapshotFleetCutoverPreparedVolumeHandoffPage(descriptor.value));
2020
+ }
2021
+ return networkSorted(result, itemArg => String(itemArg.pageIndex).padStart(8, '0'));
2022
+ });
2023
+ requireNetwork(pages.length === aggregate.pageCount);
2024
+ let handoffCount = 0;
2025
+ const seen = new Set<string>();
2026
+ let previousHandoffKey: string | null = null;
2027
+ for (const [index, page] of pages.entries()) {
2028
+ requireNetwork(page.pageIndex === index
2029
+ && page.aggregate.id === aggregate.id
2030
+ && page.aggregate.generation === aggregate.generation
2031
+ && page.aggregate.digest === aggregate.digest
2032
+ && !(await validateFleetCutoverPreparedVolumeHandoffPage(page)).length);
2033
+ const expectedLength = index < pages.length - 1
2034
+ ? fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries
2035
+ : aggregate.handoffCount
2036
+ % fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries
2037
+ || fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries;
2038
+ requireNetwork(page.handoffs.length === expectedLength);
2039
+ handoffCount += page.handoffs.length;
2040
+ for (const handoff of page.handoffs) {
2041
+ const key = `${handoff.id}\u0000${handoff.generation}\u0000${handoff.digest}`;
2042
+ requireNetwork(!seen.has(key) && (previousHandoffKey === null || previousHandoffKey < key));
2043
+ seen.add(key);
2044
+ previousHandoffKey = key;
2045
+ }
2046
+ }
2047
+ const entries = pages.map(page => ({
2048
+ pageIndex: page.pageIndex,
2049
+ handoffSetPageDigest: page.handoffSetPageDigest,
2050
+ }));
2051
+ requireNetwork(handoffCount === aggregate.handoffCount
2052
+ && aggregate.handoffSetDigest
2053
+ === await computeFleetCutoverPreparedVolumeHandoffSetDigest(entries));
2054
+ return { aggregate, pages };
2055
+ };
2056
+
1212
2057
  const volumeFence = (valueArg: unknown): TFleetCutoverVolumeFence => {
1213
2058
  requireNetwork(valueArg !== null && typeof valueArg === 'object');
1214
2059
  const state = (valueArg as Record<string, unknown>).state;
@@ -1324,6 +2169,45 @@ export const bindFleetCutoverAdoptionToVolumeFence = (
1324
2169
  return { adoption, fence };
1325
2170
  });
1326
2171
 
2172
+ /**
2173
+ * Joins the accepted adoption result to the full handoff and provider-ready claim content.
2174
+ * Native attachment fencing and accepted-record dereferencing remain owner checks.
2175
+ */
2176
+ export const bindFleetCutoverAdoptionToStorageClaim = async (
2177
+ adoptionArg: unknown,
2178
+ handoffArg: unknown,
2179
+ claimArg: unknown,
2180
+ ): Promise<{
2181
+ adoption: IFleetCutoverCorestoreAdoptionReceipt;
2182
+ handoff: IFleetCutoverPreparedVolumeHandoffIntent;
2183
+ claim: IRuntimeStorageClaim;
2184
+ }> => {
2185
+ try {
2186
+ const adoption = snapshotFleetCutoverCorestoreAdoptionReceipt(adoptionArg);
2187
+ const handoff = snapshotFleetCutoverPreparedVolumeHandoffIntent(handoffArg);
2188
+ const claim = snapshotRuntimeStorageClaim(claimArg);
2189
+ if ((await validateFleetCutoverCorestoreAdoptionReceipt(adoption)).length
2190
+ || (await validateFleetCutoverPreparedVolumeHandoffIntent(handoff)).length
2191
+ || (await validateRuntimeStorageClaim(claim)).length) fail();
2192
+ await bindRuntimeStorageClaimToIntent(claim, handoff.claimIntent);
2193
+ requireNetwork(adoption.clusterId === handoff.clusterId
2194
+ && adoption.nodeId === handoff.nodeId
2195
+ && adoption.serviceId === handoff.serviceId
2196
+ && adoption.volumeId === handoff.volumeId
2197
+ && adoption.mountIdentityDigest === handoff.observedMount.mountIdentityDigest
2198
+ && adoption.handoffIntent.id === handoff.id
2199
+ && adoption.handoffIntent.generation === handoff.generation
2200
+ && adoption.handoffIntent.digest === handoff.digest
2201
+ && adoption.claim.id === claim.id
2202
+ && adoption.claim.generation === claim.generation
2203
+ && adoption.claim.digest === claim.digest
2204
+ && adoption.acceptedAt >= handoff.acceptedAt);
2205
+ return { adoption, handoff, claim };
2206
+ } catch {
2207
+ return fail();
2208
+ }
2209
+ };
2210
+
1327
2211
  export const validateFleetCutoverVolumeHandoffIntent = async (valueArg: unknown): Promise<string[]> => {
1328
2212
  try {
1329
2213
  const value = snapshotFleetCutoverVolumeHandoffIntent(valueArg);
@@ -1373,6 +2257,273 @@ const runtimeSession = (valueArg: unknown): IRuntimeSessionBinding => {
1373
2257
  };
1374
2258
  };
1375
2259
 
2260
+ const storageAdoptionGrant = (valueArg: unknown): IFleetCutoverStorageAdoptionGrant => {
2261
+ const value = networkRecord(valueArg, [
2262
+ 'id', 'generation', 'previous', 'clusterId', 'nodeId', 'serviceId', 'volumeId',
2263
+ 'observedMount', 'runtime', 'session', 'handoffIntent', 'palletFence', 'acceptedAt',
2264
+ 'digest',
2265
+ ]);
2266
+ const id = networkId(value.id);
2267
+ const generation = networkCounter(value.generation);
2268
+ const previous = value.previous === null ? null : reference(value.previous);
2269
+ requireNetwork(generation === 1
2270
+ ? previous === null
2271
+ : previous?.id === id && previous.generation === generation - 1);
2272
+ const clusterId = networkId(value.clusterId);
2273
+ const nodeId = networkNodeId(value.nodeId);
2274
+ const runtime = runtimeReference(value.runtime);
2275
+ const session = runtimeSession(value.session);
2276
+ requireNetwork(runtime.id === clusterId && runtime.phase === 'pallet'
2277
+ && session.clusterId === clusterId
2278
+ && session.nodeId === nodeId
2279
+ && session.controller.kind === 'cloudly');
2280
+ return {
2281
+ id,
2282
+ generation,
2283
+ previous,
2284
+ clusterId,
2285
+ nodeId,
2286
+ serviceId: networkId(value.serviceId),
2287
+ volumeId: networkId(value.volumeId),
2288
+ observedMount: persistentVolumeMountDescriptor(value.observedMount),
2289
+ runtime: runtime as IFleetCutoverStorageAdoptionGrant['runtime'],
2290
+ session,
2291
+ handoffIntent: reference(value.handoffIntent),
2292
+ palletFence: reference(value.palletFence),
2293
+ acceptedAt: networkCounter(value.acceptedAt),
2294
+ digest: networkDigest(value.digest),
2295
+ };
2296
+ };
2297
+
2298
+ export const snapshotFleetCutoverStorageAdoptionGrant = (
2299
+ valueArg: unknown,
2300
+ ): IFleetCutoverStorageAdoptionGrant => safe(
2301
+ () => storageAdoptionGrant(captureNetwork(valueArg)),
2302
+ );
2303
+
2304
+ export const computeFleetCutoverStorageAdoptionGrantDigest = async (
2305
+ valueArg: unknown,
2306
+ ): Promise<TSha256Digest> => canonicalDigest(
2307
+ fleetCutoverContract.storageAdoptionGrantDigestDomain,
2308
+ snapshotFleetCutoverStorageAdoptionGrant(valueArg),
2309
+ );
2310
+
2311
+ export const validateFleetCutoverStorageAdoptionGrant = async (
2312
+ valueArg: unknown,
2313
+ ): Promise<string[]> => {
2314
+ try {
2315
+ const value = snapshotFleetCutoverStorageAdoptionGrant(valueArg);
2316
+ return value.digest === await computeFleetCutoverStorageAdoptionGrantDigest(value)
2317
+ ? [] : ['fleet cutover storage adoption grant digest does not match'];
2318
+ } catch {
2319
+ return ['fleet cutover storage adoption grant must use its exact canonical schema'];
2320
+ }
2321
+ };
2322
+
2323
+ /** Content join only; the controller must still prove this is the live current session and phase. */
2324
+ export const bindFleetCutoverStorageAdoptionGrant = async (
2325
+ grantArg: unknown,
2326
+ handoffArg: unknown,
2327
+ heldFenceArg: unknown,
2328
+ ): Promise<{
2329
+ grant: IFleetCutoverStorageAdoptionGrant;
2330
+ handoff: IFleetCutoverPreparedVolumeHandoffIntent;
2331
+ fence: Extract<TFleetCutoverVolumeFence, { state: 'held' }>;
2332
+ }> => {
2333
+ try {
2334
+ const grant = snapshotFleetCutoverStorageAdoptionGrant(grantArg);
2335
+ const handoff = snapshotFleetCutoverPreparedVolumeHandoffIntent(handoffArg);
2336
+ const fence = snapshotFleetCutoverVolumeFence(heldFenceArg);
2337
+ if ((await validateFleetCutoverStorageAdoptionGrant(grant)).length
2338
+ || (await validateFleetCutoverPreparedVolumeHandoffIntent(handoff)).length
2339
+ || (await validateFleetCutoverVolumeFence(fence)).length) fail();
2340
+ requireNetwork(fence.state === 'held'
2341
+ && grant.clusterId === handoff.clusterId
2342
+ && grant.nodeId === handoff.nodeId
2343
+ && grant.serviceId === handoff.serviceId
2344
+ && grant.volumeId === handoff.volumeId
2345
+ && grant.observedMount.type === handoff.observedMount.type
2346
+ && grant.observedMount.classification === handoff.observedMount.classification
2347
+ && grant.observedMount.sourceIdentityDigest === handoff.observedMount.sourceIdentityDigest
2348
+ && grant.observedMount.targetIdentityDigest === handoff.observedMount.targetIdentityDigest
2349
+ && grant.observedMount.readOnly === handoff.observedMount.readOnly
2350
+ && grant.observedMount.mountIdentityDigest === handoff.observedMount.mountIdentityDigest
2351
+ && grant.handoffIntent.id === handoff.id
2352
+ && grant.handoffIntent.generation === handoff.generation
2353
+ && grant.handoffIntent.digest === handoff.digest
2354
+ && grant.palletFence.id === fence.id
2355
+ && grant.palletFence.generation === fence.generation
2356
+ && grant.palletFence.digest === fence.digest
2357
+ && fence.clusterId === handoff.clusterId
2358
+ && fence.nodeId === handoff.nodeId
2359
+ && fence.serviceId === handoff.serviceId
2360
+ && fence.volumeId === handoff.volumeId
2361
+ && fence.mountIdentityDigest === handoff.observedMount.mountIdentityDigest
2362
+ && fence.handoffIntent.id === handoff.id
2363
+ && fence.handoffIntent.generation === handoff.generation
2364
+ && fence.handoffIntent.digest === handoff.digest
2365
+ && fence.heldRuntime.id === handoff.runtime.id
2366
+ && fence.heldRuntime.phase === handoff.runtime.phase
2367
+ && fence.heldRuntime.generation === handoff.runtime.generation
2368
+ && grant.session.controller.id === handoff.claimIntent.controller.id
2369
+ && grant.session.controller.epoch === handoff.claimIntent.controller.epoch
2370
+ && grant.session.runtimeNamespace === handoff.claimIntent.runtimeNamespace
2371
+ && grant.acceptedAt >= fence.heldAt);
2372
+ return { grant, handoff, fence };
2373
+ } catch {
2374
+ return fail();
2375
+ }
2376
+ };
2377
+
2378
+ export const snapshotFleetCutoverStorageAdoptionBundle = (
2379
+ valueArg: unknown,
2380
+ ): IFleetCutoverStorageAdoptionBundle => safe(() => {
2381
+ const value = networkRecord(captureNetwork(valueArg), [
2382
+ 'grant', 'fence', 'adoption', 'claim',
2383
+ ]);
2384
+ const fence = snapshotFleetCutoverVolumeFence(value.fence);
2385
+ requireNetwork(fence.state === 'consumed');
2386
+ return {
2387
+ grant: snapshotFleetCutoverStorageAdoptionGrant(value.grant),
2388
+ fence,
2389
+ adoption: snapshotFleetCutoverCorestoreAdoptionReceipt(value.adoption),
2390
+ claim: snapshotRuntimeStorageClaim(value.claim),
2391
+ };
2392
+ });
2393
+
2394
+ /**
2395
+ * Binds the complete post-adoption content. Physical lineage remains the provider-produced
2396
+ * adoption digest and is deliberately never equated to the observed Docker source identity.
2397
+ */
2398
+ export const bindFleetCutoverStorageAdoptionBundle = async (
2399
+ valueArg: unknown,
2400
+ handoffArg: unknown,
2401
+ ): Promise<{
2402
+ bundle: IFleetCutoverStorageAdoptionBundle;
2403
+ handoff: IFleetCutoverPreparedVolumeHandoffIntent;
2404
+ }> => {
2405
+ try {
2406
+ const bundle = snapshotFleetCutoverStorageAdoptionBundle(valueArg);
2407
+ const handoff = snapshotFleetCutoverPreparedVolumeHandoffIntent(handoffArg);
2408
+ const { grant, fence, adoption, claim } = bundle;
2409
+ if ((await validateFleetCutoverStorageAdoptionGrant(grant)).length
2410
+ || (await validateFleetCutoverPreparedVolumeHandoffIntent(handoff)).length
2411
+ || (await validateFleetCutoverVolumeFence(fence)).length
2412
+ || (await validateFleetCutoverCorestoreAdoptionReceipt(adoption)).length
2413
+ || (await validateRuntimeStorageClaim(claim)).length) fail();
2414
+ await bindRuntimeStorageClaimToIntent(claim, handoff.claimIntent);
2415
+ requireNetwork(grant.clusterId === handoff.clusterId
2416
+ && grant.nodeId === handoff.nodeId
2417
+ && grant.serviceId === handoff.serviceId
2418
+ && grant.volumeId === handoff.volumeId
2419
+ && grant.observedMount.type === handoff.observedMount.type
2420
+ && grant.observedMount.classification === handoff.observedMount.classification
2421
+ && grant.observedMount.sourceIdentityDigest === handoff.observedMount.sourceIdentityDigest
2422
+ && grant.observedMount.targetIdentityDigest === handoff.observedMount.targetIdentityDigest
2423
+ && grant.observedMount.readOnly === handoff.observedMount.readOnly
2424
+ && grant.observedMount.mountIdentityDigest === handoff.observedMount.mountIdentityDigest
2425
+ && grant.session.controller.id === handoff.claimIntent.controller.id
2426
+ && grant.session.controller.epoch === handoff.claimIntent.controller.epoch
2427
+ && grant.session.runtimeNamespace === handoff.claimIntent.runtimeNamespace
2428
+ && grant.handoffIntent.id === handoff.id
2429
+ && grant.handoffIntent.generation === handoff.generation
2430
+ && grant.handoffIntent.digest === handoff.digest
2431
+ && fence.clusterId === grant.clusterId
2432
+ && fence.nodeId === grant.nodeId
2433
+ && fence.serviceId === grant.serviceId
2434
+ && fence.volumeId === grant.volumeId
2435
+ && fence.mountIdentityDigest === grant.observedMount.mountIdentityDigest
2436
+ && fence.handoffIntent.id === handoff.id
2437
+ && fence.handoffIntent.generation === handoff.generation
2438
+ && fence.handoffIntent.digest === handoff.digest
2439
+ && fence.previous !== null
2440
+ && fence.previous.id === grant.palletFence.id
2441
+ && fence.previous.generation === grant.palletFence.generation
2442
+ && fence.previous.digest === grant.palletFence.digest
2443
+ && fence.consumedRuntime.id === grant.runtime.id
2444
+ && fence.consumedRuntime.phase === grant.runtime.phase
2445
+ && fence.consumedRuntime.generation === grant.runtime.generation
2446
+ && adoption.clusterId === handoff.clusterId
2447
+ && adoption.nodeId === handoff.nodeId
2448
+ && adoption.serviceId === handoff.serviceId
2449
+ && adoption.volumeId === handoff.volumeId
2450
+ && adoption.mountIdentityDigest === handoff.observedMount.mountIdentityDigest
2451
+ && adoption.handoffIntent.id === handoff.id
2452
+ && adoption.handoffIntent.generation === handoff.generation
2453
+ && adoption.handoffIntent.digest === handoff.digest
2454
+ && adoption.palletFence.id === grant.palletFence.id
2455
+ && adoption.palletFence.generation === grant.palletFence.generation
2456
+ && adoption.palletFence.digest === grant.palletFence.digest
2457
+ && adoption.claim.id === claim.id
2458
+ && adoption.claim.generation === claim.generation
2459
+ && adoption.claim.digest === claim.digest
2460
+ && fence.adoptionReceipt.id === adoption.id
2461
+ && fence.adoptionReceipt.generation === adoption.generation
2462
+ && fence.adoptionReceipt.digest === adoption.digest
2463
+ && fence.claim.id === claim.id
2464
+ && fence.claim.generation === claim.generation
2465
+ && fence.claim.digest === claim.digest
2466
+ && adoption.acceptedAt >= grant.acceptedAt
2467
+ && fence.consumedAt >= adoption.acceptedAt);
2468
+ return { bundle, handoff };
2469
+ } catch {
2470
+ return fail();
2471
+ }
2472
+ };
2473
+
2474
+ export const snapshotFleetCutoverHeldVolumeFenceEvidencePage = (
2475
+ valueArg: unknown,
2476
+ ): IFleetCutoverHeldVolumeFenceEvidencePage => safe(() => {
2477
+ const value = networkRecord(captureNetwork(valueArg), ['aggregate', 'page', 'nodeId', 'fences']);
2478
+ const aggregate = reference(value.aggregate);
2479
+ const page = preparedVolumeHandoffPage(value.page);
2480
+ const nodeId = networkNodeId(value.nodeId);
2481
+ requireNetwork(page.aggregate.id === aggregate.id
2482
+ && page.aggregate.generation === aggregate.generation
2483
+ && page.aggregate.digest === aggregate.digest);
2484
+ const pageHandoffs = page.handoffs.filter(itemArg => itemArg.nodeId === nodeId);
2485
+ const fences = networkSorted(networkList(
2486
+ value.fences,
2487
+ fleetCutoverContract.maximumPreparedVolumeHandoffMutationEntries,
2488
+ itemArg => {
2489
+ const fence = volumeFence(itemArg);
2490
+ requireNetwork(fence.state === 'held' && fence.nodeId === nodeId);
2491
+ return fence;
2492
+ },
2493
+ ), itemArg => `${itemArg.handoffIntent.id}\u0000${itemArg.handoffIntent.generation}\u0000${itemArg.handoffIntent.digest}`);
2494
+ requireNetwork(fences.length === pageHandoffs.length
2495
+ && fences.every((fenceArg, indexArg) => {
2496
+ const handoff = pageHandoffs[indexArg];
2497
+ return fenceArg.handoffIntent.id === handoff.id
2498
+ && fenceArg.handoffIntent.generation === handoff.generation
2499
+ && fenceArg.handoffIntent.digest === handoff.digest;
2500
+ }));
2501
+ return { aggregate, page, nodeId, fences };
2502
+ });
2503
+
2504
+ export const validateFleetCutoverHeldVolumeFenceEvidencePage = async (
2505
+ valueArg: unknown,
2506
+ ): Promise<string[]> => {
2507
+ try {
2508
+ const value = snapshotFleetCutoverHeldVolumeFenceEvidencePage(valueArg);
2509
+ if (new TextEncoder().encode(canonicalizeStrictJson(value, fail)).byteLength
2510
+ > fleetCutoverContract.maximumHeldVolumeFenceEvidenceBytes) {
2511
+ return ['fleet cutover held volume fence evidence page exceeds its encoded byte bound'];
2512
+ }
2513
+ if ((await validateFleetCutoverPreparedVolumeHandoffPage(value.page)).length) {
2514
+ return ['fleet cutover held volume fence evidence page is not a valid immutable page'];
2515
+ }
2516
+ for (const fence of value.fences) {
2517
+ if ((await validateFleetCutoverVolumeFence(fence)).length) {
2518
+ return ['fleet cutover held volume fence evidence contains an invalid held fence'];
2519
+ }
2520
+ }
2521
+ return [];
2522
+ } catch {
2523
+ return ['fleet cutover held volume fence evidence page must use its exact canonical schema'];
2524
+ }
2525
+ };
2526
+
1376
2527
  export const snapshotFleetCutoverCoreflowRetirementEvidence = (
1377
2528
  valueArg: unknown,
1378
2529
  ): IFleetCutoverCoreflowRetirementEvidence => safe(() => {