Skip to content

Commit 9bb3cb2

Browse files
committed
Stop CLI fallback after native pointer region refusal
1 parent 1a7ad4c commit 9bb3cb2

18 files changed

Lines changed: 212 additions & 28 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ it shipped into a `## [x.y.z] - date` section of their own; the tag's
1313

1414
## Unreleased
1515

16+
- Refuse native Wayland pointer coordinates outside all EIS regions with
17+
failed state and no automatic CLI fallback. A rejected move cannot emit
18+
a subsequent click; valid mapping and explicit CLI selection are preserved.
19+
1620
- Add passive input/capture capability snapshots to both SDK namespaces,
1721
AC_probe_capabilities, read-only MCP, Builder and Diagnostics Actions.
1822
Prerequisites remain unverified; local native state is not refreshed or

‎Progress.md‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
`WIP` — 改版仍缺以下內容;原生平台與正式環境驗收缺口另列於後。
1616
計畫 D1 仍缺 portal session 關閉/恢復、compositor 重啟的狀態與原生驗收、
1717
擷取結果與擁有者 context 的接合,以及主動診斷與原生 session 的重試/取消/停止控制。
18-
原生指標超出 EIS 區域的拒絕仍被誤分類為選用依賴缺失,須停止隱含的 CLI 備援。
1918
D2 的原生 EI 釋放驗證與 D3 的錄製/全域停止/Windows 原生崩潰證據仍缺,詳見計畫與下方生命週期待辦。
2019
計畫 E(Android/iOS)、G(MCP 逐步揭露)、H(型別深化與總驗收)尚未開始。F 還缺:
2120

‎README.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,13 @@ returning the blank XWayland root. The `screen_capture` check in
473473
`je_auto_control.api.run_diagnostics()` (and the GUI Diagnostics tab) reports
474474
which tier is in use.
475475

476+
A native Wayland pointer coordinate outside every granted EIS region, after
477+
raw-coordinate and layout-origin mapping, raises `LibeiUnavailable` with
478+
`state="failed"` and disables automatic CLI fallback. A rejected move emits
479+
no motion/frame or subsequent click. Valid coordinates, devices without
480+
regions, later valid requests on the same sender and explicit CLI selection
481+
retain their existing behavior.
482+
476483
Passive **input/capture capabilities** use `BackendContext`, `CapabilityStatus`,
477484
`CapabilitySnapshot` and `probe_capabilities` from `je_auto_control.api` (also
478485
exported at the package root). Call

‎README/README_zh-CN.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,11 @@ Wayland 的屏幕截取需要合成器对应的工具,因为没有单一工具
438438
`je_auto_control.api.run_diagnostics()`(以及 GUI 的 Diagnostics 标签页)的 `screen_capture`
439439
检查会报告当前使用的是哪一层。
440440

441+
原生 Wayland 指针坐标在原始坐标与布局原点换算后仍不属于任何授权 EIS 区域时,
442+
会以 `state="failed"` 的 `LibeiUnavailable` 拒绝,停止自动 CLI 备用,
443+
不发送移动、frame 或后续点击。有效坐标、未提供区域的设备、同一 sender 的后续有效请求,
444+
以及显式选择 CLI 的现有行为保持不变。
445+
441446
**输入/截取能力**提供 `je_auto_control.api`(也由包根目录导出)的
442447
`BackendContext`、`CapabilityStatus`、`CapabilitySnapshot` 与 `probe_capabilities`。
443448
调用 `probe_capabilities(BackendContext.current()).to_dict()`、无参数的

‎README/README_zh-TW.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,11 @@ Wayland 的螢幕擷取需要合成器對應的工具,因為沒有單一工具
438438
`je_auto_control.api.run_diagnostics()`(以及 GUI 的 Diagnostics 分頁)的 `screen_capture`
439439
檢查會回報目前使用哪一層。
440440

441+
原生 Wayland 指標座標在原始座標與版面原點換算後仍不屬於任何授權 EIS 區域時,
442+
會以 `state="failed"` 的 `LibeiUnavailable` 拒絕,停止自動 CLI 備援,
443+
不送出移動、frame 或後續點擊。有效座標、未提供區域的裝置、同一 sender 的後續有效請求,
444+
以及明確選擇 CLI 的既有行為維持不變。
445+
441446
**輸入/擷取能力**提供 `je_auto_control.api`(亦由套件根目錄匯出)的
442447
`BackendContext`、`CapabilityStatus`、`CapabilitySnapshot` 與 `probe_capabilities`。
443448
呼叫 `probe_capabilities(BackendContext.current()).to_dict()`、無參數的

‎architecture.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ macOS, Linux X11, Linux Wayland, Android and iOS.
1313

1414
## 2. Layers and directories
1515

16+
Native pointer region validation preserves raw-first then layout-origin
17+
mapping. A miss raises a failed LibeiUnavailable before motion/frame or
18+
a following click. The input selector propagates that refusal and cannot
19+
interpret it as optional dependency absence or switch automatically to CLI.
20+
The cached sender remains usable for later valid coordinates.
21+
1622
wrapper/capabilities.py owns immutable caller contexts and independent
1723
input/capture metadata. Live collection reads a bounded set of environment/
1824
PATH facts and an already-loaded sender via a nonblocking local-state read.

‎architecture_explore.md‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ iOS(WebDriverAgent)。核心能力是滑鼠/鍵盤控制、影像辨識、
2020
| 指標 | 數值 |
2121
| --- | ---: |
2222
| Python 模組總數(含周邊子專案) | 1,215 |
23-
| 程式碼總行數 | 181,185 |
23+
| 程式碼總行數 | 181,186 |
2424
| `je_auto_control/utils/` 子套件數 | 311 |
2525
| `AC_*` 動作指令數(`known_commands()` 實測) | 810 |
2626
| 套件門面 `__all__` 公開名稱數 | 1377 |
2727
| GUI 分頁數(`main_widget` 註冊) | 49 |
2828
| MCP 工具數(`build_default_tool_registry()` 實測) | 710 |
29-
| `test_*.py` 測試檔/測試函式 | 836 / 8,743 |
29+
| `test_*.py` 測試檔/測試函式 | 837 / 8,748 |
3030
| 範例腳本 | 42 |
3131

3232
**技術基線**:Python ≥ 3.10、MIT 授權、必要相依只有 `je_open_cv`/`opencv-python`/`pillow`/`mss`/
@@ -234,7 +234,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。
234234
| `uinput/keyboard.py` | 44 | uinput 鍵盤後端,介面與 X11 版一致。 |
235235
| `uinput/mouse.py` | 130 | uinput 滑鼠後端。 |
236236

237-
#### Linux Wayland(`linux_wayland/`,18 檔/3,138 行)
237+
#### Linux Wayland(`linux_wayland/`,18 檔/3,139 行)
238238

239239
| 模組 | 行數 | 職責 |
240240
| --- | ---: | --- |
@@ -246,7 +246,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。
246246
| `_select_input.py` | 103 | 決定使用原生 libei 或 CLI shim;`active_backend()` 是 keyboard/mouse 的唯一入口,`emitted()` 僅容許未分類的選用後端缺席退回 CLI;明確原生選擇、權限/裝置/協定拒絕保持失敗。 |
247247
| `_layout.py` | 83 | 版面原點的共用查詢。擷取與輸入不是同一個座標空間,差的就是這個原點:libei 的 region offset 是 `uint32`(描述不了負原點),`ydotool mousemove --absolute` 的原點是合成器夾取的那個角落——兩條路都要減掉它,所以放在這裡而不是各自複製。讀數快取一秒——擷取那一側刻意不快取,但 ydotool 每次絕對移動都會問,不快取等於每次移動多開一個 `wlr-randr` 行程。 |
248248
| `oeffis.py` | 214 | liboeffis 綁定:跑完 RemoteDesktop portal 交握,交出 EIS fd;請求後的拒絕/逾時/失敗保留明確狀態。 |
249-
| `libei.py` | 730 | libei 綁定、撤銷閂鎖與只快取中繼資料的失敗狀態;完整握手(seat 綁定能力 → 由事件取得 device → start_emulating → 每次發送後 frame)。另負責絕對指標的座標空間:讀回裝置的 region,把版面座標映射進去,沒有任何 region 涵蓋就拒絕(libei 對這種移動是靜靜丟掉的)。 快取觀測不讀事件,完整就緒涵蓋全部請求的事件種類。 |
249+
| `libei.py` | 731 | libei 綁定、撤銷閂鎖與只快取中繼資料的失敗狀態;完整握手(seat 綁定能力 → 由事件取得 device → start_emulating → 每次發送後 frame)。另負責絕對指標的座標空間:讀回裝置的 region,把版面座標映射進去,沒有任何 region 涵蓋就以 failed 拒絕,停止隱含 CLI 備援與後續點擊(不撤銷 sender)。 快取觀測不讀事件,完整就緒涵蓋全部請求的事件種類。 |
250250
| `mouse.py` | 387 | 滑鼠後端:移動、按鈕與捲動都 libei 優先,僅選用缺席時退回 ydotool;權限/裝置拒絕不重送;送往 libei 時垂直捲動軸取負(kernel `REL_WHEEL` 與 `wl_pointer` 正負號相反)。退到 ydotool 的絕對移動會先減掉版面原點(`--absolute` 是相對於版面左上角,不是版面座標的 `(0, 0)`),並依 `pointer_accel_mode()` 處理指標加速度——倍率讀不回來,只有操作者知道,所以由 `JE_AUTOCONTROL_WAYLAND_POINTER_ACCEL` 宣告:未設定=每個行程警告一次後照送、`flat`=已關掉加速度故靜靜送出、`strict`=拒絕這次移動。 |
251251
| `keyboard.py` | 206 | 鍵盤後端:按鍵/組合鍵 libei 優先,權限拒絕不改走 CLI;中斷時逐一釋放原後端已按下的鍵。文字仍用 wtype。 |
252252
| `keymap.py` | 155 | 友善鍵名 → evdev key code。 |
@@ -1161,7 +1161,7 @@ GUI 是**選用 extra**(`pip install je_auto_control[gui]`,只需要 PySide6
11611161
| `scripts/dev_release.py` | 只用標準函式庫的發佈輔助腳本 | dev 通道(PyPI 的 `je_auto_control_dev`)的版本與比對:`prepare` 把 `dev.toml` 寫成 `pyproject.toml`,版本取 PyPI 最新一版加一個 patch(`dev.toml` 的版本只是下限,不回寫 repo);`changed dist` 把建好的 wheel 跟 PyPI 最新一版逐檔比對,內容沒變就不上傳。只給 `dev.yml` 的 `publish-dev` job 與拋棄式的 checkout 用,因為 `prepare` 會蓋掉 `pyproject.toml`。 |
11621162
| `docs/` | Sphinx(`API`/`Eng`/`Zh`/`getting_started`) | Read the Docs 文件。 |
11631163
| `architecture_diagram/` | drawio + png | 既有的架構圖原始檔。 |
1164-
| `test/` | `unit_test/headless`(主要)、`unit_test/flow_control`、`integrated_test`、`gui_test`、`manual_test`、`verify`、`test_source` | 836 個 `test_*.py`/8,743 個測試函式。**注意**:`test/unit_test/` 下的 `*_test.py` 是會真的驅動滑鼠鍵盤的手動示範腳本,因此 `pyproject.toml` 把 `python_files` 釘成 `test_*.py`。`unit_test/headless/conftest.py` 有一個 autouse fixture,每個測試結束都沖掉 Qt 排隊中的 `deleteLater()`——不沖會讓殘留的 widget 在後面某個不相干的測試裡被銷毀,曾經整個直譯器 `__fastfail`。`test_doc_counts.py` 守住文件引用的指令/工具/子套件/範例數,`test_doc_line_counts.py` 守住所有行數(`--fix` 可一次重新產生)。 `verify/macos_verify.py` 是在真的 `macos-14` runner 上量測 TCC 到底允許什麼的探針(macOS 是唯一沒有容器可用的支援平台),不被 pytest 收集。 |
1164+
| `test/` | `unit_test/headless`(主要)、`unit_test/flow_control`、`integrated_test`、`gui_test`、`manual_test`、`verify`、`test_source` | 837 個 `test_*.py`/8,748 個測試函式。**注意**:`test/unit_test/` 下的 `*_test.py` 是會真的驅動滑鼠鍵盤的手動示範腳本,因此 `pyproject.toml` 把 `python_files` 釘成 `test_*.py`。`unit_test/headless/conftest.py` 有一個 autouse fixture,每個測試結束都沖掉 Qt 排隊中的 `deleteLater()`——不沖會讓殘留的 widget 在後面某個不相干的測試裡被銷毀,曾經整個直譯器 `__fastfail`。`test_doc_counts.py` 守住文件引用的指令/工具/子套件/範例數,`test_doc_line_counts.py` 守住所有行數(`--fix` 可一次重新產生)。 `verify/macos_verify.py` 是在真的 `macos-14` runner 上量測 TCC 到底允許什麼的探針(macOS 是唯一沒有容器可用的支援平台),不被 pytest 收集。 |
11651165

11661166
---
11671167

@@ -1255,7 +1255,7 @@ socket 預設綁 `127.0.0.1`;資源一律用 `with`。
12551255
| `utils/rest_api/` | 8 | 2,064 |
12561256
| `utils/agent/` | 9 | 2,142 |
12571257
| `linux_with_x11/` | 19 | 1,281 |
1258-
| `linux_wayland/` | 18 | 3,138 |
1258+
| `linux_wayland/` | 18 | 3,139 |
12591259
| `utils/triggers/` | 4 | 1,401 |
12601260
| `utils/ocr/` | 10 | 1,381 |
12611261
| `utils/usbip/` | 5 | 1,008 |
@@ -1264,5 +1264,5 @@ socket 預設綁 `127.0.0.1`;資源一律用 `with`。
12641264
| `autocontrol-lsp/` | 8 | 744 |
12651265
| `utils/hotkey/` | 7 | 861 |
12661266
| 其餘模組(約 286 個 `utils/` 子套件 + `android/`/`ios/`/周邊小工具) | 794 | 71,242 |
1267-
| **總計** | **1,208** | **181,098** |
1267+
| **總計** | **1,208** | **181,099** |
12681268

‎docker/eis_verify.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -501,18 +501,19 @@ def _check_libei_drops_out_of_region_motion(backend, server, libei) -> str:
501501

502502

503503
def _check_out_of_region_move_is_refused(backend, server, libei) -> str:
504-
"""AutoControl turns that silence into something the CLI path can act on."""
504+
"""AutoControl refuses an invalid native coordinate without selecting another provider."""
505505
before = len(server.recording.absolute_motions)
506506
try:
507507
backend.set_position(9000, 9000)
508508
except libei.LibeiUnavailable as error:
509509
_require("outside every region" in str(error),
510510
f"refused, but with an unhelpful message: {error}")
511+
_require(error.state == 'failed' and not error.status.fallback_allowed,
512+
"a region refusal was misclassified as optional backend absence")
511513
time.sleep(0.3)
512514
_require(len(server.recording.absolute_motions) == before,
513515
"the refusal still put a motion on the wire")
514-
return f"refused with {str(error)[:60]}... — _select_input.emitted "\
515-
"hands it to ydotool"
516+
return f"refused with {str(error)[:60]}... — native request stopped without CLI fallback"
516517
raise AssertionError(
517518
"set_position(9000, 9000) returned as though the pointer had moved; "
518519
"libei dropped it and nobody was told")

‎docs/API_LIFECYCLE.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Public API lifecycle
22

3+
A native Wayland pointer coordinate outside every granted EIS region, after
4+
raw-coordinate and layout-origin mapping, raises `LibeiUnavailable` with
5+
`state="failed"` and disables automatic CLI fallback. A rejected move emits
6+
no motion/frame or subsequent click. Valid coordinates, devices without
7+
regions, later valid requests on the same sender and explicit CLI selection
8+
retain their existing behavior.
9+
10+
This correction adds no public names or signatures and retains the existing
11+
exception family. It does not revoke an otherwise valid sender or its grant.
12+
313
The additive public capability API is BackendContext, CapabilityStatus,
414
CapabilitySnapshot and probe_capabilities(context), exported from the root
515
and versioned je_auto_control.api/core namespaces. Contexts are detached and

‎docs/CAPABILITY_MATRIX.md‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,14 @@ all-thread native backtraces through batch GDB; other platforms retain Python
7979
faulthandler output. This is diagnostic infrastructure, not native lifecycle
8080
acceptance or a root-cause correction; outstanding crashes remain in Progress.md.
8181

82+
Controlled region-authority checks cover automatic and native input selection,
83+
exclusive region edges, gaps, raw-first/origin mapping, no-region devices,
84+
same-sender retry, explicit CLI and original direct/wrapped control signals.
85+
A refused native coordinate is failed and emits neither CLI input nor native
86+
motion/frame/click. These are fake-backend checks. The real-EIS verifier now
87+
requires the same failure contract, but was not executed for this change;
88+
physical and multi-monitor acceptance remain deferred.
89+
8290
Passive capabilities are available through both SDK namespaces, one AC
8391
command, one read-only MCP tool, Builder and explicit Diagnostics Actions.
8492
Controlled checks cover independent input/capture prerequisites, denied/
@@ -329,10 +337,12 @@ where the negative-origin layout above reaches the input half: a region's
329337
offset is part of the coordinate rather than something to subtract, and a
330338
motion landing outside every region is dropped by libei without a return code,
331339
an event or an error — so the sender maps the point into region space and
332-
refuses what no region covers, which is what lets the `ydotool` path take it.
340+
refuses what no region covers. That refusal now carries failed state and stops
341+
implicit CLI fallback; the updated real-EIS assertion still needs a native run.
333342

334-
The `ydotool-verification` job covers the CLI fallback, which the `libei` path
335-
drops to at every failure point. A seat is what makes an injected event arrive
343+
The `ydotool-verification` job covers the separate CLI provider. Automatic
344+
selection permits it only for optional native absence; native authority,
345+
device and region refusals stop the request. A seat makes an injected event arrive
336346
somewhere; it is not what makes one observable, so no compositor is needed:
337347
`ydotoold` creates an ordinary uinput device, the kernel publishes it as
338348
`/dev/input/eventN`, and the job reads the `input_event` structs back off it.

0 commit comments

Comments
 (0)