Skip to content

Commit c26399e

Browse files
committed
Preserve bounded EI teardown evidence on dev
1 parent ea86459 commit c26399e

18 files changed

Lines changed: 442 additions & 51 deletions

File tree

‎.github/workflows/ei-lifecycle.yml‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
name: AutoControl EI lifecycle evidence
2+
3+
on:
4+
push:
5+
branches: ["dev"]
6+
paths:
7+
- "je_auto_control/linux_wayland/**"
8+
- "docker/Dockerfile.eis"
9+
- "docker/libei_verify.py"
10+
- "docker/eis_verify.py"
11+
- "docker/eis_server.py"
12+
- "pyproject.toml"
13+
- ".github/workflows/ei-lifecycle.yml"
14+
- "test/unit_test/headless/test_ei_native_verification.py"
15+
workflow_dispatch:
16+
17+
permissions:
18+
contents: read
19+
20+
concurrency:
21+
group: ei-lifecycle-${{ github.ref }}
22+
cancel-in-progress: true
23+
24+
jobs:
25+
ei-lifecycle:
26+
runs-on: ubuntu-22.04
27+
timeout-minutes: 30
28+
steps:
29+
- uses: actions/checkout@v5
30+
with:
31+
persist-credentials: false
32+
33+
- name: Prepare evidence directory
34+
run: mkdir -p native-ei
35+
36+
- name: Set up Docker Buildx
37+
# nosemgrep: yaml.github-actions.security.third-party-action-not-pinned-to-commit-sha.third-party-action-not-pinned-to-commit-sha
38+
uses: docker/setup-buildx-action@v4 # NOSONAR githubactions:S7637
39+
40+
- name: Build the isolated EI image
41+
id: build
42+
# nosemgrep: yaml.github-actions.security.third-party-action-not-pinned-to-commit-sha.third-party-action-not-pinned-to-commit-sha
43+
uses: docker/build-push-action@v7 # NOSONAR githubactions:S7637
44+
with:
45+
context: .
46+
file: docker/Dockerfile.eis
47+
tags: autocontrol-ei-lifecycle:ci
48+
load: true
49+
cache-from: type=gha,scope=ei-lifecycle
50+
cache-to: type=gha,scope=ei-lifecycle,mode=max
51+
52+
- name: Record native versions
53+
shell: bash
54+
run: |
55+
docker run --rm --entrypoint sh autocontrol-ei-lifecycle:ci \
56+
-c 'python -VV && dpkg-query -W libei1 libeis1' 2>&1 | tee native-ei/versions.log
57+
58+
- name: Verify half-open teardown and native refusal
59+
if: always() && steps.build.outcome == 'success'
60+
timeout-minutes: 5
61+
shell: bash
62+
run: |
63+
docker run --rm --name ac-libei-probe --stop-timeout 5 --entrypoint python \
64+
autocontrol-ei-lifecycle:ci /opt/verify/libei_verify.py 2>&1 | tee native-ei/libei.log
65+
66+
- name: Verify the live EIS peer independently
67+
if: always() && steps.build.outcome == 'success'
68+
timeout-minutes: 5
69+
shell: bash
70+
run: |
71+
docker run --rm --name ac-eis-peer --stop-timeout 5 --entrypoint python \
72+
autocontrol-ei-lifecycle:ci /opt/verify/eis_verify.py 2>&1 | tee native-ei/eis.log
73+
74+
- name: Remove owned verification containers
75+
if: always()
76+
shell: bash
77+
run: |
78+
for name in ac-libei-probe ac-eis-peer; do
79+
if docker container inspect "$name" >/dev/null 2>&1; then
80+
docker rm -f "$name"
81+
fi
82+
done
83+
84+
- name: Retain native EI evidence
85+
if: always()
86+
uses: actions/upload-artifact@v4
87+
with:
88+
name: ei-lifecycle-${{ github.run_id }}-${{ github.run_attempt }}
89+
path: native-ei/
90+
retention-days: 14

‎CHANGELOG.md‎

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

1414
## Unreleased
1515

16+
- Collect isolated native EI lifecycle evidence on selected dev pushes. Bound
17+
the half-open teardown child, retain fault traces and descriptor counts,
18+
reject unrelated probe failures, and verify native refusal separately from
19+
explicit CLI selection. Package runtime behavior is unchanged.
20+
1621
- Run Diagnostics through the shared owner-bound controller with localized
1722
progress/stop/retry states and no late updates after close. Add scoped
1823
cancellation primitives to both SDK namespaces and the previously absent

‎Progress.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
`WIP` — 改版仍缺以下內容;原生平台與正式環境驗收缺口另列於後。
1616
計畫 D1 仍缺 portal session 關閉/恢復、compositor 重啟的狀態與原生驗收、
1717
擷取結果與擁有者 context 的接合,以及原生 session 的重試/取消/停止控制。
18-
D2 的原生 EI 釋放驗證與 D3 的錄製/全域停止/Windows 原生崩潰證據仍缺,詳見計畫與下方生命週期待辦。
18+
D2 仍缺 `ei-lifecycle.yml` 原生結果確認與據此判斷 helper 隔離;D3 的錄製/全域停止/Windows 原生崩潰證據仍缺,詳見計畫與下方生命週期待辦。
1919
計畫 E(Android/iOS)、G(MCP 逐步揭露)、H(型別深化與總驗收)尚未開始。F 還缺:
2020

2121
- **Remote Desktop 與 Script Builder 仍在啟動時建立**(預設開啟),啟動時間 2.6–2.7 秒裡大半是它們與門面匯入。

‎README.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,15 @@ Literal argv (including whitespace/quotes) and handled `SIGINT` reach the
657657
child unchanged. Linux first verifies the runner outside the suite's debugger;
658658
its preflight logs are included in the same artifact.
659659

660+
Selected dev pushes now run `.github/workflows/ei-lifecycle.yml` against an
661+
isolated libei/libeis container. It runs the silent-peer teardown/refusal probe
662+
and live EIS peer independently, retaining available version/native logs in
663+
`ei-lifecycle-<run>-<attempt>` artifacts for 14 days. The teardown child has a
664+
10-second deadline and faulthandler; only SIGSEGV reached inside `ei_unref` is
665+
classified as the known upstream fault. A successful cleanup must report equal
666+
descriptor counts. Other failures fail CI. No compositor, device or consent
667+
dialog is involved; results must be inspected before changing native cleanup.
668+
660669
Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) and
661670
[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). Two rules the CI enforces: `import
662671
je_auto_control` must never pull in PySide6, and every feature needs both a headless

‎README/README_zh-CN.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,13 @@ Linux 安装 GDB 后,可在 `--` 前加入 `--native`。
576576
含空格/引号的 argv 仍是原始参数,已处理的 `SIGINT` 会传给子进程。
577577
Linux 先在完整套件的 debugger 外验证诊断工具;前置检查记录也收在同一 artifact。
578578

579+
选定的 dev 推送会由 `.github/workflows/ei-lifecycle.yml` 在隔离的 libei/libeis
580+
容器验证静默 peer 的释放/拒绝与独立的 live EIS peer,将可取得的版本/原生记录保留于
581+
`ei-lifecycle-<run>-<attempt>` artifact 十四天。释放子进程有十秒时限与 faulthandler;
582+
只有已进入 `ei_unref` 的 SIGSEGV 才分类为既有上游故障。释放成功必须有相同 fd 数量的
583+
证据,其余失败仍使 CI 失败。此流程不涉及合成器、设备或授权对话框;修改原生清理前仍须
584+
检查实际结果。
585+
579586
欢迎贡献——请见 [CONTRIBUTING.md](../CONTRIBUTING.md) 与
580587
[CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md)。CI 会强制两条规则:`import je_auto_control`
581588
绝不能加载 PySide6;每个功能都必须同时具备无头 API 与 GUI 界面。

‎README/README_zh-TW.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,13 @@ Linux 安裝 GDB 後,可在 `--` 前加入 `--native`。
576576
含空白/引號的 argv 仍是原始參數,已處理的 `SIGINT` 會傳給子行程。
577577
Linux 先在完整套件的 debugger 外驗證診斷工具;前置檢查紀錄也收在同一 artifact。
578578

579+
選定的 dev 推送會由 `.github/workflows/ei-lifecycle.yml` 在隔離的 libei/libeis
580+
容器驗證靜默 peer 的釋放/拒絕與獨立的 live EIS peer,將可取得的版本/原生紀錄保留於
581+
`ei-lifecycle-<run>-<attempt>` artifact 十四天。釋放子行程有十秒時限與 faulthandler;
582+
只有已進入 `ei_unref` 的 SIGSEGV 才分類為既有上游故障。釋放成功必須有相同 fd 數量的
583+
證據,其餘失敗仍使 CI 失敗。此流程不涉及合成器、裝置或授權對話框;修改原生清理前仍須
584+
檢查實際結果。
585+
579586
歡迎貢獻——請見 [CONTRIBUTING.md](../CONTRIBUTING.md) 與
580587
[CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md)。CI 會強制兩條規則:`import je_auto_control`
581588
絕不能載入 PySide6;每個功能都必須同時具備無頭 API 與 GUI 介面。

‎architecture.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,15 @@ the runner before tracing the suite. Native aborts retain all-thread
8282
backtraces and a failing exit. Dev/main upload diagnostics on success or failure.
8383
It does not import the framework, open devices or change SDK/GUI behavior.
8484

85+
Selected dev pushes now run `.github/workflows/ei-lifecycle.yml` against an
86+
isolated libei/libeis container. It runs the silent-peer teardown/refusal probe
87+
and live EIS peer independently, retaining available version/native logs in
88+
`ei-lifecycle-<run>-<attempt>` artifacts for 14 days. The teardown child has a
89+
10-second deadline and faulthandler; only SIGSEGV reached inside `ei_unref` is
90+
classified as the known upstream fault. A successful cleanup must report equal
91+
descriptor counts. Other failures fail CI. No compositor, device or consent
92+
dialog is involved; results must be inspected before changing native cleanup.
93+
8594
Wayland input uses Qt-free `_input_state` metadata to distinguish optional
8695
absence from authority/protocol refusal through portal, libei cache, selector
8796
and keyboard/mouse layers. Refusal caches hold no traceback or native handle.

‎architecture_explore.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ iOS(WebDriverAgent)。核心能力是滑鼠/鍵盤控制、影像辨識、
2626
| 套件門面 `__all__` 公開名稱數 | 1380 |
2727
| GUI 分頁數(`main_widget` 註冊) | 49 |
2828
| MCP 工具數(`build_default_tool_registry()` 實測) | 711 |
29-
| `test_*.py` 測試檔/測試函式 | 845 / 8,810 |
29+
| `test_*.py` 測試檔/測試函式 | 846 / 8,820 |
3030
| 範例腳本 | 43 |
3131

3232
**技術基線**:Python ≥ 3.10、MIT 授權、必要相依只有 `je_open_cv`/`opencv-python`/`pillow`/`mss`/
@@ -1156,14 +1156,14 @@ GUI 是**選用 extra**(`pip install je_auto_control[gui]`,只需要 PySide6
11561156
| `benchmarks/core_latency.py` | 32 行 | 對穩定無頭進入點的可重複煙霧基準測試。 |
11571157
| `examples/` | 43 個腳本 | 從截圖點擊、OCR、排程、遠端桌面、agent loop、可觀測性,一路到 computer-use、Wayland、跨主機 DAG、chatops、pytest/BDD、anchor locator、因果 peer 控制。 |
11581158
| `browser-extension/` | manifest v3 擴充 | 瀏覽器端配合元件(background/content script/popup)。 |
1159-
| `docker/` | Dockerfile ×8 + compose + 9 支驗證/伺服器腳本 | 無頭容器(`Dockerfile`)、帶 XFCE 桌面的容器(`Dockerfile.xfce`),以及四個**驗證用**映像:`Dockerfile.wayland`(sway headless,擷取路徑 + `libei_verify.py` 對真的 libei.so 解析符號)、`Dockerfile.eis`(`eis_server.py` 用 ctypes 綁 libeis 起一個真的 EIS server,`eis_verify.py` 把 libei sender 對著它跑完整握手與發送)、`Dockerfile.portal`(`portal_server.py` 自己佔住 `org.freedesktop.portal.Desktop`,真的 `dbus-daemon` + 真的 liboeffis 跑完 RemoteDesktop 交握)、`Dockerfile.ydotool`(真的 uinput 裝置,`ydotool_verify.py` 直接讀回 `/dev/input/eventN`)、`Dockerfile.seat`(`headless,libinput` + builtin seat,合成器真的吃下 ydotool 裝置,`seat_verify.py` 從 `grim -c` 的像素讀回游標落點)、`Dockerfile.x11`(真的 Xvfb + openbox,`x11_verify.py` 用 `xev` 把注入的事件從真的客戶端讀回(含 `synthetic NO`,這是 XTest 跟 `XSendEvent` 的差別),另用 ImageMagick `import` 做獨立擷取對照,跑兩種螢幕版面)。全部接在 `.github/workflows/docker.yml`。 |
1159+
| `docker/` | Dockerfile ×8 + compose + 9 支驗證/伺服器腳本 | 無頭容器(`Dockerfile`)、帶 XFCE 桌面的容器(`Dockerfile.xfce`),以及四個**驗證用**映像:`Dockerfile.wayland`(sway headless,擷取路徑 + `libei_verify.py` 對真的 libei.so 解析符號)、`Dockerfile.eis`(`eis_server.py` 用 ctypes 綁 libeis 起一個真的 EIS server,`eis_verify.py` 把 libei sender 對著它跑完整握手與發送)、`Dockerfile.portal`(`portal_server.py` 自己佔住 `org.freedesktop.portal.Desktop`,真的 `dbus-daemon` + 真的 liboeffis 跑完 RemoteDesktop 交握)、`Dockerfile.ydotool`(真的 uinput 裝置,`ydotool_verify.py` 直接讀回 `/dev/input/eventN`)、`Dockerfile.seat`(`headless,libinput` + builtin seat,合成器真的吃下 ydotool 裝置,`seat_verify.py` 從 `grim -c` 的像素讀回游標落點)、`Dockerfile.x11`(真的 Xvfb + openbox,`x11_verify.py` 用 `xev` 把注入的事件從真的客戶端讀回(含 `synthetic NO`,這是 XTest 跟 `XSendEvent` 的差別),另用 ImageMagick `import` 做獨立擷取對照,跑兩種螢幕版面)。全部接在 `.github/workflows/docker.yml`;`ei-lifecycle.yml` 另在 dev 以同一 EIS 映像分別收集半開釋放/拒絕與 live peer 證據。 |
11601160
| `k8s/helm/` | Helm chart | Kubernetes 部署。 |
11611161
| `ci_templates/.gitlab-ci.yml` | — | 供使用者專案複製的 GitLab CI 範本。 |
11621162
| `scripts/ci_native_diagnostics.py` | repo 專用 CI 診斷腳本 | 合併並串流子行程輸出、記錄套件/執行環境版本與退出碼;Linux 經停用使用者初始化/shell 啟動的 batch GDB 保留各執行緒原生堆疊,以安全暫存啟動器 exec JSON argv 避開舊 GDB 空白解析;SIGINT 傳入子行程,失敗退出不轉成功。Linux 先做獨立前置檢查,dev/main 無頭工作一律上傳七天診斷 artifact;不匯入框架或請求裝置權限。 |
11631163
| `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`。 |
11641164
| `docs/` | Sphinx(`API`/`Eng`/`Zh`/`getting_started`) | Read the Docs 文件。 |
11651165
| `architecture_diagram/` | drawio + png | 既有的架構圖原始檔。 |
1166-
| `test/` | `unit_test/headless`(主要)、`unit_test/flow_control`、`integrated_test`、`gui_test`、`manual_test`、`verify`、`test_source` | 845 個 `test_*.py`/8,810 個測試函式。**注意**:`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 收集。 |
1166+
| `test/` | `unit_test/headless`(主要)、`unit_test/flow_control`、`integrated_test`、`gui_test`、`manual_test`、`verify`、`test_source` | 846 個 `test_*.py`/8,820 個測試函式。**注意**:`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 收集。 |
11671167

11681168
---
11691169

@@ -1201,6 +1201,7 @@ GUI 是**選用 extra**(`pip install je_auto_control[gui]`,只需要 PySide6
12011201
| `quality.yml` | ruff、bandit、dependency review、九格矩陣的 headless pytest(含 coverage 地板)與 mypy。 |
12021202
| `platform-smoke.yml` | 跨平台煙霧測試。 |
12031203
| `docker.yml` | 容器映像建置。 |
1204+
| `ei-lifecycle.yml` | 選定 dev 推送的隔離 libei/libeis 驗證;獨立執行有界半開釋放與 live peer,保留版本/原生紀錄十四天,失敗仍失敗,不開啟裝置。 |
12041205
| `action-json-lint.yml` | 用 `python -m je_auto_control.utils.action_lint` 檢查 action JSON。 |
12051206

12061207
**設定基線**(`pyproject.toml`):

‎docker/Dockerfile.eis‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ RUN pip install --no-cache-dir --only-binary :all: --upgrade "pip==26.0.1" \
7272

7373
COPY docker/eis_server.py /opt/verify/eis_server.py
7474
COPY docker/eis_verify.py /opt/verify/eis_verify.py
75+
COPY docker/libei_verify.py /opt/verify/libei_verify.py
7576

7677
# Runs as root on purpose: one verification, then exit. Not a service image.
7778
ENV PYTHONUNBUFFERED=1 \

0 commit comments

Comments
 (0)