fix: self-heal dangling storageState in playwright config at load time (#2491) - #2522
Conversation
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsI've reviewed the diff thoroughly, including the lock helper, the repair logic, sensitive-path handling, all callers of Key checks:
No semantic defect on changed lines completes a consequence chain to user- or system-visible harm. No findings. [OPUS-REVIEWED] ccc0fe4 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
Design Review (Fable 5) — ✅ PASSAdvisory design-level review of Design review complete. I examined the full diff (the repair helper, the new interprocess lock, the proxy-startup hook, and both test suites) against the stated regression. Design-Verdict: PASS Load-time self-heal at the single spawn chokepoint is the right layer; repair converges with generation semantics, and every hardening layer traces to a named failure. [DESIGN-REVIEWED] ccc0fe4 |
GPT 5.6 Review — ✅ human override acceptedHuman judgment by @bolichen97 overrides the GPT 5.6 finding for This comment is updated in place on each push. The model was not re-run because an authorized human decision supersedes it. False positive or not applicable? A repository writer can comment: |
|
Re: GPT 5.6 BLOCKING — lost concurrent updates (setup.py read-modify-write) Finding accepted, but fixed forward rather than reverting the persistent rewrite: dropping the rewrite would reopen the exact trap this PR closes (the running playwright-mcp process caches the config, so an in-memory-only repair leaves the on-disk ENOENT bomb armed for every restart — spec item 2 of the issue). Fix applied in 7e54638: a shared interprocess advisory lock (
The repair can no longer install a stale snapshot over a concurrent generation — the Settings-save scenario in the finding now blocks on the lock and whichever side runs second sees the other's completed write (a fresh generation carries no dangling key, so the repair no-ops). Two new tests pin the lock: write-inside-locked-region ordering for the repair, and lock acquisition by the generator. |
86fcbdf to
7e54638
Compare
7e54638 to
64c9d77
Compare
|
Re: GPT 5.6 round-2 BLOCKING x2 (symlink interactions) — both fixed in 64c9d77
Local gates re-ran green (450 tests across the three touched/adjacent files, isort/flake8/mypy clean). |
64c9d77 to
8410e1c
Compare
|
Re: GPT 5.6 round-3 BLOCKING (sensitive-path guard) — fixed in 8410e1c Accepted as defense-in-depth. Pinned by |
8410e1c to
ac127ed
Compare
|
Re: GPT 5.6 round-4 BLOCKING (unresolvable config crashes the proxy) — fixed in ac127ed Accepted: Note on Backend Tests (3.10, 2): the shard failed on two DIFFERENT tests across two runs ( |
|
Re: GPT 5.6 round-6 — decode-error accepted (fixed in 62ab731); lock-removal rebutted
If the round-6 lock-removal position is maintained on the next pass, I'll treat it as adjudicated and use the override mechanism with this comment as the reference. |
62ab731 to
4d7d06a
Compare
|
Re: GPT 5.6 round-7 — both items addressed in 4d7d06a
Gates green: 455 tests across touched + adjacent handler files, isort/flake8 clean, 0 mypy errors in touched files. |
#2491) A playwright-config.json written before the #2209 generation-time fix — or whose storage-state file was later removed — kept a dangling contextOptions.storageState forever. Playwright raises ENOENT at context creation for it, breaking every browser_* call, and the running playwright-mcp process caches the config, so regeneration alone could not recover a live session. repair_playwright_config() (browser/setup.py) now runs at the config's load moment — proxy startup in run_proxy() — drops the dangling key (converging on the same degrade-to-unauthenticated-context behaviour a fresh generation produces), rewrites the file atomically so the repair survives restarts, and logs one WARNING naming the missing file. Valid, keyless, absent, or unparseable configs are left byte-for-byte untouched. Closes #2491
4d7d06a to
ccc0fe4
Compare
|
Re: GPT 5.6 round-8 BLOCKING (Windows follows planted sidecar symlink) — fixed in ccc0fe4 Accepted: Windows has no |
|
Re: GPT 5.6 round-9 BLOCKING (recheck/replace not atomic) — rebutted + overridden as re-litigation Adjudication history on this exact residual:
Why the demand is not implementable: no portable OS primitive provides atomic compare-and-swap on file CONTENTS (POSIX Reverting persistence instead would reopen #2491's core defect for every user (the running playwright-mcp process caches the config; an in-memory repair dies with the process). Override follows per the repo's adjudication mechanism. |
|
/ai-review override gpt ccc0fe4: Round-9 revert demand re-litigates rounds 1/5/6 — the lock + pre-write recheck it prescribed are implemented, atomic content-conditional replacement does not exist portably, and reverting persistence would reopen #2491's cached-config trap (see adjudication-history comment above). |
Human judgment recorded@bolichen97 marked the gpt AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
kirodotdev#2491) (kirodotdev#2522) A playwright-config.json written before the kirodotdev#2209 generation-time fix — or whose storage-state file was later removed — kept a dangling contextOptions.storageState forever. Playwright raises ENOENT at context creation for it, breaking every browser_* call, and the running playwright-mcp process caches the config, so regeneration alone could not recover a live session. repair_playwright_config() (browser/setup.py) now runs at the config's load moment — proxy startup in run_proxy() — drops the dangling key (converging on the same degrade-to-unauthenticated-context behaviour a fresh generation produces), rewrites the file atomically so the repair survives restarts, and logs one WARNING naming the missing file. Valid, keyless, absent, or unparseable configs are left byte-for-byte untouched. Closes kirodotdev#2491
Summary
Fixes the rc.8 regression where Browser Mode hard-breaks forever when
playwright-config.jsoncarries acontextOptions.storageStatepointing at a file that no longer exists (#2209 reproduced). #2209 fixed generation —generate_playwright_config()only attaches the key when the file exists — but nothing re-validated an already-written config, so one generated before that fix (or whose storage-state file was later removed) kept the dangling reference forever. Playwright raises ENOENT at context creation for it, breaking everybrowser_*call, and the running playwright-mcp process caches the config, so regeneration alone couldn't recover a live session.The fix:
repair_playwright_config()(browser/setup.py) runs at the config's load moment — proxy startup inrun_proxy(), the one place every config-mode launch passes through (both--config <path>and--config=<path>forms). When the referenced storage-state file is missing, it drops the key — converging on the exact behaviour a fresh generation produces, so Browser Mode degrades to an unauthenticated context instead of dying — rewrites the config atomically so the repair survives restarts, and logs one WARNING naming the missing file and the action taken.Deliberately NOT done (per the issue): no process-killing, no transport-restart — those were symptoms; once a stale config self-heals at load, the manual recovery sequence is unnecessary. No re-seeding via
refresh_storage_state()— it is a documented no-op without a cookie source (the OSS default); if a cookie source ever writes the file back, the untouched-when-present branch keeps the key on the next load.Hardening (pre-push review fleet findings, all applied)
path.resolve()before read/write) —os.replacewould otherwise swap the user's link for a regular file.storageStateis judged from the config file's directory (Playwright's vantage), not the proxy CWD — a working hand-edited relative reference is never dropped._playwright_config_lockedflock sidecar (pattern from the module's existing_kiro_mcp_locked) serializes BOTH writers ofplaywright-config.json—generate_playwright_config()and the repair's read-validate-write. The sidecar is derived from the resolved path (link and target converge on one lock), created withO_NOFOLLOW+ regular-file re-check so a pre-planted symlink at the sidecar name cannot forge a keystone flag. A re-read stand-down inside the locked region additionally yields to unlocked MANUAL edits (decode errors count as changed).Testing
TestRepairPlaywrightConfig(repair + persist + WARNING; present-file byte-identical; keyless untouched; idempotent across two loads; missing/unparseable/non-dict shapes untouched; relative-path both branches; 0600 preserved (POSIX); symlink write-through (POSIX); concurrent-writer stand-down; default-path resolution; generate→orphan→repair round-trip converges with fresh generation) andTestRunProxyConfigRepair(repaired before spawn;--config=form; no-config skip; trailing-flag safe + repair not invoked).origin/mainon the same host — identical failure sets (host forbids user namespaces →SandboxUnavailableError; zero net-new failures).isort/flake8clean;mypy: 0 errors in touched files.Review fleet
Pre-push model-pinned reviewers: GPT 5.6 Sol (3 findings — mode preservation, symlink clobber, lost-update race) and Opus 5 (0 blocking, 4 advisories — relative-path vantage,
--config=form, mode preservation, test strengthening). All applied; the interprocess-lock suggestion for the race was scoped down to the read-recheck stand-down documented above, since no lock exists for this file today and adding one across all generation paths exceeds this fix's blast radius.Closes #2491