feat(schema,engine,desktop): browser broker — wire contract, webview executor, code-mode execute tool for claude/pi (CODE-267)#188
Conversation
CODE-267 feat(schema,engine,desktop): browser broker — wire contract, desktop webview executor, code-mode execute tool for claude/pi
目标Agent 自动化 Phase B-1:paseo 式 daemon broker——agent 经 code-mode 前提事实(2026-07-17 实勘)
TasksWire(
Engine
桌面执行端
安全与默认值
验收
风险
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 38e3923d2f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const script = new Script(`(async () => {\n${code}\n})()`); | ||
| const value = (await script.runInContext(this.vmContext)) as unknown; |
There was a problem hiding this comment.
Preserve REPL bindings across execute calls
When an agent follows the documented persistent-REPL behavior and declares a normal binding such as const tabId = ... in one execute call, this per-call async IIFE makes that binding local to the wrapper and discards it as soon as the call returns; the next execute sees ReferenceError instead of the promised persistent variable. Only accidental globals persist, so the browser tool won't behave like the documented REPL for normal JavaScript.
Useful? React with 👍 / 👎.
| const script = new Script(`(async () => {\n${code}\n})()`); | ||
| const value = (await script.runInContext(this.vmContext)) as unknown; |
There was a problem hiding this comment.
Put a timeout around browser-tool code
With LINKCODE_BROWSER_TOOLS=1, model-provided code runs in the daemon process here with no vm timeout or other guard, so a non-yielding snippet such as while (true) {} blocks the daemon event loop. In that case the broker's 15s command timeout and socket message handling cannot fire, leaving every connected client hung until the daemon is killed.
Useful? React with 👍 / 👎.
Greptile SummaryThis PR adds a browser broker so code-mode agents can drive the same desktop browser tabs the user sees. The main changes are:
Confidence Score: 4/5Mostly safe, with one contained reliability issue to fix before merging. Broker routing, schema, host channel, and desktop executor changes are scoped and covered by tests. The REPL execution path lacks bounds, so one bad browser execute call can hang the affected live session or daemon path. packages/engine/src/browser/repl-host.ts
What T-Rex did
Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Agent as Agent code-mode execute tool
participant Engine as Engine BrowserReplHost/Broker
participant Hub as Transport Hub
participant Desktop as Desktop BrowserHostChannel
participant Webview as Visible Electron webview
Agent->>Engine: "browser.* JS call"
Engine->>Engine: dispatch(op,args), mint commandId
Engine->>Hub: browser.command(commandId, op, args)
Hub->>Desktop: targeted delivery to active browser host
Desktop->>Webview: execute store/webview operation
Webview-->>Desktop: operation result / screenshot / error
Desktop->>Hub: browser.command.result(commandId, result)
Hub->>Engine: forward settlement
Engine-->>Agent: resolve execute result
alt host disconnects or times out
Hub->>Engine: browser.host.detached(hostId)
Engine-->>Agent: closed-code error result
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Agent as Agent code-mode execute tool
participant Engine as Engine BrowserReplHost/Broker
participant Hub as Transport Hub
participant Desktop as Desktop BrowserHostChannel
participant Webview as Visible Electron webview
Agent->>Engine: "browser.* JS call"
Engine->>Engine: dispatch(op,args), mint commandId
Engine->>Hub: browser.command(commandId, op, args)
Hub->>Desktop: targeted delivery to active browser host
Desktop->>Webview: execute store/webview operation
Webview-->>Desktop: operation result / screenshot / error
Desktop->>Hub: browser.command.result(commandId, result)
Hub->>Engine: forward settlement
Engine-->>Agent: resolve execute result
alt host disconnects or times out
Hub->>Engine: browser.host.detached(hostId)
Engine-->>Agent: closed-code error result
end
Reviews (1): Last reviewed commit: "test(desktop): browser broker end-to-end..." | Re-trigger Greptile |
| const script = new Script(`(async () => {\n${code}\n})()`); | ||
| const value = (await script.runInContext(this.vmContext)) as unknown; |
There was a problem hiding this comment.
Bound REPL execution
execute awaits arbitrary model-supplied code without any timeout, so while (true) {} blocks the daemon event loop before the returned promise is created and await new Promise(() => {}) leaves the adapter tool call pending forever. Broker commands time out, but this REPL layer does not, so one bad browser.execute invocation can wedge the live session until the process is restarted.
Artifacts
Repro: BrowserReplHost no-timeout runtime harness
- Contains supporting evidence from the run (text/javascript; charset=utf-8).
Repro: harness output showing pending promise watchdog and sync-loop SIGKILL
- Keeps the command output available without making the summary code-heavy.
Summary
Phase B-1 of the in-app browser roadmap (CODE-267), stacked on #181 (CODE-266 multi-tab panel — its tab store is this feature's execution surface). Agents get a single code-mode
executetool driving the SAME browser tabs the user sees, per CODE-33's locked code-mode decision.browserdomain (packages/schema/src/wire/browser.ts+src/browser.ts):browser.host.register(client-minted capability, mirrors terminal attachments), Hub-synthesizedbrowser.host.detached,browser.host.changedbroadcast, daemon→hostbrowser.command/browser.command.result(closed op enum + closed error codes withretryable), and the client-sidebrowser.execute/browser.executedpair (used by the E2E now, by the B-2 stdio bridge later).browser.host.registersucceeds becomes THE single active host (last registration wins — commands have side effects and are never fanned out);browser.commandframes are delivered only there; disconnect synthesizesbrowser.host.detached(guarded by hostId so a stale drop can't clear a newer host).BrowserBrokerService(engine): pending-map correlation by daemon-mintedcommandId, 15 s timeout, fail-all on host detach/supersede/shutdown, availability broadcasts.dispatchnever rejects — every failure is a closed-code result.client-coreBrowserHostChannelregisters via the workbench hookuseBrowserHostRegistration(re-registers per connection generation); the renderer executor drives the CODE-266 tab store (open/select/close/navigate) and live webviews (executeJavaScriptsnapshot with@eNmarker-attribute refs capped at 200, click/type with stale-ref discipline,capturePagescreenshots, evaluate). Screenshot results are attached as real image content blocks.packages/engine/src/browser/repl-host.ts, reviving the sky-mcp skeleton): persistentnode:vmcontext exposing abrowsernamespace + captured console; behavior discipline is embedded in the tool description. claude-code hosts it as an in-process MCP server (createSdkMcpServer, approvals ride the existingcanUseToolflow); pi hosts it viacustomTools(TypeBox).EngineDeps.browserToolsEnabled, daemon env gateLINKCODE_BROWSER_TOOLS=1. The Settings toggle is a follow-up; host registration itself is ungated (harmless).Verification
pnpm -F @linkcode/desktop e2e:browser-broker(PASS): isolated daemon (wire 38) + real desktop; a raw wire client executestabs.open→ the desktop executor opens a visible tab and returns the tab list;tab.clickwithout a snapshot →stale-ref; desktop quit →host-unavailableinstead of a hang.LINKCODE_BROWSER_TOOLS=1drivingbrowser.*end-to-end (needs a signed-in claude), and a pi smoke. pi has no per-call approval seam — known limitation, gate-guarded.Linear: CODE-267