-
Notifications
You must be signed in to change notification settings - Fork 0
CDP Facade & Browser Agent Server
This directory is the opt-in localhost control plane for browser nodes. A browser node is an Electron <webview> guest rendered on the canvas. External agents need two capabilities: a state-changing way to ask the app to open a visible browser node, and a CDP attach surface that lets Playwright/Puppeteer drive that exact guest. The design keeps both behind loopback-only, token-gated endpoints and explicitly avoids Chromium’s raw remote-debugging port.
| File | Responsibility |
|---|---|
runtime.ts |
Per-boot token for the agent-control surface. No raw Chromium --remote-debugging-port is opened. |
manager.ts |
Secures every guest at attach, keeps the nodeId::tabId → guestId registry, and centralizes browser-node navigation/popup policy. |
agent-server.ts |
Token-gated HTTP control server: GET /info, POST /open, discovery file userData/browser-agent.json mode 0600. |
cdp-facade.ts |
Virtual CDP browser on its own loopback port: exposes guests as standard page targets, routes CDP sessions to guest debuggers, supports Playwright auto-attach and Puppeteer direct attach. |
| Tests |
cdp-facade.test.ts, agent-server.test.ts, manager.test.ts cover the facade protocol, control server, and manager behavior. |
- Both servers bind to
127.0.0.1only, never0.0.0.0, and uselisten(0)so the OS assigns a random high port. - The agent control server requires an exact
Authorization: Bearer <token>on every request. Missing or wrong auth returns401. - The CDP facade requires the same per-boot token on every HTTP discovery call and on the WebSocket upgrade, via
?token=orAuthorization: Bearer. Unauthenticated WebSocket connections are closed with code4001. - The token is shared across the agent-control surface:
StartAgentServerOptions.tokencan be passed in so the discovery file and facade metadata stay coherent. If omitted, the agent server generates a fresh random token. - The discovery file is written to
userData/browser-agent.jsonwith mode0600because it carries the bearer token. It is removed on close so a stopped endpoint is never advertised. -
POST /openvalidates the requested URL throughcore/browser-policybefore broadcasting. Empty body or empty URL becomesabout:blank; a present-but-invalid URL is denied with400. - Guest hardening happens in
manager.ts: strip preload, force context isolation and sandbox, keepwebSecurityon, block non-webwill-navigate/will-redirect, and allow only sandboxed web sign-in popups sharing the guest session. Nested popups are denied. - Debug logging is opt-in via
TERMSPRAWL_FACADE_DEBUG=1and omits payloads, including cookie data.
The directory contract explicitly states that no raw Chromium --remote-debugging-port is opened; an earlier audit found that such a port exposed the main window’s full preload bridge to local processes. The facade exists precisely to provide a narrower, token-gated surface instead.
- On boot, a per-boot token is established and shared by the agent control server and CDP facade.
- The agent control server writes
browser-agent.jsoncontainingport,token,cdp, andopen { method, path, url }. - An external agent reads the discovery file or calls
GET /info, then callsPOST /openwith a bearer token. -
agent-server.tsnormalizes and validates the URL withnormalizeAddress/ABOUT_BLANKfromcore/browser-policy. Invalid URLs return400; valid URLs are broadcast to the renderer onbrowser:agent-open. - The renderer materializes the browser node; the main-process
BrowserManagerrecords the guest undernodeId::tabIdviaregisterBrowserGuest. - The agent discovers CDP through the facade’s authenticated
/json/versionresponse, which returns awebSocketDebuggerUrlembedding the token. - Playwright
connectOverCDPor Puppeteer connects to that WebSocket. The upgrade token is checked beforecurrentWsis claimed. - Puppeteer uses
Target.getTargets+Target.attachToTarget; Playwright usesTarget.setAutoAttachand receivesTarget.attachedToTargetevents. The facade also pollsbrowserGuestIds()every 500 ms while auto-attach is active, emitting attach events for new guests and detach events for guests that died. -
attachGuestattaches Electron’swebContents.debugger, creates a facade session id, and learns the guest’s real CDP target id fromPage.getFrameTree. - Page-level CDP messages carrying a
sessionIdare routed throughsessionToGuestto the guest debugger; guest debugger events are forwarded back tocurrentWswith the same session id. - When the WebSocket closes,
resetFacadeState()detaches guest debuggers, clears routing/auto-attach/target-id maps, and keeps theforwardedGuestsmarker to avoid installing duplicate event forwarders.
sequenceDiagram
autonumber
participant Agent as External Agent
participant Server as Agent Control Server
participant Renderer as Renderer
participant Facade as CDP Facade
Agent->>Server: GET /info (Bearer token)
Server-->>Agent: { ok, cdp, open }
Agent->>Server: POST /open { url }
Server->>Server: normalizeAddress(url)
alt invalid URL
Server-->>Agent: 400 denied url
else valid or blank
Server->>Renderer: broadcast(browser:agent-open)
Server-->>Agent: 200 { ok, url }
end
Agent->>Facade: GET /json/version (token)
Facade-->>Agent: webSocketDebuggerUrl?token=...
Agent->>Facade: WebSocket upgrade + CDP
Facade->>Facade: attachGuest / route / forward
Facade-->>Agent: Target.attachedToTarget / page events
The key nodes: the agent control server is the only state-changing entry point; the renderer owns node/guest creation; the facade is the CDP translation layer. The BrowserManager registry is what makes a canvas node addressable by its stable nodeId and tab id, and browserGuestIds() is the facade’s source of live guests.
Playwright’s connectOverCDP filters targets by type and does not surface Electron webview guests because they report type webview. Puppeteer and raw CDP attach fine, but a Playwright-only agent never sees the embedded browser. The facade therefore presents a minimal “virtual browser” on its own loopback port and re-exposes every live guest as a standard page target.
Two attach models are supported:
-
Puppeteer-style direct attach:
Target.getTargetslists targets, andTarget.attachToTargetattaches directly.attachGuestcreates the session mapping and attaches the guest debugger. -
Playwright-style auto-attach:
Target.setAutoAttachturns onautoAttach. The facade attaches to every live guest, emitsTarget.attachedToTarget, and keeps polling for newcomers. Each attached target includes a non-empty syntheticbrowserContextIdbecause Playwright asserts one and buckets pages by it; the facade uses a single sharedFAKE_BROWSER_CONTEXTfor all guests.
The most delicate part is target identity. Chromium reports a <webview> guest’s CDP target id as its main frame id. Playwright resolves frame sessions by that id. If the facade reported the numeric webContents id while Page.getFrameTree reported the hex frame id, Playwright would throw “Frame has been detached” and silently degrade the page to a dummy frame. To prevent that, attachGuest learns the real target id from the guest’s own Page.getFrameTree call and stores it in guestTargetIds. buildTargetInfo then reports that real id.
Browser-level Storage cookie commands map to the live guest profile, and cookie writes flush to disk. Debug logs never include cookie payloads.
| State | Location | Purpose |
|---|---|---|
currentWs |
cdp-facade.ts |
Singleton active CDP client. The facade is deliberately single-client; a second connection effectively evicts the first, matching Chromium’s one-client debug port. |
sessionToGuest / guestToSession
|
cdp-facade.ts |
Bidirectional session-id ↔ guest webContents id routing. |
guestTargetIds |
cdp-facade.ts |
Guest id → real CDP target id, learned at attach from Page.getFrameTree. |
autoAttach, autoAttachedGuests, guestPollTimer
|
cdp-facade.ts |
Playwright auto-attach bookkeeping and 500 ms newcomer/dead-guest polling. |
forwardedGuests |
cdp-facade.ts |
Installs exactly one debugger message forwarder per guest. This set persists across resetFacadeState() because the forwarder reads live maps and no-ops when guestToSession is empty. |
FAKE_BROWSER_CONTEXT |
cdp-facade.ts |
Synthetic shared browser context id required by Playwright. |
guests |
manager.ts |
nodeId::tabId → guest WebContents id. Enables driving a node by its stable canvas id. |
| token / discovery file |
agent-server.ts, runtime.ts
|
Auth for both the control server and the CDP facade. |
- The agent server’s request body is capped at 256 KiB; larger bodies are destroyed.
- Malformed JSON or a missing
urlinPOST /openopensabout:blankrather than failing. - The agent server returns
404for unknown routes and401for missing/wrong bearer auth. - The facade returns
401for unauthenticated discovery endpoints and closes unauthenticated WebSocket upgrades with4001. - Both servers swallow server
errorevents: a port/socket failure simply leaves the endpoint undiscoverable rather than hanging startup. -
attachGuestis idempotent about attaching the debugger because Electron allows only one debugger perwebContents; a previous facade instance may have left it attached. -
resetFacadeState()detaches debuggers and clearssessionToGuest,guestToSession,autoAttachedGuests, andguestTargetIds. It deliberately keepsforwardedGueststo avoid double event delivery. -
liveGuestguards against destroyedwebContents;detachGuesttreats already-detached debuggers as safe. -
guestIdForTargetIdfalls back to interpreting a numeric target id as a guest id if no learned mapping exists. - Guest lifetime is tied to the renderer’s
<webview>DOM; the renderer destroys the guestwebContentswhen a tab’s webview is removed, andunregisterBrowserGuestonly updates the main-process map. - URL policy is centralized in
src/core/browser-policy.ts(normalizeAddress,ABOUT_BLANK,isAllowedNavUrl). Navigation checks should not be inlined into these servers. - The source comment on
StartCdpFacadeOptions.cdpInfodescribes it as loopback info for the app’s raw debug port, whileAGENTS.mdstates that no raw Chromium remote-debugging port is opened. Treat the surfacedcdpmetadata as agent-facing discovery data and verify the current boot wiring before relying on it.
- Add or adjust CDP domains/methods in the facade’s
route()path. The session maps,attachGuest,detachGuest,buildTargetInfos,emitAttached, and the per-guest event forwarder are the surrounding integration points. - Change auto-attach newcomer/dead-guest handling in the 500 ms
guestPollTimerloop. - Extend the discovery payload in
agent-server.ts(port,token,cdp,open) when new agent-facing metadata is needed. - Reuse the shared per-boot token by passing
StartAgentServerOptions.tokenandStartCdpFacadeOptions.token. - Browser-tab support is already represented by
nodeId::tabId;guestIdForNodeaccepts an optional tab id and has a legacy fallback for old nodes without a persisted tab id. - URL policy changes belong in
src/core/browser-policy.ts, not in the loopback servers.
Sources: src/main/browser/AGENTS.md, src/main/browser/agent-server.ts, src/main/browser/cdp-facade.ts, src/main/browser/cdp-facade.ts, src/main/browser/manager.ts
Generated from termsprawl at 0d4393be54c6200beedd91bb636e5296c30472c5.
App Shell & Platform Foundations
- Electron Main Process & Window Lifecycle
- Preload Bridge & IPC Contract
- Shared Domain Types and File/URL Helpers
- Renderer Bootstrap & App Composition
- Build Targets & TypeScript Configuration
Canvas, Nodes & Renderer State
- Infinite Canvas Surface & Viewport Interaction
- Workspace, Project & Tab State
- Node Links, Edges & Link Inspector
- Sticky, Group, Editor & Diff Nodes
- Keyboard Canvas Navigation & Cross-Panel Requests
- Theme, Accent & Visual Language
- Boot Overlay, Onboarding & Shared UI Kit
Terminals & Session Continuity
- PTY Lifecycle & Terminal Sessions
- tmux Session Naming & Reattach
- Scrollback Snapshots & Cold Replay
- Terminal Node Rendering (xterm.js)
- SSH Remote Projects, Terminals & Files
Persistence, Projects & Files
- Workspace Store & Project File Layout
- Project Scope, Deletion & Worktree Registry
- Workspace Bundle Export/Import
- File Service & File Tree UI
Agent Runtime & Tooling
- Agent Status Model & Hook Normalization
- Hook Server & CLI Hook Installers
- Agent Launch, CLI Probing & Managed Accounts
- Agent Tool Protocol & In-Process Server
- Agent Tool Client, CLI & MCP Entry
- Transcripts, Context Discovery & Context CLI
- Agent Canvas State & Status Badges
Chat Nodes & Model Providers
- Chat Runtime, Conversation & Cost
- Model Provider Adapters & Streaming
- Chat Tool Calling & Project Tools
- Chat Node UI
Git & Source Control
Embedded Browser Nodes
- Browser Manager & Guest Runtime
- CDP Facade & Browser Agent Server
- Browser Navigation Policy & Node UI
Server Edition
- Server Bootstrap & HTTP/WebSocket Entry
- RPC Dispatch, Handlers & Service Bridges
- Renderer Shim & Server Boundary
- Server Auth & Security Boundary
Relay & Remote Access
- Relay Hub & WebSocket Frame Routing
- Relay End-to-End Cryptography
- Relay Auth, Invites, Store & Admin API
- Relay Client, Pairing & Terminal Tunneling
- Relay Trust UI
Integrations & Secondary Surfaces
- Telegram Bot, Commands & Pairing
- A2A Peers: Protocol, Client & Server
- Node Link Engine, Registry & Scheduler
- Cloud Spaces, Snapshots & Sync
Settings, Updates & Maintenance