[1197c975] Re-add Playwright chromium to FE/UX QA images and add browser-verification prompt guidance (#406)

* [3e552255] Re-add Playwright chromium to QA images + prompt guidance (#395) (#405)

* [3e552255] feat(docker): re-add Playwright chromium-headless-shell to QA images

* [3e552255] ci(docker): add Playwright QA image build + headless smoke check workflow

* [3e552255] fix(ci): scope agent-image-smoke.yml trigger to paths only, add PR comment

The workflow was gated by `branches: [master]` on both push and
pull_request, but this repo's task-hierarchy PRs open against nested
parent feature branches, not master, until root->master assembly - so
the workflow never fired on a dev-level PR and produced zero evidence.
Drop the branch filter (path scoping is sufficient) and post the
size-delta table + smoke-check output as a PR comment via
actions/github-script, since no agent role has gh CLI or GitHub API
read access to pull check-run output directly.

* [3e552255] fix(ci): post agent-image-smoke PR comment even on step failure

The 'Post results as a PR comment' step only had
`if: github.event_name == 'pull_request'`, which GitHub implicitly ANDs
with success() — so if the docker build or headless-launch smoke check
failed, the PR comment (the only evidence-delivery path QA/PM has, since
no agent role can read the Checks tab) silently never posted. Added
always() so a partial report always lands on the PR.

This commit also re-lands the branches-filter removal + PR-comment step
from 747e4d74 to make sure this fix actually reaches the remote PR
branch — QA's needs_revision at 05:48 came after that commit's local
timestamp (05:43) but still saw the pre-fix workflow, indicating the
prior push never reached GitHub.

* [3e552255] docs(qa): add browser verification guide and CHANGELOG entry for Playwright chromium

- Added comprehensive QA browser verification guide at docs/backend/qa/browser-verification.md covering setup, examples for fe-qa/ux-qa, limitations, and troubleshooting
- Updated CHANGELOG.md with Unreleased entry documenting Playwright chromium-headless-shell re-add to agent-qa-fe and agent-ux images, CI smoke workflow, and links to QA guidance
- Guide explains when to use browser verification (rendered output, computed styles, a11y, visual design), how to launch headless chromium, and provides practical examples for both FE and UX QA use cases
- References built-in guidance in fe-qa.md/ux-qa.md identity prompts and CI smoke workflow verification

---------

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>

* [9aafe8f5] Revision: fix reflow-check CI failure and resolve orchestrator Playwright-allowance gap (#421)

* [9973237c] fix(docs): separate reflow-joined metadata lines in browser-verification.md (#419)

scripts/reflow_md.py --check treats two adjacent non-blank lines as one
paragraph and flags it as hard-wrapped prose needing a join. Insert a
blank line between the `**For:**` and `**Purpose:**` metadata lines so
each stays its own single-line paragraph; verified the reflow is now a
no-op and the non-whitespace token sequence is unchanged.

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>

* [cc355a2b] docs(qa): add no-op analysis for orchestrator.py Playwright allowance (#422)

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>

* [a506cc10] docs(qa): quote real Dockerfile Playwright snippets in no-op analysis (#424)

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>

---------

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>

* [2b35dd4e] Resolve merge conflict, confirm green CI, add Playwright MCP registration, re-verify 5 ACs (#447) (#450)

* [2b35dd4e] docs(changelog): resolve Unreleased/0.22.0 ordering conflict, keep Playwright entry

* [2b35dd4e] feat(runtime): register Playwright MCP server for fe-qa/ux-qa, per CEO round-3 note

Adds @playwright/mcp to agent-qa-fe and agent-ux images, wired via a wrapper
entrypoint that points the server at each image's already-baked
chromium-headless-shell instead of downloading a second browser. The
orchestrator registers the `playwright` MCP server only for the qa role on
the frontend/ux_ui teams, so be-qa and ux-dev never see it. Updates the QA
identity prompts and docs/backend/qa/browser-verification.md to document the
structured mcp__playwright__* tools in place of hand-scripted Bash+Python,
adds CI smoke coverage (binary + baked-chromium resolution + a real
panel-page screenshot from inside the ux-qa image), and records the change
in CHANGELOG.md.

---------

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>

* [1197c975] type the mcpServers extraction so mypy's no-any-return passes

* [1197c975] extract role-scoped MCP registration — orchestrator back under the complexity budget

The playwright branch pushed _generate_mcp_config to rank C in the merge
ref; docs/research/playwright registrations move to one helper, behavior
identical.

* [1197c975] basename-sanitize the MCP config filename

CodeQL's path-injection query re-fired on the (moved, unchanged) config
write; agent ids are orchestrator-issued, but the filename now rides the
same os.path.basename sanitizer _grok_usage_json established.

* [1197c975] basename the agent id variable itself — the sanitizer shape CodeQL models

---------

Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-11 07:39:53 +02:00
committed by GitHub
co-authored by Backend Developer 1 Backend Documenter Renn F
parent 7f138d3bf5
commit f0f09b2204
10 changed files with 545 additions and 17 deletions
+201
View File
@@ -0,0 +1,201 @@
name: Agent Image Smoke (Playwright)
# Builds the two images that carry Playwright + chromium-headless-shell
# (agent-qa-fe, agent-ux) with a real Docker daemon, verifies a headless
# chromium launch actually works inside each, verifies the `playwright` MCP
# server (registered for fe-qa/ux-qa only, see orchestrator.py
# _generate_mcp_config) is installed and wired to the baked browser, takes a
# real headless screenshot of a live panel page from inside the ux-qa image,
# and reports the real before/after image size delta against origin/master.
# This exists because dev/QA agent sandboxes have no Docker daemon of their
# own — this workflow is the only place that can produce non-estimated
# numbers for this change.
on:
push:
paths:
- 'docker/agent-base.Dockerfile'
- 'docker/agent-qa-fe.Dockerfile'
- 'docker/agent-ux.Dockerfile'
- 'docker/scripts/playwright-mcp-entrypoint.sh'
- '.github/workflows/agent-image-smoke.yml'
pull_request:
# No `branches:` filter — this repo's task hierarchy opens dev-level
# PRs against nested parent feature branches (e.g.
# feature/backend/<root>--<cell>--<subcell>), not directly against
# master, so a `branches: [master]` filter here would only ever fire
# once the assembled PR reaches root->master — too late to give QA/PM
# real evidence at the dev-PR review stage this task exists for.
paths:
- 'docker/agent-base.Dockerfile'
- 'docker/agent-qa-fe.Dockerfile'
- 'docker/agent-ux.Dockerfile'
- 'docker/scripts/playwright-mcp-entrypoint.sh'
- '.github/workflows/agent-image-smoke.yml'
workflow_dispatch:
jobs:
playwright-smoke:
name: Build QA images, verify headless chromium launch + size delta
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
# Need origin/master reachable to diff this branch's Dockerfiles
# against the pre-change baseline for the size-delta report below.
fetch-depth: 0
- name: Build agent-base (shared FROM target for both variants)
run: docker build -f docker/agent-base.Dockerfile -t roboco-agent-base .
- name: Build "after" images (this branch)
run: |
docker build -f docker/agent-qa-fe.Dockerfile -t agent-qa-fe:after .
docker build -f docker/agent-ux.Dockerfile -t agent-ux:after .
- name: Build "before" images (origin/master baseline)
run: |
mkdir -p /tmp/before
git show origin/master:docker/agent-qa-fe.Dockerfile > /tmp/before/agent-qa-fe.Dockerfile
git show origin/master:docker/agent-ux.Dockerfile > /tmp/before/agent-ux.Dockerfile
docker build -f /tmp/before/agent-qa-fe.Dockerfile -t agent-qa-fe:before .
docker build -f /tmp/before/agent-ux.Dockerfile -t agent-ux:before .
- name: Report real before/after image size delta
run: |
{
echo "### Playwright image size delta (real \`docker inspect\` sizes)"
echo
echo "| image | before | after | delta |"
echo "|---|---|---|---|"
for name in agent-qa-fe agent-ux; do
before_bytes=$(docker inspect -f '{{.Size}}' "${name}:before")
after_bytes=$(docker inspect -f '{{.Size}}' "${name}:after")
delta_mb=$(( (after_bytes - before_bytes) / 1024 / 1024 ))
echo "| ${name} | $(( before_bytes / 1024 / 1024 ))MB | $(( after_bytes / 1024 / 1024 ))MB | +${delta_mb}MB |"
done
} | tee -a "$GITHUB_STEP_SUMMARY" /tmp/smoke-report.md
- name: Headless chromium launch smoke check
run: |
{
echo
echo "### Headless chromium launch smoke check"
echo
} | tee -a /tmp/smoke-report.md
for name in agent-qa-fe agent-ux; do
echo "::group::${name} headless launch smoke"
docker run --rm --entrypoint /app/.venv/bin/python "${name}:after" -c '
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("about:blank")
assert page.title() == ""
browser.close()
print("PLAYWRIGHT_SMOKE_OK")
' | tee -a /tmp/smoke-report.md
echo "::endgroup::"
done
- name: Playwright MCP server smoke check (binary + baked-chromium wiring)
run: |
{
echo
echo "### Playwright MCP server smoke check"
echo
} | tee -a /tmp/smoke-report.md
for name in agent-qa-fe agent-ux; do
echo "::group::${name} playwright-mcp binary"
docker run --rm --entrypoint playwright-mcp "${name}:after" --version | tee -a /tmp/smoke-report.md
# Confirms the exact chromium-headless-shell path the wrapper
# entrypoint (docker/scripts/playwright-mcp-entrypoint.sh) will
# resolve and pass to `playwright-mcp --executable-path` is a
# real, executable file baked into this image — not a second,
# separately-downloaded browser.
docker run --rm --entrypoint /app/.venv/bin/python "${name}:after" -c '
import os
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
path = p.chromium.executable_path
assert os.path.isfile(path) and os.access(path, os.X_OK), f"not executable: {path}"
print("RESOLVED_CHROMIUM_PATH_OK:", path)
' | tee -a /tmp/smoke-report.md
echo "::endgroup::"
done
- name: Set up Node (for the panel screenshot step below)
uses: actions/setup-node@v6
with:
node-version: '22'
- name: Enable corepack (pnpm)
run: corepack enable
- name: Build & start the panel (headless browser verification target)
working-directory: panel
env:
# pnpm 11 prompts for confirmation on modules-purge unless CI=true
# (matches docker/panel.Dockerfile's build stage).
CI: 'true'
run: |
pnpm install --frozen-lockfile --shamefully-hoist
pnpm build
nohup pnpm start -p 3000 > /tmp/panel.log 2>&1 &
npx --yes wait-on http://localhost:3000/login -t 60000
- name: Headless browser verification screenshot (ux-qa image)
# Demonstrates the ux-qa image's browser actually renders a live
# panel page end-to-end, not just about:blank. /login needs no
# backend/auth (proxy.ts excludes it from its matcher) so this has
# no dependency on the orchestrator/DB being up.
run: |
mkdir -p /tmp/pw-artifacts
chmod 777 /tmp/pw-artifacts
docker run --rm --add-host=host.docker.internal:host-gateway \
-v /tmp/pw-artifacts:/tmp/out \
--entrypoint /app/.venv/bin/python agent-ux:after -c '
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(viewport={"width": 1280, "height": 800})
page.goto("http://host.docker.internal:3000/login", wait_until="networkidle")
page.screenshot(path="/tmp/out/ux-qa-panel-login.png")
browser.close()
print("PANEL_SCREENSHOT_OK")
'
{
echo
echo "### Headless browser verification screenshot (ux-qa image)"
echo
echo "Screenshot of a live panel page (\`/login\`), taken from inside \`agent-ux:after\`. See the \`ux-qa-panel-screenshot\` build artifact."
} | tee -a /tmp/smoke-report.md
- name: Upload panel screenshot artifact
uses: actions/upload-artifact@v4
with:
name: ux-qa-panel-screenshot
path: /tmp/pw-artifacts/ux-qa-panel-login.png
- name: Post results as a PR comment
# always() so the PR comment (the only evidence-delivery path QA/PM
# has, since no agent role can read the Checks tab directly) still
# posts a partial report even if an earlier build/smoke step failed —
# an `if:` that doesn't call a status function is otherwise
# implicitly ANDed with success(), silently swallowing the report.
if: always() && github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const body = fs.readFileSync('/tmp/smoke-report.md', 'utf8');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `## Agent Image Smoke (Playwright) results\n\n${body}`,
});
+7
View File
@@ -4,6 +4,13 @@ All notable changes to RoboCo are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- **Playwright chromium headless verification for QA images.** `agent-qa-fe` and `agent-ux` (the shared UX/QA image) now ship Playwright with `chromium-headless-shell` pre-installed for browser-based QA verification — fe-qa and ux-qa agents can now launch headless chromium to verify rendered output, computed styles, a11y trees, and visual design when reading the diff alone cannot settle an acceptance criterion. A new CI workflow (`agent-image-smoke.yml`) builds real before/after images, runs a headless-launch smoke check inside each, and reports the real `docker inspect` size delta as a PR comment — the only non-estimated verification source available since agent sandboxes have no Docker daemon. See `docs/backend/qa/browser-verification.md` for examples and `fe-qa.md` / `ux-qa.md` identity prompts for the built-in guidance.
- **Playwright MCP server for QA's browser verification.** Hand-scripting the Playwright Python sync API through `Bash` is fragile for an agent, so `agent-qa-fe`/`agent-ux` now also ship `@playwright/mcp`, registered by the orchestrator as a `playwright` MCP server (`mcp__playwright__*`) for the `fe-qa`/`ux-qa` roles only — role-gated, not image-gated, so `be-qa` and `ux-dev` (which shares `agent-ux`'s image) never see it. A wrapper entrypoint (`docker/scripts/playwright-mcp-entrypoint.sh`) points the server at the image's existing baked `chromium-headless-shell` instead of letting it download a second browser. QA now drives the browser through structured `browser_navigate`/`browser_snapshot`/`browser_evaluate`/`browser_take_screenshot` tools instead of multi-line `Bash -c` Python strings; `agent-image-smoke.yml` verifies the binary + the resolved chromium path and takes a real headless screenshot of a live panel page from inside the ux-qa image.
## [0.22.0] - 2026-07-10
### Added
+11
View File
@@ -10,3 +10,14 @@ reports_to: fe-pm
```
You are the QA agent for the Frontend Cell.
## Browser verification (Playwright MCP)
This image ships a `playwright` MCP server (`mcp__playwright__*`) pointed at the `chromium-headless-shell` baked into `docker/agent-qa-fe.Dockerfile` — no install step needed, no hand-scripted Python. Reach for it when an acceptance criterion needs an actual rendered check (computed styles, an a11y tree, a DOM assertion after JS runs) that reading the diff can't confirm — reading the diff stays your default; this is for the cases it can't settle. Drive the browser through the structured tools:
mcp__playwright__browser_navigate(url="http://localhost:3000/some-route")
mcp__playwright__browser_snapshot() # a11y tree of the current page
mcp__playwright__browser_evaluate(function="() => window.getComputedStyle(document.querySelector('.sidebar')).backgroundColor")
mcp__playwright__browser_close()
Only `chromium-headless-shell` is available (no firefox/webkit). This is a manual verification aid, not a substitute for `evidence(task_id)`; note what you checked (`note(scope='learning', ...)`) same as any other review evidence. Full tool reference and more examples: `docs/backend/qa/browser-verification.md`.
+11
View File
@@ -10,3 +10,14 @@ reports_to: ux-pm
```
You are the QA agent for the UX/UI Cell.
## Browser verification (Playwright MCP)
This image ships a `playwright` MCP server (`mcp__playwright__*`) pointed at the `chromium-headless-shell` baked into `docker/agent-ux.Dockerfile` — the same image ux-dev runs, per orchestrator's IMAGE_MAP, but the MCP registration itself is role-gated to `ux-qa` only, not image-gated. No install step needed, no hand-scripted Python. Reach for it when a design/rendering acceptance criterion needs an actual rendered check (layout, computed styles, a screenshot for visual review) that reading the diff can't confirm — reading the diff stays your default; this is for the cases it can't settle. Drive the browser through the structured tools:
mcp__playwright__browser_navigate(url="http://localhost:3000/some-route")
mcp__playwright__browser_resize(width=1280, height=800)
mcp__playwright__browser_take_screenshot(filename="review.png")
mcp__playwright__browser_close()
Only `chromium-headless-shell` is available (no firefox/webkit). This is a manual verification aid, not a substitute for `evidence(task_id)`; note what you checked (`note(scope='learning', ...)`) same as any other review evidence. Full tool reference and more examples: `docs/backend/qa/browser-verification.md`.
+22
View File
@@ -7,6 +7,28 @@ USER root
RUN npm install -g pnpm
# Playwright + chromium-headless-shell (not full chromium) for FE QA's
# browser-based rendering/a11y checks — scoped to this image so agent-dev-fe
# stays lean. Browsers land in /app/.playwright (not /app/.venv) so the
# closing chown only touches this new ~small dir, never the pre-owned .venv
# tree (a chown -R over .venv would COW-duplicate the whole thing into this
# layer — see the 0.19.0 lean-images CHANGELOG note on agent-grok's chown -R).
ENV PLAYWRIGHT_BROWSERS_PATH=/app/.playwright
RUN uv pip install --python /app/.venv/bin/python playwright \
&& /app/.venv/bin/playwright install --with-deps chromium-headless-shell \
&& chown -R agent:agent /app/.playwright
# Playwright MCP server — structured browser tools (navigate/click/snapshot/
# screenshot) for QA's browser verification, registered by the orchestrator
# for the fe-qa/ux-qa roles only (see roboco/runtime/orchestrator.py
# _generate_mcp_config). Pinned version; the wrapper entrypoint below points
# it at this image's baked chromium-headless-shell instead of letting it
# download its own bundled browser.
RUN npm install -g @playwright/mcp@0.0.78 \
&& npm cache clean --force
COPY docker/scripts/playwright-mcp-entrypoint.sh /app/scripts/playwright-mcp-entrypoint.sh
RUN chmod 0755 /app/scripts/playwright-mcp-entrypoint.sh
USER agent
LABEL role="frontend-qa"
+28
View File
@@ -8,5 +8,33 @@ FROM roboco-agent-base
# - Image generation tools
# - Design token management
USER root
# Playwright + chromium-headless-shell for UX-QA's browser-based design/
# rendering verification. No dedicated ux-qa image exists — ux-qa runs this
# same image as ux-dev (see orchestrator.py IMAGE_MAP), so ux-dev picks this
# up too. Browsers land in /app/.playwright (not /app/.venv) so the closing
# chown only touches this new dir, never the pre-owned .venv tree (a
# chown -R over .venv would COW-duplicate the whole thing into this layer —
# see the 0.19.0 lean-images CHANGELOG note on agent-grok's chown -R).
ENV PLAYWRIGHT_BROWSERS_PATH=/app/.playwright
RUN uv pip install --python /app/.venv/bin/python playwright \
&& /app/.venv/bin/playwright install --with-deps chromium-headless-shell \
&& chown -R agent:agent /app/.playwright
# Playwright MCP server — structured browser tools (navigate/click/snapshot/
# screenshot) for QA's browser verification, registered by the orchestrator
# for the fe-qa/ux-qa roles only (see roboco/runtime/orchestrator.py
# _generate_mcp_config). Pinned version; the wrapper entrypoint below points
# it at this image's baked chromium-headless-shell instead of letting it
# download its own bundled browser. ux-dev shares this image but never gets
# the MCP registration (role-gated, not image-gated).
RUN npm install -g @playwright/mcp@0.0.78 \
&& npm cache clean --force
COPY docker/scripts/playwright-mcp-entrypoint.sh /app/scripts/playwright-mcp-entrypoint.sh
RUN chmod 0755 /app/scripts/playwright-mcp-entrypoint.sh
USER agent
LABEL role="ux-designer"
LABEL description="UX/UI agent - design, prototyping, design system"
@@ -0,0 +1,20 @@
#!/bin/bash
# Playwright MCP entrypoint — resolves the chromium-headless-shell binary
# already baked into this image at build time (docker/agent-qa-fe.Dockerfile
# / docker/agent-ux.Dockerfile, `playwright install --with-deps
# chromium-headless-shell`) and execs the Playwright MCP server against it.
#
# `@playwright/mcp` bundles its own `playwright-core` and would otherwise
# download a second copy of Chromium on first use; pointing it at the
# already-installed browser via `--executable-path` keeps this image at one
# browser, not two (see the 0.19.0 lean-images CHANGELOG note this Dockerfile
# is scoped against).
set -euo pipefail
CHROMIUM_PATH="$(/app/.venv/bin/python -c '
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
print(p.chromium.executable_path)
')"
exec playwright-mcp --executable-path "$CHROMIUM_PATH" --headless --isolated
+140
View File
@@ -0,0 +1,140 @@
# Browser Verification with Playwright
**For:** Frontend QA (`fe-qa`) and UX QA (`ux-qa`) agents
**Purpose:** Verify rendered output, computed styles, a11y trees, and visual design when code review and diff analysis cannot settle an acceptance criterion
## When to use browser verification
Browser verification is a **verification aid** — use it when reading the diff cannot confirm an acceptance criterion and you need rendered output or runtime behavior.
**Read the diff first.** This is your default verification method. Browser verification supplements it in cases like:
- **Rendered output:** Layout, typography, color application, visual hierarchy (after CSS changes)
- **Computed styles:** Verifying actual applied styles vs. declared styles (cascading, overrides, media queries)
- **Accessibility:** a11y tree structure, ARIA attributes, semantic correctness, contrast (via accessibility snapshot)
- **Visual design:** Spatial relationships, viewport-specific behavior, responsive breakpoints (UX QA focus)
- **Dynamic behavior:** DOM updates after JavaScript runs, interactive states (hover, focus, disabled)
**Examples of what you CAN'T use this for:**
- Rendering interactions that require user input (animations triggered by user gestures)
- Cross-browser compatibility (only chromium-headless-shell is available)
- Performance metrics (synthetic browser is not production-representative)
- Network behavior (no network mocking setup provided)
- Audio/video playback
## Setup and examples
### What's installed
The QA image (`agent-qa-fe` for Frontend, `agent-ux` for UX) ships:
- **Python 3.13** via `/app/.venv`, with the **Playwright** library (sync API) installed in the venv
- **chromium-headless-shell** browser only (no firefox or webkit), plus system deps for headless rendering (via `playwright install --with-deps chromium-headless-shell`)
- A **`playwright` MCP server** (`@playwright/mcp`, `mcp__playwright__*` tools) registered for the `fe-qa`/`ux-qa` roles only, wired to run against that same baked chromium-headless-shell via a wrapper entrypoint (`docker/scripts/playwright-mcp-entrypoint.sh`) — see `roboco/runtime/orchestrator.py`'s `_generate_mcp_config`
Drive the browser through the structured `mcp__playwright__*` tools — hand-scripting the Python sync API against a live agent is fragile (multi-line `Bash -c` strings, no structured error surface); the MCP tools give you `browser_navigate`, `browser_snapshot`, `browser_evaluate`, `browser_take_screenshot`, and `browser_close` directly as first-class tool calls.
### Basic flow
mcp__playwright__browser_navigate(url="http://localhost:3000/some-route")
# ... inspect via browser_snapshot / browser_evaluate / browser_take_screenshot ...
mcp__playwright__browser_close()
Replace `http://localhost:3000/some-route` with the actual route under test.
### Frontend QA examples
**Check computed styles (after a CSS change):**
mcp__playwright__browser_navigate(url="http://localhost:3000/dashboard")
mcp__playwright__browser_evaluate(
function="() => window.getComputedStyle(document.querySelector('.sidebar')).backgroundColor"
)
# Assert the returned value isn't transparent ("rgba(0, 0, 0, 0)")
mcp__playwright__browser_close()
**Verify accessibility tree (after a semantic HTML change):**
mcp__playwright__browser_navigate(url="http://localhost:3000/form-page")
mcp__playwright__browser_snapshot()
# Confirm the tree includes a "Submit" button node
mcp__playwright__browser_close()
### UX QA examples
**Check layout in a specific viewport (responsive design verification):**
mcp__playwright__browser_navigate(url="http://localhost:3000/home")
mcp__playwright__browser_resize(width=1280, height=800)
mcp__playwright__browser_take_screenshot(filename="review-desktop.png")
mcp__playwright__browser_close()
**Verify CSS grid or flex layout after a design change:**
mcp__playwright__browser_navigate(url="http://localhost:3000/kanban")
mcp__playwright__browser_resize(width=1280, height=800)
mcp__playwright__browser_snapshot(boxes=True) # bounding boxes per element, to compare card x-positions
mcp__playwright__browser_close()
**Take a screenshot for visual review:**
mcp__playwright__browser_navigate(url="http://localhost:3000/new-design")
mcp__playwright__browser_resize(width=1280, height=800)
mcp__playwright__browser_take_screenshot(filename="review.png")
mcp__playwright__browser_close()
## Important notes
### Limitations
- **Only chromium-headless-shell** is available — the MCP server's `--executable-path` (set by the wrapper entrypoint) points at this image's baked browser only, no firefox/webkit
- Runs headless (`--headless`) with an in-memory profile (`--isolated`) — no visual browser window, no persisted state between sessions
- Screenshots/snapshots saved via `filename` land in the MCP server's output directory inside the container, not on your host
- JavaScript is enabled; async DOM updates may need `browser_wait_for` before snapshotting
### Documentation
- **Playwright MCP tool reference:** run `npx @playwright/mcp@latest --help` for CLI flags, or see the [`@playwright/mcp` README](https://github.com/microsoft/playwright-mcp) for the full tool list
- **Common tools:**
- `browser_navigate(url)` — Navigate to a URL
- `browser_snapshot()` — Accessibility-tree snapshot of the current page (preferred over a screenshot for verifying structure)
- `browser_evaluate(function)` — Run JavaScript in the page context
- `browser_take_screenshot(filename)` — Take a screenshot
- `browser_resize(width, height)` — Set the viewport size
- `browser_wait_for(text | textGone | time)` — Wait for content or a fixed delay
- `browser_close()` — Close the browser
### Journaling your verification
Always record browser-based verification the same way as any other evidence:
```
note(scope='learning', text='Verified sidebar computed background color matches design token via headless chromium after CSS refactor')
```
This documents what you checked and how for future reference.
## Troubleshooting
**MCP tool call errors immediately**
- Confirm you're on `fe-qa` or `ux-qa` — the `playwright` MCP server is role-gated and won't appear in any other role's tool set (see `roboco/runtime/orchestrator.py` `_generate_mcp_config`).
**"Timeout waiting for target..." / navigation hangs**
- The app may not be running on `localhost:3000`. Check that the dev server is up.
- Use `browser_wait_for(text=...)` after `browser_navigate` if the target renders asynchronously.
**"Browser exited unexpectedly"**
- Insufficient memory or display resources in the container. Chromium headless is lightweight but needs some system memory.
**No screenshot/snapshot output appears**
- Files saved via `filename` are local to the QA agent's environment and cannot be downloaded directly — use `browser_evaluate` or `browser_snapshot`'s inline (non-file) response instead when you need the content back in the conversation.
## Why the Playwright MCP registration is role-gated, not orchestrator-Bash-gated
Earlier versions of this doc argued that any orchestrator-level Playwright "allowance" would be a no-op, because the original design ran Playwright by hand-scripting its Python sync API through an unrestricted `Bash` call. That's no longer the whole picture: the `playwright` MCP server itself **is** an orchestrator-level gate — `_generate_mcp_config` registers it only when `get_agent_role(agent_id) == "qa"` and `get_agent_team(agent_id)` is `frontend` or `ux_ui`, so `be-qa` (backend QA, same role, different team) and `ux-dev` (same image as `ux-qa`, different role) never see `mcp__playwright__*` in their tool set even though the npm package and the baked browser exist in their image or a sibling image. The underlying Bash+Python path from the original no-op analysis still exists and is still unrestricted for any role that isn't Write/Edit-denied — this doc just no longer recommends it, since the structured MCP tools are strictly more reliable for an agent to drive.
## Related documentation
- **Identity prompts:** `agents/prompts/identities/fe-qa.md` and `ux-qa.md` contain the built-in browser verification guidance
- **CI verification:** `.github/workflows/agent-image-smoke.yml` runs a real headless smoke test to verify the Playwright + chromium installation, the `playwright-mcp` binary, and a real panel-page screenshot on each image build
- **Diff review:** Always start with code review; browser verification is a supplement, not a replacement
+55 -17
View File
@@ -3414,6 +3414,7 @@ class AgentOrchestrator:
- roboco-git-readonly status, log, diff, branch list
- roboco-optimal knowledge base, RAG, semantic search
- roboco-docs documentation file management (panel docs)
- playwright browser tools (fe-qa/ux-qa only see below)
The agent's role is asserted by the orchestrator API on every
verb/tool call, so all roles get the same MCP surface from this
@@ -3512,6 +3513,45 @@ class AgentOrchestrator:
},
}
self._append_role_scoped_mcp_servers(
mcp_servers, agent_id, agent_role, agent_uuid, mcp_env
)
config: dict[str, Any] = {"mcpServers": mcp_servers}
# Write to shared config directory (mounted in both orchestrator and agents)
# When running in container: /app/mcp-configs -> host's ./data/mcp-configs
# When running on host: use temp directory
if DATA_HOST_PATH:
# Running in container - use shared mounted directory
config_dir = Path("/app/mcp-configs")
config_dir.mkdir(parents=True, exist_ok=True)
else:
# Running on host - use temp directory
config_dir = Path(tempfile.gettempdir())
# basename-sanitized like _grok_usage_json: agent ids are orchestrator-
# issued slugs, but the filename must not be able to traverse anyway.
safe_agent_id = os.path.basename(agent_id)
config_path = config_dir / f"roboco-mcp-{safe_agent_id}.json"
config_path.write_text(json.dumps(config, indent=2))
return config_path
def _append_role_scoped_mcp_servers(
self,
mcp_servers: dict[str, dict[str, Any]],
agent_id: str,
agent_role: str,
agent_uuid: str,
mcp_env: dict[str, str],
) -> None:
"""Register the role-scoped MCP servers (docs, research, playwright).
Split from ``_generate_mcp_config`` so the base registration stays
within the complexity budget; each branch is fail-closed server-side
regardless of registration.
"""
# Docs server — documentation file management. Registered only for
# roles that touch panel docs; handlers still enforce per-role
# access so the surface is fail-closed.
@@ -3559,23 +3599,21 @@ class AgentOrchestrator:
"env": mcp_env,
}
config: dict[str, Any] = {"mcpServers": mcp_servers}
# Write to shared config directory (mounted in both orchestrator and agents)
# When running in container: /app/mcp-configs -> host's ./data/mcp-configs
# When running on host: use temp directory
if DATA_HOST_PATH:
# Running in container - use shared mounted directory
config_dir = Path("/app/mcp-configs")
config_dir.mkdir(parents=True, exist_ok=True)
else:
# Running on host - use temp directory
config_dir = Path(tempfile.gettempdir())
config_path = config_dir / f"roboco-mcp-{agent_id}.json"
config_path.write_text(json.dumps(config, indent=2))
return config_path
# Playwright MCP — structured browser tools (navigate/click/snapshot/
# screenshot) for QA's browser verification, replacing hand-scripted
# Bash+Python. Scoped to fe-qa/ux-qa only (role-gated, not
# image-gated: ux-dev shares agent-ux's image but never gets this),
# per the CEO's round-3 note on the Playwright QA-image work. The
# binary + wrapper entrypoint are baked into agent-qa-fe/agent-ux
# only (docker/agent-qa-fe.Dockerfile, docker/agent-ux.Dockerfile),
# so registering it for any other role would reference a command
# that doesn't exist in that role's image.
playwright_mcp_teams = ("frontend", "ux_ui")
if agent_role == "qa" and get_agent_team(agent_id) in playwright_mcp_teams:
mcp_servers["playwright"] = {
"command": "/app/scripts/playwright-mcp-entrypoint.sh",
"args": [],
}
def _generate_composed_prompt(
self, agent_id: str, ambient: str | None = None
@@ -0,0 +1,50 @@
"""The `playwright` MCP server (CEO round-3 note on the Playwright QA-image
work) is role-gated to fe-qa/ux-qa only it must never appear for be-qa
(same role, different team) or ux-dev (same image as ux-qa, different role),
since the binary + wrapper entrypoint are only baked into agent-qa-fe /
agent-ux via docker/agent-qa-fe.Dockerfile / docker/agent-ux.Dockerfile.
"""
from __future__ import annotations
import json
from pathlib import Path
from roboco.runtime.orchestrator import AgentOrchestrator
_ENTRYPOINT = "/app/scripts/playwright-mcp-entrypoint.sh"
async def _servers_for(agent_slug: str) -> dict[str, dict]:
orch = AgentOrchestrator.__new__(AgentOrchestrator)
config_path = await orch._generate_mcp_config(agent_slug)
config = json.loads(Path(config_path).read_text())
servers: dict[str, dict] = config["mcpServers"]
return servers
async def test_fe_qa_gets_playwright_mcp() -> None:
servers = await _servers_for("fe-qa")
assert "playwright" in servers
assert servers["playwright"]["command"] == _ENTRYPOINT
async def test_ux_qa_gets_playwright_mcp() -> None:
servers = await _servers_for("ux-qa")
assert "playwright" in servers
assert servers["playwright"]["command"] == _ENTRYPOINT
async def test_be_qa_does_not_get_playwright_mcp() -> None:
"""Same `qa` role as fe-qa/ux-qa, but backend team — no chromium baked
into be-qa's image, so it must not get the MCP registration."""
servers = await _servers_for("be-qa")
assert "playwright" not in servers
async def test_ux_dev_does_not_get_playwright_mcp() -> None:
"""Shares agent-ux's image with ux-qa (same Dockerfile, same baked
browser) but is a `developer`, not `qa` the gating is role-based, not
image-based, so ux-dev must not see the tool."""
servers = await _servers_for("ux-dev-1")
assert "playwright" not in servers