feat: place caller-provided files into the sandbox workspace (extra_files, --workspace-file) - #1085
Conversation
…the sandbox workspace
Greptile SummaryThe PR adds caller-provided, read-only workspace files across the engine API, CLI, manifest, and bind-mount sandbox paths.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains; both previously reported collision issues are prevented on the current paths. Important Files Changed
Reviews (5): Last reviewed commit: "drop the workspace-file size limit" | Re-trigger Greptile |
extra_files, --workspace-file)
Strix Security ReviewAll previously reported security findings have been resolved. 1 resolved finding
Review summaryReviewed the security-relevant changes for the new Updated for Reviewed by Strix |
| if not getattr(args, "workspace_files", None): | ||
| args.workspace_files = [ | ||
| workspace_file | ||
| for workspace_file in state.get("workspace_files") or [] | ||
| if isinstance(workspace_file, dict) | ||
| and Path(str(workspace_file.get("source_path", ""))).is_file() | ||
| ] |
There was a problem hiding this comment.
Revalidate persisted workspace-file metadata on resume
| if not getattr(args, "workspace_files", None): | |
| args.workspace_files = [ | |
| workspace_file | |
| for workspace_file in state.get("workspace_files") or [] | |
| if isinstance(workspace_file, dict) | |
| and Path(str(workspace_file.get("source_path", ""))).is_file() | |
| ] | |
| if not getattr(args, "workspace_files", None): | |
| restored_specs = [] | |
| for workspace_file in state.get("workspace_files") or []: | |
| if not isinstance(workspace_file, dict): | |
| continue | |
| source_path = Path(str(workspace_file.get("source_path", ""))) | |
| if not source_path.is_file(): | |
| continue | |
| workspace_path = str(workspace_file.get("workspace_path", "")) | |
| if not workspace_path.startswith("/workspace/"): | |
| continue | |
| restored_specs.append(f"{source_path}:{workspace_path.removeprefix('/workspace/')}") | |
| try: | |
| args.workspace_files = resolve_workspace_files(restored_specs) | |
| except ValueError as exc: | |
| parser.error(f"--resume {args.resume}: invalid workspace file metadata: {exc}") |
There was a problem hiding this comment.
Applied in 9c5307d: --resume now rebuilds the persisted declarations into PATH:DEST specs and runs them back through resolve_workspace_files, so a resumed run revalidates the destination, duplicates, and total size exactly like a fresh --workspace-file instead of only checking that the source still exists. An edited run.json pointing outside /workspace fails the resume with --resume <run>: invalid workspace file: …; a file deleted between runs is still dropped rather than fatal. Tests: test_resume_revalidates_persisted_workspace_files and test_resume_rejects_an_edited_workspace_file_path.
Summary
Until now the only way into the sandbox filesystem was a whole directory:
local_sources→LocalDirmanifest entries (copy backends) or bind mounts (Docker). There was no way to place a single file that is not part of a target tree, so callers had to write into the cloned target directory (dirtying what the agent inspects, and writable by the agent) or push the file after session start (extra round trip, races startup).This adds one backend-agnostic path for that, and exposes it on the CLI.
Engine API.
run_strix_scan(..., extra_files=[{"workspace_path": "/workspace/<rel>", "content": bytes | str}]), forwarded tosession_manager.create_or_reuse. Both backends materialize the same declaration:File(content=…)entry that rides the existing one-tar upload — no extra transport;read_only=Trueat the same path.Paths must be under
/workspace, with no traversal and no control characters. An entry is skipped with a warning when its path collides with a local source tree (exact match, nested under a source root, or an ancestor of one) or with an already-placed extra file, so an extra file can never replace a target'sLocalDir/mount, shadow it, or produce two mounts for one path.CLI.
--workspace-file PATH[:DEST], repeatable.DESTis a path inside/workspaceand defaults to the file name:Specs are resolved at parse time (file exists and is readable, destination inside
/workspace, no duplicate destinations, 10 MB total cap), persisted inrun.jsonso--resumeplaces the same files again, and read intoextra_filesat launch. The resolved paths are listed in the root task underFiles Provided By The User:, explicitly marked as data rather than instructions or scope, so the agent knows where to read them without the contents claiming authority.Docs:
--workspace-filein the CLI reference plus a "Workspace files" section inusage/instructionscovering destinations, the read-only guarantee, the collision and size rules, and a warning against placing secrets there.Link to Devin session: https://app.devin.ai/sessions/ea01d0ad34dc4dd28200d258ebd2198e
Requested by: @yoni-at-strix