Files
windmill/Dockerfile.sandbox
centdix 0db6cbd10c feat: add codex agent support to sandbox container
- Install @openai/codex in Dockerfile.sandbox
- Pass developer_instructions via -c flag with proper shell escaping
- Use --yolo flag for sandbox profile (container is the sandbox)
- Mount ~/.codex into container via workmux extra_mounts config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 19:27:30 +00:00

187 lines
7.1 KiB
Docker

FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
ca-certificates \
git \
iptables \
gosu \
sudo \
# Rust native build deps (for cargo check)
pkg-config \
cmake \
clang \
mold \
libtool \
libssl-dev \
libxml2-dev \
libxmlsec1-dev \
libxslt1-dev \
libffi-dev \
zlib1g-dev \
libcurl4-openssl-dev \
libclang-dev \
libkrb5-dev \
libsasl2-dev \
# PostgreSQL (for local DB during development)
postgresql \
postgresql-client \
# Node.js 22 (for npm run check / frontend dev)
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/* \
&& echo "ALL ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/sandbox \
&& chmod 0440 /etc/sudoers.d/sandbox \
&& chmod 666 /etc/passwd /etc/group /etc/shadow
# Install Rust via rustup (stable, minimal profile)
ENV RUSTUP_HOME=/opt/rustup CARGO_HOME=/opt/cargo
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
sh -s -- -y --default-toolchain stable --profile minimal && \
chmod -R a+rwX /opt/rustup /opt/cargo && \
ln -s /opt/cargo/bin/* /usr/local/bin/
# Install sqlx-cli (for database migrations) and cargo-watch (for auto-reload)
RUN cargo install sqlx-cli --no-default-features --features native-tls,postgres && \
cargo install cargo-watch && \
ln -sf /opt/cargo/bin/sqlx /usr/local/bin/sqlx && \
ln -sf /opt/cargo/bin/cargo-watch /usr/local/bin/cargo-watch && \
chmod -R a+rwX /opt/cargo
# Embed network init script (sets up iptables firewall, then drops privileges)
RUN cat <<'SCRIPT' > /usr/local/bin/network-init.sh
#!/bin/bash
set -euo pipefail
if [ -n "${WM_PROXY_HOST:-}" ] && [ -n "${WM_PROXY_PORT:-}" ]; then
# Resolve hostnames to ALL IPs (multi-A records, round-robin DNS)
PROXY_IPS=$(getent ahostsv4 "$WM_PROXY_HOST" | awk '{print $1}' | sort -u)
RPC_HOST="${WM_RPC_HOST:-$WM_PROXY_HOST}"
RPC_IPS=$(getent ahostsv4 "$RPC_HOST" | awk '{print $1}' | sort -u)
if [ -z "$PROXY_IPS" ] || [ -z "$RPC_IPS" ]; then
echo "network-init: failed to resolve proxy/RPC host" >&2
exit 1
fi
# IPv4: default deny outbound
iptables -P OUTPUT DROP
iptables -A OUTPUT -o lo -j ACCEPT
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# Allow DNS (UDP/TCP 53) to configured nameservers.
# Without this, any hostname resolution hangs until timeout.
if [ -f /etc/resolv.conf ]; then
grep '^nameserver' /etc/resolv.conf | awk '{print $2}' | while read -r ns; do
iptables -A OUTPUT -d "$ns" -p udp --dport 53 -j ACCEPT
iptables -A OUTPUT -d "$ns" -p tcp --dport 53 -j ACCEPT
done
fi
# Allow ALL resolved proxy IPs (handles multi-A DNS)
for ip in $PROXY_IPS; do
iptables -A OUTPUT -d "$ip" -p tcp --dport "$WM_PROXY_PORT" -j ACCEPT
done
# Allow ALL resolved RPC IPs
if [ -n "${WM_RPC_PORT:-}" ]; then
for ip in $RPC_IPS; do
iptables -A OUTPUT -d "$ip" -p tcp --dport "$WM_RPC_PORT" -j ACCEPT
done
fi
# Reject (not drop) everything else to fail fast instead of hanging
iptables -A OUTPUT -j REJECT
# IPv6: block entirely to prevent leaks (fail closed)
if ip6tables -L -n >/dev/null 2>&1; then
ip6tables -P OUTPUT DROP
ip6tables -A OUTPUT -o lo -j ACCEPT
ip6tables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
ip6tables -A OUTPUT -j REJECT
else
# If ip6tables unavailable, disable IPv6 via sysctl as fallback
if ! sysctl -w net.ipv6.conf.all.disable_ipv6=1 2>/dev/null; then
echo "network-init: failed to block IPv6 (neither ip6tables nor sysctl available)" >&2
exit 1
fi
fi
fi
# Add sandbox user to passwd so sudo works with arbitrary UIDs.
if ! getent passwd "${WM_TARGET_UID}" >/dev/null 2>&1; then
echo "sandbox:x:${WM_TARGET_UID}:${WM_TARGET_GID}:sandbox:/tmp:/bin/sh" >> /etc/passwd
echo "sandbox:x:${WM_TARGET_GID}:" >> /etc/group 2>/dev/null || true
fi
# Fix PTY ownership so the unprivileged user can read/write the terminal.
# Docker allocates the PTY as root; after gosu drops privileges, the user
# has no access to /dev/pts/0 unless we transfer ownership.
if [ -t 0 ]; then
chown "${WM_TARGET_UID}:${WM_TARGET_GID}" "$(tty)"
fi
# Drop privileges and exec the user command.
# gosu resets HOME via getpwuid() which returns "/" for UIDs not in /etc/passwd.
# Preserve the container's HOME=/tmp so agents find their config dirs.
exec gosu "${WM_TARGET_UID}:${WM_TARGET_GID}" env HOME=/tmp "$@"
SCRIPT
RUN chmod +x /usr/local/bin/network-init.sh
# Install workmux (needed for sandbox RPC)
RUN curl -fsSL https://raw.githubusercontent.com/raine/workmux/main/scripts/install.sh | bash
# --- claude agent ---
# Install Claude Code, then move to a globally accessible path so the binary
# is reachable when running as an arbitrary non-root UID (--user uid:gid).
RUN curl -fsSL https://claude.ai/install.sh | bash && \
target="$(readlink -f /root/.local/bin/claude)" && \
mv /root/.local/share/claude /opt/claude && \
ln -s "/opt/claude/versions/$(basename "$target")" /usr/local/bin/claude && \
# Claude Code checks $HOME/.local/bin/claude when installMethod is "native".
# Container sets HOME=/tmp, so place a symlink to suppress the warning.
mkdir -p /tmp/.local/bin && \
ln -s /usr/local/bin/claude /tmp/.local/bin/claude && \
chmod 777 /tmp/.local
# --- codex agent ---
RUN npm i -g @openai/codex
# Entrypoint that registers the dynamic UID in /etc/passwd so sudo works.
# workmux passes --user UID:GID directly, bypassing network-init.sh.
RUN cat <<'ENTRY' > /usr/local/bin/entrypoint.sh
#!/bin/sh
if ! getent passwd "$(id -u)" >/dev/null 2>&1; then
echo "sandbox:x:$(id -u):$(id -g):sandbox:/tmp:/bin/sh" >> /etc/passwd
fi
if ! getent group "$(id -g)" >/dev/null 2>&1; then
echo "sandbox:x:$(id -g):" >> /etc/group
fi
if ! grep -q "^sandbox:" /etc/shadow 2>/dev/null; then
echo "sandbox:*:19000:0:99999:7:::" >> /etc/shadow
fi
# Start PostgreSQL (unix socket in /tmp, owned by postgres user)
mkdir -p /tmp/pgdata && sudo chown postgres:postgres /tmp/pgdata
if [ ! -f /tmp/pgdata/PG_VERSION ]; then
sudo -u postgres /usr/lib/postgresql/15/bin/initdb -D /tmp/pgdata --auth=trust
fi
sudo -u postgres /usr/lib/postgresql/15/bin/pg_ctl -D /tmp/pgdata -l /tmp/pg.log start -o "-k /tmp"
sudo -u postgres psql -h /tmp -c "CREATE ROLE sandbox SUPERUSER LOGIN" 2>/dev/null || true
sudo -u postgres createdb -h /tmp windmill 2>/dev/null || true
# Run database migrations so sqlx compile-time checks work
if [ -d "$PWD/backend/migrations" ]; then
DATABASE_URL="postgres://sandbox@localhost/windmill?host=/tmp" \
sqlx migrate run --source "$PWD/backend/migrations" 2>/dev/null || true
fi
# Install frontend dependencies and generate backend client
if [ -d "$PWD/frontend" ]; then
(cd "$PWD/frontend" && npm install && npm run generate-backend-client) 2>/dev/null || true
fi
exec "$@"
ENTRY
RUN chmod +x /usr/local/bin/entrypoint.sh
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]