Repository navigation
fix(collect): read only the agent's own regular files, within a deadline - #607
earakely-scale wants to merge 5 commits into
Conversation
… and time A manifest entry is read only under base_path, and its name is stored as a key with no separators or dot segments. On a local sandbox's host, which is this machine, a file is read only where it resolves inside the sandbox's work dir or base_path; sandboxes declare that folder as app_dir_on_this_machine. Only regular files are read, links followed, up to the step's max_bytes (16 GiB by default). The sandbox sends at most the size it reported, the decoder refuses more, and a read past its deadline is abandoned. Modal exec output streams instead of being read whole, and the CUA path applies the same checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each collected key becomes a clean relative member name: backslashes separate, drive prefixes, leading separators and '.' segments are dropped, and '..' climbs no higher than the archive root. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n, time the whole read Under /app only the sandbox's work dir counts as a root: an agent could swap a folder there for a link out. A base_path outside /app still counts, resolved. Two manifest names that clean to one key get "key (2)" and on. The deadline covers exec too, which some providers run to completion. Manifest paths are matched against base_path normalized. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ranslates stat's %F names the file type in the system's language, so on a host with a non-English locale every file would have been refused. %f prints the raw mode, checked with stat.S_ISREG. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collect reads a regular file of any size again, as before this branch. Reads stay bounded to the size stat reports and to the deadline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| file_size = -1 if read_path is None else await self._get_file_size(sandbox, container, read_path) | ||
| if file_size < 0: | ||
| logger.warning(f"File not found: {source_path}") | ||
| continue |
There was a problem hiding this comment.
Agent files fill operator disk
Removing the max_bytes checks lets an agent exhaust the collector's disk with a huge sparse regular file under base_path. _collect_items now passes the full reported size into _collect_file, which writes it to a local temporary file. The decoder only rejects bytes beyond that source size, and the deadline grows with it: an 8 TiB file gets roughly 97 days. _controller_get_file likewise no longer rejects oversized files.
Restore a configurable limit before either read, with a bounded default.
How this was verified: Agent-created regular files reach local temporary-file writes without a maximum-size check, and the remaining byte and time limits grow with the source file's reported size.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent_env/task_step/task_steps/collect_artifacts.py
Line: 932-935
Comment:
**Agent files fill operator disk**
Removing the `max_bytes` checks lets an agent exhaust the collector's disk with a huge sparse regular file under `base_path`. `_collect_items` now passes the full reported size into `_collect_file`, which writes it to a local temporary file. The decoder only rejects bytes beyond that source size, and the deadline grows with it: an 8 TiB file gets roughly 97 days. `_controller_get_file` likewise no longer rejects oversized files.
Restore a configurable limit before either read, with a bounded default.
**How this was verified:** Agent-created regular files reach local temporary-file writes without a maximum-size check, and the remaining byte and time limits grow with the source file's reported size.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Deliberate: the maintainers took the size cap out of this PR, so a regular file is read whatever its size, as before this change. The PR body lists it under "Not covered here", and the remaining gap is recorded on the tracking issue. A cap can come in a follow-up if it's needed.
There was a problem hiding this comment.
Understood. Since the size cap was deliberately removed, the PR explicitly records the limitation, and the follow-up is tracked, this isn’t a defect to fix in this PR. I’m withdrawing the finding; the disk-exhaustion risk remains a documented follow-up.
Why
collect_artifactscopies what an agent leaves behind onto the operator's side. Three flaws let the agent choose more than its own deliverables:manifest_step_id) the paths come from the agent's own structured output. Every absolute path was read, insidebase_pathor not.sandbox_namealone) runs on the operator's machine. So a manifest naming~/.ssh/id_rsaor~/.aws/credentialscopied that file into the run's object store.base_pathdid the same.../../.bashrcbecame an archive entry that climbs out of the folder it's extracted into, holding bytes the agent wrote.base64 < pathuntil the sandbox closed the stream, and its size was checked only afterwards./dev/zerostreamed onto the operator's disk without end, and a FIFO hung the step.What changes
base_pathonce normalized. Any other entry is skipped with a warning..or..segments, or the file's name if nothing is left.../../.bashrcis stored as.bashrc, andlogs/run.jsonaslogs_run.json. Names that clean to the same key getkey (2),key (3)and so on.base_pathnormalized.readlink -f) inside the sandbox's work dir, or insidebase_pathwhen the task names a folder outside/app. It reads the resolved path. Anything else, a link out included, is a hard failure for that file.app_dir_on_this_machine: the folder on this machine its host commands see as/app. It's None by default, and the local sandbox's work dir. Collect asks the sandbox for it rather than checking its class.ON_THIS_MACHINEdoesn't answer the question, because a smol VM sandbox sets it but runs its commands inside a VM.base_pathoutside/appstays readable, because a run_code script on a local VM sandbox writes to host folders the task chooses, and a task author can already run commands on that host. Under/apponly the work dir counts, since the agent could swap a folder there for a link.stat -L -c %f:%s, and the raw mode bits it prints are checked withstat.S_ISREG;%F's type names are translated by locale. Anything but a regular file is refused before it's read: a device, FIFO, directory or socket, or a link to one.head -c SIZE < path | base64, so it sends no more than the size it reported, and the decoder refuses any byte past that size.execincluded, is abandoned once it outlasts 300 s plus one second per MiB. Some providers run the whole command insideexec._ModalStreamAdapteris now async-iterable. Modal and Modal VM output is decoded to disk as it arrives instead of being read whole.[ -f ], links followed)..segments are dropped;..climbs at most to the archive root;artifact.Compatibility
base_path: they used to be collected, keyed by their file name; now they're skipped. A task whose agent reports deliverables in several folders needs abase_paththat holds them all./:logs/run.jsonis now keyedlogs_run.json.base_pathis no longer followed.headin the sandbox: the read now also runshead, from the same coreutils that already providebase64.Tests
base_path, or climbing out with.., is skipped;/dev/zero, a FIFO or a directory;base_pathis read;base_pathunder/appthat the agent swapped for a link to a home folder reaches nothing outside the work dir;base_pathis matched normalized;execthat never returns is held to the deadline;../../x,/abs/../../y, backslashes and drive prefixes.report.pdfis a link to/dev/zero/app/artifact/../..statwithout-Lsized the link itself../../.bashrc,..\..\evil.sh.bashrc,evil.sh../../x,/abs/../../y../../.bashrc,..\..\evil.sh,../../x,abs/../../y.bashrc,evil.sh,x,ypython:3.12-slimsandbox, collected throughModalSandbox's exec adapter:/dev/zeroand a FIFO were each refused in 0.4 s.Not covered here
🤖 Generated with Claude Code
Restore the file-size limit before merging so agent-created files cannot exhaust the collector's disk.
Fix with agent prompt
Summary
This PR restricts manifest paths, checks local-host file locations, refuses non-regular files, streams Modal output, and cleans archive names.
Diagram
%%{init: {'theme': 'neutral'}}%% flowchart LR A[Agent file] --> B[Check path and regular-file type] B --> C[Read reported size without maximum] C --> D[Write local temporary file] D --> E[Upload artifact]Reviews (8) · Last reviewed commit: "revert(collect): drop the per-file size ..." · Reviewed by Greptile