🛰️ export from upstream (251518c)
This commit is contained in:
@@ -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.
|
||||
@@ -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
@@ -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.
|
||||
Reference in New Issue
Block a user