Repository navigation
Heal client: vision, advise, follow-ups, one selector refusal point, session origin guard - #25
philmcneely wants to merge 32 commits into
Conversation
With HEAL_VISION off the page is never asked for a screenshot and the request carries null. With auto or on LivePage takes a single viewport PNG with password inputs (frames included) and the HEAL_MASK selectors blacked out, animations frozen and the caret hidden, and sends it in the service's wire form with the viewport scroll. A capture that cannot be masked sends nothing. Tests prove zero image bytes leave with off and that the mask is painted in a real Chromium.
The CLI no longer claims no screenshot is sent: auto and on announce the masked viewport screenshot, an unknown value says it runs with vision off.
…vice in advise mode conftest no longer calls utils/ai_healing.py: with HEAL_ADVISE=on and HEAL_SERVICE_URL a test that fails for good is posted to the heal service while its page is open (probe inventory, redacted aria snapshot, stack files, last green fingerprint). The decision goes into the healing report folder, heal-summary.json and the observability report. No edit is made and no model URL, name or prompt remains on the path. utils/ai_healing.py is kept and marked deprecated.
…t what became of them on the next run
scripts/heal.py --open-pr records {heal_id, file, from, to, test, pr_url} in .heal/heals.json. On the next run each recorded
heal is classified from gh and the file on disk (open, closed unmerged, merged unchanged, edited, reverted), with whether its
test failed again and runs and days since, and posted to the service's followup route. It skips quietly when gh or the
service is unavailable and never fails the run.
…n client against a fake service The fixtures are a copy of the service's contract cases. The fake service answers from expected.json and the client must check the recorded locator live, report the scripted result, write the recorded locator into the file only on healed (and leave it alone otherwise) and end with the same decision and reason. A check that the fixtures hold no internal host or address is included.
… a pick by mark Real Chromium, the real client and the service app in process with stub models: auto takes one screenshot, the service picks by mark, and with vision off no image is sent. Skipped when the installed heal-service has no vision support.
The failure text sent to the service is cut to what its triage reads (no assertion values, locals, element HTML or code-frame source lines). PR addresses must match in full and name github.com (or HEAL_GH_HOST), and one bad record never stops the rest; a follow-up names the heal's repo and run. Service text in the advice report sits in code spans. The client caps rounds, reports coarse counts, refuses attribute-operator and value selectors, accepts only a same-origin path as the route, keeps its origin guard on from the first navigation with service workers blocked, cuts link queries, the chosen option and contenteditable text from the aria snapshot, refuses a lone surrogate, and sends Content-Type: application/json on every POST.
Default masks now cover card and one-time-code autocomplete fields, current/new-password fields, fields named pass, pwd, token, secret, ssn, card, cvv or otp (name or id), and every cross-origin or sandboxed frame whole, in the page and in each frame. Any failure while resolving them sends no screenshot. HEAL_MASK selectors are separated by newlines or ;; so a comma inside a selector survives. The docs say the screenshot shows whatever the page shows.
…ble text in frames and shadow roots, align the language helper A hit is named only when it is an exact match of an entry of the inventory that was sent (a near miss names nothing). xpath=, //, internal: engines, >> chains and regex locators are refused (the service writes none of them). The text of contenteditable regions in iframes and open shadow roots is cut from the aria snapshot. The live page records the Content-Language it used, so the by_language helper reaches the branch the client checked.
A frame is masked when the document it holds now is another origin (frame.url after redirects and script navigation), when it is sandboxed without allow-same-origin, has no document, or its url cannot be parsed. about:blank and srcdoc inherit the parent's origin. Tests: a redirecting src, a frame navigated by script, inheriting frames, odd urls, and a page of unknown origin.
…, escapes and comments waiting for, Timeout and strict mode lines keep only the locator expression (and the state words of a wait); when no printed locator is left, the locator call of a code-frame line is kept without its arguments. The cut text triages as the whole did on a corpus. locator(selector=...) is checked like a positional selector, and a selector with a backslash or a comment is refused.
… on both sides The by_language helper cannot see Content-Language during the verify re-run, so the client no longer uses it to choose the branch it checks, and does not send it to the service either.
…eplay, client hardening Conflicts: the README keeps both the vision and the follow-up/allowed-origins paragraphs; build_request takes the screenshot and the advise mode. The vision integration tests reach the in-process service at 127.0.0.1, since an open service refuses any other Host name now; the vision tests reuse the shared project fixture by assignment (ruff F811).
… from html lang and locale
…n library class names
…cks, WebSocket guard, one _origin, TS-parity masks build_locator refuses every probing selector (the TypeScript list, plus [value] and [defaultValue]) so every expression run on the live page is checked, a branch candidate's page-language branch and extra patches included, and a branch is checked like a plain locator before the live check. Foreign WebSockets are closed through context.route_web_socket. _origin is one function returning None for anything not http(s)/ws(s), so chrome-error and data frames are foreign. The screenshot clears contenteditable text, masks inputs that were ever type=password (an init-script WeakSet, a data attribute only during the shot) and uses the TypeScript autocomplete matching.
…ket guard, mask parity
…; test helper and client agree without <html lang>
|
Fix pass: no changes needed. The only review on this PR is Copilot's quota error ("insufficient remaining quota"), which has no findings, and there are 0 inline review comments. No commits were pushed. CI at the time of this check: setup, lint and pr-opened passed; unit, smoke and semgrep were still running. To get a real review, re-request Copilot at a lower effort level once quota resets, or run /code-review. 🤖 Generated with Claude Code |
|
✅ All smoke tests passed! View report |
|
✅ All chromium login tests passed! View report |
|
✅ All firefox login tests passed! View report |
The _browser() helpers in test_heal_client_vision and test_heal_parity started sync_playwright() and, when chromium.launch() raised (no browser installed, as in the CI unit job), returned without stopping it. The running driver left its event loop set on the main thread, so every later asyncio.run() in the unit suite failed with "cannot be called from a running event loop" (7 failures in CI run 42568369674). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016E8RmWaGbxMdGeZhQUgNit
|
Fix pass (2a41059). The smoke comment needed no change. The Cause: Fix: stop the driver in the Evidence (local, Python 3.12, same pinned pytest 9.1.1 / pytest-asyncio 1.4.0 / playwright 1.63.0):
|
|
✅ All smoke tests passed! View report |
|
✅ All chromium login tests passed! View report |
|
✅ All firefox login tests passed! View report |
|
CI after 2a41059: unit, lint, smoke, full and semgrep all pass. The first |
…t, top_at_center, copy_only, copy_cut, for_attr) and inventory_copy_more; vendor the current probe
…_wrap) and pass copy_wrap through the allowlist
…entory_copy_withheld and inventory_copy_withheld_data; vendor the current probe
…ontents copy counted as withheld)
…:contents copy counted)
|
✅ All smoke tests passed! View report |
|
Fix pass: no changes needed. The comment that triggered this pass is the github-actions smoke report (run 42744309809), and it shows a pass. It contains no findings. The only review is Copilot's quota error, and there are no inline comments. lint, unit, setup and smoke all pass. |
|
✅ All chromium login tests passed! View report |
|
Fix pass: no changes needed. The comment that triggered this pass is the github-actions chromium login report for run 42744309809, and it shows a pass with no findings. There are 0 inline review comments, and the only review is Copilot's quota error. Checks: lint, unit, setup, smoke and semgrep pass; |
|
✅ All firefox login tests passed! View report |
|
Fix pass: no changes needed. This pass was triggered by the github-actions firefox login report for run 42744309809, which reports a pass and contains no findings. All 6 checks pass (lint, unit, setup, smoke, full, semgrep). There are 0 inline review comments, and the only review is Copilot's quota-error notice. No commits were pushed. |
The Python heal client, updated for heal service phases 3 and 4.
What's in it
HEAL_VISION=auto|on. Password-like fields, one-time-code and card fields, any input that was evertype=password, cross-origin or sandboxed frames, andHEAL_MASKselectors are all masked. If masking fails, no screenshot is sent.build_locator. It refusesxpath=,internal:,>>, engine prefixes,text=, regexes, attribute operators on values, backslashes and comments, in every argument form.BASE_URLplusHEAL_ALLOW_ORIGINSmay load. Service workers are blocked and foreign WebSockets are closed.by_languageparity with the TypeScript helper. The difference that remains on pages without<html lang>is documented.Tests
592 unit tests pass (
--runxfail) against the matching heal-service branch, and lint is clean. Against an older installed service, the three advise and vision integration tests are skipped.Merge after the heal service PR. Its strict request schema rejects the new
scrollandContent-Typefields on older versions.