add ai-sandbox: rootless podman sandbox for agentic development

A single bash script that runs a coding harness (pi by default) inside a
rootless Ubuntu 24.04 container with only the current directory mounted at
/project, an explicit egress allowlist and an explicit environment: nothing
is passed in that was not named on the command line or in a config file.

Network enforcement here is empirical rather than assumed
(tests/spike_network.sh, docs/networking.md). The original design -- a sidecar
proxy on an internal network -- does not work rootless on this host, so allow
mode runs pasta with --map-host-loopback, installs an inet aisb ruleset inside
the container that drops every egress path except the gate's, and relays
through a squid gate bound to the host's loopback that resolves names itself
and refuses anything unlisted. The entrypoint checks that the default-drop rule
really exists before setpriv drops to uid 1000 with an empty capability set,
and exits 77/78 rather than run unenforced.

The Containerfile, the entrypoint, the squid config generator and the ruleset
renderer all live in bin/ai-sandbox, and the image is tagged by content hash,
so a policy change can never silently reuse a stale image. Tests are pure
bash: 161 assertions start no container and touch no network, 256 run real
ones, including proof that a raw socket, a stripped proxy env and DNS all stay
blocked while an allowlisted host works over both http and CONNECT.

LLM-Generated: true
This commit is contained in:
2026-10-27 15:11:29 -04:00
commit 03ab9c0cb1
27 changed files with 3426 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
# Egress allowlist -- copy to ~/.config/ai-sandbox/allow (user-wide) or to
# ./.ai-sandbox/allow in a repository (checked in, so the policy is reviewable).
#
# One entry per line. '#' starts a comment. Blank lines are ignored.
# In --net allow mode at least one entry is required; an empty list is an error.
#
# domain matched by the gate, including subdomains, at CONNECT time
# domain:port same, and a direct netfilter rule for that port
# 10.0.0.5:5432 pinned host and port (databases, internal services)
# 10.20.0.0/24 pinned subnet (default ports 80,443 unless :port given)
# [fd00::5]:8443 pinned ipv6
api.anthropic.com
registry.npmjs.org
# typical additions, by kind:
# api.openai.com
# github.com # git push, if you also mount ~/.ssh
# pypi.org
# files.pythonhosted.org
# index.crates.io
# proxy.golang.org