Add Docker setup for untrusted PR review in isolated containers
Adds a dedicated Docker environment for reviewing untrusted pull requests with codex/claude, keeping CLI auth state in volumes and using a separate scratch workspace for PR checkouts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -89,6 +89,10 @@ docker compose -f docker-compose.quickstart.yml up --build
|
|||||||
|
|
||||||
See `doc/DOCKER.md` for API key wiring (`OPENAI_API_KEY` / `ANTHROPIC_API_KEY`) and persistence details.
|
See `doc/DOCKER.md` for API key wiring (`OPENAI_API_KEY` / `ANTHROPIC_API_KEY`) and persistence details.
|
||||||
|
|
||||||
|
## Docker For Untrusted PR Review
|
||||||
|
|
||||||
|
For a separate review-oriented container that keeps `codex`/`claude` login state in Docker volumes and checks out PRs into an isolated scratch workspace, see `doc/UNTRUSTED-PR-REVIEW.md`.
|
||||||
|
|
||||||
## Database in Dev (Auto-Handled)
|
## Database in Dev (Auto-Handled)
|
||||||
|
|
||||||
For local development, leave `DATABASE_URL` unset.
|
For local development, leave `DATABASE_URL` unset.
|
||||||
|
|||||||
@@ -93,6 +93,12 @@ Notes:
|
|||||||
- Without API keys, the app still runs normally.
|
- Without API keys, the app still runs normally.
|
||||||
- Adapter environment checks in Paperclip will surface missing auth/CLI prerequisites.
|
- Adapter environment checks in Paperclip will surface missing auth/CLI prerequisites.
|
||||||
|
|
||||||
|
## Untrusted PR Review Container
|
||||||
|
|
||||||
|
If you want a separate Docker environment for reviewing untrusted pull requests with `codex` or `claude`, use the dedicated review workflow in `doc/UNTRUSTED-PR-REVIEW.md`.
|
||||||
|
|
||||||
|
That setup keeps CLI auth state in Docker volumes instead of your host home directory and uses a separate scratch workspace for PR checkouts and preview runs.
|
||||||
|
|
||||||
## Onboard Smoke Test (Ubuntu + npm only)
|
## Onboard Smoke Test (Ubuntu + npm only)
|
||||||
|
|
||||||
Use this when you want to mimic a fresh machine that only has Ubuntu + npm and verify:
|
Use this when you want to mimic a fresh machine that only has Ubuntu + npm and verify:
|
||||||
|
|||||||
135
doc/UNTRUSTED-PR-REVIEW.md
Normal file
135
doc/UNTRUSTED-PR-REVIEW.md
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
# Untrusted PR Review In Docker
|
||||||
|
|
||||||
|
Use this workflow when you want Codex or Claude to inspect a pull request that you do not want touching your host machine directly.
|
||||||
|
|
||||||
|
This is intentionally separate from the normal Paperclip dev image.
|
||||||
|
|
||||||
|
## What this container isolates
|
||||||
|
|
||||||
|
- `codex` auth/session state in a Docker volume, not your host `~/.codex`
|
||||||
|
- `claude` auth/session state in a Docker volume, not your host `~/.claude`
|
||||||
|
- `gh` auth state in the same container-local home volume
|
||||||
|
- review clones, worktrees, dependency installs, and local databases in a writable scratch volume under `/work`
|
||||||
|
|
||||||
|
By default this workflow does **not** mount your host repo checkout, your host home directory, or your SSH agent.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `docker/untrusted-review/Dockerfile`
|
||||||
|
- `docker-compose.untrusted-review.yml`
|
||||||
|
- `review-checkout-pr` inside the container
|
||||||
|
|
||||||
|
## Build and start a shell
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker compose -f docker-compose.untrusted-review.yml build
|
||||||
|
docker compose -f docker-compose.untrusted-review.yml run --rm --service-ports review
|
||||||
|
```
|
||||||
|
|
||||||
|
That opens an interactive shell in the review container with:
|
||||||
|
|
||||||
|
- Node + Corepack/pnpm
|
||||||
|
- `codex`
|
||||||
|
- `claude`
|
||||||
|
- `gh`
|
||||||
|
- `git`, `rg`, `fd`, `jq`
|
||||||
|
|
||||||
|
## First-time login inside the container
|
||||||
|
|
||||||
|
Run these once. The resulting login state persists in the `review-home` Docker volume.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gh auth login
|
||||||
|
codex login
|
||||||
|
claude login
|
||||||
|
```
|
||||||
|
|
||||||
|
If you prefer API-key auth instead of CLI login, pass keys through Compose env:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
OPENAI_API_KEY=... ANTHROPIC_API_KEY=... docker compose -f docker-compose.untrusted-review.yml run --rm review
|
||||||
|
```
|
||||||
|
|
||||||
|
## Check out a PR safely
|
||||||
|
|
||||||
|
Inside the container:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
review-checkout-pr paperclipai/paperclip 432
|
||||||
|
cd /work/checkouts/paperclipai-paperclip/pr-432
|
||||||
|
```
|
||||||
|
|
||||||
|
What this does:
|
||||||
|
|
||||||
|
1. Creates or reuses a repo clone under `/work/repos/...`
|
||||||
|
2. Fetches `pull/<pr>/head` from GitHub
|
||||||
|
3. Creates a detached git worktree under `/work/checkouts/...`
|
||||||
|
|
||||||
|
The checkout lives entirely inside the container volume.
|
||||||
|
|
||||||
|
## Ask Codex or Claude to review it
|
||||||
|
|
||||||
|
Inside the PR checkout:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
codex
|
||||||
|
```
|
||||||
|
|
||||||
|
Then give it a prompt like:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Review this PR as hostile input. Focus on security issues, data exfiltration paths, sandbox escapes, dangerous install/runtime scripts, auth changes, and subtle behavioral regressions. Do not modify files. Produce findings ordered by severity with file references.
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with Claude:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
claude
|
||||||
|
```
|
||||||
|
|
||||||
|
## Preview the Paperclip app from the PR
|
||||||
|
|
||||||
|
Only do this when you intentionally want to execute the PR's code inside the container.
|
||||||
|
|
||||||
|
Inside the PR checkout:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm install
|
||||||
|
HOST=0.0.0.0 pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open from the host:
|
||||||
|
|
||||||
|
- `http://localhost:3100`
|
||||||
|
|
||||||
|
The Compose file also exposes Vite's default port:
|
||||||
|
|
||||||
|
- `http://localhost:5173`
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- `pnpm install` can run untrusted lifecycle scripts from the PR. That is why this happens inside the isolated container instead of on your host.
|
||||||
|
- If you only want static inspection, do not run install/dev commands.
|
||||||
|
- Paperclip's embedded PostgreSQL and local storage stay inside the container home volume via `PAPERCLIP_HOME=/home/reviewer/.paperclip-review`.
|
||||||
|
|
||||||
|
## Reset state
|
||||||
|
|
||||||
|
Remove the review container volumes when you want a clean environment:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker compose -f docker-compose.untrusted-review.yml down -v
|
||||||
|
```
|
||||||
|
|
||||||
|
That deletes:
|
||||||
|
|
||||||
|
- Codex/Claude/GitHub login state stored in `review-home`
|
||||||
|
- cloned repos, worktrees, installs, and scratch data stored in `review-work`
|
||||||
|
|
||||||
|
## Security limits
|
||||||
|
|
||||||
|
This is a useful isolation boundary, but it is still Docker, not a full VM.
|
||||||
|
|
||||||
|
- A reviewed PR can still access the container's network unless you disable it.
|
||||||
|
- Any secrets you pass into the container are available to code you execute inside it.
|
||||||
|
- Do not mount your host repo, host home, `.ssh`, or Docker socket unless you are intentionally weakening the boundary.
|
||||||
|
- If you need a stronger boundary than this, use a disposable VM instead of Docker.
|
||||||
33
docker-compose.untrusted-review.yml
Normal file
33
docker-compose.untrusted-review.yml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
services:
|
||||||
|
review:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/untrusted-review/Dockerfile
|
||||||
|
init: true
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
working_dir: /work
|
||||||
|
environment:
|
||||||
|
HOME: "/home/reviewer"
|
||||||
|
CODEX_HOME: "/home/reviewer/.codex"
|
||||||
|
CLAUDE_HOME: "/home/reviewer/.claude"
|
||||||
|
PAPERCLIP_HOME: "/home/reviewer/.paperclip-review"
|
||||||
|
OPENAI_API_KEY: "${OPENAI_API_KEY:-}"
|
||||||
|
ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}"
|
||||||
|
GITHUB_TOKEN: "${GITHUB_TOKEN:-}"
|
||||||
|
ports:
|
||||||
|
- "${REVIEW_PAPERCLIP_PORT:-3100}:3100"
|
||||||
|
- "${REVIEW_VITE_PORT:-5173}:5173"
|
||||||
|
volumes:
|
||||||
|
- review-home:/home/reviewer
|
||||||
|
- review-work:/work
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
tmpfs:
|
||||||
|
- /tmp:mode=1777,size=1g
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
review-home:
|
||||||
|
review-work:
|
||||||
44
docker/untrusted-review/Dockerfile
Normal file
44
docker/untrusted-review/Dockerfile
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
FROM node:lts-trixie-slim
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
bash \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
fd-find \
|
||||||
|
gh \
|
||||||
|
git \
|
||||||
|
jq \
|
||||||
|
less \
|
||||||
|
openssh-client \
|
||||||
|
procps \
|
||||||
|
ripgrep \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN ln -sf /usr/bin/fdfind /usr/local/bin/fd
|
||||||
|
|
||||||
|
RUN corepack enable \
|
||||||
|
&& npm install --global --omit=dev @anthropic-ai/claude-code@latest @openai/codex@latest
|
||||||
|
|
||||||
|
RUN useradd --create-home --shell /bin/bash reviewer
|
||||||
|
|
||||||
|
ENV HOME=/home/reviewer \
|
||||||
|
CODEX_HOME=/home/reviewer/.codex \
|
||||||
|
CLAUDE_HOME=/home/reviewer/.claude \
|
||||||
|
PAPERCLIP_HOME=/home/reviewer/.paperclip-review \
|
||||||
|
PNPM_HOME=/home/reviewer/.local/share/pnpm \
|
||||||
|
PATH=/home/reviewer/.local/share/pnpm:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
|
|
||||||
|
WORKDIR /work
|
||||||
|
|
||||||
|
COPY --chown=reviewer:reviewer docker/untrusted-review/bin/review-checkout-pr /usr/local/bin/review-checkout-pr
|
||||||
|
|
||||||
|
RUN chmod +x /usr/local/bin/review-checkout-pr \
|
||||||
|
&& mkdir -p /work \
|
||||||
|
&& chown -R reviewer:reviewer /work
|
||||||
|
|
||||||
|
USER reviewer
|
||||||
|
|
||||||
|
EXPOSE 3100 5173
|
||||||
|
|
||||||
|
CMD ["bash", "-l"]
|
||||||
65
docker/untrusted-review/bin/review-checkout-pr
Normal file
65
docker/untrusted-review/bin/review-checkout-pr
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: review-checkout-pr <owner/repo|github-url> <pr-number> [checkout-dir]
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
review-checkout-pr paperclipai/paperclip 432
|
||||||
|
review-checkout-pr https://github.com/paperclipai/paperclip.git 432
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ $# -lt 2 || $# -gt 3 ]]; then
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
normalize_repo_slug() {
|
||||||
|
local raw="$1"
|
||||||
|
raw="${raw#git@github.com:}"
|
||||||
|
raw="${raw#ssh://git@github.com/}"
|
||||||
|
raw="${raw#https://github.com/}"
|
||||||
|
raw="${raw#http://github.com/}"
|
||||||
|
raw="${raw%.git}"
|
||||||
|
printf '%s\n' "${raw#/}"
|
||||||
|
}
|
||||||
|
|
||||||
|
repo_slug="$(normalize_repo_slug "$1")"
|
||||||
|
pr_number="$2"
|
||||||
|
|
||||||
|
if [[ ! "$repo_slug" =~ ^[^/]+/[^/]+$ ]]; then
|
||||||
|
echo "Expected GitHub repo slug like owner/repo or a GitHub repo URL, got: $1" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! "$pr_number" =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "PR number must be numeric, got: $pr_number" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
repo_key="${repo_slug//\//-}"
|
||||||
|
mirror_dir="/work/repos/${repo_key}"
|
||||||
|
checkout_dir="${3:-/work/checkouts/${repo_key}/pr-${pr_number}}"
|
||||||
|
pr_ref="refs/remotes/origin/pr/${pr_number}"
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$mirror_dir")" "$(dirname "$checkout_dir")"
|
||||||
|
|
||||||
|
if [[ ! -d "$mirror_dir/.git" ]]; then
|
||||||
|
if command -v gh >/dev/null 2>&1; then
|
||||||
|
gh repo clone "$repo_slug" "$mirror_dir" -- --filter=blob:none
|
||||||
|
else
|
||||||
|
git clone --filter=blob:none "https://github.com/${repo_slug}.git" "$mirror_dir"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
git -C "$mirror_dir" fetch --force origin "pull/${pr_number}/head:${pr_ref}"
|
||||||
|
|
||||||
|
if [[ -e "$checkout_dir" ]]; then
|
||||||
|
printf '%s\n' "$checkout_dir"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
git -C "$mirror_dir" worktree add --detach "$checkout_dir" "$pr_ref" >/dev/null
|
||||||
|
printf '%s\n' "$checkout_dir"
|
||||||
Reference in New Issue
Block a user