🛰️ 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
+74 -88
View File
@@ -11,20 +11,14 @@
--- ---
> [!CAUTION] > [!CAUTION]
> **⚠️ Unreleased Software ⚠️** > **Unreleased Software**
> >
> 🚧 This project is under active development and **not yet released**. APIs, configuration formats, and behavior may change without notice. Please **do not use without contacting the author** about the current state of the project. 🚧 > This project is under active development and **not yet released**. APIs, configuration formats, and behavior may change without notice. Please **do not use without contacting the author** about the current state of the project.
> [!WARNING] > [!WARNING]
> **⚠️ Development Tool Only ⚠️** > **Development Tool Only**
> >
> 🚧 Sandcage is designed for **local development use**. Do **not** use it in CI pipelines or production environments — container isolation is not yet hardened for those contexts. 🚧 > Sandcage is designed for **local development use**. Do **not** use it in CI pipelines or production environments — container isolation is not yet hardened for those contexts.
### Planned Features
- **Support for custom harnesses** — bring your own agent runtime beyond the built-in Claude Code, Codex, and Gemini CLI
- **Full encapsulation hardening** — for worker and CI environments, ensuring complete sandboxing of file system, network, and credentials
- **ACP integration** via [`dirigate`](https://github.com/dirigence/dirigate) — Agent Communication Protocol support for structured agent orchestration
--- ---
@@ -36,107 +30,99 @@ Sandcage gives each agent its own container with the tools it needs. Your projec
Multiple agents can run side by side. A persistent home directory means config and credentials survive between sessions, so you are not re-authenticating every time. Multiple agents can run side by side. A persistent home directory means config and credentials survive between sessions, so you are not re-authenticating every time.
<!-- TODO: Terminal demo
Recording tool: https://github.com/charmbracelet/vhs
Will show: sandcage build → sandcage claude → agent working → exit
Place animated GIF or SVG here once recorded.
-->
## Quick Start
**Prerequisites:** Docker (running) and a Rust toolchain (cargo).
```bash
# Install
cargo install --git https://github.com/dirigence/sandcage
# Build the container image
sandcage build
# Run Claude Code in the current project
sandcage claude
```
That's it. Sandcage resolves your project to its git root, mounts it into the container, and drops you into the agent.
## Usage
```
sandcage claude # Claude Code agent
sandcage codex # Codex agent
sandcage gemini # Gemini CLI agent
sandcage shell # interactive shell, same environment
sandcage claude -p ~/project # run in a specific project
sandcage claude -- --resume # forward args to the agent
sandcage claude --shell # shell for debugging
sandcage build # build/rebuild container image
sandcage init # generate .sandcage.yml for your project
sandcage setup ssh # configure SSH key access for containers
```
## How It Works ## How It Works
<p align="center"> <p align="center">
<img src="topology.svg" alt="Sandcage topology — host, Docker, container, volume mounts" width="720"> <img src="topology.svg" alt="Sandcage topology — host, Docker, container, volume mounts" width="720">
</p> </p>
1. You run `sandcage claude` (or `codex`, `gemini`, or `shell`) from your project directory 1. You run `sandcage claude` from your project directory
2. Sandcage resolves your workspace to the git root and builds Docker compose arguments 2. Sandcage resolves the workspace, loads layered config, and generates a compose definition
3. Your project, persistent home, and (optionally) SSH keys are mounted into the container 3. Your project, persistent home, and (optionally) SSH keys are mounted into the container
4. The agent runs as the container entrypoint, working in the mounted workspace 4. The agent runs as the container entrypoint, working in the mounted workspace
5. All file changes are immediately visible on your host 5. All file changes are immediately visible on your host
## Quick Start
### Prerequisites
- **Docker** (daemon must be running)
- **Rust toolchain** (cargo) — or download a prebuilt binary from [Releases](https://github.com/dirigence/sandcage/releases)
### Install
```bash
cargo install --git https://github.com/dirigence/sandcage
```
### Build images and run
```bash
sandcage build # build container image
sandcage claude # start Claude Code in the current project
```
That is it. Sandcage resolves your project to its git root, mounts it into the container, and drops you into the agent.
### More commands
```
sandcage claude -p ~/project # run in a specific project
sandcage claude -- --resume # forward args to the agent
sandcage codex -p ~/project # run Codex instead
sandcage gemini -p ~/project # run Gemini CLI instead
sandcage shell # interactive shell, same environment
sandcage claude --shell # shell in the Claude image (debugging)
sandcage init # detect ecosystem, generate .sandcage.yml
sandcage setup ssh # select and copy SSH keys for containers
sandcage setup ssh --global # store SSH config globally
sandcage setup ssh --refresh # re-sync keys after changes
```
## Configuration ## Configuration
<p align="center"> <p align="center">
<img src="config-layers.svg" alt="Configuration layering — defaults, global, project, local, CLI flags" width="720"> <img src="config-layers.svg" alt="Configuration layering — defaults, global, project, local, CLI flags" width="720">
</p> </p>
Configuration is layered. Each level overrides the one below it, so you only set what you need: Configuration is layered — each level overrides the one below:
**Compiled defaults** — sensible out of the box | Layer | File | Format |
**Global config** (`~/.sandcage/config.toml`) — user-wide preferences |-------|------|--------|
**Project config** (`.sandcage.yml`) — per-project setup, checked into version control | Global | `~/.sandcage/config.toml` | TOML |
**Local config** (`.sandcage.local.yml`) — personal overrides, gitignored (SSH keys, secrets, local mounts) | Project | `.sandcage.yml` | YAML |
**CLI flags** — per-invocation overrides | Local | `.sandcage.local.yml` | YAML |
### .sandcage.yml example
```yaml
packages:
- ripgrep
- fd-find
toolchains:
rust: stable
node: "20"
env:
DATABASE_URL: "postgres://localhost:5432/dev"
agent_args:
claude:
- --dangerously-skip-permissions
shell: zsh
# Enable/disable built-in services
services:
gemini:
enabled: false
# Control which services `sandcage build` prepares by default
# default_services:
# - claude
# - shell
```
SSH key access is configured separately via `sandcage setup ssh`, which selects only the keys needed for git and copies them into a dedicated Docker volume.
Run `sandcage init` to generate a starter config — it detects your project ecosystem (Rust, Node, Python, Go) and suggests appropriate toolchains and packages. Run `sandcage init` to generate a starter config — it detects your project ecosystem (Rust, Node, Python, Go) and suggests appropriate toolchains and packages.
## Docker Image ```yaml
# .sandcage.yml — minimal example
toolchains:
node: "20"
packages:
- ripgrep
services:
gemini:
enabled: false
```
Sandcage uses a single image (`sandcage`) based on Debian bookworm-slim, packed with dev tools: git, openssh-client, ripgrep, fd, jq, curl, zsh, bash, sudo, just, and uv. See **[Configuration Reference](docs/configuration.md)** for all available options.
AI agents (Claude Code, Codex, Gemini CLI) are installed on first run into the persistent home directory and auto-update themselves — no agent binaries baked into the image. ## Documentation
Build with `sandcage build`. Use `--force` to rebuild unconditionally. You can also specify which services to build: `sandcage build claude codex`. | Document | Description |
|----------|-------------|
| [Configuration Reference](docs/configuration.md) | All config fields, merge behavior, and examples |
| [Command Reference](docs/commands.md) | Every subcommand, flag, and usage pattern |
| [Docker Image](docs/docker-image.md) | Base image contents, building, custom Dockerfiles |
| [SSH Key Access](docs/ssh.md) | Setting up SSH for git inside containers |
## Planned Features
- **Support for custom harnesses** — bring your own agent runtime beyond the built-in Claude Code, Codex, and Gemini CLI
- **Full encapsulation hardening** — for worker and CI environments, ensuring complete sandboxing of file system, network, and credentials
- **ACP integration** via [`dirigate`](https://github.com/dirigence/dirigate) — Agent Communication Protocol support for structured agent orchestration
## Cross-Platform ## Cross-Platform
+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.