Skip to main content

Identity-aware runtime for parallel coding agents -- persistent browser pool + lease broker over CDP

Project description

escarp

Identity-aware runtime for parallel coding agents. A single broker daemon owns a pool of persistent Chrome for Testing windows and hands CDP endpoint leases to coding agents (Claude Code, Codex) over MCP. N worktrees can run N agents with N isolated browsers, no stale-lock hell.

Why? Spawn-a-browser-per-tool-call leaks chromes on every chat exit. Per-agent lockfiles strand themselves when the agent dies. Driving the user's daily-driver browser pollutes cookies and session state. Escarp separates lifecycle (persistent, owned by escarp) from leases (ephemeral, owned by the agent). The browsers always exist; agents check them out.

Install

pip install escarp

Requirements: Python 3.11+, a Chrome for Testing binary on disk. Easiest way to get one:

npx @puppeteer/browsers install chrome@stable
export ESCARP_CFT_BINARY=".../Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"

Quick start

# 1. Spawn N persistent Chrome for Testing windows (one-shot, exits immediately).
#    The chromes are detached and survive escarp restarts.
escarp launch-pool                  # default 4
ESCARP_POOL_SIZE=8 escarp launch-pool

# 2. Start the broker daemon. It discovers the chromes via /json/version,
#    serves the lease HTTP API on 127.0.0.1:7878, and runs a reaper for
#    expired leases. ^C releases the slot locks but leaves chromes alive.
escarp daemon &

# 3. Inspect the pool
curl -s http://127.0.0.1:7878/status | jq

Drive one of the leased browsers in Python:

import asyncio, httpx
from playwright.async_api import async_playwright

async def main():
    lease = httpx.post(
        "http://127.0.0.1:7878/acquire",
        json={"holder": "demo-script"},
    ).json()
    print("got slot", lease["slot"], "->", lease["cdp_ws_url"])

    async with async_playwright() as pw:
        browser = await pw.chromium.connect_over_cdp(lease["cdp_ws_url"])
        page = browser.contexts[0].pages[0]
        await page.goto("https://example.com")
        await page.screenshot(path="/tmp/example.png")
        await browser.close()  # disconnect; broker-owned chrome stays alive

    httpx.post(
        "http://127.0.0.1:7878/release",
        json={"lease_token": lease["lease_token"]},
    )  # broker auto-resets the tab to about:blank

asyncio.run(main())

Wire it into Claude Code / Codex via MCP

Register the bundled MCP shim so the model gets three first-class tools (escarp_status, escarp_acquire, escarp_release) and never has to curl the broker by hand:

# Claude Code
claude mcp add escarp -- escarp-mcp

# Codex CLI
codex mcp add escarp escarp-mcp

Auto-heartbeat lives in the shim, so a long-running session can't lose the lease mid-task. On disconnect the lease releases and the slot returns to the pool, reset to about:blank.

HTTP API (three verbs)

Verb Body Returns
GET /status -- Pool snapshot, no lease tokens leaked
POST /acquire {"holder": str, "slot"?: int, "dev_port"?: int} {slot, cdp_ws_url, lease_token, expires_at, ...}
POST /heartbeat {"lease_token": str} Refreshed lease record
POST /release {"lease_token": str} Lease record in state: free
GET /reaped -- Last 50 TTL-expired reclamations (debug)

Architecture (one paragraph)

Control plane (escarp): slot allocator with kernel-flock atomicity, lease broker with TTL + reaper, HTTP API on 7878, MCP shim. Data plane (your agent's tools): Playwright connect_over_cdp, @playwright/mcp --cdp-endpoint, chrome-devtools-mcp --browser-url, or whatever CDP client you want, attached directly to the leased cdp_ws_url. Escarp provisions and points; it never proxies your clicks. If escarp ever shows up in your per-click latency, that's a bug.

The persistence contract is the load-bearing trick: chromes are launched detached (start_new_session=True) and reparent to launchd/init. The daemon discovers them by GET /json/version; it never owns their lifecycle. Kill the daemon, chromes stay up. Kill an agent mid-task, the reaper reclaims its lease within one sweep interval (default 2 s). On every release boundary the broker PUT /json/new?about:blanks a fresh tab and closes the old ones, so no state inherits across holders.

See V2_PLAN.md for the full design and decision record.

Demos

The repo ships scripts that prove the headline claims end-to-end:

# Two holders, two browsers, asyncio.gather'd lockstep concurrency.
# Steps fire within ~70 ms across both browsers; ~2x parallel speedup.
uv run python scripts/demo_two_holders_concurrent.py

# Lease-boundary reset: drive to YouTube, release, watch the tab snap back
# to about:blank. Proof that state does not leak across holders.
uv run python scripts/demo_reset_on_release.py

Configuration

Env var Default What
ESCARP_POOL_SIZE 4 Number of browser slots
ESCARP_CDP_BASE 9222 cdp port for slot 0; slot N uses base+N
ESCARP_API_PORT 7878 Broker HTTP API port (bind-and-shift on collision)
ESCARP_LEASE_TTL_S 60 Lease expiry; reaper reclaims past this
ESCARP_CFT_BINARY autodetect Path to Chrome for Testing binary
ESCARP_BROKER_URL http://127.0.0.1:7878 Where the MCP shim looks for the broker

Per-slot resource derivation

Each slot derives all its resources from a single index, so two worktrees on different slots never collide on ports:

slot s  ->  frontend  = 3000 + s*10
            backend   = 8000 + s*10
            postgres  = 5432 + s*10
            cdp_port  = 9222 + s
            user_data = ~/.escarp/profiles/<tier>/slot-<s>

Status

v1.0.0. The four headline claims hold:

  • Two agents on different slots drive their own leased CfTs, never collide.
  • Killing an agent mid-task returns its browser within one reaper interval.
  • Pool exhaustion returns a structured 409, not a hang.
  • No lockfiles outside the broker's single source of truth.

What's not in 1.0: delegated and supervised identity tiers (v1.1 and v1.2), cross-machine pooling (v2), Docker compose orchestration (hooks only for now -- escarp doesn't own compose semantics).

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

escarp-1.0.0.tar.gz (29.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

escarp-1.0.0-py3-none-any.whl (28.7 kB view details)

Uploaded Python 3

File details

Details for the file escarp-1.0.0.tar.gz.

File metadata

  • Download URL: escarp-1.0.0.tar.gz
  • Upload date:
  • Size: 29.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for escarp-1.0.0.tar.gz
Algorithm Hash digest
SHA256 25fba100b796d34c4d9eb812540f3855299456b5e8be908af57c4846f99880cc
MD5 82abfb866def937b2ba07e7551afcca9
BLAKE2b-256 3cbffac5ea9a7c084a065e84cf230ff55a1318e4c935587a5640dbb523272804

See more details on using hashes here.

Provenance

The following attestation bundles were made for escarp-1.0.0.tar.gz:

Publisher: publish.yml on ddavidgao/escarp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file escarp-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: escarp-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 28.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for escarp-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2b88bc2973617d593836cb56a4659ba58966ce52e2eb7844a2019ee382cf8447
MD5 efff34cc3878f325502b6a375ca752d3
BLAKE2b-256 b84e71f8f2c84dae219827d19785561f36fd0c1df6ea54fcd754baec551c8ae0

See more details on using hashes here.

Provenance

The following attestation bundles were made for escarp-1.0.0-py3-none-any.whl:

Publisher: publish.yml on ddavidgao/escarp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page