mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-19 00:03:19 +00:00
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:
co-authored by
Claude Opus 4.6
parent
93f927c1c1
commit
c93b2e287c
@@ -7,3 +7,4 @@ frontend/bun.lock
|
||||
frontend/dist/
|
||||
frontend/.vite/
|
||||
public/
|
||||
.env
|
||||
|
||||
+48
-1
@@ -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