Skip to content

Commit a857775

Browse files
authored
feat(sandbox): unified dockerfile for claude and codex (#207)
* feat(sandbox): unified dockerfile for claude and codex * fix(sandbox): add pragma allowlist to sample.env to clear detect-secrets false positive * fix(sandbox): address coderabbit review comments
1 parent 68cbfbf commit a857775

5 files changed

Lines changed: 261 additions & 41 deletions

File tree

justfile

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,32 +8,58 @@ commit message:
88
git add -u
99
git commit -m "{{message}}"
1010

11-
image := "claude-sandbox"
11+
claude_image := "claude-sandbox"
12+
codex_image := "evolve-codex-sandbox"
1213
env_file := "sandbox/myenv"
1314
sandbox_dir := "sandbox"
1415
workspace := "demo/workspace"
1516
trace := "false"
1617
learn := "false"
1718

18-
# Build the sandbox Docker image
19-
sandbox-build:
20-
docker build -t {{image}} {{sandbox_dir}}
19+
# Build sandbox Docker image(s). Use target=claude or target=codex to build only one.
20+
sandbox-build target="all":
21+
#!/usr/bin/env sh
22+
set -e
23+
if [ "{{target}}" != "all" ] && [ "{{target}}" != "claude" ] && [ "{{target}}" != "codex" ]; then
24+
echo "Error: target must be one of: all, claude, codex" >&2
25+
exit 1
26+
fi
27+
if [ "{{target}}" = "all" ] || [ "{{target}}" = "claude" ]; then
28+
docker build --target claude -t {{claude_image}} {{sandbox_dir}}
29+
fi
30+
if [ "{{target}}" = "all" ] || [ "{{target}}" = "codex" ]; then
31+
docker build --target codex -t {{codex_image}} {{sandbox_dir}}
32+
fi
2133
2234
# Copy sample.env to myenv if it doesn't already exist
2335
sandbox-setup:
2436
@if [ ! -f {{env_file}} ]; then \
2537
cp sandbox/sample.env {{env_file}}; \
26-
echo "Created {{env_file}} — edit it and set your ANTHROPIC_API_KEY"; \
38+
echo "Created {{env_file}} — edit it and set your API keys"; \
2739
else \
2840
echo "{{env_file}} already exists, skipping"; \
2941
fi
3042

43+
# Remove sandbox Docker image(s). Use target=claude or target=codex to remove only one.
44+
sandbox-clean target="all":
45+
#!/usr/bin/env sh
46+
if [ "{{target}}" != "all" ] && [ "{{target}}" != "claude" ] && [ "{{target}}" != "codex" ]; then
47+
echo "Error: target must be one of: all, claude, codex" >&2
48+
exit 1
49+
fi
50+
if [ "{{target}}" = "all" ] || [ "{{target}}" = "claude" ]; then
51+
docker rmi {{claude_image}} || true
52+
fi
53+
if [ "{{target}}" = "all" ] || [ "{{target}}" = "codex" ]; then
54+
docker rmi {{codex_image}} || true
55+
fi
56+
3157
# Run an interactive Claude Code shell in the sandbox
32-
sandbox-run:
33-
docker run --rm -it --env-file {{env_file}} -v "$(cd {{workspace}} && pwd)":/workspace -v "$(pwd)/platform-integrations/claude/plugins":/plugins {{image}}
58+
claude-run:
59+
docker run --rm -it --env-file {{env_file}} -v "$(cd {{workspace}} && pwd)":/workspace -v "$(pwd)/platform-integrations/claude/plugins":/plugins {{claude_image}}
3460

3561
# Run a one-shot prompt in the sandbox (trace=true to summarize session, learn=true to run /evolve-lite:learn)
36-
sandbox-prompt prompt:
62+
claude-prompt prompt:
3763
#!/usr/bin/env sh
3864
export SANDBOX_PROMPT="$(cat <<'PROMPT_EOF'
3965
{{prompt}}
@@ -53,16 +79,20 @@ sandbox-prompt prompt:
5379
claude --plugin-dir /plugins/evolve-lite/ --dangerously-skip-permissions --continue -p '/evolve-lite:learn'
5480
"
5581
fi
56-
docker run --rm -it --env SANDBOX_PROMPT --env-file {{env_file}} -v "$(cd {{workspace}} && pwd)":/workspace -v "$(pwd)/platform-integrations/claude/plugins":/plugins {{image}} sh -c "
82+
docker run --rm -it --env SANDBOX_PROMPT --env-file {{env_file}} -v "$(cd {{workspace}} && pwd)":/workspace -v "$(pwd)/platform-integrations/claude/plugins":/plugins {{claude_image}} sh -c "
5783
claude --plugin-dir /plugins/evolve-lite/ --dangerously-skip-permissions -p \"\$SANDBOX_PROMPT\"
5884
$TRACE_CMD
5985
$LEARN_CMD
6086
"
6187
6288
# Smoke-test that Claude Code is installed and working
63-
sandbox-test:
64-
docker run --rm --env-file {{env_file}} {{image}} claude -p "who are you"
89+
claude-test:
90+
docker run --rm --env-file {{env_file}} {{claude_image}} claude -p "who are you"
91+
92+
# Run an interactive Codex shell in the sandbox
93+
codex-run:
94+
docker run --rm -it --env-file {{env_file}} -v "$(cd {{workspace}} && pwd)":/workspace {{codex_image}}
6595
66-
# Remove the sandbox Docker image
67-
sandbox-clean:
68-
docker rmi {{image}}
96+
# Smoke-test that Codex is installed and working
97+
codex-test:
98+
docker run --rm --env-file {{env_file}} {{codex_image}} codex exec --skip-git-repo-check "who are you"

sandbox/Dockerfile

Lines changed: 76 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,65 @@
1-
FROM debian:bookworm-slim
2-
3-
# Install Linux utilities, Python, and jq
4-
RUN apt-get update && apt-get install -y \
5-
# Basics & editors
6-
bash coreutils findutils grep sed gawk less nano vim tree file which procps psmisc sudo \
7-
# Networking
8-
curl wget ca-certificates iputils-ping dnsutils netcat-traditional iproute2 \
9-
# Git + archives
10-
git unzip zip tar gzip bzip2 xz-utils \
11-
# Build tools
12-
build-essential make \
13-
# Python
14-
python3 python3-venv python3-pip \
15-
# JSON parsing
1+
FROM debian:bookworm-slim AS base
2+
3+
RUN apt-get update && apt-get install -y --no-install-recommends \
4+
bash \
5+
coreutils \
6+
findutils \
7+
grep \
8+
sed \
9+
gawk \
10+
less \
11+
nano \
12+
vim \
13+
tree \
14+
file \
15+
which \
16+
procps \
17+
psmisc \
18+
sudo \
19+
curl \
20+
wget \
21+
ca-certificates \
22+
iputils-ping \
23+
dnsutils \
24+
netcat-traditional \
25+
iproute2 \
26+
git \
27+
unzip \
28+
zip \
29+
tar \
30+
gzip \
31+
bzip2 \
32+
xz-utils \
33+
build-essential \
34+
make \
35+
python3 \
36+
python3-venv \
37+
python3-pip \
1638
jq \
17-
&& rm -rf /var/lib/apt/lists/* \
18-
# GitHub CLI
19-
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
20-
-o /usr/share/keyrings/githubcli-archive-keyring.gpg \
21-
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
22-
> /etc/apt/sources.list.d/github-cli.list \
23-
&& apt-get update && apt-get install -y gh \
39+
openssh-client \
40+
ripgrep \
2441
&& rm -rf /var/lib/apt/lists/*
2542

26-
# Create non-root user
2743
RUN groupadd sandbox && useradd -m -g sandbox sandbox \
2844
&& mkdir -p /workspace && chown sandbox:sandbox /workspace
2945

30-
# Create Python venv and make it accessible to sandbox user
3146
RUN python3 -m venv /opt/claude-venv \
3247
&& /opt/claude-venv/bin/python -m pip install --upgrade pip \
3348
&& chown -R sandbox:sandbox /opt/claude-venv
3449

35-
# Install Claude Code as sandbox user
50+
51+
# Claude Code target
52+
FROM base AS claude
53+
3654
USER sandbox
55+
3756
RUN curl -fsSL -o /tmp/install.sh https://claude.ai/install.sh \
3857
&& bash /tmp/install.sh \
3958
&& rm /tmp/install.sh
4059

41-
# Add venv + Claude to PATH
4260
ENV HOME="/home/sandbox"
4361
ENV PATH="/opt/claude-venv/bin:/home/sandbox/.local/bin:${PATH}"
4462

45-
# Pre-populate bash history with common sandbox commands (most recent last)
4663
RUN cat <<'EOF' > /home/sandbox/.bash_history
4764
claude --plugin-dir /plugins/evolve-lite --dangerously-skip-permissions
4865
claude --dangerously-skip-permissions
@@ -51,3 +68,35 @@ EOF
5168
WORKDIR /workspace
5269

5370
CMD ["bash"]
71+
72+
73+
# Codex target
74+
FROM base AS codex
75+
76+
COPY --from=node:20-bookworm-slim /usr/local/bin/node /usr/local/bin/node
77+
COPY --from=node:20-bookworm-slim /usr/local/lib/node_modules /usr/local/lib/node_modules
78+
79+
RUN ln -sf /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
80+
&& ln -sf /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx
81+
82+
RUN npm install --global @openai/codex@0.122.0 \
83+
&& npm cache clean --force
84+
85+
RUN mkdir -p /codex-home \
86+
&& chown sandbox:sandbox /codex-home \
87+
&& chmod 0700 /codex-home
88+
89+
COPY codex/bootstrap_codex_config.py /usr/local/bin/bootstrap_codex_config.py
90+
COPY codex/entrypoint.sh /usr/local/bin/codex-container-entrypoint
91+
92+
RUN chmod 0755 /usr/local/bin/bootstrap_codex_config.py /usr/local/bin/codex-container-entrypoint
93+
94+
WORKDIR /workspace
95+
96+
USER sandbox
97+
98+
ENV HOME="/home/sandbox"
99+
ENV PYTHONUNBUFFERED=1
100+
101+
ENTRYPOINT ["codex-container-entrypoint"]
102+
CMD ["bash"]
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
#!/usr/bin/env python3
2+
"""Ensure a container-friendly Codex config exists."""
3+
4+
from __future__ import annotations
5+
6+
import os
7+
import re
8+
import sys
9+
from pathlib import Path
10+
11+
12+
def _matches_assignment(line: str, key: str) -> bool:
13+
return re.match(rf"^\s*{re.escape(key)}\s*=", line) is not None
14+
15+
16+
def _matches_table(line: str, table_name: str) -> bool:
17+
return re.match(rf"^\s*\[{re.escape(table_name)}\]\s*(?:#.*)?$", line) is not None
18+
19+
20+
def _is_table_header(line: str) -> bool:
21+
return re.match(r"^\s*\[", line) is not None
22+
23+
24+
def _ensure_trailing_newline(lines: list[str], index: int) -> None:
25+
if 0 <= index < len(lines) and not lines[index].endswith("\n"):
26+
lines[index] += "\n"
27+
28+
29+
def resolve_config_path(argv: list[str]) -> Path:
30+
if len(argv) > 2:
31+
raise SystemExit("usage: bootstrap_codex_config.py [config-path]")
32+
if len(argv) == 2:
33+
return Path(argv[1]).expanduser()
34+
35+
codex_home = Path(os.environ.get("CODEX_HOME", "~/.codex")).expanduser()
36+
return codex_home / "config.toml"
37+
38+
39+
def ensure_top_level_setting(lines: list[str], key: str, value: str) -> bool:
40+
rendered = f'{key} = "{value}"\n'
41+
for index, line in enumerate(lines):
42+
if _is_table_header(line):
43+
break
44+
if _matches_assignment(line, key):
45+
if line.strip() == rendered.strip():
46+
return False
47+
lines[index] = rendered
48+
return True
49+
50+
insert_at = 0
51+
for index, line in enumerate(lines):
52+
if _is_table_header(line):
53+
insert_at = index
54+
break
55+
else:
56+
insert_at = len(lines)
57+
58+
if insert_at == len(lines):
59+
_ensure_trailing_newline(lines, insert_at - 1)
60+
lines.insert(insert_at, rendered)
61+
return True
62+
63+
64+
def find_table(lines: list[str], table_name: str) -> tuple[int | None, int | None]:
65+
start = None
66+
end = None
67+
68+
for index, line in enumerate(lines):
69+
if _matches_table(line, table_name):
70+
start = index
71+
continue
72+
if start is not None and _is_table_header(line):
73+
end = index
74+
break
75+
76+
if start is not None and end is None:
77+
end = len(lines)
78+
79+
return start, end
80+
81+
82+
def ensure_feature_setting(lines: list[str], key: str, value: str) -> bool:
83+
start, end = find_table(lines, "features")
84+
rendered = f"{key} = {value}\n"
85+
86+
if start is None:
87+
if lines and lines[-1].strip():
88+
lines.append("\n")
89+
lines.extend(["[features]\n", rendered])
90+
return True
91+
92+
assert end is not None
93+
for index in range(start + 1, end):
94+
if _matches_assignment(lines[index], key):
95+
if lines[index].strip() == rendered.strip():
96+
return False
97+
lines[index] = rendered
98+
return True
99+
100+
_ensure_trailing_newline(lines, end - 1)
101+
lines.insert(end, rendered)
102+
return True
103+
104+
105+
def main(argv: list[str]) -> int:
106+
config_path = resolve_config_path(argv)
107+
config_path.parent.mkdir(parents=True, exist_ok=True)
108+
109+
if config_path.exists():
110+
lines = config_path.read_text(encoding="utf-8").splitlines(keepends=True)
111+
else:
112+
lines = []
113+
114+
changed = False
115+
changed |= ensure_top_level_setting(lines, "cli_auth_credentials_store", "file")
116+
changed |= ensure_feature_setting(lines, "codex_hooks", "true")
117+
118+
if changed:
119+
content = "".join(lines)
120+
if content and not content.endswith("\n"):
121+
content += "\n"
122+
config_path.write_text(content, encoding="utf-8")
123+
124+
return 0
125+
126+
127+
if __name__ == "__main__":
128+
raise SystemExit(main(sys.argv))

sandbox/codex/entrypoint.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
codex_home="${CODEX_HOME:-/codex-home}"
5+
6+
mkdir -p "${codex_home}"
7+
export HOME="${codex_home}"
8+
export CODEX_HOME="${codex_home}"
9+
10+
python3 /usr/local/bin/bootstrap_codex_config.py
11+
12+
exec "$@"

sandbox/sample.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
ANTHROPIC_API_KEY=sk-ant-xxxx
2+
#OPENAI_API_KEY=sk-xxxx # pragma: allowlist secret
23
EVOLVE_DEBUG=1
34
IS_SANDBOX=1

0 commit comments

Comments
 (0)