@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 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.4 <1.0.0`, and this package is developed and tested against 0.34.4.
5
- The floor is 0.34.4 because exact interactive attachment needs the host receiver, and workspace branching needs keyed snapshot, fork, lookup, and cleanup operations.
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` and `fork`, operation lookup, inventory recovery, and
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, but the
126
- marker only names a candidate: the Sandbox operation ledger still has to report
127
- a settled success before the adapter returns the resource.
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 result;
306
+ let returnedChild;
307
+ let outcome;
307
308
  try {
308
- result = await awaitWithSignal(box.fork?.(1, {
309
- metadata,
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
- }), operation?.signal);
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 fork outcome is unresolved: ${safeError(error)}`, true));
318
- }
319
- if (!result ||
320
- !validForkResult(result) ||
321
- result.idempotency === undefined ||
322
- (result.idempotency.outcome !== "created" &&
323
- result.idempotency.outcome !== "replayed") ||
324
- safeString(result.idempotency.requestDigest) === undefined) {
325
- return forkUnknown(request, "Sandbox fork returned no complete idempotent acknowledgement", true);
326
- }
327
- if (result.children.length !== 1 || result.complete !== true) {
328
- return forkUnknown(request, "Sandbox fork did not materialize exactly one complete child", true);
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 = forkMarkerFromMetadata(returnedChild.metadata) === undefined
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 fork returned a child without a complete identity; the newly created child was removed"
339
- : compensation === "unconfirmed"
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 fork returned a child marked for another source", true);
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 compensateUnmarkedForkChild(result, child, operation?.signal);
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 fork acknowledgement omitted its provider recovery marker; the newly created child was removed"
358
- : compensation === "unconfirmed"
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 fork returned a child without a valid identity", true);
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, result.idempotency.outcome);
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.list === "function" &&
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
- /** The fork equivalent of {@link checkpointOperationSucceeded}. */
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 compensateUnmarkedForkChild(result, child, signal) {
1028
- if (result.idempotency?.outcome !== "created")
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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-provider-tangle",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "AgentEnvironmentProvider adapter for Tangle sandboxes",
5
5
  "type": "module",
6
6
  "license": "MIT",