feat(docker): add official Docker sandbox template for isolated GSD auto mode (#2360)

Ship a Dockerfile.sandbox, docker-compose.yml, .env.example, and docs so
users can run GSD auto mode inside an isolated Docker sandbox (MicroVM)
without risk to the host filesystem, SSH keys, or other projects.

- Dockerfile.sandbox: Node 22 base, gsd-pi pre-installed, non-root user, port 3000
- docker-compose.yml: workspace volume mount, persistent .gsd state, env_file support
- .env.example: template for LLM provider keys and optional tool credentials
- docker/README.md: setup guide covering sandbox CLI, Compose, two-terminal workflow,
  credential injection, and network allowlisting
- .dockerignore: project-root ignore file for efficient Docker builds
- src/tests/docker-template.test.ts: 13 structural tests verifying all template files

Fixes #1544

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher 2026-03-24 15:57:59 -04:00 committed by GitHub
parent 8922f763ef
commit 67f47bea06
6 changed files with 363 additions and 0 deletions

53
.dockerignore Normal file
View file

@ -0,0 +1,53 @@
# ── Build artifacts ──
dist/
build/
coverage/
*.tsbuildinfo
# ── Dependencies ──
node_modules/
packages/*/node_modules/
# ── Environment & secrets ──
.env
.env.*
!.env.example
.gsd/
# ── IDE & OS ──
.idea/
.vscode/
*.code-workspace
.DS_Store
Thumbs.db
# ── Git ──
.git/
.github/
# ── Development files ──
.claude/
.plans/
.artifacts/
.bg-shell/
.bg_shell
*.log
*.swp
*.swo
*~
tmp/
.cache/
# ── Native build artifacts ──
native/
target/
# ── Test fixtures ──
tests/
# ── Lock files (npm is canonical via package-lock.json) ──
pnpm-lock.yaml
bun.lock
# ── Tarballs ──
*.tgz

38
docker/.env.example Normal file
View file

@ -0,0 +1,38 @@
# ──────────────────────────────────────────────
# GSD Docker Sandbox — Environment Variables
# Copy this file to .env and fill in your keys.
# ──────────────────────────────────────────────
# ── LLM Provider API Keys (at least one required) ──
# Anthropic (Claude)
# ANTHROPIC_API_KEY=sk-ant-...
# OpenAI
# OPENAI_API_KEY=sk-...
# Google (Gemini)
# GOOGLE_API_KEY=...
# OpenRouter (multi-provider gateway)
# OPENROUTER_API_KEY=sk-or-...
# ── Optional: Research & Search Tools ──
# Brave Search API
# BRAVE_API_KEY=...
# Tavily Search API
# TAVILY_API_KEY=tvly-...
# Jina AI (reader/search)
# JINA_API_KEY=...
# ── Optional: Git & GitHub ──
# GitHub personal access token (for PR operations)
# GITHUB_TOKEN=ghp_...
# Git author identity inside the sandbox
# GIT_AUTHOR_NAME=Your Name
# GIT_AUTHOR_EMAIL=you@example.com

38
docker/Dockerfile.sandbox Normal file
View file

@ -0,0 +1,38 @@
# ──────────────────────────────────────────────
# GSD Docker Sandbox Template
# Base: docker/sandbox-templates:shell
# Purpose: Isolated environment for GSD auto mode
# Usage: docker sandbox create --template ./docker
# ──────────────────────────────────────────────
FROM node:22-bookworm-slim
# System dependencies required by GSD
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
curl \
ca-certificates \
openssh-client \
&& rm -rf /var/lib/apt/lists/*
# Install GSD globally — version controlled via build arg
ARG GSD_VERSION=latest
RUN npm install -g gsd-pi@${GSD_VERSION}
# Create non-root user for sandbox isolation
RUN groupadd --gid 1000 gsd \
&& useradd --uid 1000 --gid gsd --shell /bin/bash --create-home gsd
# Persistent GSD state directory
RUN mkdir -p /home/gsd/.gsd && chown -R gsd:gsd /home/gsd/.gsd
# Workspace directory — synced from host via Docker sandbox
WORKDIR /workspace
RUN chown gsd:gsd /workspace
USER gsd
# Expose default GSD web UI port
EXPOSE 3000
ENTRYPOINT ["gsd"]
CMD ["--help"]

105
docker/README.md Normal file
View file

@ -0,0 +1,105 @@
# GSD Docker Sandbox
Run GSD auto mode inside an isolated Docker sandbox so it cannot touch your host filesystem, SSH keys, or other projects.
## Prerequisites
- Docker Desktop 4.58+ (macOS or Windows; Linux support is experimental)
- At least one LLM provider API key
## Quick Start
### Option A: Docker Sandbox CLI (recommended)
Docker Sandboxes provide MicroVM isolation — each sandbox runs in a lightweight VM with its own kernel and private Docker daemon.
```bash
# Create a sandbox from the template
docker sandbox create --template ./docker --name gsd-sandbox
# Shell into the sandbox
docker sandbox exec -it gsd-sandbox bash
# Inside the sandbox, run GSD
gsd auto "implement the feature described in issue #42"
```
### Option B: Docker Compose
For environments without Docker Sandbox support, use Compose for container-level isolation:
```bash
# 1. Configure API keys
cp docker/.env.example docker/.env
# Edit docker/.env with your keys
# 2. Start the sandbox
docker compose -f docker/docker-compose.yml up -d
# 3. Shell into the container
docker exec -it gsd-sandbox bash
# 4. Run GSD inside the container
gsd auto "implement the feature described in issue #42"
```
## Two-Terminal Workflow
GSD's recommended workflow uses two terminals — one for auto mode, one for interactive discussion:
```bash
# Terminal 1: auto mode
docker sandbox exec -it gsd-sandbox bash
gsd auto "your task description"
# Terminal 2: discuss / monitor
docker sandbox exec -it gsd-sandbox bash
gsd discuss
```
With Docker Compose, replace `docker sandbox exec` with `docker exec`.
## Credential Injection
### Docker Sandbox (automatic)
Docker's proxy layer forwards API keys set in your host shell config (`~/.bashrc`, `~/.zshrc`) into the sandbox automatically. Keys are never stored inside the sandbox.
### Docker Compose (manual)
Copy `docker/.env.example` to `docker/.env` and fill in your keys. The `.env` file is gitignored and never committed.
## Network Allowlisting
If you restrict outbound network access in your sandbox, GSD needs these endpoints:
| Purpose | Endpoints |
|---------|-----------|
| LLM APIs | `api.anthropic.com`, `api.openai.com`, `generativelanguage.googleapis.com`, `openrouter.ai` |
| Package registry | `registry.npmjs.org` |
| Research tools | `api.search.brave.com`, `api.tavily.com`, `r.jina.ai` |
| GitHub | `api.github.com`, `github.com` |
## Customizing the Image
Build with a specific GSD version:
```bash
docker compose -f docker/docker-compose.yml build --build-arg GSD_VERSION=2.43.0
```
## Cleanup
```bash
# Docker Sandbox
docker sandbox rm gsd-sandbox
# Docker Compose
docker compose -f docker/docker-compose.yml down -v
```
## Known Limitations
- **macOS/Windows only**: Docker Sandboxes require Docker Desktop 4.58+. Linux sandbox support is experimental.
- **Environment parity**: The sandbox runs Ubuntu (Debian). macOS-only dependencies may not work inside the sandbox.
- **Named agent registration**: Docker Desktop's built-in named agents (claude, codex, etc.) are registered by Docker itself. Third-party tools cannot register new named agents. GSD uses the generic shell sandbox type with a custom template instead.

34
docker/docker-compose.yml Normal file
View file

@ -0,0 +1,34 @@
# Docker Compose for running GSD in a sandbox
# Usage: docker compose -f docker/docker-compose.yml up
#
# Copy docker/.env.example to docker/.env and fill in your API keys first.
# See docker/README.md for full setup instructions.
services:
gsd:
build:
context: .
dockerfile: Dockerfile.sandbox
args:
GSD_VERSION: latest
container_name: gsd-sandbox
ports:
- "3000:3000"
volumes:
# Sync project code into the sandbox
- ../:/workspace
# Persistent GSD state across container restarts
- gsd-state:/home/gsd/.gsd
env_file:
- .env
environment:
- NODE_ENV=development
user: "1000:1000"
stdin_open: true
tty: true
# Override entrypoint for interactive shell access
# entrypoint: /bin/bash
volumes:
gsd-state:
driver: local

View file

@ -0,0 +1,95 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync, existsSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = resolve(__dirname, "../..");
function readFile(relativePath: string): string {
const full = resolve(root, relativePath);
assert.ok(existsSync(full), `expected ${relativePath} to exist`);
return readFileSync(full, "utf-8");
}
// ── Dockerfile.sandbox ──
test("docker/Dockerfile.sandbox exists and uses Node 22 base", () => {
const content = readFile("docker/Dockerfile.sandbox");
assert.match(content, /FROM node:22/);
});
test("docker/Dockerfile.sandbox installs gsd-pi globally", () => {
const content = readFile("docker/Dockerfile.sandbox");
assert.match(content, /npm install -g gsd-pi/);
});
test("docker/Dockerfile.sandbox creates a non-root user", () => {
const content = readFile("docker/Dockerfile.sandbox");
assert.match(content, /useradd/);
assert.match(content, /USER gsd/);
});
test("docker/Dockerfile.sandbox exposes port 3000", () => {
const content = readFile("docker/Dockerfile.sandbox");
assert.match(content, /EXPOSE 3000/);
});
test("docker/Dockerfile.sandbox installs git", () => {
const content = readFile("docker/Dockerfile.sandbox");
assert.match(content, /git/);
});
// ── docker-compose.yml ──
test("docker/docker-compose.yml exists and defines gsd service", () => {
const content = readFile("docker/docker-compose.yml");
assert.match(content, /services:/);
assert.match(content, /gsd:/);
});
test("docker/docker-compose.yml mounts workspace volume", () => {
const content = readFile("docker/docker-compose.yml");
assert.match(content, /\/workspace/);
});
test("docker/docker-compose.yml references Dockerfile.sandbox", () => {
const content = readFile("docker/docker-compose.yml");
assert.match(content, /Dockerfile\.sandbox/);
});
test("docker/docker-compose.yml maps port 3000", () => {
const content = readFile("docker/docker-compose.yml");
assert.match(content, /3000:3000/);
});
// ── .env.example ──
test("docker/.env.example exists and lists ANTHROPIC_API_KEY", () => {
const content = readFile("docker/.env.example");
assert.match(content, /ANTHROPIC_API_KEY/);
});
test("docker/.env.example lists OPENAI_API_KEY", () => {
const content = readFile("docker/.env.example");
assert.match(content, /OPENAI_API_KEY/);
});
// ── .dockerignore ──
test(".dockerignore exists at project root", () => {
const content = readFile(".dockerignore");
assert.match(content, /node_modules/);
assert.match(content, /\.env/);
assert.match(content, /dist/);
});
// ── README ──
test("docker/README.md exists and documents sandbox usage", () => {
const content = readFile("docker/README.md");
assert.match(content, /Docker Sandbox/i);
assert.match(content, /docker sandbox create/);
assert.match(content, /Network Allowlisting/i);
});