Skip to content

fix(sandbox): docker sandbox native file transfer + reject truncated downloads (#2618) - #2923

Open
larry-zy wants to merge 6 commits into
agentscope-ai:mainfrom
larry-zy:fix/docker-sandbox-native-file-transfer
Open

fix(sandbox): docker sandbox native file transfer + reject truncated downloads (#2618)#2923
larry-zy wants to merge 6 commits into
agentscope-ai:mainfrom
larry-zy:fix/docker-sandbox-native-file-transfer

Conversation

@larry-zy

Copy link
Copy Markdown
Contributor

Problem

Fixes #2618. SandboxBackedFilesystem.uploadFiles fails deterministically on large files. Three compounding causes:

  1. Fast path never taken — the SandboxFileTransfer interface existed but no sandbox implemented it, so every upload fell back to the base64-in-argv path.
  2. Content embedded in the command string — the fallback base64-encodes file content and concatenates it into a shell command.
  3. ARG_MAX exceededDockerSandbox passes that whole command as a single docker exec argv element, blowing past the OS argument limit (~1MB on macOS, 128KB per-arg on Linux).

Real-world trigger: SessionTree.mirrorToFilesystem mirroring 50+ messages produces JSONL >768KB; base64 inflates it ~33% and the upload permanently fails.

Fix (Option A from the issue)

  • DockerSandbox implements SandboxFileTransfersupportsFileTransfer / uploadFile / downloadFile transfer bytes via a host temp file + docker cp round trip. File content never passes through docker exec argv, so there is no ARG_MAX limit and no 512KB stdout truncation cap.
    • Workspace-constrained path validation: rejects workspace root, .. traversal, outside-workspace, and blank/null paths.
  • SandboxBackedFilesystem — when a download falls back to exec and the sandbox truncates stdout, return FileDownloadResponse.fail(...) instead of silently handing back partial base64 (avoids silent data corruption).

The dispatch logic in uploadFiles / downloadFiles (active instanceof SandboxFileTransfer) already existed; it just had no implementation to reach. Docker now actually hits the fast path.

Tests

  • Unit (26 passing): path validation (root / traversal / outside-workspace / blank / null), container edge cases, truncated-download failure.
  • Integration (opt-in -Ddocker.it=true, 9 passing against a live Docker daemon): >1MB binary bit-for-bit round trip, paths with spaces/quotes, relative/absolute paths, workspace root /, missing-file non-zero exit, temp-file cleanup.

All green locally (unit + Docker integration).

larry-zy and others added 2 commits August 31, 2026 02:32
…downloads (agentscope-ai#2618)

Implement SandboxFileTransfer for DockerSandbox via docker cp (host temp
file <=> container), so upload/download never pass file bytes through
docker exec argv (base64 E2BIG for large files) nor the 512KB stdout
truncation cap.

- DockerSandbox: supportsFileTransfer/uploadFile/downloadFile with
  workspace-constrained path validation (rejects root, traversal,
  outside-workspace, blank/null); truncation-free docker cp round trip.
- SandboxBackedFilesystem: fail downloads whose exec fallback output was
  truncated instead of silently returning partial base64.
- Tests: unit coverage for path/boundary/container edge cases; opt-in
  (-Ddocker.it=true) real round-trip integration tests against a live
  Docker daemon (>1MB binary bit-for-bit, spaces/quotes, relative/absolute,
  workspace root '/', missing-file non-zero exit, temp-file cleanup).
@codecov

codecov Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 47.76119% with 35 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...rness/agent/sandbox/impl/docker/DockerSandbox.java 44.44% 24 Missing and 11 partials ⚠️

📢 Thoughts on this report? Let us know!

@larry-zy larry-zy changed the title fix(harness): docker sandbox native file transfer + reject truncated downloads (#2618) fix(sandbox): docker sandbox native file transfer + reject truncated downloads (#2618) Aug 31, 2026
…daemon

Add an overridable runDockerCliBlocking seam and a recording DockerSandbox
subclass so unit tests drive the uploadFile/downloadFile temp-file plumbing
(mkdir, docker cp, temp write/read, cleanup on success and cp failure)
without requiring -Ddocker.it=true. Raises patch coverage past the codecov
gate; the docker cp execution paths were previously exercised only by the
opt-in integration tests, which CI does not run.
Buktal pushed a commit that referenced this pull request Sep 2, 2026
…rdown to stop flaky temp-dir deletion (#2935)

### Problem

Two CI runs fail intermittently with JUnit errors that are not
test-logic failures but teardown failures:

- `Failed to delete temp directory` (Windows, PR #2923)
- `Failed to close extension context` / `Failed to delete temp
directory` (Ubuntu, PR #2926)

Both surface only when a test builds a transient `HarnessAgent`, drives
it to completion via `.block()` / `.stream()...block()`, and uses
`@TempDir` for its workspace/state home.

### Root cause

This is a long-standing race, not a regression of any single commit. The
harness memory flush has always been asynchronous: when an agent stream
completes, `MemoryFlushMiddleware#onAgent` dispatches the flush on
`Schedulers.boundedElastic()` via `subscribe(...)` — i.e.
fire-and-forget. The calling test's `.block()` only waits for the
business stream, **not** for that background flush.

So the timeline is:

1. Test calls `.block()` and returns.
2. JUnit begins `@TempDir` teardown and deletes the temp directory.
3. The async flush on `boundedElastic` is **still writing
session/transcript mirror files into that same `@TempDir`** (or still
holds open file handles).

The result: directory/file deletion fails with `IOException` → wrapped
as `JUnitException`. It is timing-dependent (depends on IO speed,
scheduler, and how many files are written), which is why it flakes
rather than failing deterministically, and why Windows (stricter file
locking) fails more readily than Linux.

The normal production path avoids this because `HarnessAgent#close()`
calls `SessionTree.awaitMirrorQuiescence(...)` +
`MemoryBackgroundTasks.awaitQuiescence(...)`. The flaky tests never call
`close()` on their transient agent.

### Fix

Add test-side quiescence that mirrors what `HarnessAgent#close()`
already does, run **after each test method but before the `TempDir`
extension deletes the directory**:

- `HarnessBackgroundTaskQuiescenceExtension` — an `AfterEachCallback`
that calls `SessionTree.awaitMirrorQuiescence(5s)` +
`MemoryBackgroundTasks.awaitQuiescence(5s)`. When nothing is in flight
both calls return immediately, making it a no-op for tests that never
trigger a flush.
- `@HarnessQuiescence` — a composed meta-annotation
(`@ExtendWith(HarnessBackgroundTaskQuiescenceExtension.class)`) so
at-risk tests only need a one-line annotation.
- Applied `@HarnessQuiescence` to 16 harness test classes that match the
at-risk pattern (`HarnessAgent.builder()` + `.call()`/`.stream()` +
`@TempDir`), including the three classes that flaked in CI:
`JsonSessionDefaultLocationTest`, `HarnessAgentIntegrationExampleTest`,
`HarnessAgentDynamicHookBuilderTest`.

Production code is unchanged — the flush remains fire-and-forget so
conversation completion is never blocked.
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.

[Bug]: SandboxBackedFilesystem.uploadFiles 大文件确定性失败——base64 内容塞进 argv 超 ARG_MAX(error=7, Argument list too long)

1 participant