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:
parent
8922f763ef
commit
67f47bea06
6 changed files with 363 additions and 0 deletions
53
.dockerignore
Normal file
53
.dockerignore
Normal 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
38
docker/.env.example
Normal 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
38
docker/Dockerfile.sandbox
Normal 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
105
docker/README.md
Normal 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
34
docker/docker-compose.yml
Normal 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
|
||||
95
src/tests/docker-template.test.ts
Normal file
95
src/tests/docker-template.test.ts
Normal 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);
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue