Hybrid Sandbox — how to prevent an AI from blowing up your system
TatuEngine·

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

  1. Path.resolve() is the key — without it, symlinks become backdoors. With it, any attempt to bypass the prefix is neutralized.
  2. 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.
  3. Hierarchical pattern matching — the project directory is SAFE, but .ssh is DENIED even inside home. Order matters: SAFE > RESTRICTED > DENIED.
  4. Zero containers — you don’t need Docker or a VM to isolate an AI. Pure Python + Path.resolve() + pattern matching handles 99% of cases.