Skip to content

Commit d0ee9e6

Browse files
author
Dylan Storey
committed
feat: add sandboxed Ralph execution and initiative mode
Docker sandbox: - Dockerfile extending official Claude sandbox with Metis CLI and plugin - run-sandboxed-ralph.sh launcher script with auth handling Ralph improvements: - New /metis-ralph-initiative command to execute all tasks under an initiative - Tasks no longer auto-transition to completed - user reviews first - Progress logged to task documents as working memory - State file renamed to .claude/metis-ralph-active.yaml (simpler format) - --sandboxed flag to run in isolated Docker container
1 parent f0897cb commit d0ee9e6

11 files changed

+997
-64
lines changed

docker/Dockerfile.claude-sandbox

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Claude Code Sandbox with Metis
2+
# Extends Docker's official Claude sandbox with Metis work management
3+
#
4+
# Base image includes:
5+
# - Claude Code with automated credential handling
6+
# - Docker CLI, GitHub CLI, Node.js, Go, Python 3, Git, ripgrep, jq
7+
# - Runs as non-root 'agent' user with sudo access
8+
#
9+
# This image adds:
10+
# - Metis CLI and MCP server
11+
# - Metis plugin for Claude Code
12+
13+
FROM docker/sandbox-templates:claude-code
14+
15+
# Install Rust (needed to build Metis)
16+
USER root
17+
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
18+
ENV PATH="/root/.cargo/bin:${PATH}"
19+
20+
# Install build dependencies for Metis
21+
RUN apt-get update && apt-get install -y \
22+
build-essential \
23+
pkg-config \
24+
libsqlite3-dev \
25+
libssl-dev \
26+
&& rm -rf /var/lib/apt/lists/*
27+
28+
# Clone and build Metis CLI (includes MCP server via `metis mcp` subcommand)
29+
ARG METIS_VERSION=main
30+
RUN git clone --depth 1 --branch ${METIS_VERSION} https://github.com/colliery-io/metis.git /tmp/metis \
31+
&& cd /tmp/metis \
32+
&& cargo build --release -p metis-docs-cli \
33+
&& cp target/release/metis /usr/local/bin/metis \
34+
&& chmod +x /usr/local/bin/metis \
35+
&& rm -rf /tmp/metis /root/.cargo/registry /root/.cargo/git
36+
37+
# Install Metis plugin for the agent user
38+
USER agent
39+
40+
# Clone Metis repo to get plugin files (uses same version as CLI build)
41+
ARG PLUGIN_VERSION=main
42+
RUN mkdir -p /home/agent/.claude/plugins/marketplaces/colliery-io-metis/plugins \
43+
&& git clone --depth 1 --branch ${PLUGIN_VERSION} https://github.com/colliery-io/metis.git /tmp/metis-plugin \
44+
&& cp -r /tmp/metis-plugin/plugins/metis /home/agent/.claude/plugins/marketplaces/colliery-io-metis/plugins/ \
45+
&& rm -rf /tmp/metis-plugin
46+
47+
# Configure MCP server for Metis
48+
RUN mkdir -p /home/agent/.claude && \
49+
echo '{"mcpServers":{"metis":{"command":"metis","args":["mcp","--project-path","/workspace"]}}}' > /home/agent/.mcp.json
50+
51+
# Back to agent user for runtime
52+
WORKDIR /workspace
53+
CMD ["claude"]

docker/claude-sandbox.sh

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#!/bin/bash
2+
# Run Claude Code in isolated container
3+
#
4+
# Usage:
5+
# ./claude-sandbox.sh # Normal sandboxed session (no network)
6+
# ./claude-sandbox.sh --auth # Enable network for OAuth login
7+
# ./claude-sandbox.sh -- <args> # Pass args to claude
8+
#
9+
# OAuth: Tokens are stored in ~/.claude on your host. The container mounts
10+
# this directory, so once authenticated, tokens persist across runs.
11+
# If you need to re-authenticate, use --auth flag.
12+
13+
set -e
14+
15+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16+
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
17+
IMAGE_NAME="claude-sandbox-mimir"
18+
19+
# Parse arguments
20+
NETWORK_MODE="none"
21+
CLAUDE_ARGS=()
22+
23+
while [[ $# -gt 0 ]]; do
24+
case $1 in
25+
--auth)
26+
NETWORK_MODE="host"
27+
echo "⚠️ Network enabled for OAuth authentication"
28+
shift
29+
;;
30+
--)
31+
shift
32+
CLAUDE_ARGS=("$@")
33+
break
34+
;;
35+
*)
36+
CLAUDE_ARGS+=("$1")
37+
shift
38+
;;
39+
esac
40+
done
41+
42+
# Build if image doesn't exist
43+
if ! docker image inspect "$IMAGE_NAME" &>/dev/null; then
44+
echo "Building sandbox image (this may take a few minutes)..."
45+
docker build -t "$IMAGE_NAME" -f "$SCRIPT_DIR/Dockerfile.claude-sandbox" "$SCRIPT_DIR"
46+
fi
47+
48+
echo "Starting Claude Code sandbox..."
49+
echo " Project: $PROJECT_DIR"
50+
echo " Network: $NETWORK_MODE"
51+
echo ""
52+
53+
# Run container
54+
docker run -it --rm \
55+
-v "$PROJECT_DIR:/workspace" \
56+
-v "$HOME/.claude:/root/.claude" \
57+
--network "$NETWORK_MODE" \
58+
--workdir /workspace \
59+
"$IMAGE_NAME" \
60+
claude "${CLAUDE_ARGS[@]}"

docker/run-sandboxed-ralph.sh

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
#!/bin/bash
2+
# Run Metis Ralph in a sandboxed Docker container
3+
#
4+
# Usage:
5+
# ./run-sandboxed-ralph.sh <SHORT_CODE> [OPTIONS]
6+
#
7+
# Options:
8+
# --task Run single task (auto-detected from short code)
9+
# --max-iterations N Maximum iterations (default: unlimited)
10+
# --attach Attach to container instead of background
11+
#
12+
# Uses Docker's official sandbox feature which:
13+
# - Handles OAuth authentication automatically
14+
# - Stores credentials in persistent Docker volume
15+
# - Provides isolated execution environment
16+
# - Includes Claude Code and common dev tools
17+
18+
set -e
19+
20+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21+
22+
# Parse arguments
23+
SHORT_CODE=""
24+
MAX_ITERATIONS=""
25+
ATTACH_MODE=false
26+
TASK_MODE=false
27+
28+
while [[ $# -gt 0 ]]; do
29+
case $1 in
30+
--max-iterations)
31+
MAX_ITERATIONS="$2"
32+
shift 2
33+
;;
34+
--attach)
35+
ATTACH_MODE=true
36+
shift
37+
;;
38+
--task)
39+
TASK_MODE=true
40+
shift
41+
;;
42+
-h|--help)
43+
cat << 'EOF'
44+
Run Metis Ralph in a sandboxed Docker container
45+
46+
USAGE:
47+
./run-sandboxed-ralph.sh <SHORT_CODE> [OPTIONS]
48+
49+
ARGUMENTS:
50+
SHORT_CODE Task (PROJ-T-NNNN) or Initiative (PROJ-I-NNNN) short code
51+
52+
OPTIONS:
53+
--task Execute a single task (auto-detected from code format)
54+
--max-iterations N Maximum iterations before auto-stop
55+
--attach Attach to container (default: background)
56+
-h, --help Show this help
57+
58+
AUTHENTICATION:
59+
Uses Docker's official sandbox which handles OAuth automatically.
60+
On first run, you'll be prompted to authenticate via browser.
61+
Credentials are stored in Docker volume 'docker-claude-sandbox-data'.
62+
63+
EXAMPLES:
64+
# Run all tasks under an initiative
65+
./run-sandboxed-ralph.sh PROJ-I-0001
66+
67+
# Run a single task
68+
./run-sandboxed-ralph.sh PROJ-T-0001
69+
70+
# Attach to see output in real-time
71+
./run-sandboxed-ralph.sh PROJ-T-0001 --attach
72+
73+
COMPLETION:
74+
Progress is logged to Metis documents in .metis/
75+
EOF
76+
exit 0
77+
;;
78+
-*)
79+
echo "Unknown option: $1" >&2
80+
exit 1
81+
;;
82+
*)
83+
if [[ -z "$SHORT_CODE" ]]; then
84+
SHORT_CODE="$1"
85+
fi
86+
shift
87+
;;
88+
esac
89+
done
90+
91+
# Validate short code
92+
if [[ -z "$SHORT_CODE" ]]; then
93+
echo "Error: No short code provided" >&2
94+
echo "Usage: ./run-sandboxed-ralph.sh <SHORT_CODE> [OPTIONS]" >&2
95+
exit 1
96+
fi
97+
98+
# Determine mode from short code if not explicitly set
99+
if [[ "$SHORT_CODE" =~ ^[A-Z]+-T-[0-9]+$ ]]; then
100+
TASK_MODE=true
101+
CONTAINER_PREFIX="ralph-task"
102+
MODE_NAME="Task"
103+
RALPH_CMD="/metis-ralph"
104+
elif [[ "$SHORT_CODE" =~ ^[A-Z]+-I-[0-9]+$ ]]; then
105+
CONTAINER_PREFIX="ralph-initiative"
106+
MODE_NAME="Initiative"
107+
RALPH_CMD="/metis-ralph-initiative"
108+
else
109+
echo "Error: Invalid short code format: $SHORT_CODE" >&2
110+
echo "Expected: PREFIX-T-NNNN (task) or PREFIX-I-NNNN (initiative)" >&2
111+
exit 1
112+
fi
113+
114+
# Find project root (look for .metis directory)
115+
PROJECT_DIR="$(pwd)"
116+
while [[ "$PROJECT_DIR" != "/" ]]; do
117+
if [[ -d "$PROJECT_DIR/.metis" ]]; then
118+
break
119+
fi
120+
PROJECT_DIR="$(dirname "$PROJECT_DIR")"
121+
done
122+
123+
if [[ ! -d "$PROJECT_DIR/.metis" ]]; then
124+
echo "Error: Could not find .metis directory" >&2
125+
echo "Run from within a Metis project directory" >&2
126+
exit 1
127+
fi
128+
129+
echo "========================================"
130+
echo " Sandboxed Ralph Launcher"
131+
echo "========================================"
132+
echo ""
133+
echo "Mode: $MODE_NAME"
134+
echo "Short Code: $SHORT_CODE"
135+
echo "Project: $PROJECT_DIR"
136+
echo ""
137+
138+
# Build the prompt for Claude
139+
RALPH_PROMPT="Execute $RALPH_CMD $SHORT_CODE"
140+
if [[ -n "$MAX_ITERATIONS" ]]; then
141+
RALPH_PROMPT="$RALPH_PROMPT --max-iterations $MAX_ITERATIONS"
142+
fi
143+
144+
# Check if docker sandbox is available
145+
if ! docker sandbox --help &>/dev/null 2>&1; then
146+
echo "Error: 'docker sandbox' command not available" >&2
147+
echo "" >&2
148+
echo "Docker sandbox requires Docker Desktop with AI features enabled." >&2
149+
echo "See: https://docs.docker.com/ai/sandboxes/get-started/" >&2
150+
exit 1
151+
fi
152+
153+
# Check if sandbox is authenticated
154+
# Credentials are stored inside the sandbox container at /home/agent/.claude/.credentials.json
155+
check_sandbox_auth() {
156+
# Find any running claude sandbox for this workspace
157+
local container_name
158+
container_name=$(docker ps --filter "label=docker/sandbox=true" --format "{{.Names}}" 2>/dev/null | head -1)
159+
160+
if [[ -n "$container_name" ]]; then
161+
# Check if credentials exist in the running container
162+
local has_creds
163+
has_creds=$(docker exec "$container_name" test -f /home/agent/.claude/.credentials.json 2>/dev/null && echo "yes")
164+
if [[ "$has_creds" == "yes" ]]; then
165+
echo "$container_name" # Return container name for reuse
166+
return 0
167+
fi
168+
fi
169+
170+
return 1
171+
}
172+
173+
echo "Checking sandbox authentication..."
174+
EXISTING_CONTAINER=$(check_sandbox_auth)
175+
if [[ -z "$EXISTING_CONTAINER" ]]; then
176+
echo ""
177+
echo "========================================"
178+
echo " Authentication Required"
179+
echo "========================================"
180+
echo ""
181+
echo "No authenticated Docker sandbox found."
182+
echo ""
183+
echo "Please run this command in your terminal:"
184+
echo ""
185+
echo " docker sandbox run -w $PROJECT_DIR claude"
186+
echo ""
187+
echo "This will:"
188+
echo " 1. Open a browser for OAuth login"
189+
echo " 2. Start an interactive Claude session"
190+
echo " 3. Keep the sandbox running for Ralph to use"
191+
echo ""
192+
echo "IMPORTANT: Keep the sandbox running (don't exit)."
193+
echo "Then re-run this script in another terminal."
194+
echo ""
195+
echo "Alternatively, use an API key:"
196+
echo ""
197+
echo " export ANTHROPIC_API_KEY='sk-ant-...'"
198+
echo " docker sandbox run -e ANTHROPIC_API_KEY -w $PROJECT_DIR claude"
199+
echo ""
200+
exit 1
201+
fi
202+
echo "Authentication: OK"
203+
echo "Using sandbox: $EXISTING_CONTAINER"
204+
echo ""
205+
206+
echo "Launching Ralph..."
207+
echo "Prompt: $RALPH_PROMPT"
208+
echo ""
209+
210+
LOG_FILE="$PROJECT_DIR/.metis/ralph-sandbox-$(date +%Y%m%d-%H%M%S).log"
211+
212+
# Run with docker sandbox
213+
if [[ "$ATTACH_MODE" == "true" ]]; then
214+
# Attached mode - run in foreground
215+
echo "Running in foreground..."
216+
docker exec -it "$EXISTING_CONTAINER" claude --print "$RALPH_PROMPT"
217+
else
218+
# Detached mode - run in background
219+
echo "Running in background..."
220+
echo ""
221+
222+
# Run the command inside the existing sandbox container
223+
nohup docker exec "$EXISTING_CONTAINER" claude --print "$RALPH_PROMPT" > "$LOG_FILE" 2>&1 &
224+
EXEC_PID=$!
225+
226+
echo "Ralph started!"
227+
echo ""
228+
echo " Container: $EXISTING_CONTAINER"
229+
echo " PID: $EXEC_PID"
230+
echo " Log: $LOG_FILE"
231+
echo ""
232+
echo "MONITORING:"
233+
echo " tail -f $LOG_FILE"
234+
echo ""
235+
echo "PROGRESS:"
236+
echo " Progress is logged to Metis documents in:"
237+
echo " $PROJECT_DIR/.metis/"
238+
echo ""
239+
echo "STOP:"
240+
echo " kill $EXEC_PID"
241+
echo ""
242+
fi
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
---
22
description: "Cancel active Metis Ralph loop"
3-
allowed-tools: ["Bash(test -f .claude/metis-ralph.local.md:*)", "Bash(rm .claude/metis-ralph.local.md)", "Read(.claude/metis-ralph.local.md)"]
3+
allowed-tools: ["Bash(test -f .claude/metis-ralph-active.yaml:*)", "Bash(rm .claude/metis-ralph-active.yaml)", "Read(.claude/metis-ralph-active.yaml)"]
44
hide-from-slash-command-tool: "true"
55
---
66

77
# Cancel Metis Ralph
88

99
To cancel the Metis Ralph loop:
1010

11-
1. Check if `.claude/metis-ralph.local.md` exists using Bash: `test -f .claude/metis-ralph.local.md && echo "EXISTS" || echo "NOT_FOUND"`
11+
1. Check if `.claude/metis-ralph-active.yaml` exists using Bash: `test -f .claude/metis-ralph-active.yaml && echo "EXISTS" || echo "NOT_FOUND"`
1212

1313
2. **If NOT_FOUND**: Say "No active Metis Ralph loop found."
1414

1515
3. **If EXISTS**:
16-
- Read `.claude/metis-ralph.local.md` to get the current state:
16+
- Read `.claude/metis-ralph-active.yaml` to get the current state:
1717
- `iteration:` field for iteration count
1818
- `mode:` field for loop type (task or decompose)
1919
- `short_code:` field for the document being worked on
20-
- Remove the file using Bash: `rm .claude/metis-ralph.local.md`
20+
- Remove the file using Bash: `rm .claude/metis-ralph-active.yaml`
2121
- Report: "Cancelled Metis Ralph loop for [SHORT_CODE] (was at iteration N, mode: MODE)"
2222

23-
Note: Cancelling does NOT revert any Metis document phase transitions that were made during the loop.
23+
Note: Cancelling does NOT revert any Metis document phase transitions. Progress logged to the task document is preserved.

0 commit comments

Comments
 (0)