@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 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
  }
@@ -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
- validOperationDate(value.createdAt));
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
- /** The fork equivalent of {@link checkpointOperationSucceeded}. */
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
- return { child, createdAt: operationChild.createdAt };
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 compensateUnmarkedForkChild(result, child, signal) {
1021
- 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")
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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-provider-tangle",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "AgentEnvironmentProvider adapter for Tangle sandboxes",
5
5
  "type": "module",
6
6
  "license": "MIT",