@tangle-network/agent-provider-tangle 1.1.6 → 1.1.8

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.
@@ -1,21 +1,8 @@
1
- import { ConfidentialAttestationSchema, ConfidentialExecutionRequestSchema, ForkedEnvironmentRefSchema, WorkspaceCheckpointRefSchema, WorkspaceCheckpointRequestSchema, WorkspaceCleanupAcknowledgementSchema, WorkspaceCleanupRequestSchema, WorkspaceForkRequestSchema, WorkspaceOperationLookupRequestSchema, WorkspaceCheckpointResultSchema, WorkspaceCheckpointLookupResultSchema, WorkspaceForkResultSchema, WorkspaceForkLookupResultSchema, canonicalCandidateDigest, confidentialExecutionVerified, sha256Bytes, } from "@tangle-network/agent-interface";
2
- import { awaitWithSignal, boundedIdentifier, boundedString, assertBoundedJson, MAX_LIST_RESULTS, MAX_STRING_LENGTH, SANDBOX_LIST_PAGE_SIZE, } from "./tangle-contract-safety.js";
3
- import { encodeTangleConfidentialAttestationQuote, MAX_TEE_EVIDENCE_BYTES, MAX_TEE_MEASUREMENT_BYTES, } from "./tangle-confidential-attestation.js";
4
- /**
5
- * Namespace used for provider recovery metadata.
6
- *
7
- * The values are identity markers, not security evidence. A marker can tell
8
- * the provider which request produced a resource, but only the Sandbox
9
- * operation ledger and the external verifier can prove an outcome.
10
- */
11
- const MARKER_PREFIX = "tangle-agent-ws-v1";
12
- /** Marker namespace used by releases before the 128-byte tag limit. */
13
- const LEGACY_MARKER_PREFIX = "tangle-agent-sdk:workspace:v1";
14
- const FORK_METADATA_KEY = "__tangle_agent_workspace_v1";
15
- const MAX_MARKER_TAG_LENGTH = 128;
16
- const MARKER_CHUNK_SIZE = 80;
17
- const LEGACY_MARKER_CHUNK_SIZE = 240;
18
- const MAX_MARKER_CHUNKS = 512;
1
+ import { ConfidentialAttestationSchema, ConfidentialExecutionRequestSchema, ForkedEnvironmentRefSchema, WorkspaceCheckpointRequestSchema, WorkspaceCleanupAcknowledgementSchema, WorkspaceCleanupRequestSchema, WorkspaceForkRequestSchema, WorkspaceOperationLookupRequestSchema, WorkspaceCheckpointResultSchema, WorkspaceCheckpointLookupResultSchema, WorkspaceForkResultSchema, WorkspaceForkLookupResultSchema, canonicalCandidateDigest, confidentialExecutionVerified, sha256Bytes, } from "@tangle-network/agent-interface";
2
+ import { awaitWithSignal, boundedIdentifier, boundedString, MAX_STRING_LENGTH, cloneJson, safeIdentifier, safeString, } from "./tangle-contract-safety.js";
3
+ import { encodeTangleConfidentialAttestationQuote } from "./tangle-confidential-attestation.js";
4
+ import { checkpointMarkerTags, forkMarkerMetadata, markerBelongsToSource, checkpointMarkerBelongsToSource, checkpointMarkerFromTags, forkMarkerFromMetadata, } from "./tangle-workspace-markers.js";
5
+ import { checkpointRecordFromSnapshot, validSnapshotResult, reconcileCheckpoint, findManagedCheckpoint, findForkByKey, findForkChildById, completeForkChild, findBlockingForks, lookupOutcomeFromSandbox, isoDate, } from "./tangle-workspace-recovery.js";
19
6
  const TANGLE_ATTESTATION_NONCE_PATTERN = /^(?:0x)?[0-9a-fA-F]{64}(?:[0-9a-fA-F]{64})?$/;
20
7
  function confidentialForkRefusal(request) {
21
8
  const confidential = request.confidential;
@@ -532,30 +519,6 @@ function assertCleanupProvider(request, provider) {
532
519
  throw new Error("Tangle cleanup provider does not match this provider");
533
520
  }
534
521
  }
535
- function checkpointRecordFromSnapshot(request, snapshot) {
536
- try {
537
- const createdAt = isoDate(snapshot.createdAt);
538
- const checkpoint = WorkspaceCheckpointRefSchema.parse({
539
- checkpointId: boundedIdentifier(snapshot.snapshotId, "Tangle checkpoint id"),
540
- provider: request.source.provider,
541
- source: request.source,
542
- idempotencyKey: request.idempotencyKey,
543
- requestDigest: request.requestDigest,
544
- createdAt,
545
- ...(request.metadata === undefined
546
- ? {}
547
- : { metadata: cloneJson(request.metadata) }),
548
- });
549
- return {
550
- request,
551
- checkpoint,
552
- snapshotId: checkpoint.checkpointId,
553
- };
554
- }
555
- catch {
556
- return undefined;
557
- }
558
- }
559
522
  async function environmentFromChild(request, child, provider, createdAt, verifier, signal) {
560
523
  signal?.throwIfAborted();
561
524
  const environmentId = safeIdentifier(child.id);
@@ -621,15 +584,16 @@ async function confidentialAttestationForChild(request, child, provider, verifie
621
584
  }
622
585
  if (!response ||
623
586
  response.sandbox_id !== child.id ||
624
- !validTeeReport(response.attestation) ||
587
+ !response.attestation ||
625
588
  safeIdentifier(response.attestationNonce) === undefined ||
626
589
  response.attestationNonce !== confidential.nonce) {
627
590
  return undefined;
628
591
  }
629
- const measurement = sha256Bytes(Uint8Array.from(response.attestation.measurement));
592
+ // The canonical codec validates the complete report before its bytes are used.
630
593
  const quote = encodeTangleConfidentialAttestationQuote(response.attestation);
631
594
  if (quote === undefined)
632
595
  return undefined;
596
+ const measurement = sha256Bytes(Uint8Array.from(response.attestation.measurement));
633
597
  let verifiedAt;
634
598
  try {
635
599
  verifiedAt = new Date(response.attestation.timestamp * 1_000).toISOString();
@@ -703,447 +667,6 @@ async function confidentialAttestationForChild(request, child, provider, verifie
703
667
  ? attestation.data
704
668
  : undefined;
705
669
  }
706
- function validTeeReport(report) {
707
- return (!!report &&
708
- safeIdentifier(report.tee_type) !== undefined &&
709
- Array.isArray(report.evidence) &&
710
- report.evidence.length <= MAX_TEE_EVIDENCE_BYTES &&
711
- report.evidence.every((value) => Number.isInteger(value) && value >= 0 && value <= 255) &&
712
- Array.isArray(report.measurement) &&
713
- report.measurement.length <= MAX_TEE_MEASUREMENT_BYTES &&
714
- report.measurement.every((value) => Number.isInteger(value) && value >= 0 && value <= 255) &&
715
- Number.isFinite(report.timestamp) &&
716
- report.timestamp > 0);
717
- }
718
- function validSnapshotResult(result) {
719
- return (!!result &&
720
- safeIdentifier(result.snapshotId) !== undefined &&
721
- validDate(result.createdAt) &&
722
- Array.isArray(result.tags) &&
723
- result.tags.every((tag) => safeString(tag) !== undefined));
724
- }
725
- function validSnapshotInfo(snapshot, sandboxId) {
726
- return (validSnapshotResult(snapshot) &&
727
- safeIdentifier(snapshot.sandboxId) !== undefined &&
728
- (sandboxId === undefined || snapshot.sandboxId === sandboxId));
729
- }
730
- function validOperationRecord(value) {
731
- return value !== null && typeof value === "object" && !Array.isArray(value);
732
- }
733
- function validOperationDate(value) {
734
- return ((typeof value === "string" || value instanceof Date) && validDate(value));
735
- }
736
- function validSnapshotOperationResult(value) {
737
- return (validOperationRecord(value) &&
738
- safeIdentifier(value.snapshotId) !== undefined &&
739
- validOperationDate(value.createdAt));
740
- }
741
- function validTaggedSnapshotOperationResult(value) {
742
- return (validSnapshotOperationResult(value) &&
743
- Array.isArray(value.tags) &&
744
- value.tags.every((tag) => safeString(tag) !== undefined));
745
- }
746
- function validForkOperationChildResult(value) {
747
- return (validOperationRecord(value) &&
748
- safeIdentifier(value.sandboxId ?? value.id) !== undefined &&
749
- (value.createdAt === undefined ||
750
- value.createdAt === null ||
751
- validOperationDate(value.createdAt)));
752
- }
753
- function checkpointMarkerTags(request) {
754
- const marker = {
755
- version: 1,
756
- kind: "checkpoint",
757
- idempotencyKey: request.idempotencyKey,
758
- requestDigest: request.requestDigest,
759
- request,
760
- };
761
- return markerTags("checkpoint", request.idempotencyKey, request.requestDigest, marker);
762
- }
763
- /** Rebuild the exact tags used by the release before the current safe format. */
764
- function legacyCheckpointMarkerTags(request) {
765
- const marker = {
766
- version: 1,
767
- kind: "checkpoint",
768
- idempotencyKey: request.idempotencyKey,
769
- requestDigest: request.requestDigest,
770
- request,
771
- };
772
- const encoded = encodeJson(marker);
773
- if (encoded === undefined)
774
- throw new Error("workspace marker is not JSON serializable");
775
- const base = `${LEGACY_MARKER_PREFIX}:checkpoint`;
776
- const chunks = splitIntoChunks(encoded, LEGACY_MARKER_CHUNK_SIZE);
777
- if (chunks.length > MAX_MARKER_CHUNKS) {
778
- throw new Error("workspace marker exceeds the recovery bound");
779
- }
780
- return [
781
- `${base}:key:${encodeText(request.idempotencyKey)}`,
782
- `${base}:digest:${request.requestDigest}`,
783
- ...chunks.map((chunk, index) => `${base}:material:${index}:${chunks.length}:${chunk}`),
784
- ];
785
- }
786
- function forkMarkerMetadata(request, materialization = "snapshot") {
787
- if (request.metadata && Object.hasOwn(request.metadata, FORK_METADATA_KEY)) {
788
- throw new Error(`fork metadata reserves ${FORK_METADATA_KEY}`);
789
- }
790
- const marker = {
791
- version: 1,
792
- kind: "fork",
793
- idempotencyKey: request.idempotencyKey,
794
- requestDigest: request.requestDigest,
795
- request,
796
- ...(materialization === "snapshot" ? { materialization } : {}),
797
- };
798
- assertBoundedJson(marker);
799
- return {
800
- ...(request.metadata === undefined ? {} : cloneJson(request.metadata)),
801
- [FORK_METADATA_KEY]: marker,
802
- };
803
- }
804
- /**
805
- * Ask the Sandbox operation ledger whether a marked checkpoint settled.
806
- *
807
- * A marker only names a candidate resource. Nothing is returned to a caller
808
- * until the ledger reports the operation succeeded.
809
- */
810
- async function checkpointOperationLookup(box, marker, signal) {
811
- signal?.throwIfAborted();
812
- const lookup = await awaitWithSignal(box.getSnapshotOperation?.(marker.idempotencyKey, {
813
- tags: marker.legacy
814
- ? legacyCheckpointMarkerTags(marker.request)
815
- : checkpointMarkerTags(marker.request),
816
- }), signal);
817
- return lookup;
818
- }
819
- /** Read the durable record by its owner-scoped key when no request body remains. */
820
- async function checkpointOperationLookupByKey(box, idempotencyKey, signal) {
821
- signal?.throwIfAborted();
822
- return await awaitWithSignal(box.getSnapshotOperation?.(idempotencyKey), signal);
823
- }
824
- async function checkpointOperationSucceeded(box, marker, signal) {
825
- const lookup = await checkpointOperationLookup(box, marker, signal);
826
- return (lookup?.outcome === "found" &&
827
- lookup.kind === "checkpoint" &&
828
- lookup.state === "succeeded");
829
- }
830
- /** Confirm a fork child through its marker or the legacy fork ledger. */
831
- async function forkOperationLookup(box, marker, signal) {
832
- if (marker.materialization === "snapshot") {
833
- return {
834
- outcome: "found",
835
- kind: "fork",
836
- state: "succeeded",
837
- };
838
- }
839
- const lookup = await awaitWithSignal(box.getForkOperation?.(marker.idempotencyKey, {
840
- count: 1,
841
- metadata: forkMarkerMetadata(marker.request, marker.materialization),
842
- }), signal);
843
- return lookup;
844
- }
845
- async function forkOperationSucceeded(box, marker, signal) {
846
- const lookup = await forkOperationLookup(box, marker, signal);
847
- return (lookup?.outcome === "found" &&
848
- lookup.kind === "fork" &&
849
- lookup.state === "succeeded");
850
- }
851
- function markerTags(kind, idempotencyKey, requestDigest, marker) {
852
- const encoded = encodeJson(marker);
853
- if (encoded === undefined)
854
- throw new Error("workspace marker is not JSON serializable");
855
- const base = `${MARKER_PREFIX}-${kind}`;
856
- const chunks = split(encoded);
857
- if (chunks.length > MAX_MARKER_CHUNKS) {
858
- throw new Error("workspace marker exceeds the recovery bound");
859
- }
860
- return [
861
- `${base}-key-${markerKeyDigest(idempotencyKey).replace(":", "-")}`,
862
- `${base}-digest-${requestDigest.replace(":", "-")}`,
863
- ...chunks.map((chunk, index) => `${base}-material-${index}-${chunks.length}-${chunk}`),
864
- ].map((tag) => {
865
- if (Buffer.byteLength(tag, "utf8") > MAX_MARKER_TAG_LENGTH) {
866
- throw new Error("workspace marker tag exceeds the platform bound");
867
- }
868
- return tag;
869
- });
870
- }
871
- async function findCheckpointByKey(box, provider, key, signal) {
872
- let snapshots;
873
- try {
874
- signal?.throwIfAborted();
875
- const listed = await awaitWithSignal(box.listSnapshots?.(), signal);
876
- if (!Array.isArray(listed))
877
- return undefined;
878
- snapshots = listed;
879
- }
880
- catch {
881
- signal?.throwIfAborted();
882
- return undefined;
883
- }
884
- if (!Array.isArray(snapshots) || snapshots.length > MAX_LIST_RESULTS) {
885
- return undefined;
886
- }
887
- const snapshotIds = new Set();
888
- let found;
889
- let unresolved = false;
890
- for (const snapshot of snapshots) {
891
- if (!validSnapshotInfo(snapshot, box.id))
892
- return undefined;
893
- if (snapshotIds.has(snapshot.snapshotId))
894
- return undefined;
895
- snapshotIds.add(snapshot.snapshotId);
896
- const marker = checkpointMarkerFromTags(snapshot.tags, key);
897
- if (!marker)
898
- continue;
899
- if (!checkpointMarkerBelongsToSource(marker, provider, box.id)) {
900
- return undefined;
901
- }
902
- try {
903
- const lookup = await checkpointOperationLookup(box, marker, signal);
904
- if (lookup?.outcome === "found" &&
905
- lookup.kind === "checkpoint" &&
906
- lookup.state === "succeeded") {
907
- const authoritative = snapshotFromOperationResult(snapshot, lookup);
908
- if (authoritative === undefined)
909
- return undefined;
910
- if (found !== undefined)
911
- return undefined;
912
- found = { state: "found", snapshot: authoritative, marker };
913
- continue;
914
- }
915
- unresolved = true;
916
- }
917
- catch {
918
- signal?.throwIfAborted();
919
- return undefined;
920
- }
921
- }
922
- if (found !== undefined)
923
- return found;
924
- if (unresolved)
925
- return undefined;
926
- // Some storage backends retain the snapshot but omit caller tags from a
927
- // later inventory read. The owner-scoped operation record retains the exact
928
- // acknowledgement, including those tags. Bind that record to a currently
929
- // live snapshot id before recovering it; neither record is sufficient alone.
930
- let lookup;
931
- try {
932
- lookup = await checkpointOperationLookupByKey(box, key, signal);
933
- }
934
- catch {
935
- signal?.throwIfAborted();
936
- return undefined;
937
- }
938
- if (lookup?.outcome === "not_found" && lookup.kind === "checkpoint") {
939
- return null;
940
- }
941
- if (lookup?.outcome !== "found" ||
942
- lookup.kind !== "checkpoint" ||
943
- lookup.state !== "succeeded") {
944
- return undefined;
945
- }
946
- if (snapshots.length === 0)
947
- return { state: "retired" };
948
- if (!validTaggedSnapshotOperationResult(lookup.result))
949
- return undefined;
950
- const live = snapshots.filter((snapshot) => snapshot.snapshotId === lookup.result?.snapshotId);
951
- if (live.length === 0)
952
- return { state: "retired" };
953
- if (live.length !== 1)
954
- return undefined;
955
- const marker = checkpointMarkerFromTags(lookup.result.tags, key);
956
- if (!marker ||
957
- !checkpointMarkerBelongsToSource(marker, provider, box.id)) {
958
- return undefined;
959
- }
960
- const authoritative = snapshotFromOperationResult(live[0], lookup);
961
- return authoritative === undefined
962
- ? undefined
963
- : { state: "found", snapshot: authoritative, marker };
964
- }
965
- /** Normalize one remote checkpoint recovery attempt for every caller. */
966
- async function reconcileCheckpoint(box, provider, request, signal) {
967
- const recovered = await findCheckpointByKey(box, provider, request.idempotencyKey, signal);
968
- if (recovered === undefined) {
969
- return { state: "undecided", reason: "inventory_unavailable" };
970
- }
971
- if (recovered === null)
972
- return { state: "absent" };
973
- if (recovered.state === "retired")
974
- return recovered;
975
- if (recovered.marker.requestDigest !== request.requestDigest) {
976
- return {
977
- state: "conflict",
978
- existingRequestDigest: recovered.marker.requestDigest,
979
- };
980
- }
981
- const record = checkpointRecordFromSnapshot(recovered.marker.request, recovered.snapshot);
982
- return record === undefined
983
- ? { state: "undecided", reason: "metadata_invalid" }
984
- : { state: "found", record };
985
- }
986
- /**
987
- * Prefer the durable operation result over inventory metadata.
988
- *
989
- * Snapshot inventory and the operation ledger can expose different creation
990
- * timestamps. The ledger result is the acknowledgement returned by the
991
- * idempotent operation, so recovery must rebuild the exact checkpoint ref
992
- * from it when the service provides that result.
993
- */
994
- function snapshotFromOperationResult(snapshot, lookup) {
995
- if (lookup.result === undefined)
996
- return snapshot;
997
- if (!validSnapshotOperationResult(lookup.result) ||
998
- lookup.result.snapshotId !== snapshot.snapshotId) {
999
- return undefined;
1000
- }
1001
- return { ...snapshot, createdAt: lookup.result.createdAt };
1002
- }
1003
- /**
1004
- * Confirm that one snapshot id is a settled checkpoint this provider created.
1005
- *
1006
- * `expected` binds the answer to a specific checkpoint reference. A reference
1007
- * that does not match its marker is absent, not unknown: the caller supplied a
1008
- * checkpoint this source never produced.
1009
- */
1010
- async function findManagedCheckpoint(box, provider, id, expected, signal) {
1011
- try {
1012
- const snapshots = await awaitWithSignal(box.listSnapshots?.(), signal);
1013
- if (!Array.isArray(snapshots) || snapshots.length > MAX_LIST_RESULTS) {
1014
- return "unknown";
1015
- }
1016
- const snapshot = snapshots.find((candidate) => candidate.snapshotId === id);
1017
- if (!snapshot)
1018
- return false;
1019
- if (!validSnapshotInfo(snapshot, box.id))
1020
- return "unknown";
1021
- const marker = checkpointMarkerFromTags(snapshot.tags, expected?.idempotencyKey);
1022
- if (!marker)
1023
- return expected ? false : "unknown";
1024
- if (marker.request.source.provider !== provider ||
1025
- marker.request.source.environmentId !== box.id) {
1026
- return expected ? false : "unknown";
1027
- }
1028
- if (expected &&
1029
- (marker.requestDigest !== expected.requestDigest ||
1030
- canonicalCandidateDigest(marker.request.source) !==
1031
- canonicalCandidateDigest(expected.source))) {
1032
- return false;
1033
- }
1034
- return (await checkpointOperationSucceeded(box, marker, signal))
1035
- ? true
1036
- : "unknown";
1037
- }
1038
- catch {
1039
- signal?.throwIfAborted();
1040
- return "unknown";
1041
- }
1042
- }
1043
- async function findForkByKey(client, box, provider, key, signal) {
1044
- const candidates = await listMarkedForkChildren(client, box, provider, key, signal);
1045
- if (candidates === undefined)
1046
- return undefined;
1047
- let unresolved = false;
1048
- for (const candidate of candidates) {
1049
- try {
1050
- const lookup = await forkOperationLookup(box, candidate.marker, signal);
1051
- if (lookup?.outcome === "found" &&
1052
- lookup.kind === "fork" &&
1053
- lookup.state === "succeeded") {
1054
- const authoritative = childFromOperationResult(candidate.child, lookup);
1055
- if (authoritative === undefined)
1056
- return undefined;
1057
- return { ...authoritative, marker: candidate.marker };
1058
- }
1059
- unresolved = true;
1060
- }
1061
- catch {
1062
- signal?.throwIfAborted();
1063
- return undefined;
1064
- }
1065
- }
1066
- return unresolved ? undefined : null;
1067
- }
1068
- /**
1069
- * Prefer the durable fork result over account-inventory metadata.
1070
- *
1071
- * Fork inventory can report a child timestamp from a later registry read. The
1072
- * operation ledger stores the original child acknowledgement, which is the
1073
- * stable value required to replay one exact fork reference after a restart.
1074
- * Some Sandbox responses omit that timestamp, so the validated inventory
1075
- * record supplies it only when the operation result does not.
1076
- */
1077
- function childFromOperationResult(child, lookup) {
1078
- if (lookup.result === undefined) {
1079
- return { child, createdAt: child.createdAt };
1080
- }
1081
- const result = lookup.result;
1082
- if (!validOperationRecord(result))
1083
- return undefined;
1084
- const children = result.children;
1085
- if (!Array.isArray(children))
1086
- return undefined;
1087
- const operationChild = children.find((candidate) => validForkOperationChildResult(candidate) &&
1088
- (candidate.sandboxId ?? candidate.id) === child.id);
1089
- if (!operationChild)
1090
- return undefined;
1091
- const createdAt = operationChild.createdAt ?? child.createdAt;
1092
- if (!validOperationDate(createdAt))
1093
- return undefined;
1094
- return { child, createdAt };
1095
- }
1096
- async function findForkChildById(client, box, provider, id, signal) {
1097
- try {
1098
- if (typeof client.get !== "function")
1099
- return undefined;
1100
- const child = await awaitWithSignal(client.get(id, signal ? { signal } : undefined), signal);
1101
- if (child === null)
1102
- return null;
1103
- if (child.id !== id)
1104
- return undefined;
1105
- const marker = forkMarkerFromMetadata(child.metadata);
1106
- if (!marker || !markerBelongsToSource(marker, provider, box.id))
1107
- return undefined;
1108
- return (await forkOperationSucceeded(box, marker, signal))
1109
- ? child
1110
- : undefined;
1111
- }
1112
- catch {
1113
- signal?.throwIfAborted();
1114
- return undefined;
1115
- }
1116
- }
1117
- /**
1118
- * Resolve a complete child identity when an acknowledgement omits durable data.
1119
- *
1120
- * A branch response can precede a richer registry read during a rolling
1121
- * deployment. Recover the exact child when its creation time or provider
1122
- * marker is absent. Never invent either field from the request.
1123
- */
1124
- async function completeForkChild(client, child, signal) {
1125
- if (child.createdAt !== undefined &&
1126
- forkMarkerFromMetadata(child.metadata) !== undefined) {
1127
- return child;
1128
- }
1129
- if (typeof client.get !== "function" ||
1130
- safeIdentifier(child.id) === undefined) {
1131
- return undefined;
1132
- }
1133
- try {
1134
- const resolved = await awaitWithSignal(client.get(child.id, signal ? { signal } : undefined), signal);
1135
- if (!resolved ||
1136
- resolved.id !== child.id ||
1137
- resolved.createdAt === undefined) {
1138
- return undefined;
1139
- }
1140
- return resolved;
1141
- }
1142
- catch {
1143
- signal?.throwIfAborted();
1144
- return undefined;
1145
- }
1146
- }
1147
670
  /** Remove only a child this exact create call confirmed it created. */
1148
671
  async function compensateCreatedForkChild(child, outcome, signal) {
1149
672
  if (outcome !== "created")
@@ -1164,255 +687,6 @@ async function compensateCreatedForkChild(child, outcome, signal) {
1164
687
  return "unconfirmed";
1165
688
  }
1166
689
  }
1167
- async function findBlockingForks(box, client, provider, checkpointId, signal) {
1168
- const candidates = await listMarkedForkChildren(client, box, provider, undefined, signal);
1169
- if (candidates === undefined)
1170
- return undefined;
1171
- const blocking = new Set();
1172
- for (const { child, marker } of candidates) {
1173
- if (marker.request.checkpoint.checkpointId !== checkpointId)
1174
- continue;
1175
- try {
1176
- // A candidate that cannot be confirmed leaves the dependency set
1177
- // unknown, so cleanup must not proceed on a partial answer.
1178
- if (!(await forkOperationSucceeded(box, marker, signal)))
1179
- return undefined;
1180
- blocking.add(child.id);
1181
- }
1182
- catch {
1183
- signal?.throwIfAborted();
1184
- return undefined;
1185
- }
1186
- }
1187
- return [...blocking].sort();
1188
- }
1189
- /**
1190
- * Read the complete account inventory through Sandbox offset pages.
1191
- *
1192
- * Sandbox returns only an array, so a short page is the terminal marker. A
1193
- * full page requires another request; stopping there would make recovery
1194
- * report a false absence. Duplicate ids or an inventory above the safety
1195
- * bound make completeness unknowable and therefore fail closed.
1196
- */
1197
- async function listAllSandboxChildren(client, signal) {
1198
- if (typeof client.list !== "function")
1199
- return undefined;
1200
- const children = [];
1201
- const seen = new Set();
1202
- let offset = 0;
1203
- while (true) {
1204
- signal?.throwIfAborted();
1205
- let page;
1206
- try {
1207
- const listed = await awaitWithSignal(client.list({
1208
- scope: "all",
1209
- limit: SANDBOX_LIST_PAGE_SIZE,
1210
- offset,
1211
- }), signal);
1212
- if (!Array.isArray(listed) || listed.length > SANDBOX_LIST_PAGE_SIZE) {
1213
- return undefined;
1214
- }
1215
- page = listed;
1216
- }
1217
- catch {
1218
- signal?.throwIfAborted();
1219
- return undefined;
1220
- }
1221
- for (const child of page) {
1222
- if (!child ||
1223
- typeof child !== "object" ||
1224
- safeIdentifier(child.id) === undefined ||
1225
- seen.has(child.id)) {
1226
- return undefined;
1227
- }
1228
- seen.add(child.id);
1229
- }
1230
- if (children.length + page.length > MAX_LIST_RESULTS)
1231
- return undefined;
1232
- children.push(...page);
1233
- if (page.length < SANDBOX_LIST_PAGE_SIZE)
1234
- return children;
1235
- if (offset > Number.MAX_SAFE_INTEGER - SANDBOX_LIST_PAGE_SIZE) {
1236
- return undefined;
1237
- }
1238
- offset += SANDBOX_LIST_PAGE_SIZE;
1239
- }
1240
- }
1241
- /**
1242
- * Read every account child that carries a fork marker this source produced.
1243
- *
1244
- * The scan is the shared front half of fork recovery and cleanup. It returns
1245
- * undefined when the inventory itself cannot be trusted, so both callers fail
1246
- * closed on the same condition.
1247
- */
1248
- async function listMarkedForkChildren(client, box, provider, key, signal) {
1249
- const children = await listAllSandboxChildren(client, signal);
1250
- if (children === undefined)
1251
- return undefined;
1252
- const marked = [];
1253
- for (const child of children) {
1254
- if (!child ||
1255
- typeof child !== "object" ||
1256
- safeIdentifier(child.id) === undefined) {
1257
- return undefined;
1258
- }
1259
- if (child.id === box.id)
1260
- continue;
1261
- const marker = forkMarkerFromMetadata(child.metadata, key);
1262
- if (!marker || !markerBelongsToSource(marker, provider, box.id))
1263
- continue;
1264
- marked.push({ child, marker });
1265
- }
1266
- return marked;
1267
- }
1268
- function markerBelongsToSource(marker, provider, sourceEnvironmentId) {
1269
- return (marker.request.checkpoint.provider === provider &&
1270
- marker.request.checkpoint.source.environmentId === sourceEnvironmentId);
1271
- }
1272
- function checkpointMarkerBelongsToSource(marker, provider, sourceEnvironmentId) {
1273
- return (marker.request.source.provider === provider &&
1274
- marker.request.source.environmentId === sourceEnvironmentId);
1275
- }
1276
- function checkpointMarkerFromTags(tags, key) {
1277
- if (!Array.isArray(tags) ||
1278
- tags.length > MAX_MARKER_CHUNKS + 3 ||
1279
- !tags.every((tag) => safeString(tag) !== undefined)) {
1280
- return undefined;
1281
- }
1282
- const currentBase = `${MARKER_PREFIX}-checkpoint`;
1283
- const legacyBase = `${LEGACY_MARKER_PREFIX}:checkpoint`;
1284
- const hasCurrentTags = tags.some((tag) => tag.startsWith(`${currentBase}-`));
1285
- const hasLegacyTags = tags.some((tag) => tag.startsWith(`${legacyBase}:`));
1286
- if (hasCurrentTags === hasLegacyTags)
1287
- return undefined;
1288
- if (hasLegacyTags)
1289
- return legacyCheckpointMarkerFromTags(tags, key, legacyBase);
1290
- if (tags.some((tag) => Buffer.byteLength(tag, "utf8") > MAX_MARKER_TAG_LENGTH)) {
1291
- return undefined;
1292
- }
1293
- return currentCheckpointMarkerFromTags(tags, key, currentBase);
1294
- }
1295
- function currentCheckpointMarkerFromTags(tags, key, base) {
1296
- const keyTag = tags.find((tag) => tag.startsWith(`${base}-key-`));
1297
- if (keyTag &&
1298
- key !== undefined &&
1299
- keyTag.slice(`${base}-key-`.length) !==
1300
- markerKeyDigest(key).replace(":", "-")) {
1301
- return undefined;
1302
- }
1303
- const chunks = tags
1304
- .map((tag) => {
1305
- const match = tag.match(new RegExp(`^${escapeRegExp(base)}-material-(\\d+)-(\\d+)-([A-Za-z0-9_-]+)$`));
1306
- return match
1307
- ? { index: Number(match[1]), total: Number(match[2]), chunk: match[3] }
1308
- : undefined;
1309
- })
1310
- .filter((value) => value !== undefined)
1311
- .sort((left, right) => left.index - right.index);
1312
- if (chunks.length === 0 ||
1313
- chunks[0].total < 1 ||
1314
- chunks[0].total > MAX_MARKER_CHUNKS ||
1315
- chunks[0].total !== chunks.length ||
1316
- chunks.some((chunk, index) => !Number.isSafeInteger(chunk.index) ||
1317
- !Number.isSafeInteger(chunk.total) ||
1318
- chunk.index !== index ||
1319
- chunk.total !== chunks[0].total)) {
1320
- return undefined;
1321
- }
1322
- const decoded = decodeJson(chunks.map((chunk) => chunk.chunk).join(""));
1323
- return checkpointMarkerFromUnknown(decoded, key);
1324
- }
1325
- function legacyCheckpointMarkerFromTags(tags, key, base) {
1326
- const keyTag = tags.find((tag) => tag.startsWith(`${base}:key:`));
1327
- if (keyTag &&
1328
- key !== undefined &&
1329
- decodeText(keyTag.slice(`${base}:key:`.length)) !== key) {
1330
- return undefined;
1331
- }
1332
- const chunks = tags
1333
- .map((tag) => {
1334
- const match = tag.match(new RegExp(`^${escapeRegExp(base)}:material:(\\d+):(\\d+):([A-Za-z0-9_-]+)$`));
1335
- return match
1336
- ? { index: Number(match[1]), total: Number(match[2]), chunk: match[3] }
1337
- : undefined;
1338
- })
1339
- .filter((value) => value !== undefined)
1340
- .sort((left, right) => left.index - right.index);
1341
- if (chunks.length === 0 ||
1342
- chunks[0].total < 1 ||
1343
- chunks[0].total > MAX_MARKER_CHUNKS ||
1344
- chunks[0].total !== chunks.length ||
1345
- chunks.some((chunk, index) => !Number.isSafeInteger(chunk.index) ||
1346
- !Number.isSafeInteger(chunk.total) ||
1347
- chunk.index !== index ||
1348
- chunk.total !== chunks[0].total)) {
1349
- return undefined;
1350
- }
1351
- const decoded = decodeJson(chunks.map((chunk) => chunk.chunk).join(""));
1352
- return checkpointMarkerFromUnknown(decoded, key, true);
1353
- }
1354
- function checkpointMarkerFromUnknown(value, key, legacy = false) {
1355
- if (!value || typeof value !== "object")
1356
- return undefined;
1357
- const parsed = value;
1358
- if (parsed.version !== 1 ||
1359
- parsed.kind !== "checkpoint" ||
1360
- typeof parsed.idempotencyKey !== "string" ||
1361
- typeof parsed.requestDigest !== "string")
1362
- return undefined;
1363
- if (key !== undefined && parsed.idempotencyKey !== key)
1364
- return undefined;
1365
- const request = WorkspaceCheckpointRequestSchema.safeParse(parsed.request);
1366
- if (!request.success ||
1367
- request.data.idempotencyKey !== parsed.idempotencyKey ||
1368
- request.data.requestDigest !== parsed.requestDigest)
1369
- return undefined;
1370
- return {
1371
- version: 1,
1372
- kind: "checkpoint",
1373
- idempotencyKey: parsed.idempotencyKey,
1374
- requestDigest: parsed.requestDigest,
1375
- request: request.data,
1376
- ...(legacy ? { legacy: true } : {}),
1377
- };
1378
- }
1379
- function forkMarkerFromMetadata(metadata, key) {
1380
- if (!metadata ||
1381
- typeof metadata !== "object" ||
1382
- !Object.hasOwn(metadata, FORK_METADATA_KEY)) {
1383
- return undefined;
1384
- }
1385
- const value = metadata[FORK_METADATA_KEY];
1386
- if (!value || typeof value !== "object")
1387
- return undefined;
1388
- const parsed = value;
1389
- if (parsed.version !== 1 ||
1390
- parsed.kind !== "fork" ||
1391
- typeof parsed.idempotencyKey !== "string" ||
1392
- typeof parsed.requestDigest !== "string")
1393
- return undefined;
1394
- if (parsed.materialization !== undefined &&
1395
- parsed.materialization !== "snapshot") {
1396
- return undefined;
1397
- }
1398
- if (key !== undefined && parsed.idempotencyKey !== key)
1399
- return undefined;
1400
- const request = WorkspaceForkRequestSchema.safeParse(parsed.request);
1401
- if (!request.success ||
1402
- request.data.idempotencyKey !== parsed.idempotencyKey ||
1403
- request.data.requestDigest !== parsed.requestDigest)
1404
- return undefined;
1405
- return {
1406
- version: 1,
1407
- kind: "fork",
1408
- idempotencyKey: parsed.idempotencyKey,
1409
- requestDigest: parsed.requestDigest,
1410
- request: request.data,
1411
- ...(parsed.materialization === "snapshot"
1412
- ? { materialization: "snapshot" }
1413
- : {}),
1414
- };
1415
- }
1416
690
  /**
1417
691
  * Turn a failed create into a conflict only from provider-owned material.
1418
692
  *
@@ -1450,38 +724,6 @@ async function forkConflictFromRemote(client, box, provider, request, verifier,
1450
724
  ? undefined
1451
725
  : forkSuccess(request, environment, "replayed");
1452
726
  }
1453
- /**
1454
- * Read a fork ledger answer for a key that left no marked resource behind.
1455
- *
1456
- * `absent` is the settled answer: a decided operation with no inventory marker
1457
- * means the child was cleaned after creation, and the provider must not
1458
- * resurrect it from the ledger. Every other state is undecided for the caller.
1459
- */
1460
- function lookupOutcomeFromSandbox(lookup, kind) {
1461
- if (!lookup || lookup.kind !== kind) {
1462
- return {
1463
- absent: false,
1464
- message: `Sandbox returned no ${kind} lookup`,
1465
- retryable: true,
1466
- };
1467
- }
1468
- if (lookup.outcome === "conflict") {
1469
- return {
1470
- absent: false,
1471
- message: "Sandbox found a conflicting operation without provider identity",
1472
- retryable: false,
1473
- };
1474
- }
1475
- if (lookup.outcome !== "not_found" &&
1476
- (lookup.outcome === "unknown" || lookup.state !== "succeeded")) {
1477
- return {
1478
- absent: false,
1479
- message: `Sandbox ${kind} operation is not decided`,
1480
- retryable: true,
1481
- };
1482
- }
1483
- return { absent: true };
1484
- }
1485
727
  function checkpointSuccess(request, checkpoint, status) {
1486
728
  return WorkspaceCheckpointResultSchema.parse({
1487
729
  status,
@@ -1640,85 +882,7 @@ function cleanupTransportFailure(request, message, retryable) {
1640
882
  retryable,
1641
883
  });
1642
884
  }
1643
- function isoDate(value) {
1644
- const date = value instanceof Date ? value : new Date(value);
1645
- if (!Number.isFinite(date.getTime()))
1646
- throw new Error("Sandbox returned an invalid workspace timestamp");
1647
- return date.toISOString();
1648
- }
1649
- function validDate(value) {
1650
- if (value === undefined)
1651
- return false;
1652
- const date = value instanceof Date ? value : new Date(value);
1653
- return Number.isFinite(date.getTime());
1654
- }
1655
- function cloneJson(value) {
1656
- assertBoundedJson(value);
1657
- return structuredClone(value);
1658
- }
1659
- function safeIdentifier(value) {
1660
- if (typeof value !== "string" ||
1661
- value.length === 0 ||
1662
- value.length > 512 ||
1663
- value.trim() !== value)
1664
- return undefined;
1665
- return value;
1666
- }
1667
- function safeString(value) {
1668
- if (typeof value !== "string" ||
1669
- value.length === 0 ||
1670
- value.length > MAX_STRING_LENGTH)
1671
- return undefined;
1672
- return value;
1673
- }
1674
885
  function safeError(error) {
1675
886
  const message = error instanceof Error ? error.message : "transport error";
1676
887
  return message.slice(0, MAX_STRING_LENGTH);
1677
888
  }
1678
- function encodeText(value) {
1679
- return Buffer.from(value, "utf8").toString("base64url");
1680
- }
1681
- function decodeText(value) {
1682
- try {
1683
- const decoded = Buffer.from(value, "base64url").toString("utf8");
1684
- return encodeText(decoded) === value ? decoded : undefined;
1685
- }
1686
- catch {
1687
- return undefined;
1688
- }
1689
- }
1690
- function markerKeyDigest(value) {
1691
- return sha256Bytes(Buffer.from(value, "utf8"));
1692
- }
1693
- function encodeJson(value) {
1694
- try {
1695
- const serialized = JSON.stringify(value);
1696
- if (serialized === undefined)
1697
- return undefined;
1698
- return encodeText(serialized);
1699
- }
1700
- catch {
1701
- return undefined;
1702
- }
1703
- }
1704
- function decodeJson(value) {
1705
- try {
1706
- return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
1707
- }
1708
- catch {
1709
- return undefined;
1710
- }
1711
- }
1712
- function split(value) {
1713
- return splitIntoChunks(value, MARKER_CHUNK_SIZE);
1714
- }
1715
- function splitIntoChunks(value, chunkSize) {
1716
- const chunks = [];
1717
- for (let index = 0; index < value.length; index += chunkSize) {
1718
- chunks.push(value.slice(index, index + chunkSize));
1719
- }
1720
- return chunks;
1721
- }
1722
- function escapeRegExp(value) {
1723
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1724
- }