Skip to content

About

Experimental prototype — unaudited and unreviewed. Runs an agentic coding tool in an isolated, per-project box: declarative config, container or VM isolation, host-side secret masking, default-deny egress. Treat the isolation as design intent, not a guarantee; don't trust it with real secrets.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

agentbox

Warning

Experimental — not audited, and not for security-sensitive environments. agentbox has not been independently reviewed, and it is under active development. Everything below about isolation, secret masking, and egress control describes design intent, not a guarantee. Do not rely on agentbox to contain untrusted or malicious code, to protect real secrets, or for anything where a sandbox escape would actually matter. See docs/security.md for the threat model and its limits.

Run an agentic coding tool inside an isolated, per-project environment — declarative config, default-deny egress, host-side secret masking, one disposable box per project.

$ cd ~/src/myapp
$ agentbox claude

That resolves the workspace root, merges configuration, selects a backend satisfying the project's declared isolation floor, builds the project's box image, constructs a masked view of the workspace that hides secrets, starts a default-deny egress proxy, and execs the agent with the working directory mapped to match your shell's.

What this buys you: the blast radius of an agent mistake is reduced to one workspace tree and one egress allowlist, both inspectable beforehand and logged after. That is not the same claim as "safe" — read docs/security.md before running anything unattended.

Why

Coding agents want to read your tree, install packages, and talk to the network. Running them bare means one prompt injection or one confused agent can read ~/.ssh, your .env, or push to the wrong remote. Running them in an ad-hoc container means hand-rolling mounts, network rules, and secret handling per project — and getting it subtly wrong.

agentbox makes the confined path the zero-config path:

  • Zero-setup launch. Works in any directory; a committed agentbox.toml makes it reproducible for the whole team.
  • Default-deny egress you wrote yourself. The guest has no route to the outside; the only path is a proxy enforcing an allowlist. There are no curated bundles: agentbox does not guess which domains a third-party tool needs, so the list is exactly what your agentbox.toml says. agentbox logs --denied shows what was refused, which is how you build it.
  • Secret masking, enforced outside the box. .agentignore patterns (gitignore syntax, differentially tested against git check-ignore) hide matched files from the guest — reads return empty, writes never reach the host.
  • One box per project directory. cd into a project and you are in its box: nothing to name, nothing to list, nothing to pick. One box takes as many concurrent sessions as you start — run agents in parallel in the same box by running agentbox in as many terminals as you like. Want a second box, with its own filesystem and its own branch? Make a second directory: git worktree add ../feature gets one for free.
  • Honest about what it guarantees. agentbox status always names the backend, isolation tier, egress mode and mask mode in force, and every invocation that starts a box prints the same line.
  • Your image, not agentbox's. agentbox init writes an agentbox.dockerfile your team owns and edits — the toolchain, the agent, whatever this project actually builds with. agentbox installs none of it: no [toolchains], no [agents], nothing to keep up with and nothing that breaks the day you change base image. Already have an image? Point [image].ref at it. With neither, agentbox builds a baseline that has an agent user and a shell and stops there.
  • Nothing assumed. No defaults layer: every value agentbox acts on is in a file you own, and a required key that is missing is an error naming it rather than a number agentbox picked. agentbox init writes a complete config once, and after that the file is the whole story.
  • Any agent. Nothing in the data model is vendor-specific: an agent is a command you install in your image and a set of endpoints you allow.

Quickstart

$ git clone https://github.com/sblmnl/agentbox && cd agentbox && sudo make install

$ cd ~/src/myapp
$ agentbox init                  # scaffold agentbox.toml + agentbox.dockerfile + .agentignore
$ $EDITOR agentbox.dockerfile    # add your toolchain and your agent
$ agentbox up                    # build the image and start the box
$ agentbox claude                # run your agent in the box

Useful from day one:

$ agentbox status                # backend, tier, egress, masking, config drift
$ agentbox logs --denied         # what did the agent try to reach?
$ agentbox down                  # stop and remove the box; keep its home
$ agentbox rm                    # remove the box and everything it holds

Any command that is not one of those six runs inside the box, so agentbox claude, agentbox make test and agentbox bash -l all work. Use -- for a command whose name collides: agentbox -- status.

down and rm differ in what survives. down removes the guest and its networks but keeps the box's persistent home and identity, so agentbox brings back the same box. rm removes all of it — home, state, and the image agentbox built for it. An image you pinned yourself with [image].ref is never removed: agentbox reclaims only what it made.

Requirements: Linux, Docker or rootless Podman for the container tier, Docker + a Kata runtime + KVM for the vm tier, git, and Go to build. A single static binary with one dependency (BurntSushi/toml). agentbox is build-from-source only — there are no published binaries or packages. Build steps and runtime setup for each tier are in docs/install.md.

Configuration

agentbox.toml is committed, reviewable, and layered (user → workspace), with static validation — unknown keys and backend-scoped keys in the wrong place are hard errors on every machine, not just where they would apply. There is no defaults layer beneath those two and no flag layer above them: a required key nobody set is an error listing every key that is missing, and agentbox init writes a complete file to start from. Every value the box runs under is in a file somebody can review.

[image]
dockerfile = "agentbox.dockerfile"   # yours; `agentbox init` writes a baseline

[network]
mode    = "proxy"
allow   = ["api.anthropic.com", "github.com", "registry.npmjs.org"]

[security]
min_isolation = "container"

Full reference: docs/configuration.md / agentbox.toml(5). Copy-paste starting points for common stacks (Node, Python, Go, .NET, monorepos, air-gapped, VM tier, prebuilt images) are in docs/configuration-examples.md.

The image is the project's

There is no list of toolchains or agents agentbox knows how to install. Such a list is a treadmill: every entry assumes a base image, a package manager and an upstream layout that the next project changes. What a box contains is a Dockerfile question, so it is answered in a Dockerfile:

$ agentbox init            # writes agentbox.dockerfile: the baseline, annotated
$ $EDITOR agentbox.dockerfile
$ agentbox --recreate --rebuild

agentbox builds that file, keyed by its contents — edit it and the next invocation rebuilds. Three things are asked of whatever image you point it at: an agent user at the AGENTBOX_UID/AGENTBOX_GID build args agentbox passes, nothing you need kept under /home/agent (a persistent volume shadows it), and whatever your agent needs on PATH. The scaffolded file states them at the top, and [image].ref skips the build entirely for an image you already publish.

A committed config outranks yours, and says so

The workspace layer sits above your user config, so cloning a repository and running agentbox applies configuration you did not write. agentbox lets it win — that is what layering means — and then refuses to be quiet about it. Any workspace value that weakens a security-relevant key relative to a lower layer produces a warning on every invocation, naming the key, both values, and the file responsible:

$ agentbox
warning: network.mode: /src/myproject/agentbox.toml lowered this from "proxy"
  to "open" (~/.config/agentbox/config.toml set "proxy"); the workspace value
  is in effect
agentbox: backend vm (tier vm) | egress open | masking filter, 3 path(s) hidden

There is no flag that silences this, because there is no flag that reaches the key: if you disagree with a repository's config, edit your own layer or the repository's. Tightening is silent, because a project asking for a stronger box is a project config doing its job. The keys ranked this way are security.min_isolation, network.mode, security.mask_mode, workspace.readonly, security.vm.guest_root, security.vm.nested_docker, and the container hardening booleans. Egress allowlists are not ranked — a repo declaring the registries it needs is the normal case.

Security model, honestly

Two isolation tiers exist behind one contract: container (Docker or rootless Podman) and vm (Kata Containers via Docker, on KVM). They are not equivalent, and agentbox is built around never letting that difference hide:

  • A project declares the weakest tier it accepts (min_isolation); if no available backend meets it, agentbox exits 69 naming what is missing. No silent downgrade, no warned downgrade, and no override — there is no flag that lowers the floor. Change the config or fix the host.
  • Backend-specific settings are scoped ([security.container], [security.vm]) and statically validated everywhere.
  • status, and the banner printed whenever a box starts, always show the backend and tier in force.

Universal refusals: no host networking, no host PID namespace, no mounted container-runtime socket, no unconfined seccomp. Boxes cannot reach each other's network, state, or mask views.

The full property table, threat model, and documented limitations of masking are in docs/security.md and agentbox-security(7). The one-paragraph version: in scope — careless or prompt-injected agents, malicious install scripts, accidental destruction outside the workspace; out of scope — kernel/runtime escape under the container tier, hypervisor escape, determined targeted attackers.

Status

agentbox has never been released. Nothing is published, there is no version number that means anything, and there is no compatibility promise: the CLI and the config schema change whenever a better shape turns up. Do not build anything on top of this yet.

Area Status
CLI contract: dispatch, --, sysexits, signal forwarding, verbatim exit codes ✅
Identity: workspace detection, one box per workspace root ✅
Config: 2 layers, static backend-scope validation, loud weakening ✅
Isolation floor: exit 69, no downgrade, no override ✅
Image: the project's own Dockerfile or [image].ref, builds keyed on file + build args ✅
Config: no defaults layer; required keys reported together, init writes them ✅
Masking: shared gitignore matcher, per-box Layer-0 plan, mask_mode ✅
Network: off/proxy/open, project-written allowlist, deny-first, CONNECT restricted to 443, generated squid config ✅
State: XDG layout, atomic metadata, per-box locking, per-box homes ✅
Container backend: Docker + rootless Podman, proxy sidecar, no-gateway network ✅
VM backend: Kata via Docker, same sidecar topology, host-side mask view ✅
Layer-3 lookup-time filtering (mask_mode = "filter", own share daemon) ✅
Teardown: down keeps the home, rm reclaims only what agentbox built ✅
Man pages ✅
Podman for the vm tier 🚧 blocked upstream (see deviations)
Proxy credential injection 🚧 planned

Deviations, honestly stated:

  • The egress allowlist constrains who, not what, and its edges are coarse. A CONNECT proxy hands over an opaque TLS tunnel once the target is allowed, so any hostname sharing a front with an allowed domain is reachable (verified: a tunnel to raw.githubusercontent.com carrying SNI for gist.githubusercontent.com is answered). And an allowed destination is a place data can go — github.com allowed means a repo push is a permitted request. Prefer exact hostnames to wildcards, and read docs/security.md.
  • The host itself is not behind the proxy on older engines. An --internal bridge removes the route out but still carries the host's address, so anything bound to 0.0.0.0 on the host answers from inside a proxy-mode box without passing the proxy (verified with a plain HTTP server). agentbox creates the bridge with gateway_mode_ipv4=isolated, which removes the host address entirely; engines older than Docker 28 do not support it, and there the box is created anyway with a warning on every invocation.
  • The vm tier is Docker + Kata, and only that. Podman's VM path is libkrun, whose crun handler implements no exec callback: a box created under it comes up and no agentbox command can ever enter it (containers/crun#2090). Rather than offer a runtime that parses, selects, creates a box and then cannot be used, agentbox does not offer it — and says so when podman is the only thing installed. There is likewise no hypervisor or runtime configuration key: offering a choice agentbox cannot honor produces exactly that failure.
  • A Kata guest cannot use Docker's embedded DNS. The resolver lives at 127.0.0.11 inside the network namespace, and a VM guest's loopback is its own, so container names do not resolve. The vm tier therefore addresses its proxy sidecar by a pinned IP, and network = "open" bind-mounts a generated /etc/resolv.conf carrying the host's real upstream nameservers. (--dns does not help: on a user-defined network the engine still writes 127.0.0.11 and merely redirects the embedded resolver's upstream.) In proxy mode the guest needs no resolver at all — squid resolves on its behalf, which also denies the box a DNS channel of its own.
  • The Layer-3 filtered share is delivered by interposing agentbox's own FUSE filesystem underneath the runtime's virtiofsd (virtiofsd serves the filtered mount), not by replacing virtiofsd via Kata's virtio_fs_daemon, the more obvious integration point. The enforcement point is identical — host-side, outside guest reach — and the seam does not depend on runtime-specific daemon-path configuration.
  • The filtered share needs /dev/fuse and a mount route: agentbox running as root, or the setuid fusermount3 helper with user_allow_other set in /etc/fuse.conf, which lets the share daemon run entirely in the user's context. Where no route exists, mask_mode = "auto" resolves to view with a warning naming the reason, and an explicit "filter" is an error. No silent downgrade occurs in either direction.
  • The vm tier's host-side mask view needs agentbox to run with CAP_SYS_ADMIN; without it, mask mounts are expressed in the guest and agentbox warns that a guest root process could unmount them.
  • A committed workspace config can weaken your security settings; agentbox warns on every invocation rather than refusing. See "A committed config outranks yours" above.
  • VM conformance tests need KVM plus a Kata runtime and run as a separate CI job that fails if the suite skips; they are not part of go test ./... on an ordinary host.
  • Container-backend end-to-end tests need a Docker daemon and are not part of go test ./...; the policy, plan, and state logic they exercise is covered at the unit level.

Building and testing

$ go build ./cmd/agentbox        # Go 1.26, one dependency
$ go test ./...
$ make all                       # fmt-check + vet + build + test

The ignore-matcher conformance suite differentially tests against git check-ignore and needs git on PATH. The real-mount Layer-0 test requires root or CAP_SYS_ADMIN and skips visibly elsewhere; CI runs it in a privileged job.

Contributing

See CONTRIBUTING.md. Security reports: see SECURITY.md — please do not open public issues for vulnerabilities.

Documented behavior is the contract; behavior changes should update the docs, and divergences belong in the "deviations" list above, not in silence.

License

The Unlicense — this software is released into the public domain.

About

Experimental prototype — unaudited and unreviewed. Runs an agentic coding tool in an isolated, per-project box: declarative config, container or VM isolation, host-side secret masking, default-deny egress. Treat the isolation as design intent, not a guarantee; don't trust it with real secrets.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages