Skip to content

feat(e2b): route native file transfer through SandboxFileTransfer [3/3] - #3344

Open
chcodex wants to merge 1 commit into
agentscope-ai:mainfrom
chcodex:split/e2b-c-file-transfer
Open

chcodex wants to merge 1 commit into
agentscope-ai:mainfrom
chcodex:split/e2b-c-file-transfer

Conversation

@chcodex

@chcodex chcodex commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Stacked PR 3/3

Split from #2196. Merge order: #3341 → #3343 → #3344.

Base is main, so this diff currently also carries #3341 and #3343. Once both merge, this diff shrinks to 2 files / +304 — E2bSandbox implements SandboxFileTransfer plus its tests.

Review only the top commit (feat(e2b): route native file transfer through SandboxFileTransfer); that is the only change this PR owns.

Note: this replaces the original #2196 design, which introduced E2bSandboxFilesystem / E2bFilesystemSpec and a createFilesystem() factory on SandboxFilesystemSpec. Implementing the existing optional SandboxFileTransfer interface instead keeps the change inside the e2b module and adds no new abstraction — SandboxBackedFilesystem already prefers it over base64-over-exec.


Fixes #3340

Split from #2196 (stacked on #3339's scope: only the top commit is new).

What

  • E2bSandbox implements SandboxFileTransfer (supportsFileTransfer accepts absolute paths only; relative paths fall back to the exec strategy).
  • Workspace persist/hydrate use the Filesystem REST API for tar transfer (single upload + tar xf / single tar + download), replacing shell+base64.
  • Temp tar files in the sandbox are removed best-effort after transfer (success and failure paths).
  • No custom filesystem subclass; default SandboxBackedFilesystem is used.

Tests

  • E2bSandboxFileTransferTest: 10/10 pass (native dispatch, failure reporting, exec fallback, persist/hydrate tar paths incl. cleanup-on-failure).
  • E2bEnvdProcessClientTest: 35/35 pass, no regression.
  • spotless:check: pass.

@chcodex
chcodex force-pushed the split/e2b-c-file-transfer branch from 790743f to beccdc8 Compare September 28, 2026 15:16
@chcodex chcodex changed the title feat(e2b): route native file transfer through SandboxFileTransfer feat(e2b): route native file transfer through SandboxFileTransfer [3/3] Sep 28, 2026
@codecov

codecov Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.00000% with 7 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
.../agentscope/extensions/sandbox/e2b/E2bSandbox.java 75.00% 6 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@oss-maintainer oss-maintainer 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.

Summary

Step 3 of the envd split: E2bSandbox now implements SandboxFileTransfer and routes both workspace archive directions through the REST helpers added in #3343 — persist writes the tar to a sandbox temp file and downloads it, hydrate uploads it and extracts — replacing the base64-chunk + python3 bootstrap. That removes an undeclared in-image dependency and the per-argument command-line ceiling, so the refactor is welcome; unit coverage for the new routing is solid (E2bSandboxFileTransferTest, 238 lines).

What gives me pause is the path gate. supportsFileTransfer accepts every absolute path, which is materially wider than the confinement the Docker and Kubernetes backends apply to the same interface, and the harness relies on the backend to decline paths it should not handle natively. I would resolve that before merge. The archive now also crosses two buffers (sandbox /tmp file plus a host byte[]) with no size ceiling anywhere in the chain, which is worth a decision even if #3344 is not where you want to fix it.

State at review time: CLA signed, Check License, Check Module Sync, build (ubuntu-latest) and codecov pass, build (windows-latest) still running. This branch also carries #3341's stream-parsing commit, whose four review findings (flag-order validation, truncated-stream acceptance, unbounded capture, and the envdAccessToken logged in the shared integration test) are still open — none are re-listed here.

Findings

  • [Warning] E2bSandbox.java:166 — native transfer is unconfined: any absolute path is admitted, unlike DockerSandbox.requireTransferPath / KubernetesSandbox.toFileApiRelative.
  • [Warning] E2bSandbox.java:155 — persist now needs the archive to fit in sandbox /tmp and in host heap, with no bound in either place; partial tar output is not cleaned up.
  • [Warning] E2bSandbox.java:342 — best-effort temp-file cleanup swallows failures at debug level, leaving a full workspace copy in the sandbox.
  • [Info] E2bSandbox.java:207 — extract now applies the bind-mount exclude args where the old path extracted everything: restore semantics changed, no test pins it.
  • [Info] E2bSandbox.java:41 — runShellBinaryStdout loses its last caller in the module; worth deleting or keeping with a stated reason.

Not covered

No live E2B sandbox was used; behaviour was read from the diff and the mocked-transport tests.


Automated review by github-manager-bot


@Override
public boolean supportsFileTransfer(String absolutePath) {
return absolutePath != null && absolutePath.startsWith("/");

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.

[Warning] supportsFileTransfer admits every absolute path, which makes E2B the only backend that puts no boundary on the native transfer channel. The two existing implementations both confine it:

  • DockerSandbox.requireTransferPath normalizes the workspace root and throws "Path is outside the sandbox workspace" (and rejects . / .. / empty segments) — supportsFileTransfer is just that check wrapped in a boolean;
  • KubernetesSandbox.toFileApiRelative admits only paths under getFileApiBaseDir() and returns null for everything else, so the caller declines and the confined exec path handles it.

This matters because the harness delegates that decision to the backend: SandboxBackedFilesystem.resolveTransferPath validates and normalizes, then returns an absolute path unchanged (if (normalized.startsWith("/")) return normalized;) before probing supportsFileTransfer; downloadFiles probes the raw path with no resolution at all. So with this implementation, an absolute path anywhere envd's run user can reach (/home/user/.ssh/*, /etc/profile.d/*, another agent's scratch) is written and read natively rather than declined, and a declined path still has the archive fallback — so nothing is lost by confining.

Suggest resolving against e2bState.getWorkspaceRoot() and rejecting traversal segments the way Docker does, returning false instead of throwing. The temp-tar flow in this class is not affected: it calls envd().uploadFile/downloadFile directly, so it keeps working whatever the public probe decides.

Related history in this repo: #3292 (namespace boundary for absolute paths in ROOTED mode), #3314 (skill staging traversal) — a per-backend divergence here tends to get read as the intended contract.

.append(" -C ")
.append(shellSingleQuote(root))
.append(" .");
envd().runShell(e2bState, root, script.toString(), TAR_TIMEOUT_SECONDS);

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.

[Warning] Persist now materializes the archive twice: tar writes it into the sandbox filesystem, then downloadFile(tarPath) (#3343) reads it back as one byte[] before wrapping it in a ByteArrayInputStream. Two consequences worth documenting or bounding:

  1. Sandbox-side. /tmp is tmpfs in many E2B templates, so the temp tar is charged against sandbox RAM and peaks at roughly workspace + archive. The only signal is a non-zero tar exit via ExecException, which will read as a tool failure rather than a capacity problem.
  2. Host-side. Combined with the missing ceiling in feat(e2b): transfer files via envd Filesystem REST API [2/3] #3343's res.body().bytes(), a large workspace becomes a full-archive heap allocation per concurrent persist. The runShellBinaryStdout path this replaces at least sat behind the 64 MiB frame cap.

Also a small cleanup hole: the try starts after runShell, so if tar itself fails part-way the partial /tmp/agentscope-ws-*.tar is never removed.

try {
envd().runShell(e2bState, root, "rm -f " + tarPath, 30);
} catch (Exception e) {
log.debug("[sandbox-e2b] remove temp tar best-effort: {}", e.getMessage());

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.

[Warning] A failed rm leaves a complete copy of the workspace in the sandbox /tmp — archives routinely contain .env files, tokens and model output — and the only trace is one debug line carrying e.getMessage(). Since this is the point at which the sandbox is most likely to be snapshotted or kept alive, please log at warn with the path and note in the javadoc that cleanup is best-effort, so a leftover tar is distinguishable from a leak when someone is debugging.

WorkspaceMountSupport.tarExcludeArgsForBindMounts(e2bState.getWorkspaceSpec())) {
script.append(ex).append(' ');
}
script.append("xf ").append(tarPath).append(" -C ").append(shellSingleQuote(root));

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.

[Info] Two things this refactor improves, worth recording so they are not regressed later: the python3 dependency inside the sandbox image is gone, and the B64_CHUNK loop disappears along with the ~128 KiB per-argument ceiling the SandboxFileTransfer javadoc calls out.

One behaviour delta to confirm as intended: the extract command now carries tarExcludeArgsForBindMounts(...), whereas the replaced subprocess.run(['tar','xf','-','-C',d]) extracted everything. On re-hydrate, files under a bind-mounted subtree are therefore silently skipped rather than overwritten. Probably what you want, but it is a restore-semantics change rather than a pure transport swap, and E2bSandboxFileTransferTest covers upload/download routing and the archive fallback — not exclude-on-extract.

* snapshot API and {@link E2bSnapshotRefs} marker bytes in the Harness snapshot stream.
*/
public class E2bSandbox extends AbstractBaseSandbox {
public class E2bSandbox extends AbstractBaseSandbox implements SandboxFileTransfer {

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.

[Info] Replacing the last call site at line 153 leaves E2bEnvdProcessClient.runShellBinaryStdout (and the binary-stdout capture behind it) with no caller in this module. Either delete it on this branch or add a line saying why it stays: #3341 changes the stream decoder that this method depends on, and keeping an unused transport means the parse-hardening has to be tested against a path nothing exercises.

Also fine to see AbstractBaseSandbox + SandboxFileTransfer composition here matching the Docker/K8s shape — the interface javadoc's "declined paths fall back to exec" contract is what makes the backend-specific confinement discussion above actionable.

Implement SandboxFileTransfer on E2bSandbox so SandboxBackedFilesystem
prefers the envd Filesystem REST API over base64-over-exec automatically.
Only absolute paths are accepted; anything else falls back to the exec
strategy. Workspace persist/hydrate use the REST API for tar transfer.
@jujn
jujn force-pushed the split/e2b-c-file-transfer branch from beccdc8 to 0de0d0b Compare October 11, 2026 14:51

This branch has not been deployed

No deployments
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.

refactor(e2b): route native file transfer through SandboxFileTransfer

2 participants