🛰️ export from upstream (251518c)
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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