Skip to content

Snapshotter Module

The Snapshotter interface lets your agent capture workspace state as a serializable handle, then restore or branch from that handle. Useful for "explore a hypothesis, roll back if it doesn't pan out" patterns and Jupyter-style checkpointing.

Interface

typescript
interface SnapshotRef {
  readonly providerId: string;
  readonly ref: unknown;  // provider-specific opaque payload
}

interface SnapshotOptions {
  readonly signal?: AbortSignal;
  /**
   * Round-5 (A5) — opt in to restoring a snapshot whose origin sandbox
   * is NOT this session's. Default false: cross-session restores are
   * REJECTED to prevent cross-tenant data-mixing when the snapshot
   * store (e.g. R2 binding) is shared across tenants.
   */
  readonly allowCrossSession?: boolean;
}

interface Snapshotter {
  snapshot(opts?: SnapshotOptions): Promise<SnapshotRef>;
  restore(ref: SnapshotRef, opts?: SnapshotOptions): Promise<WorkspaceRef>;
  branch?(ref: SnapshotRef, opts?: SnapshotOptions): Promise<WorkspaceRef>;
}

Abort-aware via AbortSignal

SnapshotOptions.signal flows from the auto-injected workspace tools (which forward the agent's ctx.abortSignal) down to the provider. Providers SHOULD honor signal cancellation: at minimum, pre-check signal.aborted before issuing the underlying SDK call so an already-aborted agent never starts expensive long-running work. Where the underlying SDK supports mid-flight cancellation, providers should thread the signal through. Where it does not, providers should document the gap in JSDoc and rely on the pre-check.

opts is optional throughout for backwards compatibility — existing callers that don't pass it continue to work unchanged.

"Restore returns a NEW WorkspaceRef" — read this carefully

The Snapshotter contract treats snapshots as forks, not mutations. Both restore(ref) and branch(ref):

  1. Do NOT mutate the current workspace.
  2. Create a NEW workspace identity (the provider's choice — typically a new id like {originId}-restored-{shortId}).
  3. Apply the snapshot to that new workspace.
  4. Return a fresh WorkspaceRef pointing at the new workspace.

This means after a restore(ref):

  • The current workspace is unchanged.
  • You have a new ref. To start using the restored state, your code or your LLM must switch to operating against the new ref.

The framework's tool layer surfaces the new ref via the tool result — the LLM sees the ref it can pass to subsequent calls.

registry.swapRef — the auto-injected tools update the persisted ref

Because restore() and branch() return a NEW ref, the framework's auto-injected workspace__<name>__restore and workspace__<name>__branch tools also call registry.swapRef(name, newRef) (round-4 cluster A9). This:

  1. Updates the in-memory registry entry's stored ref to the new value.
  2. Triggers the framework's persistRef callback so the new ref is checkpointed alongside session state.

After a restore/branch tool call, subsequent workspace__<name>__* tool calls (fs/shell/code) resolve against the NEW workspace — not the original. On session resume, the persisted ref is the new one, so the resumed session continues to operate on the restored/branched workspace.

swapRef is registry-internal — custom tools calling Snapshotter.restore() directly do NOT trigger the swap automatically. If your custom tool wants the registry's stored ref updated, call ctx.workspaces!.swapRef(name, newRef) explicitly. Most flows should use the auto-injected tools and let the framework manage the swap.

branch? is optional

Some providers (CodeSandbox SDK, etc.) distinguish "branch" (intentional fork) from "restore" (recover prior state) at the API layer. The Snapshotter interface codifies this distinction at the interface level. For providers where the implementation is identical (like CloudflareSandboxSnapshotter), branch is implemented as a semantic alias of restore — same behavior, different id suffix.

The auto-injected workspace__<name>__branch tool is only present when the provider populates branch?.

Auto-injected tools

For a workspace named <name> with snapshot: true:

ToolSchemaReturnsWhen
workspace__<name>__snapshot{}{ ref: SnapshotRef }Always (when snapshot declared)
workspace__<name>__restore{ ref: SnapshotRef; allowCrossSession?: boolean }{ ref: WorkspaceRef } (new)Always
workspace__<name>__branch{ ref: SnapshotRef; allowCrossSession?: boolean }{ ref: WorkspaceRef } (new)Only when provider implements branch?

Cross-session ownership check (round-5 A5)

When the same snapshot store (e.g. an R2 binding for cloudflare-sandbox) is shared across tenants/sessions, a snapshot ref's originSandboxId is the only signal distinguishing this session's snapshots from another's. Pre-fix, an LLM in tenant A could pass tenant B's backup.id to its restore tool and silently restore B's data into A's workspace — cross-tenant data leakage with no audit trail.

Post-fix, restore() and branch() track the origin of every snapshot taken within the current session and REJECT any ref whose origin is not in the set. Operators with a legitimate cross-session use case (e.g. a templating workflow that seeds tenant B's workspace from a known-good template snapshot) opt in via the allowCrossSession: true field on SnapshotOptions (or the matching tool input field). Cross-session opt-ins are audit-logged at warn level so security reviewers see when the boundary is crossed.

The default-deny posture closes the cross-tenant data-mixing class. Sessions that don't share a snapshot store (the common case — each session gets its own R2 bucket prefix) are unaffected; the snapshotter records its own snapshots and trivially passes the check.

Implementation responsibility. Snapshotter providers MUST track snapshots created within the current session and reject unowned refs unless allowCrossSession: true is set. The CloudflareSandboxSnapshotter (round-5 reference impl) maintains an in-memory Set<originSandboxId> per session — DO restart loses the set, so post-restart recovery requires the explicit opt-in (intentional: post-restart we cannot distinguish recovery-of-own-snapshot from cross-session restore).

Capability config

typescript
// boolean — no policy options in v1
snapshot: boolean;

Snapshot has no fine-grained config in v1. Either you declare it or you don't.

Provider-specific snapshot configuration (R2 binding for Cloudflare Sandbox, etc.) lives on the PROVIDER config, not the capability config. See per-provider pages.

Provider-specific behavior

CloudflareSandboxSnapshotter

  • snapshot() calls sandbox.createBackup({ dir: snapshotDir }). Backup is uploaded to R2 (provider's backupR2Binding must be configured).
  • restore(ref) and branch(ref) generate a new sandbox ID and call getSandbox(namespace, newId) to obtain a stub for the new sandbox, then apply the backup via restoreBackup(payload.backup).
  • The mount is FUSE overlay — ephemeral. After the new sandbox sleeps + wakes, you must re-restore from the same SnapshotRef to recover.
  • Without backupR2Binding configured, snapshot() throws at call time with a clear error.
  • Snapshot ref payload schema (round-4 cluster D). The payload carries { backup, originSandboxId }. The OUTER wrapper is .strict() (extra fields fail loudly — they indicate a real bug or migration). The INNER backup field uses .strip() because it mirrors the @cloudflare/sandbox SDK's own backup-handle shape, which is owned by the SDK and may grow new fields as it evolves. originSandboxId is constrained to the same SANDBOX_ID_REGEX as BaseRefPayload.id to prevent tampering at the Zod boundary.

Provider support matrix

Providersnapshot supported
In-Memory
Local Bash
Cloudflare Filestore❌ (could be added — R2-backed sqlite dump — but not in v1)
Cloudflare Sandbox✅ (R2-backed; restore returns NEW sandbox; branch implemented)

Snapshot pruning / retention (round-5 D11)

The Snapshotter interface has no delete(ref) method in v1. Snapshots accumulate in the provider's backing store (R2 for CloudflareSandboxSnapshotter) without framework-side cleanup. A long-running session that snapshots once per checkpoint will grow R2 namespace size linearly with checkpoint count.

Filed as known follow-up: Snapshotter.delete(ref) for explicit pruning. Until it lands, operators must manage retention out-of-band.

Recommended retention pattern for CloudflareSandboxSnapshotter:

  1. Tag snapshots with a deterministic R2 prefix per session (e.g. snapshots/<sessionId>/<timestamp>.tar).
  2. Run a periodic worker job (Cron Trigger) that lists the R2 bucket and deletes snapshots older than your retention window via the R2 API:
    typescript
    const list = await env.BACKUPS.list({ prefix: 'snapshots/' });
    const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000; // 7 days
    for (const obj of list.objects) {
      if (obj.uploaded.getTime() < cutoff) {
        await env.BACKUPS.delete(obj.key);
      }
    }
  3. For sessions that take many snapshots in quick succession (e.g. one per LLM step), keep only the last N snapshots per session-prefix. Pair with a session-end cleanup hook to delete the entire prefix when the session terminates.

The framework cannot do this automatically because retention policy is workload-specific (legal hold, debugging windows, cost budgets all vary). Treat snapshot storage as an operator responsibility until Snapshotter.delete() lands.

Source

Released under the MIT License.