diff --git a/.sandcastle/Dockerfile b/.sandcastle/Dockerfile index 3b28ee4..6839be6 100644 --- a/.sandcastle/Dockerfile +++ b/.sandcastle/Dockerfile @@ -1,26 +1,52 @@ -# Sandcastle sandbox image — runs the implementer + reviewer agents. +# Sandcastle sandbox image — runs the implementer + reviewer + decomposer +# agents. Shape required by @ai-hero/sandcastle: a non-root `agent` user +# (UID/GID aligned with the host so bind-mounted files share owner), Claude +# Code CLI on PATH, and a long-running ENTRYPOINT so the container survives +# the gap between sandcastle creating it and exec'ing into it. # -# Includes Claude Code CLI so the sandbox can authenticate via the host's -# mounted ~/.claude/ session (sandcastle issue #191 workaround — subscription -# auth, not API-key auth, is our primary flow). Falls back to ANTHROPIC_API_KEY -# when no host credentials are available. +# Authenticates via the host's mounted ~/.claude/ session (subscription +# mode — sandcastle issue #191 workaround, our primary flow). Falls back +# to ANTHROPIC_API_KEY when no host credentials are present. -FROM node:22-bookworm-slim +FROM node:22-bookworm -# pnpm via corepack (matches the repo's pnpm version) +# System deps — git for worktree ops, curl for the Claude installer, jq for +# JSON tooling agents use, plus ca-certificates implicit in the base image. +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + curl \ + jq \ + && rm -rf /var/lib/apt/lists/* + +# pnpm via corepack (matches the repo's packageManager version). RUN corepack enable && corepack prepare pnpm@9 --activate +# Build-args for UID/GID alignment: `sandcastle docker build-image` passes +# the host user's UID/GID by default so image-built files and bind-mounted +# files share an owner without runtime chown. +ARG AGENT_UID=1000 +ARG AGENT_GID=1000 + +# Rename the base image's "node" user to "agent" and align UID/GID. +# `-o` (non-unique) is required because the host's GID may collide with a +# pre-existing system group in the base image (e.g. macOS UID:501 GID:20 +# collides with Debian's `dialout` group at GID 20). Allowing a duplicate +# GID is safe here — only one user occupies the sandbox. +RUN groupmod -o -g $AGENT_GID node && \ + usermod -o -u $AGENT_UID -g $AGENT_GID -d /home/agent -m -l agent node + +USER ${AGENT_UID}:${AGENT_GID} + # Claude Code CLI — used by sandcastle's claudeCode() agent provider. # The CLI reads credentials from ~/.claude/ inside the container; the host # mounts its ~/.claude/ over that path at sandbox start. -RUN npm install -g @anthropic-ai/claude-code +RUN curl -fsSL https://claude.ai/install.sh | bash -# Minimal system deps for git operations + healthchecks. -RUN apt-get update && apt-get install -y --no-install-recommends \ - git \ - ca-certificates \ - && rm -rf /var/lib/apt/lists/* +ENV PATH="/home/agent/.local/bin:$PATH" -WORKDIR /workspace +WORKDIR /home/agent -CMD ["bash"] +# In worktree sandbox mode, sandcastle bind-mounts the git worktree at +# ${SANDBOX_REPO_DIR} and overrides the working directory to that path at +# container start. The Dockerfile's WORKDIR is just the default home. +ENTRYPOINT ["sleep", "infinity"] diff --git a/README.md b/README.md index 1c60a94..b69ad08 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,23 @@ claude login # one-time; ~/.claude/ becomes the auth source export ANTHROPIC_API_KEY=sk-ant-... ``` +**macOS users**: subscription auth needs an extra step. Claude Code stores credentials in the macOS Keychain by default, so the host's `~/.claude/` directory has no `.credentials.json` for the sandbox to read. Two workarounds: + +```bash +# (preferred for macOS subscription users) extract keychain -> file once: +security find-generic-password -s "Claude Code-credentials" -a "$USER" -w \ + > ~/.claude/.credentials.json +chmod 600 ~/.claude/.credentials.json +# Trade-off: credentials now live as a plaintext file at the path; refresh +# when the token expires (re-run the same one-liner). The file is in your +# home directory — chmod 600 + your home permissions are the protection. + +# OR fall back to API key — no host changes needed: +export ANTHROPIC_API_KEY=sk-ant-... +``` + +Linux + WSL users with `claude login` write `~/.claude/.credentials.json` directly; nothing extra needed. + After the image exists, dispatch flows work without further setup: ```bash diff --git a/docs/guides/runbook.md b/docs/guides/runbook.md index ce91d67..666ad85 100644 --- a/docs/guides/runbook.md +++ b/docs/guides/runbook.md @@ -260,8 +260,20 @@ For the full design see `docs/architecture/agent-first-workflow-and-conformance. 3. **Authentication — pick ONE:** - **Recommended: Claude Pro / Max subscription.** Run `claude login` once on the host. Sandcastle's sandbox bind-mounts your `~/.claude/` into the container so the Claude Code CLI inside the sandbox uses your subscription session. Zero per-task token spend for subscribers. - - **Alternative: API key.** Set `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` in your environment. Falls back automatically when `~/.claude/` is absent. + + **macOS quirk:** Claude Code stores credentials in the macOS Keychain, NOT in `~/.claude/.credentials.json` — so the bind-mount finds nothing. If you hit `Not logged in · Please run /login` inside the sandbox, extract the keychain credentials to a file once: + + ```bash + security find-generic-password -s "Claude Code-credentials" -a "$USER" -w \ + > ~/.claude/.credentials.json + chmod 600 ~/.claude/.credentials.json + ``` + + Trade-off: credentials now live as a plaintext file at the path; the macOS Keychain isolation is replaced by filesystem permissions (chmod 600 + your home dir's mode). When the token expires (~30 days), re-run the same one-liner. Linux + WSL hosts write `~/.claude/.credentials.json` directly during `claude login`, so this step is macOS-only. + + - **Alternative: API key.** Set `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` in your environment. Falls back automatically when `~/.claude/` is absent. Use this if you don't want a plaintext credentials file on disk. - **Override the creds path** via `SANDCASTLE_CLAUDE_CREDS_DIR` if your Claude Code config lives somewhere non-standard. + 4. **GitHub token** (optional) — `GITHUB_TOKEN` if you want the orchestrator to create PRs. 5. **`.sandcastle/` config present** — already in tree: - `Dockerfile` — node:22 + pnpm + Claude Code CLI; reads creds from `~/.claude/` inside the container