Sandbox
Every run_command, script tool, and scheduler script 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/script/execute— script-tool extensions (script_*,ext_*)test_tool— trial runs of a script tool'sscript.py
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
Policy files are embedded in the binary and merged with the matching keys in ~/.config/agenvoy/config.json at startup (user entries add to the defaults, never replace them):
| File | config.json key | Purpose |
|---|---|---|
configs/jsons/denied_map.json |
denied_map |
Paths the sandbox refuses to expose |
configs/jsons/exclude_list.json |
— | Paths excluded from listing / walking / searching |
configs/jsons/white_list.json |
white_list |
Binaries run_command may execute |
configs/jsons/read_only_command.json |
read_only_command |
Commands that skip the confirm gate |
configs/jsons/net_white_list.json |
net_white_list |
Hosts exempt from the SSRF guard |
filesystem.LoadRuntime() injects the merged policy once:
sandbox.New(DeniedMapBytes)
filesystem.New(Policy{DeniedMap: DeniedMapBytes, ExcludeList: configs.ExcludeList})
Both go-pkg/sandbox and go-pkg/filesystem enforce IsDenied automatically — no caller-side checking needed.
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 and domain wrappers (e.g., MCPPath, MCPSessionPath). It does not duplicate read/write logic.
Subprocess argv-only schema
run_command accepts only argv: string[] (minItems 1). It does not accept a command: string with auto-tokenization. This zero-parsing approach removes shell-injection surface in the agent layer.
Shell features (pipes, redirects) require an explicit ["sh", "-c", "cmd | pipe"]. 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 and appear on the allowlist |
| No dynamic commands | A command built from a variable or command substitution is rejected outright |
rm is never allowed inside sh -c |
Rejected regardless of allowlist state |
| Shell builtins | A fixed set (cd, echo, test, export, …) passes without an allowlist entry |
Nested sh -c |
Recursively validated with the same rules |
Timeouts
Every tool carries its own timeout, defaulting to one minute and overridden per tool at registration (fetch_page and search_web 90 s, transcribe_media 5 min, download_file 10 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.