{"id":51051642,"url":"https://github.com/bugthesystem/agentjail","last_synced_at":"2026-06-22T17:32:36.012Z","repository":{"id":349988385,"uuid":"1204750632","full_name":"bugthesystem/agentjail","owner":"bugthesystem","description":"Minimal Linux sandboxes for running untrusted code. Built for AI agents, build systems, and any scenario where you need to execute code you didn't write.","archived":false,"fork":false,"pushed_at":"2026-04-28T16:00:59.000Z","size":2235,"stargazers_count":21,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-09T13:33:43.576Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bugthesystem.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-08T09:46:58.000Z","updated_at":"2026-05-04T11:10:28.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/bugthesystem/agentjail","commit_stats":null,"previous_names":["bugthesystem/agentjail"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/bugthesystem/agentjail","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bugthesystem%2Fagentjail","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bugthesystem%2Fagentjail/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bugthesystem%2Fagentjail/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bugthesystem%2Fagentjail/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bugthesystem","download_url":"https://codeload.github.com/bugthesystem/agentjail/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bugthesystem%2Fagentjail/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34659896,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-22T02:00:06.391Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-22T17:32:35.327Z","updated_at":"2026-06-22T17:32:36.006Z","avatar_url":"https://github.com/bugthesystem.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"logo.svg\" width=\"100\" height=\"100\" alt=\"agentjail\"\u003e\n\u003c/p\u003e\n\n\u003ch1 align=\"center\"\u003eagentjail\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003eMinimal Linux sandboxes for running untrusted code.\u003c/p\u003e\n\n## Why\n\nModel-generated code, `npm install` on a fresh repo, a PR's test\nsuite, an MCP tool the agent picked — all run as you, on your\nkernel. They can read `~/.ssh`, fork-bomb the host, dial any IP,\nspawn a reverse shell. Docker isn't a sandbox. `nobody` isn't a\nsandbox. agentjail is.\n\nOne jail is one child process inside fresh Linux namespaces,\npivot-rooted into a minimal rootfs, seccomp-filtered, cgroup-limited,\negress-allowlisted. No VM. No daemon. No setuid helper.\n\n\u003e Beta. Core crate (`crates/agentjail`) is the load-bearing piece,\n\u003e covered by `make test-rust-privileged`. Control plane, SDKs, web\n\u003e UI, gateway are useful but APIs may move before 1.0.\n\n## Isolation\n\n- **Namespaces** — mount, network, IPC, PID; user optional.\n- **Filesystem** — `pivot_root` to a 128-bit-random tmp root;\n  old root `umount2(MNT_DETACH)`-ed. Bind `/bin /lib /usr` ro.\n  tmpfs `/etc` with the bare minimum for dynamic linking + DNS.\n  Landlock on Linux ≥ 5.13 (hard-fail if requested on older kernels).\n- **Network** — `None`, `Loopback`, or `Allowlist(domains)`. Allowlist\n  routes through an in-process HTTP CONNECT proxy: resolves the host\n  once, rejects private/link-local/loopback/CGNAT, dials the IP not\n  the hostname (closes DNS rebinding). Veth via netlink, no `ip` binary.\n- **Syscalls** — seccomp-BPF blocklist (`Standard`/`Strict`).\n  Blocks namespace, mount, module, keyring, BPF, perf, io_uring,\n  `chroot`, `name_to_handle_at`, `ptrace`, `personality`, `clone3`,\n  `mount_setattr`, `memfd_create`, `fanotify_init`, `quotactl`,\n  `syslog`. Arg-filters `ioctl(*, TIOCSTI, …)` and\n  `socket(AF_NETLINK|AF_PACKET|AF_VSOCK, …)`.\n- **Privileges** — `PR_SET_NO_NEW_PRIVS`, `close_range(3, ~0, CLOEXEC)`\n  before exec, full bounding-set drop, `SECBIT_NOROOT_LOCKED |\n  SECBIT_NO_SETUID_FIXUP_LOCKED`, `capset` zeroing every effective /\n  permitted / inheritable cap in the grandchild after `/proc` is\n  remounted in the new PID namespace.\n- **Resources** — memory, CPU, PIDs, disk I/O via cgroup v2. Barrier\n  pipe: child blocks until the parent has assigned the cgroup, so\n  there's no unconstrained startup window.\n\n## Requirements\n\n- Linux ≥ 5.13, cgroup v2, user namespaces.\n- Rust 1.85+ (edition 2024).\n- `CAP_NET_ADMIN` — Allowlist mode only (veth + netlink).\n\n## Use\n\n```toml\n[dependencies]\nagentjail = \"0.1\"\ntokio = { version = \"1\", features = [\"rt\", \"macros\"] }\n```\n\n```rust\nuse agentjail::{Jail, preset_build};\n\n#[tokio::main]\nasync fn main() -\u003e anyhow::Result\u003c()\u003e {\n    let jail = Jail::new(preset_build(\"./src\", \"./out\"))?;\n    let out  = jail.run(\"npm\", \u0026[\"run\", \"build\"]).await?;\n    println!(\"exit={} oom={}\", out.exit_code, out.oom_killed);\n    Ok(())\n}\n```\n\n### Presets\n\n| Preset | Network | Memory | Timeout |\n|---|---|---|---|\n| `preset_build`   | None       | 512 MB | 600 s |\n| `preset_install` | Allowlist  | 512 MB | 600 s |\n| `preset_agent`   | None       | 256 MB | 300 s |\n| `preset_gpu`     | None       | 8 GB   | 3600 s |\n| `preset_dev`     | Loopback   | 1 GB   | 3600 s |\n\n`preset_install` requires explicit domains:\n\n```rust\npreset_install(\"./src\", \"./out\", vec![\n    \"registry.npmjs.org\".into(),\n    \"registry.yarnpkg.com\".into(),\n])\n```\n\n### Config\n\n```rust\nuse agentjail::{Jail, JailConfig, Network, SeccompLevel};\n\nlet jail = Jail::new(JailConfig {\n    source:        \"/code\".into(),       // ro at /workspace\n    output:        \"/artifacts\".into(),  // rw at /output\n    network:       Network::None,\n    seccomp:       SeccompLevel::Standard,\n    memory_mb:     512,\n    cpu_percent:   100,                  // 100 = 1 core\n    max_pids:      64,\n    io_read_mbps:  100,\n    io_write_mbps: 50,\n    timeout_secs:  300,\n    ..Default::default()\n})?;\n```\n\n### Network\n\n```rust\nNetwork::Allowlist(vec![\n    \"api.anthropic.com\".into(),\n    \"registry.npmjs.org\".into(),\n    \"*.mcp.example.com\".into(),\n])\n```\n\nHostname checked against the allowlist, resolved, every\nprivate/loopback/link-local/CGNAT/test-net IP filtered, connect to\nwhat's left. TLS passes through (HTTPS, SSE, WebSocket).\n\n### GPU\n\nTrusted workloads only — exposes the NVIDIA kernel-driver attack\nsurface.\n\n```rust\nJailConfig { gpu: GpuConfig { enabled: true, devices: vec![0] },\n             ..Default::default() }\n```\n\n### Resource monitoring\n\n```rust\nlet handle = jail.spawn(\"npm\", \u0026[\"run\", \"build\"])?;\nif let Some(s) = handle.stats() {\n    println!(\"mem {} / peak {} MB  pids {}\",\n        s.memory_current_bytes / 1_048_576,\n        s.memory_peak_bytes    / 1_048_576,\n        s.pids_current);\n}\nlet out = handle.wait().await?;\nif out.oom_killed { eprintln!(\"OOM\"); }\n```\n\n### Events\n\n```rust\nlet (_handle, mut rx) = jail.spawn_with_events(\"npm\", \u0026[\"run\", \"build\"])?;\nwhile let Some(ev) = rx.recv().await {\n    match ev {\n        JailEvent::Stdout(l)        =\u003e println!(\"{l}\"),\n        JailEvent::Stderr(l)        =\u003e eprintln!(\"{l}\"),\n        JailEvent::OomKilled        =\u003e eprintln!(\"OOM\"),\n        JailEvent::Completed { .. } =\u003e break,\n        _ =\u003e {}\n    }\n}\n```\n\n### Snapshots and live forks\n\nSave the output dir, restart from it later. Or branch a running jail\nwithout pausing it — useful for running N variants of an agent off\none warm state.\n\n```rust\nlet snap = Snapshot::create(\u0026output, \u0026snapshot_dir)?;\nsnap.restore()?;\n\n// FICLONE reflink where supported (btrfs, xfs, ext4-with-reflink);\n// regular copy on tmpfs / cross-filesystem. Freezer pauses the source\n// jail sub-millisecond for the duration of the clone.\nlet handle = jail.spawn(\"python\", \u0026[\"train.py\"])?;\nlet (forked, _info) = jail.live_fork(Some(\u0026handle), \"/tmp/fork-out\")?;\n```\n\nFiles are content-addressed by BLAKE3 in a shared object pool;\nunchanged files (same `size + mtime_ns`) skip rehashing and reuse\nthe prior blob. Restore strips `S_ISUID`/`S_ISGID` and rejects\nmanifest entries with absolute or `..` paths.\n\n## Threat model\n\nEach row links to the regression test that would fail if the\nprotection ever did. Tests live in\n[`crates/agentjail/tests/`](crates/agentjail/tests/).\n\n| Attack | Protection | Test |\n|---|---|---|\n| Read host `~/.ssh` / `~/.aws` | Not mounted | [`test_cannot_read_ssh_keys`](crates/agentjail/tests/security_test.rs) |\n| Read `/etc/shadow`, machine-id | Minimal `/etc` | [`test_etc_shadow_not_accessible`](crates/agentjail/tests/audit_regression_test.rs) |\n| Network exfiltration | Netns + allowlist proxy | [`test_network_none_blocks_external`](crates/agentjail/tests/security_test.rs), [`test_reverse_shell_blocked`](crates/agentjail/tests/security_test.rs) |\n| Fork bomb | PID limit | [`test_pid_limit_blocks_fork_bomb`](crates/agentjail/tests/audit_regression_test.rs) |\n| Memory blow-up | Memory limit + OOM detection | [`test_large_stdout_does_not_oom`](crates/agentjail/tests/audit_regression_test.rs) |\n| Disk thrashing | I/O bandwidth limits | [`test_io_write_bandwidth_limit_enforced`](crates/agentjail/tests/audit_regression_test.rs) |\n| Signal host processes | PID namespace | [`test_pid_namespace_full_sandbox`](crates/agentjail/tests/security_test.rs) |\n| Mount manipulation | `mount` + new mount API blocked | [`seccomp_standard_blocks_documented_syscalls`](crates/agentjail/tests/seccomp_blocklist_test.rs) |\n| `chroot` escape | `pivot_root` + detach; `chroot` seccomp-blocked | [`test_chroot_no_home`](crates/agentjail/tests/security_test.rs) |\n| io_uring bypass | `io_uring_*` blocked | [`seccomp_standard_blocks_documented_syscalls`](crates/agentjail/tests/seccomp_blocklist_test.rs) |\n| Compat-mode escape | `personality()` blocked | [`seccomp_standard_blocks_documented_syscalls`](crates/agentjail/tests/seccomp_blocklist_test.rs) |\n| Namespace escape | `clone3`, `unshare`, `setns` blocked | [`test_seccomp_blocks_unshare`](crates/agentjail/tests/audit_regression_test.rs) |\n| BPF / perf | `bpf`, `perf_event_open`, `userfaultfd` blocked | [`test_seccomp_blocks_bpf`](crates/agentjail/tests/audit_regression_test.rs) |\n| Executable memory | `memfd_create` blocked | [`seccomp_standard_blocks_documented_syscalls`](crates/agentjail/tests/seccomp_blocklist_test.rs) |\n| Write + exec on `/tmp` | `NOEXEC` | [`test_tmp_noexec`](crates/agentjail/tests/audit_regression_test.rs) |\n| Setuid escalation | `PR_SET_NO_NEW_PRIVS` | — |\n| Core-dump leak | `RLIMIT_CORE=0` | [`test_rlimit_core_disabled`](crates/agentjail/tests/audit_regression_test.rs) |\n| Parent stdout OOM | Output capped at 256 MiB / stream | [`test_large_stdout_does_not_oom`](crates/agentjail/tests/audit_regression_test.rs) |\n| FD exhaustion | `RLIMIT_NOFILE` at 4096 | [`test_fd_limit_enforced`](crates/agentjail/tests/audit_regression_test.rs) |\n| Symlink traversal | Skipped in snapshots, forks, cleanup | [`test_snapshot_restore_does_not_follow_symlinks`](crates/agentjail/tests/audit_regression_test.rs) |\n| Zombie / fd leak | `PR_SET_PDEATHSIG` + Drop kills+reaps | [`test_no_zombie_after_drop`](crates/agentjail/tests/audit_regression_test.rs) |\n| Cross-tenant read | `tenant_id` on every row; list filters, get 404s | [`operator_cannot_read_other_tenants_workspace_by_id`](crates/agentjail-ctl/tests/api.rs), [`credentials_are_tenant_scoped`](crates/agentjail-ctl/tests/api.rs) |\n| Token spent on another tenant's bill | `TokenRecord.tenant_id`; proxy looks up `keys.get(tenant, service)` | [`agentjail-phantom`](crates/agentjail-phantom/src/proxy.rs) |\n| Malicious `.gitmodules` / `core.sshCommand` | Clone-jail: strict-ish seccomp, allowlist net, no host access | [`clone_jail_clones_a_small_public_repo`](crates/agentjail-ctl/tests/clone_jail_test.rs) |\n| Operator reads platform internals via `GET /v1/config` | Admin-only fields omitted for operators | [`settings_bind_addrs_hidden_from_operators`](crates/agentjail-ctl/tests/api.rs) |\n| Snapshot rehydrate spoofing | Requires `parent_workspace_id`, verified | [`from_snapshot_requires_and_checks_parent_workspace_id`](crates/agentjail-ctl/tests/api.rs) |\n\n## Limits\n\n- Linux only. Not a VM — a kernel exploit escapes. For stronger\n  isolation, pair with [gVisor](https://gvisor.dev) or run inside\n  [Firecracker](https://firecracker-microvm.github.io).\n- GPU mode widens the attack surface to the NVIDIA driver.\n- Allowlist mode costs one veth pair per concurrent jail. Stale\n  interfaces are reaped at `agentjail-server` startup.\n\n## Control plane\n\nOptional. The library is enough for one process. The server is for\nwhen you have many: shared upstream credentials, a workspace ledger,\nsnapshots, an SSE feed of every API call, a UI.\n\n```bash\n# token@tenant:role  — every component required, no defaults.\nexport AGENTJAIL_API_KEY=\"\\\n  ak_ops@platform:admin,\\\n  ak_acme_alice@acme:operator,\\\n  ak_globex_ops@globex:operator\"\ndocker compose -f docker-compose.platform.yml up --build\n# UI:  http://localhost:3000/t/\u003ctenant\u003e\n# API: http://localhost:7000\n```\n\n### Tenancy\n\nEvery workspace, snapshot, session, jail-row, and credential is\nstamped with `tenant_id`. Operators see their own tenant. Admins\nsee all, with `?tenant=\u003cid\u003e` to scope. Cross-tenant id access\nreturns 404, never 403 — the server doesn't reveal whether a row\noutside scope exists. Full key format and DB shape:\n[`docs/tenancy.md`](docs/tenancy.md).\n\n### Phantom credentials\n\nSandboxes never see real upstream keys. Sessions hand out phantom\ntokens (`phm_\u003chex\u003e`) plus `*_BASE_URL` env vars pointing at the\nproxy; the proxy swaps the token for the real key on the way out.\nPer-tenant: a token minted for tenant A can't spend tenant B's\ncredentials even if the service matches.\n\n### Flavors\n\nRuntime \"flavors\" (`nodejs`, `python`, `bun`, …) are host\ndirectories under `$state_dir/flavors/\u003cname\u003e/`, bind-mounted ro\ninto each jail at `/opt/flavors/\u003cname\u003e/`, with `bin/` prepended to\n`PATH`. Adding `deno` is a `mkdir`, not a code change.\n\n```json\nPOST /v1/workspaces\n{ \"flavors\": [\"nodejs\", \"python\"] }\n```\n\n`GET /v1/flavors` lists names (host paths stay admin-internal).\nSee [`docs/flavors.md`](docs/flavors.md).\n\n### Clone-jail\n\n`git clone` runs in its own short-lived jail by default — strict\nseccomp, network allowlist pinned to the repo host, 60 s timeout,\nno host access. A malicious `.gitmodules` or `core.sshCommand`\ncan't reach anything off the target dir. Opt out on restricted\ncontainer runtimes:\n\n```bash\nexport AGENTJAIL_CLONE_MODE=host   # default: jail\n```\n\n### Surface\n\n- Identity — `GET /v1/whoami` · `GET /v1/flavors`\n- Credentials (per-tenant) — `POST` · `GET` · `DELETE /v1/credentials/:service` (admins: `?tenant=\u003cid\u003e`)\n- Sessions — `POST /v1/sessions` · `POST /v1/sessions/:id/exec`\n- Runs — `POST /v1/runs` · `/fork` · `/stream`\n- Workspaces — `POST /v1/workspaces` · `/fork` · `/exec` · `PATCH` · `POST /v1/workspaces/:id/snapshot` · `POST /v1/workspaces/from-snapshot` (requires `parent_workspace_id`)\n- Lists (tenant-filtered) — `GET /v1/{workspaces,snapshots,sessions,jails,audit}`\n- Detail — `GET /v1/snapshots/:id/manifest` · `GET /v1/jails/:id` · `GET /v1/config` (bind-addrs + state_dir admin-only)\n\n### Web UI\n\n![control plane](media/control-plane.png)\n\nReact 19 + Vite + Tailwind. Pages live at `/t/:tenant/...` so the\nactive tenant is bookmarkable. Pages: Dashboard, Projects, API\nSessions, Integrations, Playground, Docs. Operator tools behind\n`Advanced`: Execution Ledger, Snapshots, API Audit, Accounts,\nSystem Settings.\n\n### SDKs\n\nNode ([`@agentjail/sdk`](packages/sdk-node/README.md), zero deps,\nNode ≥ 18) and Python ([`agentjail`](packages/sdk-python/README.md),\n≥ 3.10, depends on `httpx`). Symmetrical surface.\n\n```ts\nimport { Agentjail } from \"@agentjail/sdk\";\n\nconst aj = new Agentjail({\n  baseUrl: \"http://localhost:7000\",\n  apiKey:  process.env.AGENTJAIL_API_KEY!,\n});\n\nawait aj.credentials.put({ service: \"openai\", secret: process.env.OPENAI_API_KEY! });\n\nfor await (const ev of aj.runs.stream({ code, language: \"python\" })) {\n  if (ev.type === \"stdout\") process.stdout.write(ev.line + \"\\n\");\n}\n\nconst session = await aj.sessions.create({\n  services: [\"openai\", \"github\"],\n  scopes:   { github: [\"/repos/my-org/*\"] },\n  ttlSecs:  600,\n});\nspawn(\"node\", [\"agent.js\"], { env: { ...process.env, ...session.env } });\n```\n\n## Build and test\n\n```bash\nmake test-rust                     # low-priv unit slice (Docker)\nmake test-rust-privileged          # full security suite (--privileged)\nmake test-rust-privileged-clone    # end-to-end clone-jail + workspace-exec\n( cd packages/sdk-node    \u0026\u0026 npm test )\n( cd packages/sdk-python  \u0026\u0026 pytest )\n( cd web \u0026\u0026 npm run build )\n```\n\nGPU tests need an NVIDIA GPU + the\n[Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html):\n\n```bash\ndocker compose run --rm gpu cargo test --test gpu_test -- --nocapture\n```\n\n## License\n\nMIT. See [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbugthesystem%2Fagentjail","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbugthesystem%2Fagentjail","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbugthesystem%2Fagentjail/lists"}