Skip to main content

browser-proxy

browser-proxy is a local-first, profile-aware Microsoft Edge-only automation CLI. It replaces per-agent browser MCP processes with one on-demand daemon, systemd-templated Edge instances (one per profile), and a KπX-owned Edge extension bridge.

Contract

The public interface is deliberately identical in philosophy to tick-proxy:

browser-proxy do <flat-domain-first-action> '<one-json-object>' [-o FILE] [-f json|table]
browser-proxy admin <command>

The positional payload is always exactly one JSON object or a path to a file containing exactly one JSON object. Options only control presentation or output location; they never carry business data. This applies to every action, including raw.

browser-proxy do profile-list '{}'
browser-proxy do profile-remove '{"profile":"test"}'
browser-proxy do window-create '{"profile":"default","url":"https://example.com"}'
browser-proxy do window-create '{"profile":"default","incognito":true,"layout":[{"type":"tab","url":"https://example.com"}]}'
browser-proxy do window-create '{"profile":"default","url":"https://a.example","items":[
  {"type":"tab","url":"https://b.example"},
  {"type":"group","title":"Research","tabs":["https://c.example","https://d.example"]},
  {"type":"tab","url":"https://e.example"}
]}'
browser-proxy do raw '{"method":"Target.getTargets","params":{}}'
browser-proxy do group-list '{"profile":"default"}'
browser-proxy do tab-move '{"profile":"default","tab_id":12,"after_tab_id":34}'
browser-proxy do group-add-tabs '{"profile":"default","group_id":7,"tab_ids":[12,13]}'
browser-proxy do group-remove-tabs '{"profile":"default","tab_ids":[12]}'
browser-proxy do tab-create '{"profile":"default","url":"https://example.com","incognito":true}'  # incognito creates in a fresh InPrivate context
browser-proxy do window-sync '{"profile":"default","window_id":143985332,"layout":[
  {"type":"tab","tab_id":1},
  {"type":"group","title":"Research","tab_ids":[2,3]},
  {"type":"tab","tab_id":4}
]}'
browser-proxy do page-navigate '{"profile":"default","target_id":"T1","url":"https://example.com"}'
browser-proxy do page-click '{"profile":"default","target_id":"T1","selector":"#submit"}'
browser-proxy do page-evaluate '{"profile":"default","target_id":"T1","expression":"document.title"}'
browser-proxy do cookie-list '{"profile":"default"}'

Each successful command writes this envelope to stdout:

{"meta":{"status":"ok","comment":"","edited":false},"data":{}}

Architecture

CLI → Unix socket → browser-proxyd → direct CDP → Edge profile processes
                                  └→ authenticated loopback WS → browser-proxy-ext

The disk directory $HOME/.local/share/browser-proxy/profiles/<profile> is the persistent identity of a profile; it is never a daemon cache. A "browser-proxy profile" is a whole isolated --user-data-dir (one systemd unit, one CDP port) — a different level than Edge's own internal people-profile concept (Default, Profile 1, … at chrome://settings/people, living inside one --user-data-dir); never confuse the two. materialize_edge_profile() only ever mkdirs the directory — it declares it, it does not make Edge treat it as real. paths.edge_profile_state() is the one predicate (not_declared/declared/initialized, keyed on Edge's own Local State marker file) used identically by profile-list, admin profile status, admin status, and direct-CDP action resolution — see CONTRACT.md → Profile lifecycle management. profile_state.py is the single canonical module computing disk state + systemd activation + real CDP reachability (no longer hand-duplicated between daemon.py and cli.py); materialize_edge_profile() refuses names that collide case-insensitively with a different already-declared profile. The daemon owns transport, policy, and target/window control while systemd owns Edge process execution. profile-list discovers disk profiles and reports their state plus systemd/CDP/extension status. profile-remove stops the unit if active then moves the directory to the KpihX trash (trash-put, resolved by absolute path) — never a permanent delete. The extension provides approval overlays, secret-safe user input, Edge tab-group operations, and difficult-widget fallback. Edge Workspaces are modeled as semantic containers inside profiles because Edge provides no documented Workspace API to CDP or extensions.

Extension bridge identity is per-profile too (fixed after a real bug: 3 profiles returned the byte-identical bookmark tree because only one global connection slot existed). Each profile is a separate extension install; the Options page declares which profile it belongs to, the daemon keys connections by that name, and every extension-mediated request (bookmark-*, group-*, browser-ask-user, approval overlays, …) is routed exclusively to the matching profile's own connection — a request for a profile with no matching connection fails closed by name (EXTENSION_UNAVAILABLE: <profile>), never silently answered by a different profile. See CONTRACT.md → Extension bridge identity.

Every HITL prompt is 100% transparent and redirects to itself (KπX directive). The overlay shows the REAL non-secret proposal details (real tab_ids, title, color, url, …), never just a bare action name — only genuinely secret-shaped fields (a cookie's real value, a dropped file's raw bytes, any password) are shown as <redacted> instead. The extension always brings the hosting tab AND its window to the front first — never a prompt you have to discover by accident. The same centralized tab resolution backs every HITL kind (approval, ask, dismiss-overlays, captcha, set date/combobox, drop-file), including automatic retry via a fresh temporary tab if the found one's content script turns out stale (e.g. right after the extension itself reloads) — that temporary tab is always closed again once the interaction settles, never left behind. do window-sync's layout reorganizes a whole window's tab/group layout — create, rename, recolor, add-to, remove-from, reposition — in ONE call. See CONTRACT.md → HITL transparency and redirection.

The registry covers the full Edge profile hierarchy: profiles, heuristic Workspaces, windows, tab groups, tabs, pages, and profile bookmarks. workspace-list and group-list clearly label heuristic/non-authoritative data where Edge lacks a public Workspace API. The implementation is strictly Edge-only; it does not launch, target, or publish for Chrome.

Tab/group structure is one canonical computation, not three independent views. CDP has no concept of tab groups or real tab order at all — only chrome.tabs.query/chrome.tabGroups.query (extension-side) can answer "what group is this tab in" or "what is the real left-to-right order." computeWindowLayouts() computes that ONE truth once; window-list's chrome_layout field, group-list, and the movement actions (tab-move, group-add-tabs, group-remove-tabs) all read or mutate the exact same state. See CONTRACT.md → Canonical tab/group structure for the full tabs/groups/order shape, the CDP-target_id-vs-chrome_tab_id bridging strategy, and why the 3 new movement actions are deliberately approval-free like window-create/tab-create.

🔴 InPrivate tab visibility: computeWindowLayouts() uses chrome.tabs.query({}) to list all tabs. InPrivate tabs are only visible if the extension has "Allow in incognito" enabled in edge://extensions/ AND the manifest declares "incognito": "spanning". Without these, chrome_layout is None for InPrivate windows and group/tab-repositioning operations fail.

🔴 InPrivate tab grouping limitation (Chromium platform): chrome.tabs.group() cannot resolve tabs inside InPrivate windows — throws "No tab with id" for valid tab IDs. Root cause: Chromium's GetTabById() does not search incognito Browser instances despite include_incognito_information() returning true. This is a Chromium/Edge API limitation, not a code bug. Edge's native UI drag & drop uses an internal code path not exposed via the extension API. Working InPrivate actions: tab-create, tab-close, tab-activate, page-navigate, window-close. Broken: group-create, group-add-tabs, group-remove-tabs, tab-move, window-sync layout grouping.

raw sends a browser-level CDP method and its parameters inside that same object. Conservative read-only methods (Browser.getVersion, Target.getTargets, and related inspection calls) run without approval. Every other raw method, including mutations, is blocked behind fail-closed extension approval; a payload flag can never bypass it.

Lifecycle

The daemon (browser-proxy.service, started via admin service start, or on demand by the client's own fallback) owns an exclusive lock and uses a Unix-domain socket. It has deliberately NO automatic timeout — no idle TTL, no maximum lifetime (KπX directive). It is purely launchable/stoppable on request: admin service start/admin service stop (which now sends the real shutdown RPC over the socket first, falling back to systemctl stop only if the socket is unreachable), or the OS itself. Every managed Edge window is already always visible, so KπX can directly see and close one an agent forgot — there is no case where an unattended timeout is the right way to reclaim a daemon.

Every Microsoft Edge instance is its own separate systemd-templated service, decoupled from the daemon's lifetime — never a raw subprocess.Popen, never a hand-typed microsoft-edge command. There is no headless mode and no flag to hide the window: every instance is always real and visible, by design (100% Transparency).

admin commands

# --- Service (daemon) ---
browser-proxy admin service install               # once per machine: link + enable the daemon unit
browser-proxy admin service start                  # start the daemon, verify with ping
browser-proxy admin service stop                   # graceful shutdown (RPC first, then systemctl)
browser-proxy admin service restart                # stop + start + verify
browser-proxy admin service logs                   # last 50 lines of daemon journal
browser-proxy admin service purge                  # stop + disable + unlink daemon service

# --- Profile (Edge instances) ---
browser-proxy admin profile install                # once per machine: link the profile unit template
browser-proxy admin profile start test             # real window opens
browser-proxy admin profile stop test              # stop one Edge instance
browser-proxy admin profile restart test           # restart one Edge instance
browser-proxy admin profile status test            # state / systemd_active / cdp_port / cdp_reachable / extension_connected
browser-proxy admin profile logs test              # last 50 lines of Edge profile journal
browser-proxy admin profile purge test             # stop + trash profile directory

# --- Extension ---
browser-proxy admin extension pair                 # store the pairing secret (hidden prompt)
browser-proxy admin extension unpair               # remove the stored pairing token
browser-proxy admin extension status               # token health: existence, permissions, masked preview

# --- Global ---
browser-proxy admin status                         # all services + files + symlinks + token + permissions
browser-proxy admin doctor                         # diagnose + fix missing dirs, symlinks, permissions
browser-proxy admin purge                          # full purge (all profiles, daemon, state) + uv tool hint

# --- Workflow ---
#   -> edge://extensions -> developer mode -> load unpacked -> browser-proxy-ext/
#   -> browser-proxy admin extension pair -> paste secret in the extension's options page
browser-proxy do profile-start '{"profile":"test"}'   # daemon starts the same unit if not already running
browser-proxy do profile-remove '{"profile":"test"}'  # stops the unit if active, trashes the directory (never a permanent delete)

The first start materializes $HOME/.local/share/browser-proxy/profiles/test/ before systemd is called. It therefore remains listed while stopped or after a daemon restart; profile-list itself is read-only and never creates a directory.

The loopback CDP port for a profile is deterministic (edge_cdp_port(), sha256-derived — see CONTRACT.md → Edge lifecycle), so the daemon, the CLI, and a manually-started instance all agree on it without any file-based or IPC handoff.

Development

make install-dev
make check
make smoke
make stress

Every default value (ports, TTLs, the terminal JSON preview threshold, directory names) and its overriding environment variable name lives in src/browser_proxy/config.py — never inline literals scattered across paths.py/daemon.py/bridge.py/cli.py.

Extension

browser-proxy-ext is an independent repository and Git submodule. Build it with its own Makefile; its compiled package is submitted only to Microsoft Edge Add-ons.

browser-proxy admin extension pair rotates a mode-0600 local capability without displaying it, stored under paths.persistent_state_dir() (survives reboot/logout — never the daemon's ephemeral runtime_dir()/tmpfs). admin extension status shows the token health (existence, permissions, masked preview). admin extension unpair removes the token file. The extension bridge only accepts an authenticated typed handshake and dispatches typed request/reply frames over loopback. The extension's own reconnect loop is backed by a chrome.alarms watchdog (not just setTimeout), so it recovers even after a Manifest V3 service-worker eviction.

One Options-page setup per profile, not just once per machine: each profile is a separate Edge install with its own extension storage — its Options page (browser-proxy-ext/options.html) needs both the shared secret AND a Browser-proxy profile field matching the exact profile name used with admin profile start/profile-start for that window. The shared secret can be the same value across every profile; the declared profile name must be unique per install, or requests will be routed to whichever install most recently declared that name.

Security

  • CDP endpoints bind to loopback only.
  • The CLI uses a per-user Unix socket.
  • The extension authenticates with a paired, short-lived capability.
  • Password values and secret-bearing storage are never returned to an agent.
  • raw has a conservative read-only CDP allowlist; all mutations require extension approval.

Metadata

Release files for browser-proxy 0.8.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for browser-proxy 0.8.1
File Size Uploaded
browser_proxy-0.8.1.tar.gz 94.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for browser-proxy 0.8.1
File Interpreter ABI Platform
browser_proxy-0.8.1-py3-none-any.whl Python 3 none any Details

Total release size: 197.1 kB

Release files / browser_proxy-0.8.1.tar.gz

Download URL browser_proxy-0.8.1.tar.gz
Size 94.2 kB
Tags Source
SHA-256 checksum
How to use checksums
ec41ae5dc4f93394debef8527e0be7c17ef00a76376f7f85472bf7bdbc87cdf7
BLAKE2b-256 checksum
How to use checksums
1fd287c1d8dba77bb2b5071fdbbabe6c8c85f50d9a460a3e91f5b3e04a069b64
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.10.3 {"installer":{"name":"uv","version":"0.10.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / browser_proxy-0.8.1-py3-none-any.whl

Download URL browser_proxy-0.8.1-py3-none-any.whl
Size 102.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
18d7673965ef9f0fdc1889c143c9aaab7f2ce1c45a73d21ec670fcd36c850014
BLAKE2b-256 checksum
How to use checksums
5610aa7cb3f27a634cdb49fa2b2693d7ea96adc1da1fe05ebd0539e03a90f705
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.10.3 {"installer":{"name":"uv","version":"0.10.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

This release

0.8.1 This release

2 release files

0.6.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page