Files
agent-skills/skills/sandbox-browser-automation/SKILL.md
T
Malin ebc71fdcd0 docs: add sandbox-browser-automation skill
Documents what's actually installed for browser automation in this sandbox
(Playwright via Python, no global Node package, browsers pre-cached) and a
DNS-routing quirk on *.palmasolutions.net domains, plus the circular
overflow:hidden badge-clipping bug class found while debugging sateulera-dev.
2026-08-20 21:43:56 +02:00

5.0 KiB

name, description
name description
sandbox-browser-automation Use when you need to actually render a page (screenshot, computed styles, DOM inspection) instead of guessing from curl/static file inspection — e.g. "the CSS fix is deployed but the user still says it's not showing." Documents what's really installed in this sandbox and a live DNS-routing quirk that affects `*.palmasolutions.net` fleet domains specifically.

Sandbox Browser Automation

Static HTTP/CSS inspection cannot prove what actually renders — clipping, cascade wins, and z-index/overflow interactions only show up in a real browser. This sandbox has Playwright fully installed; use it before concluding "the fix is deployed, must be a caching issue on the user's end."

What's actually installed (verified 2026-08-20)

  • Playwright 1.62.1, browsers pre-downloaded at ~/.cache/ms-playwright/ (chromium + ffmpeg). No re-download needed.
  • Python path (preferred — no setup required): the playwright package is globally importable — python3 -c "from playwright.sync_api import sync_playwright" works immediately, no venv/install step. /usr/local/bin/playwright is this same package's CLI (playwright codegen, etc.).
  • Node path: no global npm playwright package — a script needs its own local node_modules/playwright to require() it. Check /tmp/ first for a scratch dir a prior session may have already set up (e.g. past names seen: sateulera-render, mobisol-render, st-pw-package) before running a fresh npm install playwright — reusing one skips the install entirely since the browser binaries are already cached globally. /tmp is shared across concurrent sandboxes/sessions and not durable — don't assume a given scratch dir still exists, always ls first, and don't rely on one surviving into a future session.
  • No system chromium/google-chrome binary on PATH — always go through Playwright's own managed browser, not a system browser launch.

Default to the Python one-shot form when you just need a screenshot or a computed-style check — it has zero setup cost. Reach for a Node script only when reusing an existing scratch project's node_modules is actually faster, or the task needs a JS-heavy interaction sequence.

from playwright.sync_api import sync_playwright
with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_context(viewport={"width": 375, "height": 812},
                                ignore_https_errors=True).new_page()
    page.goto("https://example.com/", wait_until="domcontentloaded", timeout=45000)
    page.screenshot(path="/root/.claude/jobs/<job>/tmp/out.png")
    browser.close()

Use wait_until="domcontentloaded" by default, not "networkidle" — sites with long-polling/analytics connections can hang networkidle well past a reasonable timeout.

*.palmasolutions.net (and possibly other fleet domains) resolve differently inside this sandbox

Confirmed 2026-08-20 on sateulera-dev.palmasolutions.net: dig from this box returns the real public path (madri's Caddy IP, 195.20.235.141), but the box's own system resolver — what curl, Node, and Playwright actually use via getaddrinfo — resolves the same name directly to the backend's Tailscale IP (funky, 100.127.21.100), bypassing Caddy entirely. This is likely a split-DNS/hosts entry specific to this sandbox's network setup, not present for the end user's real browser.

In this case the content served both ways turned out to be byte-identical (verified via md5sum), so it wasn't the cause of a "still broken" report — but don't assume that holds for every fleet domain or every time. If a fix verifies correctly in Playwright from this box but the user still reports it broken, explicitly re-check via the real public path before ruling out a routing/caching difference:

curl -sk --resolve <domain>:443:<public-ip-from-dig> "https://<domain>/path" | md5sum
curl -sk "https://<domain>/path" | md5sum   # this box's default (possibly-shortcut) path

If the hashes differ, you've found a real routing/cache split, not a false alarm — investigate the reverse proxy (Caddy on madri, or nginx on proxie) rather than the origin.

Real bug class this catches that static inspection misses

A badge/element positioned with a negative offset (top:0;left:-2px, etc.) to overlap the corner of a circular overflow:hidden + border-radius:50% container gets clipped by the circle's curve even while the badge's own rectangular bounding box is still inside the container's rectangular bounds. border-radius and padding fixes on the badge itself cannot fix this — computed-style inspection via getComputedStyle will even report the badge's own box as unclipped (overflow: visible on the badge). Only a rendered screenshot (or checking the parent's computed overflow) reveals it. Fix is to drop overflow:hidden from the circular parent (the inner icon element usually already carries its own border-radius:50% and doesn't need the outer clip).