diff --git a/.github/workflows/sandbox-agent.yml b/.github/workflows/sandbox-agent.yml index a570f563..3d160f4c 100644 --- a/.github/workflows/sandbox-agent.yml +++ b/.github/workflows/sandbox-agent.yml @@ -5,6 +5,12 @@ on: branches: [main] paths: - sandbox/** + # Daily rebuild so Claude Code and the bunny CLI pick up new releases without + # a commit. The Dockerfile keys each install layer on the upstream version, so + # a day with no releases is a fully cached no-op. + # Off the hour on purpose — the top of the hour is the busiest Actions slot. + schedule: + - cron: "17 4 * * *" workflow_dispatch: jobs: diff --git a/packages/cli/README.md b/packages/cli/README.md index c26f48bf..dc22e665 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -853,9 +853,9 @@ bunny scripts docs ### `bunny sandbox` -Manage on-demand cloud sandbox environments backed by Bunny Magic Containers. Each sandbox is a fully isolated Ubuntu container with Node.js, Bun, Python, and Claude Code pre-installed. A 10 GB persistent volume is mounted at `/workplace`, your default working directory. +Manage on-demand cloud sandbox environments backed by Bunny Magic Containers. Each sandbox is a fully isolated Ubuntu container with Node.js, Bun, Python, the bunny CLI, and Claude Code pre-installed, alongside the tooling agents reach for: `git`, `gh`, `ripgrep`, `fd`, `jq`, `tmux`, `sqlite3`, `tree`, and `fzf`. A 10 GB persistent volume is mounted at `/workplace`, your default working directory. -Claude Code is pre-installed but needs your own Anthropic credentials before it can do anything: pass an API key at create time (prefer `--env-file .env` so the key stays out of your shell history), or run `claude` inside the sandbox and complete the login prompt it prints. Both survive restarts and redeploys: baked env vars live on the container, and the login flow writes to `/workplace/.claude` on the persistent volume. +Claude Code is pre-installed but needs your own Anthropic credentials before it can do anything: pass an API key at create time (prefer `--env-file .env` so the key stays out of your shell history), or run `claude` inside the sandbox and complete the login prompt it prints. Both survive restarts and redeploys: baked env vars live on the container, and config and credentials are pinned to the persistent volume — `/workplace/.claude` for Claude Code, and `/workplace/.config` for the bunny CLI and `gh`. Sandbox credentials (app ID, SSH endpoint, agent token) are stored in the CLI's local config file (`~/.config/bunnynet.json` by default) so you can reconnect without re-creating. diff --git a/sandbox/Dockerfile b/sandbox/Dockerfile index bf2683e1..c0ab0af9 100644 --- a/sandbox/Dockerfile +++ b/sandbox/Dockerfile @@ -2,6 +2,11 @@ FROM ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractive +# Ubuntu leaves the locale unset, which makes agent TUIs and Python misrender +# non-ASCII output. C.UTF-8 is built in, so this needs no locales package. +# Repeated in /etc/environment below, because ENV does not reach SSH sessions. +ENV LANG=C.UTF-8 + # System tools + SSH + Python RUN apt-get update && apt-get install -y \ openssh-server \ @@ -12,19 +17,34 @@ RUN apt-get update && apt-get install -y \ net-tools iputils-ping \ unzip zip \ jq \ + ripgrep fd-find \ + tmux \ + sqlite3 tree file fzf \ python3 python3-pip python3-venv python3-dev \ build-essential \ && ln -sf /usr/bin/python3 /usr/bin/python \ && ln -sf /usr/bin/pip3 /usr/bin/pip \ + && ln -sf /usr/bin/fdfind /usr/local/bin/fd \ && rm -rf /var/lib/apt/lists/* \ && mkdir -p /var/run/sshd \ && sed -i 's|#\?Port 22$|Port 8023|' /etc/ssh/sshd_config \ && sed -i 's|#\?PermitRootLogin.*|PermitRootLogin yes|' /etc/ssh/sshd_config \ && sed -i 's|#\?PasswordAuthentication.*|PasswordAuthentication yes|' /etc/ssh/sshd_config +# GitHub CLI (pinned apt repo with a verified signing key, same pattern as Node +# below) — agents need it for PR and issue work; plain git cannot do either. +RUN install -d -m 0755 /etc/apt/keyrings \ + && curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \ + -o /etc/apt/keyrings/githubcli-archive-keyring.gpg \ + && chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ + > /etc/apt/sources.list.d/github-cli.list \ + && apt-get update && apt-get install -y gh \ + && rm -rf /var/lib/apt/lists/* + # Node.js (pinned NodeSource apt repo with a verified signing key — no curl | bash). # apt then cryptographically verifies the nodejs package against the pinned key. -ARG NODE_MAJOR=22 +ARG NODE_MAJOR=24 RUN install -d -m 0755 /etc/apt/keyrings \ && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \ -o /etc/apt/keyrings/nodesource.asc \ @@ -49,19 +69,47 @@ RUN set -eux; \ rm -rf /tmp/bun.zip /tmp/bun ENV PATH="/root/.bun/bin:$PATH" -# Claude Code -RUN install -d -m 0755 /etc/apt/keyrings \ - && curl -fsSL https://downloads.claude.ai/keys/claude-code.asc \ - -o /etc/apt/keyrings/claude-code.asc \ - && echo "deb [signed-by=/etc/apt/keyrings/claude-code.asc] https://downloads.claude.ai/claude-code/apt/stable stable main" \ - > /etc/apt/sources.list.d/claude-code.list \ - && apt-get update && apt-get install -y claude-code \ - && rm -rf /var/lib/apt/lists/* +# Claude Code — official installer, always the newest release. +# Installs the launcher into /root/.local/bin; the installer itself +# checksum-verifies the binary it downloads. +# +# The ADD below is the cache key: its content is the current version string, so +# Docker re-runs the install exactly when a new release ships and reuses the +# layer when it hasn't. Kept after the apt/Node/Bun layers on purpose — busting +# it only reruns the cheap steps that follow. +ADD https://downloads.claude.ai/claude-code-releases/latest /tmp/claude-code-version +RUN curl -fsSL https://claude.ai/install.sh | bash \ + && rm -f /tmp/claude-code-version +ENV PATH="/root/.local/bin:$PATH" + +# bunny CLI — this repo, published to npm. No install scripts; the platform +# binary comes in as an optional dependency. Its config follows +# XDG_CONFIG_HOME, which is pinned to /workplace below, so credentials persist +# without any extra wiring. +ADD https://registry.npmjs.org/@bunny.net/cli/latest /tmp/bunny-cli-version +RUN npm install -g @bunny.net/cli@latest \ + && rm -f /tmp/bunny-cli-version -# Workplace — SSH sessions land here, bun on PATH, Claude config lives here -ENV CLAUDE_CONFIG_DIR=/workplace/.claude +# Workplace — SSH sessions land here, and Claude Code, the bunny CLI and gh all +# keep their config and credentials on the persistent volume (the latter two via +# XDG_CONFIG_HOME, which they both honour). +# +# These have to go in /etc/environment, not just ENV: sshd builds a fresh +# environment per session, so Docker ENV never reaches an SSH login. pam_env +# (UsePAM yes) reads this file for both interactive and non-interactive +# sessions, which is also why PATH is spelled out here — .bashrc alone misses +# `ssh `. ENV is kept as well so `docker exec` and the entrypoint +# see the same values. +ENV CLAUDE_CONFIG_DIR=/workplace/.claude \ + XDG_CONFIG_HOME=/workplace/.config RUN mkdir -p /workplace \ - && printf 'cd /workplace\nexport PATH="/root/.bun/bin:$PATH"\n' >> /root/.bashrc + && printf '%s\n' \ + 'PATH=/root/.local/bin:/root/.bun/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' \ + 'LANG=C.UTF-8' \ + 'CLAUDE_CONFIG_DIR=/workplace/.claude' \ + 'XDG_CONFIG_HOME=/workplace/.config' \ + >> /etc/environment \ + && printf 'cd /workplace\n' >> /root/.bashrc COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/sandbox/entrypoint.sh b/sandbox/entrypoint.sh index c911aff8..c409925f 100644 --- a/sandbox/entrypoint.sh +++ b/sandbox/entrypoint.sh @@ -7,4 +7,11 @@ if [ -z "${AGENT_TOKEN}" ]; then fi echo "root:${AGENT_TOKEN}" | chpasswd + +# The persistent volume mounts over /workplace after the image is built, so the +# config directories have to be created here — anything created at build time is +# hidden by the mount. +mkdir -p /workplace/.claude \ + /workplace/.config + exec /usr/sbin/sshd -D diff --git a/skills/bunny-cli/references/sandbox.md b/skills/bunny-cli/references/sandbox.md index ed1e0184..6a294a2a 100644 --- a/skills/bunny-cli/references/sandbox.md +++ b/skills/bunny-cli/references/sandbox.md @@ -1,10 +1,10 @@ # Sandbox Commands -All sandbox commands live under `bunny sandbox`. Each sandbox is a fully isolated Ubuntu container (Bunny Magic Containers) with Node.js, Bun, Python, and Claude Code pre-installed. A 10 GB persistent volume is mounted at `/workplace`, the default working directory; relative remote paths resolve against it. +All sandbox commands live under `bunny sandbox`. Each sandbox is a fully isolated Ubuntu container (Bunny Magic Containers) with Node.js, Bun, Python, the bunny CLI, and Claude Code pre-installed, alongside the tooling agents reach for: `git`, `gh`, `ripgrep`, `fd`, `jq`, `tmux`, `sqlite3`, `tree`, and `fzf`. A 10 GB persistent volume is mounted at `/workplace`, the default working directory; relative remote paths resolve against it. Sandbox credentials (app ID, SSH endpoint, agent token) are stored in the CLI's local config (same candidate paths as in SKILL.md), so commands reference sandboxes by name. -Claude Code is pre-installed but needs the user's own Anthropic credentials: bake an API key in at create time (prefer `--env-file .env` so the key stays out of shell history), set it later with `bunny sandbox env set`, or run `claude` inside the sandbox and complete the login prompt. Both paths survive restarts and redeploys: baked env vars live on the container, and interactive login writes to `/workplace/.claude` (the image pins `CLAUDE_CONFIG_DIR` there) on the persistent volume. +Claude Code is pre-installed but needs the user's own Anthropic credentials: bake an API key in at create time (prefer `--env-file .env` so the key stays out of shell history), set it later with `bunny sandbox env set`, or run `claude` inside the sandbox and complete the login prompt. Both paths survive restarts and redeploys: baked env vars live on the container, and interactive logins write to the persistent volume. The image pins config directories there via `/etc/environment` (Docker `ENV` alone does not reach SSH sessions): `CLAUDE_CONFIG_DIR=/workplace/.claude`, plus `XDG_CONFIG_HOME=/workplace/.config`, which the bunny CLI and `gh` both honour. The entrypoint creates these at startup because the volume mounts over anything the image created at build time. ## Typical workflows