Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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.<minor>/` 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

Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down
20 changes: 12 additions & 8 deletions SPECIFICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`)
Expand All @@ -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`
Expand Down Expand Up @@ -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.<minor>/site-packages`. A base-image system-Python bump (e.g. 3.11 → 3.13 with the trixie move) leaves an older `lib/python3.<minor>/` 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).
Expand Down Expand Up @@ -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"
Expand All @@ -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 \
Expand Down Expand Up @@ -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 \
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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` |

---

Expand Down
2 changes: 1 addition & 1 deletion docs/security/nono-roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
33 changes: 24 additions & 9 deletions sandy
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 \
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -3750,6 +3750,21 @@ if [ -d "$WORKSPACE/node_modules" ]; then
fi
fi

# Orphaned pip user-site detection: PYTHONUSERBASE stores packages under
# lib/python3.<minor>; 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
Expand Down Expand Up @@ -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"
Expand Down
15 changes: 15 additions & 0 deletions templates/user-setup.sh.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,21 @@ if [ -d "$WORKSPACE/node_modules" ]; then
fi
fi

# Orphaned pip user-site detection: PYTHONUSERBASE stores packages under
# lib/python3.<minor>; 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
Expand Down
Loading