
Hybrid Sandbox — how to prevent an AI from blowing up your system
The problem
The TatuEngine has an autopoietic agent with ToolUse — it can execute code, read files, write results. If the agent goes rogue (or hallucinates), it could try:
../../etc/passwd— classic path traversal- Credential theft (SSH keys, tokens)
- Kernel access
- Writing to system directories — persistence
I needed a system that lets the agent work but blocks everything dangerous, without depending on Docker containers or VMs.
The solution: Hybrid Sandbox, pure Python, 3 isolation zones, 51 tests passing.
The 3 zones
The sandbox defines 3 security zones:
| Zone | Read | Write | Max size |
|---|---|---|---|
| SAFE | project directories | 500 MB | |
| RESTRICTED | home + data drives | 100 MB | |
| DENIED | .ssh/*, .git/*, *.key, *.pem, .env*, /etc, /sys |
— |
The intelligence is in the hierarchical matching: a path in the project directory lands in SAFE (read + write), but an SSH key lands in DENIED even though it’s inside the home.
Path traversal detection
The sandbox detects 3 attack vectors:
# Classic path traversal
if ".." in path.split(os.sep):
return SandboxResult.err(f"Path traversal detected: {path}")
# URL-encoded traversal
if "%2e" in path.lower():
return SandboxResult.err(f"Encoded path traversal: {path}")
# Null byte
if "\x00" in path:
return SandboxResult.err("Path contains null byte")
The resolver uses Path.resolve() (which follows symlinks and resolves ..), then checks whether the absolute path starts with an allowed prefix:
def _is_safe_prefix(self, resolved: Path) -> bool:
r_str = str(resolved)
for raw in self._safe_prefixes:
expanded = str(Path(raw).expanduser())
if r_str == expanded or r_str.startswith(expanded + "/"):
return True
return False
This prevents the trick of creating a symlink pointing outside the allowed zone and accessing through it — resolve() follows the symlink and reveals the real path.
System DENIED zones
Critical paths are blocked even if not in the pattern list: /etc, /sys, /proc, /boot, /bin, /sbin, /lib. /usr only allows /usr/share, /var only /var/log.
Write: SAFE only
check_write() only allows writes in SAFE prefixes:
if allow_write:
if self.level == "restricted":
return SandboxResult.err(
f"Write in RESTRICTED sandbox not allowed: {resolved}")
if not in_safe and not in_restricted:
return SandboxResult.err(f"Write outside SAFE: {resolved}")
This means the agent can create files in project directories (SAFE), but cannot modify credentials (DENIED) or write to the data drives (RESTRICTED).
Tests
The sandbox has 439 lines of tests. It tests:
- Read in SAFE
- Write in SAFE
- Path traversal (
../etc/passwd) - URL-encoded path traversal (
%2e%2e) - Null byte in path
- DENIED reads (
.git/config, SSH keys) - Write in RESTRICTED
- Symlink pointing outside
- Path too long (>4096)
- Directory search
- Nested files in subdirectories
What I learned
Path.resolve()is the key — without it, symlinks become backdoors. With it, any attempt to bypass the prefix is neutralized.- 3 zones > 2 zones — SAFE/RESTRICTED/DENIED is more useful than just allowed/blocked. The agent can read system configs (RESTRICTED) without being able to modify them.
- Hierarchical pattern matching — the project directory is SAFE, but
.sshis DENIED even inside home. Order matters: SAFE > RESTRICTED > DENIED. - Zero containers — you don’t need Docker or a VM to isolate an AI. Pure Python +
Path.resolve()+ pattern matching handles 99% of cases.