Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/developers/qwen-serve-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ registry. Clients **must** gate UI off `features`, not off `mode` (per design
'permission_mediation', 'prompt_absolute_deadline', 'writer_idle_timeout',
'non_blocking_prompt', 'session_language', 'session_rewind',
'workspace_hooks', 'session_hooks', 'workspace_extensions',
'session_branch', 'rate_limit', 'workspace_reload']
'session_branch', 'rate_limit', 'workspace_reload',
'client_mcp_over_ws', 'cdp_tunnel_over_ws', 'browser_automation_mcp']
```

> Conditional tags appear only when their matching deployment toggle is on (see the table below). F3's `permission_mediation` tag is always-on and carries `modes: ['first-responder', 'designated', 'consensus', 'local-only']` so SDK clients can introspect the build-supported set; the runtime-active strategy is at `body.policy.permission`.
Expand Down Expand Up @@ -224,6 +225,9 @@ operator diagnostic snapshot documented below.
| `session_shell_command` | session shell execution is explicitly enabled. |
| `rate_limit` | `--rate-limit` / `QWEN_SERVE_RATE_LIMIT=1` / `ServeOptions.rateLimit` is enabled. |
| `workspace_reload` | workspace reload support is available in the embedded route configuration. |
| `client_mcp_over_ws` | the daemon accepts client-hosted MCP servers over the ACP WebSocket. This is an explicit opt-in, not required for the CDP tunnel path. |
| `cdp_tunnel_over_ws` | the daemon exposes the reverse `/cdp` WebSocket tunnel, either by explicit opt-in or because a Chrome extension origin is allowed. This only means the tunnel exists; it does not mean Chrome DevTools MCP tools are registered. |
| `browser_automation_mcp` | ACP HTTP is enabled, `cdp_tunnel_over_ws` is active, no bearer token blocks `/cdp`, and `QWEN_CDP_MCP_COMMAND` names an external stdio MCP adapter. The main CLI package does not bundle a browser automation adapter; without this tag, Chrome extension side-panel chat may still work, but console/network/screenshot/click tools are not registered by default. |

`mcp_guardrails` is **not** in this conditional table — it's an always-on tag, advertised whenever the binary supports the new `/workspace/mcp` budget fields, regardless of whether the operator configured a budget. Operators who haven't set `--mcp-client-budget` still get the new fields (with `budgetMode: 'off'`, `budgets: []`).

Expand Down
1 change: 1 addition & 0 deletions docs/users/qwen-serve.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ Notes:
- **`LOOPBACK_BINDS` includes IPv6** — `::1` and `[::1]` count as loopback for the no-token rule.
- **Host header allowlist** — on **loopback** binds the daemon checks `Host:` matches `localhost:port` / `127.0.0.1:port` / `[::1]:port` / `host.docker.internal:port` (case-insensitive per RFC 7230 §5.4) to defend against DNS rebinding. **Non-loopback binds (`--hostname 0.0.0.0`) intentionally bypass the Host allowlist** — the operator has chosen the surface area, so the bearer-token gate is the sole authentication layer; reverse proxies / SNI / client cert pinning are the operator's responsibility, not the daemon's. If you need Host-based isolation on a non-loopback bind, terminate TLS + check Host at a front proxy.
- **CORS denies any browser Origin by default** — returns `403` JSON. Pass **`--allow-origin <pattern>`** (repeatable, T2.4 #4514) to opt specific browser origins through. Each value is either the literal `*` (any origin — boot refuses if no bearer token is configured; `--require-auth` on loopback is recommended for full hardening since `/health` and `/demo` remain pre-auth on loopback by default) or a canonical URL origin (`<scheme>://<host>[:<port>]`, no trailing slash / path / userinfo). Matched origins receive proper CORS response headers (`Access-Control-Allow-Origin: <echoed>`, `Vary: Origin`, plus standard methods / headers / max-age and exposed `Retry-After`); unmatched origins still get a 403 with the same envelope as the default wall. `caps.features.allow_origin` is advertised conditionally so SDK / webui clients can pre-flight whether the daemon honors cross-origin hits before issuing them. Example: `qwen serve --allow-origin http://localhost:3000 --allow-origin http://localhost:5173`. Loopback self-origin hits (e.g. the `/demo` page) are unaffected — a separate Origin-strip shim handles them regardless of `--allow-origin`. **Browser webuis without `--allow-origin` configured** still fall back to the same Stage 1 options as before: package as a native shell (Electron/Tauri) so no `Origin` header is sent, or front the daemon with a same-origin reverse proxy.
- **Chrome extension browser automation is separate from framing.** `qwen serve --allow-origin chrome-extension://<id>` lets the extension frame the Web Shell and connect to the daemon. Console/network/screenshot/click tools require an external CDP MCP adapter command: `QWEN_CDP_MCP_COMMAND=/path/to/cdp-mcp-adapter qwen serve --allow-origin chrome-extension://<id>`. The main CLI package does not bundle a browser automation adapter; clients can check `caps.features.includes('browser_automation_mcp')` before presenting those tools as available.
- **Spawned `qwen --acp` child inherits the daemon's environment** with one explicit scrub: `QWEN_SERVER_TOKEN` is removed before the child starts (the daemon's own bearer; the agent doesn't need it). Everything else — `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / `QWEN_*` / `DASHSCOPE_API_KEY` / your custom `modelProviders[].envKey` / etc. — passes through, because the agent legitimately needs those to authenticate to the LLM. **This is intentional, not a sandbox.** The agent runs as the same UID with shell-tool access, so anything in `~/.bashrc` / `~/.aws/credentials` / `~/.npmrc` is reachable by prompt injection regardless. The env passthrough is not the security boundary; the user-as-trust-root is. Don't run `qwen serve` under an identity that has env-resident credentials you wouldn't trust the agent with.
- **Per-subscriber bounded SSE queues** — a slow client that overflows its queue gets a `client_evicted` terminal frame and is closed; one stuck consumer can't pin the daemon.
- **Per-session prompt admission cap** — defaults to 5 accepted-but-unsettled prompts per session. A buggy client cannot enqueue unbounded prompt promises or temporary SSE waits for one session.
Expand Down
24 changes: 22 additions & 2 deletions packages/chrome-extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ It does two things:
daemon serves to the browser. The panel has no UI of its own.
- **Service worker** — a CDP-tunnel pipe. It connects to the daemon's `/acp`
WebSocket and bridges `cdp_*` frames into `chrome.debugger`, so the agent can
drive the real browser (read page, screenshot, click, …) via
chrome-devtools-mcp over the tunnel.
drive the real browser when an external CDP MCP adapter is configured.

## Build

Expand Down Expand Up @@ -41,6 +40,27 @@ extension's requests. The side panel reads the id at runtime via
Once the daemon is reachable and permits framing, the side panel swaps the
welcome screen for the chat UI automatically.

## Browser Automation Tools

The command above only makes the side panel and Web Shell available. Browser
automation tools such as console/network inspection, screenshots, and page
clicking require an explicit external MCP adapter command:

```bash
QWEN_CDP_MCP_COMMAND=/path/to/cdp-mcp-adapter \
qwen serve --allow-origin chrome-extension://<this-extension-id>
```

No browser automation adapter is bundled with the main `@qwen-code/qwen-code`
package. When `QWEN_CDP_MCP_COMMAND` is unset, the extension can still open the
Web Shell, but the daemon will not register browser automation MCP tools.
Clients can distinguish the states through `/capabilities`:

- `allow_origin` means the extension may frame and call the daemon.
- `cdp_tunnel_over_ws` means the daemon exposes the reverse CDP tunnel.
- `browser_automation_mcp` means the external adapter command is configured and
browser automation MCP tools can be registered when the CDP bridge connects.

## Onboarding states

The side panel probes `GET /health` and `GET /capabilities` and shows one of:
Expand Down
29 changes: 11 additions & 18 deletions packages/chrome-extension/docs/06-plan-c-cdp-tunnel.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# Plan C — CDP 隧道:复用 chrome-devtools-mcp 全套工具驱动真实浏览器
# Plan C — CDP 隧道:复用 DevTools MCP 工具驱动真实浏览器

> 设计文档(可行性 + 实施方案)。配套:[`05-daemon-direct-architecture.md`](./05-daemon-direct-architecture.md)(Phase 1/2 已落地:side panel + 逆向工具通道 `chrome-tools`)。
> 关联 issue #5626 / PR #5777。

## 0. TL;DR

- **问题**:当前 `chrome-tools` 逆向通道(Plan A)在扩展端用 `chrome.*` **重新实现** chrome-devtools-mcp 已有的能力(console / network / screenshot…),每加一个新能力都要手写 executor。
- **Plan C**:扩展只用 `chrome.debugger` 把真实标签页的 **CDP 协议**透传给 daemon,daemon 暴露一个 CDP endpoint 让 **chrome-devtools-mcp(puppeteer)连进来** —— 复用它现成的全套工具,操作用户**真实**浏览器,不再逐个手写。
- **结论:有条件可行**。能让 puppeteer 连上单 tab(命题 A,可行);但「把*未改的* chrome-devtools-mcp 直接接上」(命题 B)**不可行 —— 必须给它打一个小补丁**(patch-package,~2 处 / 十几行 diff)。
- **形态**:不扣代码、不 submodule。`chrome-devtools-mcp` 从 `npx @latest` 改成仓库 pin 依赖 `1.4.0` + 一个 `patches/chrome-devtools-mcp+1.4.0.patch`(与现有 `ink+7.0.3.patch` 同形态)
- **问题**:当前 `chrome-tools` 逆向通道(Plan A)在扩展端用 `chrome.*` **重新实现**上游 DevTools MCP server 已有的能力(console / network / screenshot…),每加一个新能力都要手写 executor。
- **Plan C**:扩展只用 `chrome.debugger` 把真实标签页的 **CDP 协议**透传给 daemon,daemon 暴露一个 CDP endpoint 让 **外部 DevTools MCP adapter 连进来** —— 复用它现成的全套工具,操作用户**真实**浏览器,不再逐个手写。
- **结论:有条件可行**。能让浏览器自动化运行时连上单 tab(命题 A,可行);但「把*未改的*上游 adapter 直接接上」(命题 B)**不可行 —— 必须给它打一个小补丁**(patch-package,~2 处 / 十几行 diff)。
- **形态(历史方案,已不作为主包方案)**:早期评估过 pin 上游 adapter + patch-package。0.19.4 的包扫描问题证明这类依赖、patch 和安装钩子不应进入主 CLI 包;后续应走显式外部 adapter 命令
- **建议**:先做 Phase 0 半天 spike 钉死那堵墙,再决定是否全量上。

## 1. 决定性发现:为什么「零改造复用」不行(已逐行核实 1.4.0 源码)
Expand Down Expand Up @@ -41,7 +41,7 @@
## 2. 架构

```
chrome-devtools-mcp(fork) qwen serve daemon 扩展(MV3) 真实 tab
DevTools MCP adapter qwen serve daemon 扩展(MV3) 真实 tab
puppeteer.connect browser级 ┌ /cdp WS endpoint ┐ reverse cdp-bridge chrome.
({browserWS}) ───CDP───────▶ │ CdpBrowserEmulator│ WS /acp chrome. debugger
│ ①本地应答browser域│ cdp_command .debugger ──▶ Page/DOM
Expand All @@ -54,7 +54,7 @@ chrome-devtools-mcp(fork) qwen serve daemon 扩展(MV3)

组件:

- **chrome-devtools-mcp (patched) + puppeteer-core**:`puppeteer.connect({ browserWSEndpoint: 'ws://127.0.0.1:PORT/cdp' })`
- **DevTools MCP adapter + browser automation runtime**:通过 `browserWSEndpoint: 'ws://127.0.0.1:PORT/cdp'` 连接 daemon 暴露的 CDP endpoint
- **daemon `/cdp` WS endpoint(新增)**:raw CDP 帧 + `CdpBrowserEmulator`(browser-level 合成 + sessionId 打/解标签)。
- **daemon 现有 `/acp` reverse WS**:扩展已建立、过了 ACP-initialize 鉴权的那条 socket,新增 `cdp_*` 帧。
- **cdp-reverse-link**:把 `/cdp` puppeteer socket 与扩展 `/acp` 连接配对(单 daemon = 单扩展 = 单 browser)。
Expand Down Expand Up @@ -85,20 +85,13 @@ chrome-devtools-mcp(fork) qwen serve daemon 扩展(MV3)
| remorses/playwriter(MIT, 3.6k★, 活跃) | 扩展 + WS CDP relay,外部 client `connectOverCDP` 驱动真实已登录浏览器。 |
| 本仓库 `/acp` reverse 通道 | `WebSocketServer({noServer:true})` + pathname 分支 + `clientMcpOverWs` 式 feature-flag + `mcp_*` 帧拦截 —— `cdp_*` 帧照搬,鉴权/CSRF/origin/maxPayload 全复用。 |

## 5. patch-package 形态(不扣代码、不 submodule
## 5. 历史 patch-package 形态(不再作为主包方案

本仓库已用 patch-package(`postinstall: patch-package` + `patches/ink+7.0.3.patch` 样例)。

```jsonc
// package.json
"dependencies": { "chrome-devtools-mcp": "1.4.0" } // pin,不再 npx @latest
```

流程:改 `node_modules/chrome-devtools-mcp/build/src/McpContext.js`(try-catch 包 #init:81-82)→ `npx patch-package chrome-devtools-mcp` → 生成 `patches/chrome-devtools-mcp+1.4.0.patch`(进 git)→ `npm install` 自动应用。puppeteer-core pin 25.2.0(ExtensionTransport 拓扑硬编码,避免版本漂移)。
早期方案考虑过把上游 adapter 作为 pin 依赖并用 patch-package 修补启动路径。这个形态会把浏览器自动化依赖、依赖 patch 和安装钩子放进主包/安装图,容易触发包扫描器。当前方案不再把 adapter 打进主 CLI 包,而是通过显式外部命令接入。

## 6. 分阶段实施

- **Phase 0 — spike(0.5–1 天)**:独立脚本起最小 `/cdp` + ExtensionTransport 4 命令合成,跑*未改的* `chrome-devtools-mcp@1.4.0 --wsEndpoint ws://…/cdp` 调一次 `take_snapshot`,**确认它在 `McpContext.from()` 抛 `CDPSession creation failed.`** → 钉死 fork 范围。
- **Phase 0 — spike(0.5–1 天)**:独立脚本起最小 `/cdp` + ExtensionTransport 4 命令合成,跑*未改的*上游 adapter 调一次 `take_snapshot`,**确认它在 `McpContext.from()` 抛 `CDPSession creation failed.`** → 钉死 fork 范围。
- **Phase 1 — MVP(5–8 天)**:单 tab `take_snapshot`/`click` 跑通。
- daemon 新增 `packages/cli/src/serve/cdp-tunnel/{cdp-ws,cdp-browser-emulator,cdp-reverse-link}.ts`。
- daemon 改 `acp-http/index.ts`(upgrade 加 `/cdp` 分支,复用 auth/CSRF/origin)+ reverse WS 加 `isCdpFrameType` 守卫;`run-qwen-serve.ts`/`server.ts`/`serve/types.ts`/`serve/capabilities.ts` 加 `cdpTunnelOverWs` flag(仿 `clientMcpOverWs`,默认 OFF)。
Expand All @@ -122,7 +115,7 @@ chrome-devtools-mcp(fork) qwen serve daemon 扩展(MV3)
| 调试 banner / DevTools 互斥 | 中(UX) | onDetach 重连 + 用户提示 |
| 受管 Chrome 策略 `DeveloperToolsAvailability` | 中(部署) | 部署前核查策略允许 chrome.debugger(force-installed 扩展 114+ 默认禁,需值 1) |
| 大 payload(截图/getResponseBody) vs maxPayload | 中 | `/cdp` 抬高 maxPayload 或分块 |
| puppeteer 版本漂移 | 低 | pin cdp-mcp 1.4.0 + puppeteer-core 25.2.0 |
| 浏览器自动化运行时版本漂移 | 低 | 外部 adapter 自己 pin 版本;主 CLI 包不携带该依赖 |

**回退**:Phase 0 失败 → 放弃 C 回 A;Phase 1 失败 → 回 A(合成层/透传可作 A 底座);Phase 2/3 失败 → 退守单 tab 只读快照/点击形态,仍省大量工具实现。

Expand Down
Loading
Loading