@tangle-network/agent-provider-tangle 1.0.1 → 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 +71 -52
- 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
|
}
|
|
@@ -750,7 +754,7 @@ function legacyCheckpointMarkerTags(request) {
|
|
|
750
754
|
...chunks.map((chunk, index) => `${base}:material:${index}:${chunks.length}:${chunk}`),
|
|
751
755
|
];
|
|
752
756
|
}
|
|
753
|
-
function forkMarkerMetadata(request) {
|
|
757
|
+
function forkMarkerMetadata(request, materialization = "snapshot") {
|
|
754
758
|
if (request.metadata && Object.hasOwn(request.metadata, FORK_METADATA_KEY)) {
|
|
755
759
|
throw new Error(`fork metadata reserves ${FORK_METADATA_KEY}`);
|
|
756
760
|
}
|
|
@@ -760,6 +764,7 @@ function forkMarkerMetadata(request) {
|
|
|
760
764
|
idempotencyKey: request.idempotencyKey,
|
|
761
765
|
requestDigest: request.requestDigest,
|
|
762
766
|
request,
|
|
767
|
+
...(materialization === "snapshot" ? { materialization } : {}),
|
|
763
768
|
};
|
|
764
769
|
assertBoundedJson(marker);
|
|
765
770
|
return {
|
|
@@ -787,11 +792,18 @@ async function checkpointOperationSucceeded(box, marker, signal) {
|
|
|
787
792
|
lookup.kind === "checkpoint" &&
|
|
788
793
|
lookup.state === "succeeded");
|
|
789
794
|
}
|
|
790
|
-
/**
|
|
795
|
+
/** Confirm a fork child through its marker or the legacy fork ledger. */
|
|
791
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
|
+
}
|
|
792
804
|
const lookup = await awaitWithSignal(box.getForkOperation?.(marker.idempotencyKey, {
|
|
793
805
|
count: 1,
|
|
794
|
-
metadata: forkMarkerMetadata(marker.request),
|
|
806
|
+
metadata: forkMarkerMetadata(marker.request, marker.materialization),
|
|
795
807
|
}), signal);
|
|
796
808
|
return lookup;
|
|
797
809
|
}
|
|
@@ -1023,9 +1035,9 @@ async function completeForkChild(client, child, signal) {
|
|
|
1023
1035
|
return undefined;
|
|
1024
1036
|
}
|
|
1025
1037
|
}
|
|
1026
|
-
/** Remove only a child this exact call confirmed it created. */
|
|
1027
|
-
async function
|
|
1028
|
-
if (
|
|
1038
|
+
/** Remove only a child this exact create call confirmed it created. */
|
|
1039
|
+
async function compensateCreatedForkChild(child, outcome, signal) {
|
|
1040
|
+
if (outcome !== "created")
|
|
1029
1041
|
return "not_attempted";
|
|
1030
1042
|
if (typeof child.delete !== "function")
|
|
1031
1043
|
return "unconfirmed";
|
|
@@ -1266,6 +1278,10 @@ function forkMarkerFromMetadata(metadata, key) {
|
|
|
1266
1278
|
typeof parsed.idempotencyKey !== "string" ||
|
|
1267
1279
|
typeof parsed.requestDigest !== "string")
|
|
1268
1280
|
return undefined;
|
|
1281
|
+
if (parsed.materialization !== undefined &&
|
|
1282
|
+
parsed.materialization !== "snapshot") {
|
|
1283
|
+
return undefined;
|
|
1284
|
+
}
|
|
1269
1285
|
if (key !== undefined && parsed.idempotencyKey !== key)
|
|
1270
1286
|
return undefined;
|
|
1271
1287
|
const request = WorkspaceForkRequestSchema.safeParse(parsed.request);
|
|
@@ -1279,6 +1295,9 @@ function forkMarkerFromMetadata(metadata, key) {
|
|
|
1279
1295
|
idempotencyKey: parsed.idempotencyKey,
|
|
1280
1296
|
requestDigest: parsed.requestDigest,
|
|
1281
1297
|
request: request.data,
|
|
1298
|
+
...(parsed.materialization === "snapshot"
|
|
1299
|
+
? { materialization: "snapshot" }
|
|
1300
|
+
: {}),
|
|
1282
1301
|
};
|
|
1283
1302
|
}
|
|
1284
1303
|
/**
|