Skip to content

Architecture: Browy native messaging host (backend)

The Browy backend is a small Node.js process registered with Chrome / Edge / Brave as a native messaging host under the application id com.browy.host. It runs on your machine.

┌───────────────────┐ native msging ┌───────────────────┐ child process ┌───────────────────┐
│ Browser ext │ ◄───────────► │ Browy host │ ◄───────────► │ GitHub Copilot │
│ (sidepanel, │ port │ (Node, this │ │ CLI / SDK │
│ devtools panel) │ │ doc) │ │ (LLM bridge) │
└───────────────────┘ └───────────────────┘ └───────────────────┘
│
▼
┌───────────────────┐
│ CDP via │
│ chrome.debugger │
│ (executed back in │
│ the extension) │
└───────────────────┘

The extension speaks to the host over a single bidirectional length-prefixed JSON channel (Chrome’s native messaging protocol). The agent’s tools run in the host. When a tool needs the page, the host sends the extension a cdp.send message (or tab.new, tab.activate, tab.list, tab.getActive); the extension’s service worker runs it through chrome.debugger or chrome.tabs and posts a tool.result back.

`src/native-host.ts`

Process entry. Chrome starts one for each connectNative port. It opens the log file, starts the Copilot SDK and attaches one Runner to the stdio transport. It also holds the Copilot sign-in check and the helper that opens a terminal to sign in.

`src/agent/loop.ts`

The agent loop. Calls into the Copilot SDK (createSession / resumeSession), feeds it the prompt + tool inventory, streams assistant text and tool-call events back to the extension through the Runner, and routes tool results back into the SDK.

`src/agent/tools/browser.ts`

The tool registry: every Browy tool the LLM can call (inspect_page, click_index, type_index, get_network_requests, …). Each one is a register({ def, run }) call: a JSON-schema definition and a run(args, ctx) function that does the work in the host, driving the tab through ctx.cdp.

`src/agent/runner.ts`

Session routing. One Runner per host process keeps a session for each connected panel, keyed by the panel’s session id, and passes messages between the panels and the one Agent. It runs one chat turn at a time across all panels; a message sent from another panel meanwhile waits in line and starts as soon as the current turn ends. Each session’s pending CDP calls live in its ExtensionContext (src/agent/extension-context.ts).

`src/transports/`

Stdio framing (native-messaging.ts): a 4-byte little-endian length, then UTF-8 JSON. The Runner only sees the Transport interface from src/protocol.ts, where the host’s message types are defined.

`src/cli-bin.ts`

Standalone entry: the browy command (dist/cli-bin.js). The installers run its install-host subcommand to register the native host with Chrome, Edge and Brave, and uninstall-host removes it. It also has run, a terminal agent that needs no browser, plus repl and chat.

  1. User types in the side panel and hits send.
  2. The side panel posts {type: 'chat.send', sessionId, text} to the service worker, which forwards it over the native port.
  3. Host’s Runner binds the agent to that session id and calls agent.chat(text). If no Copilot session is open for the id, the agent calls the SDK’s resumeSession(id, ...) (or createSession({sessionId: id, ...}) the first time) with the filtered tool list, then sends your text with the current tab’s context and a fresh page snapshot attached.
  4. The SDK streams events back. Host forwards text chunks as {type: 'chat.delta'}. When the SDK calls a tool, the host posts {type: 'chat.tool_call'} to the panel, runs the tool, then posts {type: 'chat.tool_result'}.
  5. Each CDP command the tool needs goes to the extension as {type: 'cdp.send', sessionId, callId, method, params} (inspect_page, for example, is one Runtime.evaluate). The service worker runs it with chrome.debugger.sendCommand and posts {type: 'tool.result', sessionId, callId, ok, result}.
  6. Host hands the tool’s result back to the SDK; the loop continues until the SDK reports session.idle.
  7. Host posts {type: 'chat.done', sessionId, text}.

The whole exchange is synchronous from the model’s perspective; from the user’s perspective it streams in real time.

Browy delegates session storage to the GitHub Copilot CLI’s session machinery (in ~/.copilot/). The host uses the id the extension mints for a chat (sp-… in the side panel, dt-… in the DevTools panel) as the Copilot session id. The extension keeps the current one in chrome.storage.local, so reopening a panel resumes the same Copilot session.

The agent also has its own scratch surfaces:

  • ~/.browy/data/files/: sandboxed file scratch the save_file / read_file agent tools operate against
  • ~/.browy/data/notes.json: persistent key-value memory across chats
  • ~/.browseragent/native-host.log: single rotating 5 MB log file

Tool definitions live in src/agent/tools/browser.ts. Each one declares:

  • def: name, description, JSON-schema parameters (consumed by the SDK)
  • run(args, ctx): runs in the host. Browser tools call ctx.cdp.send(method, params); src/agent/extension-context.ts sends each call to the extension as a cdp.send message and awaits the matching tool.result reply

This means the tool logic runs in the host, and only the CDP commands run in the extension, where the page lives. Tools like save_file, note_set and run_script run entirely in the host. The host is the orchestrator and the bridge to the LLM.

src/agent/loop.ts wraps each tool with the SDK’s defineTool and builds the tool list when it creates or resumes a Copilot session. toolPolicy() in src/agent/tool-policy.ts filters it against the user’s Settings → Tools toggles, which the extension sends with every session.start. A tool you switch off is dropped from the definitions and from the SDK’s availableTools allowlist, and the opt-in host tools stay out until you switch them on.

src/native-host.ts rebinds console.* so it writes to ~/.browseragent/native-host.log as well as stderr. When the host starts and the file is over ~5 MB, it is renamed to native-host.log.1 and a fresh one begins. Only one old copy is kept; the next rotation overwrites it.

This is the first place to look when a session misbehaves. It contains the host’s diagnostic output, not transcripts.