From 67f47bea06773d296f13c57a18588415020f0410 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 24 Mar 2026 15:57:59 -0400 Subject: [PATCH] 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) --- .dockerignore | 53 +++++++++++++++ docker/.env.example | 38 +++++++++++ docker/Dockerfile.sandbox | 38 +++++++++++ docker/README.md | 105 ++++++++++++++++++++++++++++++ docker/docker-compose.yml | 34 ++++++++++ src/tests/docker-template.test.ts | 95 +++++++++++++++++++++++++++ 6 files changed, 363 insertions(+) create mode 100644 .dockerignore create mode 100644 docker/.env.example create mode 100644 docker/Dockerfile.sandbox create mode 100644 docker/README.md create mode 100644 docker/docker-compose.yml create mode 100644 src/tests/docker-template.test.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..444ee5c7f --- /dev/null +++ b/.dockerignore @@ -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 diff --git a/docker/.env.example b/docker/.env.example new file mode 100644 index 000000000..71c2f4802 --- /dev/null +++ b/docker/.env.example @@ -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 diff --git a/docker/Dockerfile.sandbox b/docker/Dockerfile.sandbox new file mode 100644 index 000000000..af1bf40d1 --- /dev/null +++ b/docker/Dockerfile.sandbox @@ -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"] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 000000000..a4bf7a65e --- /dev/null +++ b/docker/README.md @@ -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. diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 000000000..d685f3a00 --- /dev/null +++ b/docker/docker-compose.yml @@ -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 diff --git a/src/tests/docker-template.test.ts b/src/tests/docker-template.test.ts new file mode 100644 index 000000000..946b20d51 --- /dev/null +++ b/src/tests/docker-template.test.ts @@ -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); +});