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:
@@ -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
|
||||
@@ -0,0 +1,67 @@
|
||||
# config for ai-sandbox -- copy to ~/.config/ai-sandbox/config and edit
|
||||
#
|
||||
# Format: key = value. '#' starts a comment.
|
||||
# Precedence: built-in default < this file (user) < ./.ai-sandbox/config (repo)
|
||||
# < AISB_* environment variable < command line flag.
|
||||
#
|
||||
# Keys listed under "repeatable" accumulate; every other key replaces.
|
||||
|
||||
# --- network ---------------------------------------------------------------
|
||||
# none | allow | full. allow is the safe default and requires an allowlist.
|
||||
net = allow
|
||||
|
||||
# Address that stands for the host's loopback inside the container
|
||||
# (--network=pasta:--map-host-loopback,<addr>). Change only if it collides
|
||||
# with a real network you need to reach.
|
||||
# gate_addr = 100.64.0.1
|
||||
|
||||
# Preferred first port for the per-session egress gate; the next free port is
|
||||
# used if it is busy.
|
||||
# gate_port = 18081
|
||||
|
||||
# --- harness ---------------------------------------------------------------
|
||||
# Default in-container command: pi | codex | claude | none | "<your command>"
|
||||
# harness = pi
|
||||
|
||||
# --- mounts and layout -----------------------------------------------------
|
||||
# workdir = /project
|
||||
|
||||
# --- limits ----------------------------------------------------------------
|
||||
# cpus = 4
|
||||
# memory = 4g
|
||||
# pids = 512
|
||||
# tmpfs_size = 1g
|
||||
|
||||
# --- image -----------------------------------------------------------------
|
||||
# base_image = docker.io/library/ubuntu:24.04
|
||||
# node_version = 22.23.4
|
||||
# pi_version = 1.4.0
|
||||
|
||||
# --- behaviour -------------------------------------------------------------
|
||||
# SELinux bind-mount option: ':z' (shared), ':Z' (private) or empty to disable.
|
||||
# Defaults to ':z' when getenforce exists.
|
||||
# selinux = :z
|
||||
|
||||
# Persist /home/sandbox in a named volume so harness logins and tool caches
|
||||
# survive between runs. 0 = use a throwaway tmpfs instead.
|
||||
# home_cache = 1
|
||||
|
||||
# Appended verbatim to the podman invocation (power users; word-split).
|
||||
# extra_args = --annotation io.k8s.description="agent sandbox"
|
||||
|
||||
# --- repeatable keys -------------------------------------------------------
|
||||
# These may appear more than once, and are merged with --allow / --env / etc.
|
||||
#
|
||||
# allow = api.anthropic.com
|
||||
# allow = registry.npmjs.org:443
|
||||
# allow = 10.20.0.5:5432
|
||||
#
|
||||
# env = ANTHROPIC_API_KEY
|
||||
# env = CI_JOB_TOKEN
|
||||
#
|
||||
# mount = ~/.ssh:ro
|
||||
# mount = ../docs:/docs:ro
|
||||
#
|
||||
# install = postgresql-client
|
||||
#
|
||||
# label = team=platform
|
||||
@@ -0,0 +1,11 @@
|
||||
# Copy to a file only you can read, then: ai-sandbox --env-file ~/.config/ai-sandbox/env
|
||||
#
|
||||
# chmod 600 ~/.config/ai-sandbox/env
|
||||
#
|
||||
# One NAME=VALUE per line. '#' comments and blank lines are ignored.
|
||||
# Nothing here is passed to the container unless you ask for it with --env or
|
||||
# --env-file, so listing a variable is not the same as granting it.
|
||||
#
|
||||
# ANTHROPIC_API_KEY=sk-ant-...
|
||||
# OPENAI_API_KEY=sk-...
|
||||
# ANTHROPIC_MODEL=sonnet
|
||||
Reference in New Issue
Block a user