@tangle-network/agent-provider-tangle 1.0.0 → 1.0.2
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 +7 -6
- package/dist/tangle-workspace-branching.js +80 -54
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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.
|
|
5
|
-
The floor is 0.34.
|
|
4
|
+
The peer range is `>=0.34.6 <1.0.0`, and this package is developed and tested against 0.34.6.
|
|
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.
|
|
8
8
|
|
|
@@ -112,7 +112,7 @@ and recovering a checkpoint, forking one managed child, and cleaning both
|
|
|
112
112
|
resources in dependency order.
|
|
113
113
|
The adapter advertises `branching.checkpoint`, `branching.fork`, `retrySafe`,
|
|
114
114
|
`lookup`, and `cleanup` only when the linked SDK exposes the complete managed
|
|
115
|
-
surface: keyed `snapshot
|
|
115
|
+
surface: keyed `snapshot`, durable snapshot restore, inventory recovery, and
|
|
116
116
|
explicit deletion outcomes.
|
|
117
117
|
An incomplete SDK surface clears every branching flag and omits
|
|
118
118
|
`environment.workspaceBranching`.
|
|
@@ -122,9 +122,10 @@ calling Sandbox.
|
|
|
122
122
|
Retries with the same key replay the original resource, while changed material
|
|
123
123
|
returns a conflict containing the original interface digest.
|
|
124
124
|
The provider stores a bounded request marker in snapshot tags and child
|
|
125
|
-
metadata so a fresh process can recover the exact interface digest
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
metadata so a fresh process can recover the exact interface digest.
|
|
126
|
+
Snapshot-created children are validated through the marker and complete
|
|
127
|
+
account inventory, while legacy fork markers also require the Sandbox fork
|
|
128
|
+
ledger to report a settled success.
|
|
128
129
|
Checkpoint deletion reports `in_use` with every verified child that still
|
|
129
130
|
references it; delete the child first, then retry checkpoint deletion.
|
|
130
131
|
The adapter never treats an SDK response without an explicit idempotency or
|
|
@@ -303,47 +303,64 @@ export function createTangleWorkspaceBranching(options) {
|
|
|
303
303
|
: "Sandbox checkpoint inventory is unavailable", true);
|
|
304
304
|
}
|
|
305
305
|
const metadata = forkMarkerMetadata(request);
|
|
306
|
-
let
|
|
306
|
+
let returnedChild;
|
|
307
|
+
let outcome;
|
|
307
308
|
try {
|
|
308
|
-
|
|
309
|
-
|
|
309
|
+
// Sandbox fork copies live VM memory, not the durable workspace view.
|
|
310
|
+
// Restore the checkpoint so files written before the checkpoint survive.
|
|
311
|
+
returnedChild = await awaitWithSignal(client.create({
|
|
312
|
+
fromSnapshot: request.checkpoint.checkpointId,
|
|
313
|
+
fromSandboxId: box.id,
|
|
310
314
|
idempotencyKey: request.idempotencyKey,
|
|
311
|
-
|
|
315
|
+
metadata,
|
|
316
|
+
...(request.name === undefined ? {} : { name: request.name }),
|
|
317
|
+
}, operation?.signal === undefined
|
|
318
|
+
? undefined
|
|
319
|
+
: { signal: operation.signal }), operation?.signal);
|
|
320
|
+
const receipt = returnedChild.createReceipt?.();
|
|
321
|
+
if (!receipt ||
|
|
322
|
+
receipt.idempotencyKeyApplied !== true ||
|
|
323
|
+
(receipt.outcome !== "created" &&
|
|
324
|
+
receipt.outcome !== "idempotent_replay")) {
|
|
325
|
+
const compensation = await compensateCreatedForkChild(returnedChild, receipt?.outcome === "created" ? "created" : "replayed", operation?.signal);
|
|
326
|
+
const removed = compensation === "destroyed" || compensation === "already_absent";
|
|
327
|
+
return forkUnknown(request, removed
|
|
328
|
+
? "Sandbox snapshot restore returned no complete create receipt; the newly created child was removed"
|
|
329
|
+
: "Sandbox snapshot restore returned no complete create receipt; child cleanup was not confirmed", !removed);
|
|
330
|
+
}
|
|
331
|
+
outcome =
|
|
332
|
+
receipt.outcome === "idempotent_replay" ? "replayed" : "created";
|
|
312
333
|
}
|
|
313
334
|
catch (error) {
|
|
314
335
|
operation?.signal?.throwIfAborted();
|
|
315
336
|
const conflict = await forkConflictFromRemote(client, box, provider, request, options.confidentialAttestationVerifier, operation?.signal);
|
|
316
337
|
return (conflict ??
|
|
317
|
-
forkUnknown(request, `Sandbox
|
|
318
|
-
}
|
|
319
|
-
if (!
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
338
|
+
forkUnknown(request, `Sandbox snapshot restore outcome is unresolved: ${safeError(error)}`, true));
|
|
339
|
+
}
|
|
340
|
+
if (!returnedChild ||
|
|
341
|
+
typeof returnedChild !== "object" ||
|
|
342
|
+
safeIdentifier(returnedChild.id) === undefined ||
|
|
343
|
+
returnedChild.id === box.id) {
|
|
344
|
+
const compensation = returnedChild
|
|
345
|
+
? await compensateCreatedForkChild(returnedChild, outcome, operation?.signal)
|
|
346
|
+
: "not_attempted";
|
|
347
|
+
const removed = compensation === "destroyed" || compensation === "already_absent";
|
|
348
|
+
return forkUnknown(request, removed
|
|
349
|
+
? "Sandbox snapshot restore returned an invalid child; the newly created child was removed"
|
|
350
|
+
: "Sandbox snapshot restore returned an invalid child; child cleanup was not confirmed", !removed);
|
|
329
351
|
}
|
|
330
|
-
const returnedChild = result.children[0];
|
|
331
352
|
const child = await completeForkChild(client, returnedChild, operation?.signal);
|
|
332
353
|
if (!child) {
|
|
333
|
-
const compensation =
|
|
334
|
-
? await compensateUnmarkedForkChild(result, returnedChild, operation?.signal)
|
|
335
|
-
: "not_attempted";
|
|
354
|
+
const compensation = await compensateCreatedForkChild(returnedChild, outcome, operation?.signal);
|
|
336
355
|
const removed = compensation === "destroyed" || compensation === "already_absent";
|
|
337
356
|
return forkUnknown(request, removed
|
|
338
|
-
? "Sandbox
|
|
339
|
-
:
|
|
340
|
-
? "Sandbox fork returned a child without a complete identity; child cleanup was not confirmed"
|
|
341
|
-
: "Sandbox fork returned a child without a complete identity", !removed);
|
|
357
|
+
? "Sandbox snapshot restore returned a child without a complete identity; the newly created child was removed"
|
|
358
|
+
: "Sandbox snapshot restore returned a child without a complete identity; child cleanup was not confirmed", !removed);
|
|
342
359
|
}
|
|
343
360
|
const childMarker = forkMarkerFromMetadata(child.metadata);
|
|
344
361
|
if (childMarker) {
|
|
345
362
|
if (!markerBelongsToSource(childMarker, provider, box.id)) {
|
|
346
|
-
return forkUnknown(request, "Sandbox
|
|
363
|
+
return forkUnknown(request, "Sandbox snapshot restore returned a child marked for another source", true);
|
|
347
364
|
}
|
|
348
365
|
if (childMarker.idempotencyKey !== request.idempotencyKey ||
|
|
349
366
|
childMarker.requestDigest !== request.requestDigest) {
|
|
@@ -351,21 +368,19 @@ export function createTangleWorkspaceBranching(options) {
|
|
|
351
368
|
}
|
|
352
369
|
}
|
|
353
370
|
if (!childMarker) {
|
|
354
|
-
const compensation = await
|
|
371
|
+
const compensation = await compensateCreatedForkChild(returnedChild, outcome, operation?.signal);
|
|
355
372
|
const removed = compensation === "destroyed" || compensation === "already_absent";
|
|
356
373
|
return forkUnknown(request, removed
|
|
357
|
-
? "Sandbox
|
|
358
|
-
:
|
|
359
|
-
? "Sandbox fork acknowledgement omitted its provider recovery marker; child cleanup was not confirmed"
|
|
360
|
-
: "Sandbox fork acknowledgement omitted its provider recovery marker", !removed);
|
|
374
|
+
? "Sandbox snapshot restore acknowledgement omitted its provider recovery marker; the newly created child was removed"
|
|
375
|
+
: "Sandbox snapshot restore acknowledgement omitted its provider recovery marker; child cleanup was not confirmed", !removed);
|
|
361
376
|
}
|
|
362
377
|
const environment = await environmentFromChild(request, child, provider, child.createdAt, options.confidentialAttestationVerifier, operation?.signal);
|
|
363
378
|
if (!environment) {
|
|
364
|
-
return forkUnknown(request, "Sandbox
|
|
379
|
+
return forkUnknown(request, "Sandbox snapshot restore returned a child without a valid identity", true);
|
|
365
380
|
}
|
|
366
381
|
const record = { request, environment, child };
|
|
367
382
|
forks.set(request.idempotencyKey, record);
|
|
368
|
-
return forkSuccess(request, environment,
|
|
383
|
+
return forkSuccess(request, environment, outcome);
|
|
369
384
|
};
|
|
370
385
|
const lookupFork = async (input, operation) => {
|
|
371
386
|
const request = WorkspaceOperationLookupRequestSchema.parse(input);
|
|
@@ -461,14 +476,13 @@ export function createTangleWorkspaceBranching(options) {
|
|
|
461
476
|
}
|
|
462
477
|
/** Capability support requires every operation used by recovery and cleanup. */
|
|
463
478
|
export function supportsWorkspaceBranching(box, client) {
|
|
464
|
-
return (typeof client.
|
|
479
|
+
return (typeof client.create === "function" &&
|
|
480
|
+
typeof client.list === "function" &&
|
|
465
481
|
typeof client.get === "function" &&
|
|
466
482
|
typeof box.snapshot === "function" &&
|
|
467
483
|
typeof box.listSnapshots === "function" &&
|
|
468
484
|
typeof box.deleteSnapshot === "function" &&
|
|
469
|
-
typeof box.getSnapshotOperation === "function"
|
|
470
|
-
typeof box.fork === "function" &&
|
|
471
|
-
typeof box.getForkOperation === "function");
|
|
485
|
+
typeof box.getSnapshotOperation === "function");
|
|
472
486
|
}
|
|
473
487
|
/** Drop every in-process record for a resource the platform no longer holds. */
|
|
474
488
|
function forgetRecords(records, matches) {
|
|
@@ -689,16 +703,6 @@ function validSnapshotInfo(snapshot, sandboxId) {
|
|
|
689
703
|
safeIdentifier(snapshot.sandboxId) !== undefined &&
|
|
690
704
|
(sandboxId === undefined || snapshot.sandboxId === sandboxId));
|
|
691
705
|
}
|
|
692
|
-
function validForkResult(result) {
|
|
693
|
-
return (!!result &&
|
|
694
|
-
Array.isArray(result.children) &&
|
|
695
|
-
result.children.every((child) => child !== null &&
|
|
696
|
-
typeof child === "object" &&
|
|
697
|
-
safeIdentifier(child.id) !== undefined) &&
|
|
698
|
-
result.requestedCount === 1 &&
|
|
699
|
-
result.materializedCount === result.children.length &&
|
|
700
|
-
typeof result.complete === "boolean");
|
|
701
|
-
}
|
|
702
706
|
function validOperationRecord(value) {
|
|
703
707
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
704
708
|
}
|
|
@@ -713,7 +717,9 @@ function validSnapshotOperationResult(value) {
|
|
|
713
717
|
function validForkOperationChildResult(value) {
|
|
714
718
|
return (validOperationRecord(value) &&
|
|
715
719
|
safeIdentifier(value.sandboxId ?? value.id) !== undefined &&
|
|
716
|
-
|
|
720
|
+
(value.createdAt === undefined ||
|
|
721
|
+
value.createdAt === null ||
|
|
722
|
+
validOperationDate(value.createdAt)));
|
|
717
723
|
}
|
|
718
724
|
function checkpointMarkerTags(request) {
|
|
719
725
|
const marker = {
|
|
@@ -748,7 +754,7 @@ function legacyCheckpointMarkerTags(request) {
|
|
|
748
754
|
...chunks.map((chunk, index) => `${base}:material:${index}:${chunks.length}:${chunk}`),
|
|
749
755
|
];
|
|
750
756
|
}
|
|
751
|
-
function forkMarkerMetadata(request) {
|
|
757
|
+
function forkMarkerMetadata(request, materialization = "snapshot") {
|
|
752
758
|
if (request.metadata && Object.hasOwn(request.metadata, FORK_METADATA_KEY)) {
|
|
753
759
|
throw new Error(`fork metadata reserves ${FORK_METADATA_KEY}`);
|
|
754
760
|
}
|
|
@@ -758,6 +764,7 @@ function forkMarkerMetadata(request) {
|
|
|
758
764
|
idempotencyKey: request.idempotencyKey,
|
|
759
765
|
requestDigest: request.requestDigest,
|
|
760
766
|
request,
|
|
767
|
+
...(materialization === "snapshot" ? { materialization } : {}),
|
|
761
768
|
};
|
|
762
769
|
assertBoundedJson(marker);
|
|
763
770
|
return {
|
|
@@ -785,11 +792,18 @@ async function checkpointOperationSucceeded(box, marker, signal) {
|
|
|
785
792
|
lookup.kind === "checkpoint" &&
|
|
786
793
|
lookup.state === "succeeded");
|
|
787
794
|
}
|
|
788
|
-
/**
|
|
795
|
+
/** Confirm a fork child through its marker or the legacy fork ledger. */
|
|
789
796
|
async function forkOperationLookup(box, marker, signal) {
|
|
797
|
+
if (marker.materialization === "snapshot") {
|
|
798
|
+
return {
|
|
799
|
+
outcome: "found",
|
|
800
|
+
kind: "fork",
|
|
801
|
+
state: "succeeded",
|
|
802
|
+
};
|
|
803
|
+
}
|
|
790
804
|
const lookup = await awaitWithSignal(box.getForkOperation?.(marker.idempotencyKey, {
|
|
791
805
|
count: 1,
|
|
792
|
-
metadata: forkMarkerMetadata(marker.request),
|
|
806
|
+
metadata: forkMarkerMetadata(marker.request, marker.materialization),
|
|
793
807
|
}), signal);
|
|
794
808
|
return lookup;
|
|
795
809
|
}
|
|
@@ -948,6 +962,8 @@ async function findForkByKey(client, box, provider, key, signal) {
|
|
|
948
962
|
* Fork inventory can report a child timestamp from a later registry read. The
|
|
949
963
|
* operation ledger stores the original child acknowledgement, which is the
|
|
950
964
|
* stable value required to replay one exact fork reference after a restart.
|
|
965
|
+
* Some Sandbox responses omit that timestamp, so the validated inventory
|
|
966
|
+
* record supplies it only when the operation result does not.
|
|
951
967
|
*/
|
|
952
968
|
function childFromOperationResult(child, lookup) {
|
|
953
969
|
if (lookup.result === undefined) {
|
|
@@ -963,7 +979,10 @@ function childFromOperationResult(child, lookup) {
|
|
|
963
979
|
(candidate.sandboxId ?? candidate.id) === child.id);
|
|
964
980
|
if (!operationChild)
|
|
965
981
|
return undefined;
|
|
966
|
-
|
|
982
|
+
const createdAt = operationChild.createdAt ?? child.createdAt;
|
|
983
|
+
if (!validOperationDate(createdAt))
|
|
984
|
+
return undefined;
|
|
985
|
+
return { child, createdAt };
|
|
967
986
|
}
|
|
968
987
|
async function findForkChildById(client, box, provider, id, signal) {
|
|
969
988
|
try {
|
|
@@ -1016,9 +1035,9 @@ async function completeForkChild(client, child, signal) {
|
|
|
1016
1035
|
return undefined;
|
|
1017
1036
|
}
|
|
1018
1037
|
}
|
|
1019
|
-
/** Remove only a child this exact call confirmed it created. */
|
|
1020
|
-
async function
|
|
1021
|
-
if (
|
|
1038
|
+
/** Remove only a child this exact create call confirmed it created. */
|
|
1039
|
+
async function compensateCreatedForkChild(child, outcome, signal) {
|
|
1040
|
+
if (outcome !== "created")
|
|
1022
1041
|
return "not_attempted";
|
|
1023
1042
|
if (typeof child.delete !== "function")
|
|
1024
1043
|
return "unconfirmed";
|
|
@@ -1259,6 +1278,10 @@ function forkMarkerFromMetadata(metadata, key) {
|
|
|
1259
1278
|
typeof parsed.idempotencyKey !== "string" ||
|
|
1260
1279
|
typeof parsed.requestDigest !== "string")
|
|
1261
1280
|
return undefined;
|
|
1281
|
+
if (parsed.materialization !== undefined &&
|
|
1282
|
+
parsed.materialization !== "snapshot") {
|
|
1283
|
+
return undefined;
|
|
1284
|
+
}
|
|
1262
1285
|
if (key !== undefined && parsed.idempotencyKey !== key)
|
|
1263
1286
|
return undefined;
|
|
1264
1287
|
const request = WorkspaceForkRequestSchema.safeParse(parsed.request);
|
|
@@ -1272,6 +1295,9 @@ function forkMarkerFromMetadata(metadata, key) {
|
|
|
1272
1295
|
idempotencyKey: parsed.idempotencyKey,
|
|
1273
1296
|
requestDigest: parsed.requestDigest,
|
|
1274
1297
|
request: request.data,
|
|
1298
|
+
...(parsed.materialization === "snapshot"
|
|
1299
|
+
? { materialization: "snapshot" }
|
|
1300
|
+
: {}),
|
|
1275
1301
|
};
|
|
1276
1302
|
}
|
|
1277
1303
|
/**
|