@tangle-network/agent-provider-cli-bridge 2.0.5 → 2.0.7

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
@@ -200,6 +200,7 @@ The endpoint holds only a scheme, a host, and an explicit port; userinfo, path,
200
200
 
201
201
  `@tangle-network/agent-provider-cli-bridge/sandbox` turns a vendor sandbox that runs CLI Bridge into an `AgentEnvironmentProvider`.
202
202
  The vendor adapter implements `BridgeSandboxVendor`: `create`, `connect`, `list`, and per sandbox `exec`, `spawn`, `read`, `write`, `endpoint`, `status` and `destroy`.
203
+ A vendor whose command call runs to completion, as Daytona's and computesdk's do, builds `exec` from `markedExec` and `spawn` from `envFileLaunch`.
203
204
  `createBridgeSandboxProvider({ vendor, router: { key } })` does the rest:
204
205
 
205
206
  - It refuses every create field the vendor cannot honor with an `EnvironmentProviderError` that names the field, before any vendor call, and validates the profile with the materializer the Bridge in the box uses.
@@ -0,0 +1,26 @@
1
+ import { type CheckpointRef, type CheckpointRequest, type CreateAgentEnvironmentInput, type ForkRequest } from "@tangle-network/agent-interface/environment-provider";
2
+ import type { BridgeSandboxVendor, BridgeVendorSnapshots } from "./vendor.js";
3
+ /**
4
+ * Vendor-snapshot checkpointing: the portable seam between the optional
5
+ * `AgentEnvironment.checkpoint`/`fork` surface and a vendor's snapshot
6
+ * lifecycle. A vendor without {@link BridgeVendorSnapshots} refuses both,
7
+ * exactly as it refuses every capability it cannot honor.
8
+ */
9
+ /** The vendor snapshot lifecycle, or a refusal naming the vendor. */
10
+ export declare function vendorSnapshots(vendor: BridgeSandboxVendor): BridgeVendorSnapshots;
11
+ /** A checkpoint name that cannot collide with a caller-chosen vendor snapshot name. */
12
+ export declare function checkpointSnapshotName(name: string | undefined): string;
13
+ /** Snapshot one running sandbox and return the portable checkpoint reference. */
14
+ export declare function createVendorSnapshot(vendor: BridgeSandboxVendor, sandboxId: string, options?: CheckpointRequest & {
15
+ signal?: AbortSignal;
16
+ }): Promise<CheckpointRef>;
17
+ /** A fork must restore this vendor's snapshot; a foreign checkpoint reference is refused. */
18
+ export declare function assertForkableCheckpoint(ref: CheckpointRef, vendorName: string): void;
19
+ /**
20
+ * The create input a fork runs with: the source environment's input, minus
21
+ * everything the snapshot already carries. A repository clone would overwrite
22
+ * the snapshot's workspace, and a caller-chosen environment would boot a
23
+ * different image than the checkpoint captured; both are dropped, while the
24
+ * requested working directory keeps its placement.
25
+ */
26
+ export declare function forkCreateInput(source: CreateAgentEnvironmentInput, request?: ForkRequest): CreateAgentEnvironmentInput;
@@ -0,0 +1,82 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { EnvironmentProviderError, } from "@tangle-network/agent-interface/environment-provider";
3
+ /**
4
+ * Vendor-snapshot checkpointing: the portable seam between the optional
5
+ * `AgentEnvironment.checkpoint`/`fork` surface and a vendor's snapshot
6
+ * lifecycle. A vendor without {@link BridgeVendorSnapshots} refuses both,
7
+ * exactly as it refuses every capability it cannot honor.
8
+ */
9
+ /** The vendor snapshot lifecycle, or a refusal naming the vendor. */
10
+ export function vendorSnapshots(vendor) {
11
+ if (vendor.snapshots === undefined) {
12
+ throw new EnvironmentProviderError("refused", `vendor ${vendor.name} exposes no snapshot lifecycle`, { provider: vendor.name });
13
+ }
14
+ return vendor.snapshots;
15
+ }
16
+ /** Snapshot metadata must be string-valued, exactly like vendor sandbox tags. */
17
+ function snapshotMetadata(metadata, provider) {
18
+ if (metadata === undefined)
19
+ return undefined;
20
+ const out = {};
21
+ for (const [key, value] of Object.entries(metadata)) {
22
+ if (typeof value !== "string") {
23
+ throw new EnvironmentProviderError("refused", `snapshot metadata holds string values; ${key} is not a string`, { path: `metadata.${key}`, provider });
24
+ }
25
+ out[key] = value;
26
+ }
27
+ return out;
28
+ }
29
+ /** A checkpoint name that cannot collide with a caller-chosen vendor snapshot name. */
30
+ export function checkpointSnapshotName(name) {
31
+ return name ?? `tangle-ckpt-${randomUUID()}`;
32
+ }
33
+ /** Snapshot one running sandbox and return the portable checkpoint reference. */
34
+ export async function createVendorSnapshot(vendor, sandboxId, options) {
35
+ const snapshots = vendorSnapshots(vendor);
36
+ const request = options === undefined
37
+ ? {}
38
+ : {
39
+ ...(options.name === undefined ? {} : { name: options.name }),
40
+ ...(options.metadata === undefined ? {} : { metadata: snapshotMetadata(options.metadata, vendor.name) }),
41
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
42
+ };
43
+ const record = await snapshots.create(sandboxId, Object.keys(request).length === 0 ? undefined : request);
44
+ if (typeof record.id !== "string" || record.id.length === 0) {
45
+ throw new EnvironmentProviderError("provision_failed", `vendor ${vendor.name} returned no snapshot id for ${sandboxId}`, { provider: vendor.name });
46
+ }
47
+ return {
48
+ id: record.id,
49
+ provider: vendor.name,
50
+ ...(record.createdAt === undefined ? {} : { metadata: { createdAt: record.createdAt } }),
51
+ };
52
+ }
53
+ /** A fork must restore this vendor's snapshot; a foreign checkpoint reference is refused. */
54
+ export function assertForkableCheckpoint(ref, vendorName) {
55
+ if (typeof ref.id !== "string" || ref.id.length === 0) {
56
+ throw new EnvironmentProviderError("invalid_input", "a checkpoint reference carries a non-empty id", { path: "checkpoint.id", provider: vendorName });
57
+ }
58
+ if (ref.provider !== undefined && ref.provider !== vendorName) {
59
+ throw new EnvironmentProviderError("refused", `checkpoint from provider ${ref.provider} cannot restore on ${vendorName}`, { path: "checkpoint.provider", provider: vendorName });
60
+ }
61
+ }
62
+ /**
63
+ * The create input a fork runs with: the source environment's input, minus
64
+ * everything the snapshot already carries. A repository clone would overwrite
65
+ * the snapshot's workspace, and a caller-chosen environment would boot a
66
+ * different image than the checkpoint captured; both are dropped, while the
67
+ * requested working directory keeps its placement.
68
+ */
69
+ export function forkCreateInput(source, request) {
70
+ const { workspace, name, metadata, signal, ...rest } = source;
71
+ const cwd = workspace?.cwd;
72
+ return {
73
+ ...rest,
74
+ ...(request?.name === undefined
75
+ ? (name === undefined ? {} : { name })
76
+ : { name: request.name }),
77
+ ...(metadata === undefined && request?.metadata === undefined
78
+ ? {}
79
+ : { metadata: { ...metadata, ...request?.metadata } }),
80
+ ...(cwd === undefined ? {} : { workspace: { cwd } }),
81
+ };
82
+ }
@@ -4,6 +4,8 @@ import type { HarnessType } from "@tangle-network/agent-interface";
4
4
  *
5
5
  * Every cloud builds from this text: E2B and Daytona build their template and
6
6
  * image from it, and a local Docker build of it is the same box. Harness
7
- * versions come from the registry that owns the pins.
7
+ * versions come from the registry that owns the pins. `baseImage` overrides
8
+ * the Node base for a box that carries more than the pins; the install layer
9
+ * is unchanged, so every box keeps the same Bridge and harness versions.
8
10
  */
9
- export declare function renderBoxDockerfile(harnesses?: readonly HarnessType[]): string;
11
+ export declare function renderBoxDockerfile(harnesses?: readonly HarnessType[], baseImage?: string): string;
@@ -4,9 +4,11 @@ import { BOX_HARNESSES, BOX_HOME, BOX_NODE_IMAGE, BOX_PINS, BOX_WORKSPACE_DIR, b
4
4
  *
5
5
  * Every cloud builds from this text: E2B and Daytona build their template and
6
6
  * image from it, and a local Docker build of it is the same box. Harness
7
- * versions come from the registry that owns the pins.
7
+ * versions come from the registry that owns the pins. `baseImage` overrides
8
+ * the Node base for a box that carries more than the pins; the install layer
9
+ * is unchanged, so every box keeps the same Bridge and harness versions.
8
10
  */
9
- export function renderBoxDockerfile(harnesses = Object.keys(BOX_HARNESSES)) {
11
+ export function renderBoxDockerfile(harnesses = Object.keys(BOX_HARNESSES), baseImage = BOX_NODE_IMAGE) {
10
12
  const installs = harnesses.map((name) => {
11
13
  const entry = BOX_HARNESSES[name];
12
14
  if (entry === undefined)
@@ -17,7 +19,7 @@ export function renderBoxDockerfile(harnesses = Object.keys(BOX_HARNESSES)) {
17
19
  const packages = installs.map(({ entry, version }) => `${entry.npmPackage}@${version}`);
18
20
  const probes = installs.map(({ entry, version }) => `test "$(${entry.bin} --version | cut -d' ' -f1)" = "${version}"`);
19
21
  return [
20
- `FROM ${BOX_NODE_IMAGE}`,
22
+ `FROM ${baseImage}`,
21
23
  "RUN apt-get update && apt-get install -y --no-install-recommends git curl ca-certificates python3 make g++ ripgrep procps \\",
22
24
  " && rm -rf /var/lib/apt/lists/*",
23
25
  `RUN npm install -g ${[...packages, bridge].join(" ")} \\`,
@@ -1,6 +1,8 @@
1
1
  export { createBridgeSandboxProvider, BRIDGE_RUN_TAG } from "./provider.js";
2
2
  export type { BridgeSandboxProviderOptions } from "./provider.js";
3
- export type { BridgeSandbox, BridgeSandboxCreateRequest, BridgeSandboxSummary, BridgeSandboxVendor, BridgeVendorSupport, } from "./vendor.js";
3
+ export type { BridgeSandbox, BridgeSandboxCreateRequest, BridgeSandboxSummary, BridgeSandboxVendor, BridgeVendorSnapshot, BridgeVendorSnapshots, BridgeVendorSupport, } from "./vendor.js";
4
+ export { checkpointSnapshotName } from "./checkpoint.js";
4
5
  export { validateBoxProfile } from "./refusal.js";
6
+ export { envFileLaunch, markedExec, untilAborted } from "./shell.js";
5
7
  export { BOX_HARNESSES, BOX_HOME, BOX_PINS, BOX_TEMPLATE_NAME, BOX_WORKSPACE_DIR, BRIDGE_PORT, boxHarnessVersion, } from "./box.js";
6
8
  export { renderBoxDockerfile } from "./image.js";
@@ -1,4 +1,6 @@
1
1
  export { createBridgeSandboxProvider, BRIDGE_RUN_TAG } from "./provider.js";
2
+ export { checkpointSnapshotName } from "./checkpoint.js";
2
3
  export { validateBoxProfile } from "./refusal.js";
4
+ export { envFileLaunch, markedExec, untilAborted } from "./shell.js";
3
5
  export { BOX_HARNESSES, BOX_HOME, BOX_PINS, BOX_TEMPLATE_NAME, BOX_WORKSPACE_DIR, BRIDGE_PORT, boxHarnessVersion, } from "./box.js";
4
6
  export { renderBoxDockerfile } from "./image.js";
@@ -3,10 +3,12 @@ import { posix } from "node:path";
3
3
  import { Agent, fetch as undiciFetch } from "undici";
4
4
  import { canonicalAgentProfileDigest, TANGLE_ROUTER_DEFAULT_ROOT_URL, } from "@tangle-network/agent-interface";
5
5
  import { AgentEnvironmentGetOptionsSchema, AgentTurnInputSchema, EnvironmentProviderError, } from "@tangle-network/agent-interface/environment-provider";
6
+ import { shellQuote } from "@tangle-network/agent-core/node";
6
7
  import { harnessModelSupportFor } from "@tangle-network/cli-agent-registry";
7
8
  import { createCliBridgeProvider, defaultCliBridgeCapabilities } from "../index.js";
8
9
  import { BOX_WORKSPACE_DIR, BRIDGE_PORT, BRIDGE_START_COMMAND, bridgeProcessEnv, boxHarness, boxHarnessVersion, } from "./box.js";
9
10
  import { assertProfileValid, declarableHarnesses, plannerLowers, refuseUnsupportedCreateInput, resolveBoxHarness, validateBoxProfile } from "./refusal.js";
11
+ import { assertForkableCheckpoint, createVendorSnapshot, forkCreateInput, } from "./checkpoint.js";
10
12
  export const BRIDGE_RUN_TAG = "tangle-bridge-run";
11
13
  const DEFAULT_LIFETIME_MS = 2 * 60 * 60 * 1000;
12
14
  const DEFAULT_READY_TIMEOUT_MS = 120_000;
@@ -35,9 +37,12 @@ export function createBridgeSandboxProvider(options) {
35
37
  const lifetimeMs = options.lifetimeMs ?? DEFAULT_LIFETIME_MS;
36
38
  const readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
37
39
  const providerCapabilities = (harness) => environmentCapabilities(defaultCliBridgeCapabilities(harness), vendor, harness === undefined ? false : plannerLowers(harness, "hooks"));
38
- async function create(input) {
40
+ async function create(input, restore) {
39
41
  input.signal?.throwIfAborted();
40
42
  refuseUnsupportedCreateInput(input, vendor.support);
43
+ if (restore !== undefined && input.workspace?.environment !== undefined) {
44
+ throw new EnvironmentProviderError("refused", "a fork restores its checkpoint; workspace.environment names a different one", { path: "workspace.environment" });
45
+ }
41
46
  const profile = input.profile;
42
47
  const validation = validateBoxProfile(profile, input.backend, declared);
43
48
  assertProfileValid(validation);
@@ -57,6 +62,7 @@ export function createBridgeSandboxProvider(options) {
57
62
  env: input.env ?? {},
58
63
  lifetimeMs,
59
64
  ...(input.workspace?.environment === undefined ? {} : { environment: input.workspace.environment }),
65
+ ...(restore === undefined ? {} : { snapshot: restore.snapshot }),
60
66
  ...(input.workspace?.image === undefined ? {} : { image: input.workspace.image }),
61
67
  ...(input.resources === undefined ? {} : { resources: input.resources }),
62
68
  ...(input.egress === undefined ? {} : { egress: egressFor(input, routerBaseUrl) }),
@@ -88,7 +94,8 @@ export function createBridgeSandboxProvider(options) {
88
94
  workspace: { cwd: { base: "host", path: workspaceDir } },
89
95
  });
90
96
  input.signal?.throwIfAborted();
91
- return wrapEnvironment({ inner, sandbox, vendor, harness, profile, workspaceDir, dispatcher, capabilities: environmentCapabilities(inner.capabilities ?? providerCapabilities(harness), vendor, plannerLowers(harness, "hooks")), input });
97
+ const forkFromCheckpoint = vendor.snapshots === undefined ? undefined : (checkpoint, request, signal) => create({ ...forkCreateInput(input, request), ...(signal === undefined ? {} : { signal }) }, { snapshot: checkpoint.id });
98
+ return wrapEnvironment({ inner, sandbox, vendor, harness, profile, workspaceDir, dispatcher, capabilities: environmentCapabilities(inner.capabilities ?? providerCapabilities(harness), vendor, plannerLowers(harness, "hooks")), input, ...(forkFromCheckpoint === undefined ? {} : { forkFromCheckpoint }) });
92
99
  }
93
100
  catch (error) {
94
101
  await dispatcher?.close().catch(() => undefined);
@@ -214,12 +221,11 @@ async function cloneRepository(sandbox, input, secrets) {
214
221
  if (repoUrl === undefined)
215
222
  return;
216
223
  const gitRef = input.workspace?.gitRef;
217
- const quote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
218
224
  const command = [
219
- `rm -rf ${quote(BOX_WORKSPACE_DIR)}/.git`,
220
- `git clone ${quote(repoUrl)} ${quote(BOX_WORKSPACE_DIR)}.clone`,
221
- ...(gitRef === undefined ? [] : [`git -C ${quote(BOX_WORKSPACE_DIR)}.clone checkout ${quote(gitRef)}`]),
222
- `cp -a ${quote(BOX_WORKSPACE_DIR)}.clone/. ${quote(BOX_WORKSPACE_DIR)}/ && rm -rf ${quote(BOX_WORKSPACE_DIR)}.clone`,
225
+ `rm -rf ${shellQuote(BOX_WORKSPACE_DIR)}/.git`,
226
+ `git clone ${shellQuote(repoUrl)} ${shellQuote(BOX_WORKSPACE_DIR)}.clone`,
227
+ ...(gitRef === undefined ? [] : [`git -C ${shellQuote(BOX_WORKSPACE_DIR)}.clone checkout ${shellQuote(gitRef)}`]),
228
+ `cp -a ${shellQuote(BOX_WORKSPACE_DIR)}.clone/. ${shellQuote(BOX_WORKSPACE_DIR)}/ && rm -rf ${shellQuote(BOX_WORKSPACE_DIR)}.clone`,
223
229
  ].join(" && ");
224
230
  const result = await sandbox.exec(command, { timeoutMs: 600_000, ...signalOption(input.signal) });
225
231
  if (result.exitCode !== 0) {
@@ -227,7 +233,7 @@ async function cloneRepository(sandbox, input, secrets) {
227
233
  }
228
234
  }
229
235
  async function ensureDirectory(sandbox, directory) {
230
- const result = await sandbox.exec(`mkdir -p '${directory.replace(/'/g, `'\\''`)}'`, { timeoutMs: 30_000 });
236
+ const result = await sandbox.exec(`mkdir -p ${shellQuote(directory)}`, { timeoutMs: 30_000 });
231
237
  if (result.exitCode !== 0) {
232
238
  throw new EnvironmentProviderError("provision_failed", `cannot create workspace directory ${directory}: ${result.stderr.trim().slice(0, 200)}`, { path: "workspace.cwd" });
233
239
  }
@@ -264,10 +270,12 @@ function environmentCapabilities(bridge, vendor, hooks) {
264
270
  ...base,
265
271
  profile: { ...base.profile, validation: true, hooks: hooks === true },
266
272
  workspace: { read: true, write: true, exec: true, git: false, upload: false, download: false, cwdBases: { repository: true, host: false } },
273
+ branching: { checkpoint: vendor.snapshots !== undefined, fork: vendor.snapshots !== undefined },
267
274
  placement: true,
268
275
  create: {
269
276
  durableIdempotency: false,
270
277
  ...(vendor.support.egress.length === 0 ? {} : { egress: [...vendor.support.egress] }),
278
+ ...(vendor.snapshots === undefined ? {} : { workspaceSnapshot: false }),
271
279
  },
272
280
  };
273
281
  }
@@ -348,6 +356,14 @@ function wrapEnvironment(args) {
348
356
  status: operations.status,
349
357
  stream: exclusive,
350
358
  ...(inner.session === undefined ? {} : { session: (id, options) => inner.session(id, options) }),
359
+ ...(vendor.snapshots === undefined ? {} : {
360
+ checkpoint: (options) => createVendorSnapshot(vendor, sandbox.id, options),
361
+ fork: async (checkpoint, options) => {
362
+ assertForkableCheckpoint(checkpoint, vendor.name);
363
+ const { signal, ...request } = options ?? {};
364
+ return args.forkFromCheckpoint(checkpoint, Object.keys(request).length === 0 ? undefined : request, signal);
365
+ },
366
+ }),
351
367
  ...(inner.dispatch === undefined
352
368
  ? {}
353
369
  : {
@@ -0,0 +1,16 @@
1
+ import { type ExecRequest, type ExecResult } from "@tangle-network/agent-interface/environment-provider";
2
+ /** Exec over a run-to-completion command call: a timeout or abort kills every process carrying the exec's marker, so the remote work stops too. */
3
+ export declare function markedExec(command: string, options: ExecRequest | undefined, provider: string, run: (script: string, signal: AbortSignal) => Promise<ExecResult>): Promise<ExecResult>;
4
+ /** The Bridge's credentials go to a file in a directory only the box user can open, sourced and deleted before exec, so no value enters an argv. */
5
+ export declare function envFileLaunch(command: string, { cwd, env }: {
6
+ cwd?: string;
7
+ env: Record<string, string>;
8
+ }): {
9
+ prepare: string;
10
+ file: string;
11
+ content: string;
12
+ launch: string;
13
+ cleanup: string;
14
+ };
15
+ /** For an SDK that takes no AbortSignal: an abort settles the call at once, and `late` receives a result that arrives afterwards. */
16
+ export declare function untilAborted<T>(pending: Promise<T>, signal: AbortSignal | undefined, late?: (value: T) => Promise<unknown>): Promise<T>;
@@ -0,0 +1,50 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { shellQuote } from "@tangle-network/agent-core/node";
3
+ import { EnvironmentProviderError } from "@tangle-network/agent-interface/environment-provider";
4
+ /** Exec over a run-to-completion command call: a timeout or abort kills every process carrying the exec's marker, so the remote work stops too. */
5
+ export async function markedExec(command, options, provider, run) {
6
+ options?.signal?.throwIfAborted();
7
+ const marker = randomBytes(6).toString("hex");
8
+ const env = Object.entries(options?.env ?? {}).map(([key, value]) => `export ${key}=${shellQuote(value)}; `).join("");
9
+ const ended = new AbortController();
10
+ const timer = options?.timeoutMs === undefined ? undefined : setTimeout(() => ended.abort(new EnvironmentProviderError("timeout", `exec exceeded ${options.timeoutMs} ms`, { provider })), options.timeoutMs);
11
+ const signal = options?.signal === undefined ? ended.signal : AbortSignal.any([options.signal, ended.signal]);
12
+ try {
13
+ return await run(`export TBOX_EXEC=${marker}; ${env}${options?.cwd === undefined ? "" : `cd ${shellQuote(options.cwd)} && `}${command}`, signal);
14
+ }
15
+ catch (error) {
16
+ if (signal.aborted)
17
+ await run(`for p in /proc/[0-9]*; do grep -qz 'TBOX_EXEC=${marker}' "$p/environ" 2>/dev/null && kill -9 "\${p#/proc/}" 2>/dev/null; done; true`, AbortSignal.timeout(30_000)).catch(() => undefined);
18
+ throw error;
19
+ }
20
+ finally {
21
+ clearTimeout(timer);
22
+ }
23
+ }
24
+ /** The Bridge's credentials go to a file in a directory only the box user can open, sourced and deleted before exec, so no value enters an argv. */
25
+ export function envFileLaunch(command, { cwd, env }) {
26
+ const dir = `/tmp/tbox-${randomBytes(8).toString("hex")}`;
27
+ return {
28
+ prepare: `umask 077 && mkdir ${dir}`,
29
+ file: `${dir}/env`,
30
+ content: Object.entries(env).map(([key, value]) => `export ${key}=${shellQuote(value)}\n`).join(""),
31
+ launch: `. ${dir}/env && rm -rf ${dir} && ${cwd === undefined ? "" : `cd ${shellQuote(cwd)} && `}exec ${command} >/tmp/bridge.log 2>&1`,
32
+ cleanup: `rm -rf ${dir}`,
33
+ };
34
+ }
35
+ /** For an SDK that takes no AbortSignal: an abort settles the call at once, and `late` receives a result that arrives afterwards. */
36
+ export async function untilAborted(pending, signal, late) {
37
+ if (signal === undefined)
38
+ return pending;
39
+ signal.throwIfAborted();
40
+ let onAbort = () => undefined;
41
+ const aborted = new Promise((_, reject) => signal.addEventListener("abort", (onAbort = () => reject(signal.reason)), { once: true }));
42
+ try {
43
+ return await Promise.race([pending, aborted]);
44
+ }
45
+ finally {
46
+ signal.removeEventListener("abort", onAbort);
47
+ if (signal.aborted && late !== undefined)
48
+ void pending.then(late).catch(() => undefined);
49
+ }
50
+ }
@@ -61,6 +61,8 @@ export interface BridgeSandboxCreateRequest {
61
61
  lifetimeMs: number;
62
62
  /** Vendor template, snapshot or image the caller chose; absent selects the published box image. */
63
63
  environment?: string;
64
+ /** Snapshot id {@link BridgeVendorSnapshots.create} returned; restores that exact state. */
65
+ snapshot?: string;
64
66
  image?: string;
65
67
  resources?: ResourceRequest;
66
68
  egress?: {
@@ -86,10 +88,33 @@ export interface BridgeVendorSupport {
86
88
  /** Egress modes the vendor enforces; absent modes are refused. */
87
89
  egress: readonly AgentEnvironmentEgressMode[];
88
90
  }
91
+ /** A vendor snapshot of one sandbox's state, restorable through `create`. */
92
+ export interface BridgeVendorSnapshot {
93
+ /** Vendor snapshot identifier; restores via `BridgeSandboxCreateRequest.snapshot`. */
94
+ readonly id: string;
95
+ /** Vendor-reported creation time, when the vendor reports one. */
96
+ readonly createdAt?: string;
97
+ }
98
+ /** Snapshot lifecycle a vendor may expose beyond create, exec and destroy. */
99
+ export interface BridgeVendorSnapshots {
100
+ /**
101
+ * Snapshot a running sandbox. The returned id restores via
102
+ * `create({ snapshot: id })`, producing a sandbox that starts from the
103
+ * source's captured state. What a snapshot carries (filesystem only, or
104
+ * memory too) is vendor truth, reported nowhere else.
105
+ */
106
+ create(sandboxId: string, options?: {
107
+ name?: string;
108
+ metadata?: Record<string, string>;
109
+ signal?: AbortSignal;
110
+ }): Promise<BridgeVendorSnapshot>;
111
+ }
89
112
  export interface BridgeSandboxVendor {
90
113
  /** Provider name reported on every environment, for example `e2b`. */
91
114
  readonly name: string;
92
115
  readonly support: BridgeVendorSupport;
116
+ /** Present only when the vendor snapshots running sandboxes. */
117
+ readonly snapshots?: BridgeVendorSnapshots;
93
118
  create(request: BridgeSandboxCreateRequest): Promise<BridgeSandbox>;
94
119
  /** `null` only when the vendor definitively reports no such sandbox; transport failures throw. */
95
120
  connect(id: string, options?: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-provider-cli-bridge",
3
- "version": "2.0.5",
3
+ "version": "2.0.7",
4
4
  "description": "AgentEnvironmentProvider adapter for cli-bridge retained sessions and one-shot turns",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -31,60 +31,16 @@
31
31
  "registry": "https://registry.npmjs.org"
32
32
  },
33
33
  "files": [
34
- "dist/index.d.ts",
35
- "dist/index.js",
36
- "dist/environment-identity.d.ts",
37
- "dist/environment-identity.js",
38
- "dist/capability-discovery.d.ts",
39
- "dist/capability-discovery.js",
40
- "dist/context-transfer.d.ts",
41
- "dist/context-transfer.js",
42
- "dist/interaction-response.d.ts",
43
- "dist/interaction-response.js",
44
- "dist/observation.d.ts",
45
- "dist/observation.js",
46
- "dist/provider-options.d.ts",
47
- "dist/provider-options.js",
48
- "dist/retained-canonical-event.d.ts",
49
- "dist/retained-canonical-event.js",
50
- "dist/retained-control.d.ts",
51
- "dist/retained-control.js",
52
- "dist/retained-environment.d.ts",
53
- "dist/retained-environment.js",
54
- "dist/retained-event-cursor.d.ts",
55
- "dist/retained-event-cursor.js",
56
- "dist/retained-execution.d.ts",
57
- "dist/retained-execution.js",
58
- "dist/retained-native.d.ts",
59
- "dist/retained-native.js",
60
- "dist/retained-run-state.d.ts",
61
- "dist/retained-run-state.js",
62
- "dist/retained-stream.d.ts",
63
- "dist/retained-stream.js",
64
- "dist/transport.d.ts",
65
- "dist/transport.js",
66
- "dist/wire.d.ts",
67
- "dist/wire.js",
68
- "dist/sandbox/index.d.ts",
69
- "dist/sandbox/index.js",
70
- "dist/sandbox/box.d.ts",
71
- "dist/sandbox/box.js",
72
- "dist/sandbox/image.d.ts",
73
- "dist/sandbox/image.js",
74
- "dist/sandbox/provider.d.ts",
75
- "dist/sandbox/provider.js",
76
- "dist/sandbox/refusal.d.ts",
77
- "dist/sandbox/refusal.js",
78
- "dist/sandbox/vendor.d.ts",
79
- "dist/sandbox/vendor.js",
34
+ "dist",
80
35
  "README.md",
81
36
  "LICENSE"
82
37
  ],
83
38
  "dependencies": {
39
+ "@tangle-network/agent-core": "^1.0.2",
84
40
  "@tangle-network/agent-interface": "^5.10.12",
85
- "undici": "8.12.3",
86
41
  "@tangle-network/agent-profile-materialize": "1.1.4",
87
- "@tangle-network/cli-agent-registry": "^0.14.0"
42
+ "@tangle-network/cli-agent-registry": "^0.14.0",
43
+ "undici": "8.12.3"
88
44
  },
89
45
  "devDependencies": {
90
46
  "@tangle-network/agent-provider-testkit": "0.12.4",