Skip to content

Repository files navigation

agent-sandbox

Multi-tenant sandboxed execution for untrusted code — where the escape-attempt suite is the headline feature, not an afterthought.

Anyone can spawn a subprocess with a timeout. The engineering is in making the escape surface small and demonstrably small, and in never quietly providing weaker isolation than the operator asked for. Every control here has a paired negative test that actively tries to defeat it, and the sandbox refuses to run if the host cannot enforce the policy you specified.

$ agsbx doctor
platform          : darwin: process_ceiling, rlimits, sandbox_exec
kernel mediation  : True
memory ceiling    : False
settable rlimits  : core, cpu, fsize, nofile, nproc

ISOLATION GAPS on this host:
  - hard memory ceiling (RLIMIT_AS unsettable, no cgroup v2)

Jobs needing these controls are refused unless --allow-degraded is passed.

That output is the point of the project. macOS defines RLIMIT_AS but its kernel rejects every attempt to set it, so a memory ceiling on Darwin without cgroups is a fiction. Most sandboxes apply the limit, ignore the error, and report success. This one probes what the host actually accepts, refuses the job, and — if you override — records NO-MEMORY-CEILING(rlimit) in the job's permanent record.

Quickstart (60 seconds)

git clone https://github.com/vinzabe/agent-sandbox && cd agent-sandbox
python -m pip install -e ".[dev]"

agsbx doctor                                  # what can this host enforce?
agsbx run -c 'print("hello")'                 # refused if there are gaps
agsbx run --allow-degraded -c 'print("hi")'   # runs, gap recorded on the job
pytest tests/escapes/ -v                      # watch the escape attempts fail

Layered defence

Layer Mechanism Verified by
Policy Declarative limits validated before a process exists test_policy.py
Filesystem Private scratch root; writes confined to the job dir escapes/test_fs_escapes.py
Kernel seccomp allowlist (Linux) / sandbox-exec SBPL (macOS) escapes/test_profile_invariants.py
Resources Probed rlimits; cgroup v2 where present escapes/test_resource_escapes.py
Environment Minimal env; loader-hijack vars refused at construction escapes/test_env_escapes.py
Network Denied by default (netns on Linux, SBPL on macOS) test_profile_invariants.py
Lifecycle Durable job records + a reaper that cannot leak test_service.py

What the escape suite actually attempts

Not "does it run" — does it stay contained:

  • CPU spin → killed by wall clock
  • 512 MiB allocation against a 64 MiB ceiling → contained, or skipped with a stated reason if the host cannot cap memory
  • Fork bomb (2000 forks) → RLIMIT_NPROC refuses or wall clock kills; the host survives
  • 10 MB stdout flood → truncated at read time, flagged output_truncated
  • 64 MiB file write against a 1 MiB RLIMIT_FSIZE → denied
  • 10 000 open file descriptors → bounded by RLIMIT_NOFILE
  • LD_PRELOAD, DYLD_INSERT_LIBRARIES, PYTHONPATH, NODE_OPTIONS, BASH_ENV (12 vars) → rejected at policy construction, not filtered later
  • Write to /etc → denied; read ~/.ssh, ~/Documents, ~/Library/Keychains → denied
  • Child raising its own RLIMIT_AS → refused (hard == soft)
  • A rule table edit that widens the syscall allowlist → caught by an invariant test asserting SECCOMP_ALLOW ∩ SECCOMP_NEVER == ∅

Exit codes (contractual)

Code Meaning
0 Job completed successfully
2 Job ran but failed, or was killed / timed out
3 Host cannot provide the requested isolation — job not run
1 Usage or runtime error

2 and 3 are deliberately distinct: "your code is broken" and "my sandbox is weaker than you asked for" demand completely different responses, and collapsing them into one non-zero code is how degraded isolation goes unnoticed.

Commands

Command Purpose
agsbx run Execute untrusted Python. --wall, --cpu, --memory-mb, --max-procs, --network, --allow-env, --tenant, --allow-degraded, --json
agsbx doctor Report host capabilities and isolation gaps
agsbx recover Reap jobs orphaned by a crash
agsbx jobs List recent jobs with state and confinement
agsbx policy Print the exact confinement a policy requests (syscall allowlist included)

The no-leaked-process guarantee

Two mechanisms, because one is not enough:

  1. Every child gets its own process group (setsid() in preexec_fn) and is killed as a group — SIGTERM, then SIGKILL after a grace period. The finally block runs on timeout, exception, and interpreter shutdown alike.
  2. Jobs are written to SQLite before the process starts. A crash mid-execution leaves a running row, and agsbx recover finds it, kills any surviving group, and marks it reaped. Call it at startup.

An orphaned sandbox surviving a restart is the failure mode that actually bites in production — far more often than a clever syscall escape. It gets two controls and its own tests (test_recover_reaps_orphans, test_recover_is_idempotent).

Operating the tool

  • Call agsbx recover on startup. Without it, a crash leaks a sandbox permanently.
  • Read doctor before trusting a host. Linux with cgroup v2 + seccomp is the only configuration where every advertised control is real.
  • --allow-degraded is recorded, not forgotten. The weakened confinement is stored on the job row, so an audit can find every job that ran under reduced isolation.
  • State is agsbx.db (SQLite, WAL). Back it up; it is your job ledger.
  • Boundaries and non-goals: THREAT_MODEL.md. Read it before pointing this at genuinely hostile input.

Development

python -m pip install -e ".[dev]"
pytest --cov=agsbx          # 91 tests, ~91% coverage
pytest tests/escapes/ -v    # the interesting part
mypy --strict src/agsbx     # clean
ruff check src tests        # clean

License

MIT © vinzabe

About

Multi-tenant sandboxed execution for untrusted code: layered OS isolation, probed resource ceilings, durable job lifecycle with a crash reaper, and an adversarial escape-attempt suite.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages