From 6bfad07ed8ac5c2fb3a2e375c70c4d30461d1b34 Mon Sep 17 00:00:00 2001 From: therichardngai-code Date: Sat, 28 Mar 2026 13:17:08 +0700 Subject: [PATCH] fix(docker): restore base capabilities in sandbox overlay (#523) Sandbox overlay's cap_add replaces (not merges) the base compose, dropping SETUID, SETGID, CHOWN. This causes credential copy to fail with Permission denied when combining sandbox + claude-cli overlays. Changes: - Re-include base capabilities in sandbox overlay's cap_add - Use umask 077 for atomic permission-safe credential copy - Add ENABLE_CLAUDE_CLI build arg to pre-install Claude CLI in image - Add runtime warning when credentials mounted but CLI binary missing - Add WITH_CLAUDE_CLI to Makefile for overlay consistency - Add security warning comment for sandbox overlay attack surface --- .env.example | 4 ++++ Dockerfile | 7 ++++++- Makefile | 3 +++ docker-compose.claude-cli.yml | 8 ++++++-- docker-compose.sandbox.yml | 8 +++++++- docker-entrypoint.sh | 14 ++++++++++++-- 6 files changed, 38 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index 46d5886c..e615d566 100644 --- a/.env.example +++ b/.env.example @@ -13,5 +13,9 @@ POSTGRES_PASSWORD= # Docker Compose auto-builds this from POSTGRES_USER/PASSWORD/DB. # GOCLAW_POSTGRES_DSN=postgres://user:pass@host:5432/dbname?sslmode=disable +# --- Sandbox (only when using docker-compose.sandbox.yml) --- +# Docker socket GID: 999 on Linux, 0 on Windows/macOS Docker Desktop. +# DOCKER_GID=0 + # --- Debug --- # GOCLAW_TRACE_VERBOSE=1 diff --git a/Dockerfile b/Dockerfile index 54f40443..09b04637 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,6 +45,7 @@ ARG ENABLE_SANDBOX=false ARG ENABLE_PYTHON=false ARG ENABLE_NODE=false ARG ENABLE_FULL_SKILLS=false +ARG ENABLE_CLAUDE_CLI=false # Install ca-certificates + wget (healthcheck) + optional runtimes. # ENABLE_FULL_SKILLS=true pre-installs all skill deps (larger image, no on-demand install needed). @@ -66,9 +67,13 @@ RUN set -eux; \ apk add --no-cache python3 py3-pip; \ pip3 install --no-cache-dir --break-system-packages edge-tts; \ fi; \ - if [ "$ENABLE_NODE" = "true" ]; then \ + if [ "$ENABLE_NODE" = "true" ] || [ "$ENABLE_CLAUDE_CLI" = "true" ]; then \ apk add --no-cache nodejs npm; \ fi; \ + fi; \ + if [ "$ENABLE_CLAUDE_CLI" = "true" ]; then \ + npm install -g --cache /tmp/npm-cache @anthropic-ai/claude-code; \ + rm -rf /tmp/npm-cache; \ fi # Non-root user diff --git a/Makefile b/Makefile index 5d982e55..11596330 100644 --- a/Makefile +++ b/Makefile @@ -33,6 +33,9 @@ endif ifdef WITH_REDIS COMPOSE_EXTRA += -f docker-compose.redis.yml endif +ifdef WITH_CLAUDE_CLI +COMPOSE_EXTRA += -f docker-compose.claude-cli.yml +endif COMPOSE = $(COMPOSE_BASE) $(COMPOSE_EXTRA) UPGRADE = docker compose -f docker-compose.yml -f docker-compose.postgres.yml -f docker-compose.upgrade.yml diff --git a/docker-compose.claude-cli.yml b/docker-compose.claude-cli.yml index 99aacc56..7ad41fe8 100644 --- a/docker-compose.claude-cli.yml +++ b/docker-compose.claude-cli.yml @@ -1,10 +1,14 @@ -# Optional overlay: sync Claude CLI credentials from host into container. +# Optional overlay: install Claude CLI and sync credentials from host into container. # Mounts host ~/.claude as read-only; entrypoint copies credentials to the data volume. +# Adds ENABLE_CLAUDE_CLI build arg to install nodejs + @anthropic-ai/claude-code. # # Usage: -# docker compose -f docker-compose.yml -f docker-compose.postgres.yml -f docker-compose.claude-cli.yml up -d +# docker compose -f docker-compose.yml -f docker-compose.postgres.yml -f docker-compose.claude-cli.yml up -d --build services: goclaw: + build: + args: + ENABLE_CLAUDE_CLI: "true" volumes: - ${HOME}/.claude:/app/.claude-host:ro diff --git a/docker-compose.sandbox.yml b/docker-compose.sandbox.yml index eeaa014d..90a17153 100644 --- a/docker-compose.sandbox.yml +++ b/docker-compose.sandbox.yml @@ -26,10 +26,16 @@ services: - GOCLAW_SANDBOX_CPUS=1.0 - GOCLAW_SANDBOX_TIMEOUT_SEC=300 - GOCLAW_SANDBOX_NETWORK=false - # Override base cap_drop to allow Docker socket access + # Override base cap_drop to allow Docker socket access. + # Re-include base caps (SETUID/SETGID/CHOWN) lost when overriding cap_add. + # WARNING: SETUID/SETGID with security_opt cleared (no no-new-privileges) + # increases attack surface. Only use in trusted environments. cap_drop: [] cap_add: - NET_BIND_SERVICE + - SETUID + - SETGID + - CHOWN security_opt: [] group_add: - ${DOCKER_GID:-999} diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index dfb9e624..6c30180e 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -74,13 +74,23 @@ fi # Copy Claude CLI credentials from root-owned read-only mount to goclaw-accessible location. # /app/.claude is a symlink → /app/data/.claude (writable volume, see Dockerfile). -# Uses install(1) for atomic copy with correct ownership+permissions (no temp file needed). +# Uses su-exec to copy as goclaw user because sandbox overlay's cap_add override +# may remove CHOWN needed by install(1). umask 077 ensures file is created with 600. if [ -f /app/.claude-host/.credentials.json ]; then (mkdir -p /app/data/.claude \ - && install -m 600 -o goclaw -g goclaw /app/.claude-host/.credentials.json /app/data/.claude/.credentials.json \ + && if command -v su-exec >/dev/null 2>&1 && [ "$(id -u)" = "0" ]; then + su-exec goclaw sh -c 'umask 077 && cp /app/.claude-host/.credentials.json /app/data/.claude/.credentials.json' + else + ( umask 077 && cp /app/.claude-host/.credentials.json /app/data/.claude/.credentials.json ) + fi \ && echo "Claude CLI credentials synced from host.") || echo "WARNING: Claude credentials copy failed (non-fatal)" fi +# Warn if Claude credentials are mounted but CLI binary is missing (forgot --build). +if [ -d /app/.claude-host ] && ! command -v claude >/dev/null 2>&1; then + echo "WARNING: Claude credentials mounted but claude CLI not installed. Rebuild with: --build" +fi + # Run command with privilege drop (su-exec in Docker, direct otherwise). run_as_goclaw() { if command -v su-exec >/dev/null 2>&1 && [ "$(id -u)" = "0" ]; then