🛰️ export from upstream (251518c)

This commit is contained in:
2026-05-24 19:52:42 +02:00
parent 0cd116aff2
commit 2e0c340768
4 changed files with 631 additions and 88 deletions
+223
View File
@@ -0,0 +1,223 @@
# Command Reference
Sandcage provides subcommands for running AI coding agents in isolated Docker containers.
```
sandcage <COMMAND> [OPTIONS]
```
---
## claude
Run the Claude Code agent in a sandboxed container.
```
sandcage claude [OPTIONS] [-- AGENT_ARGS...]
```
**Options:**
| Flag | Description |
|------|-------------|
| `-p, --path <PATH>` | Path to the project directory (defaults to current directory) |
| `--shell` | Drop into a shell instead of launching the agent |
**Trailing arguments:** Any arguments after `--` are forwarded directly to the Claude Code binary inside the container.
**Examples:**
```sh
# Run Claude Code on the current directory
sandcage claude
# Run Claude Code on a specific project
sandcage claude --path /home/user/myproject
# Drop into a shell in the Claude container environment
sandcage claude --shell
# Forward arguments to the agent
sandcage claude -- --resume
```
---
## codex
Run the OpenAI Codex agent in a sandboxed container.
```
sandcage codex [OPTIONS] [-- AGENT_ARGS...]
```
**Options:**
| Flag | Description |
|------|-------------|
| `-p, --path <PATH>` | Path to the project directory (defaults to current directory) |
| `--shell` | Drop into a shell instead of launching the agent |
**Trailing arguments:** Any arguments after `--` are forwarded directly to the Codex binary inside the container.
**Examples:**
```sh
# Run Codex on the current directory
sandcage codex
# Forward arguments to Codex
sandcage codex -- --model o4-mini
```
---
## gemini
Run the Gemini CLI agent in a sandboxed container.
```
sandcage gemini [OPTIONS] [-- AGENT_ARGS...]
```
**Options:**
| Flag | Description |
|------|-------------|
| `-p, --path <PATH>` | Path to the project directory (defaults to current directory) |
| `--shell` | Drop into a shell instead of launching the agent |
**Trailing arguments:** Any arguments after `--` are forwarded directly to the Gemini CLI binary inside the container.
**Examples:**
```sh
# Run Gemini CLI on the current directory
sandcage gemini
# Drop into a shell in the Gemini container
sandcage gemini --shell
```
---
## shell
Open an interactive shell (zsh) with the same sandboxed environment used by agents.
```
sandcage shell [OPTIONS]
```
**Options:**
| Flag | Description |
|------|-------------|
| `-p, --path <PATH>` | Path to the project directory (defaults to current directory) |
**Examples:**
```sh
# Interactive shell in the sandbox for current project
sandcage shell
# Interactive shell for a specific project
sandcage shell --path /home/user/myproject
```
---
## build
Build the container images used by sandcage services.
```
sandcage build [OPTIONS] [SERVICES...]
```
**Options:**
| Flag | Description |
|------|-------------|
| `-f, --force` | Force rebuild even if images are up to date |
**Positional arguments:** Optionally specify which services to build (e.g. `claude`, `codex`, `gemini`). When omitted, all enabled services are built.
**Examples:**
```sh
# Build all enabled service images
sandcage build
# Force rebuild of all images
sandcage build --force
# Build only the claude image
sandcage build claude
# Build claude and gemini images
sandcage build claude gemini
```
---
## init
Initialize a `.sandcage.yml` configuration file for a project.
```
sandcage init
```
Scaffolds a project configuration in the current workspace directory. No options.
**Examples:**
```sh
# Initialize sandcage config in current directory
sandcage init
```
---
## setup ssh
Configure SSH key access for containers. Copies selected SSH keys into a Docker volume accessible by sandcage containers.
```
sandcage setup ssh [OPTIONS]
```
**Options:**
| Flag | Description |
|------|-------------|
| `--global` | Write to global config (`~/.sandcage/config.toml`) instead of project config |
| `--yes` | Skip confirmation prompt |
| `--refresh` | Re-populate the SSH volume using the previously saved selection |
| `--bind` | Use legacy full bind mount instead of volume copy |
**Examples:**
```sh
# Interactive SSH key setup for the current project
sandcage setup ssh
# Non-interactive setup writing to global config
sandcage setup ssh --global --yes
# Refresh the SSH volume after adding new keys
sandcage setup ssh --refresh
```
---
## Common Patterns
**Project path resolution:** All agent commands and `shell` accept `--path` (`-p`) to specify the project directory. When omitted, sandcage resolves the workspace from the current directory.
**Shell override:** The `--shell` flag on agent commands (`claude`, `codex`, `gemini`) drops you into an interactive shell inside the agent's container without launching the agent itself. Useful for debugging the container environment.
**Forwarding arguments to agents:** Agent commands accept trailing arguments (after `--`) that are passed through to the underlying agent binary. This allows setting agent-specific flags without sandcage needing to know about them.
**Available services:** The built-in services are `claude`, `codex`, `gemini`, and `shell`. Each agent service auto-installs its binary on first run if not already present in the container.
+110
View File
@@ -0,0 +1,110 @@
# Docker Image
## Base image contents
The sandcage image is built on `debian:bookworm-slim` and includes:
- **Shell:** zsh (default), bash
- **VCS:** git, openssh-client
- **Search:** ripgrep, fd-find
- **Utilities:** jq, curl, sudo, ca-certificates, npm
- **Task runner:** just (installed from upstream binary)
- **Python tooling:** uv (installed per-user at `/home/agent/.local/bin`)
The image creates an `agent` user (UID 1000) with a home at `/home/agent`, zsh as the default shell, and passwordless sudo.
Pre-created directories: `/workspace`, `/home/agent/.claude`, `/home/agent/.codex`, `/home/agent/.gemini`.
## How agents are installed
AI coding agents (Claude Code, Codex, Gemini CLI) are **not** baked into the image. They are installed on first run via their respective entrypoint scripts. The agent home directories (`~/.claude`, `~/.codex`, `~/.gemini`) are persisted across runs through volume mounts to `~/.sandcage/` on the host, so installation only happens once.
Each service has its own entrypoint script installed at `/usr/local/bin/sandcage-<service>-entrypoint`.
## Building images
Build the sandcage image with:
```
sandcage build
```
### Force rebuild
To bypass the cache and rebuild unconditionally:
```
sandcage build --force
```
When `--force` is used, Docker's `--no-cache` flag is also passed to ensure layers are rebuilt from scratch.
### Service filter
Build only images required for specific services:
```
sandcage build claude shell
```
Unknown service names produce an error listing available services.
## Cache awareness
Sandcage uses hash-based rebuild detection to avoid unnecessary image builds.
On each build, the SHA-256 hash of the Dockerfile content (bundled or custom) is computed and compared against the stored hash in `~/.sandcage/.build-hashes`. If they match, the build is skipped with an "up to date" message.
After a successful build, the new hash is persisted. The hash file uses a simple `image:hash` line format.
This means:
- Editing the Dockerfile (or switching to/from a custom one) triggers a rebuild automatically.
- The `--force` flag bypasses hash comparison entirely.
## Custom Dockerfiles
You can override the bundled Dockerfile by specifying a path in configuration.
### Global config (`~/.sandcage/config.toml`)
```toml
[dockerfiles]
sandcage = "/path/to/my/Dockerfile"
```
### Project config (`.sandcage.yml`)
```yaml
dockerfiles:
sandcage: ./docker/Dockerfile.custom
```
The path can point to either:
- A **file** -- used as the Dockerfile with a temporary build context.
- A **directory** -- used as the full build context (must contain a `Dockerfile`).
### trusted_projects requirement
Project-level Dockerfile overrides are only applied if the project directory is listed in the global `trusted_projects` setting. This prevents untrusted repositories from injecting arbitrary build instructions.
```toml
# ~/.sandcage/config.toml
trusted_projects = [
"/home/user/projects/my-trusted-repo",
"/home/user/work",
]
```
A project is considered trusted if its canonical path starts with any entry in `trusted_projects` (prefix matching). If a project specifies `dockerfiles` but is not trusted, the override is silently ignored with a warning.
Global config overrides (`~/.sandcage/config.toml`) are always trusted.
## Cross-platform notes
### UID/GID handling
On **Linux**, sandcage queries the host user's UID and GID via `id -u` / `id -g` and passes them into the container as `SANDCAGE_UID` and `SANDCAGE_GID`. This ensures files created inside the container have correct ownership on bind-mounted host directories.
On **Windows**, UID/GID passthrough is not meaningful. Sandcage hardcodes both to `1000`, matching the `agent` user built into the image. File ownership on Windows bind mounts is handled by Docker Desktop's filesystem sharing layer.
+224
View File
@@ -0,0 +1,224 @@
# SSH Key Access
## Overview
Containers launched by sandcage often need SSH access to clone private
repositories or push code. Since the agent user inside the container does not
have access to your host SSH keys by default, sandcage provides a setup command
that copies or mounts the relevant keys into the container environment.
## Modes
Sandcage supports two SSH modes, configured via the `ssh_mode` field:
### Volume mode (recommended)
```
ssh_mode: volume
```
A Docker named volume (`sandcage-ssh`) is created and populated with only the
keys required for your git remotes. The volume is mounted read-only at
`/home/agent/.ssh` inside the container. A synthesized SSH config is written
into the volume so the agent can connect to the correct hosts with the correct
keys.
Advantages:
- Only selected keys are copied (not your entire `~/.ssh` directory).
- A minimal SSH config is synthesized from your host config.
- The volume persists across container restarts without re-reading host files.
- Works identically on Linux, macOS, and Windows (Docker Desktop).
### Bind mode
```
ssh_mode: bind
```
Your host `~/.ssh` directory is bind-mounted read-only into the container at
`/home/agent/.ssh:ro`. This exposes all files in that directory to the
container.
Advantages:
- Zero setup after initial configuration.
- Changes to host keys are immediately visible.
Disadvantages:
- Exposes all keys and config, not just the ones needed.
- On some platforms, file permission translation can cause issues.
### None
```
ssh_mode: none
```
No SSH mount is added. Use this if you do not need SSH inside containers or
handle key injection through other means.
## Setup workflow
Run the interactive setup:
```
sandcage setup ssh
```
The command:
1. Scans `~/.ssh/config` (including `Include` directives) for `Host` blocks
with `User git`.
2. Runs `git remote -v` in the current directory to discover SSH remote hosts.
3. Merges the two sources and displays discovered host/key pairs.
4. Prompts you to select which keys to copy.
5. Asks whether to include `known_hosts`.
6. Populates the `sandcage-ssh` Docker volume with the selected keys and a
synthesized SSH config.
7. Writes the configuration to the appropriate config file.
### Flags
| Flag | Effect |
|------|--------|
| `--global` | Write to `~/.sandcage/config.toml` instead of project-local config |
| `--yes` | Accept all defaults without prompting |
| `--bind` | Use bind-mount mode instead of volume mode |
| `--refresh` | Re-populate the volume from existing config (no interactive selection) |
### Examples
```
# Interactive setup for current project
sandcage setup ssh
# Non-interactive, global config
sandcage setup ssh --global --yes
# Legacy bind-mount mode
sandcage setup ssh --bind
# Refresh volume after adding a new key to ~/.ssh
sandcage setup ssh --refresh
```
## Configuration
### Project config (`.sandcage.local.yml` or `.sandcage.yml`)
```yaml
ssh_mode: volume
ssh_keys:
- host: github.com
identity_file: ~/.ssh/id_ed25519
- host: gitea
identity_file: ~/.ssh/work_gitea
```
### Global config (`~/.sandcage/config.toml`)
```toml
ssh_mode = "volume"
[[ssh_keys]]
host = "github.com"
identity_file = "~/.ssh/id_ed25519"
[[ssh_keys]]
host = "gitea"
identity_file = "~/.ssh/work_gitea"
```
### Fields
| Field | Type | Description |
|-------|------|-------------|
| `ssh_mode` | string | One of `volume`, `bind`, or `none` |
| `ssh_keys` | array | List of host/key pairs to include in the volume |
| `ssh_keys[].host` | string | SSH host alias (matches `Host` line in SSH config) |
| `ssh_keys[].identity_file` | string | Path to the private key (supports `~/` expansion) |
Configuration layers are merged in order: global, project, local. Later layers
override earlier ones.
## Refreshing keys
After adding or rotating SSH keys on the host, update the container volume:
```
sandcage setup ssh --refresh
```
This re-reads the existing `ssh_keys` configuration and the current
`~/.ssh/config`, then repopulates the `sandcage-ssh` Docker volume with fresh
copies.
For bind mode, no refresh is needed since the host directory is mounted
directly.
## Security considerations
- **Volume mode** copies only the selected private keys into a Docker volume.
The volume is mounted read-only into containers. Keys are owned by the
`agent` user with `600` permissions.
- **Bind mode** mounts the entire `~/.ssh` directory read-only. The container
can read all keys and config present in that directory.
- Neither mode exposes keys to other containers or Docker networks. Keys exist
only on the filesystem inside the running container.
- The `sandcage-ssh` volume persists on disk until explicitly removed
(`docker volume rm sandcage-ssh`). Run `--refresh` or delete the volume when
decommissioning keys.
## Troubleshooting
### Volume not found
```
sandcage: SSH volume not found — run 'sandcage setup ssh --refresh' to populate it
```
The `sandcage-ssh` volume does not exist or was removed. Re-run:
```
sandcage setup ssh --refresh
```
Or, if no config exists yet:
```
sandcage setup ssh
```
### Permission denied inside container
If the agent cannot read keys, the volume population step may have failed or
the image does not have an `agent` user at the expected UID. Verify:
```
docker run --rm -v sandcage-ssh:/home/agent/.ssh:ro sandcage:latest ls -la /home/agent/.ssh
```
Private keys should be `600` and owned by `agent:agent`.
### No git SSH hosts found
```
sandcage: no git SSH hosts found in remotes or ~/.ssh/config
```
This means:
- The current directory has no SSH git remotes (all remotes use HTTPS).
- `~/.ssh/config` has no `Host` blocks with `User git`.
Add an SSH remote or configure your SSH config, then re-run setup.
### Migration from legacy bind mount
If your config contains a raw `~/.ssh:/home/agent/.ssh:ro` entry in `mounts`
without an `ssh_mode` field, sandcage detects this as a legacy configuration
and offers to migrate to volume mode. Accepting the migration removes the old
mount entry and writes `ssh_mode: volume` with discovered keys. Declining sets
`ssh_mode: bind` explicitly to suppress future prompts.