Skip to content

Commit 7b997cb

Browse files
committed
Present REST RBAC mode and preserve legacy gates
1 parent 7d47958 commit 7b997cb

19 files changed

Lines changed: 229 additions & 22 deletions

File tree

‎CHANGELOG.md‎

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

1414
## Unreleased
1515

16+
- REST registry status adds `rbac_enabled` and suppresses the inactive shared
17+
token in RBAC mode (`token: null`). The GUI explains user-token authentication
18+
and disables its shared-token field. The Python server's `token` property stays
19+
unchanged. Check-only custom gates work in shared-token mode; an identityless
20+
success cannot bypass a server's configured RBAC policy.
21+
1622
- The GUI tab close button now releases factory-built feature panels and their
1723
destruction-bound resources; reopening creates a fresh panel. View-menu hiding
1824
and `hide_tab` retain state. Explicit `TabRegistry.open/close` and `close_tab`

‎Progress.md‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,9 +210,7 @@ U-20261009-06 與 -04 的 macOS 部分是在 Windows 上對假的 pyobjc/Quart
210210
強制簽章經 socket/REST/MCP 的端到端沒有跑過,覆蓋的是共用的 `read_executable_action_json` 與指令本身。
211211
- **RBAC**(`JE_AUTOCONTROL_RBAC_USERS`,預設關閉):
212212
- 使用者只能用 Python 管(`UserStore.add_user/set_role/rotate_token/remove_user`),沒有 `AC_*`、CLI 或 GUI;`Capability.MANAGE_USERS` 因此沒有任何路由或工具對應。
213-
- `gui/rest_api_tab.py` 仍顯示共用 token,RBAC 開啟後那個 token 會被拒絕。
214213
- viewer 拿得到所有標成唯讀的工具,包括 `ac_sql_query`、`ac_load_dotenv`、`ac_get_clipboard`、`ac_jwt_encode`,沒有重新分類。
215-
- 只實作 `check()` 的自製 REST gate 現在會 `AttributeError`(要有 `authenticate()`)。
216214
- 沒對真的 client(Claude Desktop、VS Code、內建 dashboard)試過;SSE、無狀態與 `subscriptions/listen` 路徑、TLS 加 RBAC 沒有測試。
217215
- **MCP 路徑根目錄**(`JE_AUTOCONTROL_MCP_PATH_ROOTS`,預設關閉)管不到的參數:執行動作清單的工具、
218216
有時才是路徑的參數(`ac_open_path`/`ac_plan_open`/`ac_file_association`/`ac_act_in_view` 的 `target`,`ac_launch_process`/`ac_shell` 的 `argv`/`command`)、

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,8 @@ The MCP window-state tools `ac_window_minimize`, `ac_window_maximize` and `ac_wi
306306

307307
**Opt-in hardening.** Each of these is off until configured, and a server without them behaves as before. `JE_AUTOCONTROL_RBAC_USERS=<user store file>` makes the REST API and the MCP HTTP transport resolve the bearer token to a user and authorise each route, tool and privileged `AC_*` command by role (viewer / operator / admin); the shared token is then refused. `JE_AUTOCONTROL_MCP_PATH_ROOTS` (directories separated by the OS path separator) confines every MCP tool argument that is a file path to those directories, `JE_AUTOCONTROL_MCP_PATH_ROOTS_FROM_CLIENT=1` adds the client's `roots/list`, and `JE_AUTOCONTROL_MCP_ENV_REF_ALLOW` limits which `env://` names `ac_resolve_ref` may read. `JE_AUTOCONTROL_ACTION_SIGNING_PUBLIC_KEY` makes an endpoint verify Ed25519-signed action files without being able to sign them (`create_signing_keypair`; the private key stays on the signing machine). A remote-desktop viewer writes files pushed by a host only below `~/Downloads/AutoControl` (`JE_AUTOCONTROL_REMOTE_DOWNLOAD_DIR`); that one is on by default.
308308

309+
The REST GUI shows RBAC mode and disables the shared-token field while a user store is active. `rest_api_registry.status()` includes `rbac_enabled` and returns `token: null` in that mode; use an individual user token. Custom REST gates that only implement `check()` remain supported in shared-token mode; RBAC requires an authenticated user context and refuses an identityless success.
310+
309311
Delayed interval/cron jobs, triggers, hotkeys, watchdog rules, webhooks, e-mail triggers and observer callbacks retain their registrant’s identity. Store-backed users and roles are checked again on dispatch; deleted users, unavailable stores and viewers cannot execute deferred work. Starting a daemon does not transfer its registrations to the starter. Ownership metadata retains no RBAC bearer token; explicit Python `AuthorizationContext` values without `store_path` keep their captured role, and work registered outside RBAC clears worker identity. Observer/watchdog callbacks get fresh variable scopes, and failed watchdog actions do not count as dismissed popups. Nested callbacks retain the caller’s failure count; a webhook with a revoked owner returns HTTP 403 and `fired: false`.
310312

311313
### How the remote-desktop wire protocol works

‎README/README_zh-CN.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,8 @@ MCP 窗口状态工具 `ac_window_minimize`、`ac_window_maximize` 与 `ac_windo
287287

288288
**可选的加固设置。** 下列各项在设置之前都是关闭的,没设置的服务器行为与先前相同。`JE_AUTOCONTROL_RBAC_USERS=<用户文件>` 让 REST API 与 MCP HTTP 传输把 bearer token 对应到用户,并按角色(viewer/operator/admin)授权每一个路由、工具与特权 `AC_*` 命令;此时共用 token 会被拒绝。`JE_AUTOCONTROL_MCP_PATH_ROOTS`(以操作系统的路径分隔符分开的目录)把每一个属于文件路径的 MCP 工具参数限制在这些目录里,`JE_AUTOCONTROL_MCP_PATH_ROOTS_FROM_CLIENT=1` 再加上 client 的 `roots/list`,`JE_AUTOCONTROL_MCP_ENV_REF_ALLOW` 限制 `ac_resolve_ref` 可以读哪些 `env://` 名称。`JE_AUTOCONTROL_ACTION_SIGNING_PUBLIC_KEY` 让端点只能验证 Ed25519 签名的动作文件而不能签名(`create_signing_keypair`;私钥留在签名的机器上)。远程桌面的 viewer 只把 host 推来的文件写在 `~/Downloads/AutoControl` 下面(`JE_AUTOCONTROL_REMOTE_DOWNLOAD_DIR`);这一项默认就是开的。
289289

290+
REST GUI 在用户存储文件启用时显示 RBAC 模式并禁用共用 token 字段。`rest_api_registry.status()` 提供 `rbac_enabled`,该模式的 `token` 为 `null`;请使用个别用户 token。只实现 `check()` 的自制 REST gate 仍可用于共用 token 模式;RBAC 要求已验证的用户 context,会拒绝没有身份的成功结果。
291+
290292
间隔/cron 调度、触发器、热键、watchdog 规则、webhook、e-mail 触发器与 observer 回调会保留注册者身份。每次触发重新检查来源用户文件与角色;已删除的用户、无法使用的来源文件与 viewer 不能执行后台工作。启动 daemon 不会改变已有工作的所有者。所有者信息不保存 RBAC bearer token;未带 `store_path` 的显式 Python `AuthorizationContext` 保留角色快照,未在 RBAC 下注册的工作会清除 worker 原有身份。observer/watchdog 回调使用独立变量作用域,watchdog 动作失败不会算成成功消除弹窗。嵌套回调会保留调用端的失败计数;webhook 所有者被撤销权限时响应 HTTP 403 与 `fired: false`。
291293

292294
### 远程桌面的线路协议

‎README/README_zh-TW.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,8 @@ MCP 視窗狀態工具 `ac_window_minimize`、`ac_window_maximize` 與 `ac_windo
286286

287287
**選用的強化設定。** 下列各項在設定之前都是關閉的,沒設定的伺服器行為與先前相同。`JE_AUTOCONTROL_RBAC_USERS=<使用者檔>` 讓 REST API 與 MCP HTTP 傳輸把 bearer token 對到使用者,並依角色(viewer/operator/admin)授權每一個路由、工具與特權 `AC_*` 命令;此時共用 token 會被拒絕。`JE_AUTOCONTROL_MCP_PATH_ROOTS`(以作業系統的路徑分隔字元分開的目錄)把每一個屬於檔案路徑的 MCP 工具參數限制在這些目錄裡,`JE_AUTOCONTROL_MCP_PATH_ROOTS_FROM_CLIENT=1` 再加上 client 的 `roots/list`,`JE_AUTOCONTROL_MCP_ENV_REF_ALLOW` 限制 `ac_resolve_ref` 可以讀哪些 `env://` 名稱。`JE_AUTOCONTROL_ACTION_SIGNING_PUBLIC_KEY` 讓端點只能驗證 Ed25519 簽章的動作檔而不能簽章(`create_signing_keypair`;私鑰留在簽章的機器上)。遠端桌面的 viewer 只把 host 推來的檔案寫在 `~/Downloads/AutoControl` 底下(`JE_AUTOCONTROL_REMOTE_DOWNLOAD_DIR`);這一項預設就是開的。
288288

289+
REST GUI 在使用者存放檔啟用時顯示 RBAC 模式並停用共用 token 欄位。`rest_api_registry.status()` 提供 `rbac_enabled`,該模式的 `token` 為 `null`;請使用個別使用者 token。只實作 `check()` 的自製 REST gate 仍可用於共用 token 模式;RBAC 要求已驗證的使用者 context,會拒絕沒有身分的成功結果。
290+
289291
間隔/cron 排程、觸發器、熱鍵、watchdog 規則、webhook、e-mail 觸發器與 observer 回呼會保留註冊者身分。每次觸發重新檢查來源使用者檔與角色;已刪除的使用者、無法使用的來源檔與 viewer 不能執行背景工作。啟動 daemon 不會改變既有工作的擁有者。擁有者資訊不保存 RBAC bearer token;未帶 `store_path` 的明確 Python `AuthorizationContext` 保留角色快照,未在 RBAC 下註冊的工作會清除 worker 原有身分。observer/watchdog 回呼使用獨立變數範圍,watchdog 動作失敗不會算成成功排除彈窗。巢狀回呼會保留呼叫端的失敗計數;webhook 擁有者被撤銷權限時回應 HTTP 403 與 `fired: false`。
290292

291293
### 遠端桌面的線路協定

‎architecture.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ AC_web_run / AC_web_run_actions → utils/webrunner_bridge/bridge.py
8585
(event_dict["WR_*"] on a WebRunner without execute_one); a failure → WebRunnerBridgeError
8686
```
8787

88+
REST gate adaptation preserves check-only custom gates under shared-token auth;
89+
store-backed servers require an authenticated user context even from replacement
90+
gates. Registry status exposes `rbac_enabled` and omits the inactive shared token
91+
so the GUI presents the authentication mode that the server actually accepts.
92+
8893
Delayed work captures registration authority through `utils/rbac/deferred.py`.
8994
Schedulers, triggers, hotkeys, watchdogs, webhooks, e-mail watchers and observers
9095
bind that owner at dispatch, independently of the worker's ambient context.

‎architecture_explore.md‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ iOS(WebDriverAgent)。核心能力是滑鼠/鍵盤控制、影像辨識、
2020
| 指標 | 數值 |
2121
| --- | ---: |
2222
| Python 模組總數(含周邊子專案) | 1,080 |
23-
| 程式碼總行數 | 161,971 |
23+
| 程式碼總行數 | 161,997 |
2424
| `je_auto_control/utils/` 子套件數 | 310 |
2525
| `AC_*` 動作指令數(`known_commands()` 實測) | 779 |
2626
| 套件門面 `__all__` 公開名稱數 | 1,244 |
@@ -527,7 +527,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。
527527

528528
### 5.4.11 伺服器、網路協定與外部整合
529529

530-
> 24 個套件、約 7,101 行。
530+
> 24 個套件、約 7,121 行。
531531
532532
| 模組 | 行數 | 職責 |
533533
| --- | ---: | --- |
@@ -549,7 +549,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。
549549
| `utils/otp/` | 37 | TOTP 一次性密碼產生(自動化 2FA 登入) |
550550
| `utils/outbox/` | 107 | 交易式 outbox,保證至少一次的事件投遞 |
551551
| `utils/pytest_plugin/` | 283 | pytest 外掛 + BDD step library(`pytest11` entry point) |
552-
| `utils/rest_api/` | 2,039 | 純標準庫 REST 前端:路由、Bearer 驗證、限流、Prometheus 指標、OpenAPI 3.1 產生 |
552+
| `utils/rest_api/` | 2,059 | 純標準庫 REST 前端:路由、Bearer 驗證、限流、Prometheus 指標、OpenAPI 3.1 產生 |
553553
| `utils/socket_server/` | 163 | 執行 action JSON 的執行緒式 TCP 指令伺服器(預設綁 127.0.0.1) |
554554
| `utils/sse_client/` | 142 | Server-Sent Events 用戶端解析 |
555555
| `utils/tls_acme/` | 473 | TLS 自動化:HTTP-01 挑戰伺服器、金鑰/CSR、自動續期 |
@@ -820,16 +820,16 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。
820820
| `usbip/libusb_backend.py` | 224 | 以 PyUSB/libusb 執行 URB 的正式後端。 |
821821
| `usbip/backend.py` | 87 | 可插拔 URB 執行後端。 |
822822

823-
#### `utils/rest_api/`(2,039 行)
823+
#### `utils/rest_api/`(2,059 行)
824824

825825
| 檔案 | 行數 | 職責 |
826826
| --- | ---: | --- |
827-
| `rest_server.py` | 634 | HTTP 前端主體。 |
827+
| `rest_server.py` | 650 | HTTP 前端主體;相容 check-only gate,RBAC 必須帶使用者身分。 |
828828
| `rest_handlers.py` | 528 | 端點實作。 |
829829
| `rest_openapi.py` | 443 | 走訪路由表產生 OpenAPI 3.1 規格。 |
830830
| `rest_auth.py` | 202 | Bearer token 驗證 + 逐 client 限流閘門。 |
831831
| `rest_metrics.py` | 75 | Prometheus 曝露端點。 |
832-
| `rest_registry.py` | 77 | 保存執行中 REST 伺服器的行程級單例。 |
832+
| `rest_registry.py` | 81 | 保存執行中 REST 伺服器的行程級單例,狀態包含 RBAC 模式並隱去停用的共用 token。 |
833833
| `__main__.py` | 66 | `python -m je_auto_control.utils.rest_api` 進入點。 |
834834

835835
#### 其他多檔子套件
@@ -899,7 +899,7 @@ GUI 是**選用 extra**(`pip install je_auto_control[gui]`,只需要 PySide6
899899
| `_screen_geometry.py` | 55 | Qt 邏輯座標與截圖用的原生像素互轉:`native_region()`、`screen_at_native()`、`logical_point()`(每個螢幕的左上角在兩者相同,螢幕內依 device pixel ratio 縮放)。區域選取與主機端標註覆蓋層都用它。 |
900900
| `_daemon_thread.py` | 79 | `DaemonThread`:`QThread` 的替代品,保留遠端桌面 worker 用到的介面(`start`/`run`/`isRunning`/`wait`/`requestInterruption`/`started`/`finished`),但 `run()` 跑在 daemon `threading.Thread` 上,刪除物件或程式結束都不會銷毀執行中的執行緒。 |
901901
| `_worker_thread.py` | 216 | `start_worker()`:在 daemon `threading.Thread` 上執行 `QObject` worker 的 `run()`(沒有 `QThread` 可被銷毀),並經由分頁擁有的中繼物件回報結果(回呼一律在 GUI 執行緒;worker 沒處理的例外也送到 `on_fail`);worker 留在模組登錄表直到 GUI 執行緒看到它結束,回傳 `WorkerHandle`(`isRunning()`);程式結束時先呼叫 worker 的 `request_stop()`,最多等 10 秒,仍在跑的隨行程結束。 |
902-
| `language_wrapper/` | 5,071 | 四語系字典(英/日/簡中/繁中)+ `multi_language_wrapper` 執行期切換器與監聽註冊表。 |
902+
| `language_wrapper/` | 5,075 | 四語系字典(英/日/簡中/繁中)+ `multi_language_wrapper` 執行期切換器與監聽註冊表。 |
903903
| `selector/` | 216 | 拖曳選取螢幕區域的半透明全螢幕覆蓋層與樣板裁切工具(互動式,但都有對應的程式化 API)。 |
904904

905905
> **分頁指令一律走 Actions 選單**:分頁本身只放輸入、表格與結果檢視,指令由視窗層選單暴露。
@@ -949,7 +949,7 @@ GUI 是**選用 extra**(`pip install je_auto_control[gui]`,只需要 PySide6
949949
| trace_replay | `trace_replay_tab.py` | 187 | 拖曳捲動時光回溯錄製內容。 |
950950
| remote_desktop | `remote_desktop/`(17 檔) | 6,240 | 見下。 |
951951
| presence | `presence_tab.py` | 152 | 多檢視者在場名單。 |
952-
| rest_api | `rest_api_tab.py` | 198 | 啟停 HTTP 前端並顯示 URL 與 token。 |
952+
| rest_api | `rest_api_tab.py` | 198 | 啟停 HTTP 前端並顯示 URL、實際驗證模式與可用 token。 |
953953
| admin_console | `admin_console_tab.py` | 313 | 管理多個遠端 AutoControl REST 端點。 |
954954
| audit_log | `audit_log_tab.py` | 192 | 瀏覽並驗證防竄改雜湊鏈。 |
955955
| inspector | `inspector_tab.py` | 121 | WebRTC 檢測器:即時摘要與近期統計取樣。 |
@@ -1081,7 +1081,7 @@ socket 預設綁 `127.0.0.1`;資源一律用 `with`。
10811081

10821082
| 層/子系統 | 檔案數 | 行數 |
10831083
| --- | ---: | ---: |
1084-
| `gui/` | 100 | 28,782 |
1084+
| `gui/` | 100 | 28,788 |
10851085
| `utils/mcp_server/` | 37 | 19,164 |
10861086
| `utils/remote_desktop/` | 56 | 13,301 |
10871087
| `utils/executor/` | 8 | 9,666 |
@@ -1090,7 +1090,7 @@ socket 預設綁 `127.0.0.1`;資源一律用 `with`。
10901090
| `utils/accessibility/` | 14 | 3,143 |
10911091
| `wrapper/` | 19 | 4,099 |
10921092
| `windows/` | 23 | 2,154 |
1093-
| `utils/rest_api/` | 8 | 2,039 |
1093+
| `utils/rest_api/` | 8 | 2,059 |
10941094
| `utils/agent/` | 9 | 2,142 |
10951095
| `linux_with_x11/` | 19 | 1,281 |
10961096
| `linux_wayland/` | 17 | 2,921 |
@@ -1102,5 +1102,5 @@ socket 預設綁 `127.0.0.1`;資源一律用 `with`。
11021102
| `autocontrol-lsp/` | 8 | 744 |
11031103
| `utils/hotkey/` | 7 | 857 |
11041104
| 其餘模組(約 286 個 `utils/` 子套件 + `android/`/`ios/`/周邊小工具) | 690 | 58,669 |
1105-
| **總計** | **1,074** | **161,906** |
1105+
| **總計** | **1,074** | **161,932** |
11061106

‎docs/source/Eng/doc/operations_layer/operations_layer_doc.rst‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,17 @@ CLI::
137137
Roles (opt-in RBAC)
138138
-------------------
139139

140+
The REST GUI labels RBAC mode and disables its shared-token input. Registry
141+
status includes ``rbac_enabled`` and reports ``token: null`` while a user store is
142+
configured; use a token belonging to an individual user. Stopping the server
143+
restores the shared-token controls. The Python server's ``token`` property still
144+
holds its configured shared token, which RBAC does not accept.
145+
146+
Custom REST gates implementing only ``check(client_ip=..., header_value=...)``
147+
retain their verdict and rate-limit behavior in shared-token mode. When the
148+
server has a user store, a successful gate result must include an authenticated
149+
``AuthorizationContext``; an identityless success returns HTTP 401.
150+
140151
By default every caller holds the one shared token and may use every
141152
endpoint. Point the server at a user store and each caller gets a token of
142153
their own and a role instead:

‎docs/source/Zh/doc/operations_layer/operations_layer_doc.rst‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ CLI::
128128
角色(選用的 RBAC)
129129
--------------------
130130

131+
REST GUI 會標示 RBAC 模式並停用共用 token 輸入。Registry 狀態提供 ``rbac_enabled``,
132+
使用者存放檔啟用時回報 ``token: null``;請改用個別使用者的 token。停止伺服器會恢復共用
133+
token 控制項。Python 伺服器的 ``token`` 屬性仍保留設定的共用 token,RBAC 不接受它。
134+
135+
只實作 ``check(client_ip=..., header_value=...)`` 的自製 REST gate 在共用 token 模式下
136+
保留原本的判定與限流行為。伺服器設定使用者存放檔時,成功結果必須包含已驗證的
137+
``AuthorizationContext``;沒有身分的成功判定會回覆 HTTP 401。
138+
131139
預設情況下所有呼叫端共用同一個 token,可以使用所有端點。把伺服器指向一個使用者
132140
存放檔之後,每個呼叫端改為擁有自己的 token 與角色:
133141

‎docs/updates/2026-10.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,3 +356,11 @@ Jeffrey_RPA kept its own `KEY_ALIASES` (40 friendly names) and `_EXTRA_KEY_CODES
356356
- **Validation**: 99 selected GUI tests pass, followed by all eight final lifecycle cases covering lazy open, refused close, Presence subscription teardown/recreation, HUD log detachment, hidden-panel release and retained legacy forms. 100 documentation, budget and cross-project checks pass; English/Traditional Chinese strict Sphinx builds pass. Three-platform mypy reports zero error modules; wheel and source distribution builds pass.
357357
- **Files**: `gui/tab_registry.py`, `gui/main_widget.py`, lifecycle regression tests, three READMEs, Sphinx GUI guide, changelog and architecture maps.
358358
- **Open items**: F1 startup deferral, legacy panel/session destruction and the broader F2–F4 work remain in `Progress.md`.
359+
360+
## U-20261018-08 · 2026-10-18 · Present REST RBAC mode and preserve legacy gate compatibility · #done #rest #rbac
361+
362+
- **What**: REST registry status now includes `rbac_enabled` and suppresses the inactive shared token when a user store is configured. The GUI explains user-token authentication in all four languages, disables the shared-token field and restores it after stopping RBAC. The Python server's existing `token` property remains unchanged.
363+
- **Compatibility/security**: custom gates implementing only `check()` retain their verdicts and rate limiting under shared-token authentication. Store-backed servers require an authenticated user context independently of the gate implementation; an identityless success returns HTTP 401, including from a replacement gate with `authenticate()`.
364+
- **Validation**: 170 REST/RBAC regression tests pass; 14 final focused/thread-safety cases pass, covering real local HTTP, explicit/environment stores, shared tokens, mode transitions and GUI copy behavior. 75 additional endpoint, schema, metrics and documentation/budget checks pass. Three-platform mypy reports zero error modules; English and Traditional Chinese strict Sphinx builds pass.
365+
- **Files**: REST server/registry, GUI REST panel and four language catalogs, regression tests, three READMEs, Sphinx roles guide, changelog and architecture maps.
366+
- **Open items**: user management surfaces, sensitive viewer tool classification and real-client/TLS RBAC acceptance remain in `Progress.md`.

0 commit comments

Comments
 (0)