dsh-plugin-lcu 0.2.9 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/README.zh.md CHANGED
@@ -2,12 +2,11 @@
2
2
 
3
3
  [English](../README.md) | 中文
4
4
 
5
- 把 [LCU](https://github.com/amontlabs/lcu)(*Codex computer use, decoupled from the app*)接进
6
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness),让 Agent 能看屏幕、点鼠标、操作真实浏览器标签页。
5
+ 用 **ChatGPT 桌面应用内置的 computer-use 运行时**,从 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 驱动桌面和 Chrome。
7
6
 
8
- LCU 把 **ChatGPT 桌面端内置的那套 computer-use 运行时**包装成 MCP server。本插件把它变成 DSH 的一等能力:
9
- 启用了模式的会话会拿到一个 `js` 工具。**全程不涉及 Codex 认证** —— 运行时来自你本地的 ChatGPT 安装,
10
- LCU 从不解包、下载、认证或改写它。
7
+ 本插件**直接启动那个运行时**并通过 MCP 与它通信。整个集成都在这里:定位并校验应用、构造运行时需要的环境、批准桥、回合生命周期、以及负责逐轮清理的 macOS 服务 wrapper。**不需要额外安装任何东西** —— 不需要别的包装项目、不需要第二个解释器、不需要 Python。
8
+
9
+ **不涉及 Codex 或 ChatGPT 的登录。** 运行时来自你本机的安装,插件从不下载、改写或认证它。
11
10
 
12
11
  ## 目录
13
12
 
@@ -19,105 +18,73 @@ LCU 从不解包、下载、认证或改写它。
19
18
  - [批准与安全模型](#批准与安全模型)
20
19
  - [实现说明](#实现说明)
21
20
  - [排障](#排障)
22
- - [随附工具](#随附工具)
23
- - [已知限制](#已知限制)
21
+ - [与 LCU 的关系](#与-lcu-的关系)
22
+ - [配套工具](#配套工具)
23
+ - [已知限制与未做的工作](#已知限制与未做的工作)
24
24
  - [开发](#开发)
25
25
  - [许可](#许可)
26
26
 
27
27
  ## 能力
28
28
 
29
- 两个模型可见工具,**schema 完全由服务端提供**,本插件不自己发明:
30
-
31
29
  | 工具 | 作用 |
32
30
  |---|---|
33
- | `js` | 针对 `cua` 桌面/浏览器 API 执行一段 JavaScript。首次调用会返回 API 文档;选中应用或标签页时结果里会带初始 UI 状态。 |
34
- | `js_reset` | 丢弃持久化的 JavaScript 会话,重建运行时。 |
31
+ | `js` | 对 `cua` 桌面/浏览器 API 运行一段 JavaScript。第一次调用会返回 API 文档;选中应用或标签页会返回初始 UI 状态。 |
32
+ | `js_reset` | 丢弃持久的 JavaScript 会话,重新起一个运行时。 |
35
33
 
36
- 另有两个 host-only 工具只对宿主可见、**绝不暴露给模型**:`turn_ended`(每轮清理)与
37
- `js_add_node_module_dir`。
34
+ 另外两个**只给宿主、永不暴露给模型**的工具:`turn_ended`(逐轮清理)和 `js_add_node_module_dir`。
38
35
 
39
- 插件自己**只加一个**工具:
36
+ 插件自己只加了一个工具:
40
37
 
41
38
  | 工具 | 作用 |
42
39
  |---|---|
43
- | `computer_use_stop` | 不带参数时列出运行时当前为本次会话持有的应用;带 `app`(上面列出的 bundle identifier)时释放它。这就是 LCU 的显式按应用 Stop —— 它的 Pi adapter 把它暴露为 `/lcu stop` —— 能在**不结束会话**的前提下清掉宿主应用里"computer use 正在使用"的状态。 |
40
+ | `computer_use_stop` | 不带参数时列出运行时当前为本次会话持有的应用;带 `app`(其中一个 bundle id)时释放它。它清除宿主应用对**某一个**应用的"正在使用你的电脑"状态,而不结束会话。 |
44
41
 
45
- 截图会作为**持久化图片**经 DSH 的 attachment store 送达 —— 所以声明了 image input 的模型路由是真的能"看见"屏幕的。
42
+ 截图通过 DSH 的附件存储以持久图片返回,所以声明了图像输入的模型路由**真的能看见屏幕**。
46
43
 
47
44
  ## 前置条件
48
45
 
49
46
  | | |
50
47
  |---|---|
51
- | 操作系统 | Apple Silicon 上的 macOS。LCU 也支持 Linux,但本插件在 macOS 上开发与验证。 |
52
- | ChatGPT 桌面端 | 已安装、OpenAI 签名的官方版本。运行时与 instructions 由它提供。 |
53
- | Python | 3.12 或更新,位于 `PATH`,或 `/opt/homebrew/bin`、`/usr/local/bin`、`/usr/bin`。 |
54
- | LCU | 需另行安装,见下。 |
48
+ | 系统 | **Apple Silicon 上的 macOS。** 启动器解析 macOS 应用包,生命周期 wrapper 是 macOS 服务;其他平台这两块都要重做。 |
49
+ | ChatGPT 桌面应用 | 装在 `/Applications/ChatGPT.app`(或用 `app` 指定)。它提供运行时、指令和签名助手。 |
55
50
  | DSH | 一个你能装 bundle 的 profile。 |
56
51
 
57
- 插件面向 **macOS**:`host-guard` 依赖 `ps`/`plutil`,生命周期走 LCU 的 macOS 路径。移植 Linux 需要改这两处。
52
+ **没有** Python 要求,**没有**需要单独安装的运行时。插件启动的是应用自己的 `node` 和它自己的入口。
58
53
 
59
54
  ## 安装
60
55
 
61
- ### 1. 安装 LCU
62
-
63
- 下载对应平台的 release 归档,**校验 checksum**,然后运行它自带的安装器。**不要注册其他 harness** ——
64
- 本插件就是你的 harness。
65
-
66
- ```sh
67
- TAG=v0.9.6
68
- TARGET=darwin-arm64
69
- curl -fLO "https://github.com/amontlabs/lcu/releases/download/$TAG/lcu-${TAG#v}-$TARGET.tar.gz"
70
- curl -fLO "https://github.com/amontlabs/lcu/releases/download/$TAG/lcu-${TAG#v}-$TARGET.tar.gz.sha256"
71
- shasum -a 256 -c "lcu-${TAG#v}-$TARGET.tar.gz.sha256" # 必须打印 OK
72
- tar -xzf "lcu-${TAG#v}-$TARGET.tar.gz" && cd "lcu-${TAG#v}-$TARGET"
73
- ./scripts/install.sh --runtime-only --yes
74
- ```
75
-
76
- 确认运行时能加载:
77
-
78
- ```sh
79
- ~/.local/share/lcu/current/bin/lcu doctor --non-interactive
80
- ```
81
-
82
- 期望看到 `Original Mac provider loaded; app listing and app-state methods are available`。
83
- 隐私权限是**首次使用时**才授予的,不在这里。
84
-
85
- ### 2. 把插件装进 profile
56
+ ### 1. 把插件装进 profile
86
57
 
87
- 装完插件后,该 profile 里就会有一行 LCU host 处于 active。**加载时不启动任何进程。**
58
+ 装完这一行配置就在该 profile 里生效。**加载时不启动任何东西。**
88
59
 
89
60
  ```sh
90
61
  dsh plugin --profile <profile> add dsh-plugin-lcu
91
62
  ```
92
63
 
93
- 桌面版 App 的 **desktop** profile 被应用独占,CLI 会拒绝;请改用 App 内的插件管理器
94
- (Settings ▸ Plugins),它执行的是同一个 pnpm 操作。
64
+ 桌面版 App 的托管 profile 用 CLI 会被拒;请用 App 内的插件管理器(设置 ▸ 插件),它跑的是同一个 pnpm 操作。
95
65
 
96
- ### 3. 生成 preset
66
+ ### 2. 生成 preset
97
67
 
98
- DSH 的 Agent preset **没有继承**:`config.plugins` 就是完整清单,而 patch 是整段替换而非深合并。
99
- 所以自定义 preset 必须重述它的基线。与其手抄,不如从**实际安装的那份** preset 生成:
68
+ DSH 的 agent preset **没有继承**:一个 preset 的 `config.plugins` 就是它完整的插件列表,而 patch 是**整体替换**一个条目而不是合并进去。所以自定义 preset 必须重述它的基底。与其手抄那份列表,不如从**实际装着的** preset 生成:
100
69
 
101
70
  ```sh
102
71
  node node_modules/dsh-plugin-lcu/scripts/gen-presets.mjs --profile ~/.dsh/profiles/<profile>
103
72
  ```
104
73
 
105
- 它会在该 profile 的 `cordis.patch.yml` 里写入一个带标记的块,包含两个 preset:
74
+ 它会在该 profile 的 `cordis.patch.yml` 里写一个带标记的块,含两个 preset:
106
75
 
107
- | preset | 基线 | 额外内容 |
76
+ | preset | 基底 | 增加 |
108
77
  |---|---|---|
109
- | `daily`(显示名「日常」) | 内置 `ptc` preset | 启用 `subagent_codex` |
110
- | `heavy`(显示名「重活」) | 同上,且 `tool-presentation: both` | 以上全部,并且本插件会 attach |
78
+ | `daily` | 自带的 `ptc` preset | 启用 `subagent_codex` |
79
+ | `heavy` | 同上,且 `tool-presentation: both` | 以上全部,且本插件接入 |
111
80
 
112
- DSH 升级后**重跑一次**,让副本跟上。`--dry-run` 只打印不写入,`--out FILE` 写到别处。
81
+ DSH 升级后重跑一次,让副本跟上。`--with-heavy` 是隐含的;`--dry-run` 只打印不写;`--out FILE` 写到别处。
113
82
 
114
- > `heavy` 特意把工具呈现设成 `both`。纯 `ptc` 呈现下模型只看到 `run_code`,
115
- > `js` 就得作为一段 JavaScript 字符串**嵌套**在另一段 JavaScript 程序里。`both` 让 `js` 可以直接调。
83
+ > `heavy` 特意把工具呈现设为 `both`。纯 `ptc` 呈现下模型只看得到 `run_code`,`js` 就得嵌成另一个 JavaScript 程序里的字符串。`both` 让 `js` 可以被直接调用。
116
84
 
117
- ### 4. 配置哪些模式能用
85
+ ### 3. 配置哪些模式能用
118
86
 
119
- 插件是**一个 root 行 + 显式 `presets` 白名单**。编辑已安装的 `cordis.patch.yml`
120
- (或 profile patch)让它与你生成的 preset id 一致:
87
+ 插件是一个根行,带 `presets` 白名单。编辑已安装的 `cordis.patch.yml`(或 profile patch),让它匹配你生成的 preset id:
121
88
 
122
89
  ```yaml
123
90
  - id: lcu
@@ -127,13 +94,22 @@ DSH 升级后**重跑一次**,让副本跟上。`--dry-run` 只打印不写入
127
94
  - heavy
128
95
  ```
129
96
 
130
- ### 5. 重启,然后先做一次无害调用
97
+ ### 4. 重启,然后做一次调用
131
98
 
132
- **必须重启** —— 插件的代码与配置都**不会热更新**。然后以 `heavy`(「重活」)模式新建任务,说一句:
99
+ 重启 harness —— 插件**代码和配置都不热更新**。然后在 `heavy` 模式(显示为**重活**)里起一个任务,让它做点无害的事:
133
100
 
134
- > 用 `js` 工具执行 `await cua.getState();`,告诉我哪些应用正在运行。
101
+ > 用 `js` 工具运行 `await cua.getState();`,告诉我哪些应用在跑。
135
102
 
136
- 首次操作某个应用时,运行时会请求批准,见下文。
103
+ 第一次触碰某个应用时,运行时会请求批准。见下文。
104
+
105
+ 首次 attach 时插件会解析应用,并把结果写进诊断日志:
106
+
107
+ ```
108
+ app: /Applications/ChatGPT.app version=26.1002.52244 runtime=0.0.29/20261003001300-807782c586fc
109
+ attach: launching /Applications/ChatGPT.app/Contents/Resources/cua_node/bin/node [...]
110
+ ```
111
+
112
+ 如果应用缺失,或者它某个文件**别的账号可以改写**,attach 会被拒绝并写明原因,会话就只是没有这个能力而已。
137
113
 
138
114
  ### 可选:启用 Chrome
139
115
 
@@ -142,20 +118,13 @@ config:
142
118
  chrome: true
143
119
  ```
144
120
 
145
- 然后:
121
+ 这会打开运行时的浏览器面。浏览器那一半是 OpenAI 的,驱动页面靠**官方 ChatGPT Chrome 扩展**,它连的是一个 native messaging host。在装了 ChatGPT 桌面应用的机器上,那个 host 已经注册好了,所以**不需要再做别的**:在你想驱动的浏览器 profile 里启用该扩展,并确认它出现在 `cua.getState()` 里。
146
122
 
147
- ```sh
148
- ~/.local/share/lcu/current/bin/lcu browser install
149
- ```
150
-
151
- 在你要驱动的 Chrome profile 里启用**官方 ChatGPT 扩展**,并重启 Chrome(或在 `chrome://extensions`
152
- 把该扩展关掉再开),让它剥离旧的 native host、重连到 LCU 的 relay。
153
- `lcu browser status` 会告诉你连接器是否已指向本次 LCU 安装。站点仍然逐个精确 origin 批准。
123
+ 站点仍然是逐个精确 origin 批准。
154
124
 
155
125
  ### 可选:预授权站点
156
126
 
157
- 运行时首次使用每个站点都会问一次。对可信来源跳过问询,就列出**精确 origin**
158
- (必须能通过 `new URL(...).origin` 原样往返):
127
+ 运行时想用的每个站点都会问一次。要跳过你信任的 origin 的提示,就列出**精确 origin** —— 消息必须经 `new URL(...).origin` 往返后不变:
159
128
 
160
129
  ```yaml
161
130
  config:
@@ -163,133 +132,148 @@ config:
163
132
  - http://localhost:3000
164
133
  ```
165
134
 
166
- 诊断日志会记下每一个被问到的 origin,这是发现该填什么的最省事办法。
135
+ 诊断日志会记下每一个被问到的 origin,这是发现它们最方便的办法。
167
136
 
168
137
  ## 使用
169
138
 
170
- 新建任务时选择已启用的模式。工具是**按 Agent** 挂载的:其他模式的会话永远看不到它们,也永远不会拉起运行时。
139
+ 起任务时选一个已启用的模式。工具是**按 Agent 挂载**的:其他模式下的会话永远看不到它们,也永远不会起运行时。
171
140
 
172
- 典型请求:
141
+ 典型的请求:
173
142
 
174
143
  ```
175
144
  截一张 Finder 窗口的图,告诉我分辨率。
176
- 列出我当前 Chrome 的标签页。
177
- 打开 Safari 访问 example.com,读出页面标题。
145
+ 列出我当前的 Chrome 标签页。
146
+ 打开 Safari,去 example.com,把页面标题读回来。
178
147
  ```
179
148
 
180
149
  ## 配置
181
150
 
182
- | 字段 | 默认值 | 含义 |
151
+ | 字段 | 默认 | 含义 |
183
152
  |---|---|---|
184
- | `command` | `~/.local/share/lcu/current/bin/lcu` | LCU 启动器。自定义 `--prefix` 时改这里。 |
185
- | `chrome` | `false` | 传 `--chrome`,启用浏览器面。 |
186
- | `audio` | `false` | 传 `--audio`,启用运行时的电脑录音 API。 |
187
- | `presets` | `["heavy"]` | 允许拿到工具的 Agent preset id 列表。 |
188
- | `allowedOrigins` | `[]` | 免问询的精确 HTTP(S) origin。非法项直接丢弃,**绝不放宽**。 |
189
- | `sectionOrder` | `0` | 注入的 LCU instructions 在 prompt 中的排序。 |
153
+ | `app` | `/Applications/ChatGPT.app` | 提供 computer use 的应用。 |
154
+ | `command` | 未设 | 用一个显式可执行文件替换计算出的启动方式,用于内置解析够不到的应用。它会跳过运行时自己的环境设置。 |
155
+ | `chrome` | `false` | 打开浏览器面。 |
156
+ | `audio` | `false` | 打开运行时的 computer-audio API。 |
157
+ | `presets` | `["heavy"]` | 允许使用这些工具的 Agent preset id。 |
158
+ | `allowedOrigins` | `[]` | 免问的精确 HTTP(S) origin。非法条目会被丢弃,绝不放宽。 |
159
+ | `allowedApps` | `[]` | 免批准的 bundle identifier。**承载 agent 的应用即使被列进去也会被拒**。 |
160
+ | `sectionOrder` | `0` | 注入的运行时指令在 prompt 中的排序。 |
190
161
 
191
162
  ## 批准与安全模型
192
163
 
193
- **模型不能批准任何东西**,每个决定都是人的:
164
+ **模型不能批准任何东西。每个决定都是人的。**
165
+
166
+ - **按应用批准。** 运行时在用某个应用前会问。插件把它的选项 —— *仅此次*、*本次会话*、*始终允许*(运行时提供哪些就渲染哪些)、以及 *拒绝* —— 通过 DSH 的提问界面呈现。回答被精确映射回运行时提供的那个 scope;**运行时没提供的 scope 授不出去**。
167
+ - **站点批准。** 浏览器访问按精确 origin 询问。`allowedOrigins` 只匹配精确 origin;多一个斜杠、带路径、大小写不同都算不同 origin,会被问而不是被授予。
168
+ - **承载 agent 的宿主永不可批准。** Computer use 能点击已批准应用显示的任何东西,**包括批准弹窗本身**。守卫在问任何问题**之前**就拒绝承载 agent 的应用 —— 通过进程祖先和一份 agent 宿主/终端名单。
169
+ - **Fail closed。** 没有提问界面、被忽略的提示、无法识别的请求形状、被中止的调用 —— 全都以 *cancel* 结束,而运行时把它当作拒绝。
194
170
 
195
- - **按应用批准。** 运行时在使用一个应用前会问。插件把运行时提供的选项原样呈现出来 ——
196
- *Allow once*、运行时提供时的 *Allow for this session* 与 *Always allow*、以及 *Decline* ——
197
- 经 DSH 的提问界面。选择会映射回**恰好被提供的**那个 scope;运行时没提供的 scope 无法被授予。
198
- - **站点批准。** 浏览器访问按精确 origin 逐次批准。`allowedOrigins` 只做精确匹配:
199
- 带尾部斜杠、带路径、大小写不同,都是**另一个 origin**,会去问而不是放行。
200
- - **承载 agent 自己的应用永不可批准。** computer use 能点被批准应用里的任何东西,**包括批准弹窗本身**。
201
- 守卫在问用户之前就拒绝承载 agent 的应用 —— 依据是本进程的祖先链和一份 agent 宿主/终端名单。
202
- - **Fail closed。** 没有提问界面、用户关掉弹窗、请求形状无法识别、调用被中断 —— 全部以 *cancel* 结束,
203
- 而运行时把 cancel 当作拒绝。
171
+ 插件自己不存任何权限缓存;"始终允许"由运行时按应用记住。
204
172
 
205
- LCU 自身不保留权限缓存;`Always allow` 是**运行时**按应用记住的。
173
+ ### 无人值守
174
+
175
+ 每个批准都属于人,而**没人回答就是拒绝** —— 运行时不默认授予。所以无人值守的运行必须两半都提前定好:
176
+
177
+ ```yaml
178
+ config:
179
+ allowedApps:
180
+ - com.google.Chrome # 免问使用这个应用
181
+ allowedOrigins:
182
+ - https://example.com # 免问访问这个精确 origin
183
+ ```
184
+
185
+ - `allowedApps` 按 bundle identifier 匹配,忽略大小写。**承载 agent 的应用在查列表之前就被拒**,所以没有任何条目能授权它。
186
+ - `allowedOrigins` 只匹配精确 origin;带路径、多斜杠或大小写不同都算不同 origin,仍然会问。
187
+ - 没列进去的都会被问,而没人在场时就是拒绝。
188
+
189
+ 诊断日志会记下每一个被问到的应用和 origin(`approval: site https://example.com asking (add it to allowedOrigins to skip this)`),这是发现一次运行需要哪些确切值的方法。
206
190
 
207
191
  ## 实现说明
208
192
 
209
193
  ```
194
+ src/app.ts 定位并校验应用;构造运行时环境;规划启动
210
195
  src/connection.ts MCP 客户端:握手、工具发现、调用、elicitation、生命周期
211
196
  src/approval.ts 批准形状识别与 label→value 映射
212
- src/host-guard.ts 防自批准守卫
213
- src/tool.ts 工具定义、文本投影、持久化截图
214
- src/index.ts 插件:按 Agent attach、instructions、turn_ended、批准桥
215
- src/diag.ts attach / 批准 诊断日志
197
+ src/host-guard.ts 防自我批准的守卫
198
+ src/tool.ts 工具定义、文本投影、持久截图
199
+ src/index.ts 插件:按 Agent 挂载、指令、turn_ended、批准
200
+ src/control.ts relay:把运行时的控制 API 在插件与 wrapper 之间搬运
201
+ src/session.ts 连接门面,跟随应用更新
202
+ src/diag.ts attach/批准 诊断日志
203
+ helper/sky-service.mjs 运行时的 Sky 服务:turn-ended 钩子 + 控制通道
216
204
  ```
217
205
 
218
- **不依赖任何 MCP SDK。** harness 自带的 MCP 桥声明 `capabilities: {}`,因此**答不了 elicitation** ——
219
- 而那正是 LCU 请求批准的方式;同时往 profile 插件里塞第二个 SDK 又会引入宿主并不拥有的版本。
220
- MCP over stdio 就是 newline-delimited JSON-RPC,所以这条线自己实现。
221
- 再配合对 DSH 包的 **type-only import**,本插件**运行时零依赖**。
206
+ **运行时是被直接启动的。** `src/app.ts` 找到 `ChatGPT.app`,确认它需要的部件都在、且**别的账号改不了**,然后算出运行时需要的环境 —— 它自己的 `node` 和 `node_repl`、模块根、trusted code paths、API 面、以及 macOS native pipe 用的签名助手。它**不**把 `NODE_REPL_TRUSTED_SERVICES` 设成应用默认值:原因见下。
207
+
208
+ **turn-ended 钩子是运行时唯一缺的东西。** 一个永不结束的回合,就是一个**永不释放**的按应用 Stop,之后应用会拒绝每一个后续回合。运行时向它的 trusted services 暴露 `addTurnEndedHandler`,但自己**没有装任何 handler**,所以 `helper/sky-service.mjs` 是**作为** `sky` trusted service 被加载的:它把每个请求原样转发给应用自己的服务,并加上那个钩子 —— 钩子通过**应用自己的签名客户端**请应用清理已结束的回合。
209
+
210
+ 这**刻意少于**那个显而易见的实现。让一个监督者进程去做、再让它 spawn 签名客户端并传 `turn-ended` 参数,需要 **Apple Events** —— 而当负责进程是一个 hardened-runtime 的 harness 时,macOS **既拒绝授予、也拒绝弹窗询问**。那一步**必然超时**。应用自己的 IPC 才是执行清理的那一步,不需要 Apple Events,**几十毫秒就完成**。所以这里没有监督者进程,也没有 lifetime socket。
211
+
212
+ **控制通道是一个 relay,而且由插件自己 serve。** 提前释放某个应用需要用运行时的控制 API,而它活在运行时的进程里。wrapper 连上插件 serve 的 socket,自称 *service*,上报它见过的回合 context,并在那里回答 `status` 与 `stop`。**relay 不做任何判断**:某个会话和回合是否真实、某个应用是否真被持有,只有 wrapper 能回答 —— 因为只有它手里有运行时用来标记这些状态的回合元数据。
213
+
214
+ 这条通道有两个细节猜不出来。wrapper 运行在运行时的 JavaScript 沙箱里,而沙箱**拒绝普通 socket 连接(`EPERM`)** —— 无论 socket 在每用户临时目录还是 `/private/tmp` —— 所以它走运行时自己的 `nativePipe` API。而且那条管道**会静默丢弃字符串写入**,消息必须以 Buffer 写出;这一点极易被误判成"对端从未应答"。
215
+
216
+ **没有 MCP SDK 依赖。** harness 自带的 MCP 桥声明 `capabilities: {}`,因此**无法回答 elicitation** —— 而那正是运行时请求批准的方式;把第二个 SDK 拉进 profile 插件又会钉住一个宿主并不拥有的版本。MCP over stdio 就是按行分隔的 JSON-RPC,所以这里自己拥有这条线。加上对 DSH 包只用类型导入,本插件**没有任何运行时依赖**。
222
217
 
223
- **工具是按 Agent 注册的,不是挂载时注册。** 服务端拥有工具 schema,只有握手之后才能取到。
224
- 连接在 Agent 创建时、或它提交 preset 选择时建立;插件贡献的一切都注册进**该 Agent 自己的 context**,
225
- 因此会随其销毁而回退。
218
+ **工具是按 Agent 注册的,不是在挂载时。** 工具 schema 归服务器所有,所以只能握手之后取。一个 Agent 的连接在它被创建时、或它确定了 preset 选择时打开,插件贡献的一切都注册进**该 Agent 自己的 context**,所以销毁时一起回退。
226
219
 
227
- **两种 preset 时序都处理了。** 新任务先以部署默认 preset 创建,选择器的选择是**之后**才生效的,
228
- 所以只看 `agent/created` 会看到错误的组合;注册表会重新发出 `agent-preset/selected`,插件同时也监听它。
220
+ **两种 preset 时序都处理了。** 新任务先用部署默认值创建,picker 的选择之后再应用,所以只看 `agent/created` 会看到错误的组合;注册表会重新发出 `agent-preset/selected`,插件也响应它。
229
221
 
230
- **天然懒启动。** 加载时什么都不起。没有启用的会话,就没有 `lcu` 进程。
222
+ **构造上就是懒的。** 加载时不启动任何东西。没有启用的会话,就没有运行时进程。
231
223
 
232
- **instructions 会注入。** 服务端 `initialize.instructions` 成为该 Agent 的一个 prompt section。
233
- 它本身很短 —— API 手册在 `js` 的工具描述和首次调用结果里。
224
+ **指令是注入的。** 服务器的 `initialize.instructions` 变成 Agent 上的一个 prompt 段。它刻意很短 —— API 手册在 `js` 工具描述和第一次工具结果里。
234
225
 
235
226
  ## 排障
236
227
 
237
- 插件关于 attach 与批准的每个决定都会追加到:
228
+ 插件关于 attach、启动和批准的每个决定都会追加到:
238
229
 
239
230
  ```
240
231
  ~/.dsh/lcu-diag.log
241
232
  ```
242
233
 
243
- 超过 1 MB 会清空重来。`LCU_DIAG=0` 可关闭。**这是第一个该看的地方**:
244
- harness 没有当前会话可读的插件日志出口,而失败的 `agent/created` 监听器否则会被静默吞掉。
234
+ 超过 1 MB 就重头开始。`LCU_DIAG=0` 关闭它。**这是第一个该看的地方**:harness 没有运行中的会话能读的插件日志界面,而一个抛错的 `agent/created` 监听器否则会被静默吞掉。
245
235
 
246
236
  | 现象 | 原因与处理 |
247
237
  |---|---|
248
- | 启用的模式里始终没有工具 | 在日志里找 `decide … composed=`。若组合出的 preset 不在 `presets` 里,修正白名单;若压根没有 `agent-preset/selected` 行,说明模式从未提交。 |
249
- | `no userQuestions service -> cancel (fail closed)` | 该 profile 没有挂载提问面。 |
250
- | `refusing to approve the app hosting this agent` | 按设计如此;换一个应用。 |
251
- | 一轮之后调用被挡住 | 运行时的一轮清理尚未结束;插件会在下次调用前重试,未成功前拒绝调用。 |
252
- | `lcu doctor` 报 socket 路径错误 | 签名助手把 socket 绑在 home 目录下,路径超过 103 字节会被拒。换一个 home 路径更短的账号。 |
253
- | attach 报 spawn 失败 | 直接跑 `~/.local/share/lcu/current/bin/lcu doctor`,再检查配置里的 `command`。 |
254
- | ChatGPT 里仍显示某个应用在用 computer use | 运行时还持有它。让 agent 调 `computer_use_stop`,或直接关掉那个会话 —— 连接持有整棵运行时进程树,关闭时会一并释放。 |
255
- | `lcu status` 报 `changed_since_install` | ChatGPT 应用在会话运行期间自我更新了。LCU 警告这种会话可能"混用新旧文件":停掉这些会话并重启 harness,让所有东西来自同一个 app 版本。 |
256
-
257
- `node scripts/probe-lcu.mjs` 不经 harness 直连 LCU,打印协议版本、服务端身份、instructions 长度与工具清单 ——
258
- 用来把「插件的问题」和「LCU 的问题」分开。
259
-
260
- ## 随附工具
261
-
262
- `scripts/` 里还有两个服务于同级 Codex subagent bundle 的工具(同一个 profile 通常两个都要):
263
-
264
- - **`update-codex.mjs`** —— 把 profile 里的 `@openai/codex` 保持在**能通过三道门**的最新版本上
265
- (握手、协议 schema 断言、一次真实回合),任一门不过自动回退。官方发布的
266
- `@deepseek-ai/dsh-subagent-codex` 钉死 `0.153.4`,而它**并不能服务所有当前的 ChatGPT 账号模型**;
267
- 该脚本通过 profile 层、作用域限定的 pnpm override 把它顶上去。
268
- 用法见 `node scripts/update-codex.mjs --help`(`--check` / `--verify-only` / `--to` / `--rollback`)。
269
- - **`codex-baseline.json`** —— 最近一次通过全部三道门的版本。
270
-
271
- ## 已知限制
272
-
273
- - **被委派的子代理无法弹出批准。** DSH 只接受「精确的 live runtime root」上的人工作答,
274
- 因此子代理里的 LCU 批准会 fail closed。子代理可以做**不需要批准**的只读操作;
275
- 需要批准的事必须由顶层会话发起。
276
- - **一次"按应用停止"可能活得比会话长。** `computer_use_stop`、以及在宿主应用"正在使用你的电脑"
277
- 横幅上按 **Esc**,都是要求运行时停止使用某个应用。清除它要靠宿主应用自己的 turn-ended 清理,
278
- 而这一步在这里**不可靠**:它的 Apple Events 环节被 macOS 拒绝(hardened runtime 的 harness 不给弹窗)。
279
- 如果某个应用此后每一轮都回"已被用户显式停止",**退出并重启 ChatGPT 应用**即可清除。
280
- **日常使用(截图、点击、打字、浏览器标签页)不受影响,永远不需要重启**——插件的工具描述里写明了这一点,
281
- 所以模型不会自作主张去停止应用。
282
- - **`js` 沙箱不能写文件。** 所有写入都以 `EPERM` 失败(连临时目录也不行),所以**无法在 `js` 里存截图**。
283
- 图片改为作为附件交给 harness,每张存下来的图片都会在工具结果里给出它的**宿主文件系统路径**;
284
- 复制进工作区只需一行 `bash`(`install -m 644 '<path>' <target>`——存储对象是 mode 400)。
285
- 这两件事插件既写在工具结果里,也写进它注入的 instructions。
286
- - **macOS 权限是给 harness 的,不是给 OpenAI 助手的。** macOS 把权限请求归给**负责进程**,
287
- 而 harness 派生的一切,负责进程就是 harness 本身。所以必须在系统设置里给 **DeepSeek Harness**
288
- 开启屏幕录制与辅助功能;只给 ChatGPT 或 "Codex Computer Use" 是不够的,而且 macOS 不会自己弹出缺失的那项。
289
- - **每个 Agent 一条 LCU 连接。** LCU 的 JavaScript 会话是按连接隔离的,其批准绑定真实 session 与 turn,
290
- 跨 Agent 共用会互相串扰。
291
- - **`chrome` 需要扩展。** 只开开关、没装官方 ChatGPT 扩展、或没跑 `lcu browser install`,都不会有浏览器面。
292
- - **未在 Linux 上验证。** 见[前置条件](#前置条件)。
238
+ | 已启用的模式里工具从不出现 | 看日志里的 `decide … composed=`。如果组合出的 preset 不在 `presets` 里,修白名单。如果没有 `agent-preset/selected` 行,说明模式从未被确定。 |
239
+ | 加载时报 `computer use is unavailable (…)`,或没有工具 | 应用解析失败。日志里有原因和路径;检查 `app`。 |
240
+ | `no userQuestions service -> cancel (fail closed)` | 这个 profile 里没挂载提问界面。 |
241
+ | `refusing to approve the app hosting this agent` | 按设计工作;换一个应用。 |
242
+ | ChatGPT 应用仍显示某应用正被 computer use 占用 | 让模型调 `computer_use_stop`,或关掉会话 —— 连接拥有运行时进程树,退出时会释放。 |
243
+ | `chrome: true` 但 Chrome 标签页一直不出现 | 那个浏览器 profile 里没启用官方扩展。打开 `chrome://extensions` 启用它,并确认它出现在 `cua.getState()` 里。 |
244
+ | 某应用后续每个回合都报 "explicitly stopped by the user" | 宿主应用的回合清理没跑成。检查日志里的 `turn cleanup` 失败;退出并重启 ChatGPT 应用可清除该状态。 |
245
+
246
+ `node scripts/probe-lcu.mjs` 会在**完全不涉及 harness** 的情况下启动运行时,打印协议版本、服务器身份、指令长度和工具列表 —— 用来区分是插件问题还是运行时问题。
247
+
248
+ `helper/sky-service.mjs` 有三个诊断开关(默认全关),因为它运行的地方**显而易见的通道都不可用**:运行时的 JavaScript 沙箱拒绝文件写入,而且它会捕获 console 输出。`DSH_SKY_DEBUG=1` 追踪钩子,`DSH_SKY_REPORT=1` 让下一次调用带着上一次清理的结果失败,`DSH_SKY_FORCE_ERROR=1` 用来证明模块确实被加载了。
249
+
250
+ ## 与 LCU 的关系
251
+
252
+ [LCU](https://github.com/amontlabs/lcu)(MIT,Amont Labs)是证明这条路可行的项目:*Codex computer use, decoupled from the app*。它定位同一个运行时并把它作为 MCP 暴露出来。
253
+
254
+ **本插件自己做了这件事,不再安装或调用 LCU。** 有意义的差别:
255
+
256
+ - **不需要第二个解释器。** LCU 的启动器是 Python,要求 3.12+;这里是 TypeScript,唯一的要求就是应用本身。attach 因此快了大约一个数量级。
257
+ - **没有监督者进程,也不需要 Apple Events。** 回合清理走应用自己的 IPC,而不是一个去 shell 出签名客户端的监督者。
258
+ - **在失败那一刻给出诊断。** 应用在加载时就解析并报告,每个 attach、启动和批准决定都是日志里的一行。
259
+
260
+ ## 配套工具
261
+
262
+ `scripts/` 还带了两个给姊妹 Codex 子代理 bundle 的工具,因为同一个 profile 通常两个都要:
263
+
264
+ - **`update-codex.mjs`** —— 把 profile 的 `@openai/codex` 保持在仍能通过三道闸(握手、协议 schema 断言、真实回合)的最新版本,失败时自动回滚。已发布的 `@deepseek-ai/dsh-subagent-codex` 钉在 `0.153.4`,它并不服务当前所有 ChatGPT 账号模型;这个脚本通过 profile 级、限定范围的 pnpm override 把它顶上去。`node scripts/update-codex.mjs --help` 里有 `--check`、`--verify-only`、`--to` 和 `--rollback`。
265
+ - **`codex-baseline.json`** —— 最后一次三道闸全过的版本。
266
+
267
+ ## 已知限制与未做的工作
268
+
269
+ - **被委派的子代理无法被询问批准。** DSH 只接受来自活跃 runtime root 的人的回答,所以子代理的批准会 fail closed。子代理可以做不需要批准的只读工作;需要批准的事必须从顶层会话驱动。
270
+ - **按应用的 Stop 会被"请求它的那个回合"释放。** `computer_use_stop`(以及在宿主应用的"正在使用你的电脑"横幅上按 Esc)会请求运行时**在当前回合内**停用某一个应用。插件通过应用自己的 IPC 执行宿主应用的 turn-ended 清理,所以**下一个回合可以重新使用那个应用**;不带参数的 `computer_use_stop` 会报告当前持有哪些。如果某应用后续每个回合仍被拒绝,说明那次清理失败了,日志会写明。**常规使用 —— 截图、点击、输入、浏览器标签页 —— 不受影响**;插件的工具描述也这么写,所以模型不会自行去停用某个应用。
271
+ - **`js` 沙箱不能写文件。** 每一次写入都以 `EPERM` 失败,**包括临时目录**,所以截图无法从 `js` 里保存。图片改为以附件形式交给 harness,每张存下来的图都会在工具结果里带上它的宿主文件系统路径;把它复制进工作区是一行 `bash`(`install -m 644 '<path>' <target>` —— 存储对象的模式是 400)。插件在工具结果和注入的指令里都写了这两点。
272
+ - **macOS 权限属于 harness,不属于 OpenAI 助手。** macOS 把权限请求归给**负责进程**,而 harness 派生的一切都归给 harness 自己。所以屏幕录制和辅助功能必须给 **DeepSeek Harness** 打开;只给 ChatGPT 或 "Codex Computer Use" 是不够的,而且 macOS **不会自己弹窗**询问缺失的那些。
273
+ - **没有 Codex 账号时的浏览器面还没接。** 驱动页面靠桌面应用注册的 native messaging host。那台"没有 Codex 应用"的机器上需要的 relay(强制扩展的 agent-request header)还没有实现。
274
+ - **应用在会话运行期间更新,只做了警告。** 运行时是从应用更新时会替换的文件里执行的,所以长会话可能同时跑两代文件。应用更新后请重启 harness。
275
+ - **每个 Agent 一个连接。** 运行时的 JavaScript 会话是按连接隔离的,它的批准绑定到真实的会话和回合,所以让多个 Agent 共用一个连接会让两者交错。
276
+ - **未在 Linux / Windows 上验证。** 见[前置条件](#前置条件)。
293
277
 
294
278
  ## 开发
295
279
 
@@ -300,11 +284,10 @@ npm run build # 产出 lib/
300
284
  npm test # node --test,无需构建
301
285
  ```
302
286
 
303
- 连接层测试会连**真实安装的** `lcu`,未安装时自动跳过 —— 所以本地 `npm test` 有意义,
304
- CI 上也能通过。批准、投影、守卫三组是纯函数测试,永远会跑。
287
+ 连接与启动器测试会与**真实的**已安装计算机使用运行时通信,应用不存在时自动跳过,所以 `npm test` 在本地有意义、在 CI 上也能过。批准、投影、守卫和 wrapper 那几套是纯函数,始终会跑。
305
288
 
306
- 插件的**代码与配置都不会热更新**:运行中的进程持有它加载时的那个模块。改完要重新构建并重启。
289
+ 插件代码和配置**都不被 harness 热更新**:运行中的进程保留它加载的那个模块。改完要重新构建并重启。
307
290
 
308
291
  ## 许可
309
292
 
310
- MIT。LCU 为 MIT(Amont Labs);ChatGPT 应用及其 instructions 仍受其自身条款约束,且取自你本地的安装。
293
+ MIT。**没有内联任何东西**:原生那一半是应用自己的,本包没有运行时依赖。ChatGPT 应用及其指令仍按其自身条款,来自你本机的安装。