Sandbox
Every run_command, script tool, and test_tool execution is wrapped by go-pkg/sandbox:
| Platform | Mechanism |
|---|---|
| Linux | bubblewrap (bwrap) |
| macOS | sandbox-exec |
Sandbox restrictions: no privileged execution, restricted filesystem write scope, configurable network access, configurable CPU/memory limits.
Three callers, one entry point
The sandbox has exactly three callers, all calling sandbox.Wrap(ctx, binary, args, workDir, opt) directly:
run_command— arbitrary user-issued commands (internal/tools/runCommand.go)toolAdapter/scriptexecute — script-tool extensions (script_*,ext_*)test_tool— trial runs of a script tool'sscript.py(60 s limit)
There is no wrapper layer between callers and sandbox.Wrap. Adding behavior (e.g., new resource limits) means contributing to go-pkg/sandbox, not adding shims in agenvoy.
Policy injection
filesystem.LoadRuntime() reads ~/.config/agenvoy/config.json at startup and hands the merged policy to go-pkg/sandbox and go-pkg/filesystem. Some policies ship embedded in the binary and merge with the user's entries; others exist only as user config.
| Policy | Embedded default | config.json key | Purpose |
|---|---|---|---|
| Sensitive paths | configs/jsons/sensitive_path.json |
sensitive_path |
Paths holding credentials or key material — reachable only after a password-backed per-session grant. Four buckets: dirs, files, prefixes, extensions |
| Exclusions | configs/jsons/exclude_list.json |
— | Paths excluded from listing / walking / searching |
| Read-only commands | configs/jsons/read_only_command.json |
read_only_command |
Commands that skip the confirm gate |
| Denied commands | — | denied_command |
Binaries run_command refuses outright, inside sh -c too |
| Denied paths | — | denied_path |
Paths permanently off limits for reads and writes; no prompt can approve them. Entries must be absolute or ~/-prefixed, the root is refused, and symlink targets are denied as well |
| SSRF exemptions | — | net_white_list |
Hosts exempt from the http_request SSRF guard |
The command policy is a denylist. There is no binary allowlist: white_list and path_white_list were removed, and sensitive_map was renamed to sensitive_path; a config still carrying any of these keys gets a startup warning rather than silent behavior. Path access outside $HOME is granted per session through the confirmation prompt, not through a config list.
denied_path is handed to go-pkg/sandbox as its denied map; boundary.Resolve and boundary.WriteBinds check it on the agenvoy side before any file tool or write_paths bind proceeds.
Filesystem write guard
go-pkg/filesystem write APIs (WriteFile, WriteJSON, AppendText, CheckDir) all enforce IsDenied internally. Any agenvoy code that bypasses go-pkg/filesystem and writes via os.WriteFile directly escapes the policy — this is forbidden.
The internal/filesystem package retains only path computation, runtime limits, and domain wrappers. It does not duplicate read/write logic.
Outside $HOME, run_command needs the paths declared up front: the call carries write_paths (absolute paths only), the user approves them with the system password, and only then are they bound read-write into the sandbox for that session. A write to an unapproved path fails with an explicit message, and a permission error on an unbound path outside $HOME gets a hint to re-run with write_paths rather than a bare error the model would misread as an ownership problem.
Subprocess argv-only schema
run_command accepts only argv: string[] (minItems 1) plus optional write_paths. It does not accept a command: string with auto-tokenization. This zero-parsing approach removes shell-injection surface in the agent layer.
At the top level, sudo is rejected (declare write_paths instead), rm is routed to the trash, and cd switches the work directory after verifying the path.
Shell features (pipes, redirects) require an explicit ["sh", "-c", "cmd | pipe"] (or bash -c). That script is parsed with mvdan.cc/sh rather than string-matched, and every command node inside it is checked:
| Rule | Effect |
|---|---|
| Bare command names only | /usr/bin/curl is rejected — the binary must be written as curl |
| No dynamic commands | A command built from a variable or command substitution is rejected outright |
No rm or sudo |
Both are rejected inside sh -c |
| Denied binaries | Anything on denied_command is rejected, inside sh -c as well |
| Shell builtins | A fixed set (cd, echo, test, export, ...) passes without an allowlist entry |
Nested sh -c |
Recursively validated with the same rules; a nested script that is not a static string is rejected |
Timeouts
Every tool carries its own timeout, defaulting to one minute and overridden per tool at registration (open_file 10 s, fetch_page and search_web 90 s, generate_audio 5 min, download_file 10 min, generate_image 15 min, run_command 60 min). Subagent invocations, including slot-wait time, are capped by MaxSubagentTimeoutMin (30 min).
These are package-level values — the environment-variable overrides that used to control them were removed.