// product 01
umwelt
available
A hardened sandbox for LLM agents.
What it is
Letting an LLM run shell commands on your machine is an architectural choice, not a detail. umwelt is the boundary for it: one podman container per project, closed by default.
Inside, the agent sees its workspace and a handful of read-only mounts — nothing else, and no network. When it needs more it asks; you decide; the wall moves one grant at a time, and every grant is logged.
The model is your choice. umwelt is model- and LLM-agnostic; the LLM server runs separately.
The floor
| network | none by default |
|---|---|
| root filesystem | read-only |
| identity | container uid = your uid (userns=keep-id) |
| privileges | no-new-privileges |
| resources | memory / cpus / pids, always capped |
What it does
- Host actions run from a policy the agent cannot edit. When the agent needs to touch a host service — validate an nginx drop-in, reload it — it may request one named action at a time. The allowlist is root-owned at /etc/umwelt/host-actions.toml: never in the repo, never in the workspace the agent can write. An action is a fixed command with no shell and no free-form arguments, ask-gated, timeout-bounded and audited. The approval is a courtesy; the policy is the boundary.
- The same session can drive a sandbox on another host. Pass host = "app-server" to any acting tool and the call runs in that server’s sandbox instead of the local one, over a single ssh channel reused per server. Setup adds one authorized_keys line for a dedicated key whose only power is the umwelt relay — a forced command, forwarding denied. The container runs as that host’s own user, the server’s own floor still applies, and every remote call is audited locally.
- Egress is off until a project asks, and then it is enforced twice. No network at all is the default, and umwelt refuses to start an egress sandbox whose prerequisites are missing rather than weaken the rules. Once a project opts in, a kernel nftables ruleset — default-drop, fixed at container create, with no CAP_NET_ADMIN and no nft in the sandbox to change it — bounds the network, and a live gateway applies host-and-path policy, so an API rule can be scoped to /repos/<user>/* instead of the whole host. Every request is logged.
- Grants use the host’s own paths. A granted path is the same absolute path inside the container as on the host, so host tooling and the sandbox never disagree about where a file is. A source outside grant_roots is rejected outright, file grants are always read-only, and a rw grant nested inside a ro one makes exactly that subtree writable.
- A dev server is reachable without leaving the machine. Ingress publishes a chosen container port on the host loopback only — never the LAN — at the same port number, so a Vite server inside the sandbox is one local URL away. It grants no outbound access.
- Every widening is logged. Grants, host actions and egress requests are recorded: per-project audit files, an egress log per workspace, and a central notes log for whatever the agent finds surprising.
What it does not protect
- A rw grant is read/write. That is the point. Review each one; prefer ro where reading suffices.
- The agent can prepare its own persisted grants by editing .umwelt.toml in the workspace it can write. The edit is surfaced in the tool result and takes effect on the next control-plane restart — inspect the file after a session.
- Same-user trust boundary: this separates the agent’s processes from the host, not one local user from another.
- A read-only rootfs means no state outside the workspace, a tmpfs and granted paths — so no online package installs unless the project is granted egress.
- HTTPS path rules work because TLS is terminated at the gateway: the sandbox trusts a umwelt CA, and the gateway can read the method, host and path of the requests it allows. It does not inspect or store bodies.
- On SELinux-enforcing hosts the container runs with label=disable so bind grants work; the hardcoded flags carry the posture.
Requirements
- Linux, with rootless podman on the systemd cgroup driver (the default on openSUSE and Fedora; other distributions need configuration).
- Python 3.11+.
- OpenCode v2 as the agent harness — umwelt installs its permission policy and MCP server. The model behind it is your choice.
- The one-time cgroup delegation needs sudo. Without it umwelt refuses to start rather than weakening the caps.
Get it
git clone https://git.sync.wtf/troed/umwelt ~/dev/umwelt bash ~/dev/umwelt/umwelt/install.sh install
Full documentation lives in the repository.