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","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 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.

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.6.0

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.6.0
File Size Uploaded
browser_proxy-0.6.0.tar.gz 90.3 kB Details

Built distribution (wheel)

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

Total release size: 189.2 kB

Release files / browser_proxy-0.6.0.tar.gz

Download URL browser_proxy-0.6.0.tar.gz
Size 90.3 kB
Tags Source
SHA-256 checksum
How to use checksums
86b1823c325655d27a806daa128416400dfdbfe1c179a89791cebf8dcdf323b1
BLAKE2b-256 checksum
How to use checksums
0a125cd89490ddca6137112bec6068d10c40c499e97b3a0c925fa2d6dcae6100
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.6.0-py3-none-any.whl

Download URL browser_proxy-0.6.0-py3-none-any.whl
Size 99.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a7ed4711930e07bfe32fd49100df22a52112f9943afb4f4660344f4f7f7413c3
BLAKE2b-256 checksum
How to use checksums
d2c1cc5992fd852c84c8ff87941f22929aca0c5a9cfc6117cd443410ccf4cccd
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

0.8.1

2 release files

This release

0.6.0 This release

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