From c93b2e287c2bd37b95bb790e92cf6d3004cb54b8 Mon Sep 17 00:00:00 2001 From: centdix Date: Sun, 22 Feb 2026 00:36:17 +0000 Subject: [PATCH] feat: add screenshot capture and R2 upload support for sandbox agents Co-Authored-By: Claude Opus 4.6 --- Dockerfile.sandbox | 27 +++++++++++++++ dev-dashboard/.gitignore | 1 + dev-dashboard/README.md | 49 +++++++++++++++++++++++++++- dev-dashboard/backend/src/workmux.ts | 42 ++++++++++++++++++++++-- dev-dashboard/dev.sh | 5 +++ dev-dashboard/run.sh | 5 +++ 6 files changed, 125 insertions(+), 4 deletions(-) diff --git a/Dockerfile.sandbox b/Dockerfile.sandbox index e14e25205c..ec12bce048 100644 --- a/Dockerfile.sandbox +++ b/Dockerfile.sandbox @@ -7,6 +7,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ iptables \ gosu \ sudo \ + unzip \ # Rust native build deps (for cargo check) pkg-config \ cmake \ @@ -147,6 +148,32 @@ RUN curl -fsSL https://claude.ai/install.sh | bash && \ # --- codex agent --- RUN npm i -g @openai/codex +# ── Install bun (globally accessible) ───────────────────────────────────────── +ENV BUN_INSTALL=/opt/bun +RUN curl -fsSL https://bun.sh/install | bash \ + && ln -s /opt/bun/bin/bun /usr/local/bin/bun \ + && ln -s /opt/bun/bin/bunx /usr/local/bin/bunx \ + && chmod -R a+rX /opt/bun + +# ── Playwright + Chromium (for screenshots) ────────────────────────────────── +ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers +RUN bun add -g @playwright/test \ + && bunx playwright install chromium --with-deps \ + && chmod -R a+rwX /opt/bun/install/ \ + && rm -rf /tmp/bunx-* \ + && chmod -R a+rX /opt/playwright-browsers \ + && rm -rf /var/lib/apt/lists/* + +# ── AWS CLI (for S3-compatible uploads to R2) ───────────────────────────────── +RUN curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o /tmp/awscliv2.zip \ + && unzip -q /tmp/awscliv2.zip -d /tmp \ + && /tmp/aws/install \ + && rm -rf /tmp/aws /tmp/awscliv2.zip + +# Pre-configure AWS CLI for R2: credentials come from env vars automatically +# (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY), just set the defaults. +ENV AWS_DEFAULT_REGION=auto + # 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 diff --git a/dev-dashboard/.gitignore b/dev-dashboard/.gitignore index e90fbf6a68..f9c7a52508 100644 --- a/dev-dashboard/.gitignore +++ b/dev-dashboard/.gitignore @@ -7,3 +7,4 @@ frontend/bun.lock frontend/dist/ frontend/.vite/ public/ +.env diff --git a/dev-dashboard/README.md b/dev-dashboard/README.md index 08a531a549..2c9ca711a2 100644 --- a/dev-dashboard/README.md +++ b/dev-dashboard/README.md @@ -17,10 +17,26 @@ nerdfont: false sandbox: image: windmill-sandbox + + # Forward R2/AWS credentials into sandbox containers (for screenshot uploads). + # The actual values come from dev-dashboard/.env, sourced by dev.sh/run.sh. + env_passthrough: + - AWS_ACCESS_KEY_ID + - AWS_SECRET_ACCESS_KEY + - R2_ENDPOINT + - R2_BUCKET + - R2_PUBLIC_URL + extra_mounts: + # Codex agent credentials - host_path: ~/.codex guest_path: /tmp/.codex writable: true + # EE repo access (optional — only needed for enterprise features) + - host_path: ~/windmill-ee-private + writable: true + - host_path: ~/windmill-ee-private__worktrees + writable: true EOF # 3. (Optional) Build sandbox image — only needed for agent-yolo profile @@ -91,17 +107,28 @@ nerdfont: false sandbox: image: windmill-sandbox + env_passthrough: + - AWS_ACCESS_KEY_ID + - AWS_SECRET_ACCESS_KEY + - R2_ENDPOINT + - R2_BUCKET + - R2_PUBLIC_URL extra_mounts: - host_path: ~/.codex guest_path: /tmp/.codex writable: true + - host_path: ~/windmill-ee-private + writable: true + - host_path: ~/windmill-ee-private__worktrees + writable: true ``` **Fields:** - **`nerdfont`** — Set to `true` if your terminal uses a Nerd Font (adds icons to `workmux list` output). Default `false`. - **`sandbox.image`** — Docker image used for `agent-yolo` sandboxed worktrees. Must be pre-built with `workmux sandbox build` or pulled with `workmux sandbox pull`. -- **`sandbox.extra_mounts`** — Additional bind mounts into sandbox containers. The example above mounts Codex credentials so the Codex agent can authenticate from inside the container. +- **`sandbox.env_passthrough`** — Host env vars to forward into sandbox containers (global config only). Used here for R2 screenshot upload credentials. +- **`sandbox.extra_mounts`** — Additional bind mounts into sandbox containers. Mounts Codex credentials and the EE repo for enterprise features. To build the sandbox image (from the Windmill repo root): @@ -155,6 +182,26 @@ Open http://localhost:5112 in your browser. The frontend dev server is hardcoded to port `5112` and proxies `/api/*` and `/ws/*` to the backend. +### Screenshot uploads (optional) + +Sandbox agents can take screenshots of the frontend UI with Playwright and upload them to a Cloudflare R2 bucket for use in PR descriptions. To enable this, create a `dev-dashboard/.env` file (already gitignored): + +```bash +# Cloudflare R2 credentials — get from: +# Dashboard → R2 → Manage R2 API Tokens → Create API Token (Object Read & Write, scoped to your bucket) +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= + +# Account ID is on the R2 overview page (right sidebar) +R2_ENDPOINT=https://.r2.cloudflarestorage.com +R2_BUCKET=windmill-screenshots + +# Enable public access on the bucket (Settings → Public access → r2.dev subdomain) +R2_PUBLIC_URL=https://pub-.r2.dev +``` + +When these are set, `dev.sh`/`run.sh` source the file and the env vars are inlined onto the `workmux sandbox agent` command. The workmux global config's `env_passthrough` (see [above](#workmux-global-config)) forwards them into the container. The agent's system prompt automatically includes screenshot instructions when R2 is configured. + ## API | Method | Endpoint | Description | diff --git a/dev-dashboard/backend/src/workmux.ts b/dev-dashboard/backend/src/workmux.ts index 21b7513991..e656b74a4c 100644 --- a/dev-dashboard/backend/src/workmux.ts +++ b/dev-dashboard/backend/src/workmux.ts @@ -87,23 +87,59 @@ export { readEnvLocal } from "./env"; function buildSandboxSystemPrompt(env: Record): string { const backendPort = env.BACKEND_PORT || "8000"; const frontendPort = env.FRONTEND_PORT || "3000"; + const hasR2 = !!(process.env.R2_ENDPOINT && process.env.R2_BUCKET && process.env.R2_PUBLIC_URL); + console.log(`[workmux:buildSandboxSystemPrompt] hasR2=${hasR2}`); const lines: string[] = [ "You are running inside a sandboxed container with full permissions.", `This worktree is configured with the following ports:`, `- Backend: port ${backendPort}. Start with: cd backend && PORT=${backendPort} DATABASE_URL=postgres://postgres:changeme@localhost:5432/windmill cargo watch -x run`, `- Frontend: port ${frontendPort}. Start with: cd frontend && REMOTE=http://localhost:${backendPort} npm run dev -- --port ${frontendPort} --host 0.0.0.0`, ]; + if (hasR2) { + lines.push( + `--- Screenshots ---`, + `You can take screenshots of the frontend UI and upload them to R2 for use in PR descriptions.`, + `1) Take a screenshot: PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers bunx playwright screenshot --browser chromium http://localhost:${frontendPort}/path/to/page /tmp/screenshot.png`, + `2) Upload to R2: aws s3 cp /tmp/screenshot.png "s3://$R2_BUCKET/$(git rev-parse --abbrev-ref HEAD)/screenshot.png" --endpoint-url "$R2_ENDPOINT"`, + `3) The public URL will be: $R2_PUBLIC_URL//screenshot.png`, + `4) Include screenshots in PR descriptions as markdown images: ![description]($R2_PUBLIC_URL//screenshot.png)`, + ); + } return lines.join(" "); } +/** Env vars to forward into the sandbox container (via workmux env_passthrough). */ +const SANDBOX_ENV_PASSTHROUGH = [ + "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY", + "R2_ENDPOINT", + "R2_BUCKET", + "R2_PUBLIC_URL", +]; + +/** Build an inline env prefix (e.g. "KEY=val KEY2=val2 ") from process.env. */ +function buildEnvPrefix(): string { + const parts: string[] = []; + for (const key of SANDBOX_ENV_PASSTHROUGH) { + const val = process.env[key]; + if (val) { + // Shell-escape the value (single quotes, escaping inner single quotes) + const escaped = val.replace(/'/g, "'\\''"); + parts.push(`${key}='${escaped}'`); + } + } + return parts.length > 0 ? parts.join(" ") + " " : ""; +} + function buildSandboxAgentCmd(env: Record, agent: Agent): string { const prompt = buildSandboxSystemPrompt(env); const innerEscaped = prompt.replace(/["\\$`]/g, "\\$&"); + const envPrefix = buildEnvPrefix(); if (agent === "codex") { - return `workmux sandbox agent -- codex --yolo -c '"developer_instructions=${innerEscaped}"'`; + return `${envPrefix}workmux sandbox agent -- codex --yolo -c '"developer_instructions=${innerEscaped}"'`; } - return `workmux sandbox agent -- claude --dangerously-skip-permissions --append-system-prompt '"${innerEscaped}"'`; + return `${envPrefix}workmux sandbox agent -- claude --dangerously-skip-permissions --append-system-prompt '"${innerEscaped}"'`; } export async function addWorktree( @@ -163,7 +199,7 @@ export async function addWorktree( for (let i = paneIds.length - 1; i >= 1; i--) { Bun.spawnSync(["tmux", "kill-pane", "-t", `${windowTarget}.${paneIds[i]}`]); } - // Build and send agent command for sandbox + // Build and send agent command for sandbox (env vars are inlined as a prefix) const agentCmd = buildSandboxAgentCmd(env, agent); console.log(`[workmux] sending command to ${windowTarget}.0:\n${agentCmd}`); Bun.spawnSync(["tmux", "send-keys", "-t", `${windowTarget}.0`, agentCmd, "Enter"]); diff --git a/dev-dashboard/dev.sh b/dev-dashboard/dev.sh index 8f22f62c93..623dd7fffe 100755 --- a/dev-dashboard/dev.sh +++ b/dev-dashboard/dev.sh @@ -2,6 +2,11 @@ set -euo pipefail cd "$(dirname "$0")" +# Load env vars (R2 credentials, etc.) if present +if [ -f .env ]; then + set -a; source .env; set +a +fi + cleanup() { kill $BE_PID $FE_PID 2>/dev/null || true } diff --git a/dev-dashboard/run.sh b/dev-dashboard/run.sh index 67acab2582..5e22df0b31 100755 --- a/dev-dashboard/run.sh +++ b/dev-dashboard/run.sh @@ -2,6 +2,11 @@ set -euo pipefail cd "$(dirname "$0")" +# Load env vars (R2 credentials, etc.) if present +if [ -f .env ]; then + set -a; source .env; set +a +fi + # Build frontend cd frontend bun run build