dsh-plugin-lcu 0.1.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/LICENSE +21 -0
- package/README.md +316 -0
- package/cordis.patch.yml +23 -0
- package/dev-types/dsh.d.ts +292 -0
- package/docs/README.zh.md +291 -0
- package/lib/approval.js +152 -0
- package/lib/approval.js.map +1 -0
- package/lib/connection.js +384 -0
- package/lib/connection.js.map +1 -0
- package/lib/diag.js +52 -0
- package/lib/diag.js.map +1 -0
- package/lib/host-guard.js +137 -0
- package/lib/host-guard.js.map +1 -0
- package/lib/index.js +318 -0
- package/lib/index.js.map +1 -0
- package/lib/tool.js +262 -0
- package/lib/tool.js.map +1 -0
- package/lib/types/approval.d.ts +71 -0
- package/lib/types/approval.d.ts.map +1 -0
- package/lib/types/connection.d.ts +178 -0
- package/lib/types/connection.d.ts.map +1 -0
- package/lib/types/diag.d.ts +17 -0
- package/lib/types/diag.d.ts.map +1 -0
- package/lib/types/host-guard.d.ts +46 -0
- package/lib/types/host-guard.d.ts.map +1 -0
- package/lib/types/index.d.ts +49 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/tool.d.ts +90 -0
- package/lib/types/tool.d.ts.map +1 -0
- package/package.json +79 -0
- package/scripts/codex-baseline.json +10 -0
- package/scripts/gen-presets.mjs +266 -0
- package/scripts/probe-lcu.mjs +154 -0
- package/scripts/update-codex.mjs +525 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# dsh-plugin-lcu
|
|
2
|
+
|
|
3
|
+
[English](../README.md) | 中文
|
|
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 能看屏幕、点鼠标、操作真实浏览器标签页。
|
|
7
|
+
|
|
8
|
+
LCU 把 **ChatGPT 桌面端内置的那套 computer-use 运行时**包装成 MCP server。本插件把它变成 DSH 的一等能力:
|
|
9
|
+
启用了模式的会话会拿到一个 `js` 工具。**全程不涉及 Codex 认证** —— 运行时来自你本地的 ChatGPT 安装,
|
|
10
|
+
LCU 从不解包、下载、认证或改写它。
|
|
11
|
+
|
|
12
|
+
## 目录
|
|
13
|
+
|
|
14
|
+
- [能力](#能力)
|
|
15
|
+
- [前置条件](#前置条件)
|
|
16
|
+
- [安装](#安装)
|
|
17
|
+
- [使用](#使用)
|
|
18
|
+
- [配置](#配置)
|
|
19
|
+
- [批准与安全模型](#批准与安全模型)
|
|
20
|
+
- [实现说明](#实现说明)
|
|
21
|
+
- [排障](#排障)
|
|
22
|
+
- [随附工具](#随附工具)
|
|
23
|
+
- [已知限制](#已知限制)
|
|
24
|
+
- [开发](#开发)
|
|
25
|
+
- [许可](#许可)
|
|
26
|
+
|
|
27
|
+
## 能力
|
|
28
|
+
|
|
29
|
+
两个模型可见工具,**schema 完全由服务端提供**,本插件不自己发明:
|
|
30
|
+
|
|
31
|
+
| 工具 | 作用 |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `js` | 针对 `cua` 桌面/浏览器 API 执行一段 JavaScript。首次调用会返回 API 文档;选中应用或标签页时结果里会带初始 UI 状态。 |
|
|
34
|
+
| `js_reset` | 丢弃持久化的 JavaScript 会话,重建运行时。 |
|
|
35
|
+
|
|
36
|
+
另有两个 host-only 工具只对宿主可见、**绝不暴露给模型**:`turn_ended`(每轮清理)与
|
|
37
|
+
`js_add_node_module_dir`。
|
|
38
|
+
|
|
39
|
+
截图会作为**持久化图片**经 DSH 的 attachment store 送达 —— 所以声明了 image input 的模型路由是真的能"看见"屏幕的。
|
|
40
|
+
|
|
41
|
+
## 前置条件
|
|
42
|
+
|
|
43
|
+
| | |
|
|
44
|
+
|---|---|
|
|
45
|
+
| 操作系统 | Apple Silicon 上的 macOS。LCU 也支持 Linux,但本插件在 macOS 上开发与验证。 |
|
|
46
|
+
| ChatGPT 桌面端 | 已安装、OpenAI 签名的官方版本。运行时与 instructions 由它提供。 |
|
|
47
|
+
| Python | 3.12 或更新,位于 `PATH`,或 `/opt/homebrew/bin`、`/usr/local/bin`、`/usr/bin`。 |
|
|
48
|
+
| LCU | 需另行安装,见下。 |
|
|
49
|
+
| DSH | 一个你能装 bundle 的 profile。 |
|
|
50
|
+
|
|
51
|
+
插件面向 **macOS**:`host-guard` 依赖 `ps`/`plutil`,生命周期走 LCU 的 macOS 路径。移植 Linux 需要改这两处。
|
|
52
|
+
|
|
53
|
+
## 安装
|
|
54
|
+
|
|
55
|
+
### 1. 安装 LCU
|
|
56
|
+
|
|
57
|
+
下载对应平台的 release 归档,**校验 checksum**,然后运行它自带的安装器。**不要注册其他 harness** ——
|
|
58
|
+
本插件就是你的 harness。
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
TAG=v0.9.6
|
|
62
|
+
TARGET=darwin-arm64
|
|
63
|
+
curl -fLO "https://github.com/amontlabs/lcu/releases/download/$TAG/lcu-${TAG#v}-$TARGET.tar.gz"
|
|
64
|
+
curl -fLO "https://github.com/amontlabs/lcu/releases/download/$TAG/lcu-${TAG#v}-$TARGET.tar.gz.sha256"
|
|
65
|
+
shasum -a 256 -c "lcu-${TAG#v}-$TARGET.tar.gz.sha256" # 必须打印 OK
|
|
66
|
+
tar -xzf "lcu-${TAG#v}-$TARGET.tar.gz" && cd "lcu-${TAG#v}-$TARGET"
|
|
67
|
+
./scripts/install.sh --runtime-only --yes
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
确认运行时能加载:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
~/.local/share/lcu/current/bin/lcu doctor --non-interactive
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
期望看到 `Original Mac provider loaded; app listing and app-state methods are available`。
|
|
77
|
+
隐私权限是**首次使用时**才授予的,不在这里。
|
|
78
|
+
|
|
79
|
+
### 2. 把插件装进 profile
|
|
80
|
+
|
|
81
|
+
装完插件后,该 profile 里就会有一行 LCU host 处于 active。**加载时不启动任何进程。**
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
dsh plugin --profile <profile> add dsh-plugin-lcu
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
桌面版 App 的 **desktop** profile 被应用独占,CLI 会拒绝;请改用 App 内的插件管理器
|
|
88
|
+
(Settings ▸ Plugins),它执行的是同一个 pnpm 操作。
|
|
89
|
+
|
|
90
|
+
### 3. 生成 preset
|
|
91
|
+
|
|
92
|
+
DSH 的 Agent preset **没有继承**:`config.plugins` 就是完整清单,而 patch 是整段替换而非深合并。
|
|
93
|
+
所以自定义 preset 必须重述它的基线。与其手抄,不如从**实际安装的那份** preset 生成:
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
node node_modules/dsh-plugin-lcu/scripts/gen-presets.mjs --profile ~/.dsh/profiles/<profile>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
它会在该 profile 的 `cordis.patch.yml` 里写入一个带标记的块,包含两个 preset:
|
|
100
|
+
|
|
101
|
+
| preset | 基线 | 额外内容 |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| `daily`(显示名「日常」) | 内置 `ptc` preset | 启用 `subagent_codex` |
|
|
104
|
+
| `heavy`(显示名「重活」) | 同上,且 `tool-presentation: both` | 以上全部,并且本插件会 attach |
|
|
105
|
+
|
|
106
|
+
DSH 升级后**重跑一次**,让副本跟上。`--dry-run` 只打印不写入,`--out FILE` 写到别处。
|
|
107
|
+
|
|
108
|
+
> `heavy` 特意把工具呈现设成 `both`。纯 `ptc` 呈现下模型只看到 `run_code`,
|
|
109
|
+
> `js` 就得作为一段 JavaScript 字符串**嵌套**在另一段 JavaScript 程序里。`both` 让 `js` 可以直接调。
|
|
110
|
+
|
|
111
|
+
### 4. 配置哪些模式能用
|
|
112
|
+
|
|
113
|
+
插件是**一个 root 行 + 显式 `presets` 白名单**。编辑已安装的 `cordis.patch.yml`
|
|
114
|
+
(或 profile patch)让它与你生成的 preset id 一致:
|
|
115
|
+
|
|
116
|
+
```yaml
|
|
117
|
+
- id: lcu
|
|
118
|
+
name: 'dsh-plugin-lcu'
|
|
119
|
+
config:
|
|
120
|
+
presets:
|
|
121
|
+
- heavy
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 5. 重启,然后先做一次无害调用
|
|
125
|
+
|
|
126
|
+
**必须重启** —— 插件的代码与配置都**不会热更新**。然后以 `heavy`(「重活」)模式新建任务,说一句:
|
|
127
|
+
|
|
128
|
+
> 用 `js` 工具执行 `await cua.getState();`,告诉我哪些应用正在运行。
|
|
129
|
+
|
|
130
|
+
首次操作某个应用时,运行时会请求批准,见下文。
|
|
131
|
+
|
|
132
|
+
### 可选:启用 Chrome
|
|
133
|
+
|
|
134
|
+
```yaml
|
|
135
|
+
config:
|
|
136
|
+
chrome: true
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
然后:
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
~/.local/share/lcu/current/bin/lcu browser install
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
在你要驱动的 Chrome profile 里启用**官方 ChatGPT 扩展**,并重启 Chrome(或在 `chrome://extensions`
|
|
146
|
+
把该扩展关掉再开),让它剥离旧的 native host、重连到 LCU 的 relay。
|
|
147
|
+
`lcu browser status` 会告诉你连接器是否已指向本次 LCU 安装。站点仍然逐个精确 origin 批准。
|
|
148
|
+
|
|
149
|
+
### 可选:预授权站点
|
|
150
|
+
|
|
151
|
+
运行时首次使用每个站点都会问一次。对可信来源跳过问询,就列出**精确 origin**
|
|
152
|
+
(必须能通过 `new URL(...).origin` 原样往返):
|
|
153
|
+
|
|
154
|
+
```yaml
|
|
155
|
+
config:
|
|
156
|
+
allowedOrigins:
|
|
157
|
+
- http://localhost:3000
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
诊断日志会记下每一个被问到的 origin,这是发现该填什么的最省事办法。
|
|
161
|
+
|
|
162
|
+
## 使用
|
|
163
|
+
|
|
164
|
+
新建任务时选择已启用的模式。工具是**按 Agent** 挂载的:其他模式的会话永远看不到它们,也永远不会拉起运行时。
|
|
165
|
+
|
|
166
|
+
典型请求:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
截一张 Finder 窗口的图,告诉我分辨率。
|
|
170
|
+
列出我当前 Chrome 的标签页。
|
|
171
|
+
打开 Safari 访问 example.com,读出页面标题。
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## 配置
|
|
175
|
+
|
|
176
|
+
| 字段 | 默认值 | 含义 |
|
|
177
|
+
|---|---|---|
|
|
178
|
+
| `command` | `~/.local/share/lcu/current/bin/lcu` | LCU 启动器。自定义 `--prefix` 时改这里。 |
|
|
179
|
+
| `chrome` | `false` | 传 `--chrome`,启用浏览器面。 |
|
|
180
|
+
| `audio` | `false` | 传 `--audio`,启用运行时的电脑录音 API。 |
|
|
181
|
+
| `presets` | `["heavy"]` | 允许拿到工具的 Agent preset id 列表。 |
|
|
182
|
+
| `allowedOrigins` | `[]` | 免问询的精确 HTTP(S) origin。非法项直接丢弃,**绝不放宽**。 |
|
|
183
|
+
| `sectionOrder` | `0` | 注入的 LCU instructions 在 prompt 中的排序。 |
|
|
184
|
+
|
|
185
|
+
## 批准与安全模型
|
|
186
|
+
|
|
187
|
+
**模型不能批准任何东西**,每个决定都是人的:
|
|
188
|
+
|
|
189
|
+
- **按应用批准。** 运行时在使用一个应用前会问。插件把运行时提供的选项原样呈现出来 ——
|
|
190
|
+
*Allow once*、运行时提供时的 *Allow for this session* 与 *Always allow*、以及 *Decline* ——
|
|
191
|
+
经 DSH 的提问界面。选择会映射回**恰好被提供的**那个 scope;运行时没提供的 scope 无法被授予。
|
|
192
|
+
- **站点批准。** 浏览器访问按精确 origin 逐次批准。`allowedOrigins` 只做精确匹配:
|
|
193
|
+
带尾部斜杠、带路径、大小写不同,都是**另一个 origin**,会去问而不是放行。
|
|
194
|
+
- **承载 agent 自己的应用永不可批准。** computer use 能点被批准应用里的任何东西,**包括批准弹窗本身**。
|
|
195
|
+
守卫在问用户之前就拒绝承载 agent 的应用 —— 依据是本进程的祖先链和一份 agent 宿主/终端名单。
|
|
196
|
+
- **Fail closed。** 没有提问界面、用户关掉弹窗、请求形状无法识别、调用被中断 —— 全部以 *cancel* 结束,
|
|
197
|
+
而运行时把 cancel 当作拒绝。
|
|
198
|
+
|
|
199
|
+
LCU 自身不保留权限缓存;`Always allow` 是**运行时**按应用记住的。
|
|
200
|
+
|
|
201
|
+
## 实现说明
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
src/connection.ts MCP 客户端:握手、工具发现、调用、elicitation、生命周期
|
|
205
|
+
src/approval.ts 批准形状识别与 label→value 映射
|
|
206
|
+
src/host-guard.ts 防自批准守卫
|
|
207
|
+
src/tool.ts 工具定义、文本投影、持久化截图
|
|
208
|
+
src/index.ts 插件:按 Agent attach、instructions、turn_ended、批准桥
|
|
209
|
+
src/diag.ts attach / 批准 诊断日志
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**不依赖任何 MCP SDK。** harness 自带的 MCP 桥声明 `capabilities: {}`,因此**答不了 elicitation** ——
|
|
213
|
+
而那正是 LCU 请求批准的方式;同时往 profile 插件里塞第二个 SDK 又会引入宿主并不拥有的版本。
|
|
214
|
+
MCP over stdio 就是 newline-delimited JSON-RPC,所以这条线自己实现。
|
|
215
|
+
再配合对 DSH 包的 **type-only import**,本插件**运行时零依赖**。
|
|
216
|
+
|
|
217
|
+
**工具是按 Agent 注册的,不是挂载时注册。** 服务端拥有工具 schema,只有握手之后才能取到。
|
|
218
|
+
连接在 Agent 创建时、或它提交 preset 选择时建立;插件贡献的一切都注册进**该 Agent 自己的 context**,
|
|
219
|
+
因此会随其销毁而回退。
|
|
220
|
+
|
|
221
|
+
**两种 preset 时序都处理了。** 新任务先以部署默认 preset 创建,选择器的选择是**之后**才生效的,
|
|
222
|
+
所以只看 `agent/created` 会看到错误的组合;注册表会重新发出 `agent-preset/selected`,插件同时也监听它。
|
|
223
|
+
|
|
224
|
+
**天然懒启动。** 加载时什么都不起。没有启用的会话,就没有 `lcu` 进程。
|
|
225
|
+
|
|
226
|
+
**instructions 会注入。** 服务端 `initialize.instructions` 成为该 Agent 的一个 prompt section。
|
|
227
|
+
它本身很短 —— API 手册在 `js` 的工具描述和首次调用结果里。
|
|
228
|
+
|
|
229
|
+
## 排障
|
|
230
|
+
|
|
231
|
+
插件关于 attach 与批准的每个决定都会追加到:
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
~/.dsh/lcu-diag.log
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
超过 1 MB 会清空重来。`LCU_DIAG=0` 可关闭。**这是第一个该看的地方**:
|
|
238
|
+
harness 没有当前会话可读的插件日志出口,而失败的 `agent/created` 监听器否则会被静默吞掉。
|
|
239
|
+
|
|
240
|
+
| 现象 | 原因与处理 |
|
|
241
|
+
|---|---|
|
|
242
|
+
| 启用的模式里始终没有工具 | 在日志里找 `decide … composed=`。若组合出的 preset 不在 `presets` 里,修正白名单;若压根没有 `agent-preset/selected` 行,说明模式从未提交。 |
|
|
243
|
+
| `no userQuestions service -> cancel (fail closed)` | 该 profile 没有挂载提问面。 |
|
|
244
|
+
| `refusing to approve the app hosting this agent` | 按设计如此;换一个应用。 |
|
|
245
|
+
| 一轮之后调用被挡住 | 运行时的一轮清理尚未结束;插件会在下次调用前重试,未成功前拒绝调用。 |
|
|
246
|
+
| `lcu doctor` 报 socket 路径错误 | 签名助手把 socket 绑在 home 目录下,路径超过 103 字节会被拒。换一个 home 路径更短的账号。 |
|
|
247
|
+
| attach 报 spawn 失败 | 直接跑 `~/.local/share/lcu/current/bin/lcu doctor`,再检查配置里的 `command`。 |
|
|
248
|
+
|
|
249
|
+
`node scripts/probe-lcu.mjs` 不经 harness 直连 LCU,打印协议版本、服务端身份、instructions 长度与工具清单 ——
|
|
250
|
+
用来把「插件的问题」和「LCU 的问题」分开。
|
|
251
|
+
|
|
252
|
+
## 随附工具
|
|
253
|
+
|
|
254
|
+
`scripts/` 里还有两个服务于同级 Codex subagent bundle 的工具(同一个 profile 通常两个都要):
|
|
255
|
+
|
|
256
|
+
- **`update-codex.mjs`** —— 把 profile 里的 `@openai/codex` 保持在**能通过三道门**的最新版本上
|
|
257
|
+
(握手、协议 schema 断言、一次真实回合),任一门不过自动回退。官方发布的
|
|
258
|
+
`@deepseek-ai/dsh-subagent-codex` 钉死 `0.153.4`,而它**并不能服务所有当前的 ChatGPT 账号模型**;
|
|
259
|
+
该脚本通过 profile 层、作用域限定的 pnpm override 把它顶上去。
|
|
260
|
+
用法见 `node scripts/update-codex.mjs --help`(`--check` / `--verify-only` / `--to` / `--rollback`)。
|
|
261
|
+
- **`codex-baseline.json`** —— 最近一次通过全部三道门的版本。
|
|
262
|
+
|
|
263
|
+
## 已知限制
|
|
264
|
+
|
|
265
|
+
- **被委派的子代理无法弹出批准。** DSH 只接受「精确的 live runtime root」上的人工作答,
|
|
266
|
+
因此子代理里的 LCU 批准会 fail closed。子代理可以做**不需要批准**的只读操作;
|
|
267
|
+
需要批准的事必须由顶层会话发起。
|
|
268
|
+
- **macOS 的一轮清理偶尔会超出宿主的等待。** 签名助手偶尔对 `turn-ended` 这一步响应很慢。
|
|
269
|
+
运行时会继续在后台清理并重试;插件会在它结束前**挡住下一次调用**,而不是在半拆解的桌面上操作。
|
|
270
|
+
- **每个 Agent 一条 LCU 连接。** LCU 的 JavaScript 会话是按连接隔离的,其批准绑定真实 session 与 turn,
|
|
271
|
+
跨 Agent 共用会互相串扰。
|
|
272
|
+
- **`chrome` 需要扩展。** 只开开关、没装官方 ChatGPT 扩展、或没跑 `lcu browser install`,都不会有浏览器面。
|
|
273
|
+
- **未在 Linux 上验证。** 见[前置条件](#前置条件)。
|
|
274
|
+
|
|
275
|
+
## 开发
|
|
276
|
+
|
|
277
|
+
```sh
|
|
278
|
+
npm install
|
|
279
|
+
npm run typecheck # strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes
|
|
280
|
+
npm run build # 产出 lib/
|
|
281
|
+
npm test # node --test,无需构建
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
连接层测试会连**真实安装的** `lcu`,未安装时自动跳过 —— 所以本地 `npm test` 有意义,
|
|
285
|
+
CI 上也能通过。批准、投影、守卫三组是纯函数测试,永远会跑。
|
|
286
|
+
|
|
287
|
+
插件的**代码与配置都不会热更新**:运行中的进程持有它加载时的那个模块。改完要重新构建并重启。
|
|
288
|
+
|
|
289
|
+
## 许可
|
|
290
|
+
|
|
291
|
+
MIT。LCU 为 MIT(Amont Labs);ChatGPT 应用及其 instructions 仍受其自身条款约束,且取自你本地的安装。
|
package/lib/approval.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LCU approval shapes.
|
|
3
|
+
*
|
|
4
|
+
* LCU's original runtime asks the host before it touches an app, and the request
|
|
5
|
+
* is a specific `elicitation/create` form. This ports the recognition and
|
|
6
|
+
* mapping rules from LCU's own shared client so a DSH host presents exactly the
|
|
7
|
+
* choices the runtime offered — no more (granting an unoffered persistence scope
|
|
8
|
+
* would let the agent keep desktop access the runtime meant to bound) and no
|
|
9
|
+
* fewer (dropping `always` would nag the user every turn).
|
|
10
|
+
*
|
|
11
|
+
* @module dsh-plugin-lcu/approval
|
|
12
|
+
*/
|
|
13
|
+
/** Persistence scopes the runtime can offer, in the order LCU presents them. */
|
|
14
|
+
const PERSISTENCE = [
|
|
15
|
+
['session', 'Allow for this session'],
|
|
16
|
+
['always', 'Always allow'],
|
|
17
|
+
];
|
|
18
|
+
/** The browser-origin approval's metadata key. */
|
|
19
|
+
const ORIGIN_TOOL_NAME = 'access_browser_origin';
|
|
20
|
+
const asObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value) ? value : undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Recognize the runtime's native-app approval request.
|
|
23
|
+
*
|
|
24
|
+
* The shape is deliberately narrow: an empty-schema form whose `_meta` names the
|
|
25
|
+
* computer-use connector and a concrete app. Every other elicitation the server
|
|
26
|
+
* may send is *not* this, and must be handled elsewhere or cancelled.
|
|
27
|
+
*
|
|
28
|
+
* @param request - one `elicitation/create` payload.
|
|
29
|
+
* @returns the recognized request, or `undefined` when it is a different shape.
|
|
30
|
+
*/
|
|
31
|
+
export function nativeAppApproval(request) {
|
|
32
|
+
const meta = asObject(request._meta);
|
|
33
|
+
const schema = asObject(request.requestedSchema);
|
|
34
|
+
const properties = asObject(schema?.properties);
|
|
35
|
+
const app = asObject(meta?.tool_params)?.app;
|
|
36
|
+
if (request.mode !== 'form')
|
|
37
|
+
return undefined;
|
|
38
|
+
if (typeof request.message !== 'string' || request.message === '')
|
|
39
|
+
return undefined;
|
|
40
|
+
if (schema?.type !== 'object' || properties === undefined)
|
|
41
|
+
return undefined;
|
|
42
|
+
if (Object.keys(properties).length !== 0)
|
|
43
|
+
return undefined;
|
|
44
|
+
const required = schema.required;
|
|
45
|
+
if (required !== undefined && (!Array.isArray(required) || required.length !== 0))
|
|
46
|
+
return undefined;
|
|
47
|
+
if (meta?.codex_approval_kind !== 'mcp_tool_call')
|
|
48
|
+
return undefined;
|
|
49
|
+
if (meta?.connector_id !== 'computer-use')
|
|
50
|
+
return undefined;
|
|
51
|
+
if (typeof app !== 'string' || app === '')
|
|
52
|
+
return undefined;
|
|
53
|
+
const requested = new Set(Array.isArray(meta?.persist) ? meta.persist : []);
|
|
54
|
+
const choices = [{ value: 'once', label: 'Allow once' }];
|
|
55
|
+
for (const [value, label] of PERSISTENCE) {
|
|
56
|
+
if (requested.has(value))
|
|
57
|
+
choices.push({ value, label });
|
|
58
|
+
}
|
|
59
|
+
choices.push({ value: 'decline', label: 'Decline' });
|
|
60
|
+
return { message: request.message, resource: app, choices };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Map the user's choice back to the response the runtime accepts.
|
|
64
|
+
*
|
|
65
|
+
* @param request - the same payload the choices came from.
|
|
66
|
+
* @param value - the chosen value, or `'cancel'` for a dismissed prompt.
|
|
67
|
+
* @returns an accept/decline/cancel answer; anything unrecognized cancels.
|
|
68
|
+
*/
|
|
69
|
+
export function nativeAppApprovalResponse(request, value) {
|
|
70
|
+
const approval = nativeAppApproval(request);
|
|
71
|
+
if (approval === undefined)
|
|
72
|
+
return { action: 'cancel' };
|
|
73
|
+
if (value === 'cancel')
|
|
74
|
+
return { action: 'cancel' };
|
|
75
|
+
if (value === 'decline')
|
|
76
|
+
return { action: 'decline' };
|
|
77
|
+
const offered = approval.choices.some((choice) => choice.value === value);
|
|
78
|
+
if (!offered)
|
|
79
|
+
return { action: 'cancel' };
|
|
80
|
+
if (value === 'once')
|
|
81
|
+
return { action: 'accept', content: {} };
|
|
82
|
+
if (value === 'session' || value === 'always') {
|
|
83
|
+
return { action: 'accept', content: {}, _meta: { persist: value } };
|
|
84
|
+
}
|
|
85
|
+
return { action: 'cancel' };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Map a presented option label back to the value the runtime accepts.
|
|
89
|
+
*
|
|
90
|
+
* These are different strings: the question surface answers with the option's
|
|
91
|
+
* LABEL (`"Always allow"`), while the elicitation response needs the choice
|
|
92
|
+
* VALUE (`"always"`). Passing the label straight through looks up nothing and
|
|
93
|
+
* silently cancels a choice the user actually made.
|
|
94
|
+
*
|
|
95
|
+
* @param approval - the request the labels were presented for.
|
|
96
|
+
* @param label - the label the user selected, or `undefined` when dismissed.
|
|
97
|
+
* @returns the choice value, or `'cancel'` when the label matches no option.
|
|
98
|
+
*/
|
|
99
|
+
export function approvalValueForLabel(approval, label) {
|
|
100
|
+
if (label === undefined)
|
|
101
|
+
return 'cancel';
|
|
102
|
+
return approval.choices.find((choice) => choice.label === label)?.value ?? 'cancel';
|
|
103
|
+
}
|
|
104
|
+
/** The exact origin a browser-site approval names, when it names one. */
|
|
105
|
+
export function originApprovalOrigin(request) {
|
|
106
|
+
const meta = asObject(request._meta) ?? asObject(request.meta);
|
|
107
|
+
if (meta?.tool_name !== ORIGIN_TOOL_NAME || typeof meta.origin !== 'string')
|
|
108
|
+
return undefined;
|
|
109
|
+
try {
|
|
110
|
+
const url = new URL(meta.origin);
|
|
111
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
112
|
+
return undefined;
|
|
113
|
+
if (url.origin !== meta.origin)
|
|
114
|
+
return undefined;
|
|
115
|
+
return url.origin;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** Whether the request names an already-authorized exact origin. */
|
|
122
|
+
export function isPreApprovedOrigin(request, allowedOrigins) {
|
|
123
|
+
const origin = originApprovalOrigin(request);
|
|
124
|
+
return origin !== undefined && allowedOrigins.has(origin);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Normalize a set of user-configured origins.
|
|
128
|
+
*
|
|
129
|
+
* Only exact HTTP(S) origins are accepted; anything else is dropped rather than
|
|
130
|
+
* widened, because a relaxed match here is a site the user never approved.
|
|
131
|
+
*
|
|
132
|
+
* @param values - raw origin strings from configuration.
|
|
133
|
+
* @returns the accepted origins.
|
|
134
|
+
*/
|
|
135
|
+
export function normalizeOrigins(values) {
|
|
136
|
+
const accepted = new Set();
|
|
137
|
+
for (const value of values) {
|
|
138
|
+
try {
|
|
139
|
+
const url = new URL(value);
|
|
140
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
141
|
+
continue;
|
|
142
|
+
if (url.origin !== value)
|
|
143
|
+
continue;
|
|
144
|
+
accepted.add(value);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return accepted;
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=approval.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval.js","sourceRoot":"","sources":["../src/approval.ts"],"names":[],"mappings":"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"}
|