fix(deepagents): add default chunked upload/download to BaseSandbox#1402
fix(deepagents): add default chunked upload/download to BaseSandbox#1402Dr. Tristan Behrens (AI-Guru) wants to merge 4 commits intolangchain-ai:mainfrom
Conversation
BaseSandbox already provides default implementations of read(), write(), edit(), ls_info(), glob_info(), and grep_raw() via execute(). However, upload_files() and download_files() are abstract, forcing every sandbox backend to reimplement the same base64+heredoc pattern. More importantly, the common pattern of embedding the entire base64 payload in the command string hits the Linux kernel ARG_MAX limit (~128KB per argument) for files larger than ~100KB. This causes "argument list too long" errors when uploading binary files like PDFs to Docker containers with tmpfs mounts (where put_archive doesn't work). This commit: - Adds concrete upload_files() and download_files() to BaseSandbox - Small files (<64KB) use a single execute() call (no change) - Large files are split into 64KB base64 chunks, each written via a separate execute() call, then decoded in the sandbox - Downloads also chunk large files to avoid execute() output truncation - Methods are non-abstract so backends with native file transfer (e.g. SSH/SFTP, Daytona REST) can still override them - Adds 6 unit tests covering small/large/error paths for both operations Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- SIM108: use ternary for upload/download strategy selection - Q003: use single outer quotes to avoid escaping inner double quotes - BLE001: catch (ValueError, binascii.Error) instead of blind Exception - F401: remove unused FileDownloadResponse/FileUploadResponse imports Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
|
||
| return responses | ||
|
|
||
| def _upload_single(self, file_path: str, b64: str) -> ExecuteResponse: |
There was a problem hiding this comment.
If you're interested in working on this feature, could you try to match the style of the other provided implementations (e.,g., write_file etc)?
offload as much as you can into the template which attempts to make it easier to see the boundary between python and shell code (makes it easier to verify that the implementation does not security issues)
Make sure that everything that needs to be escaped is escaped properly so there's no additional attack surface
There was a problem hiding this comment.
Done! Refactored all upload/download methods to use module-level _COMMAND_TEMPLATE constants matching the existing _WRITE_COMMAND_TEMPLATE / _EDIT_COMMAND_TEMPLATE pattern.
Seven new templates added: _UPLOAD_COMMAND_TEMPLATE, _UPLOAD_CHUNK_COMMAND_TEMPLATE, _UPLOAD_DECODE_COMMAND_TEMPLATE, _REMOVE_COMMAND_TEMPLATE, _DOWNLOAD_SIZE_COMMAND_TEMPLATE, _DOWNLOAD_COMMAND_TEMPLATE, _DOWNLOAD_CHUNK_COMMAND_TEMPLATE.
All file paths are now base64-encoded before interpolation (safe charset [A-Za-z0-9+/=]), which also eliminates the shell injection risk from the previous string concatenation approach. Template format tests added for each.
|
|
||
| Args: | ||
| file_path: Absolute path inside the sandbox. | ||
| b64: Base64-encoded file content (must fit within ARG_MAX). |
There was a problem hiding this comment.
is ARG_MAX relevant if we're feeding through stdin?
There was a problem hiding this comment.
Good question! ARG_MAX itself (typically ~2MB) is the total size limit for all arguments + environment passed to execve(). But the more relevant constraint here is MAX_ARG_STRLEN (typically PAGE_SIZE * 32 = 128KB on Linux), which limits any single argument string.
When we use bash -c 'script<<heredoc', the entire string — including the heredoc content — is passed as a single argument to execve(). So heredocs via bash -c don't bypass the limit the way a standalone heredoc in an interactive shell would (where bash itself reads from stdin).
With 64KB raw chunks → ~87KB base64, we stay safely under the 128KB MAX_ARG_STRLEN per-argument limit.
I've updated the comments in the code to reference MAX_ARG_STRLEN instead of ARG_MAX to be more precise about which kernel limit actually applies.
Replace string concatenation in upload/download methods with module-level _COMMAND_TEMPLATE constants, matching the existing pattern used by _WRITE_COMMAND_TEMPLATE and _EDIT_COMMAND_TEMPLATE. All file paths are now base64-encoded before interpolation, eliminating shell injection risk from path concatenation. Seven new templates added: - _UPLOAD_COMMAND_TEMPLATE (small file, heredoc stdin) - _UPLOAD_CHUNK_COMMAND_TEMPLATE (append chunk to temp file) - _UPLOAD_DECODE_COMMAND_TEMPLATE (decode assembled base64) - _REMOVE_COMMAND_TEMPLATE (cleanup helper) - _DOWNLOAD_SIZE_COMMAND_TEMPLATE (get file size) - _DOWNLOAD_COMMAND_TEMPLATE (small file download) - _DOWNLOAD_CHUNK_COMMAND_TEMPLATE (chunked download) Also updates ARG_MAX comments to reference MAX_ARG_STRLEN (128KB per-argument limit on Linux) which is the actual constraint when heredocs are used inside bash -c. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
upload_files()anddownload_files()implementations toBaseSandbox, replacing the current@abstractmethodstubsexecute()call (same as before)ARG_MAXlimit (~128KB per argument)execute()output truncationProblem
When using sandbox backends that route file transfers through
execute()(Docker with tmpfs, microsandbox), uploading binary files larger than ~100KB fails with:This happens because the base64-encoded file content is embedded in the command string passed to
exec_run(), which exceeds the kernel'sARG_MAXlimit. Docker'sput_archiveAPI cannot be used as a workaround because tmpfs mounts are invisible to it.The error is particularly problematic for autonomous workflows that generate binary artifacts (PDFs, images) inside sandboxed containers.
Solution
BaseSandboxalready provides default implementations ofread(),write(),edit(),ls_info(),glob_info(), andgrep_raw()viaexecute(). This PR extends that pattern toupload_files()anddownload_files():execute()calls, then the assembled base64 is decoded to the final pathexecute()calls, then reassembled on the hostAll operations go through
execute(), respecting the full middleware chain.Test plan
🤖 Generated with Claude Code