feat: add screenshot capture and R2 upload support for sandbox agents
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
1
dev-dashboard/.gitignore
vendored
1
dev-dashboard/.gitignore
vendored
@@ -7,3 +7,4 @@ frontend/bun.lock
|
||||
frontend/dist/
|
||||
frontend/.vite/
|
||||
public/
|
||||
.env
|
||||
|
||||
@@ -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=<your-r2-access-key>
|
||||
AWS_SECRET_ACCESS_KEY=<your-r2-secret-key>
|
||||
|
||||
# Account ID is on the R2 overview page (right sidebar)
|
||||
R2_ENDPOINT=https://<ACCOUNT_ID>.r2.cloudflarestorage.com
|
||||
R2_BUCKET=windmill-screenshots
|
||||
|
||||
# Enable public access on the bucket (Settings → Public access → r2.dev subdomain)
|
||||
R2_PUBLIC_URL=https://pub-<hash>.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 |
|
||||
|
||||
@@ -87,23 +87,59 @@ export { readEnvLocal } from "./env";
|
||||
function buildSandboxSystemPrompt(env: Record<string, string>): 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/<branch>/screenshot.png`,
|
||||
`4) Include screenshots in PR descriptions as markdown images: `,
|
||||
);
|
||||
}
|
||||
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<string, string>, 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"]);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user