@tangle-network/agent-provider-tangle 0.13.3 → 0.14.0

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,9 @@
1
1
  # @tangle-network/agent-provider-tangle
2
2
 
3
3
  Wraps `@tangle-network/sandbox` as an `AgentEnvironmentProvider`.
4
- The peer range is `>=0.30.1 <1.0.0`, and this package is developed and tested against 0.30.1.
5
- The floor is 0.30.1 because interaction claims use the Sandbox backend catalog exposed by `listBackends()`.
4
+ The peer range is `>=0.33.1 <1.0.0`, and this package is developed and tested against 0.33.1.
5
+ The floor is 0.33.1 because workspace branching uses the keyed snapshot, fork,
6
+ lookup, and cleanup operations added to that SDK.
6
7
  The provider fails closed when the configured backend or its catalog entry cannot be read.
7
8
  Newer SDKs may also provide `getBackend()` as a lookup over the same catalog.
8
9
 
@@ -104,8 +105,95 @@ After `session.prompt()` admits another turn, that session object's `controlRef`
104
105
  Sandbox keeps execution identifiers optional for older or unproven service paths, so this adapter fails closed when a dispatch or prompt does not return one and never falls back to latest-session state.
105
106
  Sessions reconstructed without a control reference may start a new prompt, but result lookup, cancellation, and cursor replay fail before calling Sandbox because those operations could otherwise select the newest unrelated execution.
106
107
  It also rejects `contextTransfer` and `nativeContinuation` inputs explicitly until those operations have native Sandbox support instead of silently dropping them.
107
- The adapter never advertises `branching.checkpoint` or `branching.fork`.
108
- Sandbox exposes `snapshot`, `listSnapshots`, `deleteSnapshot`, and `branch(count)` with different semantics; durable workspace branching stays unadvertised until the full `AgentWorkspaceBranching` contract — retry, lookup, conflict, and cleanup together — is implemented over that surface.
108
+
109
+ ### Workspace branching
110
+
111
+ `environment.workspaceBranching` is the single operation surface for creating
112
+ and recovering a checkpoint, forking one managed child, and cleaning both
113
+ resources in dependency order.
114
+ The adapter advertises `branching.checkpoint`, `branching.fork`, `retrySafe`,
115
+ `lookup`, and `cleanup` only when the linked SDK exposes the complete managed
116
+ surface: keyed `snapshot` and `fork`, operation lookup, inventory recovery, and
117
+ explicit deletion outcomes.
118
+ An incomplete SDK surface clears every branching flag and omits
119
+ `environment.workspaceBranching`.
120
+
121
+ Every operation validates the canonical agent-interface request digest before
122
+ calling Sandbox.
123
+ Retries with the same key replay the original resource, while changed material
124
+ returns a conflict containing the original interface digest.
125
+ The provider stores a bounded request marker in snapshot tags and child
126
+ metadata so a fresh process can recover the exact interface digest, but the
127
+ marker only names a candidate: the Sandbox operation ledger still has to report
128
+ a settled success before the adapter returns the resource.
129
+ Checkpoint deletion reports `in_use` with every verified child that still
130
+ references it; delete the child first, then retry checkpoint deletion.
131
+ The adapter never treats an SDK response without an explicit idempotency or
132
+ deletion outcome as success.
133
+
134
+ After a provider process restart, `provider.workspaceBranching.forEnvironment()`
135
+ returns a fresh source-scoped handle for lookup and cleanup, or `null` when it
136
+ cannot prove the complete operation surface.
137
+
138
+ Recovery reads the account inventory through Sandbox offset pages of at most
139
+ 1,000 sandboxes and continues until a short page proves the inventory is
140
+ complete.
141
+ The adapter also reads checkpoints from the previous marker format while writing only the current bounded format.
142
+ Malformed, repeated, failed, or over-bound pages return `unknown` and do not
143
+ mutate a resource.
144
+ Sandbox exposes snapshots only through the live source instance.
145
+ If that source was deleted, this provider cannot recover its checkpoints or
146
+ forked children through the public SDK.
147
+ Clean branch resources before deleting the source or use a platform reaper.
148
+
149
+ ```ts
150
+ import {
151
+ workspaceCheckpointRequestDigest,
152
+ workspaceForkRequestDigest,
153
+ } from '@tangle-network/agent-interface'
154
+
155
+ const checkpoint = await environment.workspaceBranching?.checkpoint({
156
+ source: exactRun,
157
+ idempotencyKey: 'checkpoint-before-analysis',
158
+ requestDigest: workspaceCheckpointRequestDigest({ source: exactRun }),
159
+ })
160
+
161
+ if (checkpoint?.status === 'created' || checkpoint?.status === 'replayed') {
162
+ const fork = await environment.workspaceBranching?.fork({
163
+ checkpoint: checkpoint.checkpoint,
164
+ placement: { kind: 'sandbox', sandboxId: 'analysis-worker' },
165
+ idempotencyKey: 'analysis-worker',
166
+ requestDigest: workspaceForkRequestDigest({
167
+ checkpoint: checkpoint.checkpoint,
168
+ placement: { kind: 'sandbox', sandboxId: 'analysis-worker' },
169
+ }),
170
+ })
171
+ }
172
+ ```
173
+
174
+ ### Confidential forks
175
+
176
+ Sandbox returns raw TEE evidence, not a verified claim.
177
+ Pass `confidentialAttestationVerifier` to `createTangleProvider` to connect a
178
+ trusted provider-key and measurement verifier.
179
+ The callback receives the raw report, the expected environment binding, and
180
+ the canonical request-bound attestation material.
181
+ It returns a provider key id, signature, and optional normalized measurement
182
+ only after verification succeeds.
183
+ Returning `null`, throwing, a mismatched measurement, or a copied quote leaves
184
+ the result unverified while preserving `confidentialRequested: true`.
185
+ The same unverified result occurs when no trusted verifier is configured.
186
+ The adapter does not trust the requested nonce, child metadata, or any legacy
187
+ `confidential: true` assertion as proof.
188
+ `verifyTangleQuote` below represents a verifier supplied by the caller.
189
+
190
+ ```ts
191
+ const provider = createTangleProvider({
192
+ client: new Sandbox({ apiKey: process.env.TANGLE_API_KEY }),
193
+ confidentialAttestationVerifier: async ({ report, attestation }) =>
194
+ (await verifyTangleQuote({ report, attestation })) ?? null,
195
+ })
196
+ ```
109
197
 
110
198
  ## Environment observation
111
199
 
@@ -1,8 +1,7 @@
1
- import { assertBoundedJson, attachCleanupHandle, awaitWithSignal, boundedIdentifier, boundedString, exactProcessRequestDigest, isBoundedJson, MAX_LIST_RESULTS, } from "./tangle-contract-safety.js";
1
+ import { assertBoundedJson, attachCleanupHandle, awaitWithSignal, boundedIdentifier, boundedString, exactProcessRequestDigest, isBoundedJson, MAX_LIST_RESULTS, SANDBOX_LIST_PAGE_SIZE, } from "./tangle-contract-safety.js";
2
2
  import { sandboxInstanceAsExactProcessEnvironment } from "./tangle-exact-process-environment.js";
3
3
  import { assertExactProcessSandbox, assertSupportedProviderOptions, assertUnreservedMetadata, EXACT_PROCESS_METADATA_KEY, isExactProcessRequestConflict, isExactProcessSandbox, metadataMatches, assertSignalOptions, } from "./tangle-exact-process-validation.js";
4
4
  const IMMUTABLE_TANGLE_IMAGE = /^(?:sha256:[a-f0-9]{64}|\S+@sha256:[a-f0-9]{64})$/i;
5
- const LIST_PAGE_SIZE = 1_000;
6
5
  export function createTangleExactProcessProvider(input) {
7
6
  const { client, options, providerName } = input;
8
7
  boundedIdentifier(providerName, "Tangle exact process provider");
@@ -98,7 +97,7 @@ export function createTangleExactProcessProvider(input) {
98
97
  assertSupportedProviderOptions(query?.providerOptions);
99
98
  assertExactProcessListQuery(query);
100
99
  const matches = [];
101
- for (let offset = 0;; offset += LIST_PAGE_SIZE) {
100
+ for (let offset = 0;; offset += SANDBOX_LIST_PAGE_SIZE) {
102
101
  if (offset > MAX_LIST_RESULTS) {
103
102
  throw new Error("Tangle exact process list exceeded its page bound");
104
103
  }
@@ -107,11 +106,11 @@ export function createTangleExactProcessProvider(input) {
107
106
  ...(options.teamId
108
107
  ? { scope: `team:${options.teamId}` }
109
108
  : { scope: "personal" }),
110
- limit: LIST_PAGE_SIZE,
109
+ limit: SANDBOX_LIST_PAGE_SIZE,
111
110
  offset,
112
111
  ...(signal ? { signal } : {}),
113
112
  }), signal);
114
- if (!Array.isArray(page) || page.length > LIST_PAGE_SIZE) {
113
+ if (!Array.isArray(page) || page.length > SANDBOX_LIST_PAGE_SIZE) {
115
114
  throw new Error("Tangle exact process list returned an invalid page size");
116
115
  }
117
116
  if (offset + page.length > MAX_LIST_RESULTS) {
@@ -132,7 +131,7 @@ export function createTangleExactProcessProvider(input) {
132
131
  }
133
132
  }
134
133
  }
135
- if (page.length < LIST_PAGE_SIZE)
134
+ if (page.length < SANDBOX_LIST_PAGE_SIZE)
136
135
  return matches;
137
136
  }
138
137
  },
package/dist/index.d.ts CHANGED
@@ -2,4 +2,6 @@ export type { TangleExactProcessOptions } from "./tangle-types.js";
2
2
  export * from "./tangle-types.js";
3
3
  export { createTangleProvider } from "./tangle-provider.js";
4
4
  export { defaultTangleSandboxCapabilities } from "./tangle-capabilities.js";
5
+ export { createTangleWorkspaceBranching, supportsWorkspaceBranching, } from "./tangle-workspace-branching.js";
6
+ export type { TangleWorkspaceBranchingOptions } from "./tangle-workspace-branching.js";
5
7
  export { safeEndpointFromConnection } from "./tangle-observation.js";
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./tangle-types.js";
2
2
  export { createTangleProvider } from "./tangle-provider.js";
3
3
  export { defaultTangleSandboxCapabilities } from "./tangle-capabilities.js";
4
+ export { createTangleWorkspaceBranching, supportsWorkspaceBranching, } from "./tangle-workspace-branching.js";
4
5
  export { safeEndpointFromConnection } from "./tangle-observation.js";
@@ -4,6 +4,7 @@ import type { SandboxClientLike, SandboxInstanceLike } from "./tangle-types.js";
4
4
  import type { DeploymentCapabilitySupport } from "./tangle-deployment-capabilities.js";
5
5
  import type { ResourceProfile } from "@tangle-network/agent-interface";
6
6
  import { type ObservationSurfaceSupport } from "./tangle-observation.js";
7
+ import type { TangleConfidentialAttestationVerifier } from "./tangle-types.js";
7
8
  /**
8
9
  * The full capability document this adapter supports when the Sandbox client
9
10
  * implements every optional method.
@@ -47,6 +48,10 @@ export interface SandboxCapabilitySupport {
47
48
  interactiveTerminal: boolean;
48
49
  /** The SDK can drive the existing native TUI and read its terminal metadata. */
49
50
  interactiveAgent: boolean;
51
+ /** Snapshot/fork methods and inventory recovery are all present. */
52
+ workspaceBranching: boolean;
53
+ /** The SDK can request raw TEE evidence for this sandbox. */
54
+ confidentialAttestation: boolean;
50
55
  }
51
56
  export declare function sandboxCapabilitySupport(box: SandboxInstanceLike, client: SandboxClientLike, requestedResources?: ResourceProfile): SandboxCapabilitySupport;
52
57
  /**
@@ -99,7 +104,9 @@ export declare function tangleInteractiveAgentSupported(declared: AgentEnvironme
99
104
  * idempotency needs the deployment to honor the exact reference that
100
105
  * identifies a repeated turn.
101
106
  */
102
- export declare function narrowedTangleCapabilities(declared: AgentEnvironmentCapabilities, support: SandboxCapabilitySupport, deployment: DeploymentCapabilitySupport): AgentEnvironmentCapabilities;
107
+ export declare function narrowedTangleCapabilities(declared: AgentEnvironmentCapabilities, support: SandboxCapabilitySupport, deployment: DeploymentCapabilitySupport, options?: {
108
+ confidentialAttestationVerifier?: TangleConfidentialAttestationVerifier;
109
+ }): AgentEnvironmentCapabilities;
103
110
  /**
104
111
  * Narrow provider-level claims to facts the client can prove before any
105
112
  * sandbox exists.
@@ -112,7 +119,9 @@ export declare function narrowedTangleCapabilities(declared: AgentEnvironmentCap
112
119
  * the answer as `AgentEnvironment.capabilities`, which is the document a
113
120
  * caller reads to decide which operation to offer against that environment.
114
121
  */
115
- export declare function capabilitiesForClient(declared: AgentEnvironmentCapabilities, client: SandboxClientLike): AgentEnvironmentCapabilities;
122
+ export declare function capabilitiesForClient(declared: AgentEnvironmentCapabilities, client: SandboxClientLike, options?: {
123
+ confidentialAttestationVerifier?: TangleConfidentialAttestationVerifier;
124
+ }): AgentEnvironmentCapabilities;
116
125
  /**
117
126
  * Freeze a capability document before an environment publishes it.
118
127
  *
@@ -125,4 +134,6 @@ export declare function frozenCapabilityDocument<T>(document: T): T;
125
134
  * Narrow a declared capability document to what this Sandbox instance backs
126
135
  * and what the deployment behind it reports.
127
136
  */
128
- export declare function capabilitiesForSandbox(declared: AgentEnvironmentCapabilities, support: SandboxCapabilitySupport, deployment: DeploymentCapabilitySupport): AgentEnvironmentCapabilities;
137
+ export declare function capabilitiesForSandbox(declared: AgentEnvironmentCapabilities, support: SandboxCapabilitySupport, deployment: DeploymentCapabilitySupport, options?: {
138
+ confidentialAttestationVerifier?: TangleConfidentialAttestationVerifier;
139
+ }): AgentEnvironmentCapabilities;
@@ -4,6 +4,7 @@ import { ADAPTER_CEILING_DEPLOYMENT, deploymentBacksInteractiveAgent, deployment
4
4
  import { clientObservationSurfaceSupport, observationSurfaceSupport, } from "./tangle-observation.js";
5
5
  import { sandboxBacksInteractiveTerminal } from "./tangle-terminal.js";
6
6
  import { sandboxBacksInteractiveAgent } from "./tangle-interactive.js";
7
+ import { supportsWorkspaceBranching } from "./tangle-workspace-branching.js";
7
8
  /**
8
9
  * The full capability document this adapter supports when the Sandbox client
9
10
  * implements every optional method.
@@ -72,14 +73,20 @@ export function defaultTangleSandboxCapabilities(harness) {
72
73
  cancellationIdempotency: true,
73
74
  },
74
75
  workspace: { read: true, write: true, exec: true, git: false, upload: true, download: true },
75
- // Sandbox exposes snapshot/branch, not the checkpoint/fork contract, and
76
- // durable branching needs retry, lookup, conflict, and cleanup together.
77
- branching: { checkpoint: false, fork: false },
76
+ // The complete Sandbox SDK surface backs this contract. Narrowing below
77
+ // removes every flag when any recovery or cleanup method is absent.
78
+ branching: {
79
+ checkpoint: true,
80
+ fork: true,
81
+ retrySafe: true,
82
+ lookup: true,
83
+ cleanup: true,
84
+ },
78
85
  placement: true,
79
86
  usage: false,
80
- // Confidential execution needs verified attestation evidence, which this
81
- // adapter does not yet obtain, so it is never declared by default.
82
- confidential: false,
87
+ // This is intent only. Narrowing requires both raw TEE evidence and the
88
+ // caller's external provider-key verifier before the flag survives.
89
+ confidential: true,
83
90
  // Observation surfaces are declared as intent and narrowed per sandbox to
84
91
  // the sources that can put a value on each one.
85
92
  observation: {
@@ -171,6 +178,8 @@ export function sandboxCapabilitySupport(box, client, requestedResources) {
171
178
  observation: observationSurfaceSupport(box, client, requestedResources),
172
179
  interactiveTerminal: sandboxBacksInteractiveTerminal(box),
173
180
  interactiveAgent: sandboxBacksInteractiveAgent(box),
181
+ workspaceBranching: supportsWorkspaceBranching(box, client),
182
+ confidentialAttestation: typeof box.getTeeAttestation === "function",
174
183
  };
175
184
  }
176
185
  /**
@@ -231,6 +240,8 @@ export function clientCapabilitySupport(client) {
231
240
  observation,
232
241
  interactiveTerminal: true,
233
242
  interactiveAgent: false,
243
+ workspaceBranching: false,
244
+ confidentialAttestation: false,
234
245
  };
235
246
  }
236
247
  /**
@@ -292,7 +303,14 @@ export function tangleInteractiveAgentSupported(declared, support, deployment) {
292
303
  * idempotency needs the deployment to honor the exact reference that
293
304
  * identifies a repeated turn.
294
305
  */
295
- export function narrowedTangleCapabilities(declared, support, deployment) {
306
+ export function narrowedTangleCapabilities(declared, support, deployment, options) {
307
+ // JavaScript callers can omit required fields before schema validation.
308
+ // Missing branching intent must disable the surface, not throw while it is
309
+ // being narrowed.
310
+ const declaredBranching = declared.branching ?? {
311
+ checkpoint: false,
312
+ fork: false,
313
+ };
296
314
  const supportsRetainedControl = tangleRetainedControlSupported(declared, support, deployment);
297
315
  const supportsDetach = support.dispatchPrompt && support.session && deployment.exactDispatch;
298
316
  // A cleared fact forces false; a held fact passes the declared value
@@ -325,14 +343,18 @@ export function narrowedTangleCapabilities(declared, support, deployment) {
325
343
  upload: support.write ? declared.workspace.upload : false,
326
344
  download: support.read ? declared.workspace.download : false,
327
345
  },
328
- branching: {
329
- ...declared.branching,
330
- checkpoint: false,
331
- fork: false,
332
- ...(declared.branching.retrySafe !== undefined ? { retrySafe: false } : {}),
333
- ...(declared.branching.lookup !== undefined ? { lookup: false } : {}),
334
- ...(declared.branching.cleanup !== undefined ? { cleanup: false } : {}),
335
- },
346
+ // An incomplete Sandbox surface clears every branching flag together. A
347
+ // partial claim would let a caller start an operation it cannot recover.
348
+ branching: support.workspaceBranching
349
+ ? { ...declaredBranching }
350
+ : {
351
+ ...declaredBranching,
352
+ checkpoint: false,
353
+ fork: false,
354
+ ...(declaredBranching.retrySafe !== undefined ? { retrySafe: false } : {}),
355
+ ...(declaredBranching.lookup !== undefined ? { lookup: false } : {}),
356
+ ...(declaredBranching.cleanup !== undefined ? { cleanup: false } : {}),
357
+ },
336
358
  placement: support.placement ? declared.placement : false,
337
359
  usage: false,
338
360
  ...(declared.observation === undefined
@@ -358,6 +380,11 @@ export function narrowedTangleCapabilities(declared, support, deployment) {
358
380
  delete narrowed.nativeContinuation;
359
381
  if (!supportsRetainedControl)
360
382
  delete narrowed.retainedControl;
383
+ if (narrowed.confidential &&
384
+ (!support.confidentialAttestation ||
385
+ typeof options?.confidentialAttestationVerifier !== "function")) {
386
+ narrowed.confidential = false;
387
+ }
361
388
  return narrowed;
362
389
  }
363
390
  /**
@@ -417,8 +444,8 @@ function narrowedInteractiveAgent(declared, supported) {
417
444
  * the answer as `AgentEnvironment.capabilities`, which is the document a
418
445
  * caller reads to decide which operation to offer against that environment.
419
446
  */
420
- export function capabilitiesForClient(declared, client) {
421
- return narrowedTangleCapabilities(declared, clientCapabilitySupport(client), ADAPTER_CEILING_DEPLOYMENT);
447
+ export function capabilitiesForClient(declared, client, options) {
448
+ return narrowedTangleCapabilities(declared, clientCapabilitySupport(client), ADAPTER_CEILING_DEPLOYMENT, options);
422
449
  }
423
450
  /**
424
451
  * Freeze a capability document before an environment publishes it.
@@ -434,6 +461,6 @@ export function frozenCapabilityDocument(document) {
434
461
  * Narrow a declared capability document to what this Sandbox instance backs
435
462
  * and what the deployment behind it reports.
436
463
  */
437
- export function capabilitiesForSandbox(declared, support, deployment) {
438
- return narrowedTangleCapabilities(declared, support, deployment);
464
+ export function capabilitiesForSandbox(declared, support, deployment, options) {
465
+ return narrowedTangleCapabilities(declared, support, deployment, options);
439
466
  }
@@ -2,6 +2,8 @@ import type { CreateAgentExactProcessEnvironmentInput } from "@tangle-network/ag
2
2
  import type { TangleExactProcessOptions } from "./tangle-types.js";
3
3
  export declare const MAX_EXACT_FILE_BYTES: number;
4
4
  export declare const MAX_LIST_RESULTS = 100000;
5
+ /** Sandbox caps list responses at 1,000 resources. */
6
+ export declare const SANDBOX_LIST_PAGE_SIZE = 1000;
5
7
  export declare const MAX_IDENTIFIER_LENGTH = 512;
6
8
  export declare const MAX_STRING_LENGTH = 16384;
7
9
  export declare const MAX_ARRAY_LENGTH = 1024;
@@ -1,6 +1,8 @@
1
1
  import { canonicalCandidateDigest } from "@tangle-network/agent-interface";
2
2
  export const MAX_EXACT_FILE_BYTES = 64 * 1024 * 1024;
3
3
  export const MAX_LIST_RESULTS = 100_000;
4
+ /** Sandbox caps list responses at 1,000 resources. */
5
+ export const SANDBOX_LIST_PAGE_SIZE = 1_000;
4
6
  export const MAX_IDENTIFIER_LENGTH = 512;
5
7
  export const MAX_STRING_LENGTH = 16_384;
6
8
  export const MAX_ARRAY_LENGTH = 1_024;
@@ -1,5 +1,6 @@
1
1
  import type { AgentEnvironment, AgentEnvironmentCapabilities, ResourceProfile } from "@tangle-network/agent-interface/environment-provider";
2
2
  import type { SandboxClientLike, SandboxInstanceLike } from "./tangle-types.js";
3
+ import type { TangleConfidentialAttestationVerifier } from "./tangle-types.js";
3
4
  /**
4
5
  * Compose one concrete sandbox into an environment.
5
6
  *
@@ -25,4 +26,5 @@ export declare function sandboxInstanceAsEnvironment(box: SandboxInstanceLike, p
25
26
  signal?: AbortSignal;
26
27
  }, request?: {
27
28
  resources?: ResourceProfile;
29
+ confidentialAttestationVerifier?: TangleConfidentialAttestationVerifier;
28
30
  }): Promise<AgentEnvironment>;
@@ -17,6 +17,7 @@ import { createExecutionUsageLog } from "./tangle-usage-log.js";
17
17
  import { observeTangleEnvironment } from "./tangle-observation.js";
18
18
  import { createTangleTerminalRegistry } from "./tangle-terminal.js";
19
19
  import { createTangleInteractiveAgentRegistry } from "./tangle-interactive.js";
20
+ import { confidentialVerifierOption, createTangleWorkspaceBranching, } from "./tangle-workspace-branching.js";
20
21
  /**
21
22
  * Compose one concrete sandbox into an environment.
22
23
  *
@@ -49,7 +50,18 @@ export async function sandboxInstanceAsEnvironment(box, providerName, client, de
49
50
  }
50
51
  const support = sandboxCapabilitySupport(box, client, request?.resources);
51
52
  const deployment = await readDeploymentCapabilitySupport(box, operation);
52
- const capabilities = frozenCapabilityDocument(AgentEnvironmentCapabilitiesSchema.parse(capabilitiesForSandbox(declaredCapabilities, support, deployment)));
53
+ const capabilities = frozenCapabilityDocument(AgentEnvironmentCapabilitiesSchema.parse(capabilitiesForSandbox(declaredCapabilities, support, deployment, confidentialVerifierOption(request?.confidentialAttestationVerifier))));
54
+ const workspaceBranching = capabilities.branching.checkpoint &&
55
+ capabilities.branching.fork &&
56
+ capabilities.branching.lookup === true &&
57
+ capabilities.branching.cleanup === true
58
+ ? createTangleWorkspaceBranching({
59
+ box,
60
+ client,
61
+ provider: providerName,
62
+ ...confidentialVerifierOption(request?.confidentialAttestationVerifier),
63
+ })
64
+ : undefined;
53
65
  // The published document is the single source for what this environment
54
66
  // offers, so the session surface reads its grant from there.
55
67
  const retainedControl = capabilities.retainedControl !== undefined;
@@ -84,9 +96,12 @@ export async function sandboxInstanceAsEnvironment(box, providerName, client, de
84
96
  ...(box.name ? { name: boundedString(box.name, "Tangle environment name") } : {}),
85
97
  ...(box.metadata ? { metadata: snapshotMetadata(box.metadata) } : {}),
86
98
  capabilities,
99
+ ...(workspaceBranching === undefined
100
+ ? {}
101
+ : { workspaceBranching }),
87
102
  async status(options) {
88
103
  assertOptionKeys(options, ["signal"], "Tangle environment status");
89
- await awaitWithSignal(box.refresh?.(options), options?.signal);
104
+ await awaitWithSignal(box.refresh?.(options?.signal), options?.signal);
90
105
  return statusFromUnknown(box.status);
91
106
  },
92
107
  async *stream(input) {
@@ -284,7 +299,7 @@ export async function sandboxInstanceAsEnvironment(box, providerName, client, de
284
299
  async refresh(options) {
285
300
  assertOptionKeys(options, ["signal"], "Tangle refresh");
286
301
  options?.signal?.throwIfAborted();
287
- await awaitWithSignal(box.refresh?.(options), options?.signal);
302
+ await awaitWithSignal(box.refresh?.(options?.signal), options?.signal);
288
303
  options?.signal?.throwIfAborted();
289
304
  },
290
305
  ...(support.destroy
@@ -167,7 +167,7 @@ async function refreshBeforeObservation(box, options) {
167
167
  return "the Sandbox client cannot refresh this environment";
168
168
  }
169
169
  try {
170
- await awaitWithSignal(box.refresh(options), options?.signal);
170
+ await awaitWithSignal(box.refresh(options?.signal), options?.signal);
171
171
  return undefined;
172
172
  }
173
173
  catch (error) {
@@ -2,10 +2,11 @@ import { AgentEnvironmentCapabilitiesSchema, createAgentEnvironmentWithIdempoten
2
2
  import { createTangleExactProcessProvider, } from "./exact-process.js";
3
3
  import { capabilitiesForClient, defaultTangleSandboxCapabilities, narrowTangleCapabilitiesToBackend, } from "./tangle-capabilities.js";
4
4
  import { sandboxInstanceAsEnvironment } from "./tangle-environment.js";
5
+ import { confidentialVerifierOption, createTangleWorkspaceBranching, } from "./tangle-workspace-branching.js";
5
6
  import { assertCreateInputShape, assertMappedCreateOptions, assertMappedSecretNames, assertNoInlineSecretValues, sandboxOptionsFromCreateInput } from "./tangle-create-options.js";
6
7
  import { statusFromUnknown } from "./tangle-environment-values.js";
7
8
  import { requestedResourceProfile } from "./tangle-resources.js";
8
- import { assertBoundedJson, attachCleanupHandle, awaitWithSignal, boundedIdentifier, boundedString, MAX_LIST_RESULTS, } from "./tangle-contract-safety.js";
9
+ import { assertBoundedJson, attachCleanupHandle, awaitWithSignal, boundedIdentifier, boundedString, MAX_LIST_RESULTS, SANDBOX_LIST_PAGE_SIZE, } from "./tangle-contract-safety.js";
9
10
  export function createTangleProvider(options) {
10
11
  const providerName = options.name ?? "tangle-sandbox";
11
12
  boundedIdentifier(providerName, "Tangle provider name");
@@ -38,7 +39,7 @@ export function createTangleProvider(options) {
38
39
  };
39
40
  // Provider-boundary document: client-stage facts only. It also validates
40
41
  // the configured document, so create() and get() call it before any effect.
41
- const narrowedProviderCapabilities = (declared) => AgentEnvironmentCapabilitiesSchema.parse(capabilitiesForClient(declared, options.client));
42
+ const narrowedProviderCapabilities = (declared) => AgentEnvironmentCapabilitiesSchema.parse(capabilitiesForClient(declared, options.client, confidentialVerifierOption(options.confidentialAttestationVerifier)));
42
43
  const resolveCapabilities = async () => narrowedProviderCapabilities(await resolveDeclaredCapabilities());
43
44
  const createRecords = new Map();
44
45
  const createEnvironment = async (input) => {
@@ -89,7 +90,12 @@ export function createTangleProvider(options) {
89
90
  try {
90
91
  input.signal?.throwIfAborted();
91
92
  const requestedResources = requestedResourceProfile(input.resources);
92
- const environment = await sandboxInstanceAsEnvironment(box, providerName, options.client, declaredCapabilities, input.signal ? { signal: input.signal } : undefined, requestedResources === undefined ? undefined : { resources: requestedResources });
93
+ const environment = await sandboxInstanceAsEnvironment(box, providerName, options.client, declaredCapabilities, input.signal ? { signal: input.signal } : undefined, {
94
+ ...(requestedResources === undefined
95
+ ? {}
96
+ : { resources: requestedResources }),
97
+ ...confidentialVerifierOption(options.confidentialAttestationVerifier),
98
+ });
93
99
  input.signal?.throwIfAborted();
94
100
  return environment;
95
101
  }
@@ -109,9 +115,30 @@ export function createTangleProvider(options) {
109
115
  throw error;
110
116
  }
111
117
  };
118
+ const workspaceBranching = options.client.get
119
+ ? {
120
+ async forEnvironment(sourceEnvironmentId, operation) {
121
+ assertProviderOperationOptions(operation, "Tangle workspaceBranching.forEnvironment");
122
+ const environmentId = boundedIdentifier(sourceEnvironmentId, "Tangle workspace branching source environment id");
123
+ operation?.signal?.throwIfAborted();
124
+ const box = await awaitWithSignal(options.client.get?.(environmentId, operation), operation?.signal);
125
+ operation?.signal?.throwIfAborted();
126
+ if (!box || box.id !== environmentId)
127
+ return null;
128
+ boundedIdentifier(box.id, "Tangle workspace branching environment id");
129
+ return (createTangleWorkspaceBranching({
130
+ box,
131
+ client: options.client,
132
+ provider: providerName,
133
+ ...confidentialVerifierOption(options.confidentialAttestationVerifier),
134
+ }) ?? null);
135
+ },
136
+ }
137
+ : undefined;
112
138
  return {
113
139
  name: providerName,
114
140
  ...(exactProcess ? { exactProcess } : {}),
141
+ ...(workspaceBranching === undefined ? {} : { workspaceBranching }),
115
142
  capabilities: resolveCapabilities,
116
143
  ...(options.validateProfile ? { validateProfile: options.validateProfile } : {}),
117
144
  create(input) {
@@ -129,7 +156,7 @@ export function createTangleProvider(options) {
129
156
  operation?.signal?.throwIfAborted();
130
157
  if (!box || boundedIdentifier(box.id, "Tangle environment id") !== id)
131
158
  return null;
132
- return await sandboxInstanceAsEnvironment(box, providerName, options.client, declaredCapabilities, operation?.signal ? { signal: operation.signal } : undefined);
159
+ return await sandboxInstanceAsEnvironment(box, providerName, options.client, declaredCapabilities, operation?.signal ? { signal: operation.signal } : undefined, confidentialVerifierOption(options.confidentialAttestationVerifier));
133
160
  },
134
161
  }
135
162
  : {}),
@@ -156,9 +183,26 @@ export function createTangleProvider(options) {
156
183
  }
157
184
  assertBoundedJson(query.metadata);
158
185
  }
159
- const boxes = await awaitWithSignal(options.client.list?.(operation?.signal ? { signal: operation.signal } : undefined), operation?.signal);
160
- if (!Array.isArray(boxes) || boxes.length > MAX_LIST_RESULTS) {
161
- throw new Error("Tangle environment list exceeded its result bound");
186
+ const boxes = [];
187
+ for (let offset = 0;; offset += SANDBOX_LIST_PAGE_SIZE) {
188
+ if (offset > MAX_LIST_RESULTS) {
189
+ throw new Error("Tangle environment list exceeded its page bound");
190
+ }
191
+ operation?.signal?.throwIfAborted();
192
+ const page = await awaitWithSignal(options.client.list?.({
193
+ limit: SANDBOX_LIST_PAGE_SIZE,
194
+ offset,
195
+ ...(operation?.signal ? { signal: operation.signal } : {}),
196
+ }), operation?.signal);
197
+ if (!Array.isArray(page) || page.length > SANDBOX_LIST_PAGE_SIZE) {
198
+ throw new Error("Tangle environment list returned an invalid page size");
199
+ }
200
+ if (boxes.length + page.length > MAX_LIST_RESULTS) {
201
+ throw new Error("Tangle environment list exceeded its result bound");
202
+ }
203
+ boxes.push(...page);
204
+ if (page.length < SANDBOX_LIST_PAGE_SIZE)
205
+ break;
162
206
  }
163
207
  const summaries = (boxes ?? []).filter((box) => {
164
208
  boundedIdentifier(box.id, "Tangle environment id");