From 1b3d509b878b7e26e41128f7c313e0642953c131 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 11:42:13 +0000 Subject: [PATCH 1/3] Initial plan From eb78b3e33b483a8978d1d0093dba64176ad43814 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 11:54:42 +0000 Subject: [PATCH 2/3] Initial plan: Add Playwright CLI mode support Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 4e7e9a0d876..0baf5dc664f 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( charm.land/bubbletea/v2 v2.0.2 charm.land/lipgloss/v2 v2.0.2 github.com/charmbracelet/huh v0.8.0 + github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc github.com/charmbracelet/x/exp/golden v0.0.0-20251215102626-e0db08df7383 github.com/cli/go-gh/v2 v2.13.0 github.com/creack/pty v1.1.24 @@ -42,7 +43,6 @@ require ( github.com/charmbracelet/bubbletea v1.3.10 // indirect github.com/charmbracelet/colorprofile v0.4.2 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect - github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc // indirect github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect From 8ebf208d401db071bd6c4ccfe047d3047a90c7ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 12:16:11 +0000 Subject: [PATCH 3/3] feat: add Playwright CLI mode as default, add tools.playwright.mode: cli | mcp - Add Mode field to PlaywrightToolConfig (cli=default, mcp=docker) - Add GetMode() helper defaulting to cli - Parse mode field in parsePlaywrightTool - Add renderPlaywrightCLIConfigWithOptions for JSON CLI rendering - Add renderPlaywrightCLITOML for TOML CLI rendering - Update RenderPlaywrightMCP to dispatch based on mode - Skip Docker image download in CLI mode - Update JSON schema with mode field - Update golden tests and test assertions - Recompile all 174 workflow files Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/blog-auditor.lock.yml | 15 +--- .github/workflows/cloclo.lock.yml | 15 +--- .../daily-multi-device-docs-tester.lock.yml | 15 +--- .github/workflows/docs-noob-tester.lock.yml | 8 +- .../workflows/slide-deck-maintainer.lock.yml | 8 +- .github/workflows/smoke-claude.lock.yml | 15 +--- .github/workflows/smoke-codex.lock.yml | 31 +++----- .github/workflows/smoke-copilot-arm.lock.yml | 8 +- .github/workflows/smoke-copilot.lock.yml | 8 +- .github/workflows/unbloat-docs.lock.yml | 15 +--- .../weekly-editors-health-check.lock.yml | 8 +- pkg/parser/schemas/main_workflow_schema.json | 10 ++- pkg/workflow/docker.go | 15 ++-- pkg/workflow/importable_tools_test.go | 16 ++-- .../mcp_config_playwright_renderer.go | 77 +++++++++++++++++++ pkg/workflow/mcp_logs_upload_test.go | 9 ++- pkg/workflow/mcp_renderer_builtin.go | 43 ++++++++++- .../smoke-copilot.golden | 8 +- pkg/workflow/tools_parser.go | 5 ++ pkg/workflow/tools_types.go | 9 +++ 20 files changed, 212 insertions(+), 126 deletions(-) diff --git a/.github/workflows/blog-auditor.lock.yml b/.github/workflows/blog-auditor.lock.yml index f71970e1063..7099bc67a54 100644 --- a/.github/workflows/blog-auditor.lock.yml +++ b/.github/workflows/blog-auditor.lock.yml @@ -342,7 +342,7 @@ jobs: const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - name: Download container images - run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.24.2 ghcr.io/github/gh-aw-firewall/api-proxy:0.24.2 ghcr.io/github/gh-aw-firewall/squid:0.24.2 ghcr.io/github/gh-aw-mcpg:v0.1.17 ghcr.io/github/github-mcp-server:v0.32.0 mcr.microsoft.com/playwright/mcp node:lts-alpine + run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.24.2 ghcr.io/github/gh-aw-firewall/api-proxy:0.24.2 ghcr.io/github/gh-aw-firewall/squid:0.24.2 ghcr.io/github/gh-aw-mcpg:v0.1.17 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine - name: Write Safe Outputs Config run: | mkdir -p ${RUNNER_TEMP}/gh-aw/safeoutputs @@ -535,21 +535,14 @@ jobs: } }, "playwright": { - "container": "mcr.microsoft.com/playwright/mcp", + "command": "npx", "args": [ - "--init", - "--network", - "host", - "--security-opt", - "seccomp=unconfined", - "--ipc=host" - ], - "entrypointArgs": [ + "-y", + "@playwright/mcp", "--output-dir", "/tmp/gh-aw/mcp-logs/playwright", "--no-sandbox" ], - "mounts": ["/tmp/gh-aw/mcp-logs:/tmp/gh-aw/mcp-logs:rw"], "guard-policies": { "write-sink": { "accept": [ diff --git a/.github/workflows/cloclo.lock.yml b/.github/workflows/cloclo.lock.yml index d77dd923cbc..8ff421ab928 100644 --- a/.github/workflows/cloclo.lock.yml +++ b/.github/workflows/cloclo.lock.yml @@ -524,7 +524,7 @@ jobs: const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - name: Download container images - run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.24.2 ghcr.io/github/gh-aw-firewall/api-proxy:0.24.2 ghcr.io/github/gh-aw-firewall/squid:0.24.2 ghcr.io/github/gh-aw-mcpg:v0.1.17 ghcr.io/github/github-mcp-server:v0.32.0 ghcr.io/github/serena-mcp-server:latest mcr.microsoft.com/playwright/mcp node:lts-alpine + run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.24.2 ghcr.io/github/gh-aw-firewall/api-proxy:0.24.2 ghcr.io/github/gh-aw-firewall/squid:0.24.2 ghcr.io/github/gh-aw-mcpg:v0.1.17 ghcr.io/github/github-mcp-server:v0.32.0 ghcr.io/github/serena-mcp-server:latest node:lts-alpine - name: Install gh-aw extension env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -789,21 +789,14 @@ jobs: } }, "playwright": { - "container": "mcr.microsoft.com/playwright/mcp", + "command": "npx", "args": [ - "--init", - "--network", - "host", - "--security-opt", - "seccomp=unconfined", - "--ipc=host" - ], - "entrypointArgs": [ + "-y", + "@playwright/mcp", "--output-dir", "/tmp/gh-aw/mcp-logs/playwright", "--no-sandbox" ], - "mounts": ["/tmp/gh-aw/mcp-logs:/tmp/gh-aw/mcp-logs:rw"], "guard-policies": { "write-sink": { "accept": [ diff --git a/.github/workflows/daily-multi-device-docs-tester.lock.yml b/.github/workflows/daily-multi-device-docs-tester.lock.yml index 01677659f6f..10ade6fb429 100644 --- a/.github/workflows/daily-multi-device-docs-tester.lock.yml +++ b/.github/workflows/daily-multi-device-docs-tester.lock.yml @@ -355,7 +355,7 @@ jobs: const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - name: Download container images - run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.24.2 ghcr.io/github/gh-aw-firewall/api-proxy:0.24.2 ghcr.io/github/gh-aw-firewall/squid:0.24.2 ghcr.io/github/gh-aw-mcpg:v0.1.17 ghcr.io/github/github-mcp-server:v0.32.0 mcr.microsoft.com/playwright/mcp node:lts-alpine + run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.24.2 ghcr.io/github/gh-aw-firewall/api-proxy:0.24.2 ghcr.io/github/gh-aw-firewall/squid:0.24.2 ghcr.io/github/gh-aw-mcpg:v0.1.17 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine - name: Write Safe Outputs Config run: | mkdir -p ${RUNNER_TEMP}/gh-aw/safeoutputs @@ -568,21 +568,14 @@ jobs: } }, "playwright": { - "container": "mcr.microsoft.com/playwright/mcp", + "command": "npx", "args": [ - "--init", - "--network", - "host", - "--security-opt", - "seccomp=unconfined", - "--ipc=host" - ], - "entrypointArgs": [ + "-y", + "@playwright/mcp@v1.56.1", "--output-dir", "/tmp/gh-aw/mcp-logs/playwright", "--no-sandbox" ], - "mounts": ["/tmp/gh-aw/mcp-logs:/tmp/gh-aw/mcp-logs:rw"], "guard-policies": { "write-sink": { "accept": [ diff --git a/.github/workflows/docs-noob-tester.lock.yml b/.github/workflows/docs-noob-tester.lock.yml index 1a4729edebf..d09c593a511 100644 --- a/.github/workflows/docs-noob-tester.lock.yml +++ b/.github/workflows/docs-noob-tester.lock.yml @@ -337,7 +337,7 @@ jobs: const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - name: Download container images - run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.24.2 ghcr.io/github/gh-aw-firewall/api-proxy:0.24.2 ghcr.io/github/gh-aw-firewall/squid:0.24.2 ghcr.io/github/gh-aw-mcpg:v0.1.17 ghcr.io/github/github-mcp-server:v0.32.0 mcr.microsoft.com/playwright/mcp node:lts-alpine + run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.24.2 ghcr.io/github/gh-aw-firewall/api-proxy:0.24.2 ghcr.io/github/gh-aw-firewall/squid:0.24.2 ghcr.io/github/gh-aw-mcpg:v0.1.17 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine - name: Write Safe Outputs Config run: | mkdir -p ${RUNNER_TEMP}/gh-aw/safeoutputs @@ -546,10 +546,8 @@ jobs: }, "playwright": { "type": "stdio", - "container": "mcr.microsoft.com/playwright/mcp", - "args": ["--init", "--network", "host", "--security-opt", "seccomp=unconfined", "--ipc=host"], - "entrypointArgs": ["--output-dir", "/tmp/gh-aw/mcp-logs/playwright", "--no-sandbox"], - "mounts": ["/tmp/gh-aw/mcp-logs:/tmp/gh-aw/mcp-logs:rw"], + "command": "npx", + "args": ["-y", "@playwright/mcp", "--output-dir", "/tmp/gh-aw/mcp-logs/playwright", "--no-sandbox"], "guard-policies": { "write-sink": { "accept": [ diff --git a/.github/workflows/slide-deck-maintainer.lock.yml b/.github/workflows/slide-deck-maintainer.lock.yml index 5c0bf66a4eb..0e51b50312b 100644 --- a/.github/workflows/slide-deck-maintainer.lock.yml +++ b/.github/workflows/slide-deck-maintainer.lock.yml @@ -385,7 +385,7 @@ jobs: const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - name: Download container images - run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.24.2 ghcr.io/github/gh-aw-firewall/api-proxy:0.24.2 ghcr.io/github/gh-aw-firewall/squid:0.24.2 ghcr.io/github/gh-aw-mcpg:v0.1.17 ghcr.io/github/github-mcp-server:v0.32.0 mcr.microsoft.com/playwright/mcp node:lts-alpine + run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.24.2 ghcr.io/github/gh-aw-firewall/api-proxy:0.24.2 ghcr.io/github/gh-aw-firewall/squid:0.24.2 ghcr.io/github/gh-aw-mcpg:v0.1.17 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine - name: Write Safe Outputs Config run: | mkdir -p ${RUNNER_TEMP}/gh-aw/safeoutputs @@ -591,10 +591,8 @@ jobs: }, "playwright": { "type": "stdio", - "container": "mcr.microsoft.com/playwright/mcp", - "args": ["--init", "--network", "host", "--security-opt", "seccomp=unconfined", "--ipc=host"], - "entrypointArgs": ["--output-dir", "/tmp/gh-aw/mcp-logs/playwright", "--no-sandbox"], - "mounts": ["/tmp/gh-aw/mcp-logs:/tmp/gh-aw/mcp-logs:rw"], + "command": "npx", + "args": ["-y", "@playwright/mcp@v1.56.1", "--output-dir", "/tmp/gh-aw/mcp-logs/playwright", "--no-sandbox"], "guard-policies": { "write-sink": { "accept": [ diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index b113788e9ac..44cc83b20b2 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -825,7 +825,7 @@ jobs: const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - name: Download container images - run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.24.2 ghcr.io/github/gh-aw-firewall/api-proxy:0.24.2 ghcr.io/github/gh-aw-firewall/squid:0.24.2 ghcr.io/github/gh-aw-mcpg:v0.1.17 ghcr.io/github/github-mcp-server:v0.32.0 ghcr.io/github/serena-mcp-server:latest mcr.microsoft.com/playwright/mcp node:lts-alpine + run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.24.2 ghcr.io/github/gh-aw-firewall/api-proxy:0.24.2 ghcr.io/github/gh-aw-firewall/squid:0.24.2 ghcr.io/github/gh-aw-mcpg:v0.1.17 ghcr.io/github/github-mcp-server:v0.32.0 ghcr.io/github/serena-mcp-server:latest node:lts-alpine - name: Install gh-aw extension env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1842,21 +1842,14 @@ jobs: } }, "playwright": { - "container": "mcr.microsoft.com/playwright/mcp", + "command": "npx", "args": [ - "--init", - "--network", - "host", - "--security-opt", - "seccomp=unconfined", - "--ipc=host" - ], - "entrypointArgs": [ + "-y", + "@playwright/mcp", "--output-dir", "/tmp/gh-aw/mcp-logs/playwright", "--no-sandbox" ], - "mounts": ["/tmp/gh-aw/mcp-logs:/tmp/gh-aw/mcp-logs:rw"], "guard-policies": { "write-sink": { "accept": [ diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index 69a6b28693f..900295b2082 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -423,7 +423,7 @@ jobs: const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - name: Download container images - run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.24.2 ghcr.io/github/gh-aw-firewall/api-proxy:0.24.2 ghcr.io/github/gh-aw-firewall/squid:0.24.2 ghcr.io/github/gh-aw-mcpg:v0.1.17 ghcr.io/github/github-mcp-server:v0.32.0 ghcr.io/github/serena-mcp-server:latest mcp/fetch mcr.microsoft.com/playwright/mcp node:lts-alpine + run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.24.2 ghcr.io/github/gh-aw-firewall/api-proxy:0.24.2 ghcr.io/github/gh-aw-firewall/squid:0.24.2 ghcr.io/github/gh-aw-mcpg:v0.1.17 ghcr.io/github/github-mcp-server:v0.32.0 ghcr.io/github/serena-mcp-server:latest mcp/fetch node:lts-alpine - name: Write Safe Outputs Config run: | mkdir -p ${RUNNER_TEMP}/gh-aw/safeoutputs @@ -842,20 +842,14 @@ jobs: accept = ["*"] [mcp_servers.playwright] - container = "mcr.microsoft.com/playwright/mcp" + command = "npx" args = [ - "--init", - "--network", - "host", - "--security-opt", - "seccomp=unconfined", - "--ipc=host", - ] - entrypointArgs = [ + "-y", + "@playwright/mcp", "--output-dir", - "/tmp/gh-aw/mcp-logs/playwright" + "/tmp/gh-aw/mcp-logs/playwright", + "--no-sandbox" ] - mounts = ["/tmp/gh-aw/mcp-logs:/tmp/gh-aw/mcp-logs:rw"] [mcp_servers.playwright."guard-policies"] @@ -938,21 +932,14 @@ jobs: } }, "playwright": { - "container": "mcr.microsoft.com/playwright/mcp", + "command": "npx", "args": [ - "--init", - "--network", - "host", - "--security-opt", - "seccomp=unconfined", - "--ipc=host" - ], - "entrypointArgs": [ + "-y", + "@playwright/mcp", "--output-dir", "/tmp/gh-aw/mcp-logs/playwright", "--no-sandbox" ], - "mounts": ["/tmp/gh-aw/mcp-logs:/tmp/gh-aw/mcp-logs:rw"], "guard-policies": { "write-sink": { "accept": [ diff --git a/.github/workflows/smoke-copilot-arm.lock.yml b/.github/workflows/smoke-copilot-arm.lock.yml index 3cdc5c6d802..047680c33c8 100644 --- a/.github/workflows/smoke-copilot-arm.lock.yml +++ b/.github/workflows/smoke-copilot-arm.lock.yml @@ -455,7 +455,7 @@ jobs: const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - name: Download container images - run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.24.2 ghcr.io/github/gh-aw-firewall/api-proxy:0.24.2 ghcr.io/github/gh-aw-firewall/squid:0.24.2 ghcr.io/github/gh-aw-mcpg:v0.1.17 ghcr.io/github/github-mcp-server:v0.32.0 ghcr.io/github/serena-mcp-server:latest mcr.microsoft.com/playwright/mcp node:lts-alpine + run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.24.2 ghcr.io/github/gh-aw-firewall/api-proxy:0.24.2 ghcr.io/github/gh-aw-firewall/squid:0.24.2 ghcr.io/github/gh-aw-mcpg:v0.1.17 ghcr.io/github/github-mcp-server:v0.32.0 ghcr.io/github/serena-mcp-server:latest node:lts-alpine - name: Install gh-aw extension env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1402,10 +1402,8 @@ jobs: }, "playwright": { "type": "stdio", - "container": "mcr.microsoft.com/playwright/mcp", - "args": ["--init", "--network", "host", "--security-opt", "seccomp=unconfined", "--ipc=host"], - "entrypointArgs": ["--output-dir", "/tmp/gh-aw/mcp-logs/playwright", "--no-sandbox"], - "mounts": ["/tmp/gh-aw/mcp-logs:/tmp/gh-aw/mcp-logs:rw"], + "command": "npx", + "args": ["-y", "@playwright/mcp", "--output-dir", "/tmp/gh-aw/mcp-logs/playwright", "--no-sandbox"], "guard-policies": { "write-sink": { "accept": [ diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index 30f4fff4d51..1f6cb7f0f3b 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -462,7 +462,7 @@ jobs: const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - name: Download container images - run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.24.2 ghcr.io/github/gh-aw-firewall/api-proxy:0.24.2 ghcr.io/github/gh-aw-firewall/squid:0.24.2 ghcr.io/github/gh-aw-mcpg:v0.1.17 ghcr.io/github/github-mcp-server:v0.32.0 ghcr.io/github/serena-mcp-server:latest mcr.microsoft.com/playwright/mcp node:lts-alpine + run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.24.2 ghcr.io/github/gh-aw-firewall/api-proxy:0.24.2 ghcr.io/github/gh-aw-firewall/squid:0.24.2 ghcr.io/github/gh-aw-mcpg:v0.1.17 ghcr.io/github/github-mcp-server:v0.32.0 ghcr.io/github/serena-mcp-server:latest node:lts-alpine - name: Install gh-aw extension env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1450,10 +1450,8 @@ jobs: }, "playwright": { "type": "stdio", - "container": "mcr.microsoft.com/playwright/mcp", - "args": ["--init", "--network", "host", "--security-opt", "seccomp=unconfined", "--ipc=host"], - "entrypointArgs": ["--output-dir", "/tmp/gh-aw/mcp-logs/playwright", "--no-sandbox"], - "mounts": ["/tmp/gh-aw/mcp-logs:/tmp/gh-aw/mcp-logs:rw"], + "command": "npx", + "args": ["-y", "@playwright/mcp", "--output-dir", "/tmp/gh-aw/mcp-logs/playwright", "--no-sandbox"], "guard-policies": { "write-sink": { "accept": [ diff --git a/.github/workflows/unbloat-docs.lock.yml b/.github/workflows/unbloat-docs.lock.yml index fe0ecaa83f8..f969e710f68 100644 --- a/.github/workflows/unbloat-docs.lock.yml +++ b/.github/workflows/unbloat-docs.lock.yml @@ -449,7 +449,7 @@ jobs: const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - name: Download container images - run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.24.2 ghcr.io/github/gh-aw-firewall/api-proxy:0.24.2 ghcr.io/github/gh-aw-firewall/squid:0.24.2 ghcr.io/github/gh-aw-mcpg:v0.1.17 ghcr.io/github/github-mcp-server:v0.32.0 mcr.microsoft.com/playwright/mcp node:lts-alpine + run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.24.2 ghcr.io/github/gh-aw-firewall/api-proxy:0.24.2 ghcr.io/github/gh-aw-firewall/squid:0.24.2 ghcr.io/github/gh-aw-mcpg:v0.1.17 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine - name: Write Safe Outputs Config run: | mkdir -p ${RUNNER_TEMP}/gh-aw/safeoutputs @@ -800,23 +800,16 @@ jobs: } }, "playwright": { - "container": "mcr.microsoft.com/playwright/mcp", + "command": "npx", "args": [ - "--init", - "--network", - "host", - "--security-opt", - "seccomp=unconfined", - "--ipc=host" - ], - "entrypointArgs": [ + "-y", + "@playwright/mcp", "--output-dir", "/tmp/gh-aw/mcp-logs/playwright", "--no-sandbox", "--viewport-size", "1920x1080" ], - "mounts": ["/tmp/gh-aw/mcp-logs:/tmp/gh-aw/mcp-logs:rw"], "guard-policies": { "write-sink": { "accept": [ diff --git a/.github/workflows/weekly-editors-health-check.lock.yml b/.github/workflows/weekly-editors-health-check.lock.yml index fd0d6b9f019..364a28091f1 100644 --- a/.github/workflows/weekly-editors-health-check.lock.yml +++ b/.github/workflows/weekly-editors-health-check.lock.yml @@ -333,7 +333,7 @@ jobs: const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - name: Download container images - run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.24.2 ghcr.io/github/gh-aw-firewall/api-proxy:0.24.2 ghcr.io/github/gh-aw-firewall/squid:0.24.2 ghcr.io/github/gh-aw-mcpg:v0.1.17 ghcr.io/github/github-mcp-server:v0.32.0 mcr.microsoft.com/playwright/mcp node:lts-alpine + run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.24.2 ghcr.io/github/gh-aw-firewall/api-proxy:0.24.2 ghcr.io/github/gh-aw-firewall/squid:0.24.2 ghcr.io/github/gh-aw-mcpg:v0.1.17 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine - name: Write Safe Outputs Config run: | mkdir -p ${RUNNER_TEMP}/gh-aw/safeoutputs @@ -552,10 +552,8 @@ jobs: }, "playwright": { "type": "stdio", - "container": "mcr.microsoft.com/playwright/mcp", - "args": ["--init", "--network", "host", "--security-opt", "seccomp=unconfined", "--ipc=host"], - "entrypointArgs": ["--output-dir", "/tmp/gh-aw/mcp-logs/playwright", "--no-sandbox"], - "mounts": ["/tmp/gh-aw/mcp-logs:/tmp/gh-aw/mcp-logs:rw"], + "command": "npx", + "args": ["-y", "@playwright/mcp", "--output-dir", "/tmp/gh-aw/mcp-logs/playwright", "--no-sandbox"], "guard-policies": { "write-sink": { "accept": [ diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index a0f4abd5dc8..b79575434bc 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -3412,11 +3412,11 @@ ] }, "playwright": { - "description": "Playwright browser automation tool for web scraping, testing, and UI interactions in containerized browsers", + "description": "Playwright browser automation tool for web scraping, testing, and UI interactions", "oneOf": [ { "type": "null", - "description": "Enable Playwright tool with default settings" + "description": "Enable Playwright tool with default settings (cli mode)" }, { "type": "object", @@ -3433,6 +3433,12 @@ "items": { "type": "string" } + }, + "mode": { + "type": "string", + "description": "Playwright mode: 'cli' (default) runs via npx @playwright/mcp, 'mcp' runs in a Docker container", + "enum": ["cli", "mcp"], + "default": "cli" } }, "additionalProperties": false diff --git a/pkg/workflow/docker.go b/pkg/workflow/docker.go index c1a3ae10bd6..c94a1031d4d 100644 --- a/pkg/workflow/docker.go +++ b/pkg/workflow/docker.go @@ -30,12 +30,15 @@ func collectDockerImages(tools map[string]any, workflowData *WorkflowData, actio } } - // Check for Playwright tool (uses Docker image - no version tag, only one image) - if _, hasPlaywright := tools["playwright"]; hasPlaywright { - image := "mcr.microsoft.com/playwright/mcp" - if !imageSet[image] { - images = append(images, image) - imageSet[image] = true + // Check for Playwright tool (uses Docker image only in mcp mode; cli mode uses npx directly) + if playwrightTool, hasPlaywright := tools["playwright"]; hasPlaywright { + config := parsePlaywrightTool(playwrightTool) + if config.GetMode() == "mcp" { + image := "mcr.microsoft.com/playwright/mcp" + if !imageSet[image] { + images = append(images, image) + imageSet[image] = true + } } } diff --git a/pkg/workflow/importable_tools_test.go b/pkg/workflow/importable_tools_test.go index ff5cc95b1da..0a3030c1af6 100644 --- a/pkg/workflow/importable_tools_test.go +++ b/pkg/workflow/importable_tools_test.go @@ -78,9 +78,12 @@ Uses imported playwright tool. t.Error("Expected compiled workflow to contain playwright tool") } - // Verify playwright Docker image - if !strings.Contains(workflowData, "mcr.microsoft.com/playwright/mcp") { - t.Error("Expected compiled workflow to contain playwright Docker image") + // Verify playwright CLI command (default mode is cli) + if !strings.Contains(workflowData, `"command": "npx"`) { + t.Error("Expected compiled workflow to use npx command for playwright (cli mode)") + } + if !strings.Contains(workflowData, "@playwright/mcp@v1.41.0") { + t.Error("Expected compiled workflow to contain versioned playwright package") } } @@ -335,8 +338,11 @@ Uses all imported tools. } // Verify specific configurations - if !strings.Contains(workflowData, "mcr.microsoft.com/playwright/mcp") { - t.Error("Expected compiled workflow to contain playwright Docker image") + if !strings.Contains(workflowData, `"command": "npx"`) { + t.Error("Expected compiled workflow to use npx command for playwright (cli mode)") + } + if !strings.Contains(workflowData, "@playwright/mcp") { + t.Error("Expected compiled workflow to reference @playwright/mcp package") } if !strings.Contains(workflowData, "ghcr.io/github/serena-mcp-server:latest") { t.Error("Expected compiled workflow to contain serena Docker container") diff --git a/pkg/workflow/mcp_config_playwright_renderer.go b/pkg/workflow/mcp_config_playwright_renderer.go index 195e8f72707..0ac0f40035e 100644 --- a/pkg/workflow/mcp_config_playwright_renderer.go +++ b/pkg/workflow/mcp_config_playwright_renderer.go @@ -172,3 +172,80 @@ func renderPlaywrightMCPConfigWithOptions(yaml *strings.Builder, playwrightConfi yaml.WriteString(" },\n") } } + +// getPlaywrightPackage returns the @playwright/mcp npm package string with optional version +func getPlaywrightPackage(playwrightConfig *PlaywrightToolConfig) string { + if playwrightConfig != nil && playwrightConfig.Version != "" { + return "@playwright/mcp@" + playwrightConfig.Version + } + return "@playwright/mcp" +} + +// renderPlaywrightCLIConfigWithOptions generates the Playwright CLI (npx) MCP server configuration +// In CLI mode, Playwright runs via npx @playwright/mcp directly instead of a Docker container. +// This avoids pulling the mcr.microsoft.com/playwright/mcp Docker image. +func renderPlaywrightCLIConfigWithOptions(yaml *strings.Builder, playwrightConfig *PlaywrightToolConfig, isLast bool, includeCopilotFields bool, inlineArgs bool, guardPolicies map[string]any) { + mcpPlaywrightLog.Printf("Rendering Playwright CLI config options: copilot_fields=%t, inline_args=%t", includeCopilotFields, inlineArgs) + customArgs := getPlaywrightCustomArgs(playwrightConfig) + + // Extract all expressions from playwright arguments and replace them with env var references + expressions := extractExpressionsFromPlaywrightArgs(customArgs) + + // Replace expressions in custom args + if len(customArgs) > 0 { + mcpPlaywrightLog.Printf("Applying %d custom Playwright args with %d extracted expressions", len(customArgs), len(expressions)) + customArgs = replaceExpressionsInPlaywrightArgs(customArgs, expressions) + } + + playwrightPackage := getPlaywrightPackage(playwrightConfig) + + // Build the full args list: -y, @playwright/mcp[@version], --output-dir, ..., --no-sandbox, [customArgs...] + allArgs := []string{"-y", playwrightPackage, "--output-dir", "/tmp/gh-aw/mcp-logs/playwright", "--no-sandbox"} + if len(customArgs) > 0 { + allArgs = append(allArgs, customArgs...) + } + + yaml.WriteString(" \"playwright\": {\n") + + // Add type field for Copilot (per MCP Gateway Specification) + if includeCopilotFields { + yaml.WriteString(" \"type\": \"stdio\",\n") + } + + yaml.WriteString(" \"command\": \"npx\",\n") + + // Render args + if inlineArgs { + yaml.WriteString(" \"args\": [") + for i, arg := range allArgs { + if i > 0 { + yaml.WriteString(", ") + } + yaml.WriteString("\"" + arg + "\"") + } + yaml.WriteString("]") + } else { + yaml.WriteString(" \"args\": [\n") + for i, arg := range allArgs { + yaml.WriteString(" \"" + arg + "\"") + if i < len(allArgs)-1 { + yaml.WriteString(",") + } + yaml.WriteString("\n") + } + yaml.WriteString(" ]") + } + + if len(guardPolicies) > 0 { + yaml.WriteString(",\n") + renderGuardPoliciesJSON(yaml, guardPolicies, " ") + } else { + yaml.WriteString("\n") + } + + if isLast { + yaml.WriteString(" }\n") + } else { + yaml.WriteString(" },\n") + } +} diff --git a/pkg/workflow/mcp_logs_upload_test.go b/pkg/workflow/mcp_logs_upload_test.go index 584457a4005..69c6c82d6ee 100644 --- a/pkg/workflow/mcp_logs_upload_test.go +++ b/pkg/workflow/mcp_logs_upload_test.go @@ -55,9 +55,12 @@ Please navigate to example.com and take a screenshot. lockContentStr := string(lockContent) - // Verify Playwright MCP configuration uses official Docker image - if !strings.Contains(lockContentStr, "mcr.microsoft.com/playwright/mcp") { - t.Error("Expected Playwright MCP configuration to include official Docker image 'mcr.microsoft.com/playwright/mcp'") + // Verify Playwright CLI configuration uses npx command (default cli mode) + if !strings.Contains(lockContentStr, `"command": "npx"`) { + t.Error("Expected Playwright CLI configuration to use npx command") + } + if !strings.Contains(lockContentStr, "@playwright/mcp") { + t.Error("Expected Playwright CLI configuration to reference @playwright/mcp package") } // Verify the playwright output directory is pre-created so the Docker container diff --git a/pkg/workflow/mcp_renderer_builtin.go b/pkg/workflow/mcp_renderer_builtin.go index ee0777faddc..bca3c76690e 100644 --- a/pkg/workflow/mcp_renderer_builtin.go +++ b/pkg/workflow/mcp_renderer_builtin.go @@ -18,7 +18,11 @@ func (r *MCPConfigRendererUnified) RenderPlaywrightMCP(yaml *strings.Builder, pl playwrightConfig := parsePlaywrightTool(playwrightTool) if r.options.Format == "toml" { - r.renderPlaywrightTOML(yaml, playwrightConfig) + if playwrightConfig.GetMode() == "mcp" { + r.renderPlaywrightTOML(yaml, playwrightConfig) + } else { + r.renderPlaywrightCLITOML(yaml, playwrightConfig) + } // Add guard policies for TOML format as a separate section if len(r.options.WriteSinkGuardPolicies) > 0 { mcpRendererLog.Print("Adding guard-policies to playwright TOML (derived from GitHub guard-policy)") @@ -28,10 +32,14 @@ func (r *MCPConfigRendererUnified) RenderPlaywrightMCP(yaml *strings.Builder, pl } // JSON format - renderPlaywrightMCPConfigWithOptions(yaml, playwrightConfig, r.options.IsLast, r.options.IncludeCopilotFields, r.options.InlineArgs, r.options.WriteSinkGuardPolicies) + if playwrightConfig.GetMode() == "mcp" { + renderPlaywrightMCPConfigWithOptions(yaml, playwrightConfig, r.options.IsLast, r.options.IncludeCopilotFields, r.options.InlineArgs, r.options.WriteSinkGuardPolicies) + } else { + renderPlaywrightCLIConfigWithOptions(yaml, playwrightConfig, r.options.IsLast, r.options.IncludeCopilotFields, r.options.InlineArgs, r.options.WriteSinkGuardPolicies) + } } -// renderPlaywrightTOML generates Playwright MCP configuration in TOML format +// renderPlaywrightTOML generates Playwright MCP configuration in TOML format using a Docker container. // Per MCP Gateway Specification v1.0.0 section 3.2.1, stdio-based MCP servers MUST be containerized. // Uses MCP Gateway spec format: container, entrypointArgs, mounts, and args fields. func (r *MCPConfigRendererUnified) renderPlaywrightTOML(yaml *strings.Builder, playwrightConfig *PlaywrightToolConfig) { @@ -73,6 +81,35 @@ func (r *MCPConfigRendererUnified) renderPlaywrightTOML(yaml *strings.Builder, p yaml.WriteString(" mounts = [\"/tmp/gh-aw/mcp-logs:/tmp/gh-aw/mcp-logs:rw\"]\n") } +// renderPlaywrightCLITOML generates Playwright CLI configuration in TOML format using npx. +// In CLI mode, Playwright runs via npx @playwright/mcp directly without a Docker container. +func (r *MCPConfigRendererUnified) renderPlaywrightCLITOML(yaml *strings.Builder, playwrightConfig *PlaywrightToolConfig) { + mcpRendererBuiltinLog.Print("Rendering Playwright CLI in TOML format") + customArgs := getPlaywrightCustomArgs(playwrightConfig) + + playwrightPackage := getPlaywrightPackage(playwrightConfig) + + yaml.WriteString(" \n") + yaml.WriteString(" [mcp_servers.playwright]\n") + yaml.WriteString(" command = \"npx\"\n") + + // Build args: -y, @playwright/mcp[@version], --output-dir, ..., --no-sandbox, [customArgs...] + allArgs := []string{"-y", playwrightPackage, "--output-dir", "/tmp/gh-aw/mcp-logs/playwright", "--no-sandbox"} + if len(customArgs) > 0 { + allArgs = append(allArgs, customArgs...) + } + + yaml.WriteString(" args = [\n") + for i, arg := range allArgs { + yaml.WriteString(" \"" + arg + "\"") + if i < len(allArgs)-1 { + yaml.WriteString(",") + } + yaml.WriteString("\n") + } + yaml.WriteString(" ]\n") +} + // RenderSerenaMCP generates Serena MCP server configuration func (r *MCPConfigRendererUnified) RenderSerenaMCP(yaml *strings.Builder, serenaTool any) { mcpRendererLog.Printf("Rendering Serena MCP: format=%s, inline_args=%t", r.options.Format, r.options.InlineArgs) diff --git a/pkg/workflow/testdata/wasm_golden/TestWasmGolden_CompileFixtures/smoke-copilot.golden b/pkg/workflow/testdata/wasm_golden/TestWasmGolden_CompileFixtures/smoke-copilot.golden index 51a6929101f..8498989296c 100644 --- a/pkg/workflow/testdata/wasm_golden/TestWasmGolden_CompileFixtures/smoke-copilot.golden +++ b/pkg/workflow/testdata/wasm_golden/TestWasmGolden_CompileFixtures/smoke-copilot.golden @@ -406,7 +406,7 @@ jobs: const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - name: Download container images - run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.24.2 ghcr.io/github/gh-aw-firewall/api-proxy:0.24.2 ghcr.io/github/gh-aw-firewall/squid:0.24.2 ghcr.io/github/gh-aw-mcpg:v0.1.17 ghcr.io/github/github-mcp-server:v0.32.0 ghcr.io/github/serena-mcp-server:latest mcr.microsoft.com/playwright/mcp + run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.24.2 ghcr.io/github/gh-aw-firewall/api-proxy:0.24.2 ghcr.io/github/gh-aw-firewall/squid:0.24.2 ghcr.io/github/gh-aw-mcpg:v0.1.17 ghcr.io/github/github-mcp-server:v0.32.0 ghcr.io/github/serena-mcp-server:latest - name: Install gh-aw extension env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -498,10 +498,8 @@ jobs: }, "playwright": { "type": "stdio", - "container": "mcr.microsoft.com/playwright/mcp", - "args": ["--init", "--network", "host", "--security-opt", "seccomp=unconfined", "--ipc=host"], - "entrypointArgs": ["--output-dir", "/tmp/gh-aw/mcp-logs/playwright", "--no-sandbox"], - "mounts": ["/tmp/gh-aw/mcp-logs:/tmp/gh-aw/mcp-logs:rw"], + "command": "npx", + "args": ["-y", "@playwright/mcp", "--output-dir", "/tmp/gh-aw/mcp-logs/playwright", "--no-sandbox"], "guard-policies": { "write-sink": { "accept": [ diff --git a/pkg/workflow/tools_parser.go b/pkg/workflow/tools_parser.go index 81305ecaa8b..1dd1cbbb96c 100644 --- a/pkg/workflow/tools_parser.go +++ b/pkg/workflow/tools_parser.go @@ -309,6 +309,11 @@ func parsePlaywrightTool(val any) *PlaywrightToolConfig { config.Version = fmt.Sprintf("%g", versionNum) } + // Handle mode field - "cli" (default) or "mcp" + if mode, ok := configMap["mode"].(string); ok { + config.Mode = mode + } + // Handle args field - can be []any or []string if argsValue, ok := configMap["args"]; ok { if arr, ok := argsValue.([]any); ok { diff --git a/pkg/workflow/tools_types.go b/pkg/workflow/tools_types.go index 22f666171b3..69ee349b108 100644 --- a/pkg/workflow/tools_types.go +++ b/pkg/workflow/tools_types.go @@ -304,9 +304,18 @@ type GitHubToolConfig struct { // PlaywrightToolConfig represents the configuration for the Playwright tool type PlaywrightToolConfig struct { Version string `yaml:"version,omitempty"` + Mode string `yaml:"mode,omitempty"` // "cli" (default) or "mcp" Args []string `yaml:"args,omitempty"` } +// GetMode returns the effective Playwright mode, defaulting to "cli" if not specified. +func (c *PlaywrightToolConfig) GetMode() string { + if c != nil && c.Mode == "mcp" { + return "mcp" + } + return "cli" +} + // SerenaToolConfig represents the configuration for the Serena MCP tool type SerenaToolConfig struct { Version string `yaml:"version,omitempty"`