# ============================================================================= # Agent Base Image — multi-stage build # ============================================================================= # Shared runtime for Claude Code agent containers: Python venv + Node.js + # @anthropic-ai/claude-code CLI. Specialized images (agent-dev-be, agent-qa-fe, # etc.) extend this one. # ============================================================================= # ---- Builder ---------------------------------------------------------------- FROM python:3.13-slim-bookworm AS builder RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ git \ && rm -rf /var/lib/apt/lists/* COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv WORKDIR /app ENV UV_HTTP_TIMEOUT=300 \ UV_CONCURRENT_DOWNLOADS=4 \ UV_LINK_MODE=copy \ UV_PYTHON_PREFERENCE=only-system # Use base-image Python so venv symlinks stay valid after COPY to runner # (uv's managed python at /root/.local/share/uv/python isn't carried across). COPY pyproject.toml uv.lock README.md /app/ RUN uv sync --frozen --no-dev --no-install-project COPY roboco /app/roboco RUN uv sync --frozen --no-dev # ---- Runner ----------------------------------------------------------------- FROM python:3.13-slim-bookworm AS runner # Runtime deps: Node.js 22 for claude CLI, git for workspace ops, jq for hooks. # gnupg is only needed to add the NodeSource repo, purged after install. RUN apt-get update && apt-get install -y --no-install-recommends \ curl ca-certificates git gnupg jq \ && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ && apt-get install -y --no-install-recommends nodejs \ && npm install -g @anthropic-ai/claude-code \ && npm cache clean --force \ && apt-get purge -y --auto-remove gnupg \ && rm -rf /var/lib/apt/lists/* /root/.npm /tmp/* RUN useradd -m -s /bin/bash agent # uv is required at runtime: mcp-config.json spawns every MCP server via # `uv run python -m roboco.mcp.`. Without it, all 10 roboco MCP # servers fail to start and the agent falls back to raw HTTP, losing every # guardrail and inline schema the MCP layer provides. COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv WORKDIR /app # Copy the pre-built venv + source from builder, owned by agent user COPY --from=builder --chown=agent:agent /app /app # Hook scripts: 0755 so the `agent` user (not root) can read+execute them. # SessionStart hook runs these as agent; stricter perms break the hook with # "Permission denied" (exit 126). COPY docker/scripts/sdk-startup-hook.sh /app/scripts/sdk-startup-hook.sh COPY docker/scripts/a2a-check-hook.sh /app/scripts/a2a-check-hook.sh COPY docker/scripts/traceability-hook.sh /app/scripts/traceability-hook.sh COPY docker/scripts/bash-guard-hook.sh /app/scripts/bash-guard-hook.sh COPY docker/scripts/post-tool-budget-hook.sh /app/scripts/post-tool-budget-hook.sh COPY docker/scripts/stop-hook.sh /app/scripts/stop-hook.sh COPY docker/scripts/user-prompt-hook.sh /app/scripts/user-prompt-hook.sh COPY docker/scripts/pre-compact-hook.sh /app/scripts/pre-compact-hook.sh COPY docker/scripts/session-end-hook.sh /app/scripts/session-end-hook.sh RUN chmod 0755 /app/scripts/*.sh USER agent # Workspaces are cloned by the orchestrator (running as root) into a shared # volume, so inside the agent container they show up owned by a different # uid. Git 2.35+ refuses to operate on such repos with "dubious ownership" # until safe.directory is set. `*` trusts everything, which is fine inside # a per-agent sandbox that only mounts its own workspace. RUN git config --global --add safe.directory '*' ENV PATH="/app/.venv/bin:$PATH" \ VIRTUAL_ENV=/app/.venv # Claude Code uses mounted ~/.claude for auth. # System prompt mounted at /app/system-prompt.md at spawn time. # MCP config generated at runtime. EXPOSE 9000 ENTRYPOINT ["claude"]