@anweat/dsh-browser 0.1.4 → 0.1.6
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/README.md +229 -92
- package/cordis.patch.yml +15 -15
- package/lib/approval-policy.d.ts +11 -0
- package/lib/approval-policy.js +33 -0
- package/lib/approval-policy.js.map +1 -0
- package/lib/auth-profiles.d.ts +20 -0
- package/lib/auth-profiles.js +28 -0
- package/lib/auth-profiles.js.map +1 -0
- package/lib/automation.d.ts +67 -0
- package/lib/automation.js +181 -0
- package/lib/automation.js.map +1 -0
- package/lib/browser-service.d.ts +63 -0
- package/lib/browser-service.js +222 -62
- package/lib/browser-service.js.map +1 -1
- package/lib/config.d.ts +11 -0
- package/lib/config.js +23 -0
- package/lib/config.js.map +1 -1
- package/lib/deps.js +3 -7
- package/lib/deps.js.map +1 -1
- package/lib/index.d.ts +6 -1
- package/lib/index.js +11 -0
- package/lib/index.js.map +1 -1
- package/lib/rule-packs.d.ts +32 -0
- package/lib/rule-packs.js +77 -0
- package/lib/rule-packs.js.map +1 -0
- package/lib/scripts.d.ts +35 -0
- package/lib/scripts.js +181 -0
- package/lib/scripts.js.map +1 -0
- package/lib/tools.js +262 -1
- package/lib/tools.js.map +1 -1
- package/package.json +78 -74
package/README.md
CHANGED
|
@@ -1,92 +1,229 @@
|
|
|
1
|
-
# dsh-browser
|
|
2
|
-
|
|
3
|
-
自包含的浏览器运行时插件 for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)。
|
|
4
|
-
|
|
5
|
-
把 **Playwright(chromium 内核)** 与 **OpenCLI** 作为插件自身的 npm 依赖打包(优先插件本地,缺省回退全局复用),对外提供一个 `browser` 服务 + 一组交互式浏览器工具。`dsh-web-search-pro` 通过 `inject: ['browser']` 注入该服务,驱动它的 playwright / opencli 后端——**不再依赖全局 CLI**。
|
|
6
|
-
|
|
7
|
-
## 安装
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
dsh plugin --profile web add @anweat/dsh-browser
|
|
11
|
-
# 或本地目录 / tarball:
|
|
12
|
-
dsh plugin --profile web add ./dsh-browser
|
|
13
|
-
# 重启(web profile 关闭了 HMR):
|
|
14
|
-
dsh --profile web
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
> 依赖 `@deepseek-ai/*` 已发布到 npm(`^0.1.0-rc.6`)。
|
|
18
|
-
> 若你的 harness 是本地源码 checkout(如 `0.1.0-rc.5`),版本号可能有出入——用
|
|
19
|
-
> `dsh plugin --profile web add ./<path>` 并在 profile 的 `pnpm-workspace.yaml`
|
|
20
|
-
> 里对齐版本后重装即可。
|
|
21
|
-
|
|
22
|
-
##
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
|
27
|
-
|
|
28
|
-
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
`
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
1
|
+
# dsh-browser
|
|
2
|
+
|
|
3
|
+
自包含的浏览器运行时插件 for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)。
|
|
4
|
+
|
|
5
|
+
把 **Playwright(chromium 内核)** 与 **OpenCLI** 作为插件自身的 npm 依赖打包(优先插件本地,缺省回退全局复用),对外提供一个 `browser` 服务 + 一组交互式浏览器工具。`dsh-web-search-pro` 通过 `inject: ['browser']` 注入该服务,驱动它的 playwright / opencli 后端——**不再依赖全局 CLI**。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
dsh plugin --profile web add @anweat/dsh-browser
|
|
11
|
+
# 或本地目录 / tarball:
|
|
12
|
+
dsh plugin --profile web add ./dsh-browser
|
|
13
|
+
# 重启(web profile 关闭了 HMR):
|
|
14
|
+
dsh --profile web
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
> 依赖 `@deepseek-ai/*` 已发布到 npm(`^0.1.0-rc.6`)。
|
|
18
|
+
> 若你的 harness 是本地源码 checkout(如 `0.1.0-rc.5`),版本号可能有出入——用
|
|
19
|
+
> `dsh plugin --profile web add ./<path>` 并在 profile 的 `pnpm-workspace.yaml`
|
|
20
|
+
> 里对齐版本后重装即可。
|
|
21
|
+
|
|
22
|
+
## 快速使用与适用情形
|
|
23
|
+
|
|
24
|
+
安装并重启后,可先让模型调用 `browser_status`,再按任务选择工具:
|
|
25
|
+
|
|
26
|
+
| 情形 | 推荐方式 | 关键边界 |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| 公开网页读取、截图 | `browser_open` → `browser_read` / `browser_screenshot` | 不需要登录态 |
|
|
29
|
+
| 表单、分页、懒加载 | `browser_click` / `browser_type` / `browser_scroll` | 选择器由调用方明确提供 |
|
|
30
|
+
| 登录后站点 | `authProfile` | 必须配置 `allowedDomains`;默认不回写 Cookie |
|
|
31
|
+
| 固定站点增强 | `rulePack` | 只允许有界步骤;本地 init script 必须 SHA-256 固定且 ≤64KB |
|
|
32
|
+
| 模型生成的多步操作 | `browser_recipe_run` | 声明式步骤;包含页面写操作时触发 DSH 原生一次性审批 |
|
|
33
|
+
| 默认只读脚本 | `browser_script_catalog` → `browser_script_run_builtin` | 内置 article/links/JSON-LD/forms,不执行外来代码 |
|
|
34
|
+
| 外部模型生成 UserScript | `browser_script_validate` → `browser_userscript_run` | 必须 `@match` + `@grant none`;执行前原生审批 |
|
|
35
|
+
| Reddit/小红书等 OpenCLI 平台 | `browser_opencli_status` / `browser_opencli_run` | 通用调用始终审批;Chrome 扩展与目标站点登录态必须在线 |
|
|
36
|
+
|
|
37
|
+
DSH 会话示例:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
先调用 browser_status;然后用 browser_open 打开目标页。
|
|
41
|
+
若页面需要登录,使用 authProfile=forum;不要把 Cookie 放进工具参数。
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 内核与依赖的"打包 vs 复用"
|
|
45
|
+
|
|
46
|
+
| 层 | 实际是什么 | 打包还是复用 |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| **chromium 内核** | 共享缓存 `%LOCALAPPDATA%\ms-playwright`(约 400MB) | **永远复用共享缓存**,不塞进插件、不重复下载;缺失时 `browser_install` 一键补 |
|
|
49
|
+
| **playwright 驱动**(JS 包) | `playwright` npm 依赖 | 插件本地 node_modules 优先,缺省回退全局 npm |
|
|
50
|
+
| **opencli**(纯 Node CLI) | `@jackwener/opencli` npm 依赖 | 同上,本地优先 / 全局复用 |
|
|
51
|
+
|
|
52
|
+
## 服务:`browser`
|
|
53
|
+
|
|
54
|
+
`dsh-browser` 在 `apply()` 里 `ctx.provide('browser', service)`。任何插件声明
|
|
55
|
+
`inject: ['browser']` 即可消费:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
export const inject = ['tools', 'browser']
|
|
59
|
+
export function apply(ctx: Context) {
|
|
60
|
+
const browser = ctx.get('browser') as BrowserService
|
|
61
|
+
// browser.render / snapshot / searchResults / opencli / recipe /
|
|
62
|
+
// runBuiltinScript / runUserscript / open / click / type / scroll / read / screenshot / close
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
服务接口(结构性,无需共享类型包)见 `src/browser-service.ts`。
|
|
67
|
+
|
|
68
|
+
## 工具(16 个)
|
|
69
|
+
|
|
70
|
+
| 工具 | 作用 |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `browser_open` | 打开 URL,返回标题/可读文本/全页截图路径(持久页会话) |
|
|
73
|
+
| `browser_click` | 按 CSS 选择器点击 |
|
|
74
|
+
| `browser_type` | 向 input/textarea 输入 |
|
|
75
|
+
| `browser_scroll` | 纵向滚动(触发懒加载) |
|
|
76
|
+
| `browser_read` | 读当前页 URL/标题/文本(不截图) |
|
|
77
|
+
| `browser_screenshot` | 当前页全页截图 |
|
|
78
|
+
| `browser_close` | 关闭当前页(下次 open 全新) |
|
|
79
|
+
| `browser_status` | 运行时状态(channel/headless/chromium 是否就绪/opencli 是否启用/当前页) |
|
|
80
|
+
| `browser_install` | 安装 playwright chromium(`browser_status` 报缺失时执行一次) |
|
|
81
|
+
| `browser_script_catalog` | 列出内置只读脚本及其 SHA-256 |
|
|
82
|
+
| `browser_script_validate` | 解析外部 UserScript 的元数据、域名、grant、能力与哈希,不执行 |
|
|
83
|
+
| `browser_script_run_builtin` | 在独立 Playwright context 中运行内置只读脚本 |
|
|
84
|
+
| `browser_userscript_run` | 运行外部 UserScript;强制域名匹配和 DSH 原生一次性审批 |
|
|
85
|
+
| `browser_recipe_run` | 最多 25 步 Playwright Recipe;支持等待、定位、表单、键盘、提取、断言和截图 |
|
|
86
|
+
| `browser_opencli_status` | 实际运行 OpenCLI doctor,报告 daemon/extension/profile 连通性 |
|
|
87
|
+
| `browser_opencli_run` | 通用 OpenCLI argv 网关;始终触发 DSH 原生一次性审批 |
|
|
88
|
+
|
|
89
|
+
## 外部模型脚本:推荐流程
|
|
90
|
+
|
|
91
|
+
外部模型可以输出 Tampermonkey/UserScript 格式源码,但不要直接执行。让当前 DSH Agent 先调用
|
|
92
|
+
`browser_script_validate`,展示名称、`@match`、SHA-256 和能力,再调用
|
|
93
|
+
`browser_userscript_run`。执行调用会进入 Harness 的 `tools/pre-execute → approval` 原生流程;用户拒绝、
|
|
94
|
+
没有 approval 服务或调用不属于 Agent 时都不会运行。
|
|
95
|
+
|
|
96
|
+
最小脚本示例:
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
// ==UserScript==
|
|
100
|
+
// @name Read Search Cards
|
|
101
|
+
// @match https://example.com/search*
|
|
102
|
+
// @grant none
|
|
103
|
+
// ==/UserScript==
|
|
104
|
+
return [...document.querySelectorAll('.result')].slice(0, 20).map(card => ({
|
|
105
|
+
title: card.querySelector('h2')?.textContent?.trim() || '',
|
|
106
|
+
url: card.querySelector('a')?.href || '',
|
|
107
|
+
}))
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
当前兼容的是 UserScript 元数据和页面脚本执行模型,不模拟完整 Tampermonkey:
|
|
111
|
+
|
|
112
|
+
- 只支持 `@grant none`;`GM_cookie`、`GM_xmlhttpRequest`、`unsafeWindow` 等不提供。
|
|
113
|
+
- 不支持 `@require`,避免审批过的源码在运行时再拉取未审查代码。
|
|
114
|
+
- 源码 ≤64KB、结果 ≤100,000 字符、单次运行最长 30 秒。
|
|
115
|
+
- 使用显式 URL,新建独立 Playwright context;需要登录态时只能选已限域的 `authProfile`。
|
|
116
|
+
- 审批代表允许该脚本以当前站点登录身份操作页面;静态能力报告只用于解释,不是沙箱。
|
|
117
|
+
|
|
118
|
+
常见读取任务优先用内置脚本:`article-clean`、`links`、`jsonld`、`forms`。它们不返回表单当前值,
|
|
119
|
+
也不触发点击或网络写操作。
|
|
120
|
+
|
|
121
|
+
## Playwright Recipe
|
|
122
|
+
|
|
123
|
+
Recipe 适合让模型生成可审计、可复现的多步操作,不必生成 JavaScript:
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"url": "https://example.com/search",
|
|
128
|
+
"steps": [
|
|
129
|
+
{ "type": "wait", "condition": "selector", "value": "#query" },
|
|
130
|
+
{ "type": "fill", "selector": "#query", "value": "DeepSeek Harness" },
|
|
131
|
+
{ "type": "press", "selector": "#query", "key": "Enter" },
|
|
132
|
+
{ "type": "wait", "condition": "load" },
|
|
133
|
+
{ "type": "extract", "selector": "main", "mode": "links", "limit": 30 },
|
|
134
|
+
{ "type": "screenshot" }
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
支持的步骤为:`wait`、`click`、`fill`、`type`、`press`、`select`、`check`、`hover`、
|
|
140
|
+
`scroll`、`extract`、`assert`、`screenshot`。纯读取步骤直接执行;出现点击、输入、键盘、选择、
|
|
141
|
+
勾选、悬停或滚动时,整个 Recipe 只询问一次审批,批准后顺序执行。
|
|
142
|
+
|
|
143
|
+
## 配置(cordis.yml / patch config)
|
|
144
|
+
|
|
145
|
+
```yaml
|
|
146
|
+
- insert:
|
|
147
|
+
- id: browser
|
|
148
|
+
name: '@anweat/dsh-browser'
|
|
149
|
+
config:
|
|
150
|
+
channel: chromium # 'chromium'(打包内核)| 'msedge'(系统 Edge)
|
|
151
|
+
headless: true
|
|
152
|
+
opencliEnabled: true
|
|
153
|
+
storageStatePath: '' # Playwright 登录态 JSON(复用已登录会话)
|
|
154
|
+
authProfiles:
|
|
155
|
+
forum:
|
|
156
|
+
storageStatePath: 'D:/secrets/forum.json'
|
|
157
|
+
allowedDomains: [example.com]
|
|
158
|
+
persistState: false # 默认只读;true 才会原子回写刷新后的状态
|
|
159
|
+
rulePacks:
|
|
160
|
+
forum-enhanced:
|
|
161
|
+
matches: [example.com]
|
|
162
|
+
initScriptPath: 'D:/dsh/rules/forum.js'
|
|
163
|
+
initScriptSha256: '<64位sha256>'
|
|
164
|
+
steps:
|
|
165
|
+
- { type: waitFor, selector: '#results', timeoutMs: 10000 }
|
|
166
|
+
- { type: scroll, deltaY: 1600, repeat: 2, waitMs: 300 }
|
|
167
|
+
autoInstall: false # 缺内核时是否自动 install chromium
|
|
168
|
+
verbose: false
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## 登录态复用
|
|
172
|
+
|
|
173
|
+
- `channel: chromium` + `storageStatePath` 指向一份 storageState JSON,即可用你已登录的身份抓受限页面。
|
|
174
|
+
- 新配置优先使用 `authProfiles`:按名称复用全局登录态,但必须用 `allowedDomains` 限域;默认只读,避免一次搜索意外改写 Cookie Vault。
|
|
175
|
+
- `browser_open` 和 web-search-pro 的平台搜索可选择 `authProfile` / `rulePack`。`browser_status` 只显示 profile 名称、域名和回写状态,不显示文件路径或 Cookie。
|
|
176
|
+
- RulePack 仍只允许有界动作;init script 必须是本地、SHA-256 固定且不超过 64KB。外部模型 JavaScript 使用独立的 UserScript 工具,并强制一次性审批,不能冒充 RulePack。
|
|
177
|
+
- 生成登录态:`npx playwright codegen --save-storage=storageState.json`(或复用 `dsh-web-search-pro` 的 `scripts/save-login.mjs`),把产物路径填进 `storageStatePath`。
|
|
178
|
+
- opencli 的社交平台后端(小红书/推特/Reddit/IG/FB)仍需浏览器扩展 + 登录态在线,即使 opencli 已打包为依赖也绕不开扩展。
|
|
179
|
+
|
|
180
|
+
### OpenCLI 连接检查
|
|
181
|
+
|
|
182
|
+
插件运行时优先使用自己依赖的 OpenCLI。需要在终端排查 Browser Bridge 时,可全局安装同一 CLI 并检查:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
npm i -g @jackwener/opencli
|
|
186
|
+
opencli daemon status
|
|
187
|
+
opencli doctor
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
健康状态应同时包含 daemon running、extension connected 和一个 connected Chrome profile。仅安装 npm 包不等于 Browser Bridge 可用;Chrome 扩展断开时,OpenCLI 社区搜索会明确失败,而普通 Playwright 浏览器工具不受影响。
|
|
191
|
+
|
|
192
|
+
插件内先调用 `browser_opencli_status`,不要只看 `browser_status.opencliEnabled`。后者表示配置开关,
|
|
193
|
+
前者才是真实连接。通用调用以 argv 数组传入,不经过 shell,也不会自行拼接引号:
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
{
|
|
197
|
+
"profile": "chrome",
|
|
198
|
+
"args": ["reddit", "search", "DeepSeek Harness", "-f", "json"]
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
优先级建议:已有站点 adapter(`opencli <site> <command>`)→ `opencli web read` / `extract` →
|
|
203
|
+
`browser network` → DOM state/find/action → 最后才是只读 `eval`。`opencli browser` 必须包含显式 session:
|
|
204
|
+
|
|
205
|
+
```text
|
|
206
|
+
["browser", "research", "open", "https://example.com"]
|
|
207
|
+
["browser", "research", "state"]
|
|
208
|
+
["browser", "research", "network", "--filter", "title,url"]
|
|
209
|
+
["browser", "research", "extract", "--selector", "main"]
|
|
210
|
+
["browser", "research", "close"]
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`browser_opencli_run` 是通用高级入口,可能调用发布、删除、发帖等 adapter,因此无论命令看起来是否只读,
|
|
214
|
+
都要求原生一次性审批。常规搜索仍优先走 `dsh-web-search-pro` 的只读工具。
|
|
215
|
+
|
|
216
|
+
## 发布 / 构建
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
pnpm install # 装依赖(playwright / opencli / @deepseek-ai/*)
|
|
220
|
+
pnpm test
|
|
221
|
+
pnpm run build # tsc → lib/
|
|
222
|
+
node scripts/install-browser.mjs # 安装 chromium 内核(发布前验证,可选)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## 与 dsh-web-search-pro 的关系
|
|
226
|
+
|
|
227
|
+
`dsh-web-search-pro` 现在 `inject: ['browser']`,其 `web_snapshot` / `web_fetch_pro`(playwright 后端) /
|
|
228
|
+
`web_platform_search`(中文社区 playwright + 社交平台 opencli) 全部走本插件的 `browser` 服务。
|
|
229
|
+
两者可独立安装,但 web-search-pro 的浏览器类能力依赖 dsh-browser 先行提供 `browser` 服务(Cordis `inject` 自动排序,无需手动控制挂载顺序)。
|
package/cordis.patch.yml
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
# dsh-browser bundle patch — installs the browser runtime as one more row over
|
|
2
|
-
# the active profile. Install with:
|
|
3
|
-
# dsh plugin --profile <name> add @anweat/dsh-browser
|
|
4
|
-
# then boot the profile (the web profile has HMR disabled, so a restart applies it).
|
|
5
|
-
#
|
|
6
|
-
# Login state:
|
|
7
|
-
# README)
|
|
8
|
-
- insert:
|
|
9
|
-
- id: browser
|
|
10
|
-
name: '@anweat/dsh-browser'
|
|
11
|
-
config:
|
|
12
|
-
channel: chromium
|
|
13
|
-
headless: true
|
|
14
|
-
opencliEnabled: true
|
|
15
|
-
verbose: false
|
|
1
|
+
# dsh-browser bundle patch — installs the browser runtime as one more row over
|
|
2
|
+
# the active profile. Install with:
|
|
3
|
+
# dsh plugin --profile <name> add @anweat/dsh-browser
|
|
4
|
+
# then boot the profile (the web profile has HMR disabled, so a restart applies it).
|
|
5
|
+
#
|
|
6
|
+
# Login state: prefer named `authProfiles` with explicit `allowedDomains` (see
|
|
7
|
+
# README). `storageStatePath` remains as a legacy global fallback.
|
|
8
|
+
- insert:
|
|
9
|
+
- id: browser
|
|
10
|
+
name: '@anweat/dsh-browser'
|
|
11
|
+
config:
|
|
12
|
+
channel: chromium
|
|
13
|
+
headless: true
|
|
14
|
+
opencliEnabled: true
|
|
15
|
+
verbose: false
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Approval classification for multi-action and arbitrary-code browser tools. */
|
|
2
|
+
export type BrowserPolicyDecision = {
|
|
3
|
+
kind: 'allow';
|
|
4
|
+
} | {
|
|
5
|
+
kind: 'deny';
|
|
6
|
+
reason: string;
|
|
7
|
+
} | {
|
|
8
|
+
kind: 'ask';
|
|
9
|
+
reason: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function browserPolicyDecision(name: string, args: unknown): BrowserPolicyDecision;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Approval classification for multi-action and arbitrary-code browser tools. */
|
|
2
|
+
import { recipeNeedsApproval } from "./automation.js";
|
|
3
|
+
import { validateUserscript } from "./scripts.js";
|
|
4
|
+
export function browserPolicyDecision(name, args) {
|
|
5
|
+
if (name === 'browser_userscript_run') {
|
|
6
|
+
const input = args;
|
|
7
|
+
if (typeof input.source !== 'string' || typeof input.url !== 'string')
|
|
8
|
+
return { kind: 'deny', reason: 'external userscript requires source and URL' };
|
|
9
|
+
const validation = validateUserscript(input.source, input.url);
|
|
10
|
+
if (!validation.valid)
|
|
11
|
+
return { kind: 'deny', reason: 'invalid external userscript: ' + validation.errors.join('; ') };
|
|
12
|
+
const host = new URL(input.url).hostname;
|
|
13
|
+
return {
|
|
14
|
+
kind: 'ask',
|
|
15
|
+
reason: `Run external userscript "${validation.metadata.name}" (${validation.sha256.slice(0, 12)}) on ${host}; capabilities: ${validation.capabilities.join(', ')}`,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if (name === 'browser_opencli_run') {
|
|
19
|
+
const input = args;
|
|
20
|
+
const argv = Array.isArray(input.args) ? input.args.filter(value => typeof value === 'string') : [];
|
|
21
|
+
return { kind: 'ask', reason: 'Run a general OpenCLI command with the logged-in Chrome profile: ' + (argv.slice(0, 3).join(' ') || '(empty)') };
|
|
22
|
+
}
|
|
23
|
+
if (name === 'browser_recipe_run') {
|
|
24
|
+
const input = args;
|
|
25
|
+
const steps = Array.isArray(input.steps) ? input.steps : [];
|
|
26
|
+
if (recipeNeedsApproval(steps)) {
|
|
27
|
+
const actions = [...new Set(steps.map(step => step.type).filter(type => !['wait', 'extract', 'assert', 'screenshot'].includes(type)))];
|
|
28
|
+
return { kind: 'ask', reason: 'Run a multi-step Playwright recipe with page mutations: ' + actions.join(', ') };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { kind: 'allow' };
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=approval-policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval-policy.js","sourceRoot":"","sources":["../src/approval-policy.ts"],"names":[],"mappings":"AAAA,iFAAiF;AAEjF,OAAO,EAAE,mBAAmB,EAA0B,MAAM,iBAAiB,CAAA;AAC7E,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAOjD,MAAM,UAAU,qBAAqB,CAAC,IAAY,EAAE,IAAa;IAC/D,IAAI,IAAI,KAAK,wBAAwB,EAAE,CAAC;QACtC,MAAM,KAAK,GAAG,IAA2C,CAAA;QACzD,IAAI,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ,IAAI,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ;YAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,6CAA6C,EAAE,CAAA;QACrJ,MAAM,UAAU,GAAG,kBAAkB,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,CAAA;QAC9D,IAAI,CAAC,UAAU,CAAC,KAAK;YAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,+BAA+B,GAAG,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAA;QACtH,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAA;QACxC,OAAO;YACL,IAAI,EAAE,KAAK;YACX,MAAM,EAAE,4BAA4B,UAAU,CAAC,QAAQ,CAAC,IAAI,MAAM,UAAU,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,IAAI,mBAAmB,UAAU,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;SACpK,CAAA;IACH,CAAC;IACD,IAAI,IAAI,KAAK,qBAAqB,EAAE,CAAC;QACnC,MAAM,KAAK,GAAG,IAA0B,CAAA;QACxC,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAa,CAAC,CAAC,CAAC,EAAE,CAAA;QAC/G,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,mEAAmE,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,SAAS,CAAC,EAAE,CAAA;IACjJ,CAAC;IACD,IAAI,IAAI,KAAK,oBAAoB,EAAE,CAAC;QAClC,MAAM,KAAK,GAAG,IAA2B,CAAA;QACzC,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAA4B,CAAC,CAAC,CAAC,EAAE,CAAA;QAClF,IAAI,mBAAmB,CAAC,KAAK,CAAC,EAAE,CAAC;YAC/B,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;YACtI,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,0DAA0D,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAA;QACjH,CAAC;IACH,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAA;AAC1B,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface AuthProfileConfig {
|
|
2
|
+
storageStatePath: string;
|
|
3
|
+
allowedDomains: string[];
|
|
4
|
+
/** Persist cookies/localStorage back after a transient context closes. Defaults to false. */
|
|
5
|
+
persistState?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface ResolvedAuthProfile extends AuthProfileConfig {
|
|
8
|
+
id: string;
|
|
9
|
+
persistState: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare class AuthProfileStore {
|
|
12
|
+
private readonly profiles;
|
|
13
|
+
constructor(profiles?: Record<string, AuthProfileConfig>);
|
|
14
|
+
resolve(id: string, targetUrl: string): ResolvedAuthProfile;
|
|
15
|
+
list(): {
|
|
16
|
+
id: string;
|
|
17
|
+
allowedDomains: string[];
|
|
18
|
+
persistState: boolean;
|
|
19
|
+
}[];
|
|
20
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function hostAllowed(hostname, domains) {
|
|
2
|
+
const host = hostname.toLowerCase().replace(/\.$/, '');
|
|
3
|
+
return domains.some(value => {
|
|
4
|
+
const domain = value.toLowerCase().trim().replace(/^\*\./, '').replace(/\.$/, '');
|
|
5
|
+
return domain.length > 0 && (host === domain || host.endsWith('.' + domain));
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
export class AuthProfileStore {
|
|
9
|
+
profiles;
|
|
10
|
+
constructor(profiles = {}) {
|
|
11
|
+
this.profiles = profiles;
|
|
12
|
+
}
|
|
13
|
+
resolve(id, targetUrl) {
|
|
14
|
+
const profile = this.profiles[id];
|
|
15
|
+
if (!profile)
|
|
16
|
+
throw new Error('unknown auth profile: ' + id);
|
|
17
|
+
const url = new URL(targetUrl);
|
|
18
|
+
if (!hostAllowed(url.hostname, profile.allowedDomains ?? []))
|
|
19
|
+
throw new Error('auth profile ' + id + ' is not allowed for ' + url.hostname);
|
|
20
|
+
if (!profile.storageStatePath?.trim())
|
|
21
|
+
throw new Error('auth profile ' + id + ' has no storageStatePath');
|
|
22
|
+
return { id, ...profile, persistState: profile.persistState ?? false };
|
|
23
|
+
}
|
|
24
|
+
list() {
|
|
25
|
+
return Object.entries(this.profiles).sort(([a], [b]) => a.localeCompare(b)).map(([id, profile]) => ({ id, allowedDomains: [...(profile.allowedDomains ?? [])], persistState: profile.persistState ?? false }));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=auth-profiles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-profiles.js","sourceRoot":"","sources":["../src/auth-profiles.ts"],"names":[],"mappings":"AAYA,SAAS,WAAW,CAAC,QAAgB,EAAE,OAA0B;IAC/D,MAAM,IAAI,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;IACtD,OAAO,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE;QAC1B,MAAM,MAAM,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;QACjF,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,GAAG,MAAM,CAAC,CAAC,CAAA;IAC9E,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,OAAO,gBAAgB;IACE;IAA7B,YAA6B,WAA8C,EAAE;QAAhD,aAAQ,GAAR,QAAQ,CAAwC;IAAG,CAAC;IAEjF,OAAO,CAAC,EAAU,EAAE,SAAiB;QACnC,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;QACjC,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,GAAG,EAAE,CAAC,CAAA;QAC5D,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAA;QAC9B,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,cAAc,IAAI,EAAE,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,eAAe,GAAG,EAAE,GAAG,sBAAsB,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAA;QAC3I,IAAI,CAAC,OAAO,CAAC,gBAAgB,EAAE,IAAI,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,eAAe,GAAG,EAAE,GAAG,0BAA0B,CAAC,CAAA;QACzG,OAAO,EAAE,EAAE,EAAE,GAAG,OAAO,EAAE,YAAY,EAAE,OAAO,CAAC,YAAY,IAAI,KAAK,EAAE,CAAA;IACxE,CAAC;IAED,IAAI;QACF,OAAO,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,cAAc,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,cAAc,IAAI,EAAE,CAAC,CAAC,EAAE,YAAY,EAAE,OAAO,CAAC,YAAY,IAAI,KAAK,EAAE,CAAC,CAAC,CAAA;IAChN,CAAC;CACF"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded, auditable Playwright recipes for model-generated browser flows.
|
|
3
|
+
* Recipes deliberately expose named operations instead of arbitrary JavaScript.
|
|
4
|
+
* @module dsh-browser/automation
|
|
5
|
+
*/
|
|
6
|
+
export type BrowserRecipeStep = {
|
|
7
|
+
type: 'wait';
|
|
8
|
+
condition: 'selector' | 'text' | 'load' | 'time';
|
|
9
|
+
value?: string;
|
|
10
|
+
waitMs?: number;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
} | {
|
|
13
|
+
type: 'click';
|
|
14
|
+
selector: string;
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
} | {
|
|
17
|
+
type: 'fill';
|
|
18
|
+
selector: string;
|
|
19
|
+
value: string;
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
} | {
|
|
22
|
+
type: 'type';
|
|
23
|
+
selector: string;
|
|
24
|
+
value: string;
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
} | {
|
|
27
|
+
type: 'press';
|
|
28
|
+
key: string;
|
|
29
|
+
selector?: string;
|
|
30
|
+
} | {
|
|
31
|
+
type: 'select';
|
|
32
|
+
selector: string;
|
|
33
|
+
value: string;
|
|
34
|
+
} | {
|
|
35
|
+
type: 'check';
|
|
36
|
+
selector: string;
|
|
37
|
+
checked?: boolean;
|
|
38
|
+
} | {
|
|
39
|
+
type: 'hover';
|
|
40
|
+
selector: string;
|
|
41
|
+
} | {
|
|
42
|
+
type: 'scroll';
|
|
43
|
+
deltaY?: number;
|
|
44
|
+
waitMs?: number;
|
|
45
|
+
} | {
|
|
46
|
+
type: 'extract';
|
|
47
|
+
selector?: string;
|
|
48
|
+
mode?: 'text' | 'html' | 'links' | 'attribute';
|
|
49
|
+
attribute?: string;
|
|
50
|
+
limit?: number;
|
|
51
|
+
} | {
|
|
52
|
+
type: 'assert';
|
|
53
|
+
selector?: string;
|
|
54
|
+
text?: string;
|
|
55
|
+
timeoutMs?: number;
|
|
56
|
+
} | {
|
|
57
|
+
type: 'screenshot';
|
|
58
|
+
};
|
|
59
|
+
export interface RecipeStepResult {
|
|
60
|
+
step: number;
|
|
61
|
+
action: BrowserRecipeStep['type'];
|
|
62
|
+
ok: boolean;
|
|
63
|
+
value?: string;
|
|
64
|
+
}
|
|
65
|
+
export declare function validateRecipe(steps: readonly BrowserRecipeStep[]): void;
|
|
66
|
+
export declare function recipeNeedsApproval(steps: readonly BrowserRecipeStep[]): boolean;
|
|
67
|
+
export declare function runRecipe(page: any, steps: readonly BrowserRecipeStep[], captureScreenshot: () => Promise<string>, signal?: AbortSignal): Promise<RecipeStepResult[]>;
|