commit b31f3e0291b38c5d09191506c9a882c1a83ff1e3 Author: Christian Nennemann Date: Sun Feb 22 22:19:03 2026 +0100 Initial commit Co-authored-by: Cursor diff --git a/sandbox.sh b/sandbox.sh new file mode 100755 index 0000000..8993fdc --- /dev/null +++ b/sandbox.sh @@ -0,0 +1,396 @@ +#!/usr/bin/env bash +# sandbox.sh - Safe AI coding agent sandbox +# Usage: ./sandbox.sh [options] +# Commands: setup, run, shell, status, clean +set -euo pipefail + +# ── Config ──────────────────────────────────────────────────────────────────── +SANDBOX_IMAGE="${SANDBOX_IMAGE:-ai-sandbox:latest}" +SANDBOX_DIR="${SANDBOX_DIR:-$HOME/.ai-sandbox}" +WORKDIR_HOST="${SANDBOX_DIR}/workspace" +CONFIG_DIR="${SANDBOX_DIR}/config" # persistent agent credentials (host-only, chmod 700) +TMPDIR_SIZE="${TMPDIR_SIZE:-512m}" +CPU_LIMIT="${CPU_LIMIT:-1.0}" +MEM_LIMIT="${MEM_LIMIT:-512m}" +NETWORK_MODE="${NETWORK_MODE:-none}" # none = no internet; bridge = allow + +# Colors +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' +info() { echo -e "${CYAN}[sandbox]${NC} $*"; } +ok() { echo -e "${GREEN}[ok]${NC} $*"; } +warn() { echo -e "${YELLOW}[warn]${NC} $*"; } +die() { echo -e "${RED}[error]${NC} $*" >&2; exit 1; } + +# ── OS detection ────────────────────────────────────────────────────────────── +OS="$(uname -s)" +case "$OS" in + Linux) PLATFORM=linux ;; + Darwin) PLATFORM=macos ;; + *) die "Unsupported OS: $OS" ;; +esac + +# ── Helpers ─────────────────────────────────────────────────────────────────── +require_docker() { + command -v docker &>/dev/null || die "Docker not found. Run: ./sandbox.sh setup" + docker info &>/dev/null || die "Docker daemon not running." +} + +has_nsjail() { + [[ "$PLATFORM" == "linux" ]] && command -v nsjail &>/dev/null +} + +has_firejail() { + command -v firejail &>/dev/null +} + +# Verify that host networking/DNS was not changed by this script +assert_host_untouched() { + # We never modify /etc/hosts, /etc/resolv.conf, iptables, or network interfaces. + # This function just documents that guarantee and does a quick sanity check. + if [[ "$PLATFORM" == "linux" ]]; then + local resolv_hash_before resolv_hash_after + resolv_hash_before="${_RESOLV_HASH_BEFORE:-}" + resolv_hash_after="$(md5sum /etc/resolv.conf 2>/dev/null | cut -d' ' -f1)" + if [[ -n "$resolv_hash_before" && "$resolv_hash_before" != "$resolv_hash_after" ]]; then + die "/etc/resolv.conf was modified unexpectedly! Aborting." + fi + fi +} + +snapshot_host() { + if [[ "$PLATFORM" == "linux" ]]; then + export _RESOLV_HASH_BEFORE="$(md5sum /etc/resolv.conf 2>/dev/null | cut -d' ' -f1)" + fi +} + +# ── Dockerfile (embedded) ───────────────────────────────────────────────────── +write_dockerfile() { + local dir="$1" + cat > "$dir/Dockerfile" <<'EOF' +FROM ubuntu:24.04 + +ARG DEBIAN_FRONTEND=noninteractive + +# System packages +RUN apt-get update -qq && apt-get install -y --no-install-recommends \ + python3 python3-pip python3-venv \ + git curl wget ca-certificates gnupg \ + build-essential \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Node.js LTS (via NodeSource — newer than Ubuntu's bundled version) +RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \ + && apt-get install -y --no-install-recommends nodejs \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# GitHub CLI +RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \ + | gpg --dearmor -o /usr/share/keyrings/githubcli-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) \ + signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] \ + https://cli.github.com/packages stable main" \ + > /etc/apt/sources.list.d/github-cli.list \ + && apt-get update -qq && apt-get install -y --no-install-recommends gh \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# AI CLI tools (npm global) +RUN npm install -g --no-audit --no-fund \ + @anthropic-ai/claude-code \ + @google/gemini-cli + +# Non-root sandbox user (fixed uid/gid 999) +RUN groupadd -r -g 999 agent \ + && useradd -r -u 999 -g agent -m -d /home/agent agent + +# Workspace dir +RUN mkdir -p /workspace && chown agent:agent /workspace + +USER agent +ENV HOME=/home/agent + +# Stub dirs for bind/tmpfs mounts (must exist before runtime mounts) +RUN mkdir -p \ + /home/agent/.claude \ + /home/agent/.cache \ + /home/agent/.config/gh \ + /home/agent/.config/gemini \ + /home/agent/.local/state + +# gh Copilot extension — baked into image; read-only at runtime is fine +RUN gh extension install github/gh-copilot 2>/dev/null || true + +USER root +WORKDIR /workspace +USER agent + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +CMD ["/bin/bash"] +EOF +} + +# ── Commands ────────────────────────────────────────────────────────────────── + +cmd_setup() { + snapshot_host + info "Setting up AI sandbox on $PLATFORM..." + + # 1. Docker + if ! command -v docker &>/dev/null; then + if [[ "$PLATFORM" == "linux" ]]; then + info "Installing Docker (rootless)..." + # Use official convenience script but only if user confirms + read -rp " Install Docker via get.docker.com? [y/N] " ans + [[ "$ans" =~ ^[Yy]$ ]] || die "Aborted. Install Docker manually: https://docs.docker.com/engine/install/" + curl -fsSL https://get.docker.com | sh + # Rootless setup (optional, avoids root daemon) + if command -v dockerd-rootless-setuptool.sh &>/dev/null; then + info "Configuring rootless Docker..." + dockerd-rootless-setuptool.sh install 2>/dev/null || warn "Rootless setup skipped." + fi + elif [[ "$PLATFORM" == "macos" ]]; then + if command -v brew &>/dev/null; then + info "Installing Docker via Homebrew..." + brew install --cask docker + info "Please open Docker.app to finish installation, then re-run: ./sandbox.sh setup" + open /Applications/Docker.app 2>/dev/null || true + exit 0 + else + die "Install Docker Desktop from https://docs.docker.com/desktop/mac/install/" + fi + fi + else + ok "Docker already installed: $(docker --version)" + fi + + # 2. Verify Docker daemon + if ! docker info &>/dev/null; then + die "Docker daemon not running. Start Docker and retry." + fi + + # 3. Optional: nsjail on Linux + if [[ "$PLATFORM" == "linux" ]] && ! command -v nsjail &>/dev/null; then + warn "nsjail not found (optional). For extra hardening: sudo apt install nsjail" + elif has_nsjail; then + ok "nsjail available: $(nsjail --version 2>&1 | head -1)" + fi + + # 4. Create sandbox dirs + mkdir -p "$WORKDIR_HOST" + ok "Workspace: $WORKDIR_HOST" + + # Persistent config dirs — host-only, never world-readable + mkdir -p \ + "$CONFIG_DIR/claude" \ + "$CONFIG_DIR/gh" \ + "$CONFIG_DIR/gemini" + chmod 700 "$CONFIG_DIR" "$CONFIG_DIR/claude" "$CONFIG_DIR/gh" "$CONFIG_DIR/gemini" + ok "Config dir: $CONFIG_DIR (chmod 700)" + + # 5. Build Docker image + local build_dir + build_dir="$(mktemp -d)" + write_dockerfile "$build_dir" + info "Building sandbox image ($SANDBOX_IMAGE)..." + docker build -t "$SANDBOX_IMAGE" "$build_dir" --quiet + rm -rf "$build_dir" + ok "Image built: $SANDBOX_IMAGE" + + assert_host_untouched + echo "" + ok "Setup complete. Host network/DNS: unchanged." + info "Run agent code: ./sandbox.sh run " + info "Interactive: ./sandbox.sh shell" +} + +cmd_run() { + require_docker + snapshot_host + + local script="${1:-}" + [[ -z "$script" ]] && die "Usage: ./sandbox.sh run