diff --git a/CHANGELOG.md b/CHANGELOG.md index d7845799..02ba21d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -238,6 +238,23 @@ it shipped into a `## [x.y.z] - date` section of their own; the tag's ### Changed +- RBAC: 29 MCP tools that return stored records or configuration (run history, + costs, traces, heal logs, journals, the USB ACL, lease tokens, mobile + clipboards) now need `read_data` instead of `read_screen`. The built-in + `viewer` role can no longer call them; `operator` and `admin` are unaffected. +- MCP plug-ins: a callable with `mcp_read_only = True` is offered on a + read-only server and to `viewer`. The declaration is trusted, not verified. +- ChatOps: `/run` is ended by a stop-all (`AC_stop_execution` without a run id, + the GUI's stop-all, MCP `ac_stop_execution`); `/stop [run-id]` is new. +- Config sync: a device's first sync against a non-empty bucket no longer + commits a revision or adds itself to `peers`. A device that has only read the + bucket and first sends an edit more than 30 days after the entry was deleted + elsewhere re-creates it without a conflict. +- Logging: a command's result is masked before it is logged, so fields named + `token`, `password`, `secret`, `api_key` and the like read `***` in the + `execute: [...] -> ...` line. The returned record is unchanged. +- Script Builder: the result of `AC_jwt_encode` is masked like the user-token + commands; read it through **One-time values**. - Stop: inside a stoppable run the polling waits (`AC_wait_window`, `AC_wait_text`, the smart waits, `AC_expect_poll`, `AC_assert_eventually`, `AC_wait_actionable` and others) end with `ExecutionStopped` as soon as a diff --git a/CLAUDE.md b/CLAUDE.md index b49796a7..295991e0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -42,7 +42,9 @@ executed: measured, that was 11,962 statements and ~24 percentage points module `je_auto_control_pytest`, which imports only pytest (`test_pytest_entrypoint_light.py` keeps it so), but `coverage run` remains the rule: it does not depend on what a plugin imports or on which build is -installed, and `pytest --cov` has not been re-measured since. +installed. Re-measured on 2026-10-09 the two agreed (87.35% against 87.32%), +and the maintainer decided the same day to keep `coverage run` as the only +spelling. `test/unit_test/headless/test_coverage_measurement.py` holds CI to the correct spelling. After changing the entry point, reinstall (`pip install -e .`) for it to take effect. diff --git a/Progress.md b/Progress.md index faa458e5..81f1fce0 100644 --- a/Progress.md +++ b/Progress.md @@ -2,7 +2,7 @@ ## 跨平台與 GUI 全面改版 -`WIP` — 計畫 A–H 的程式都已交付(A 在 U-20261009-01…14,B–H 在 U-20261009-15…26,留下的缺口在 U-20261009-27…37);還沒做到的是**實機驗證**、要維護者決定的幾件事,與下面各節列的少數缺口 +`WIP` — 計畫 A–H 的程式都已交付(A 在 U-20261009-01…14,B–H 在 U-20261009-15…26,留下的缺口在 U-20261009-27…38);還沒做到的是**實機驗證**、要維護者決定的幾件事,與下面各節列的少數缺口 核准設計:[跨平台自動化與 GUI 改版](docs/superpowers/specs/2026-10-02-platform-gui-modernization-design.md)。 實作計畫:[分階段交付計畫](docs/superpowers/plans/2026-10-02-modernization-index.md)。 @@ -226,15 +226,14 @@ U-20261009-06 與 -04 的 macOS 部分是在 Windows 上對假的 pyobjc/Quart ## 安全與伺服器:留下的缺口 -`TODO` — U-20261009-29、-30、-34、-35 之後還開著的 +`TODO` — U-20261009-29、-30、-34、-35、-37 之後還開著的 **要維護者過目或決定的:** -- `utils/rbac/policy.py` 的 `DATA_TOOLS`(42 個需要 `read_data` 的工具)是實作時分的類;刻意留在 `read_screen` 的有 - `ac_list_run_history`、`ac_costs_*`、`ac_trace_export`、`ac_self_heal_log_list`、`ac_usb_acl_list`、`ac_vlm_locate`/`ac_self_heal_locate`(會花 VLM 費用)。 -- Script Builder 現在永遠不顯示 `AC_user_add`/`AC_user_rotate_token` 回傳的 token(要從 Users 群組、CLI 或腳本取得)。 -- 唯讀模式現在在三種工具模式都強制執行,而外掛工具一律被登記成會變更的:**唯讀的 MCP server 不會執行任何外掛工具**。要不要讓外掛宣告自己唯讀。 -- 設定同步:一台裝置加入「已經有內容」的 bucket 時仍會多提交一個 revision(這個登記是之後的刪除會等它的依據;要拿掉是 `merge.awaits_ack` 的一行,代價是那個保證)。 +- **`DECIDE`:REST 的 `GET /history` 與 `GET /usb/acl` 仍只要 `read_screen`**(`REST_ROUTE_CAPABILITIES`;REST 文件寫 viewer 可以用「除了兩個以外的所有 GET」)。 + MCP 那邊同樣的資料已改成要 `read_data`(`DATA_TOOLS` 42 → 71),所以 viewer 的 token 經 MCP 讀不到執行歷史與 USB ACL,經 REST 仍讀得到。 +- **`DECIDE`:`AC_lease_secret`/`AC_lease_active` 回傳的租約 token 在 Script Builder 仍以明文顯示**(後者巢狀在 `leases` 底下,遮蔽與一次性顯示都只看最上層欄位)。 + `AC_rest_api_start`/`AC_rest_api_status` 回傳共用的 REST bearer token,沒有 Script Builder schema,從 raw JSON 檢視執行時明文顯示。 **還沒做的:** @@ -254,6 +253,8 @@ U-20261009-06 與 -04 的 macOS 部分是在 Windows 上對假的 pyobjc/Quart - 沒有做過真的兩台機器同步,也沒有真的多行程 server;伺服器端的測試在本機是略過的(共用環境沒裝 `fastapi`),只在 CI 跑。 - 墓碑:已確認的刪除現在會保留 30 天(`TOMBSTONE_HOLD_S`),沒被列過的機器帶舊資料來時變成衝突。剩下的風險:超過 30 天才第一次同步的機器 仍可能讓項目復活;舊版 client 提交時會丟掉保留中的墓碑並去掉 `deleted_at`,所以保護程度取決於還在提交的最舊 client。沒對真的舊版安裝跑過。 + - 加入 bucket 不再提交 revision 之後變弱的保證:只讀過 bucket 的裝置改了某個項目,而且在該項目於別處被刪除超過 30 天之後才第一次送出那個編輯, + 項目會被重新建立而沒有衝突(以前墓碑會無限期等它)。只讀的裝置不再出現在 `peers`,所以 `max_offline_s` 退役與 `FullResyncRequired` 不會套用到它。 - `ConfigBucket.upsert` 不帶 `origin=` 時現在存的是帶版本的項目,直接讀 `bucket.sections[s][id]["field"]` 的程式會壞(`bucket.values(section)` 兩種形狀都能讀)。 - 沒有自動的剪貼簿轉送,所以 `automatic=True`(`ClipboardEchoGuard.should_send`)在產品裡沒有呼叫端。 - blob:鎖檔超過 30 秒會被視為過期而接管,單次寫入比這更久時仍可能超出配額;用 `publish_assets` 手動上傳的 blob 沒有 bucket 項目引用, @@ -296,20 +297,21 @@ U-20261009-18 的每一個裝置面呼叫都只對假的 ADB/uiautomator2/WD ## GUI:留下的缺口 -`TODO` — U-20261009-27、-28、-34、-36 之後還開著的 +`TODO` — U-20261009-27、-28、-34、-36、-38 之後還開著的 - **Windows UIA 後端的執行緒模型完全沒對真的 COM 驗過**:現在每個呼叫執行緒各有一個自動化物件(`accessibility/backends/windows_automation.py`; 主執行緒 STA、其他執行緒進 MTA),Accessibility 分頁與 A11y Audit 已移出 GUI 執行緒。開發環境沒裝 `comtypes`,CI 也沒有同時有 `comtypes` 與桌面的 job, 測試全是假 COM 物件。未驗證的假設:MTA 工作執行緒上的 UIA 呼叫可用;焦點事件 callback 不需要 message pump; `import comtypes` 第一次發生在已經是 MTA 的執行緒上會丟 `OSError`(憑記憶,沒處理)。`AC_wait_for_focus_change` 的原生 `Queue.get` 仍不會被 stop 叫醒。 +- **Script Builder 的一次性值**:在區塊指令(`AC_loop`、`AC_try`、`AC_if_*`)裡產生的 token 拿不到——區塊只記錄自己的摘要,巢狀指令的結果不在任何紀錄裡(結果畫面會說明並列出指令)。 + 要補需要 executor 的結果掛鉤(`utils/executor/action_executor.py` 的 `_run_one_action` 與 `gui/script_builder/step_model.py::nested_sensitive_commands`)。 + 對話框開過之後結果畫面仍顯示「有值可看」那一行;複製到系統剪貼簿的 token 關閉時不清除;對話框從未實際顯示過。 - **stop 還叫不醒的**:已經進到後端的單一指令(一次影像搜尋、OCR、HTTP 請求、shell 指令)、單一手勢內的節奏 sleep、`AC_wait_for_focus_change`; MCP 自己的輪詢(`ac_wait_for_image`/`ac_wait_for_pixel`)不在可停止的執行裡。非腳本的分頁工作在後端無法取消,只是結果被丟掉。 - ChatOps 的 `/run` 只有在腳本自己含 `AC_run_stoppable` 時才停得掉(刻意沒有整個包起來:那會讓 GUI 的「全部停止」也停掉聊天裡的執行,要維護者決定)。 + ChatOps:`SlackBot.poll_once` 在輪詢執行緒上依序分派,所以同一個 bot 自己的 `/run` 還在跑時貼的 `/stop` 要等它結束才會被讀到(不帶參數的 `/stop` 因此只停 `chatops-` 開頭的執行);沒對真的 Slack workspace 跑過。 新轉成可停止的等待裡,`AC_handle_file_dialog`、`AC_wait_until_app_idle`、`AC_wait_for_composition_commit`、`AC_wait_for_unlock` 與行動裝置的 `wait_for_app` 只對假探針測過。 - **WebRTC**:`StatsPoller.start()` 會在 GUI 執行緒上最多等 2 秒(`future.result(timeout=2.0)`,是啟動路徑,沒動)。LAN 廣播的註冊與取消已移出 GUI 執行緒, 但真的 zeroconf 的時間與名稱衝突行為只對假物件測過。 -- **`DECIDE`:釋放 Remote Desktop 分頁時,WebRTC 的 host 與 viewer session 現在會被結束**(TCP/WebSocket 的仍留在 registry)。理由:WebRTC host 只屬於面板, - 面板釋放後沒有東西能停它,會在看不到的情況下繼續分享畫面。要改回去是拿掉兩個 `dispose()` 裡的 `self._stop_host_if_any` 與 `self._end_session_for_good`。 - **切換主題後那幾個事件迴圈回合內**,以被暫存的頁面為 parent 的 `QMessageBox` 會相對於隱藏的暫存 widget 定位(只影響位置)。 系統匣路徑在 offscreen 下沒有 tray,`_on_tray_open` 是直接呼叫測的。基準(`benchmarks/gui_*.py`)CI 仍不設門檻。 - **把 Python 建的 widget 放進分頁列在結束時崩潰**:已查明觸發條件——分頁列按鈕上接了 Python callable、又有排隊中的 `deleteLater()`、而行程經 `os._exit` 離開; @@ -326,8 +328,6 @@ U-20261009-18 的每一個裝置面呼叫都只對假的 ADB/uiautomator2/WD `TODO` — 小項目,各自獨立 -- **`DECIDE`:`pytest --cov` 可以放行了嗎**。進入點搬到 `je_auto_control_pytest` 之後重新量過(2026-10-09):`coverage run -m pytest` 87.35%,`pytest --cov` 87.32%。 - `CLAUDE.md` 與 `test_coverage_measurement.py` 仍規定只能用 `coverage run`;要不要放寬由維護者決定。 - **本機的共用 `.venv` 裝著 `je_auto_control_dev 0.0.136`**,它的 `pytest11` 進入點還是舊的重量級路徑;重裝即可,不影響 CI。 - **free-threaded**:呼叫者身分、綁定的裝置、自愈的執行標記與 stop token 都改成只在設定它的執行緒上有效(`utils/thread_bound.py`)。 這台機器沒有 free-threaded 直譯器:驗證用的是模擬繼承與真的 `-X thread_inherit_context=1`(3.14.7,GIL 開著);`free-threaded-scope` job(3.14t)是第一次真的跑。 diff --git a/architecture_explore.md b/architecture_explore.md index 5f001852..8a889ea2 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -6,7 +6,7 @@ > 擷取每個模組的 docstring 與頂層公開名稱;統計數字取自實際檔案,非估算。 > 指令數與公開 API 數以 `executor.known_commands()` 與 `je_auto_control.__all__` 在工作樹上實測取得。 > -> **掃描時間**:2026-10-09 **版本**:`pyproject.toml` version `0.0.230` **分支**:`feat/progress-sweep-4` +> **掃描時間**:2026-10-09 **版本**:`pyproject.toml` version `0.0.232` **分支**:`feat/maintainer-decisions` --- @@ -19,8 +19,8 @@ iOS(WebDriverAgent)。核心能力是滑鼠/鍵盤控制、影像辨識、 | 指標 | 數值 | | --- | ---: | -| Python 模組總數(含周邊子專案) | 1,175 | -| 程式碼總行數 | 187,645 | +| Python 模組總數(含周邊子專案) | 1,176 | +| 程式碼總行數 | 188,115 | | `je_auto_control/utils/` 子套件數 | 311 | | `AC_*` 動作指令數(`known_commands()` 實測) | 852 | | 套件門面 `__all__` 公開名稱數 | 1,244 | @@ -272,7 +272,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 ### 5.4.1 執行引擎與腳本資產 -> 25 個套件、約 18,209 行。 +> 25 個套件、約 18,210 行。 | 模組 | 行數 | 職責 | | --- | ---: | --- | @@ -283,7 +283,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `utils/dag/` | 564 | 跨主機 DAG 編排器(圖模型 + runner) | | `utils/decision_table/` | 112 | DMN 風格決策表:規則 + 命中策略,把分支外部化 | | `utils/deterministic/` | 116 | 決定性執行控制:固定亂數種子 + 凍結時鐘 | -| `utils/executor/` | 10,368 | **核心**。`Executor` 指令分派表(852 個 `AC_*`)、參數插值、乾跑、逐步 callback;`flow_control` 提供 34 個區塊指令(迴圈/分支/try/巨集/變數) | +| `utils/executor/` | 10,369 | **核心**。`Executor` 指令分派表(852 個 `AC_*`)、參數插值、乾跑、逐步 callback;`flow_control` 提供 34 個區塊指令(迴圈/分支/try/巨集/變數) | | `utils/action_journal/` | 1,001 | 選用的動作日誌:執行邊界記錄 typed 輸入/結果與 parent id,先遮蔽再寫入 JSONL;`codegen.journal_import` 由它產生候選腳本 | | `utils/flow_debugger/` | 166 | action list 的單步除錯器與追蹤器 | | `utils/input_macro/` | 469 | 定時輸入事件:錄製結果的整形(`timeline`/`InputRecorder`,Windows 與 macOS 共用)、重播與宣告式輸入序列 DSL | @@ -495,7 +495,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 ### 5.4.9 AI / Agent / LLM -> 13 個套件、約 25,801 行。 +> 13 個套件、約 25,860 行。 | 模組 | 行數 | 職責 | | --- | ---: | --- | @@ -508,19 +508,19 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `utils/cua_action/` | 204 | 標準化 computer-use 動作結構(Anthropic/OpenAI → `AC_*`) | | `utils/llm/` | 387 | 自然語言 → action list 規劃器 + Anthropic/null 後端 | | `utils/mcp_registry/` | 97 | MCP registry `server.json` 資訊清單產生(可被發現) | -| `utils/mcp_server/` | 21,113 | **無頭 MCP 伺服器**(16K LOC,預設註冊 757 個工具=738 個 `ac_*` + 19 個別名):stdio + HTTP 傳輸、工具工廠與處理器、資源、prompt、稽核、限流、外掛熱重載 | +| `utils/mcp_server/` | 21,172 | **無頭 MCP 伺服器**(16K LOC,預設註冊 757 個工具=738 個 `ac_*` + 19 個別名):stdio + HTTP 傳輸、工具工廠與處理器、資源、prompt、稽核、限流、外掛熱重載 | | `utils/tool_use_schema/` | 195 | 把 `AC_*` 指令匯出成 Claude/OpenAI 的 tool-use schema | | `utils/trajectory_eval/` | 132 | agent 軌跡評估:依評分規準為一次執行打分 | | `utils/vision/` | 580 | VLM 元素定位器(依描述找元素)+ Anthropic/OpenAI/null 後端 | ### 5.4.10 遠端桌面與 USB -> 6 個套件、約 24,954 行。 +> 6 個套件、約 24,961 行。 | 模組 | 行數 | 職責 | | --- | ---: | --- | | `utils/admin/` | 418 | 多主機管理主控台:平行輪詢 N 個 AutoControl REST 端點 | -| `utils/config_sync/` | 4,089 | 透過訊令伺服器做跨機器設定同步 | +| `utils/config_sync/` | 4,096 | 透過訊令伺服器做跨機器設定同步 | | `utils/device_matrix/` | 203 | 行動裝置矩陣:同一 action list 於多台裝置平行執行 | | `utils/remote_desktop/` | 14,179 | **遠端桌面子系統**(56 檔/11.7K LOC):TCP/WebSocket/WebRTC 三條傳輸路徑、主機與檢視端、訊令伺服器、TURN/中繼、多檢視者、錄影、信任清單、TOTP、稽核鏈 | | `utils/usb/` | 5,057 | 跨平台 USB 列舉/熱插拔/裝置直通(WinUSB、IOKit、libusb 後端 + ACL + WebRTC DataChannel 通道) | @@ -535,15 +535,16 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 > **2026-10-09 第四輪新增的模組**:`utils/thread_bound.py`(只在設定它的執行緒上有效的 context 變數,RBAC 身分、裝置綁定、自愈標記與 stop token 共用)、 > `utils/accessibility/backends/windows_automation.py`(每個執行緒的 COM apartment 與 UIAutomation 物件)、 > `gui/_weak_call.py`(弱參照持有的工作回呼)、`gui/_qt_typed.py`(對真實 Qt 型別檢查用的小工具)。 +> 之後新增:`gui/script_builder/one_time_dialog.py`(Script Builder 的一次性值對話框,值只留在記憶體)。 ### 5.4.11 伺服器、網路協定與外部整合 -> 24 個套件、約 7,165 行。 +> 24 個套件、約 7,241 行。 | 模組 | 行數 | 職責 | | --- | ---: | --- | | `utils/acme_v2/` | 617 | 完整 ACME v2 用戶端(RFC 8555),不依賴 certbot | -| `utils/chatops/` | 682 | Chat-ops bot:接收 Slack/Discord/webhook 的 slash 指令並路由到動作 | +| `utils/chatops/` | 758 | Chat-ops bot:接收 Slack/Discord/webhook 的 slash 指令並路由到動作 | | `utils/cookie_jar/` | 191 | RFC 6265 cookie jar | | `utils/email_send/` | 118 | SMTP 寄信(email 觸發器的發送端搭檔) | | `utils/events/` | 106 | 對外 CloudEvents 發送(執行生命週期事件) | @@ -641,7 +642,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 ### 5.4.14 安全、機密與合規 -> 13 個套件、約 3,907 行。 +> 13 個套件、約 3,931 行。 | 模組 | 行數 | 職責 | | --- | ---: | --- | @@ -650,7 +651,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `utils/governance/` | 242 | 治理:maker-checker 核准閘門與即時憑證租約 | | `utils/license_policy/` | 240 | 以 SBOM 元件評估 SPDX 授權允許/拒絕政策 | | `utils/provenance/` | 126 | SLSA 建置來源證明(in-toto v1) | -| `utils/rbac/` | 1,161 | 角色型存取控制:使用者、角色與權杖驗證(尚未接到 REST/MCP) | +| `utils/rbac/` | 1,185 | 角色型存取控制:使用者、角色與權杖驗證(尚未接到 REST/MCP) | | `utils/redaction/` | 508 | 截圖遮蔽層:規則偵測 + 政策 + 協調器(上傳 VLM 前先遮) | | `utils/sbom/` | 170 | SBOM(CycloneDX)產生 | | `utils/secret_ref/` | 242 | URI scheme 形式的值參照解析 | @@ -707,11 +708,11 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 上表以子套件為單位;以下把行數最大的幾個子系統展開到檔案層。 -#### `utils/executor/`(10,368 行)— 執行核心 +#### `utils/executor/`(10,369 行)— 執行核心 | 檔案 | 行數 | 職責 | | --- | ---: | --- | -| `action_executor.py` | 8,730 | `Executor` 類別與 `event_dict` 分派表(852 個指令),另含數百個把 utils 能力接成指令的 adapter 函式;全域單例 `executor` 與 `add_command_to_executor()` 擴充點。 | +| `action_executor.py` | 8,731 | `Executor` 類別與 `event_dict` 分派表(852 個指令),另含數百個把 utils 能力接成指令的 adapter 函式;全域單例 `executor` 與 `add_command_to_executor()` 擴充點。 | | `flow_control.py` | 693 | 真正的流程控制:`AC_loop`/`AC_for_each`/`AC_while_*`/`AC_if_*`/`AC_try`/`AC_retry`/`AC_parallel`/`AC_define_macro`/`AC_call_macro`/變數指令(`AC_set_var`/`AC_get_var`/`AC_inc_var`)。`LoopBreak`/`LoopContinue` 以例外實作。34 個區塊指令的分派表 `BLOCK_COMMANDS` 也在這裡,含下一列匯入的資料來源指令。 | | `flow_data_commands.py` | 272 | `AC_*_to_var` 資料來源與轉換指令:shell、時鐘、亂數、PDF、TOTP、SQL、檔案、HTTP、OCR,加上 `AC_assert_var`/`AC_assert_db`/`AC_assert_duration`/`AC_transform_var`。都不執行巢狀 action list,所以沒有迴圈/分支語意。 | | `action_schema.py` | 223 | action list 的結構驗證:形狀、參數型別、未知指令拒絕。單一走訪同時支援兩種消費方式:`validate_actions()` 遇到第一個問題就拋、`unknown_command_names()` 收齊全部不認得的名字(REST `/execute` 用它回 400)。 | @@ -719,7 +720,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `mouse_aliases.py` | 39 | 單鍵點擊別名(`AC_click_left` 等),executor 與 callback executor 共用。 | | `flags.py` | 19 | 旗標讀取 `as_bool`:依拼法(`"true"`/`"yes"`/`"on"`/`"1"`)而非 Python 真值判斷,所以 JSON、CLI、MCP 傳來的 `"false"` 是關;所有 adapter 與流程指令共用。 | -#### `utils/mcp_server/`(21,113 行,757 個工具)— 最大子系統 +#### `utils/mcp_server/`(21,172 行,757 個工具)— 最大子系統 | 檔案 | 行數 | 職責 | | --- | ---: | --- | @@ -747,10 +748,10 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。 | `resources.py` | 309 | MCP resource 提供者。 | | `prompts.py` | 220 | MCP prompt 目錄。 | | `fake_backend.py` | 187 | CI/無頭測試用的記憶體內假後端。 | -| `plugin_watcher.py` | 168 | 檔案變更時熱重載外掛工具的背景 watcher。 | +| `plugin_watcher.py` | 169 | 檔案變更時熱重載外掛工具的背景 watcher。 | | `tools/_base.py` | 190 | 工具註冊表的共用型別與輔助。 | | `tools/_validation.py` | 122 | MCP 工具用到的 JSON Schema 子集驗證器。 | -| `tools/plugin_tools.py` | 89 | 把外掛載入的 `AC_*` callable 包成 `MCPTool`。 | +| `tools/plugin_tools.py` | 147 | 把外掛載入的 `AC_*` callable 包成 `MCPTool`。 | | `log_bridge.py` | 118 | 把 Python logging 記錄橋接成 MCP `notifications/message`;2026-07-28 的請求只收到自己設了 `logLevel` 時產生的記錄。 | | `audit.py` | 126 | MCP 工具呼叫稽核記錄。 | | `context.py` | 71 | 傳給 opt-in 工具處理器的每次呼叫上下文。 | @@ -914,7 +915,7 @@ GUI 是**選用 extra**(`pip install je_auto_control[gui]`,PySide6),且 | `_screen_geometry.py` | 55 | Qt 邏輯座標與截圖用的原生像素互轉:`native_region()`、`screen_at_native()`、`logical_point()`(每個螢幕的左上角在兩者相同,螢幕內依 device pixel ratio 縮放)。區域選取與主機端標註覆蓋層都用它。 | | `_daemon_thread.py` | 79 | `DaemonThread`:`QThread` 的替代品,保留遠端桌面 worker 用到的介面(`start`/`run`/`isRunning`/`wait`/`requestInterruption`/`started`/`finished`),但 `run()` 跑在 daemon `threading.Thread` 上,刪除物件或程式結束都不會銷毀執行中的執行緒。 | | `_worker_thread.py` | 266 | `start_worker()`:在 daemon `threading.Thread` 上執行 `QObject` worker 的 `run()`(沒有 `QThread` 可被銷毀),並經由分頁擁有的中繼物件回報結果(回呼一律在 GUI 執行緒;worker 沒處理的例外也送到 `on_fail`);worker 留在模組登錄表直到 GUI 執行緒看到它結束,回傳 `WorkerHandle`(`isRunning()`);程式結束時先呼叫 worker 的 `request_stop()`,最多等 10 秒,仍在跑的隨行程結束。 | -| `language_wrapper/` | 6,055 | 四語系字典(英/日/簡中/繁中)+ `multi_language_wrapper` 執行期切換器與監聽註冊表。 | +| `language_wrapper/` | 6,107 | 四語系字典(英/日/簡中/繁中)+ `multi_language_wrapper` 執行期切換器與監聽註冊表。 | | `selector/` | 216 | 拖曳選取螢幕區域的半透明全螢幕覆蓋層與樣板裁切工具(互動式,但都有對應的程式化 API)。 | > **分頁指令一律走 Actions 選單**:分頁本身只放輸入、表格與結果檢視,指令由視窗層選單暴露。 @@ -1098,10 +1099,10 @@ socket 預設綁 `127.0.0.1`;資源一律用 `with`。 | 層/子系統 | 檔案數 | 行數 | | --- | ---: | ---: | -| `gui/` | 125 | 34,684 | -| `utils/mcp_server/` | 43 | 21,113 | +| `gui/` | 126 | 34,987 | +| `utils/mcp_server/` | 43 | 21,172 | | `utils/remote_desktop/` | 57 | 14,179 | -| `utils/executor/` | 9 | 10,368 | +| `utils/executor/` | 9 | 10,369 | | `utils/usb/` | 20 | 5,057 | | `je_auto_control/`(頂層 3 檔) | 3 | 2,676 | | `utils/accessibility/` | 15 | 3,290 | @@ -1118,6 +1119,6 @@ socket 預設綁 `127.0.0.1`;資源一律用 `with`。 | `osx/` | 17 | 981 | | `autocontrol-lsp/` | 8 | 744 | | `utils/hotkey/` | 7 | 861 | -| 其餘模組(約 286 個 `utils/` 子套件 + `android/`/`ios/`/周邊小工具) | 724 | 68,557 | -| **總計** | **1,163** | **186,805** | +| 其餘模組(約 286 個 `utils/` 子套件 + `android/`/`ios/`/周邊小工具) | 724 | 68,664 | +| **總計** | **1,164** | **187,275** | diff --git a/docs/source/Eng/doc/config_sync/config_sync_doc.rst b/docs/source/Eng/doc/config_sync/config_sync_doc.rst index 0f736b16..6fb302e4 100644 --- a/docs/source/Eng/doc/config_sync/config_sync_doc.rst +++ b/docs/source/Eng/doc/config_sync/config_sync_doc.rst @@ -215,10 +215,27 @@ applies the hold only when ``now`` is given. A tombstone stamped by a release from before this has no ``deleted_at`` and is dropped on acknowledgement as before; such a release also drops acknowledged tombstones without holding them, so the hold is only as good as the oldest client still committing. -A device joins ``peers`` with its first push; ``push_operations([])`` from a -device the bucket does not list is such a push when the bucket holds entries -(their later deletion must wait for it) and writes nothing when it holds -none -- a first sync of an empty account commits no revision. +A device joins ``peers`` with the first change it commits. +``push_operations([])`` from a device the bucket does not list writes +nothing, whether the bucket holds entries or not: a device that only reads a +bucket commits no revision for joining it, and a first sync of an empty +account commits none either. (Earlier a join against a non-empty bucket +wrote one revision so that later deletions would wait for the device.) + +**What an unlisted device still gets, and what it does not.** A deletion made +while the device is unlisted reaches it through the held tombstone: inside +the thirty days its untouched copy is removed, and a copy it edited in the +meantime becomes a conflict. After the hold the tombstone is gone. A copy +the device did not touch is still removed -- its own baseline records that +the entry came from the bucket, so its absence is a deletion made elsewhere. +What is no longer guaranteed is the edited copy: a device that has only ever +read the bucket, edits an entry, and first sends that edit more than +``TOMBSTONE_HOLD_S`` after the entry was deleted elsewhere **re-creates the +entry with no conflict**. With the join revision the tombstone would have +waited for that device indefinitely and the edit would have been a +conflict. The remedy is the same as for a machine that never synced: sync +every machine at least once every thirty days, or have it commit a change +before it goes away. A device that will not come back is retired with ``client.retire_peer(device_id)`` (or automatically with diff --git a/docs/source/Eng/doc/mcp_server/mcp_server_doc.rst b/docs/source/Eng/doc/mcp_server/mcp_server_doc.rst index a570cb62..b83b7e6e 100644 --- a/docs/source/Eng/doc/mcp_server/mcp_server_doc.rst +++ b/docs/source/Eng/doc/mcp_server/mcp_server_doc.rst @@ -337,6 +337,38 @@ bearer token and is never subject to RBAC. ``DATA_TOOLS`` in ``je_auto_control.utils.rbac.policy``. A ``viewer`` keeps every other read-only tool: screen size, windows, pixels, image and text location, accessibility reads, waits. +- The rule behind that list: a tool that returns **records or configuration + the host keeps** needs ``read_data``; a tool that only observes the live + screen needs ``read_screen``. So ``read_data`` also covers run history and + what is computed from it (``ac_list_run_history``, ``ac_flaky_report``, + ``ac_rank_tests``, ``ac_select_tests``, ``ac_shard_suite``); agent traces + and the cost ledger (``ac_trace_export``, ``ac_trace_summary``, + ``ac_costs_list``, ``ac_costs_summary``); action journals + (``ac_journal_read``, ``ac_journal_runs``); the self-healing log, its + statistics, stored revisions and datasets (``ac_self_heal_log_list``, + ``ac_heal_stats``, ``ac_self_heal_revision_list``, + ``ac_self_heal_evaluate``); locator strategy history, repairs and saved + libraries (``ac_ab_report``, ``ac_ab_best_strategy``, ``ac_repair_pending``, + ``ac_repair_resolved``, ``ac_element_list``, ``ac_skill_list``, + ``ac_skill_search``); stored policy and state (``ac_usb_acl_list``, + ``ac_lease_active``, ``ac_quarantine_list``, ``ac_pending_artifacts``, + ``ac_config_sync_status``); and a connected device's clipboard + (``ac_android_get_clipboard``, ``ac_ios_get_clipboard``). These were + ``read_screen`` before, so **a viewer token can no longer call them**; + ``operator`` and ``admin`` are unaffected. +- What a call costs is not a capability. ``ac_vlm_locate`` and + ``ac_self_heal_locate`` read the screen and stay ``read_screen`` although a + call may be billed by a model provider; limit spend with the rate limiter + or by not configuring a VLM backend for a server viewers reach. +- The classification covers MCP tools only. The REST routes ``GET /history`` + and ``GET /usb/acl`` still need ``read_screen``, as the REST table in the + operations chapter says, so a viewer token reads the same run history and + USB ACL there. +- Tools that report the live state of this process rather than a stored + record -- ``ac_list_executions``, ``ac_scheduler_list_jobs``, + ``ac_trigger_list``, ``ac_hotkey_list``, ``ac_observe_list``, + ``ac_watchdog_list``, ``ac_journal_status``, the USB device lists -- stay + ``read_screen``. - ``tools/list`` returns only the tools the caller may call, and ``tools/call`` on any other answers JSON-RPC error ``-32003`` (``Forbidden: ...``, ``data.required_capability``) without running it. @@ -669,11 +701,12 @@ that is not marked read-only and is registered on a running read-only server out of ``tools/list`` and a call to it is answered ``-32602`` and recorded in the audit log as ``denied``. In the default ``full`` mode the filter used to run only when the registry was built, so such a tool was listed and ran. -Plugin tools are registered as destructive, so **a read-only server runs no -plugin tool**; a tool your own code registers with ``readOnlyHint`` true is -still offered. ``MCPServer(tools=[...])`` with the variable set is filtered -the same way -- pass ``read_only=False`` to an embedded server that must -ignore the variable. +Plugin tools are registered as destructive unless the plugin declares +otherwise, so **a read-only server runs no plugin tool that has not declared +itself read-only** (see `Plugin hot-reload`_); a tool your own code registers +with ``readOnlyHint`` true is still offered. ``MCPServer(tools=[...])`` with +the variable set is filtered the same way -- pass ``read_only=False`` to an +embedded server that must ignore the variable. Confining file arguments to root directories ============================================ @@ -882,6 +915,48 @@ Each register / unregister fires ``notifications/tools/list_changed`` so the client refreshes its cached catalogue automatically. +**Declaring a plugin tool read-only.** A plugin tool is registered as +mutating: it is absent from a read-only server and needs ``drive_input`` +under RBAC. A plugin opts one of its callables out by setting the attribute +``mcp_read_only`` to ``True`` on it: + +.. code-block:: python + + def AC_queue_depth(): + """How many jobs are waiting.""" + return {"depth": len(_pending)} + + AC_queue_depth.mcp_read_only = True + +The tool is then registered with ``readOnlyHint`` true. It is listed and +runs on a read-only server in all three tool modes (``full``, +``progressive`` -- once enabled -- and ``static``, when the profile names +it), and under RBAC it needs only ``read_screen``, so a ``viewer`` may call +it. Only the read-only hint is taken from the plugin; the tool is not +marked idempotent. + +.. warning:: + + The declaration is **trusted, not verified**. It is the plugin author's + word: the server cannot tell what arbitrary Python does, and a callable + that declares itself read-only and then writes files or sends input will + do so on a read-only server and for a viewer. Declaring it does not + sandbox it. Review a plugin before giving it a directory a read-only or + viewer-facing server watches. Each time such a tool is registered the + server logs a warning naming it (``plugin tool 'plugin_ac_queue_depth' is + registered read-only because its plugin declared mcp_read_only=True; the + declaration is trusted, not verified``). + +Anything other than the boolean ``True`` is not a declaration: ``"true"``, +``1``, a callable, an attribute that raises. Such a value is logged and the +tool stays mutating, as does a tool with no attribute or with ``False``. +The same rule applies to tools registered through +``register_plugin_tools(server, commands)`` and ``make_plugin_tool``; the +attribute's name is ``PLUGIN_READ_ONLY_ATTRIBUTE`` in +``je_auto_control.utils.mcp_server.tools.plugin_tools``. The declaration +says nothing to the ``AC_*`` executor command of the same name, and the path +roots still do not reach a plugin tool's arguments. + CI smoke tests with the fake backend ==================================== diff --git a/docs/source/Eng/doc/new_features/new_features_doc.rst b/docs/source/Eng/doc/new_features/new_features_doc.rst index 3d3bfec8..ab8d5900 100644 --- a/docs/source/Eng/doc/new_features/new_features_doc.rst +++ b/docs/source/Eng/doc/new_features/new_features_doc.rst @@ -560,8 +560,11 @@ Over MCP the same two are the tools ``ac_stop_execution`` (``run_id``, ``ac_list_executions`` (read-only). There is no separate "run stoppably" tool: ``ac_execute_actions`` and ``ac_execute_action_file`` take any action list, so the list itself carries the ``AC_run_stoppable`` block shown above. A ChatOps -command whose run is stopped replies `` stopped.`` (with the reason -in parentheses) instead of reporting a failure. +``/run`` is a stoppable run by itself, named ``chatops-