File Sandbox
All file tools (ReadFile, WriteFile, ListDir, EditFile) operate within a sandbox root — a canonical directory that no tool operation can escape.
SandboxRoot
#![allow(unused)] fn main() { let root = SandboxRoot::new("/home/user/project")?; root.validate("src/main.rs")?; // Ok — within sandbox root.validate("../../etc/passwd")?; // Err — escapes sandbox }
The sandbox root is the project root by default, auto-detected by walking up from the current directory looking for markers (.rho/config.toml, .git/, Cargo.toml, package.json, etc.). It can be overridden with --root <path>.
Validation strategies
Existing paths
validate() resolves symlinks and .. components via std::fs::canonicalize, then checks the canonical path starts with the canonical root. This prevents path traversal through symlinks or directory climbing.
Not-yet-existing paths
validate_for_write() handles new files (where canonicalize would fail). It:
- Walks up from the path until it finds an existing ancestor.
- Canonicalises that ancestor.
- Re-appends the remaining components.
- Rejects any
..component in the not-yet-existing suffix immediately. - Verifies the resolved path stays within the sandbox root.
This allows WriteFile to create new files within the project while still preventing escapes like new_dir/../../../etc/passwd.
Opt-out
The sandbox is always enabled and cannot be disabled.
TOCTOU note
There is a theoretical time-of-check-time-of-use race between validate_for_write and the subsequent create_dir_all + write. This is accepted because:
- The threat model does not include concurrent adversarial filesystem modification.
- If the model has shell access, it can write directly via the shell — the sandbox only constrains the tool interface.
- The approval gate is the primary defense against model-initiated writes.