A live review report (web-ui-system-darkmode, commit 4b2047e) flagged
two uncertainties a text-only diff review genuinely can't close: no
actual rendering checked, and contrast only "visually plausibilized"
from hex values. Adds a fixed, narrowly-scoped screenshot.py: given a
commit hash and a repo-relative .html path, it extracts that exact file
via `git show <hash>:<path>` (never the live working tree, which may
have moved on by review time) and renders it headless in both light and
dark prefers-color-scheme, for the Reviewer to inspect via its own Read
tool.
Wired only into run_review()'s Claude branch -- Codex's --sandbox
read-only blocks every write, so Chromium has nowhere to put a
screenshot there. --allowedTools gets a second, dynamically-built entry
pinning the exact resolved script invocation, same fixed-command
precedent as the existing Bash(git *) grant; the script re-validates
its own arguments regardless.
Two things verified live before wiring anything up, not assumed:
--force-prefers-color-scheme has no effect in this Chromium build
(--blink-settings=preferredColorScheme=<0|1> does, confirmed by actual
pixel color); and --allowedTools does honor a fixed-script pattern the
same way it honors Bash(git *). New pkgs.chromium dependency, run with
--no-sandbox since NoNewPrivileges=true already forbids the privilege
escalation Chromium's own setuid sandbox needs -- containment comes
from the outer systemd sandbox instead, a deliberate trade-off noted in
both the intent doc and README.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016w25covboHfcYuLXBAvX3g
97 lines
4.2 KiB
Python
97 lines
4.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Fixed, narrowly-scoped headless-Chromium screenshot helper for the
|
|
board Reviewer's visual verification of an HTML/CSS change
|
|
(docs/intent/board-reviewer-visual-verification.md).
|
|
|
|
Takes only a commit hash and a repo-relative .html path -- extracts that
|
|
exact file's content via `git show <hash>:<path>` into a throwaway temp
|
|
file rather than reading the live working tree (which may have moved past
|
|
the reviewed commit by the time Review actually runs), then renders it
|
|
headless in both light and dark `prefers-color-scheme`.
|
|
|
|
Deliberately a single fixed script, not a raw chromium invocation the
|
|
model constructs itself: the Reviewer's --allowedTools grant pins this
|
|
exact script path (see worker.py's run_review()), so the model can only
|
|
ever run *this* script, with *some* two arguments -- both of which this
|
|
script re-validates on its own regardless of what the allowlist already
|
|
constrains, same "wrapper enforces, model proposes" precedent as every
|
|
other board-agent role.
|
|
|
|
Only ever wired up for the Claude runtime: Codex's review sandbox
|
|
(`--sandbox read-only`) blocks every write, verified live and documented
|
|
in this directory's README ("Why read-only, not a scoped writable dir") --
|
|
Chromium has nowhere to write a screenshot to under that sandbox, so this
|
|
tool is simply not offered to a Codex/OpenRouter review at all.
|
|
"""
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
COMMIT_RE = re.compile(r"^[0-9a-f]{7,40}$")
|
|
|
|
# Empirically verified 2026-09-10 against the deployed Chromium build:
|
|
# --force-prefers-color-scheme has no effect on a file:// page in headless
|
|
# mode here (identical output regardless of value); --blink-settings=
|
|
# preferredColorScheme=<0|1> does. 0 renders dark, 1 renders light --
|
|
# verified by the actual rendered pixel color, not assumed from the
|
|
# flag's nominal enum ordering.
|
|
COLOR_SCHEMES = {"dark": 0, "light": 1}
|
|
TIMEOUT_SECONDS = 30
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) != 3:
|
|
print("usage: screenshot.py <commit-hash> <repo-relative .html path>", file=sys.stderr)
|
|
return 2
|
|
commit_hash, relative = sys.argv[1], sys.argv[2]
|
|
if not COMMIT_RE.fullmatch(commit_hash):
|
|
print("commit hash must be a plain hex sha", file=sys.stderr)
|
|
return 2
|
|
if not relative.endswith(".html") or relative.startswith("/") or ".." in Path(relative).parts:
|
|
print("only a repo-relative .html path is allowed", file=sys.stderr)
|
|
return 2
|
|
|
|
repo_dir = os.environ["BOARD_REPO_DIR"]
|
|
show = subprocess.run(
|
|
["git", "-C", repo_dir, "show", f"{commit_hash}:{relative}"],
|
|
capture_output=True, text=True, timeout=TIMEOUT_SECONDS, check=False,
|
|
)
|
|
if show.returncode != 0 or not show.stdout.strip():
|
|
print(f"git show {commit_hash}:{relative} failed: {show.stderr.strip()}", file=sys.stderr)
|
|
return 1
|
|
|
|
out_dir = Path(tempfile.mkdtemp(prefix="board-review-screenshot-"))
|
|
html_path = out_dir / "page.html"
|
|
html_path.write_text(show.stdout, encoding="utf-8")
|
|
|
|
chromium = os.environ["BOARD_CHROMIUM_BIN"]
|
|
for scheme, value in COLOR_SCHEMES.items():
|
|
out_path = out_dir / f"{scheme}.png"
|
|
result = subprocess.run(
|
|
[
|
|
chromium, "--headless=new", "--disable-gpu",
|
|
# Chromium's own setuid sandbox needs privilege escalation
|
|
# this service's NoNewPrivileges=true already forbids, so
|
|
# it would fail to start without this -- containment comes
|
|
# entirely from the outer systemd sandbox instead (see the
|
|
# nix module comment next to BOARD_CHROMIUM_BIN).
|
|
"--no-sandbox",
|
|
f"--blink-settings=preferredColorScheme={value}",
|
|
"--window-size=1280,900",
|
|
f"--screenshot={out_path}",
|
|
f"file://{html_path}",
|
|
],
|
|
capture_output=True, text=True, timeout=TIMEOUT_SECONDS, check=False,
|
|
)
|
|
if result.returncode != 0 or not out_path.exists():
|
|
print(f"screenshot failed for {scheme}: {result.stderr.strip()[-500:]}", file=sys.stderr)
|
|
return 1
|
|
print(out_path)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|