From c76e443e34396724bcca029353967f4d9d095d7a Mon Sep 17 00:00:00 2001 From: Daniel Rapp Date: Tue, 11 Aug 2026 20:04:29 +0000 Subject: [PATCH] chore(base): Debian bookworm -> trixie (system Python 3.11 -> 3.13) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Debian 12 bookworm's regular security support ended 2026-07-12 and it is now on community LTS only. Trixie (Debian 13) is current stable. Follows #131 (Go 1.26 + Node 24); together these refresh every pinned toolchain. Two apt packages were RENAMED in trixie and would have failed the build: - libpango1.0-0 -> libpango-1.0-0 (base image apt list) - libgdk-pixbuf2.0-dev -> libgdk-pixbuf-2.0-dev (all six agent generators) Note libpango1.0-dev and libcairo2-dev keep their old-style names in trixie and are deliberately unchanged. All 26 apt-installed packages across the base image and the six agent generators were verified present in trixie via Debian's madison API, not spot-checked. --pull is added to the BASE image build so debian:trixie-slim re-resolves to the current digest on rebuild -- the same HF-incident Issue 3 rationale the proxy build already uses. Deliberately NOT added to the agent, skills, or per-project builds: those build FROM a LOCAL sandy-base image, and --pull would attempt a registry fetch and fail. Python 3.11 -> 3.13 has one user-visible consequence: PYTHONUSERBASE stores packages under lib/python3., so persistent 'pip install --user' trees from the 3.11 image are invisible to 3.13. user-setup.sh now detects a mismatched tree and prints a warn-only notice with the path and the cleanup command -- matching the existing environment-drift warnings (broken .venv symlink, foreign native modules), which likewise warn rather than auto-fix, since silently deleting a user's installed packages is the wrong default. SANDY_SANDBOX_MIN_COMPAT is deliberately NOT moved: the sandbox still works, so this is not a compat-floor event (the 1.x forward-compat promise caps it at 1.0.0 anyway, guarded by run-tests.sh §60). Existing sandboxes need no recreation. The .venv overlay is unaffected -- it uses uv-managed interpreters, independent of the container's system python3. PEP 668 is already neutralized via PIP_BREAK_SYSTEM_PACKAGES=1. Docs synced: README, CLAUDE.md (incl. a new Automatic Environment Detection bullet), SPECIFICATION.md (Appendix A verbatim copy + Appendix B table), and a stale 'Debian bookworm' reference in docs/security/nono-roadmap.md. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 5 +++-- README.md | 5 +++-- SPECIFICATION.md | 20 ++++++++++++-------- docs/security/nono-roadmap.md | 2 +- sandy | 33 ++++++++++++++++++++++++--------- templates/user-setup.sh.tmpl | 15 +++++++++++++++ 6 files changed, 58 insertions(+), 22 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index bdef785..b1b0083 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -508,7 +508,7 @@ Two modes controlled by `SANDY_SSH`: ## Language Environments -The base image ships with fixed versions of each toolchain: Python 3 (Debian bookworm's default), Node.js 24, Go 1.26, Rust stable, and C/C++ (build-essential). `uv` is also pre-installed for Python version management. +The base image ships with fixed versions of each toolchain: Python 3.13 (Debian trixie's default), Node.js 24, Go 1.26, Rust stable, and C/C++ (build-essential). `uv` is also pre-installed for Python version management. ### Persistent Package Installs @@ -526,7 +526,7 @@ These are per-project — packages installed in one project sandbox don't leak t ### Python Version Management -The base image includes a single system Python (whatever Debian bookworm ships). For projects that need a specific Python version, use `uv`: +The base image includes a single system Python (whatever Debian trixie ships — currently 3.13). For projects that need a specific Python version, use `uv`: ```sh uv python install 3.11 @@ -558,6 +558,7 @@ On every session start, the entrypoint checks the workspace for common issues: - **`.python-version`**: Auto-installs the specified Python version via `uv python install` (idempotent, persists). - **Broken `.venv`**: If `.venv/bin/python` is a dead symlink (host/container Python mismatch), warns with the fix command. - **Foreign native modules**: If `node_modules/` contains `.node` files compiled for a different platform (e.g. macOS → Linux), warns with `npm rebuild` as the fix. +- **Orphaned pip user-site**: If `PYTHONUSERBASE`'s `lib/python3./` doesn't match the running system Python's minor version (e.g. after a base-image Python bump), warns with the old path and a reinstall/cleanup pointer. ### Gotchas diff --git a/README.md b/README.md index 62c3a18..8dc6aba 100644 --- a/README.md +++ b/README.md @@ -454,7 +454,7 @@ Sandy's base image is a self-contained development environment. Everything below | Toolchain | Version | Notes | |---|---|---| -| Python 3 | Debian bookworm default | System Python; use `uv` for other versions | +| Python 3 | Debian trixie default (3.13) | System Python; use `uv` for other versions | | Node.js | 24 LTS | Via NodeSource | | Go | 1.26 | Latest 1.26.x patch resolved at image build | | Rust | stable | Via rustup | @@ -585,7 +585,7 @@ These are per-project — packages installed in one project don't leak to anothe ### Python version management -The base image ships one system Python (Debian bookworm's default). If your project needs a specific version, use `uv`: +The base image ships one system Python (Debian trixie's default, 3.13). If your project needs a specific version, use `uv`: ```bash uv python install 3.11 # downloads once, persists across sessions @@ -646,6 +646,7 @@ Sandy checks your project on startup and handles common issues: - **`.python-version`** — if present, sandy auto-installs that Python version via `uv` (persists across sessions) - **Host `.venv/`** — shadowed with a sandbox-owned overlay (see above). The host venv is never modified; the container gets its own materialized venv matching the host's Python version, auto-activated via `VIRTUAL_ENV` + `PATH`. Drift between the overlay and `.python-version` triggers a warning on relaunch - **Foreign native modules** — if `node_modules/` contains native addons compiled for a different platform (e.g. macOS), sandy warns with `npm rebuild` as the fix +- **Orphaned pip user-site** — if persistent `pip install --user` packages were installed under a different Python minor version than the image now ships (e.g. after a base-image Python bump), sandy warns with the old path and a reinstall/cleanup pointer These checks run on every session start and add negligible overhead. diff --git a/SPECIFICATION.md b/SPECIFICATION.md index fe3b723..c84dec2 100644 --- a/SPECIFICATION.md +++ b/SPECIFICATION.md @@ -370,7 +370,7 @@ Sandy generates all Dockerfiles, entrypoint scripts, and config files at runtime **Rebuild trigger**: Content hash of Dockerfile.base changes, or `--rebuild` flag Contents: -- **OS**: Debian bookworm-slim +- **OS**: Debian trixie-slim - **System tools**: build-essential, git, git-lfs, jq, ripgrep, socat, tmux, curl, cmake, openssh-client, less, pkg-config, gosu - **GitHub CLI**: `gh` - **Node.js 24 LTS**: Via NodeSource @@ -379,7 +379,7 @@ Contents: - **Bun**: Via `curl https://bun.sh/install` - **uv**: Via `curl https://astral.sh/uv/install.sh` (installed to `/usr/local/bin`) - **Python 3**: Debian system Python + python3-venv -- **Libraries**: libcairo2, libgdk-pixbuf-2.0-0, libpango1.0-0, libssl-dev, ncurses-term +- **Libraries**: libcairo2, libgdk-pixbuf-2.0-0, libpango-1.0-0, libssl-dev, ncurses-term - **User**: `claude` (UID 1001, shell `/bin/bash`) ### Phase 2: Claude Code Image (`sandy-claude-code`) @@ -390,7 +390,7 @@ Contents: Contents: - `FROM sandy-base` - Claude Code: Native binary installed via `curl https://claude.ai/install.sh`, relocated to `/usr/local/bin/claude` and `/opt/claude-code` -- synthkit dependencies: libpango1.0-dev, libcairo2-dev, libgdk-pixbuf2.0-dev (WeasyPrint needs these) +- synthkit dependencies: libpango1.0-dev, libcairo2-dev, libgdk-pixbuf-2.0-dev (WeasyPrint needs these) - synthkit: Installed via `UV_TOOL_DIR=/opt/uv-tools UV_TOOL_BIN_DIR=/usr/local/bin uv tool install synthkit` - `COPY`: entrypoint.sh, user-setup.sh, tmux.conf - Claude Code version cached at `/opt/claude-code/.version` @@ -1015,6 +1015,10 @@ If `.venv/bin/python` is a broken symlink (host/container Python version mismatc Scans `node_modules/` for `.node` files. If they're not ELF binaries (e.g., Mach-O from macOS host), warns with `npm rebuild` as the fix. +### Orphaned pip user-site + +`PYTHONUSERBASE` (the persistent `pip/` sandbox mount, `~/.pip-packages`) stores `pip install --user` packages under `lib/python3./site-packages`. A base-image system-Python bump (e.g. 3.11 → 3.13 with the trixie move) leaves an older `lib/python3./` tree on disk but invisible to the new interpreter. Warn-only: for each `lib/python3.*` dir under `$PYTHONUSERBASE` whose minor version doesn't match the running `python3`'s, prints the stale path and a reinstall/`rm -rf` pointer. Never fails the session. + ### Git LFS If workspace is a git repo and `.gitattributes` contains `filter=lfs` (checked up to 3 levels deep), runs `git lfs install` (idempotent). @@ -1304,7 +1308,7 @@ Sandy generates all build and runtime files as heredocs embedded in the script. **Generator**: `generate_dockerfile_base()` — quoted heredoc (`<<'DOCKERFILE_BASE'`), no variable expansion. ```dockerfile -FROM debian:bookworm-slim +FROM debian:trixie-slim # Some Docker Desktop versions prevent the _apt user from reading the # temp files apt stages for gpgv, producing spurious "invalid signature" @@ -1324,7 +1328,7 @@ RUN apt-get update && apt-get install -y \ less \ libcairo2 \ libgdk-pixbuf-2.0-0 \ - libpango1.0-0 \ + libpango-1.0-0 \ libssl-dev \ ncurses-term \ openssh-client \ @@ -1432,7 +1436,7 @@ RUN HOME=/home/claude su -s /bin/bash claude -c \ # synthkit dependencies (WeasyPrint needs pango/cairo/gdk-pixbuf) RUN apt-get update && apt-get install -y --no-install-recommends \ - libpango1.0-dev libcairo2-dev libgdk-pixbuf2.0-dev \ + libpango1.0-dev libcairo2-dev libgdk-pixbuf-2.0-dev \ && rm -rf /var/lib/apt/lists/* RUN UV_TOOL_DIR=/opt/uv-tools UV_TOOL_BIN_DIR=/usr/local/bin \ @@ -1464,7 +1468,7 @@ RUN npm install -g @openai/codex \ && mkdir -p /opt/codex \ && { codex --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' > /opt/codex/.version || true; } RUN apt-get update && apt-get install -y --no-install-recommends \ - libpango1.0-dev libcairo2-dev libgdk-pixbuf2.0-dev \ + libpango1.0-dev libcairo2-dev libgdk-pixbuf-2.0-dev \ && rm -rf /var/lib/apt/lists/* RUN UV_TOOL_DIR=/opt/uv-tools UV_TOOL_BIN_DIR=/usr/local/bin uv tool install --python-preference system synthkit COPY tmux.conf /etc/tmux.conf @@ -1782,7 +1786,7 @@ Capabilities SETUID/SETGID are needed for `gosu` privilege drop. CHOWN/DAC_OVERR | Rust | stable (latest) | rustup | | Bun | latest | `curl https://bun.sh/install` | | uv | latest | `curl https://astral.sh/uv/install.sh` | -| Python | Debian bookworm system default | `apt-get install python3` | +| Python | Debian trixie system default (3.13) | `apt-get install python3` | --- diff --git a/docs/security/nono-roadmap.md b/docs/security/nono-roadmap.md index 431e5d6..7a75fc2 100644 --- a/docs/security/nono-roadmap.md +++ b/docs/security/nono-roadmap.md @@ -15,7 +15,7 @@ The whole "better together" claim rests on this, and it is **not** obvious it wo **Open technical questions to answer:** 1. **seccomp-notify** — nono's supervisor installs a seccomp filter with `SECCOMP_FILTER_FLAG_NEW_LISTENER`. Installing a filter needs *either* `CAP_SYS_ADMIN` *or* `no_new_privs=1`; sandy sets `no_new_privs=1`, so a filter *should* install without caps — **but** obtaining the user-notification listener fd can be gated (kernel/config-dependent, sometimes `CAP_SYS_ADMIN`). Does it work in sandy's cap-dropped container? **This is the single highest-risk unknown.** 2. **Landlock** — needs no privileges (kernel 5.13+), but Docker's default **seccomp profile must allow `landlock_create_ruleset` / `landlock_add_rule` / `landlock_restrict_self`**, and the **host kernel** (the container shares it) must have Landlock enabled. Verify both. -3. **Install path** — nono is a Rust binary; does it install/run on the sandy base image (Debian bookworm, `/home` tmpfs, no root), or does it need baking into the image? +3. **Install path** — nono is a Rust binary; does it install/run on the sandy base image (Debian trixie, `/home` tmpfs, no root), or does it need baking into the image? **Deliverable:** a one-page spike report — *runs / doesn't run inside sandy*, and if not, the **exact blocker** plus whether a **narrow** sandy change unblocks it (e.g. a seccomp-profile allowance) **without weakening sandy's posture**. I'll write the test script; a maintainer runs it on a real Docker host. diff --git a/sandy b/sandy index 8e3b962..ad1961c 100755 --- a/sandy +++ b/sandy @@ -2676,7 +2676,7 @@ rm -f "$SANDY_HOME/.sandy-write-test" 2>/dev/null || true # --- Build file generators --- generate_dockerfile_base() { cat > "$SANDY_HOME/Dockerfile.base.new" <<'DOCKERFILE_BASE' -FROM debian:bookworm-slim +FROM debian:trixie-slim # Some Docker Desktop versions prevent the _apt user from reading the # temp files apt stages for gpgv, producing spurious "invalid signature" @@ -2696,7 +2696,7 @@ RUN apt-get update && apt-get install -y \ less \ libcairo2 \ libgdk-pixbuf-2.0-0 \ - libpango1.0-0 \ + libpango-1.0-0 \ libssl-dev \ ncurses-term \ openssh-client \ @@ -2860,7 +2860,7 @@ RUN HOME=/home/claude su -s /bin/bash claude -c "curl -fsSL https://claude.ai/in && { /usr/local/bin/claude --version 2>/dev/null | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' > /opt/claude-code/.version || true; } # synthkit dependencies (WeasyPrint needs pango/cairo/gdk-pixbuf) RUN apt-get update && apt-get install -y --no-install-recommends \\ - libpango1.0-dev libcairo2-dev libgdk-pixbuf2.0-dev \\ + libpango1.0-dev libcairo2-dev libgdk-pixbuf-2.0-dev \\ && rm -rf /var/lib/apt/lists/* RUN UV_TOOL_DIR=/opt/uv-tools UV_TOOL_BIN_DIR=/usr/local/bin uv tool install --python-preference system synthkit COPY tmux.conf /etc/tmux.conf @@ -2883,7 +2883,7 @@ RUN npm install -g @google/gemini-cli \\ | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' > /opt/gemini-cli/.version || true; } # synthkit dependencies (WeasyPrint needs pango/cairo/gdk-pixbuf) RUN apt-get update && apt-get install -y --no-install-recommends \\ - libpango1.0-dev libcairo2-dev libgdk-pixbuf2.0-dev \\ + libpango1.0-dev libcairo2-dev libgdk-pixbuf-2.0-dev \\ && rm -rf /var/lib/apt/lists/* RUN UV_TOOL_DIR=/opt/uv-tools UV_TOOL_BIN_DIR=/usr/local/bin uv tool install --python-preference system synthkit # osv-scanner: required by the gemini-cli-security extension (pre-built binary, not go install) @@ -2909,7 +2909,7 @@ RUN npm install -g @openai/codex \\ && { codex --version 2>/dev/null | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' > /opt/codex/.version || true; } # synthkit dependencies (WeasyPrint needs pango/cairo/gdk-pixbuf) RUN apt-get update && apt-get install -y --no-install-recommends \\ - libpango1.0-dev libcairo2-dev libgdk-pixbuf2.0-dev \\ + libpango1.0-dev libcairo2-dev libgdk-pixbuf-2.0-dev \\ && rm -rf /var/lib/apt/lists/* RUN UV_TOOL_DIR=/opt/uv-tools UV_TOOL_BIN_DIR=/usr/local/bin uv tool install --python-preference system synthkit COPY tmux.conf /etc/tmux.conf @@ -2941,7 +2941,7 @@ RUN set -eu; \\ grok --version 2>/dev/null | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' > /opt/grok/.version || true # synthkit dependencies (WeasyPrint needs pango/cairo/gdk-pixbuf) RUN apt-get update && apt-get install -y --no-install-recommends \\ - libpango1.0-dev libcairo2-dev libgdk-pixbuf2.0-dev \\ + libpango1.0-dev libcairo2-dev libgdk-pixbuf-2.0-dev \\ && rm -rf /var/lib/apt/lists/* RUN UV_TOOL_DIR=/opt/uv-tools UV_TOOL_BIN_DIR=/usr/local/bin uv tool install --python-preference system synthkit COPY tmux.conf /etc/tmux.conf @@ -2963,7 +2963,7 @@ RUN npm install -g opencode-ai \\ && { opencode --version 2>/dev/null | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' > /opt/opencode/.version || true; } # synthkit dependencies (WeasyPrint needs pango/cairo/gdk-pixbuf) RUN apt-get update && apt-get install -y --no-install-recommends \\ - libpango1.0-dev libcairo2-dev libgdk-pixbuf2.0-dev \\ + libpango1.0-dev libcairo2-dev libgdk-pixbuf-2.0-dev \\ && rm -rf /var/lib/apt/lists/* RUN UV_TOOL_DIR=/opt/uv-tools UV_TOOL_BIN_DIR=/usr/local/bin uv tool install --python-preference system synthkit COPY tmux.conf /etc/tmux.conf @@ -3010,7 +3010,7 @@ RUN set -eu; \\ grok --version 2>/dev/null | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' > /opt/grok/.version || true # synthkit dependencies (WeasyPrint needs pango/cairo/gdk-pixbuf) RUN apt-get update && apt-get install -y --no-install-recommends \\ - libpango1.0-dev libcairo2-dev libgdk-pixbuf2.0-dev \\ + libpango1.0-dev libcairo2-dev libgdk-pixbuf-2.0-dev \\ && rm -rf /var/lib/apt/lists/* RUN UV_TOOL_DIR=/opt/uv-tools UV_TOOL_BIN_DIR=/usr/local/bin uv tool install --python-preference system synthkit # osv-scanner: required by the gemini-cli-security extension (pre-built binary, not go install) @@ -3750,6 +3750,21 @@ if [ -d "$WORKSPACE/node_modules" ]; then fi fi +# Orphaned pip user-site detection: PYTHONUSERBASE stores packages under +# lib/python3.; a base-image Python bump (3.11 -> 3.13 with trixie) +# leaves the old tree invisible to the new interpreter. Warn-only. +_sys_py="$(python3 -c 'import sys; print("%d.%d" % sys.version_info[:2])' 2>/dev/null || true)" +if [ -n "$_sys_py" ] && [ -d "$PYTHONUSERBASE/lib" ]; then + for _d in "$PYTHONUSERBASE"/lib/python3.*; do + [ -d "$_d" ] || continue + _v="${_d##*/python}" + if [ "$_v" != "$_sys_py" ]; then + printf "\033[0;33m⚠ Persistent pip packages for Python %s found (%s); system Python is now %s.\033[0m\n" "$_v" "$_d" "$_sys_py" + printf "\033[0;33m Reinstall what you need (pip install --user ...), then: rm -rf %s\033[0m\n" "$_d" + fi + done +fi + sandy_log "Configuring git auth" # Authenticate gh CLI with all available accounts (works in both token and agent modes) if [ -n "${GH_ACCOUNTS:-}" ] && command -v gh &>/dev/null; then @@ -5991,7 +6006,7 @@ BASE_REBUILT=false if [ ! -f "$BASE_HASH_FILE" ] || [ "$(cat "$BASE_HASH_FILE")" != "$BASE_HASH" ] || ! docker image inspect "$BASE_IMAGE_NAME" &>/dev/null; then info "Building base image (language runtimes) — this may take several minutes on first run..." _old_base_id="$(docker image inspect -f '{{.Id}}' "$BASE_IMAGE_NAME" 2>/dev/null || true)" - docker build ${_BASE_BUILD_Q[@]+"${_BASE_BUILD_Q[@]}"} --no-cache --label sandy.managed=1 -t "$BASE_IMAGE_NAME" -f "$SANDY_HOME/Dockerfile.base" "$SANDY_HOME" + docker build ${_BASE_BUILD_Q[@]+"${_BASE_BUILD_Q[@]}"} --no-cache --pull --label sandy.managed=1 -t "$BASE_IMAGE_NAME" -f "$SANDY_HOME/Dockerfile.base" "$SANDY_HOME" echo "$BASE_HASH" > "$BASE_HASH_FILE" BASE_REBUILT=true _sandy_prune_old_image "$_old_base_id" "$BASE_IMAGE_NAME" diff --git a/templates/user-setup.sh.tmpl b/templates/user-setup.sh.tmpl index cd840a7..c00ed8e 100644 --- a/templates/user-setup.sh.tmpl +++ b/templates/user-setup.sh.tmpl @@ -495,6 +495,21 @@ if [ -d "$WORKSPACE/node_modules" ]; then fi fi +# Orphaned pip user-site detection: PYTHONUSERBASE stores packages under +# lib/python3.; a base-image Python bump (3.11 -> 3.13 with trixie) +# leaves the old tree invisible to the new interpreter. Warn-only. +_sys_py="$(python3 -c 'import sys; print("%d.%d" % sys.version_info[:2])' 2>/dev/null || true)" +if [ -n "$_sys_py" ] && [ -d "$PYTHONUSERBASE/lib" ]; then + for _d in "$PYTHONUSERBASE"/lib/python3.*; do + [ -d "$_d" ] || continue + _v="${_d##*/python}" + if [ "$_v" != "$_sys_py" ]; then + printf "\033[0;33m⚠ Persistent pip packages for Python %s found (%s); system Python is now %s.\033[0m\n" "$_v" "$_d" "$_sys_py" + printf "\033[0;33m Reinstall what you need (pip install --user ...), then: rm -rf %s\033[0m\n" "$_d" + fi + done +fi + sandy_log "Configuring git auth" # Authenticate gh CLI with all available accounts (works in both token and agent modes) if [ -n "${GH_ACCOUNTS:-}" ] && command -v gh &>/dev/null; then