@angelky7/dsh-browser-control 0.0.0-stage → 1.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 +662 -0
- package/README.en.md +317 -0
- package/README.md +310 -2
- package/cordis.patch.yml +14 -0
- package/lib/index.js +2587 -0
- package/lib/invariant.js +25 -0
- package/lib/types/errors.d.ts +64 -0
- package/lib/types/errors.js +161 -0
- package/lib/types/index.d.ts +62 -0
- package/lib/types/index.js +1284 -0
- package/lib/types/invariant.d.ts +15 -0
- package/lib/types/invariant.js +23 -0
- package/lib/types/policy.d.ts +62 -0
- package/lib/types/policy.js +85 -0
- package/lib/types/server.d.ts +158 -0
- package/lib/types/server.js +678 -0
- package/lib/types/ws.d.ts +35 -0
- package/lib/types/ws.js +106 -0
- package/llms.txt +43 -0
- package/package.json +83 -4
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side RFC 6455 frame codec for the single local extension link. The
|
|
3
|
+
* extension always masks its frames and never sends binary payloads; the
|
|
4
|
+
* server replies unmasked. Fragmented text messages accumulate across
|
|
5
|
+
* continuation frames, so a multi-megabyte screenshot result survives socket
|
|
6
|
+
* chunk boundaries.
|
|
7
|
+
* @module @deepseek-ai/dsh-browser-bridge/ws
|
|
8
|
+
*/
|
|
9
|
+
/** Hard ceiling on one frame's payload; a larger frame is a protocol error. */
|
|
10
|
+
export declare const MAX_FRAME_BYTES: number;
|
|
11
|
+
/** One parsed WebSocket frame. `opcode` follows RFC 6455: 1 text, 8 close, 9 ping, 10 pong, 0 continuation. */
|
|
12
|
+
export interface WsFrame {
|
|
13
|
+
readonly fin: boolean;
|
|
14
|
+
readonly opcode: number;
|
|
15
|
+
readonly payload: Buffer;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Incremental frame parser over arbitrary TCP chunks. `drain` parses every
|
|
19
|
+
* complete frame currently buffered and keeps the remainder; an oversized or
|
|
20
|
+
* unmasked data frame throws, and the caller closes the link.
|
|
21
|
+
*/
|
|
22
|
+
export declare class FrameReader {
|
|
23
|
+
private buffer;
|
|
24
|
+
/** Append one TCP chunk to the parse buffer. */
|
|
25
|
+
push(chunk: Buffer): void;
|
|
26
|
+
/** Parse and consume every complete frame, invoking `onFrame` per frame in order. */
|
|
27
|
+
drain(onFrame: (frame: WsFrame) => void): void;
|
|
28
|
+
private readFrame;
|
|
29
|
+
}
|
|
30
|
+
/** Encode one unmasked text frame carrying a UTF-8 JSON message. */
|
|
31
|
+
export declare function encodeTextFrame(text: string): Buffer;
|
|
32
|
+
/** Encode one control frame (opcode 0x8 close, 0x9 ping, 0xA pong); payload stays ≤ 125 bytes. */
|
|
33
|
+
export declare function encodeControlFrame(opcode: number, payload?: Buffer): Buffer;
|
|
34
|
+
/** Build the close-frame bytes for a status code plus a UTF-8 reason within the 125-byte limit. */
|
|
35
|
+
export declare function encodeCloseFrame(code: number, reason: string): Buffer;
|
package/lib/types/ws.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side RFC 6455 frame codec for the single local extension link. The
|
|
3
|
+
* extension always masks its frames and never sends binary payloads; the
|
|
4
|
+
* server replies unmasked. Fragmented text messages accumulate across
|
|
5
|
+
* continuation frames, so a multi-megabyte screenshot result survives socket
|
|
6
|
+
* chunk boundaries.
|
|
7
|
+
* @module @deepseek-ai/dsh-browser-bridge/ws
|
|
8
|
+
*/
|
|
9
|
+
/** Hard ceiling on one frame's payload; a larger frame is a protocol error. */
|
|
10
|
+
export const MAX_FRAME_BYTES = 64 * 1024 * 1024;
|
|
11
|
+
/**
|
|
12
|
+
* Incremental frame parser over arbitrary TCP chunks. `drain` parses every
|
|
13
|
+
* complete frame currently buffered and keeps the remainder; an oversized or
|
|
14
|
+
* unmasked data frame throws, and the caller closes the link.
|
|
15
|
+
*/
|
|
16
|
+
export class FrameReader {
|
|
17
|
+
buffer = Buffer.alloc(0);
|
|
18
|
+
/** Append one TCP chunk to the parse buffer. */
|
|
19
|
+
push(chunk) {
|
|
20
|
+
this.buffer = this.buffer.length === 0 ? chunk : Buffer.concat([this.buffer, chunk]);
|
|
21
|
+
}
|
|
22
|
+
/** Parse and consume every complete frame, invoking `onFrame` per frame in order. */
|
|
23
|
+
drain(onFrame) {
|
|
24
|
+
for (;;) {
|
|
25
|
+
const frame = this.readFrame();
|
|
26
|
+
if (!frame)
|
|
27
|
+
return;
|
|
28
|
+
onFrame(frame);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
readFrame() {
|
|
32
|
+
const buf = this.buffer;
|
|
33
|
+
if (buf.length < 2)
|
|
34
|
+
return null;
|
|
35
|
+
const fin = (buf[0] & 0x80) !== 0;
|
|
36
|
+
const opcode = buf[0] & 0x0f;
|
|
37
|
+
const masked = (buf[1] & 0x80) !== 0;
|
|
38
|
+
let length = buf[1] & 0x7f;
|
|
39
|
+
let offset = 2;
|
|
40
|
+
if (length === 126) {
|
|
41
|
+
if (buf.length < offset + 2)
|
|
42
|
+
return null;
|
|
43
|
+
length = buf.readUInt16BE(offset);
|
|
44
|
+
offset += 2;
|
|
45
|
+
}
|
|
46
|
+
else if (length === 127) {
|
|
47
|
+
if (buf.length < offset + 8)
|
|
48
|
+
return null;
|
|
49
|
+
const extended = buf.readBigUInt64BE(offset);
|
|
50
|
+
offset += 8;
|
|
51
|
+
if (extended > BigInt(MAX_FRAME_BYTES))
|
|
52
|
+
throw new Error(`websocket frame too large: ${extended} bytes`);
|
|
53
|
+
length = Number(extended);
|
|
54
|
+
}
|
|
55
|
+
let maskKey = null;
|
|
56
|
+
if (masked) {
|
|
57
|
+
if (buf.length < offset + 4)
|
|
58
|
+
return null;
|
|
59
|
+
maskKey = buf.subarray(offset, offset + 4);
|
|
60
|
+
offset += 4;
|
|
61
|
+
}
|
|
62
|
+
if (buf.length < offset + length)
|
|
63
|
+
return null;
|
|
64
|
+
if (!masked && (opcode === 0x1 || opcode === 0x0 || opcode === 0x2)) {
|
|
65
|
+
throw new Error('websocket client sent an unmasked data frame');
|
|
66
|
+
}
|
|
67
|
+
const payload = Buffer.from(buf.subarray(offset, offset + length));
|
|
68
|
+
if (maskKey) {
|
|
69
|
+
for (let i = 0; i < payload.length; i++)
|
|
70
|
+
payload[i] = payload[i] ^ maskKey[i & 3];
|
|
71
|
+
}
|
|
72
|
+
this.buffer = buf.subarray(offset + length);
|
|
73
|
+
return { fin, opcode, payload };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Encode one unmasked text frame carrying a UTF-8 JSON message. */
|
|
77
|
+
export function encodeTextFrame(text) {
|
|
78
|
+
const payload = Buffer.from(text, 'utf8');
|
|
79
|
+
if (payload.length < 126) {
|
|
80
|
+
return Buffer.from([0x81, payload.length, ...payload]);
|
|
81
|
+
}
|
|
82
|
+
if (payload.length < 65_536) {
|
|
83
|
+
const header = Buffer.alloc(4);
|
|
84
|
+
header[0] = 0x81;
|
|
85
|
+
header[1] = 126;
|
|
86
|
+
header.writeUInt16BE(payload.length, 2);
|
|
87
|
+
return Buffer.concat([header, payload]);
|
|
88
|
+
}
|
|
89
|
+
const header = Buffer.alloc(10);
|
|
90
|
+
header[0] = 0x81;
|
|
91
|
+
header[1] = 127;
|
|
92
|
+
header.writeBigUInt64BE(BigInt(payload.length), 2);
|
|
93
|
+
return Buffer.concat([header, payload]);
|
|
94
|
+
}
|
|
95
|
+
/** Encode one control frame (opcode 0x8 close, 0x9 ping, 0xA pong); payload stays ≤ 125 bytes. */
|
|
96
|
+
export function encodeControlFrame(opcode, payload = Buffer.alloc(0)) {
|
|
97
|
+
return Buffer.concat([Buffer.from([0x80 | opcode, payload.length]), payload]);
|
|
98
|
+
}
|
|
99
|
+
/** Build the close-frame bytes for a status code plus a UTF-8 reason within the 125-byte limit. */
|
|
100
|
+
export function encodeCloseFrame(code, reason) {
|
|
101
|
+
const reasonBytes = Buffer.from(reason, 'utf8').subarray(0, 123);
|
|
102
|
+
const body = Buffer.alloc(2 + reasonBytes.length);
|
|
103
|
+
body.writeUInt16BE(code, 0);
|
|
104
|
+
reasonBytes.copy(body, 2);
|
|
105
|
+
return encodeControlFrame(0x8, body);
|
|
106
|
+
}
|
package/llms.txt
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# DSH Browser Control
|
|
2
|
+
|
|
3
|
+
Chrome 浏览器扩展 + DeepSeek Harness 插件,让 AI Agent 通过 CDP 驱动用户的真实浏览器。
|
|
4
|
+
|
|
5
|
+
## 文件结构
|
|
6
|
+
|
|
7
|
+
- `extension/` - Chrome MV3 扩展(background.js Service Worker + popup UI)
|
|
8
|
+
- `src/` - dsh 插件源码(WebSocket 桥 + HTTP 面 + `browser_*` 工具)
|
|
9
|
+
- `lib/` - 构建产物,按约定提交(npm 包只含宿主侧这一份,没有浏览器端模块)
|
|
10
|
+
- `verify/` - 纯 node 实测台,`npm run verify` 跑完(见 `verify/README.md`)
|
|
11
|
+
- `install.sh` - 旧「复制进 harness 源码树」方式的安装脚本(v1.0.2 及以前)
|
|
12
|
+
- `cordis.patch.yml` - 插件挂载配置(`enabled: true`,与 schema 默认保持一致)
|
|
13
|
+
- `dsh-config/` - dsh 配置项参考
|
|
14
|
+
|
|
15
|
+
## 技术栈
|
|
16
|
+
|
|
17
|
+
- Chrome DevTools Protocol(CDP)via chrome.debugger API
|
|
18
|
+
- WebSocket(扩展 → 桥,JSON 文本帧)
|
|
19
|
+
- Node.js HTTP(状态页 + 命令面 + 清理 API)
|
|
20
|
+
- RFC 6455 帧编解码
|
|
21
|
+
- TypeScript(strict 模式)
|
|
22
|
+
- Cordis 插件系统(DeepSeek Harness 生态)
|
|
23
|
+
|
|
24
|
+
## 信任边界
|
|
25
|
+
|
|
26
|
+
- 只绑 `127.0.0.1`;**Token 才是真正的边界,回环绑定不是**
|
|
27
|
+
- `/api/command`、`/api/cleanup` 要求 Token(`Authorization: Bearer` / `X-Token` / `?token=`),与 WS 同一个凭据,常数时间比较
|
|
28
|
+
- WS 升级校验 `Host` 必须是回环(挡 DNS 重绑定)并拒绝网页 `Origin`,只放行 `*-extension://` 与无 Origin 的本机客户端
|
|
29
|
+
- 所有 HTTP 响应都不带 `Access-Control-Allow-Origin`
|
|
30
|
+
- `/` 与 `/api/status` 免鉴权,只报链路状态
|
|
31
|
+
- 两个本机策略开关(profile 的 `browser-bridge` 配置):`readOnly: true` 拒绝一切会改变页面/浏览器状态或删本机文件的命令,`allowScript: false` 单独关掉 `browser_evaluate`(它能做其它所有工具能做的事)。闸门在把命令发给扩展**之前**,所以被拒的调用零副作用;它们只约束模型那条工具面,HTTP 面按同一 token 走、不受影响,也**不是**防有权限的人
|
|
32
|
+
- 扩展弹窗里的「锁定」是另一层:只有人工通道能开与关,冻结期间命令在扩展的 `runCommand` 入口就被拒绝(连接保持),读不出锁状态时按冻结处理(fail-closed)
|
|
33
|
+
- `browser_hold`(接管浮层)是**软层**:它告诉用户模型在操作这一页并挡住误触,用户双击即可关掉;卡片上按「中止操作」后扩展拒绝 `click`/`input`/`press`/`nav`,但模型可以调 `hold action=off` 解除。它不是安全边界,也不是防逃逸手段——要冻住浏览器用扩展弹窗里的「锁定」
|
|
34
|
+
|
|
35
|
+
## 关键文件
|
|
36
|
+
|
|
37
|
+
- `src/index.ts` - 21 个 `browser_*` 工具注册 + settings 接线的版本兼容分支
|
|
38
|
+
- `src/policy.ts` - 三张互不相同的表:只读白名单、脚本命令、插件本地破坏性命令(`browser_cleanup` 不经过 `execute()`,必须自己站闸门)
|
|
39
|
+
- `src/server.ts` - WebSocket 服务 + HTTP 面(鉴权、来源校验、状态页)
|
|
40
|
+
- `src/ws.ts` - RFC 6455 帧编解码
|
|
41
|
+
- `src/errors.ts` - 失败分类:`code` / `effectState` / `hint` 写进模型读得到的那句话
|
|
42
|
+
- `extension/background.js` - CDP 命令分发 + 持久 debugger 附着 + 本地硬锁 + 接管浮层(`hold`)
|
|
43
|
+
- `extension/popup.js` - 弹窗状态 UI 与「锁定」按钮
|
package/package.json
CHANGED
|
@@ -1,6 +1,85 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@angelky7/dsh-browser-control",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"description": "Local WebSocket bridge between dsh and the DSH Browser Control browser extension, with model-facing browser_* tools and a Settings-managed on/off switch",
|
|
4
|
+
"version": "1.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/types/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/types/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./invariant": {
|
|
14
|
+
"types": "./lib/types/invariant.d.ts",
|
|
15
|
+
"default": "./lib/invariant.js"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"cordis.patch.yml",
|
|
21
|
+
"lib/index.js",
|
|
22
|
+
"lib/invariant.js",
|
|
23
|
+
"lib/types/**/*.js",
|
|
24
|
+
"lib/types/**/*.d.ts",
|
|
25
|
+
"README.md",
|
|
26
|
+
"README.en.md",
|
|
27
|
+
"llms.txt",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"license": "AGPL-3.0-only",
|
|
31
|
+
"dsh": {
|
|
32
|
+
"bundle": {
|
|
33
|
+
"patch": "./cordis.patch.yml"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc -p tsconfig.json && tsdown",
|
|
38
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
39
|
+
"check": "npm run typecheck && npm run build && publint",
|
|
40
|
+
"verify": "node verify/verify_config_patch.mjs && node verify/verify_peers.mjs && node verify/verify_surface.mjs && node verify/verify_tab_groups.mjs && node verify/verify_tabs_close_many.mjs && node verify/verify_cdp_retry.mjs && node verify/verify_snapshot_ax.mjs && node verify/verify_hard_lock.mjs && node verify/verify_policy.mjs && node verify/verify_extension_e2e.mjs && node verify/verify_plugin.mjs legacy && node verify/verify_plugin.mjs conflict && node verify/verify_plugin.mjs new && node verify/verify_http.mjs && node verify/verify_errors.mjs && node verify/verify_hold.mjs && node verify/verify_protocol.mjs && node verify/verify_bundle_http.mjs && node verify/verify_nav_report.mjs && node verify/verify_nav_landing.mjs && node verify/verify_render_contract.mjs && node verify/verify_press_events.mjs"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public",
|
|
44
|
+
"registry": "https://registry.npmjs.org/"
|
|
45
|
+
},
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/angelky7/dsh-browser-control.git"
|
|
49
|
+
},
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/angelky7/dsh-browser-control/issues"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://github.com/angelky7/dsh-browser-control#readme",
|
|
54
|
+
"keywords": [
|
|
55
|
+
"deepseek-harness",
|
|
56
|
+
"deepseek",
|
|
57
|
+
"dsh",
|
|
58
|
+
"dsh-plugin",
|
|
59
|
+
"browser",
|
|
60
|
+
"browser-automation",
|
|
61
|
+
"cdp",
|
|
62
|
+
"chrome-extension",
|
|
63
|
+
"websocket"
|
|
64
|
+
],
|
|
65
|
+
"peerDependencies": {
|
|
66
|
+
"@deepseek-ai/cordis": "~4.0.4",
|
|
67
|
+
"@deepseek-ai/dsh-invariants": "0.2.0-rc.2 || >=0.1.0-rc.1 <0.3.0-0",
|
|
68
|
+
"@deepseek-ai/dsh-settings": "0.2.0-rc.2 || >=0.1.0-rc.1 <0.3.0-0",
|
|
69
|
+
"@deepseek-ai/dsh-tools": "0.2.0-rc.2 || >=0.1.0-rc.1 <0.3.0-0"
|
|
70
|
+
},
|
|
71
|
+
"dependencies": {
|
|
72
|
+
"@deepseek-ai/schemastery": "~3.18.4"
|
|
73
|
+
},
|
|
74
|
+
"devDependencies": {
|
|
75
|
+
"@deepseek-ai/cordis": "~4.0.4",
|
|
76
|
+
"@deepseek-ai/dsh-invariants": "0.2.0-rc.2",
|
|
77
|
+
"@deepseek-ai/dsh-settings": "0.2.0-rc.2",
|
|
78
|
+
"@deepseek-ai/dsh-tools": "0.2.0-rc.2",
|
|
79
|
+
"@types/node": "^22.20.0",
|
|
80
|
+
"publint": "^0.3.21",
|
|
81
|
+
"semver": "^7.8.5",
|
|
82
|
+
"tsdown": "^0.22.2",
|
|
83
|
+
"typescript": "^6.0.3"
|
|
84
|
+
}
|
|
85
|
+
}
|