Skip to content

flow: check a click target's hover response, and guard pixel clicks from check to click - #1334

Merged
abrichr merged 13 commits into
mainfrom
claude/hover-identity
Oct 19, 2026
Merged

abrichr merged 13 commits into
mainfrom
claude/hover-identity

Conversation

@abrichr

@abrichr abrichr commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Stacked on #1333 (its commits show here until it merges). This PR's own change is the last commit.

What

On pixel surfaces, an icon-only click carries no text that names its control, so the right-record check had nothing to compare. Most apps answer a pointer resting on a control (highlight, tooltip, status text). Now:

  • Record: Recorder(capture_hover=True) rests the pointer on each click target, waits 0.8 s, and keeps the frame (frames/{i}_hover.png). Agent-driven RDP and Citrix authoring sessions turn it on.
  • Compile: for a click with no other identity evidence, what changed near the target is stored as Step.hover (HoverEvidence: the box relative to the click, and its pixels under templates/hover/, sealed with the other crops).
  • Replay: at the step-entry check, on the pixel rungs, on a backend with hover, the runtime rests the pointer on the resolved target, waits the same time, and compares (runtime/hover_evidence.py). The verdict is bound into the step's identity check (mode="hover").

Fail-closed rules

  • Hover evidence only adds stops. A response with other content, or in another place or shape, stops the run before the click.
  • A missing response, or one that can't be compared, is recorded and changes nothing (the step behaves as before).
  • Same render: pixels are compared block by block after a small alignment, so a changed tooltip word isn't averaged away. Another render (ClearType switched, display scaling): the tooltip text decides.
  • The pre-click check takes no input; only the step-entry check hovers.
  • Bundles compiled before this change keep their sealed digest: an absent Step.hover is a reviewed v2 omission; a recorded response is sealed content.
  • Backends that can hover: browser, remote-display (Citrix/RDP client window), FreeRDP. Not yet: Aardwolf, native Windows/macOS/Linux; those take no hover look.

Check-to-click guard (pixel clicks)

A pixel step is checked on one frame and clicked a moment later (the gap the "visual confused deputy" work calls TOCTOU). Consequential identity-gated clicks were already bound to the exact checked frame by hash. Every other pixel click (click, double-click, right-click) now gets Replayer._pixel_recheck:

  • right before the click, take a fresh frame and compare the target, identity and hover regions with the frame the step was checked on (the hover frame after a hover look);
  • on a change (a dialog opened, the list re-sorted), don't click: re-resolve and re-run the identity check on the fresh frame with no input, and click only when the next frame matches;
  • after 2 re-observations that still see a change, or when the target or identity can't be confirmed again, refuse the click (safety_halt);
  • each change is recorded on the step (StepResult.check_to_click, omitted when empty). Changes under 4 pixels or 24 levels per channel (encoder noise) don't count. Structured targets keep their element tokens and aren't rechecked.

Tests (tests/test_check_to_click.py): a dialog opening between check and click aborts with nothing clicked; a moved target is re-observed and clicked where it is now; a screen that keeps changing is refused; a clock elsewhere and encoder noise don't stop the click; the hover frame is the baseline after a hover look; structured targets aren't rechecked.

Benchmark

benchmark/hover_identity/run_hover_sweep.py: the appointment kiosk with Save as a toolbar icon, plus an identical-looking Clear form icon that differs only by tooltip; the kiosk draws Tk-style hover feedback (kiosk_render.py, off by default so the display-scale sweep is unchanged). Then the two icons swap places. On that kiosk the look-alike icon was clicked in 2 of 3 runs before and 0 of 3 after; numbers and docs in #1340.

Tests

tests/test_hover_evidence.py (19): response measurement, compare verdicts, look-alike icon with other tooltip stops, rasterizer change with same text verifies, no hover at the pre-click check, structural rung takes no hover, backend without hover keeps old behaviour, compiler keeps the response, recorder saves the hover frame only when asked.

No new commands.

🤖 Generated with Claude Code

abrichr and others added 8 commits October 19, 2026 09:21
On a pixel surface, an OCR identity read that abstains only because the
record ID has a look-alike glyph (1/l, 0/O) now takes extra looks before
the run stops: two fresh frames, the 2x re-read, the known-ID list, and,
where configured, a select-and-copy of the identifier's text box. Only the
list or a copy equal to a run parameter can settle the record; any look
that names another record is final. Each look is recorded on the step's
identity check. The copy look is input, so only the step-entry check takes
it; the actuation check reuses it only on matching pixels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a pixel surface, an icon-only click has no text that names its
control. The recorder can now rest the pointer on each click target and
keep that frame; the compiler stores what changed near the target (the
control's highlight or tooltip) as hover evidence for clicks with no other
identity evidence. At replay, on the pixel rungs, the runtime rests the
pointer on the resolved target and compares. A different response stops
the run before the click; a missing or uncomparable one is recorded and
changes nothing. Hover works on the browser, remote-display and FreeRDP
backends. Agent-driven RDP and Citrix recordings capture it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An absent Step.hover is omitted from the content digest, like other
additive v2 step fields; a recorded hover response is sealed content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The copy drag now runs to the edges of the identifier's text box; it
  stopped inside the last glyph and copied one character short.
- A split identifier (103 045 against 103045) is matched as one string,
  against the read and the known-ID list.
- The check just before the click reuses the step-entry verdict when the
  identifier shows the same pixels, instead of re-reading the selected text.
- A run-parameter mismatch that is only a look-alike misread (C0X1234 read
  as COX1234) gets the same looks. The mismatch stands unless the list or
  an exact copy of the run value settles the record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	packages/flow/openadapt_flow/runtime/replayer.py
Consequential identity-gated clicks were already bound to the exact checked
frame. Every other pixel click now compares the target, identity and hover
regions on a fresh frame taken right before the click with the frame the step
was checked on (the hover frame after a hover look). On a change it does not
click: it re-resolves the target and re-runs the identity check on the fresh
frame with no input, and clicks only when the next frame matches. Two
re-observations that still see a change, or a target or identity that cannot
be confirmed again, refuse the click. Each change is recorded on the step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@abrichr abrichr changed the title flow: check a click target's hover response before an icon-only click flow: check a click target's hover response, and guard pixel clicks from check to click Oct 19, 2026
abrichr and others added 4 commits October 19, 2026 15:00
- identity_looks_dense: pass a typed click point and ActionKind (mypy).
- LIMITS.md: name identity_looks.py by its package path, which the docs
  consistency check resolves.
- The look-alike twin test asserts the stop; it accepts a read that is
  already final, as Linux OCR builds read it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- replayer: assert the anchor before the identity check so mypy can narrow
  it when a hover check stands in for the OCR check.
- LIMITS.md: name hover_evidence.py by its package path.
- test_state_dependency: the never-settling fixture now animates a strip
  clear of the click target. An animation over the target itself is a
  change between check and click, which the new guard refuses; the test is
  about the readiness default, not about clicking under a moving bar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@abrichr
abrichr merged commit e58e118 into main Oct 19, 2026
41 checks passed
@abrichr
abrichr deleted the claude/hover-identity branch October 19, 2026 22:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant