Security and Sandbox¶
Tether never relies on system prompt instructions ("please do not read .env or run rm -rf") for safety. Security boundaries around bash and edit are enforced deterministically by SandboxPolicy and the OS kernel.
Sandbox Profiles¶
| Profile | File Reads (edit view) |
File Writes (edit write/replace) |
bash Execution |
|---|---|---|---|
workspace-write (default) |
Allowed inside workspace_root (except protected paths) |
Allowed inside workspace_root (except protected paths) |
Allowed inside workspace_root (with kernel bwrap or command guard) |
read-only |
Allowed inside workspace_root (except protected paths) |
Blocked (SandboxViolationError) |
Restricted to read-only inspection allowlist (ls, cat, grep, git, rg, etc.) |
full-access |
Allowed inside workspace_root |
Allowed inside workspace_root |
Allowed without command denylist (still scrubs secrets unless disabled) |
Four Layers of Deterministic Defense¶
1. Canonical Path Confinement (resolve_safe_path)¶
Every path passed to edit or referenced in bash is canonicalized via Path.resolve() and verified to reside inside workspace_root. Traversal attempts (../../etc/passwd) raise SandboxViolationError before touching the filesystem.
2. Protected Sensitive Paths¶
Even within workspace_root, Tether deterministically blocks access to sensitive files and directories:
.envand.env.*.git/configand.git/hooks/*.tether/*(the harness's own SQLite database).ssh/*,id_rsa,id_ed25519
3. Environment Allowlist¶
bash subprocesses receive only a small allowlist of variables (PATH, LANG, LC_*, TERM, TZ, USER, LOGNAME, SHELL, TMPDIR, VIRTUAL_ENV, CI, PYTHONPATH, ...) plus HOME/PWD set to the workspace. Everything else (DATABASE_URL, STRIPE_*, *_WEBHOOK, cloud and API keys, session variables) is dropped, so a secret with an unexpected name cannot leak. To pass something through deliberately, set TETHER_ENV_PASSTHROUGH=NAME1,NAME2.
4. Linux Bubblewrap (bwrap) Kernel Isolation¶
On Linux, when bwrap is available and functional, bash subprocesses automatically execute inside an unprivileged user/mount/IPC/PID namespace:
/(usr,bin,lib,etc) is mounted read-only (--ro-bind).$HOMEis masked with an empty ephemeral--tmpfs.workspace_rootis bind-mounted (--bindinworkspace-writemode,--ro-bindinread-onlymode).- Secret-looking files inside the workspace at any depth (
.env*,*.pem,*.key,id_rsa,id_ed25519,authorized_keys), plus.tether/,.git/configand.git/hooks, are masked (/dev/nullbind or empty--tmpfs), so globbing or shell indirection cannot reach them. Files with arbitrary names (for examplecreds.json) cannot be detected; keep them out of the workspace. /home,/media,/mnt,/srvand/var/tmpare hidden behind empty tmpfs,/run/useris hidden, and every unix socket under/run(for exampledocker.sock) is masked. A read-only bind does not stopconnect(), so an exposeddocker.sockwould be a host escape.--unshare-pid,--unshare-ipcand--die-with-parentensure runaway child processes are reaped immediately on timeout.
5. Optional network isolation (--no-network)¶
By default bash keeps network access. tether run --no-network ... (or SandboxPolicy(network=False)) adds --unshare-net, which also separates abstract unix sockets. Sub-agents and pipeline stages inherit it and can never re-enable it. Without bwrap the policy cannot be enforced, so bash refuses to run (fail closed) instead of running un-isolated. web fetch runs in the harness process and is not affected by --no-network.
6. web fetch SSRF guard¶
web fetch resolves the host (and every redirect hop) and refuses non-public addresses: loopback, RFC1918, link-local, and cloud metadata (169.254.169.254). Set TETHER_WEB_ALLOW_PRIVATE=1 to fetch local services on purpose.
Configuring SandboxPolicy in Python¶
from pathlib import Path
from tether import Harness, SandboxPolicy
policy = SandboxPolicy(
profile="read-only",
workspace_root=Path("./my_repo"),
timeout_seconds=15.0,
max_output_chars=8000,
network=False, # needs bwrap; sub-agents can only keep or narrow it
)
harness = Harness(
model="ollama/qwen3.5:9b",
tools=["bash", "edit"],
sandbox=policy,
)
Example: awk/sed programs that start with /¶
The command guard (the non-bwrap fallback check on bash commands) treats tokens starting with / as absolute paths. Since v0.1.4 the program or pattern argument of awk, sed, grep, rg and perl (and /regex{...} tokens given to them) is not treated as a path, so awk '/Módulo/ {n++}' plan.md and sed -n '/^## M/p' plan.md work in bash and in verify: gates. File operands (sed -n p /etc/hosts, awk -f /etc/passwd) are still checked (#31).
Known limits¶
- Network is on unless you pass
--no-network; with it on,curlcan reach localhost services and the internet. - There is no process-count limit inside
bwrap(a fork bomb is only stopped by the timeout). - Prompt injection still steers the model; the envelope only limits what an injected command can reach.