2.0.8 batch: five Sentry-evidenced fixes (F-509, F-783, F-816, F-817, F-818) - #64
2.0.8 batch: five Sentry-evidenced fixes (F-509, F-783, F-816, F-817, F-818)#64AminDhouib wants to merge 6 commits into
Conversation
nodriver 0.47's Connection.remove_handler ends in a bare `del self.handlers[evt_dom]` that deletes the whole key, so two overlapping Tab.wait()s on one tab race and the loser crashes with KeyError(<class 'nodriver.cdp.page.FrameStoppedLoading'>). The race is transient by construction, so _resolve_with_recovery now treats a KeyError naming a nodriver.cdp event class as recoverable alongside the -32000 stale-node case (same retry bound, same backoff, one classifier). If it persists, the caller gets a readable ToolError naming the condition instead of a bare class repr leaking into error messages. Unrelated KeyErrors (string keys, non-nodriver classes) propagate untouched, pinned by two negative-control tests. Fixes STEALTH-CHROME-DEVTOOLS-MCP-2R Fixes STEALTH-CHROME-DEVTOOLS-MCP-2S Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
F-813 forgave command-NAME spelling; params still required nodriver's
snake_case, so the CDP docs' own spelling ('awaitPromise') crashed with
TypeError. build_cdp_call now maps each incoming key onto the resolved
method's real signature with the same underscore-stripped fold, so
'returnByValue' and 'return_by_value' are the same argument. A key that
matches nothing raises ToolError listing the command's valid params,
honored through the method's KEEP {"success": False} contract exactly
like the F-813 unknown-command path.
Fixes STEALTH-CHROME-DEVTOOLS-MCP-22
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nodriver's Browser.get() locates the new page target with a bare next(filter(...)); with no page target left (browser mid-teardown, last tab closed, targets degraded to raw Connections) the StopIteration becomes "RuntimeError: coroutine raised StopIteration" out of _replace_main_tab. The one exposed call site now converts exactly that shape (isinstance(e.__cause__, StopIteration)) into an actionable ToolError; unrelated RuntimeErrors propagate untouched. navigate's unknown-instance raise becomes InstanceNotFoundError and its timeout raise becomes ToolError (convention 2). Line-neutral at the file's exact 1532 cap; the four remaining bare raises are deliberate (spawn-failure operator-hint contract F-811, internal invariants). Fixes STEALTH-CHROME-DEVTOOLS-MCP-2K Fixes STEALTH-CHROME-DEVTOOLS-MCP-12 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
All 21 bare `raise Exception` sites in dom_handler.py (element lookups, argument guards, and their re-wraps) and _with_cdp_timeout's timeout raise become ToolError, message text byte-identical. The F-815 Sentry filter now drops the whole family as expected tool failures — which also dissolves the per-instance-UUID issue splitting the timeout messages caused. Cancellation still propagates CancelledError untouched, and spawn_browser's inner bare Exception is left deliberately so genuine spawn failures (the F-811 exhaustion class) keep shipping. query_elements now passes an already-ToolError from the resolution layer through unchanged instead of double-wrapping it, and the execute_cdp_command docstring stops claiming params must be snake_case (F-816 made both spellings resolve). server.py stays at its exact 3411 cap. Fixes STEALTH-CHROME-DEVTOOLS-MCP-2H Fixes STEALTH-CHROME-DEVTOOLS-MCP-Y Fixes STEALTH-CHROME-DEVTOOLS-MCP-2F Fixes STEALTH-CHROME-DEVTOOLS-MCP-2P Fixes STEALTH-CHROME-DEVTOOLS-MCP-2V Fixes STEALTH-CHROME-DEVTOOLS-MCP-2Q Fixes STEALTH-CHROME-DEVTOOLS-MCP-2M Fixes STEALTH-CHROME-DEVTOOLS-MCP-2N Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A Windows excluded/reserved port range (Hyper-V/WinNAT) forbids binding 19222 with WinError 10013 but has no listener, so the connect-based foreign-held probe read it as free, selection kept it, and uvicorn died in the child on every start — the backend never came up at all. bindable_port/_port_is_forbidden join _free_port in proxy_forwarder (the one port-primitive home): a bind-and-close probe that disqualifies a port ONLY on PermissionError. Occupied-but-permitted ports are deliberately NOT disqualified — a herd of cold-starting proxies probes concurrently, and treating any bind failure as disqualifying scattered 11 of 12 sessions onto private ports nothing would ever serve (herd test 240s timeout vs 3.9s). singleton passes its existing conflict verdict down as force_new; selection policy, restart_backend's contract, and the identity/adoption gates are unchanged. singleton.py stays at its exact 1000 cap. Fixes STEALTH-CHROME-DEVTOOLS-MCP-2J Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI arbitration — the reds are pre-existing environmental findings, not this PRTwo runs, byte-identical trees (
The macOS failures are F-806 with new, conclusive evidence. Run 1's junit: The version probed early in the run was Chrome 150; the CDP-launched browser was Chrome 151 — Chrome's updater upgraded the binary mid-run on the macOS runner. That is exactly F-806's documented mechanism (pre-launch version probe memoized → masked UA contradicts the live browser's own client hints), which is why both stealth gates ( This PR's diff has zero overlap with UA masking / Local evidence for this PR: hermetic lane 1636 passed ×3 (twice pre-push hook, once direct), budget gate exit 0, all LOC caps exact. |
Stacked on #63 (it builds on the F-815 Sentry filter there). Retarget to
mainafter #63 merges.Five fixes, each driven by a live Sentry stacktrace, each TDD'd RED-first against the exact production error shape. Full hermetic lane: 1636 passed (+42 new tests), budget gate green, all LOC caps exact.
The fixes
F-817 — nodriver handler-table race (
element_resolution.py) — Fixes STEALTH-CHROME-DEVTOOLS-MCP-2R, -2Snodriver 0.47's
remove_handlerends in a baredel self.handlers[evt_dom]that deletes the whole key; two overlappingTab.wait()s race and the loser crashes withKeyError: <class 'nodriver.cdp.page.FrameStoppedLoading'>. The existing recovery wrapper now retries it like the -32000 stale-node case (one classifier, no parallel path); if it persists, a readable ToolError replaces the class-repr leak. Unrelated KeyErrors propagate untouched (two negative-control pins).F-816 — CDP param-name folding (
cdp_function_executor.py, 1010/1012) — Fixes -22F-813 forgave command-name spelling; params still crashed on the CDP docs' own casing (
awaitPromise).build_cdp_callfolds incoming keys onto the resolved method's real signature; unknown params raise ToolError listing the valid names, through the method's KEEP contract.F-818 — target-less browser + typed instance-not-found (
browser_manager.py, exactly 1532/1532, line-neutral) — Fixes -2K, -12Browser.get()'s barenext(filter(...))turns intoRuntimeError: coroutine raised StopIterationwhen no page target remains; the one exposed call site converts exactly that shape into an actionable ToolError.navigate's unknown-instance raise becomesInstanceNotFoundError, its timeout ToolError. Census of all 7 bare raises included in the commit; 4 left deliberately (F-811 operator-hint contract, internal invariants).F-783 closed — error-convention sweep (
dom_handler.py,server.pyexactly 3411/3411) — Fixes -2H, -Y, -2F, -2P, -2V, -2Q, -2M, -2NAll 21 bare
raise Exceptionsites in dom_handler +_with_cdp_timeout's timeout raise become ToolError (messages byte-identical). The F-815 filter now drops the family — which also dissolves the per-instance-UUID splitting of CDP-timeout issues. Deliberately kept shipping: cancellation (stillCancelledError) and spawn failures (server.py:454stays bare so the F-811 exhaustion class remains visible in Sentry). Audit finding doc annotated closed.F-509 — reserved-port fallback (
singleton.pyexactly 1000/1000,proxy_forwarder.py) — Fixes -2JWindows excluded-port ranges forbid binding 19222 (WinError 10013) but have no listener, so the connect-based foreign-held probe read the port as free and uvicorn died in the child on every start — seen on an external user's machine. New bind-and-close probe disqualifies a port only on PermissionError: treating any bind failure as disqualifying scattered the cold-start herd onto private ports (herd test 240s timeout vs 3.9s — the A/B that found it is pinned in
TestOccupiedIsNotForbidden). Selection policy,restart_backend's contract, and identity/adoption gates unchanged.Known residuals (flagged, not fixed here)
remove_handler's unguardeddels remain — we work around, not vendor-patch.dom_handler.py:859ValueErrorfor invalid scroll direction still ships to Sentry (chain contains a non-ToolError).Integration lane runs on this PR's CI (E2E across 3 OSes), same as #63.