From 0ad4920a8ed01854732ef992cabbf7fcb73f29e9 Mon Sep 17 00:00:00 2001 From: yiliang114 Date: Thu, 16 Jul 2026 11:02:20 +0800 Subject: [PATCH 01/13] feat(chrome): add native browser tools --- ...07-13-chrome-extension-native-tools-pr1.md | 340 ++++++ package-lock.json | 1 + package.json | 2 +- packages/chrome-extension/README.md | 96 +- .../config/esbuild.background.config.js | 8 +- .../docs/05-daemon-direct-architecture.md | 65 +- packages/chrome-extension/package.json | 5 +- .../chrome-extension/public/sidepanel.html | 69 +- packages/chrome-extension/public/sidepanel.js | 300 +++++- .../chrome-extension/scripts/artifact-scan.js | 106 ++ .../scripts/package-extension.js | 32 + .../browser-mcp/browser-tools.test.ts | 589 +++++++++++ .../background/browser-mcp/browser-tools.ts | 986 ++++++++++++++++++ .../background/browser-mcp/connection.test.ts | 61 ++ .../src/background/browser-mcp/connection.ts | 39 + .../browser-mcp/debugger-session.test.ts | 253 +++++ .../browser-mcp/debugger-session.ts | 214 ++++ .../src/background/browser-mcp/server.test.ts | 98 ++ .../src/background/browser-mcp/server.ts | 143 +++ .../src/background/service-worker.test.ts | 192 ++++ .../src/background/service-worker.ts | 145 ++- .../chrome-extension/src/daemon/config.ts | 11 +- .../src/daemon/discovery.test.ts | 159 +++ .../chrome-extension/src/daemon/discovery.ts | 135 ++- .../client-mcp-sender-registry.test.ts | 25 + .../acp-http/client-mcp-sender-registry.ts | 5 + .../src/serve/acp-http/client-mcp-ws.test.ts | 197 +++- .../cli/src/serve/acp-http/client-mcp-ws.ts | 40 +- packages/cli/src/serve/acp-http/index.ts | 83 +- .../cli/src/serve/extension-pairing-routes.ts | 84 ++ .../cli/src/serve/extension-pairing.test.ts | 186 ++++ packages/cli/src/serve/extension-pairing.ts | 209 ++++ packages/cli/src/serve/run-qwen-serve.test.ts | 271 ++++- packages/cli/src/serve/run-qwen-serve.ts | 82 +- packages/cli/src/serve/server.ts | 6 + packages/cli/src/serve/types.ts | 16 +- .../tests/chrome-extension-package.test.js | 66 +- 37 files changed, 5131 insertions(+), 188 deletions(-) create mode 100644 docs/plans/2026-07-13-chrome-extension-native-tools-pr1.md create mode 100644 packages/chrome-extension/scripts/artifact-scan.js create mode 100644 packages/chrome-extension/scripts/package-extension.js create mode 100644 packages/chrome-extension/src/background/browser-mcp/browser-tools.test.ts create mode 100644 packages/chrome-extension/src/background/browser-mcp/browser-tools.ts create mode 100644 packages/chrome-extension/src/background/browser-mcp/connection.test.ts create mode 100644 packages/chrome-extension/src/background/browser-mcp/connection.ts create mode 100644 packages/chrome-extension/src/background/browser-mcp/debugger-session.test.ts create mode 100644 packages/chrome-extension/src/background/browser-mcp/debugger-session.ts create mode 100644 packages/chrome-extension/src/background/browser-mcp/server.test.ts create mode 100644 packages/chrome-extension/src/background/browser-mcp/server.ts create mode 100644 packages/chrome-extension/src/background/service-worker.test.ts create mode 100644 packages/chrome-extension/src/daemon/discovery.test.ts create mode 100644 packages/cli/src/serve/extension-pairing-routes.ts create mode 100644 packages/cli/src/serve/extension-pairing.test.ts create mode 100644 packages/cli/src/serve/extension-pairing.ts diff --git a/docs/plans/2026-07-13-chrome-extension-native-tools-pr1.md b/docs/plans/2026-07-13-chrome-extension-native-tools-pr1.md new file mode 100644 index 00000000000..5e542797c22 --- /dev/null +++ b/docs/plans/2026-07-13-chrome-extension-native-tools-pr1.md @@ -0,0 +1,340 @@ +# Chrome Extension Native Browser Tools PR1 Implementation Plan + +**Goal:** Ship PR1 for the official Qwen Code Chrome extension so users can install the extension, run `qwen serve`, and get baseline browser debugging tools without bundling or installing `chrome-devtools-mcp`. + +**Architecture:** Keep Qwen Code's agent loop and tool orchestration in the local daemon. The Chrome extension connects to the daemon over `/acp`, hosts a small native MCP server inside the extension, and implements browser tools with `chrome.debugger` CDP access. Add a one-time daemon-to-extension pairing step so the extension only exposes browser-control tools to a trusted local daemon. + +**Tech Stack:** TypeScript, Chrome Manifest V3 service worker, `chrome.debugger`, daemon `/acp` WebSocket, existing `qwen serve` Express server, Vitest, esbuild, npm package scanner. + +## Scope + +PR1 must include: + +- Native extension-hosted browser tools: navigation, click/fill/key/scroll/wait, screenshot/snapshot, console, network, evaluate, and request sending. +- Default `qwen serve` compatibility for the official extension ID, with no `QWEN_SERVE_CLIENT_MCP_OVER_WS` or `QWEN_SERVE_CDP_TUNNEL_OVER_WS` required. +- No bundled `chrome-devtools-mcp`, Puppeteer, or external browser automation server in the main npm package or extension ZIP. +- Compatibility fallback for explicit external adapter users through `QWEN_CDP_MCP_COMMAND`. +- One-time pairing/auth so a random local process cannot impersonate the daemon and invoke extension-hosted browser tools. +- Self-contained verification: unit tests, extension packaging scan, CLI tests, typecheck/build/bundle/package scan, and real Chrome smoke test. + +Out of scope for PR1: + +- Recording/replay workflows. +- Performance timeline/profiling UI. +- Multi-tab orchestration beyond active-tab control. +- Publishing to Chrome Web Store. + +## Current Baseline + +The worktree contains the native browser-tools implementation and daemon-extension pairing. First-use mutual HMAC proof keeps the terminal code and derived credential secret off the wire; stored credentials are challenge-verified before `/acp`, pairing routes precede bearer authentication, and failed attempts are bounded. + +## Task 1: Sync Base Branch + +**Files:** + +- No direct source edits. + +**Step 1: Fetch latest main** + +Run: + +```bash +git fetch origin main +``` + +Expected: fetch succeeds. + +**Step 2: Merge latest main into the worktree** + +Run: + +```bash +git merge origin/main +``` + +Expected: merge succeeds or exposes concrete conflicts to resolve. + +**Step 3: Inspect conflicts or changed upstream serve/extension code** + +Run: + +```bash +git status --short --branch +git diff --name-only --diff-filter=U +``` + +Expected: no unresolved conflicts before continuing. + +## Task 2: Define Pairing Contract Tests + +**Files:** + +- Create: `packages/cli/src/serve/extension-pairing.test.ts` +- Create: `packages/cli/src/serve/extension-pairing.ts` +- Modify: `packages/cli/src/serve/run-qwen-serve.test.ts` +- Modify: `packages/chrome-extension/src/daemon/discovery.test.ts` +- Modify: `packages/chrome-extension/src/background/service-worker.ts` +- Modify: `packages/chrome-extension/src/daemon/discovery.ts` + +**Behavior:** + +- Daemon prints a time-limited pairing code in the terminal and exposes only + pairing status on loopback. +- Extension can store a daemon trust credential after explicit pairing. +- `/acp` browser-tools connection must carry the trust credential after pairing. +- Official extension still shows `qwen serve` as the default startup command. +- If no credential is present, the extension must not register browser tools with an untrusted daemon. + +**Step 1: Write failing CLI pairing tests** + +Add tests for: + +- Pairing challenge generation returns a high-entropy code, nonce, and expiration timestamp. +- Pairing exchange rejects wrong or expired HMAC proofs without receiving the code. +- Pairing exchange mutually authenticates the daemon and derives a persistent credential without transferring its secret. +- Stored credential verification accepts the issued credential and rejects random values. + +Run: + +```bash +cd packages/cli && npx vitest run src/serve/extension-pairing.test.ts +``` + +Expected: fails because the module does not exist or behavior is missing. + +**Step 2: Write failing extension discovery tests** + +Add tests for: + +- Reading pairing state from storage. +- Returning `unpaired` when daemon is reachable but no trust credential exists. +- Returning `ready` when daemon is reachable and trust credential is accepted. + +Run: + +```bash +npm -w packages/chrome-extension run test -- src/daemon/discovery.test.ts +``` + +Expected: fails because pairing behavior is missing. + +## Task 3: Implement Minimal Daemon Pairing + +**Files:** + +- Create: `packages/cli/src/serve/extension-pairing.ts` +- Modify: `packages/cli/src/serve/run-qwen-serve.ts` +- Modify: `packages/cli/src/serve/run-qwen-serve.test.ts` +- Modify: `packages/cli/src/serve/types.ts` only if a shared type is truly needed. + +**Implementation:** + +- Keep state in the running daemon process for PR1: current pairing code, expiration, and issued trust credential hash. +- Generate a pasteable 128-bit code and nonce with Node `crypto`. +- Add loopback-only HTTP endpoints: + - `GET /extension/pairing` returns pairing status and expiration metadata, + but never returns the terminal code. + - `POST /extension/pairing/confirm` accepts a client proof and returns a credential ID plus daemon proof; the secret is derived independently at both ends. + - `POST /extension/pairing/verify` returns a challenge proof for the public + credential id without receiving the credential secret. +- Use constant-time comparison for credential verification. +- Do not write secrets to logs. +- Do not persist credentials in the repo; extension stores its copy in Chrome storage. + +**Step 1: Implement module only** + +Run: + +```bash +cd packages/cli && npx vitest run src/serve/extension-pairing.test.ts +``` + +Expected: pairing module tests pass. + +**Step 2: Wire routes into `qwen serve`** + +Run: + +```bash +cd packages/cli && npx vitest run src/serve/run-qwen-serve.test.ts +``` + +Expected: existing serve tests plus new pairing route tests pass. + +## Task 4: Implement Extension Pairing Flow + +**Files:** + +- Modify: `packages/chrome-extension/src/daemon/discovery.ts` +- Modify: `packages/chrome-extension/src/daemon/discovery.test.ts` +- Modify: `packages/chrome-extension/src/background/service-worker.ts` +- Modify: `packages/chrome-extension/public/sidepanel.js` +- Modify: `packages/chrome-extension/public/sidepanel.html` only if the existing markup cannot support the pairing UI. + +**Implementation:** + +- Add a `paired` daemon state in discovery. +- Store the daemon credential under the existing daemon storage object. +- Side panel states: + - daemon down: show `qwen serve`; + - daemon up but unpaired: ask the user to paste the pairing code shown + in the `qwen serve` terminal; + - daemon up and paired: frame Web Shell. +- Service worker behavior: + - Do not connect/register native browser tools until pairing verifies. + - Include the trust credential in `/acp` authentication using the existing token/subprotocol mechanism only if it does not collide with `QWEN_SERVER_TOKEN`; otherwise add a small pairing-specific message before MCP registration. +- Keep external adapter fallback unchanged. + +**Step 1: Add failing extension tests** + +Run: + +```bash +npm -w packages/chrome-extension run test -- src/daemon/discovery.test.ts src/background/service-worker.test.ts +``` + +Expected: tests fail for missing pairing state or service worker gating. + +**Step 2: Implement minimal pairing UI and service worker gating** + +Run: + +```bash +npm -w packages/chrome-extension run test +``` + +Expected: all extension tests pass. + +## Task 5: Preserve Packaging and Scanner Guarantees + +**Files:** + +- Modify: `packages/chrome-extension/scripts/artifact-scan.js` only if new signatures must be added. +- Modify: `scripts/tests/chrome-extension-package.test.js` if packaging expectations change. +- Modify: `packages/chrome-extension/package.json` only if package scripts need adjustment. + +**Behavior:** + +- Extension production ZIP must not contain `chrome-devtools-mcp`, Puppeteer, or external MCP server code. +- Main npm final package must not contain those signatures either. +- Native CDP code remains extension-only. + +**Step 1: Run extension release test** + +Run: + +```bash +npm -w packages/chrome-extension run test:release +``` + +Expected: tests, typecheck, build/package, and artifact scan pass. + +**Step 2: Run root package scanner** + +Run: + +```bash +npm run build +npm run bundle +npm run prepare:package +``` + +Expected: build and final package scan pass. + +## Task 6: Real Chrome Smoke Test + +**Files:** + +- No source edits unless smoke test exposes a product bug. + +**Setup:** + +Run daemon without browser env flags: + +```bash +node packages/cli/dist/index.js serve --port 4170 --hostname 127.0.0.1 --no-web +``` + +Load the built extension ZIP or unpacked `packages/chrome-extension/dist/extension`. + +**Verify manually or with Playwright:** + +- Extension side panel detects daemon. +- First run requires pairing. +- After pairing, reload the extension and verify it stays paired. +- `/workspace/mcp` includes `qwen-browser-tools`. +- Agent can: + - navigate to `http://127.0.0.1:4170/demo`; + - inspect snapshot; + - fill and click; + - evaluate JavaScript; + - read console output; + - read network requests and response metadata. + +Expected: all checks pass without `QWEN_SERVE_CLIENT_MCP_OVER_WS`, `QWEN_SERVE_CDP_TUNNEL_OVER_WS`, or `QWEN_CDP_MCP_COMMAND`. + +## Task 7: Final Review and PR Readiness + +**Files:** + +- Modify docs as needed: + - `packages/chrome-extension/README.md` + - `packages/chrome-extension/docs/05-daemon-direct-architecture.md` + +**Step 1: Run changed-file lint** + +Run: + +```bash +npm run lint +``` + +Expected: no errors. + +**Step 2: Run full typecheck** + +Run: + +```bash +npm run typecheck +``` + +Expected: no errors. + +**Step 3: Run code review workflow** + +Use review and ponytail checks on the final diff. + +Expected: no Critical or High issues remain. Any remaining Medium/Low items are documented as PR2 follow-ups. + +**Step 4: Prepare Draft PR** + +Draft PR must state: + +- PR1 user flow. +- Pairing behavior and security model. +- Native tools included. +- Explicit statement that `chrome-devtools-mcp` is not bundled. +- Test evidence with exact commands. +- Known PR2 follow-ups: recording/replay, performance profiling, richer multi-tab workflow. + +## Validation Results + +Validated on `origin/main` at `536cb713c6`: + +- Extension tests: 54 passed. +- Relevant CLI pairing, reverse MCP, ACP transport, and serve tests: 478 passed. +- Release-script tests: 3 passed. +- `npm run lint`, `npm run build`, `npm run typecheck`, and `npm run bundle`: passed. +- Extension `test:release`: passed, including ZIP creation and artifact scan. +- Real Chrome smoke test: pairing succeeded without browser environment flags, the Web Shell iframe loaded, `qwen-browser-tools` registered, and all 20 tools were discoverable. +- Main npm tarball: 23,231,294 bytes and 833 files; path and content scans found no `chrome-devtools-mcp`, Puppeteer, extension ZIP/manifest, or native browser-MCP source. + +## Acceptance Criteria + +- Official extension + `qwen serve` gives browser tools after one-time pairing. +- No external `chrome-devtools-mcp` install is required for PR1 baseline tools. +- No browser env flags are required for the official extension path. +- Main npm package and extension ZIP scans do not flag `chrome-devtools-mcp` or Puppeteer. +- Untrusted local processes cannot silently use the extension-hosted tools. +- All listed verification commands pass in this worktree before declaring the PR ready. diff --git a/package-lock.json b/package-lock.json index 49afd855a02..3c9783a0e30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28093,6 +28093,7 @@ "license": "Apache-2.0", "devDependencies": { "@types/chrome": "^0.1.32", + "archiver": "^7.0.1", "esbuild": "^0.25.3", "typescript": "^5.8.3" }, diff --git a/package.json b/package.json index 2efad459b3f..dcaa158cef4 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "bundle": "npm run generate && node esbuild.config.js && node scripts/copy_bundle_assets.js", "test": "cross-env NODE_OPTIONS=\"--max-old-space-size=3072\" npm run test --workspaces --if-present --parallel", "test:ci": "cross-env NODE_OPTIONS=\"--max-old-space-size=3072\" npm run test:ci --workspaces --if-present --parallel && npm run test:scripts", - "test:release": "cross-env NODE_OPTIONS=\"--max-old-space-size=3072\" npm run test:ci --workspaces --if-present --parallel -- --coverage.enabled=false && npm run test:scripts", + "test:release": "cross-env NODE_OPTIONS=\"--max-old-space-size=3072\" npm run test:ci --workspaces --if-present --parallel -- --coverage.enabled=false && npm run test:scripts && npm -w packages/chrome-extension run package", "test:scripts": "vitest run --config ./scripts/tests/vitest.config.ts", "test:e2e": "cross-env VERBOSE=true KEEP_OUTPUT=true npm run test:integration:sandbox:none", "test:integration:all": "npm run test:integration:sandbox:none && npm run test:integration:sandbox:docker && npm run test:integration:sandbox:podman", diff --git a/packages/chrome-extension/README.md b/packages/chrome-extension/README.md index 05f3f221deb..dfc19279ad5 100644 --- a/packages/chrome-extension/README.md +++ b/packages/chrome-extension/README.md @@ -6,11 +6,10 @@ Messaging host to install. It does two things: -- **Side panel** — frames the daemon's Web Shell (chat + tools), the same UI the - 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 when an external CDP MCP adapter is configured. +- **Side panel** — handles daemon discovery and pairing, then frames the + daemon's Web Shell (chat + tools). +- **Service worker** — hosts Qwen's browser MCP tools and executes them through + `chrome.debugger`. Tool calls travel over the daemon's reverse MCP WebSocket. ## Build @@ -23,53 +22,70 @@ Then load it: `chrome://extensions` → enable Developer mode → **Load unpacke ## Run -The extension is a client; the daemon does the work and must be started -separately (an extension cannot spawn a local process). Open the side panel and -it will tell you exactly what to run — it generates the command with this -extension's own id: +The extension cannot spawn a local process, so start the daemon separately: ```bash -qwen serve --allow-origin chrome-extension:// +qwen serve ``` -`--allow-origin chrome-extension://` is required: it lets the daemon's Web -Shell be framed by the extension (the `frame-ancestors` CSP) and accepts the -extension's requests. The side panel reads the id at runtime via -`chrome.runtime.id`, so you never have to look it up. +The official extension id is pinned by `qwen serve`, so no browser-related +environment variables or `--allow-origin` flag are required. Custom or forked +extension builds must still pass their own origin explicitly: -Once the daemon is reachable and permits framing, the side panel swaps the -welcome screen for the chat UI automatically. +```bash +qwen serve --allow-origin chrome-extension:// +``` + +Paste the pairing code printed by `qwen serve`. The credential remains in +Chrome storage across extension reloads, but a restarted daemon requires a new +pairing code because the daemon keeps trust state in memory. Once pairing +succeeds, the panel opens the chat UI and browser tools register immediately. +If Chrome storage is cleared while the daemon is still running, restart the +daemon to generate fresh pairing material. + +The first-use exchange sends only an HMAC challenge proof; the pairing code and +derived credential secret never cross HTTP. The extension verifies the daemon's +proof before storing that credential, then uses a separate challenge-response +before sending it over `/acp`. Pairing endpoints intentionally precede bearer +authentication so an unknown process never receives a stored bearer token. The +pairing code is time-limited and failed attempts are bounded. ## 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: +Browser debugging tools are implemented in and bundled with this Chrome +extension. The main `@qwen-code/qwen-code` npm package does not contain an +external Chrome DevTools MCP server. The first-release catalog covers page +snapshot/navigation/input, screenshots, JavaScript evaluation, console output, +and network request/response inspection. -```bash -QWEN_CDP_MCP_COMMAND=/path/to/cdp-mcp-adapter \ -qwen serve --allow-origin chrome-extension:// -``` +Tools act on the active tab. `evaluate_script` and `send_request` execute in the +page context and can access that page's authenticated session, so use a dedicated +browser profile or tab for untrusted sites and keep normal tool approval enabled. -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`: +An explicitly configured `QWEN_CDP_MCP_COMMAND` remains a deprecated +compatibility path targeted for removal in PR2. When present, the extension does +not register its native tool catalog and instead keeps the CDP tunnel available +to that adapter. + +Relevant `/capabilities` tags: - `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. +- `client_mcp_over_ws` means extension-hosted tools can register over `/acp`. +- `browser_automation_mcp` means the legacy external adapter is configured. ## Onboarding states The side panel probes `GET /health` and `GET /capabilities` and shows one of: -| State | Meaning | Shown | -| -------------------- | -------------------------------------- | -------------------------------- | -| `down` | no daemon reachable | "Start qwen serve" + command | -| `needs-allow-origin` | daemon up but `--allow-origin` not set | "Allow this extension" + command | -| `ready` | daemon up and framing permitted | the Web Shell (chat) | +| State | Meaning | Shown | +| -------------------- | ---------------------------------------- | -------------------------------- | +| `down` | no daemon reachable | "Start qwen serve" + command | +| `needs-upgrade` | daemon lacks secure extension pairing | Qwen Code update command | +| `needs-restart` | Chrome lost the active daemon credential | daemon restart guidance | +| `needs-allow-origin` | daemon up but `--allow-origin` not set | "Allow this extension" + command | +| `needs-pairing` | daemon reachable, credential not trusted | pairing-code form | +| `ready` | daemon reachable and paired | the Web Shell (chat) | ## Packaging for the Chrome Web Store @@ -77,6 +93,12 @@ The side panel probes `GET /health` and `GET /capabilities` and shows one of: npm run package # -> chrome-extension.zip (manifest at the zip root) ``` -Upload the zip to the Chrome Web Store Developer Dashboard. Note that the -`debugger` and `` permissions will draw manual review — justify them -in the store listing. +Upload the zip to the Chrome Web Store Developer Dashboard. The `debugger` +permission will draw manual review; explain that it is used only after a paired +local Qwen Code daemon requests a browser debugging action. Host permissions +are limited to the loopback daemon. + +Release the matching Qwen Code CLI before publishing the extension update. The +pairing handshake intentionally does not downgrade for older daemons; the side +panel detects them and shows an update command instead of sending browser tools +to an unauthenticated local process. diff --git a/packages/chrome-extension/config/esbuild.background.config.js b/packages/chrome-extension/config/esbuild.background.config.js index c90ebf0a502..3b14e3d0f61 100644 --- a/packages/chrome-extension/config/esbuild.background.config.js +++ b/packages/chrome-extension/config/esbuild.background.config.js @@ -38,6 +38,7 @@ async function build() { target: ['chrome115'], minify: isProduction, sourcemap: !isProduction, + metafile: !isWatch, outdir: path.join(projectRoot, outDir), outbase: path.join(projectRoot, 'src'), logLevel: 'info', @@ -47,7 +48,12 @@ async function build() { console.log('Watching background/content scripts...'); await ctx.watch(); } else { - await ctx.rebuild(); + const result = await ctx.rebuild(); + if (result.metafile) { + const metafilePath = path.join(projectRoot, 'dist/esbuild.json'); + fs.mkdirSync(path.dirname(metafilePath), { recursive: true }); + fs.writeFileSync(metafilePath, JSON.stringify(result.metafile, null, 2)); + } await ctx.dispose(); console.log('Background/content build complete!'); } diff --git a/packages/chrome-extension/docs/05-daemon-direct-architecture.md b/packages/chrome-extension/docs/05-daemon-direct-architecture.md index d1c38982cb7..4c670c0ec1f 100644 --- a/packages/chrome-extension/docs/05-daemon-direct-architecture.md +++ b/packages/chrome-extension/docs/05-daemon-direct-architecture.md @@ -1,40 +1,40 @@ # Daemon-Direct Architecture (issue #5626) Revival of the Chrome extension on the `qwen serve` daemon, dropping Native -Messaging. This doc is the concrete implementation spec for the two phases. +Messaging and external browser-tool servers from the default path. ``` ┌─ Chrome extension (pure web client) ──────────────┐ -│ Side panel (React, @qwen-code/webui) │ -│ DaemonSessionProvider ── chat over HTTP+SSE ───┼──┐ +│ Side panel │ +│ daemon discovery + framed Web Shell ───────────┼──┐ │ Service worker │ │ │ browser-tools MCP server (over WS) ────────────┼─┐│ -│ Content scripts (DOM / a11y / network capture) │ ││ +│ chrome.debugger CDP tools + event capture ──────┼─┘│ └───────────────────────────────────────────────────┘ ││ ▼▼ qwen serve daemon (localhost:4170, loopback auth-free) ``` -## Phase 1 — chat (no daemon changes) +## Chat and pairing The side panel is a daemon client. `@qwen-code/webui`'s `DaemonSessionProvider` ({ baseUrl, token? }) handles connect / session-create / SSE / reconnect / heartbeat. Loopback ⇒ `token` omitted, `workspaceCwd` omitted (daemon uses its bound workspace). -- `src/daemon/config.ts` — `{ baseUrl, token? }`, default `http://127.0.0.1:4170`, - overridable via `chrome.storage.local`. -- `src/daemon/discovery.ts` — `GET /health` probe; gate the chat on reachability, - otherwise show a "run `qwen serve`" hint. -- Side panel renders transcript/streaming/permissions from the webui daemon hooks, - reusing the existing presentational components + `ChromePlatformProvider`. +- `src/daemon/config.ts` stores the loopback base URL, optional daemon bearer, + and the paired extension credential in `chrome.storage.local`. +- `src/daemon/discovery.ts` probes daemon health and verifies a pairing + challenge before the panel or service worker trusts that daemon. +- The side panel frames the daemon Web Shell after discovery and pairing. +- Pairing state is process-local in PR1, so restarting `qwen serve` requires a + fresh terminal code. First-use mutual HMAC proof keeps both the code and the + derived credential secret off the wire; later discovery also uses an HMAC + challenge so stored credentials are not sent to an unknown process. -The native-messaging transport (`background/native-connection.ts`, -`native-message-handler.ts`, `native-messaging.ts` wiring) is dropped; the -browser-tool executors, catalog, router, network tools, and content scripts are -kept for Phase 2. +The native-messaging transport is not part of this path. -## Phase 2 — browser tools (reverse channel; touches the daemon contract) +## Browser tools — extension-hosted reverse MCP A browser extension cannot be a listening MCP server. The agent runs inside the daemon and must reach tools that execute in the extension. The mechanism already @@ -64,31 +64,28 @@ agent MCP client → SdkControlClientTransport.send → daemon: resolve sendMcpMessage promise → agent gets the tool result ``` -### Daemon side (new — `packages/cli/src/serve`, public-contract surface) +### Daemon side (`packages/cli/src/serve`, public-contract surface) 1. WS message types on the serve transport: `mcp_register` (client advertises a - server name + tool catalog), `mcp_message` (bidirectional JSON-RPC with an - `id` for request/response correlation), `mcp_unregister`. + server name; tools are discovered through MCP), `mcp_message` (bidirectional + JSON-RPC with an `id` for request/response correlation), `mcp_unregister`. 2. On `mcp_register`, register a runtime **SDK-type** MCP server for the session (reuse `addRuntimeMcpServer` + `isSdkMcpServerConfig`), wiring its `sendSdkMcpMessage` callback to push `mcp_message` frames down this client's WS and await the correlated response. 3. Tear down on WS close / `mcp_unregister`. -4. Gate behind a capability flag (`caps.features` += `client_mcp_over_ws`) until - the contract is settled — this is the open question raised in #5626. - -### Extension side (reuses existing executors) - -- `src/background/browser-tools-server.ts` (new): build an MCP `Server` - (`@modelcontextprotocol/sdk`) whose `tools/list` = the kept `tool-catalog.ts` - and `tools/call` dispatches via the existing `tool-router.ts` → - `browser-tool-executors.ts` / `browser-network-tools.ts`, formatting with - `mcp-tool-result.ts`. Connect it to a transport that sends/receives - `mcp_message` over the DaemonClient WS; register on connect. -- MVP catalog (~6 tools, read-first): `chrome_read_page`, `chrome_screenshot`, - `chrome_console`, `chrome_navigate`, `chrome_click_element`, `chrome_fill_or_select`. - Write/navigate tools gated behind per-tool consent (security: browser origin - defaults to read-only). +4. Advertise `client_mcp_over_ws`; paired extension clients work by default. + Operators can disable the channel with `QWEN_SERVE_CLIENT_MCP_OVER_WS=0` or + explicitly set it to `1` to permit legacy unpaired reverse MCP clients. + +### Extension side + +- `src/background/browser-mcp/server.ts` implements the small MCP JSON-RPC + surface needed by the daemon transport without bundling another server. +- `src/background/browser-mcp/browser-tools.ts` owns the tool catalog and the + bounded Console/Network recorders. +- `src/background/browser-mcp/debugger-session.ts` owns the active tab debugger + attachment and CDP command/event lifecycle. ## Daemon lifecycle (issue #5626 Q3) diff --git a/packages/chrome-extension/package.json b/packages/chrome-extension/package.json index 598794e1605..92af263d894 100644 --- a/packages/chrome-extension/package.json +++ b/packages/chrome-extension/package.json @@ -32,8 +32,10 @@ "build": "node scripts/sync-extension.js && node config/esbuild.background.config.js --production", "test": "vitest run --config vitest.config.ts", "test:ci": "vitest run --config vitest.config.ts", + "test:release": "npm run test:ci && npm run typecheck && npm run package", + "scan:artifacts": "node scripts/artifact-scan.js", "dev:chrome": "open -a 'Google Chrome' --args --load-extension=$PWD/dist/extension --auto-open-devtools-for-tabs", - "package": "npm run build && cd dist/extension && zip -r ../../chrome-extension.zip .", + "package": "npm run build && node scripts/package-extension.js && npm run scan:artifacts", "clean": "./scripts/clean.sh", "typecheck": "tsc --noEmit" }, @@ -42,6 +44,7 @@ }, "devDependencies": { "@types/chrome": "^0.1.32", + "archiver": "^7.0.1", "esbuild": "^0.25.3", "typescript": "^5.8.3" } diff --git a/packages/chrome-extension/public/sidepanel.html b/packages/chrome-extension/public/sidepanel.html index b4b7ea9f733..36db5f04454 100644 --- a/packages/chrome-extension/public/sidepanel.html +++ b/packages/chrome-extension/public/sidepanel.html @@ -272,6 +272,53 @@ display: inline; } + .pair { + display: grid; + gap: 9px; + animation: rise 0.55s 0.21s both; + } + .pair__row { + display: flex; + gap: 8px; + } + .pair__input { + min-width: 0; + flex: 1; + padding: 10px 11px; + color: var(--text); + background: var(--panel); + border: 1px solid var(--border); + border-radius: 8px; + font: + 600 13px/1 ui-monospace, + SFMono-Regular, + monospace; + letter-spacing: 0; + } + .pair__input:focus { + outline: 2px solid var(--accent); + outline-offset: 1px; + } + .pair__button { + flex: none; + padding: 0 13px; + color: var(--bg); + background: var(--status); + border: 1px solid transparent; + border-radius: 8px; + font-weight: 650; + cursor: pointer; + } + .pair__button:disabled { + opacity: 0.6; + cursor: wait; + } + .pair__message { + min-height: 18px; + color: var(--muted); + font-size: 12px; + } + /* ---- live status ---- */ .status { display: flex; @@ -365,8 +412,7 @@

Start qwen serve

No local qwen serve daemon is reachable. Run this in a terminal - and leave it running — this panel connects on its own. Browser - automation tools require QWEN_CDP_MCP_COMMAND. + and leave it running — this panel connects on its own.

@@ -427,6 +473,25 @@

Start qwen serve

Copy command + +