fix(tools): contain file read/write/edit to workspace root by default#311
Open
Jiangrong-W wants to merge 1 commit into
Open
Conversation
…ault The built-in file tools resolved model-/LLM-supplied paths to an absolute host path and read, wrote, or edited them directly. Workspace containment was only enforced when the optional Docker sandbox was active (disabled by default), so in the default configuration a prompt-injected or mistaken model could escape the repository root via absolute paths or ../ traversal. This consolidates two related fixes for the same root cause (containment only under Docker) across the whole file-tool family: - read_file: read arbitrary UTF-8 host files outside the workspace, subject only to the narrow credential denylist. - write_file / edit_file: in the default interactive UI these are gated by a permission prompt and edit-diff approval, but the shipped non-interactive runners (the --task-worker used for background agents, and print mode) install a permission callback that always returns True and supply no edit_approval_prompt; in that auto-approved path a model could overwrite or edit host files outside the repository. Enforce workspace containment for read_file, write_file, and edit_file before any read or write, independent of the Docker sandbox and of the approval path, reusing the repo's existing path-boundary check (validate_sandbox_path) via a new validate_workspace_path helper. Containment fails closed before the user is prompted. It is on by default and can be relaxed per session through the new filesystem settings (restrict_to_workspace / allow_paths), surfaced to tools as execution metadata. Backward compatible: tools default to containment-on when no metadata is supplied. Signed-off-by: christop <825583681@qq.com>
Jiangrong-W
force-pushed
the
harness-fix/openharness-file-path-containment-consolidated
branch
from
June 19, 2026 03:59
3646bc5 to
dc333b1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
What problem does this PR solve? The built-in file tools (
read_file,write_file,edit_file) resolved a model-/LLM-supplied path to an absolute host path and then read or wrote it directly. Workspace containment was only enforced inside theis_docker_sandbox_active()branch (which callsvalidate_sandbox_path). The Docker sandbox is disabled by default (SandboxSettings.enabled = False), so in the default, unsandboxed configuration nothing kept these tools inside the repository root. A prompt-injected or mistaken model could therefore escape the workspace via an absolute path or..traversal:read_fileis read-only, soPermissionChecker.evaluateauto-allows it after only the credential denylist / optional path rules — out-of-workspace reads of arbitrary UTF-8 host files succeeded with no prompt.write_file/edit_fileare gated by a permission prompt and edit-diff approval in the interactive UI, but the shipped non-interactive runners (run_task_worker, used for background teammates spawned with--task-worker, andrun_print_mode) install a permission callback that always returnsTrueand pass noedit_approval_prompt. On that auto-approved path, out-of-workspace writes and edits also succeeded.Affected sites (decisive sink per path):
read_file—src/openharness/tools/file_read_tool.py:52(raw = path.read_bytes())write_file—src/openharness/tools/file_write_tool.py:59(path.write_text(arguments.content, encoding=\"utf-8\"))edit_file—src/openharness/tools/file_edit_tool.py(path.write_text(updated, ...), both the sandbox and non-sandbox branches)What changed? Workspace containment is now enforced for
read_file,write_file, andedit_filebefore any read or write, independent of the Docker sandbox and independent of the approval path, by reusing the repo's existing path-boundary check:validate_workspace_path(path, cwd, metadata)insrc/openharness/sandbox/path_validator.py— the always-on counterpart tovalidate_sandbox_path, which it reuses for the actual containment check (real-path resolution + ancestor check, already rejecting absolute-outside paths,..escapes, and symlink escapes). Exported fromsrc/openharness/sandbox/__init__.py.validate_workspace_path(...)right after path resolution. Forwrite_file/edit_filethe check runs before both the approval branch and the auto-approve branch, so it fails closed before the user is ever prompted and also holds on the interactive path.FilesystemSettings(restrict_to_workspace: bool = True,allow_paths: list[str] = []) onSettings.filesystem(src/openharness/config/settings.py), surfaced into the enginetool_metadatainsrc/openharness/ui/runtime.py(including the task-worker and print-mode runtimes, so the auto-approve paths inherit containment).Containment is on by default and security-positive: out-of-workspace reads, writes, and edits are denied by default. It is backward compatible — no public API signature changes,
validate_sandbox_pathis untouched, the new helper is additive, and tools called without metadata (e.g. in tests) default to containment-on. Operators who need to reach outside the repository can opt out per session withfilesystem.restrict_to_workspace = false, or scope extra roots withfilesystem.allow_paths. In-workspace edits keep the existing approval-prompt behavior.Validation
uv run ruff check src tests scriptsuv run pytest -qcd frontend/terminal && npx tsc --noEmit(if frontend touched)Test summary (added with this change, covering both affected tools):
tests/test_tools/test_file_tool_containment.py—read_filescope (5 tests): rejects absolute-outside, rejects..escape, allows in-workspace, honorsrestrict_to_workspace=Falseopt-out, honorsallow_pathsextra root.tests/test_tools/test_file_write_containment.py—write_file/edit_filescope (10 tests): rejects..escape and absolute-outside on the headless auto-approve path, rejects an escape even when an approval callback is set (and asserts the callback is never invoked — fails closed), plus in-workspace, opt-out, andallow_pathsallow-cases for both tools.Both new suites fail on the base commit (the escapes succeed) and pass after this change:
Run together with the adjacent tool / sandbox / config suites it is regression-clean (
137 passed, 1 skipped).uv run ruff checkpasses on every file touched here. The frontend was not modified, so the TypeScript check has nothing to run for this PR. The fulluv run pytest -qsuite is left for a maintainer's green CI run.Notes
Unreleasedentry could optionally be added toCHANGELOG.mdto record the default-on workspace containment for the built-in file tools.