From 14c34daa2b12d711fc0f649c1b3ff7f26a616b27 Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Tue, 3 Nov 2026 23:36:15 -0800 Subject: [PATCH 1/4] feat(computer): the probe's fixture records the same effects under X11, macOS and Windows The Fixture Desk was written for X11's Tk, so a macOS driver failed four checks for the platform, not the driver. It now records Aqua's "," and " " keysyms under X11's names, binds Ctrl+/ to select-all itself (Tk does on X11 alone; Aqua's is Cmd+A), and counts events by their delta where Tk reports the wheel so (Aqua, Windows, Tk 9), X11's buttons 4 and 5 where Tk 8.6 does. layout.json's state says which windowing system it ran under. Co-Authored-By: Claude Opus 5.5 --- .../computer/conformance/fixture.py | 44 ++++++++++++++----- .../tests/test_computer_conformance.py | 8 ++++ 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/packages/agentenv-protocol/src/agentenv_protocol/computer/conformance/fixture.py b/packages/agentenv-protocol/src/agentenv_protocol/computer/conformance/fixture.py index 63b8a1869..d24ec859d 100644 --- a/packages/agentenv-protocol/src/agentenv_protocol/computer/conformance/fixture.py +++ b/packages/agentenv-protocol/src/agentenv_protocol/computer/conformance/fixture.py @@ -1,7 +1,8 @@ -"""The Linux fixture the conformance probe stages with data/add and launches with computer-exec: a Tk window, the -Fixture Desk, whose widgets' screen boxes are written to /layout.json and every effect that reaches it, -with whatever lands in /inbox.txt, to /state.json, so the probe can aim at it and read back -what arrived. Run as ``python3 fixture.py ``; it needs a 1280x800 screen or larger.""" +"""The fixture the conformance probe stages with data/add and launches with computer-exec: a Tk window, the Fixture +Desk, whose widgets' screen boxes are written to /layout.json and every effect that reaches it, with +whatever lands in /inbox.txt, to /state.json, so the probe can aim at it and read back what +arrived. It records the same effects under X11, macOS's Aqua and Windows, whose Tk names keys, binds select-all and +reports the wheel differently. Run as ``python3 fixture.py ``; it needs a 1280x800 screen or larger.""" from __future__ import annotations import json @@ -19,6 +20,8 @@ (20, 37, "#00ffff")) CLIPBOARD = "agentenv-probe clipboard" MIN_SIZE = (1280, 800) +#: X11's keysyms for the keys Aqua's Tk names by their character. +KEYSYMS = {",": "comma", " ": "space"} def main(state_dir: Path) -> None: @@ -32,7 +35,7 @@ def main(state_dir: Path) -> None: state: dict = {"name": "", "notes": "", "keys": [], "saved_count": 0, "subscribed": False, "selected": None, "scroll_top": 0.0, "cells": {}, "right_clicks": 0, "double_clicks": 0, "drags": [], "wheel": {"up": 0, "down": 0, "left": 0, "right": 0}, "clipboard": None, "loaded": None, - "screen": [width, height]} + "screen": [width, height], "windowing": root.tk.call("tk", "windowingsystem")} def save() -> None: part = state_dir / ".state.json.part" @@ -52,12 +55,20 @@ def save() -> None: def key(event) -> None: state["name"] = name.get() - state["keys"] = (state["keys"] + [event.keysym])[-100:] + state["keys"] = (state["keys"] + [KEYSYMS.get(event.keysym, event.keysym)])[-100:] state["clipboard"] = _clipboard(root) save() name.bind("", key) + def select_all(event) -> str: + name.selection_range(0, "end") + name.icursor("end") + return "break" + + # Tk binds Ctrl+/ to select-all on X11 alone; Aqua's is Cmd+A. + name.bind("", select_all) + def on_save() -> None: state["saved_count"] += 1 save() @@ -157,14 +168,19 @@ def release(event) -> None: wheel = tk.Canvas(root, width=200, height=120, bg="#e6f0ff", highlightthickness=0) wheel.place(x=840, y=500) - def wheeled(event, button: int) -> None: - # Tk 8.6 delivers X buttons 6 and 7 (horizontal) as 4 and 5 with Shift held. + def wheeled(event, steps: int) -> None: + # Tk delivers a horizontal scroll as a vertical one with Shift held, X's buttons 6 and 7 as 4 and 5. horizontal = bool(event.state & 1) - state["wheel"][("left" if horizontal else "up") if button == 4 else ("right" if horizontal else "down")] += 1 + way = ("left" if horizontal else "up") if steps > 0 else ("right" if horizontal else "down") + state["wheel"][way] += abs(steps) save() - wheel.bind("", lambda e: wheeled(e, 4)) - wheel.bind("", lambda e: wheeled(e, 5)) + # Tk 8.6 on X11 reports the wheel as buttons 4 and 5; Aqua, Windows and Tk 9 everywhere as . + if root.tk.call("tk", "windowingsystem") == "x11" and tk.TkVersion < 8.7: + wheel.bind("", lambda e: wheeled(e, 1)) + wheel.bind("", lambda e: wheeled(e, -1)) + else: + wheel.bind("", lambda e: wheeled(e, wheel_steps(e.delta))) field = tk.Canvas(root, width=48, height=48, bg="#000000", highlightthickness=0, bd=0) field.place(x=1080, y=500) @@ -198,6 +214,12 @@ def write_layout() -> None: root.mainloop() +def wheel_steps(delta: int) -> int: + """The wheel steps a event's ``delta`` holds, up positive: whole notches of 120 where Tk scales them + so (Windows, and Tk 9 everywhere), else one a unit (Aqua's Tk 8.6).""" + return int(delta / 120) if abs(delta) >= 120 else delta + + def _clipboard(root: tk.Tk) -> str | None: try: return root.clipboard_get() diff --git a/packages/agentenv-protocol/tests/test_computer_conformance.py b/packages/agentenv-protocol/tests/test_computer_conformance.py index d4ff28957..eb9dce157 100644 --- a/packages/agentenv-protocol/tests/test_computer_conformance.py +++ b/packages/agentenv-protocol/tests/test_computer_conformance.py @@ -32,6 +32,7 @@ ) from agentenv_protocol.computer.conformance import CheckResult, ProbeReport, run_probe # noqa: E402 from agentenv_protocol.computer.conformance import checks # noqa: E402 +from agentenv_protocol.computer.conformance import fixture # noqa: E402 from agentenv_protocol.computer.conformance.client import Driver # noqa: E402 from agentenv_protocol.computer.conformance.__main__ import main # noqa: E402 from agentenv_protocol import CONTROL_TOKEN_ENV # noqa: E402 @@ -284,6 +285,13 @@ async def test_a_fixture_that_doesnt_start_leaves_full_unverified_and_is_cleaned assert (["kill", "4243"] in probed.commands) is (driver is SilentFixture) +@pytest.mark.parametrize("delta, steps", [(1, 1), (-3, -3), (120, 1), (-360, -3), (240, 2)], + ids=["aqua-up", "aqua-down-3", "scaled-up", "scaled-down-3", "scaled-up-2"]) +def test_the_fixture_counts_a_wheel_events_steps_whether_tk_scales_them_by_120_or_not(delta, steps): + """Aqua's Tk 8.6 reports a step as a delta of 1; Windows, and Tk 9 everywhere, as 120.""" + assert fixture.wheel_steps(delta) == steps + + def test_a_refusal_must_carry_the_code_and_applied_no(): def result(error: bool, **data) -> CallToolResult: return CallToolResult(content=[TextContent(type="text", text="x")], structuredContent=data, isError=error) From e3b6f107fefd502f5685803a74814c4054d559eb Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Tue, 3 Nov 2026 23:39:58 -0800 Subject: [PATCH 2/4] test(computer): read the fixture with Tk stood in for, as the test's Python may have none Co-Authored-By: Claude Opus 5.5 --- .../tests/test_computer_conformance.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/agentenv-protocol/tests/test_computer_conformance.py b/packages/agentenv-protocol/tests/test_computer_conformance.py index eb9dce157..25f111b2f 100644 --- a/packages/agentenv-protocol/tests/test_computer_conformance.py +++ b/packages/agentenv-protocol/tests/test_computer_conformance.py @@ -4,8 +4,10 @@ fronts that exports is asked for its export once; the report's verdicts and the CLI.""" from __future__ import annotations +import importlib import json -from types import SimpleNamespace +import sys +from types import ModuleType, SimpleNamespace import pytest @@ -32,7 +34,6 @@ ) from agentenv_protocol.computer.conformance import CheckResult, ProbeReport, run_probe # noqa: E402 from agentenv_protocol.computer.conformance import checks # noqa: E402 -from agentenv_protocol.computer.conformance import fixture # noqa: E402 from agentenv_protocol.computer.conformance.client import Driver # noqa: E402 from agentenv_protocol.computer.conformance.__main__ import main # noqa: E402 from agentenv_protocol import CONTROL_TOKEN_ENV # noqa: E402 @@ -287,8 +288,14 @@ async def test_a_fixture_that_doesnt_start_leaves_full_unverified_and_is_cleaned @pytest.mark.parametrize("delta, steps", [(1, 1), (-3, -3), (120, 1), (-360, -3), (240, 2)], ids=["aqua-up", "aqua-down-3", "scaled-up", "scaled-down-3", "scaled-up-2"]) -def test_the_fixture_counts_a_wheel_events_steps_whether_tk_scales_them_by_120_or_not(delta, steps): - """Aqua's Tk 8.6 reports a step as a delta of 1; Windows, and Tk 9 everywhere, as 120.""" +def test_the_fixture_counts_a_wheel_events_steps_whether_tk_scales_them_by_120_or_not(delta, steps, monkeypatch): + """Aqua's Tk 8.6 reports a step as a delta of 1; Windows, and Tk 9 everywhere, as 120. The fixture runs in the + computer, so it's read here with Tk stood in for, as this Python may have none.""" + monkeypatch.setitem(sys.modules, "tkinter", ModuleType("tkinter")) + monkeypatch.delitem(sys.modules, "agentenv_protocol.computer.conformance.fixture", raising=False) + + fixture = importlib.import_module("agentenv_protocol.computer.conformance.fixture") + assert fixture.wheel_steps(delta) == steps From aa2111c78574ced61f5d3baab98924067ebe3aff Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Wed, 4 Nov 2026 00:07:30 -0800 Subject: [PATCH 3/4] feat(computer): the probe's fixture counts a scroll Tk 9 reports in pixels Tk 9 reports a scroll posted in pixels, as a macOS driver posts it, as , not , so the Fixture Desk's wheel saw none of cua-driver's scrolls on macOS. It now counts those by the lines they move, 10 pixels a line, since macOS coalesces quick scrolls into one event. Co-Authored-By: Claude Opus 5.5 --- .../computer/conformance/fixture.py | 33 +++++++++++++++---- .../tests/test_computer_conformance.py | 22 +++++++++---- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/packages/agentenv-protocol/src/agentenv_protocol/computer/conformance/fixture.py b/packages/agentenv-protocol/src/agentenv_protocol/computer/conformance/fixture.py index d24ec859d..b4f88d587 100644 --- a/packages/agentenv-protocol/src/agentenv_protocol/computer/conformance/fixture.py +++ b/packages/agentenv-protocol/src/agentenv_protocol/computer/conformance/fixture.py @@ -22,6 +22,8 @@ MIN_SIZE = (1280, 800) #: X11's keysyms for the keys Aqua's Tk names by their character. KEYSYMS = {",": "comma", " ": "space"} +#: The pixels of a scrolled line, macOS's for a scroll posted in lines. +LINE_PX = 10 def main(state_dir: Path) -> None: @@ -168,19 +170,25 @@ def release(event) -> None: wheel = tk.Canvas(root, width=200, height=120, bg="#e6f0ff", highlightthickness=0) wheel.place(x=840, y=500) - def wheeled(event, steps: int) -> None: - # Tk delivers a horizontal scroll as a vertical one with Shift held, X's buttons 6 and 7 as 4 and 5. - horizontal = bool(event.state & 1) + def wheeled(horizontal: bool, steps: int) -> None: way = ("left" if horizontal else "up") if steps > 0 else ("right" if horizontal else "down") state["wheel"][way] += abs(steps) save() - # Tk 8.6 on X11 reports the wheel as buttons 4 and 5; Aqua, Windows and Tk 9 everywhere as . + def scrolled(delta: int) -> None: + if steps := touchpad_steps(delta): + wheeled(*steps) + + # Tk 8.6 on X11 reports the wheel as buttons 4 and 5, Aqua, Windows and Tk 9 as , a horizontal scroll + # (X's buttons 6 and 7) as a vertical one with Shift held. Tk 9 reports a scroll in pixels, a trackpad's or one a + # driver posts as Aqua's do, as . if root.tk.call("tk", "windowingsystem") == "x11" and tk.TkVersion < 8.7: - wheel.bind("", lambda e: wheeled(e, 1)) - wheel.bind("", lambda e: wheeled(e, -1)) + wheel.bind("", lambda e: wheeled(bool(e.state & 1), 1)) + wheel.bind("", lambda e: wheeled(bool(e.state & 1), -1)) else: - wheel.bind("", lambda e: wheeled(e, wheel_steps(e.delta))) + wheel.bind("", lambda e: wheeled(bool(e.state & 1), wheel_steps(e.delta))) + if tk.TkVersion >= 8.7: + wheel.bind("", lambda e: scrolled(e.delta)) field = tk.Canvas(root, width=48, height=48, bg="#000000", highlightthickness=0, bd=0) field.place(x=1080, y=500) @@ -220,6 +228,17 @@ def wheel_steps(delta: int) -> int: return int(delta / 120) if abs(delta) >= 120 else delta +def touchpad_steps(delta: int) -> tuple[bool, int] | None: + """The steps a event's ``delta`` holds, as horizontal or not and up or left positive, or None + when it moves nothing. Tk packs the x distance in its high 16 bits and the y distance, signed, in its low 16; a + step is a line, LINE_PX, and macOS may coalesce several into one event.""" + dx, dy = delta >> 16, (delta & 0xFFFF) - (0x10000 if delta & 0x8000 else 0) + if not (dx or dy): + return None + distance = dy or dx + return not dy, (1 if distance > 0 else -1) * max(1, round(abs(distance) / LINE_PX)) + + def _clipboard(root: tk.Tk) -> str | None: try: return root.clipboard_get() diff --git a/packages/agentenv-protocol/tests/test_computer_conformance.py b/packages/agentenv-protocol/tests/test_computer_conformance.py index 25f111b2f..a7dcfe8fe 100644 --- a/packages/agentenv-protocol/tests/test_computer_conformance.py +++ b/packages/agentenv-protocol/tests/test_computer_conformance.py @@ -289,14 +289,24 @@ async def test_a_fixture_that_doesnt_start_leaves_full_unverified_and_is_cleaned @pytest.mark.parametrize("delta, steps", [(1, 1), (-3, -3), (120, 1), (-360, -3), (240, 2)], ids=["aqua-up", "aqua-down-3", "scaled-up", "scaled-down-3", "scaled-up-2"]) def test_the_fixture_counts_a_wheel_events_steps_whether_tk_scales_them_by_120_or_not(delta, steps, monkeypatch): - """Aqua's Tk 8.6 reports a step as a delta of 1; Windows, and Tk 9 everywhere, as 120. The fixture runs in the - computer, so it's read here with Tk stood in for, as this Python may have none.""" - monkeypatch.setitem(sys.modules, "tkinter", ModuleType("tkinter")) - monkeypatch.delitem(sys.modules, "agentenv_protocol.computer.conformance.fixture", raising=False) + """Aqua's Tk 8.6 reports a step as a delta of 1; Windows, and Tk 9 everywhere, as 120.""" + assert _fixture(monkeypatch).wheel_steps(delta) == steps + - fixture = importlib.import_module("agentenv_protocol.computer.conformance.fixture") +@pytest.mark.parametrize("delta, steps", [(10, (False, 1)), (65526, (False, -1)), (65516, (False, -2)), + (655360, (True, 1)), (-655360, (True, -1)), (0, None)], + ids=["up", "down", "down-2-coalesced", "left", "right", "still"]) +def test_the_fixture_counts_a_scroll_tk_9_reports_in_pixels_by_the_lines_it_moves(delta, steps, monkeypatch): + """The deltas Tk 9 on macOS gave cua-driver's scrolls: 10 pixels a line, two lines at once where macOS coalesced + them.""" + assert _fixture(monkeypatch).touchpad_steps(delta) == steps - assert fixture.wheel_steps(delta) == steps + +def _fixture(monkeypatch): + """The fixture module, which runs in the computer, read with Tk stood in for, as this Python may have none.""" + monkeypatch.setitem(sys.modules, "tkinter", ModuleType("tkinter")) + monkeypatch.delitem(sys.modules, "agentenv_protocol.computer.conformance.fixture", raising=False) + return importlib.import_module("agentenv_protocol.computer.conformance.fixture") def test_a_refusal_must_carry_the_code_and_applied_no(): From f146c86dbb2568517bfcc7783c89eeb73c5aa104 Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Wed, 4 Nov 2026 00:16:22 -0800 Subject: [PATCH 4/4] refactor(computer): name the fixture's wheel notch and trackpad packing Co-Authored-By: Claude Opus 5.5 --- .../computer/conformance/fixture.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/agentenv-protocol/src/agentenv_protocol/computer/conformance/fixture.py b/packages/agentenv-protocol/src/agentenv_protocol/computer/conformance/fixture.py index b4f88d587..d68760db3 100644 --- a/packages/agentenv-protocol/src/agentenv_protocol/computer/conformance/fixture.py +++ b/packages/agentenv-protocol/src/agentenv_protocol/computer/conformance/fixture.py @@ -5,6 +5,7 @@ reports the wheel differently. Run as ``python3 fixture.py ``; it needs a 1280x800 screen or larger.""" from __future__ import annotations +import ctypes import json import os import sys @@ -22,6 +23,10 @@ MIN_SIZE = (1280, 800) #: X11's keysyms for the keys Aqua's Tk names by their character. KEYSYMS = {",": "comma", " ": "space"} +#: A wheel notch's delta where Tk scales the wheel: Windows', and Tk 9's everywhere. +NOTCH_DELTA = 120 +#: Tk 9 packs a 's distances into its delta: y, a signed 16-bit number, in the low bits, x above. +Y_BITS = 16 #: The pixels of a scrolled line, macOS's for a scroll posted in lines. LINE_PX = 10 @@ -225,14 +230,13 @@ def write_layout() -> None: def wheel_steps(delta: int) -> int: """The wheel steps a event's ``delta`` holds, up positive: whole notches of 120 where Tk scales them so (Windows, and Tk 9 everywhere), else one a unit (Aqua's Tk 8.6).""" - return int(delta / 120) if abs(delta) >= 120 else delta + return int(delta / NOTCH_DELTA) if abs(delta) >= NOTCH_DELTA else delta def touchpad_steps(delta: int) -> tuple[bool, int] | None: """The steps a event's ``delta`` holds, as horizontal or not and up or left positive, or None - when it moves nothing. Tk packs the x distance in its high 16 bits and the y distance, signed, in its low 16; a - step is a line, LINE_PX, and macOS may coalesce several into one event.""" - dx, dy = delta >> 16, (delta & 0xFFFF) - (0x10000 if delta & 0x8000 else 0) + when it moves nothing. A step is a line, LINE_PX, and macOS may coalesce several into one event.""" + dx, dy = delta >> Y_BITS, ctypes.c_int16(delta).value if not (dx or dy): return None distance = dy or dx