This repository has been archived on 2026-09-17. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
nixos-config/hosts/blunix/localServices/board-agent/screenshot.py
pijukka 9cb6111fed board: give the Claude Reviewer real visual verification for HTML/CSS diffs
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
2026-09-10 19:01:05 +02:00

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())