@tangle-network/agent-provider-tangle 1.1.1 → 1.1.3
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.
- package/README.md +9 -1
- package/dist/tangle-capabilities.js +4 -1
- package/dist/tangle-workspace-branching.js +161 -48
- package/package.json +20 -29
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @tangle-network/agent-provider-tangle
|
|
2
2
|
|
|
3
3
|
Wraps `@tangle-network/sandbox` as an `AgentEnvironmentProvider`.
|
|
4
|
-
The peer range is `>=0.34.6 <1.0.0`, and this package is developed and tested against 0.
|
|
4
|
+
The peer range is `>=0.34.6 <1.0.0`, and this package is developed and tested against 0.36.4.
|
|
5
5
|
The floor is 0.34.6 because exact interactive attachment needs the host receiver, and workspace branching needs keyed snapshots, durable restores, inventory recovery, and cleanup.
|
|
6
6
|
The provider fails closed when the configured backend or its catalog entry cannot be read.
|
|
7
7
|
Newer SDKs may also provide `getBackend()` as a lookup over the same catalog.
|
|
@@ -205,6 +205,11 @@ cannot prove the complete operation surface.
|
|
|
205
205
|
Recovery reads the account inventory through Sandbox offset pages of at most
|
|
206
206
|
1,000 sandboxes and continues until a short page proves the inventory is
|
|
207
207
|
complete.
|
|
208
|
+
If a live snapshot loses its marker tags, recovery reads the owner-scoped
|
|
209
|
+
operation record by key and binds its stored result to that live snapshot id.
|
|
210
|
+
The operation record alone never revives a deleted snapshot.
|
|
211
|
+
An older Sandbox deployment that cannot answer the owner-scoped lookup returns
|
|
212
|
+
`unknown` and does not create or mutate a checkpoint.
|
|
208
213
|
The adapter also reads checkpoints from the previous marker format while writing only the current bounded format.
|
|
209
214
|
Malformed, repeated, failed, or over-bound pages return `unknown` and do not
|
|
210
215
|
mutate a resource.
|
|
@@ -241,6 +246,9 @@ if (checkpoint?.status === "created" || checkpoint?.status === "replayed") {
|
|
|
241
246
|
### Confidential forks
|
|
242
247
|
|
|
243
248
|
Sandbox returns raw TEE evidence, not a verified claim.
|
|
249
|
+
The deployed Tangle job does not carry snapshot restore or sealed-persistence inputs.
|
|
250
|
+
The provider therefore refuses new confidential workspace forks before creation.
|
|
251
|
+
Existing confidential fork records still require a matching TEE report and an external verifier before they become trusted.
|
|
244
252
|
Pass `confidentialAttestationVerifier` to `createTangleProvider` to connect a
|
|
245
253
|
trusted provider-key and measurement verifier.
|
|
246
254
|
The callback receives the raw report, the expected environment binding, and
|
|
@@ -86,6 +86,8 @@ export function defaultTangleSandboxCapabilities(harness) {
|
|
|
86
86
|
branching: {
|
|
87
87
|
checkpoint: true,
|
|
88
88
|
fork: true,
|
|
89
|
+
// The deployed Tangle job does not carry snapshot restore inputs.
|
|
90
|
+
confidential: false,
|
|
89
91
|
retrySafe: true,
|
|
90
92
|
lookup: true,
|
|
91
93
|
cleanup: true,
|
|
@@ -361,11 +363,12 @@ export function narrowedTangleCapabilities(declared, support, deployment, option
|
|
|
361
363
|
// An incomplete Sandbox surface clears every branching flag together. A
|
|
362
364
|
// partial claim would let a caller start an operation it cannot recover.
|
|
363
365
|
branching: support.workspaceBranching
|
|
364
|
-
? { ...declaredBranching }
|
|
366
|
+
? { ...declaredBranching, confidential: false }
|
|
365
367
|
: {
|
|
366
368
|
...declaredBranching,
|
|
367
369
|
checkpoint: false,
|
|
368
370
|
fork: false,
|
|
371
|
+
confidential: false,
|
|
369
372
|
...(declaredBranching.retrySafe !== undefined ? { retrySafe: false } : {}),
|
|
370
373
|
...(declaredBranching.lookup !== undefined ? { lookup: false } : {}),
|
|
371
374
|
...(declaredBranching.cleanup !== undefined ? { cleanup: false } : {}),
|
|
@@ -16,6 +16,18 @@ const MAX_MARKER_TAG_LENGTH = 128;
|
|
|
16
16
|
const MARKER_CHUNK_SIZE = 80;
|
|
17
17
|
const LEGACY_MARKER_CHUNK_SIZE = 240;
|
|
18
18
|
const MAX_MARKER_CHUNKS = 512;
|
|
19
|
+
const TANGLE_ATTESTATION_NONCE_PATTERN = /^(?:0x)?[0-9a-fA-F]{64}(?:[0-9a-fA-F]{64})?$/;
|
|
20
|
+
function confidentialForkRefusal(request) {
|
|
21
|
+
const confidential = request.confidential;
|
|
22
|
+
if (confidential?.requested !== true)
|
|
23
|
+
return undefined;
|
|
24
|
+
if (confidential.nonce === undefined ||
|
|
25
|
+
!TANGLE_ATTESTATION_NONCE_PATTERN.test(confidential.nonce))
|
|
26
|
+
return "Tangle confidential forks require a 32-64 byte hexadecimal attestation nonce";
|
|
27
|
+
if (confidential.sealed === true)
|
|
28
|
+
return "Tangle confidential forks cannot prove sealed no-persistence because the deployed job does not carry that requirement";
|
|
29
|
+
return "Tangle confidential workspace forks are unavailable because the deployed job does not carry snapshot restore inputs";
|
|
30
|
+
}
|
|
19
31
|
/**
|
|
20
32
|
* Carry the verifier only when the caller supplied one. Every option type that
|
|
21
33
|
* accepts it is exact-optional, so an explicit `undefined` is not an absent key.
|
|
@@ -45,8 +57,8 @@ export function createTangleWorkspaceBranching(options) {
|
|
|
45
57
|
*
|
|
46
58
|
* The in-process record answers first; otherwise the Sandbox inventory and
|
|
47
59
|
* operation ledger rebuild it after a restart. `absent` means the key has no
|
|
48
|
-
*
|
|
49
|
-
*
|
|
60
|
+
* record, which is the only state where a caller may go on to create one.
|
|
61
|
+
* `retired` means the durable operation settled but its snapshot is gone.
|
|
50
62
|
*/
|
|
51
63
|
const resolveCheckpoint = async (request, signal) => {
|
|
52
64
|
const local = checkpoints.get(request.idempotencyKey);
|
|
@@ -58,30 +70,20 @@ export function createTangleWorkspaceBranching(options) {
|
|
|
58
70
|
existingRequestDigest: local.request.requestDigest,
|
|
59
71
|
};
|
|
60
72
|
}
|
|
61
|
-
const recovered = await
|
|
62
|
-
if (recovered ===
|
|
73
|
+
const recovered = await reconcileCheckpoint(box, provider, request, signal);
|
|
74
|
+
if (recovered.state === "undecided") {
|
|
63
75
|
return {
|
|
64
76
|
state: "undecided",
|
|
65
|
-
message:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (!recovered)
|
|
69
|
-
return { state: "absent" };
|
|
70
|
-
if (recovered.marker.requestDigest !== request.requestDigest) {
|
|
71
|
-
return {
|
|
72
|
-
state: "conflict",
|
|
73
|
-
existingRequestDigest: recovered.marker.requestDigest,
|
|
77
|
+
message: recovered.reason === "inventory_unavailable"
|
|
78
|
+
? "Sandbox checkpoint inventory is unavailable"
|
|
79
|
+
: "Sandbox checkpoint metadata is invalid",
|
|
74
80
|
};
|
|
75
81
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return {
|
|
79
|
-
state: "undecided",
|
|
80
|
-
message: "Sandbox checkpoint metadata is invalid",
|
|
81
|
-
};
|
|
82
|
+
if (recovered.state === "found") {
|
|
83
|
+
checkpoints.set(request.idempotencyKey, recovered.record);
|
|
84
|
+
return { state: "known", record: recovered.record };
|
|
82
85
|
}
|
|
83
|
-
|
|
84
|
-
return { state: "known", record };
|
|
86
|
+
return recovered;
|
|
85
87
|
};
|
|
86
88
|
/** The fork equivalent of {@link resolveCheckpoint}. */
|
|
87
89
|
const resolveFork = async (request, signal) => {
|
|
@@ -141,17 +143,21 @@ export function createTangleWorkspaceBranching(options) {
|
|
|
141
143
|
if (known.state === "undecided") {
|
|
142
144
|
return checkpointUnknown(request, `${known.message}; retry after reconciliation`, true);
|
|
143
145
|
}
|
|
146
|
+
if (known.state === "retired") {
|
|
147
|
+
return checkpointUnknown(request, "Sandbox checkpoint operation is settled but its snapshot is no longer present; the idempotency key cannot be reused", false);
|
|
148
|
+
}
|
|
144
149
|
if (known.state === "known") {
|
|
145
150
|
return checkpointSuccess(request, known.record.checkpoint, "replayed");
|
|
146
151
|
}
|
|
147
152
|
const tags = checkpointMarkerTags(request);
|
|
148
153
|
let result;
|
|
149
154
|
try {
|
|
155
|
+
operation?.signal?.throwIfAborted();
|
|
150
156
|
result = await awaitWithSignal(box.snapshot?.({ tags, idempotencyKey: request.idempotencyKey }), operation?.signal);
|
|
151
157
|
}
|
|
152
158
|
catch (error) {
|
|
153
159
|
operation?.signal?.throwIfAborted();
|
|
154
|
-
const conflict = await checkpointConflictFromRemote(box, request, operation?.signal);
|
|
160
|
+
const conflict = await checkpointConflictFromRemote(box, provider, request, operation?.signal);
|
|
155
161
|
return (conflict ??
|
|
156
162
|
checkpointUnknown(request, `Sandbox checkpoint outcome is unresolved: ${safeError(error)}`, true));
|
|
157
163
|
}
|
|
@@ -168,6 +174,26 @@ export function createTangleWorkspaceBranching(options) {
|
|
|
168
174
|
if (resultMarker && resultMarker.requestDigest !== request.requestDigest) {
|
|
169
175
|
return checkpointConflict(request, resultMarker.requestDigest);
|
|
170
176
|
}
|
|
177
|
+
if (resultMarker &&
|
|
178
|
+
!checkpointMarkerBelongsToSource(resultMarker, provider, box.id)) {
|
|
179
|
+
return checkpointUnknown(request, "Sandbox checkpoint acknowledgement belongs to another source", false);
|
|
180
|
+
}
|
|
181
|
+
if (result.idempotency.outcome === "replayed") {
|
|
182
|
+
const recovered = await reconcileCheckpoint(box, provider, request, operation?.signal);
|
|
183
|
+
if (recovered.state === "retired" || recovered.state === "absent") {
|
|
184
|
+
return checkpointUnknown(request, "Sandbox replay acknowledged a checkpoint that is no longer present", false);
|
|
185
|
+
}
|
|
186
|
+
if (recovered.state === "undecided") {
|
|
187
|
+
return checkpointUnknown(request, recovered.reason === "metadata_invalid"
|
|
188
|
+
? "Sandbox replay returned invalid checkpoint metadata"
|
|
189
|
+
: "Sandbox replay could not be reconciled with live checkpoint inventory", true);
|
|
190
|
+
}
|
|
191
|
+
if (recovered.state === "conflict") {
|
|
192
|
+
return checkpointConflict(request, recovered.existingRequestDigest);
|
|
193
|
+
}
|
|
194
|
+
checkpoints.set(request.idempotencyKey, recovered.record);
|
|
195
|
+
return checkpointSuccess(request, recovered.record.checkpoint, "replayed");
|
|
196
|
+
}
|
|
171
197
|
if (!resultMarker) {
|
|
172
198
|
return checkpointUnknown(request, "Sandbox checkpoint acknowledgement omitted its provider recovery marker", true);
|
|
173
199
|
}
|
|
@@ -187,20 +213,13 @@ export function createTangleWorkspaceBranching(options) {
|
|
|
187
213
|
if (known.state === "undecided") {
|
|
188
214
|
return checkpointLookupUnknown(request, known.message, true);
|
|
189
215
|
}
|
|
216
|
+
if (known.state === "retired") {
|
|
217
|
+
return checkpointNotFound(request);
|
|
218
|
+
}
|
|
190
219
|
if (known.state === "known") {
|
|
191
220
|
return checkpointFound(request, known.record.checkpoint);
|
|
192
221
|
}
|
|
193
|
-
|
|
194
|
-
const lookup = await awaitWithSignal(box.getSnapshotOperation?.(request.idempotencyKey, { tags: [] }), operation?.signal);
|
|
195
|
-
const settled = lookupOutcomeFromSandbox(lookup, "checkpoint");
|
|
196
|
-
return settled.absent
|
|
197
|
-
? checkpointNotFound(request)
|
|
198
|
-
: checkpointLookupUnknown(request, settled.message, settled.retryable);
|
|
199
|
-
}
|
|
200
|
-
catch (error) {
|
|
201
|
-
operation?.signal?.throwIfAborted();
|
|
202
|
-
return checkpointLookupUnknown(request, `Sandbox checkpoint lookup failed: ${safeError(error)}`, true);
|
|
203
|
-
}
|
|
222
|
+
return checkpointNotFound(request);
|
|
204
223
|
};
|
|
205
224
|
const deleteCheckpoint = async (input, operation) => {
|
|
206
225
|
const request = WorkspaceCleanupRequestSchema.parse(input);
|
|
@@ -293,6 +312,10 @@ export function createTangleWorkspaceBranching(options) {
|
|
|
293
312
|
if (known.state === "known") {
|
|
294
313
|
return forkSuccess(request, known.record.environment, "replayed");
|
|
295
314
|
}
|
|
315
|
+
const confidentialRefusal = confidentialForkRefusal(request);
|
|
316
|
+
if (confidentialRefusal !== undefined) {
|
|
317
|
+
return forkUnknown(request, confidentialRefusal, false);
|
|
318
|
+
}
|
|
296
319
|
const checkpoint = [...checkpoints.values()].some((record) => canonicalCandidateDigest(record.checkpoint) ===
|
|
297
320
|
canonicalCandidateDigest(request.checkpoint))
|
|
298
321
|
? true
|
|
@@ -624,6 +647,7 @@ async function confidentialAttestationForChild(request, child, provider, verifie
|
|
|
624
647
|
const material = {
|
|
625
648
|
provider,
|
|
626
649
|
requested: true,
|
|
650
|
+
tee: response.attestation.tee_type,
|
|
627
651
|
nonce: confidential.nonce,
|
|
628
652
|
measurement,
|
|
629
653
|
environmentId: child.id,
|
|
@@ -681,7 +705,7 @@ async function confidentialAttestationForChild(request, child, provider, verifie
|
|
|
681
705
|
}
|
|
682
706
|
function validTeeReport(report) {
|
|
683
707
|
return (!!report &&
|
|
684
|
-
|
|
708
|
+
safeIdentifier(report.tee_type) !== undefined &&
|
|
685
709
|
Array.isArray(report.evidence) &&
|
|
686
710
|
report.evidence.length <= MAX_TEE_EVIDENCE_BYTES &&
|
|
687
711
|
report.evidence.every((value) => Number.isInteger(value) && value >= 0 && value <= 255) &&
|
|
@@ -714,6 +738,11 @@ function validSnapshotOperationResult(value) {
|
|
|
714
738
|
safeIdentifier(value.snapshotId) !== undefined &&
|
|
715
739
|
validOperationDate(value.createdAt));
|
|
716
740
|
}
|
|
741
|
+
function validTaggedSnapshotOperationResult(value) {
|
|
742
|
+
return (validSnapshotOperationResult(value) &&
|
|
743
|
+
Array.isArray(value.tags) &&
|
|
744
|
+
value.tags.every((tag) => safeString(tag) !== undefined));
|
|
745
|
+
}
|
|
717
746
|
function validForkOperationChildResult(value) {
|
|
718
747
|
return (validOperationRecord(value) &&
|
|
719
748
|
safeIdentifier(value.sandboxId ?? value.id) !== undefined &&
|
|
@@ -779,6 +808,7 @@ function forkMarkerMetadata(request, materialization = "snapshot") {
|
|
|
779
808
|
* until the ledger reports the operation succeeded.
|
|
780
809
|
*/
|
|
781
810
|
async function checkpointOperationLookup(box, marker, signal) {
|
|
811
|
+
signal?.throwIfAborted();
|
|
782
812
|
const lookup = await awaitWithSignal(box.getSnapshotOperation?.(marker.idempotencyKey, {
|
|
783
813
|
tags: marker.legacy
|
|
784
814
|
? legacyCheckpointMarkerTags(marker.request)
|
|
@@ -786,6 +816,11 @@ async function checkpointOperationLookup(box, marker, signal) {
|
|
|
786
816
|
}), signal);
|
|
787
817
|
return lookup;
|
|
788
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
|
+
}
|
|
789
824
|
async function checkpointOperationSucceeded(box, marker, signal) {
|
|
790
825
|
const lookup = await checkpointOperationLookup(box, marker, signal);
|
|
791
826
|
return (lookup?.outcome === "found" &&
|
|
@@ -833,9 +868,10 @@ function markerTags(kind, idempotencyKey, requestDigest, marker) {
|
|
|
833
868
|
return tag;
|
|
834
869
|
});
|
|
835
870
|
}
|
|
836
|
-
async function findCheckpointByKey(box, key, signal) {
|
|
871
|
+
async function findCheckpointByKey(box, provider, key, signal) {
|
|
837
872
|
let snapshots;
|
|
838
873
|
try {
|
|
874
|
+
signal?.throwIfAborted();
|
|
839
875
|
const listed = await awaitWithSignal(box.listSnapshots?.(), signal);
|
|
840
876
|
if (!Array.isArray(listed))
|
|
841
877
|
return undefined;
|
|
@@ -848,13 +884,21 @@ async function findCheckpointByKey(box, key, signal) {
|
|
|
848
884
|
if (!Array.isArray(snapshots) || snapshots.length > MAX_LIST_RESULTS) {
|
|
849
885
|
return undefined;
|
|
850
886
|
}
|
|
887
|
+
const snapshotIds = new Set();
|
|
888
|
+
let found;
|
|
851
889
|
let unresolved = false;
|
|
852
890
|
for (const snapshot of snapshots) {
|
|
853
891
|
if (!validSnapshotInfo(snapshot, box.id))
|
|
854
892
|
return undefined;
|
|
893
|
+
if (snapshotIds.has(snapshot.snapshotId))
|
|
894
|
+
return undefined;
|
|
895
|
+
snapshotIds.add(snapshot.snapshotId);
|
|
855
896
|
const marker = checkpointMarkerFromTags(snapshot.tags, key);
|
|
856
897
|
if (!marker)
|
|
857
898
|
continue;
|
|
899
|
+
if (!checkpointMarkerBelongsToSource(marker, provider, box.id)) {
|
|
900
|
+
return undefined;
|
|
901
|
+
}
|
|
858
902
|
try {
|
|
859
903
|
const lookup = await checkpointOperationLookup(box, marker, signal);
|
|
860
904
|
if (lookup?.outcome === "found" &&
|
|
@@ -863,7 +907,10 @@ async function findCheckpointByKey(box, key, signal) {
|
|
|
863
907
|
const authoritative = snapshotFromOperationResult(snapshot, lookup);
|
|
864
908
|
if (authoritative === undefined)
|
|
865
909
|
return undefined;
|
|
866
|
-
|
|
910
|
+
if (found !== undefined)
|
|
911
|
+
return undefined;
|
|
912
|
+
found = { state: "found", snapshot: authoritative, marker };
|
|
913
|
+
continue;
|
|
867
914
|
}
|
|
868
915
|
unresolved = true;
|
|
869
916
|
}
|
|
@@ -872,7 +919,69 @@ async function findCheckpointByKey(box, key, signal) {
|
|
|
872
919
|
return undefined;
|
|
873
920
|
}
|
|
874
921
|
}
|
|
875
|
-
|
|
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 };
|
|
876
985
|
}
|
|
877
986
|
/**
|
|
878
987
|
* Prefer the durable operation result over inventory metadata.
|
|
@@ -1160,6 +1269,10 @@ function markerBelongsToSource(marker, provider, sourceEnvironmentId) {
|
|
|
1160
1269
|
return (marker.request.checkpoint.provider === provider &&
|
|
1161
1270
|
marker.request.checkpoint.source.environmentId === sourceEnvironmentId);
|
|
1162
1271
|
}
|
|
1272
|
+
function checkpointMarkerBelongsToSource(marker, provider, sourceEnvironmentId) {
|
|
1273
|
+
return (marker.request.source.provider === provider &&
|
|
1274
|
+
marker.request.source.environmentId === sourceEnvironmentId);
|
|
1275
|
+
}
|
|
1163
1276
|
function checkpointMarkerFromTags(tags, key) {
|
|
1164
1277
|
if (!Array.isArray(tags) ||
|
|
1165
1278
|
tags.length > MAX_MARKER_CHUNKS + 3 ||
|
|
@@ -1307,19 +1420,19 @@ function forkMarkerFromMetadata(metadata, key) {
|
|
|
1307
1420
|
* digest covers the Sandbox request body, not this interface's identity, so it
|
|
1308
1421
|
* is never presented as an interface digest.
|
|
1309
1422
|
*/
|
|
1310
|
-
async function checkpointConflictFromRemote(box, request, signal) {
|
|
1423
|
+
async function checkpointConflictFromRemote(box, provider, request, signal) {
|
|
1311
1424
|
signal?.throwIfAborted();
|
|
1312
|
-
const recovered = await
|
|
1425
|
+
const recovered = await reconcileCheckpoint(box, provider, request, signal);
|
|
1313
1426
|
signal?.throwIfAborted();
|
|
1314
|
-
if (
|
|
1315
|
-
return
|
|
1316
|
-
if (recovered.marker.requestDigest !== request.requestDigest) {
|
|
1317
|
-
return checkpointConflict(request, recovered.marker.requestDigest);
|
|
1427
|
+
if (recovered.state === "retired") {
|
|
1428
|
+
return checkpointUnknown(request, "Sandbox checkpoint operation is settled but its snapshot is no longer present; the idempotency key cannot be reused", false);
|
|
1318
1429
|
}
|
|
1319
|
-
|
|
1320
|
-
|
|
1430
|
+
if (recovered.state === "conflict") {
|
|
1431
|
+
return checkpointConflict(request, recovered.existingRequestDigest);
|
|
1432
|
+
}
|
|
1433
|
+
return recovered.state !== "found"
|
|
1321
1434
|
? undefined
|
|
1322
|
-
: checkpointSuccess(request, record.checkpoint, "replayed");
|
|
1435
|
+
: checkpointSuccess(request, recovered.record.checkpoint, "replayed");
|
|
1323
1436
|
}
|
|
1324
1437
|
/** The fork equivalent of {@link checkpointConflictFromRemote}. */
|
|
1325
1438
|
async function forkConflictFromRemote(client, box, provider, request, verifier, signal) {
|
|
@@ -1338,10 +1451,10 @@ async function forkConflictFromRemote(client, box, provider, request, verifier,
|
|
|
1338
1451
|
: forkSuccess(request, environment, "replayed");
|
|
1339
1452
|
}
|
|
1340
1453
|
/**
|
|
1341
|
-
* Read a ledger answer for a key that left no marked resource behind.
|
|
1454
|
+
* Read a fork ledger answer for a key that left no marked resource behind.
|
|
1342
1455
|
*
|
|
1343
1456
|
* `absent` is the settled answer: a decided operation with no inventory marker
|
|
1344
|
-
* means the
|
|
1457
|
+
* means the child was cleaned after creation, and the provider must not
|
|
1345
1458
|
* resurrect it from the ledger. Every other state is undecided for the caller.
|
|
1346
1459
|
*/
|
|
1347
1460
|
function lookupOutcomeFromSandbox(lookup, kind) {
|
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-provider-tangle",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "AgentEnvironmentProvider adapter for Tangle sandboxes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"main": "dist/index.js",
|
|
8
|
-
"types": "
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
11
|
"import": "./dist/index.js",
|
|
12
|
-
"types": "./
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
13
14
|
}
|
|
14
15
|
},
|
|
15
16
|
"repository": {
|
|
@@ -19,16 +20,7 @@
|
|
|
19
20
|
},
|
|
20
21
|
"publishConfig": {
|
|
21
22
|
"access": "public",
|
|
22
|
-
"registry": "https://registry.npmjs.org"
|
|
23
|
-
"main": "./dist/index.js",
|
|
24
|
-
"types": "./dist/index.d.ts",
|
|
25
|
-
"exports": {
|
|
26
|
-
".": {
|
|
27
|
-
"import": "./dist/index.js",
|
|
28
|
-
"types": "./dist/index.d.ts",
|
|
29
|
-
"default": "./dist/index.js"
|
|
30
|
-
}
|
|
31
|
-
}
|
|
23
|
+
"registry": "https://registry.npmjs.org"
|
|
32
24
|
},
|
|
33
25
|
"files": [
|
|
34
26
|
"dist/index.d.ts",
|
|
@@ -98,15 +90,8 @@
|
|
|
98
90
|
"README.md",
|
|
99
91
|
"LICENSE"
|
|
100
92
|
],
|
|
101
|
-
"scripts": {
|
|
102
|
-
"build": "tsc -p tsconfig.json",
|
|
103
|
-
"check-types": "tsc --noEmit",
|
|
104
|
-
"clean": "rm -rf dist",
|
|
105
|
-
"prepublishOnly": "pnpm run build",
|
|
106
|
-
"test": "vitest run src"
|
|
107
|
-
},
|
|
108
93
|
"dependencies": {
|
|
109
|
-
"@tangle-network/agent-interface": "^2.
|
|
94
|
+
"@tangle-network/agent-interface": "^2.3.0"
|
|
110
95
|
},
|
|
111
96
|
"peerDependencies": {
|
|
112
97
|
"@tangle-network/sandbox": ">=0.34.6 <1.0.0"
|
|
@@ -117,12 +102,18 @@
|
|
|
117
102
|
}
|
|
118
103
|
},
|
|
119
104
|
"devDependencies": {
|
|
120
|
-
"@tangle-network/agent-eval": "0.
|
|
121
|
-
"@tangle-network/agent-
|
|
122
|
-
"@tangle-network/
|
|
123
|
-
"@
|
|
124
|
-
"@types/node": "catalog:",
|
|
105
|
+
"@tangle-network/agent-eval": "0.173.0",
|
|
106
|
+
"@tangle-network/agent-runtime": "0.190.0",
|
|
107
|
+
"@tangle-network/sandbox": "0.36.4",
|
|
108
|
+
"@types/node": "26.4.0",
|
|
125
109
|
"typescript": "7.0.2",
|
|
126
|
-
"vitest": "
|
|
110
|
+
"vitest": "4.1.11",
|
|
111
|
+
"@tangle-network/agent-provider-testkit": "0.9.1"
|
|
112
|
+
},
|
|
113
|
+
"scripts": {
|
|
114
|
+
"build": "tsc -p tsconfig.json",
|
|
115
|
+
"check-types": "tsc --noEmit",
|
|
116
|
+
"clean": "rm -rf dist",
|
|
117
|
+
"test": "vitest run src"
|
|
127
118
|
}
|
|
128
|
-
}
|
|
119
|
+
}
|