Skip to content

feat(e2b): support mounting pre-existing volumes at sandbox creation - #3581

Merged
jujn merged 3 commits into
agentscope-ai:mainfrom
chcodex:feat/e2b-volume-mounts
Oct 22, 2026
Merged

jujn merged 3 commits into
agentscope-ai:mainfrom
chcodex:feat/e2b-volume-mounts

Conversation

@chcodex

@chcodex chcodex commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Closes #3155.

E2B offers persistent volumes that exist independently of sandbox lifecycles. This PR wires attach-at-create through the E2B backend so users no longer need TAR persist/hydrate on every turn for cross-sandbox persistence.

What is included

  • New E2bVolumeMount POJO (existing volume name + absolute in-sandbox path, validation + normalization)
  • E2bSandboxClientOptions.volumeMounts (fluent adders, distinct-path check) and E2bFilesystemSpec.volumeMount/volumeMounts
  • POST /sandboxes serializes volumeMounts: [{name, path}] (omitted when empty); platform 400s propagate with the response body
  • Mounts threaded through E2bSandboxClient.create into E2bSandboxState and across all three creation paths (new, connect-fallback recreate, native-snapshot restore)
  • workspaceOnVolume detection (root.equals(mount) || root.startsWith(mount + "/")): TAR persist/hydrate become no-ops, destroy skips rm -rf; nested mounts get TAR --exclude args
  • Hardening found via live testing: fail fast on non-absolute workspaceRoot, shell-quote TAR excludes, run workspace mkdir from /
  • E2bSandboxState JSON stays backward compatible; empty E2bCloudBucketMountStrategy placeholder removed
  • Docs: E2B volumes section in filesystem.md (EN/ZH)

Tests

  • Unit: request-body assertions (MockWebServer), validation/normalization, merge, serde round-trip incl. legacy compat, recovery with mounts
  • Live (E2bVolumeMountLiveTest, opt-in via E2B_API_KEY, skipped in CI): mount visibility, unknown-volume 400, cross-sandbox persistence, workspaceOnVolume end-to-end, native-snapshot restore, nested-mount independence from snapshots — all passing against the real API
  • Verified live: platform snapshots do not capture volume bytes, so nested mounts are unaffected by snapshot restore

@chcodex

chcodex commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

@jujn @Buktal Could you help review this PR?

Suggested focus areas:

  1. cwd change in E2bSandbox#doSetupWorkspace (mkdir now runs from / instead of the workspace root itself) — found via live testing: when the workspace is a not-yet-existing subdirectory on a freshly mounted volume, envd rejects process start because the cwd does not exist. This is a guaranteed failure for the volume scenario; the default /home/user path is unaffected (the mkdir target is absolute).
  2. NATIVE_SNAPSHOT vs. volume interaction (comment in doPersistWorkspace) — verified live: platform snapshots do not capture volume bytes, so nested mounts are unaffected by snapshot restore; when the workspace sits on a volume, snapshots still run (software environment) while TAR persistence is a no-op.
  3. Empty volumeMounts treated as unset in merge() (same convention as AgentRun OSS mounts) — the Spec-level javadoc documents this; please confirm this is acceptable.
  4. Live test maintainability (E2bVolumeMountLiveTest, skipped in CI without E2B_API_KEY): every case kills its sandboxes and deletes snapshot templates; verified all green against the real API with no leftover resources.

@jujn

jujn commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator

我先看完这个[捂脸]:#3562

@Buktal Buktal left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E2bSandboxState gains volumeMounts/workspaceOnVolume but neither the class nor the client's mapper sets @JsonIgnoreProperties(ignoreUnknown = true) (E2bSandboxState.java, E2bSandboxClient.java:48-51 — Jackson's FAIL_ON_UNKNOWN_PROPERTIES is on by default). Once a state written with mounts is loaded by an older build, deserializeState throws and SandboxManager falls through to a fresh create (SandboxManager.java:122-129), losing session continuity for those scopes. The sandbox snapshot classes already carry this annotation (LocalSandboxSnapshot.java:42) — please add it to E2bSandboxState or disable the failure on the mapper.

WorkspaceMountSupport.tarExcludeArgsForAbsolutePaths misses workspaceRoot = "/": root + "/" becomes "//" so mount.startsWith(root + "/") never matches and no nested mount is excluded (WorkspaceMountSupport.java:105) — tar -C / . then archives the entire volume. Please guard the root-"/" case (cf. the special-case in E2bSandbox.workspaceTransferPath) and collapse duplicate slashes, which currently also skip exclusion while E2bVolumeMount.normalizeMountPath normalizes them.

Removing the published mounts/E2bCloudBucketMountStrategy interface is a binary/source break for downstream consumers even though nothing in this repo references it. Please either keep it @Deprecated for a transition release, or flag the removal as a breaking change for maintainer sign-off — the contribution rules ask to preserve public interfaces.

E2bSandboxState.setVolumeMounts skips the distinct-path validation that E2bSandboxClientOptions.setVolumeMounts enforces, so deserialized or directly-set state can send duplicate mount paths to the platform. Please mirror the validation (or centralize it in E2bVolumeMount).

The volume-critical branches — persist no-op, empty-stream hydrate return, destroy skip (E2bSandbox.java) — are only exercised by the E2B_API_KEY-gated live test and show as uncovered in codecov (48% on E2bSandbox). They are unit-testable with MockWebServer plus the reflection pattern already used in E2bVolumeMountTest.recreateCarriesMounts; please add unit tests so CI can catch regressions.

Minor:

  1. The "Returns whether the workspace root lives on a volume" javadoc is attached to requireAbsoluteWorkspaceRoot (E2bSandboxClient.java:174-179) but describes isWorkspaceOnVolume.
  2. resume() skips the requireAbsoluteWorkspaceRoot check enforced on create(), so a persisted relative root still slips through.
  3. E2bVolumeMountLiveTest.volumeName() falls back to a hardcoded probe-ws-test1 — other accounts get an opaque 400; please require E2B_VOLUME_NAME.
  4. The docs section should note volumes are read-write and multi-mountable with no locking guarantees (E2B beta limitation), which matters for the workspace-on-volume example it encourages.
  5. Stray double blank lines at the end of the new section in docs/v2/en/docs/harness/filesystem.md and the zh counterpart.

Sequencing note: #3562 reworks the same ensureSandbox/restoreSandboxFromSnapshotTemplate paths without mounts; whichever merges second must thread e2bState.getVolumeMounts() through the new helpers, or NATIVE_SNAPSHOT + volume sessions silently lose their mounts on restore.

@chcodex

chcodex commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

Addressed all review feedback in 6b0b53e (all verified: 115 unit + 6 live green, spotless clean, no leftover resources):

  1. Forward compat: E2bSandboxState now carries @JsonIgnoreProperties(ignoreUnknown = true), so states written with mounts load on older builds instead of falling back to fresh create. Locked by serdeIgnoresUnknownFieldsForForwardCompat.
  2. Root / exclusion: tarExcludeArgsForAbsolutePaths now special-cases workspaceRoot = "/" and collapses duplicate slashes, matching E2bVolumeMount normalization. Covered by tarExcludeArgsForAbsolutePaths_rootSlashAndNormalization.
  3. Placeholder interface: E2bCloudBucketMountStrategy restored as @Deprecated for one transition release instead of outright removal.
  4. State validation: distinct-mount-path check centralized in E2bVolumeMount.requireDistinctMountPaths and enforced in E2bSandboxState.setVolumeMounts too. Covered by stateRejectsDuplicateMountPaths.
  5. Unit coverage for volume branches: persist no-op / empty hydrate / destroy skip are now tested without envd in workspaceOnVolumeBranchesAreNoOpsWithoutEnvd (any backend call would fail without a sandbox, proving the short-circuit).
  6. Minors: javadoc moved to the right method + absolute-root check in resume(); live tests now require E2B_VOLUME_NAME (clear error instead of opaque 400); docs note read-write/multi-mount/no-locking semantics; stray blank lines removed.

Note: #3562 is still open — if it merges first, I will rebase and thread mounts through the reworked helpers.

@chcodex
chcodex force-pushed the feat/e2b-volume-mounts branch 4 times, most recently from 2f10589 to 3cca0f5 Compare October 16, 2026 02:05
@chcodex

chcodex commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

CI is green again and the branch is rebased onto latest main. Two updates since the review:

  • Fixed a real failure my earlier push introduced: WorkspaceMountSupport.normalizeAbsolutePath collapsed repeated slashes after stripping the trailing slash, so //mnt//data// kept a trailing slash and produced --exclude=./mnt/data/. Order swapped; WorkspaceMountSupportTest (6) and the full harness suite (1247) pass locally.
  • Rebased onto dc83a06e0 (conflict-free).

All checks pass now — build green on both ubuntu and windows. @Buktal ready for re-review when you get a chance.

@chcodex
chcodex force-pushed the feat/e2b-volume-mounts branch from 3cca0f5 to 0ff259e Compare October 19, 2026 13:47

@Buktal Buktal left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildTarCommand applies the new volume-mount excludes only on the create branch (E2bSandbox.java:587-591), while the pre-existing bind-mount excludes are appended for both create and extract (E2bSandbox.java:582-586). An archive that predates the current mount configuration — e.g. a NATIVE_SNAPSHOT template created before a volume was mounted under the workspace root — therefore extracts its old ./data/... entries straight through the mount point into the persistent volume on hydrate. Please apply volumeTarExcludeArgs() on the -xf branch as well, matching the bind-mount handling in the same method.

This PR adds a second near-copy of path normalization: WorkspaceMountSupport.normalizeAbsolutePath/stripTrailingSlash (WorkspaceMountSupport.java:120-132) duplicates E2bVolumeMount.normalizeMountPath (E2bVolumeMount.java:94-108 — identical collapse/strip logic, different error contract), next to the third variant in E2bSandbox.workspaceTransferPath (E2bSandbox.java:619-623). The duplication already bit this PR once — the collapse/strip ordering bug fixed after review lived in exactly one of the copies. Since the e2b module already calls into WorkspaceMountSupport (E2bSandbox.java:551), please consolidate on one shared core instead of maintaining parallel copies that can drift again.

Minor:

  1. Quoting the bind-mount excludes (E2bSandbox.java:584-585) changes the generated command for existing bind-mount users, but only the volume branch is locked by a test — please mirror tarCommandQuotesExcludeArgs for the bind-mount excludes.
  2. doDestroyWorkspace guards only the workspaceOnVolume case (E2bSandbox.java:243-246); with a nested mount, rm -rf on the root would traverse the mount point into the volume. Nothing calls it in production today (real teardown is killSandbox), so it is latent — but if the "on shutdown" contract of AbstractBaseSandbox (AbstractBaseSandbox.java:45) gets wired, this needs to exclude nested mounts the way the TAR path does.
  3. Consider @JsonIgnoreProperties(ignoreUnknown = true) on E2bVolumeMount too: the annotation on E2bSandboxState does not cover nested types, so a future field inside a mount entry would fail deserialization of the whole state — the same failure mode just fixed at the state level.
  4. The 0-byte archive doubles as an implicit no-op marker: a workspace that flips between volume and non-volume persistence across runs can have a real archive replaced by a 0-byte marker (or hydrate silently skip one). If intended, a one-line comment stating the marker semantics would help.

- Add E2bVolumeMount POJO (existing volume name + absolute in-sandbox
  path, with validation and normalization)
- Add volumeMounts to E2bSandboxClientOptions (fluent adders, distinct
  path check) and E2bFilesystemSpec.volumeMount/volumeMounts
- Serialize volumeMounts [{name, path}] into POST /sandboxes (omitted
  when empty); platform 400s propagate with the response body
- Thread mounts through E2bSandboxClient.create into E2bSandboxState
  and across all creation paths (new, connect-fallback recreate,
  native-snapshot restore)
- Track workspaceOnVolume (root.equals(mount) ||
  root.startsWith(mount + "/")); TAR persist/hydrate become no-ops and
  destroy skips rm -rf when true; nested mounts are excluded from TAR
  archives via WorkspaceMountSupport.tarExcludeArgsForAbsolutePaths
- Fail fast on non-absolute workspaceRoot in create(); shell-quote TAR
  --exclude args; run workspace mkdir from / so volume subdirectories
  can be created on first start
- Keep E2bSandboxState JSON backward compatible; remove the
  E2bCloudBucketMountStrategy placeholder
- Cover with unit tests (request body, validation, merge, serde,
  recovery) and document volumes in filesystem.md (EN/ZH)

Refs: agentscope-ai#3155
Opt-in suite (runs only with E2B_API_KEY set; skipped in CI):
mount visibility, unknown-volume 400, cross-sandbox persistence,
workspaceOnVolume end-to-end, native-snapshot restore, and nested
mount independence from snapshots. Volume name via E2B_VOLUME_NAME.

Verified live: platform snapshots do not capture volume bytes.

Refs: agentscope-ai#3155
- Keep E2bSandboxState forward compatible with
  @JsonIgnoreProperties(ignoreUnknown = true) so states written with
  mounts load on older builds instead of falling back to fresh create
- Fix tarExcludeArgsForAbsolutePaths for workspaceRoot "/" and
  collapse duplicate slashes, matching E2bVolumeMount normalization
- Keep E2bCloudBucketMountStrategy as @deprecated for one transition
  release instead of removing the public interface outright
- Centralize distinct-mount-path validation in E2bVolumeMount and
  enforce it in E2bSandboxState.setVolumeMounts as well
- Add unit tests for the volume-critical branches (persist no-op,
  empty hydrate, destroy skip), "/" excludes, state validation and
  unknown-field serde so CI catches regressions
- Fix misplaced javadoc on requireAbsoluteWorkspaceRoot and enforce
  the absolute-root check in resume() too
- Require E2B_VOLUME_NAME for live tests instead of a hardcoded
  default volume
- Document read-write/multi-mount/no-locking volume semantics

Refs: agentscope-ai#3155
@chcodex
chcodex force-pushed the feat/e2b-volume-mounts branch from 0ff259e to 5f6b677 Compare October 21, 2026 10:11
@mintlify

mintlify Bot commented Oct 21, 2026

Copy link
Copy Markdown

Preview approval required

This pull request comes from a fork, so Mintlify won't build a preview until someone with write access approves commit 5f6b677.

Approve preview

@chcodex

chcodex commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Addressed this second round in 5f6b677bc, rebased onto current main (9ea054034):

Mandatory 1 — volume excludes missing on -xf: buildTarCommand now applies volumeTarExcludeArgs() on both branches. The extract branch guards against archive entries that predate the current mount configuration (e.g. a NATIVE_SNAPSHOT archive written before the volume was mounted under the workspace root) writing through the mount point into the shared volume. The existing quoting test now also asserts the -xf form.

Mandatory 2 — three normalization copies: consolidated on one shared core, WorkspaceMountSupport.normalizeVirtualPath (e2b already depends on harness, so the core lives at the layer both can see). E2bVolumeMount.normalizeMountPath keeps its error contract and delegates the core; E2bSandbox.workspaceTransferPath dropped its hand-rolled while-loop. Added a direct unit test for the core. If a future behavior difference is wanted per-site, it now has to be explicit instead of silently drifting.

Minor 1: added tarCommandQuotesBindMountExcludeArgs mirroring the volume test (mount key with spaces), asserting quoting on both -cf and -xf.

Minor 2: make doDestroyWorkspace a no-op by contract rather than mount-excluding the rm. Reasoning: an E2B workspace's lifetime is bounded by the sandbox itself — the disk vanishes with the sandbox, so a pre-teardown rm can never have an observable effect, while any execution path that deletes could stretch through a nested mount into shared persistent data. This keeps every destruction path zero-risk regardless of whether the "on shutdown" contract gets wired later; a real reset-during-lifetime requirement, if it ever appears, can reintroduce a scoped delete then. Happy to hear counterarguments.

Minor 3: @JsonIgnoreProperties(ignoreUnknown = true) added to E2bVolumeMount.

Minor 4: the empty-archive marker semantics are now documented in both doPersistWorkspace and doHydrateWorkspace.

Verification: full harness suite (1257), e2b mock suites (145), and all 6 live tests (including snapshot restore under the new -xf exclude behavior) pass locally; spotless clean. @Buktal ready for re-review when you get a chance.

@Buktal Buktal left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified 5f6b677 end to end: volume excludes now apply on both the -cf and -xf branches with the quoting test extended to the extract form; normalization is consolidated into WorkspaceMountSupport.normalizeVirtualPath with E2bVolumeMount.normalizeMountPath and workspaceTransferPath delegating to it, plus a direct unit test; @JsonIgnoreProperties on E2bVolumeMount and the empty-archive marker docs are in place; the new tarCommandQuotesBindMountExcludeArgs locks quoting on both branches. Ran the harness and e2b suites locally on this head (153 e2b tests, 0 failures, 9 live-gated skips; harness green) on top of the green dual-OS CI. The doDestroyWorkspace no-op is the right call for this backend — the disk is bounded by the sandbox lifetime, so a pre-teardown delete has no observable effect and could only reach through a nested mount into shared volume data; the override documents exactly that.

@jujn
jujn merged commit 9730e81 into agentscope-ai:main Oct 22, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: E2B sandbox volumes support (mount existing volumes)

3 participants