From f5d38a388c86262e2d886bb1077bc6c48c391fc1 Mon Sep 17 00:00:00 2001 From: abrichr Date: Tue, 3 Nov 2026 00:15:51 -0500 Subject: [PATCH 1/2] owned_window_over: an owned dialog over part of the region counts agnostic-repair's live run: the Session notice dialog covers the right part of the identity band, not its center, so the center rule answered None and the repair never applied. The topmost owned window that meets the region is now reported. Unchanged: any other process's window, or an app window not owned by the capture, meeting the region anywhere above the captured window answers None; the caller matches the uncovered part of the band and rechecks identity after the dialog is dismissed. --- .../backends/win_agent/server.py | 12 ++++----- .../backends/windows_backend.py | 5 ++-- packages/flow/tests/test_win_agent_server.py | 27 ++++++++++++++----- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/packages/flow/openadapt_flow/backends/win_agent/server.py b/packages/flow/openadapt_flow/backends/win_agent/server.py index 755aa392f..7316fe1ce 100644 --- a/packages/flow/openadapt_flow/backends/win_agent/server.py +++ b/packages/flow/openadapt_flow/backends/win_agent/server.py @@ -1702,8 +1702,10 @@ def _window_operation( to the captured window and answers with a window only when all of this holds: no visible window of another process, and none of the captured process that its owner chain does not tie to the captured window, meets - the region anywhere above the captured window; and an owned window - covers the region's center (its visible frame, not the resize border). + the region anywhere above the captured window; and an owned window meets + the region (its visible frame, not the resize border). The topmost such + window is the answer. Whether the covered part hides anything that + matters is for the caller, which still sees the uncovered part. A captured window that is gone or changed process, a cloak state that cannot be read for one of the app's windows, and anything else answer ``None``. @@ -1755,7 +1757,6 @@ def _window_operation( if not user32.is_window(hwnd) or user32.pid_of(hwnd) != pid: return {"status": "ok", "window": None} box = (region[0], region[1], region[2], region[3]) - center = (region[0] + region[2] // 2, region[1] + region[3] // 2) owned: Optional[dict[str, Any]] = None reached = False for candidate in user32.top_level_windows(): @@ -1785,10 +1786,7 @@ def _window_operation( link = user32.owner(link) if link != hwnd: return {"status": "ok", "window": None} # not tied to the capture - covers_center = ( - rect[0] <= center[0] < rect[2] and rect[1] <= center[1] < rect[3] - ) - if owned is None and covers_center: + if owned is None: owned = { "title": user32.title(candidate)[:512] or None, "hwnd": candidate, diff --git a/packages/flow/openadapt_flow/backends/windows_backend.py b/packages/flow/openadapt_flow/backends/windows_backend.py index fbb397f62..ff5699ae1 100644 --- a/packages/flow/openadapt_flow/backends/windows_backend.py +++ b/packages/flow/openadapt_flow/backends/windows_backend.py @@ -861,8 +861,9 @@ def owned_window_over( :meth:`record_verified_window`). Returns ``{"title", "hwnd", "owner_hwnd", "pid"}`` only when the first visible window above the recorded one, of the same process and with an owner chain reaching it - (an owned dialog, a message box), covers the region's center, and no - window of another process, or unowned, meets the region above it. Returns None with no record for that step, when the + (an owned dialog, a message box), meets the region, and no window of + another process, or unowned, meets the region above it. The topmost + owned window that meets it is returned. Returns None with no record for that step, when the recorded window is gone or now another process's, for any other process's window or an unowned one, and on any failure. Never raises. """ diff --git a/packages/flow/tests/test_win_agent_server.py b/packages/flow/tests/test_win_agent_server.py index 695692367..ffe805778 100644 --- a/packages/flow/tests/test_win_agent_server.py +++ b/packages/flow/tests/test_win_agent_server.py @@ -1338,10 +1338,27 @@ def test_a_topmost_owned_popup_does_not_hide_a_foreign_window_over_the_target( assert _owned_over(monkeypatch, desktop) is None -def test_an_owned_window_that_only_clips_the_region_is_none(monkeypatch) -> None: - # A shadow or glow window of the app's own grazing one corner. +def test_an_owned_dialog_over_part_of_the_region_is_reported(monkeypatch) -> None: + # Live lab finding: the "Session notice" dialog covers the right part of + # the identity band, not its center; the caller still sees the rest. desktop = _FakeDesktop() desktop.add(0x200, owner=_FakeDesktop.PINNED, rect=(295, 265, 400, 300)) + assert _owned_over(monkeypatch, desktop)["hwnd"] == 0x200 + + +def test_a_foreign_window_beside_an_owned_dialog_over_the_band_is_none( + monkeypatch, +) -> None: + # Review attack (a): the app's dialog covers one end of the band and + # another process's window the other end. + desktop = _FakeDesktop() + desktop.add(0x200, owner=_FakeDesktop.PINNED, rect=(280, 240, 400, 300)) + desktop.add(0x900, pid=99, rect=(200, 240, 262, 300)) + assert _owned_over(monkeypatch, desktop) is None + # Same with the foreign window below the dialog in z-order. + desktop = _FakeDesktop() + desktop.add(0x900, pid=99, rect=(200, 240, 262, 300)) + desktop.add(0x200, owner=_FakeDesktop.PINNED, rect=(280, 240, 400, 300)) assert _owned_over(monkeypatch, desktop) is None @@ -1389,13 +1406,11 @@ def test_a_dialog_owned_through_a_chain_counts(monkeypatch) -> None: assert _owned_over(monkeypatch, desktop)["hwnd"] == 0x300 -def test_an_owned_window_over_the_center_wins_over_one_only_clipping( - monkeypatch, -) -> None: +def test_the_topmost_owned_window_over_the_region_is_reported(monkeypatch) -> None: desktop = _FakeDesktop() desktop.add(0x200, owner=_FakeDesktop.PINNED, title="Session notice") desktop.add(0x300, owner=_FakeDesktop.PINNED, rect=(295, 265, 400, 300)) - assert _owned_over(monkeypatch, desktop)["hwnd"] == 0x200 + assert _owned_over(monkeypatch, desktop)["hwnd"] == 0x300 @pytest.mark.parametrize( From df016fe01ac5784584436b8f58aae60ec1c210fc Mon Sep 17 00:00:00 2001 From: abrichr Date: Tue, 3 Nov 2026 00:19:44 -0500 Subject: [PATCH 2/2] owned_window_over: measure another process's window by everything it may paint Review should-fix: a self-drawn frame (Electron, Chromium) paints into the ~7 px border DWM leaves out of the visible frame, so a strip of a foreign window at the band's edge went unseen. Foreign windows are now measured by the union of GetWindowRect and the visible frame; the app's own keep the visible frame. --- .../backends/win_agent/server.py | 22 ++++++++++++++++++- packages/flow/tests/test_win_agent_server.py | 16 ++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/flow/openadapt_flow/backends/win_agent/server.py b/packages/flow/openadapt_flow/backends/win_agent/server.py index 7316fe1ce..d73a9db27 100644 --- a/packages/flow/openadapt_flow/backends/win_agent/server.py +++ b/packages/flow/openadapt_flow/backends/win_agent/server.py @@ -1512,6 +1512,24 @@ def rect(self, hwnd: int) -> Optional[tuple[int, int, int, int]]: return None return (int(r.left), int(r.top), int(r.right), int(r.bottom)) + def outer_rect(self, hwnd: int) -> Optional[tuple[int, int, int, int]]: + """Everything the window may paint: GetWindowRect joined with the + visible frame (a self-drawn frame can paint into the border DWM + leaves out).""" + r = self._wintypes.RECT() + frame = self.rect(hwnd) + if not self._dll.GetWindowRect(hwnd, self._ctypes.byref(r)): + return frame + outer = (int(r.left), int(r.top), int(r.right), int(r.bottom)) + if frame is None: + return outer + return ( + min(outer[0], frame[0]), + min(outer[1], frame[1]), + max(outer[2], frame[2]), + max(outer[3], frame[3]), + ) + def per_monitor_dpi(self) -> bool: """Put this thread in per-monitor-v2 DPI awareness, so window rects and points are physical pixels like the screenshot's.""" @@ -1771,7 +1789,9 @@ def _window_operation( return {"status": "ok", "window": None} # cannot tell if it shows if cloaked: continue - rect = user32.rect(candidate) + # Another process's window is measured by everything it may paint; + # the app's own by its visible frame. + rect = user32.rect(candidate) if same_process else user32.outer_rect(candidate) if rect is None or rect[2] <= rect[0] or rect[3] <= rect[1]: continue if not _intersects(rect, box): diff --git a/packages/flow/tests/test_win_agent_server.py b/packages/flow/tests/test_win_agent_server.py index ffe805778..3fd5dbcf7 100644 --- a/packages/flow/tests/test_win_agent_server.py +++ b/packages/flow/tests/test_win_agent_server.py @@ -1249,6 +1249,7 @@ def __init__(self) -> None: self.dpi_ok = True self.foreground_window = self.PINNED self.roots: dict[int, int] = {} # child hwnd -> top-level + self.paint_border: dict[int, int] = {} # self-drawn frame, in px def add( self, @@ -1279,6 +1280,11 @@ def owner(self, hwnd): def rect(self, hwnd): return self.windows[hwnd][2] + def outer_rect(self, hwnd): + x0, y0, x1, y1 = self.windows[hwnd][2] + grow = self.paint_border.get(hwnd, 0) + return (x0 - grow, y0 - grow, x1 + grow, y1 + grow) + def is_visible(self, hwnd): return self.windows[hwnd][3] @@ -1346,6 +1352,16 @@ def test_an_owned_dialog_over_part_of_the_region_is_reported(monkeypatch) -> Non assert _owned_over(monkeypatch, desktop)["hwnd"] == 0x200 +def test_a_foreign_self_drawn_frame_reaching_the_band_is_none(monkeypatch) -> None: + # Review should-fix: an Electron-style window paints into the ~7 px border + # DWM leaves out of its visible frame. + desktop = _FakeDesktop() + desktop.add(0x200, owner=_FakeDesktop.PINNED, rect=(280, 240, 400, 300)) + desktop.add(0x900, pid=99, rect=(200, 240, 245, 300)) # frame ends 5 px short + desktop.paint_border[0x900] = 7 + assert _owned_over(monkeypatch, desktop) is None + + def test_a_foreign_window_beside_an_owned_dialog_over_the_band_is_none( monkeypatch, ) -> None: