Skip to content

About

Browser pool for agents. Caller identity is verified by the kernel; a browser is reclaimed as soon as its process exits. Memory-based admission, one network policy. | agent 专用浏览器池。调用方身份由内核校验, 进程退出即自动回收。按内存准入, 统一出网策略

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

-d-browser

Headless Chromium for programs and for AI agents. Each caller gets its own browser, admitted by memory, leased per session, and closed as soon as the caller that owns it exits.

Two uses share one pool:

  • Programs render a page to HTML (pbrowse render) or run a short Python script against a browser (pbrowse script).
  • Agents open interactive sessions and drive them step by step (open, click, fill, read text, take screenshots), through the pbrowse command line or the pbrowse-mcp MCP server.

An optional agent Chrome runs a headful Chrome on a host you choose. It is reached over a private network (Tailscale) through a gate, and it has a viewer for a person to watch and operate. Read the Security section before enabling it.

Requirements

  • Linux or macOS.
  • Python 3.10 or newer (the repository pins 3.12 in .python-version) and uv.
  • Chromium, installed through Playwright (see Install).
  • Optional: Tailscale for tailnet addresses. Without it the gate admits only loopback.
  • Optional, agent Chrome viewer on Linux: an X server (Xvnc) and a Chrome build.

Install

git clone <this repository> d-browser
cd d-browser
uv sync --frozen
uv run playwright install chromium
uv run pbrowse status

uv sync --frozen installs exactly the versions in uv.lock.

Quick start

Render a public page as a program would:

uv run pbrowse render https://example.com --net public

The reply is one JSON object with ok, url, status, title, html and host.

Open an interactive session and read it:

sid=$(uv run pbrowse open https://example.com --target pool --net public)
uv run pbrowse text "$sid"
uv run pbrowse close "$sid"

MCP server

pbrowse-mcp speaks MCP over stdio. Register it with your MCP client, for example:

{
  "mcpServers": {
    "d-browser": {
      "command": "uv",
      "args": ["--directory", "/path/to/d-browser", "run", "pbrowse-mcp"]
    }
  }
}

The server exposes these tools: browser_open, browser_goto, browser_click, browser_fill, browser_paste, browser_press, browser_select, browser_upload, browser_text, browser_shot, browser_wait, browser_wait_human, browser_sessions, browser_status, browser_close, browser_cdp_url, browser_view_url, browser_login, and browser_login_doctor. The login_* tools appear only on a host configured as the login host (see Configuration).

Command line

Group Commands
Sessions open, goto, text, fill, paste, press, select, upload, shot, wait-human, sessions, close
Programs render URL --net public|local, script (reads {"script", "argv"} on stdin)
Inspection status, limits (every setting with its source), targets, policy, owner, audit
Agent Chrome serve, login, login-doctor, agent-chrome, exec-display, exec-chrome, exec-chrome-mac, ensure-password, set-password, set-login-token
Extensions ext add, ext status (Linux agent Chrome)

pbrowse --help and pbrowse <command> --help give every option. Commands print JSON on stdout and use the exit code to report failure.

Rules the pool enforces

  • Owner. Every request names its owner process, and the pool verifies it: the caller itself or one of its ancestors. The owner is found from PBROWSE_OWNER_PID when set, then the nearest agent process, the nearest sshd session for forwarded calls, the MCP server process, or, for pbrowse script, the program that ran it. Each owner gets its own Chromium, and only that owner can use its sessions. When the owner exits, its Chromium closes at once. Exit is observed through pidfd (Linux) or kqueue (macOS); nothing polls.
  • Capacity. A new Chromium starts only when the memory of all pool Chromiums, plus one estimated Chromium, stays under the memory limit. Chromiums of the same user are counted, including orphans of a dead pool. Memory is PSS on Linux and RSS elsewhere.
  • Leases. A script's tab closes when the job ends. A short session closes after short_idle_s without a step; each step renews it. A session opened with --hold debug closes debug_max_s after opening. An owner's Chromium closes chrome_idle_s after its last tab. A lease never closes a tab in the middle of a step.
  • Network. Every connection a page makes goes through the pool's policy proxy: http, https, ws, wss, redirect hops, and requests from data: pages. The proxy resolves each host once, refuses the connection unless every resolved address is allowed, and connects to the address it checked, so DNS rebinding cannot change the answer. local (the default) allows loopback and the tailnet ranges (100.64.0.0/10, fd7a:115c:a1e0::/48). public allows only public addresses, and only ports 80, 443, 8080 and 8443. WebRTC is limited to proxied connections and QUIC is off.
  • Gate access. Each gate listens on 127.0.0.1 and requires a random token, which Chromium receives as the proxy credentials. A request without it gets 407.
  • Egress. A render or script job may name an upstream HTTP proxy. The address goes on stdin, never in argv. The egress must be loopback or public; a LAN or tailnet egress needs egress_private=1. Link-local, multicast, reserved and unspecified addresses are always refused.
  • Access. The pool listens on a 0600 Unix socket and accepts only peers with the same user ID. The pool recomputes each script's SHA-256 and refuses a mismatch.
  • Screenshots and uploads. Screenshots are written only to ~/.local/state/d-browser/shots/, as new 0600 files. Uploads are staged in a per-session 0700 folder, and symlinks inside a --dir tree are refused.
  • Frozen pool. A pool that accepts a connection but sends no acknowledgement within 5 s is killed, with its marked Chromium groups. Each kill is recorded in ~/.cache/d-browser/kills.jsonl.

Configuration

Every setting is read on each request, in this order: environment variable, then ~/.config/d-browser/limits.json, then the default. pbrowse limits shows the value and its source. examples/limits.json lists the keys.

key env default meaning
memory_mb PBROWSE_MEMORY_MB 8192 memory limit for all pool Chromiums
chrome_mb PBROWSE_CHROME_MB 400 estimate for one more Chromium
chrome_idle_s PBROWSE_CHROME_IDLE_S 30 how long an owner's Chromium lives after its last tab
short_idle_s PBROWSE_SHORT_IDLE_S 120 idle time of a short session lease
debug_max_s PBROWSE_DEBUG_MAX_S 7200 cap of a --hold debug lease
egress_private PBROWSE_EGRESS_PRIVATE 0 1 allows a LAN or tailnet egress address
agent_queue_max PBROWSE_AGENT_QUEUE_MAX 32 steps and opens waiting on the agent Chrome (1-256)
human_idle_s PBROWSE_HUMAN_IDLE_S 30 seconds without a person's input before the agent Chrome is observing again (1-3600)
upload_mb PBROWSE_UPLOAD_MB 50 largest single upload, in MB (1-512)

Invalid values are skipped and named in pbrowse status.

Host files

These optional files describe one host. Templates are in examples/.

  • ~/.config/d-browser/pool.json: how this host runs its agent Chrome. agent_chrome is "agent" (the default: headless or agent-controlled) or "login" (a person's own Chrome for signing in; the gate never attaches). display names the X display for visible sessions, geometry the window size, and slice a systemd slice that contains the browser processes.
  • ~/.config/d-browser/inventory.json: which hosts are known and where the agent Chrome runs. It gives this host's self name, the hosts with their tailnet names and optional LAN addresses, deny (hosts that may not connect), agent_chrome (the host, the viewer address and the CDP address), and targets (more agent Chromes by name). Two targets on one host are refused at load. The file is plain JSON that you write for your own network; the code does not infer it.

Security

Read this before enabling the agent Chrome. The design favours containment on a host you control, and it has these exposures:

  • Admission is by network identity only. On the agent Chrome gate, a tailnet peer is admitted when Tailscale names it as a registered host that is not on deny. There is no per-user credential and no token on the CDP or viewer endpoints. A LAN address is never admitted.
  • An admitted peer gets full control of a logged-in profile. The /cdp endpoint gives the full DevTools protocol for the agent Chrome: cookies, storage and every tab. A compromised registered host therefore reaches every account signed in to that Chrome. Keep the set of trusted hosts small, and use deny for hosts that should not connect.
  • Local file upload. pbrowse upload and browser_upload read any file the calling process can read and set it on a page's file input. A prompt-injected agent can be induced to upload local secrets to a hostile page. Restrict the agent's file access, or do not give it upload.
  • Script execution. pbrowse script runs caller-supplied Python inside the pool. The boundary is the 0600 socket and the same-user check, so this is not a privilege escalation, but any same-user code runs with the pool's rights.
  • Login targets are off by default. A login Chrome is used only when pool.json sets agent_chrome to "login", and the login_* tools are offered only on that host. Enable it only on a machine where a person signs in.
  • Local DevTools port. Chromium's DevTools port listens on 127.0.0.1, so any local user on a multi-user host can reach it. Use a single-user host for the agent Chrome.
  • Prompt injection. Page text, titles and HTML are returned to the agent as model input.
  • Transport. The viewer and CDP endpoints are plain HTTP and WebSocket. Confidentiality relies on the WireGuard encryption of the tailnet. Do not expose these ports to other networks.
  • Credentials at rest. The login-extension token is stored 0600 and refused if readable by others. The profile directory is 0700. Egress credentials are read from stdin and held in memory only.
  • Viewer password. The VNC viewer password (DES, 8 characters) protects the viewer between the gate and the X server, on a Unix socket. It is legacy-weak and is never offered on TCP.
  • Force-installed extension. The agent Chrome managed policy (agentchrome.py, the TAMPERMONKEY constant) force-installs the Tampermonkey userscript manager, Chrome Web Store ID dhdgffkkebhmkfjojejmpbldmpobfkfo. The extension is optional in the sense that it can be removed from the policy, but the ID is not read from a configuration file: changing or removing it means editing that constant, and the policy file changes with it. The code closes the welcome tab that the extension opens at start.

To report a security issue, use the repository's Report a vulnerability button (GitHub security advisories) in the Security tab. Do not open a public issue for it.

Limitations

  • The agent Chrome is for one trusted user on one or more trusted hosts. It is not a multi-tenant service.
  • Playwright controls Chromium; behaviour follows the Chromium version it installs.
  • The gate admits by Tailscale identity. Without Tailscale only loopback is reachable.
  • Login flows against real sites are a person's task: the tools report a slider, captcha or verification step and never bypass it.

Tests

The tests use a fake browser, real Unix sockets and real exit notification. Run them with:

uv run --python 3.12 --frozen python -W error::SyntaxWarning -m unittest discover -s tests

Related projects

These four projects were built together and can be used together. Each one also runs on its own.

Project What it does How it connects
-agents-msger Messages between agent sessions on different machines. Sender identity is detected from the process tree. Its hub lists which sessions are alive on each host (/who, /status).
-d-browser A browser pool for agents. One Chromium per caller, verified by the kernel, closed when the caller exits. Provides the pbrowse command. For a call forwarded from another machine, it asks the agents-msger hub whether that caller is still alive and closes its Chromium when it is gone.
-wsearch Search service. The calling model writes the query in several languages; fixed code runs the searches and merges the results. Its browser features (page rendering, scripts) use d-browser's pbrowse.
-verdict Decision recall. Before an agent decides, a hook gives it similar earlier decisions and the general design rules. Recall is done by code (BM25). No dependency on the others.

Using them together, in this order:

  1. agents-msger: run one hub, and one shim on every machine (see the agents-msger README, Install). Its installer writes ~/.config/mcp-msgers/config.json, which holds the hub address and the token file.
  2. d-browser: install it on the machines that run browsers. It reads that same agents-msger config to find the hub; nothing else needs to be set. Its pbrowse command is installed with it.
  3. wsearch: install it, and point PBROWSE_BIN at d-browser's pbrowse (or put pbrowse on PATH).
  4. verdict: install the hook in Claude Code (see the verdict README, Hook setup). It needs none of the others.

The agents-msger link is optional. d-browser reads the hub address and token from the agents-msger config (~/.config/mcp-msgers/config.json, or the file named by MCP_MSGERS_CONFIG). Without it, a forwarded caller's Chromium closes when that caller's connection closes.

License

MIT. See LICENSE. Bundled third-party parts (noVNC, pako, the Playwright Extension CRX) keep their own licenses; see NOTICE.

About

Browser pool for agents. Caller identity is verified by the kernel; a browser is reclaimed as soon as its process exits. Memory-based admission, one network policy. | agent 专用浏览器池。调用方身份由内核校验, 进程退出即自动回收。按内存准入, 统一出网策略

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages