Skip to main content

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

Project description

escarp

Escarp leases persistent browser slots, not just CDP endpoints. A single broker daemon owns a pool of persistent Chrome for Testing slots, binds each slot to a stable identity, and hands that identity — along with its CDP transport URL — to coding agents over MCP.

The mental model:

  • CDP is the automation transport (deterministic, no visible cursor, concurrent-safe).
  • CUA is the visible interaction transport (real cursor, OS overlays, native dialogs).
  • The slot identity is the common anchor. CDP uses the leased DevTools URL. Native Codex CUA uses the leased per-slot app bundle identity. Escarp's job is to make the slot addressable and verifiable before an agent touches it.

Install

pip install escarp

Requirements: Python 3.11+ and a Chrome for Testing binary on disk. On macOS you also get OS-window identity binding for free (via PyObjC).

npx @puppeteer/browsers install chrome@stable

Quick start

escarp launch-pool --cua-apps  # spawn N detached CfT slots with per-slot app IDs
escarp daemon &             # discover them, broker leases, run reaper
escarp setup codex          # or: setup claude-code

Look at the pool:

escarp window 0
# slot 0
#   os_window_id:      (not calibrated)
#   owner_pid:         (unknown)
#   app:               Google Chrome for Testing
#   cua_app:           dev.escarp.chrome.slot0
#   bounds:            (unknown)
#   cdp_port:          9222
#   cdp_window_id:     (unknown)
#   cdp_target_id:     (unknown)
#   verified_alive:    false
#   note:              CUA app identity mode; OS-window calibration skipped

Every field after verified_* is queried live — not from cached state.

Lease + drive a window:

escarp acquire --holder me --focus --prompt
# ... drive via Codex Desktop / Playwright / chrome-devtools-mcp ...
escarp release --mine

Commands

Command Purpose
escarp launch-pool [--cua-apps] Spawn N detached Chrome for Testing slots. One-shot; chromes outlive this command. Idempotent (skips slots already listening). --cua-apps creates per-slot macOS app bundle identities so native Codex CUA can target slots as separate apps.
escarp daemon Discover live chromes, broker leases on 127.0.0.1:7878, run the reaper. In CUA app mode it records the per-slot bundle identity and avoids resizing windows; otherwise it calibrates each slot to an OS-window identity. Does NOT own chrome lifecycles.
escarp window <slot> Print a slot's identity. In CUA app mode this is the per-slot app bundle ID. In OS-window mode it also returns os_window_id, owner_pid, bounds, and live verification fields. Supports --json and --verify-key for OS-window mode.
escarp acquire --holder X [--focus] [--prompt] Lease a slot. Persists token to ~/.escarp/leases.json.
escarp release {--mine | --slot N | --holder NAME | --token T} Token-free release for humans.
escarp focus <slot> Best-effort helper that uses the OS-window identity to bring a slot's window forward. CDP Page.bringToFront + osascript activate + AX raise by geometric match, with post-focus verification against os_window_id. Reports success only when the identity check confirms the right window is key.
escarp setup codex Idempotent: preflight (CfT, daemon, pool, MCP path, codex CLI), register escarp-mcp, smoke test.
escarp setup claude-code Same shape, for Claude Code.

The MCP shim

Register the bundled MCP server:

escarp setup codex            # or: escarp setup claude-code

The model gets three tools that return structured identity, not text:

// escarp_acquire returns:
{
  "slot": 1,
  "os_window_id": null,
  "owner_pid": null,
  "bounds": null,
  "cua_app_bundle_id": "dev.escarp.chrome.slot1",
  "cua_app_name": "Escarp Chrome Slot 1",
  "cua_app_path": "/Users/me/.escarp/cua-apps/Escarp Chrome Slot 1.app",
  "cdp_port": 9223,
  "cdp_window_id": null,
  "cdp_target_id": null,
  "cdp_ws_url": "ws://127.0.0.1:9223/devtools/browser/...",
  "dev_port": null,
  "expires_at": 1780174370,
  "auto_heartbeat_interval_s": 60.0
}

escarp_status returns the same shape per slot. Auto-heartbeat keeps the lease alive while the shim is running.

When the pool is full, agents do not need to infer whether another holder is dead. /status includes last_heartbeat, expires_at, suspected_stale, available_after_s, and retry_after_s per slot. The broker reaper remains the only authority that frees expired leases; agents should wait for retry_after_s, retry acquire, or surface pool exhaustion.

CDP vs CUA — pick the right transport

Both transports act on the same persistent slot. Pick by what you need:

Property CUA (Codex Desktop) CDP (Playwright / chrome-devtools-mcp)
Visible OS cursor ✅ moves on screen ❌ no cursor movement
Native OS overlays (file pickers, downloads, password manager, permission sheets) ✅ fully supported ❌ DOM only
Determinism screenshot + AX tree per turn deterministic CDP commands
Concurrent multi-slot ✅ on macOS with escarp launch-pool --cua-apps ✅ true parallel agents
Slot targeting by leased per-slot app bundle ID by leased cdp_ws_url
Best for end-user-facing tasks, demos, anything with native UI dev automation, parallel test runs, headless

Why per-slot app bundles: Codex CUA's addressing model is per-app — it operates on the key window of an app per turn. Two CUA sessions against two windows from the same CfT bundle both resolve to "the CfT app → its frontmost window." escarp launch-pool --cua-apps fixes that by cloning lightweight per-slot app bundles such as dev.escarp.chrome.slot0 and dev.escarp.chrome.slot1, each with its own profile and CDP port. On APFS the bundle clone is copy-on-write, so disk overhead is mostly metadata until files diverge.

Escarp's job for native CUA work is therefore: lease the slot, expose its bundle ID, and keep the lease alive while the agent is using it. The prompt from escarp acquire --prompt tells Codex CUA to target that bundle ID directly.

For Claude Code today, Escarp validates cleanly through MCP/CDP (escarp setup claude-code and the leased cdp_ws_url). If a Claude Code build exposes native app-targeted Computer Use, it should use the same per-slot bundle IDs. Without that native CUA tool, Claude Code does not show the Codex CUA cursor path.

HTTP API

Verb Body Returns
GET /status -- Pool snapshot incl. slot identity, holder, heartbeat/expiry, suspected_stale, and bounded retry fields
POST /acquire {"holder": str, "slot"?: int, "dev_port"?: int} Lease record with full identity payload
POST /heartbeat {"lease_token": str} Refreshed lease
POST /release {"lease_token": str} Lease in state: free
GET /reaped -- Last 50 TTL-expired reclamations (debug)

Architecture

Control plane: slot allocator with kernel-flock atomicity, lease broker with TTL + reaper, HTTP API on 7878, MCP shim, and two identity modes: per-slot app bundle IDs for native CUA, or OS-window calibration (kCGWindowNumber on macOS) for same-bundle/CDP workflows. Data plane: Codex CUA via OS Accessibility, or Playwright / Chrome DevTools MCP / any CDP client over the leased cdp_ws_url. Escarp provisions and points; it does not proxy clicks.

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 /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 creates a fresh about:blank tab and closes the rest — no state inherits across holders.

Lease liveness is broker-owned. Heartbeat is POST /heartbeat with the secret lease token; a valid heartbeat refreshes last_heartbeat and extends expires_at. MCP shims send it automatically at TTL / 3. If a holder stops heartbeating, the slot is not stolen by another agent; it becomes reclaimable only when the broker reaper observes that expires_at has passed.

See V2_PLAN.md for the v0→v2 design notes and research/cua_targeting.md for the CUA addressing analysis.

Demos

# Two holders, two browsers, asyncio.gather lockstep concurrency (CDP).
# Proves the lease/concurrency model on the CDP transport.
uv run python scripts/demo_two_holders_concurrent.py

# Lease-boundary reset: drive to YouTube, release, watch the tab snap back.
uv run python scripts/demo_reset_on_release.py

Native-CUA flow:

escarp launch-pool --pool-size 2 --cua-apps
ESCARP_POOL_SIZE=2 escarp daemon
escarp acquire --slot 0 --holder cua-demo --prompt
# paste the printed bundle-ID preamble into Codex Desktop, append a task
escarp release --mine

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
ESCARP_LEASES_FILE ~/.escarp/leases.json Local cache of lease tokens for escarp release --mine

Per-slot resource derivation

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>
            cua_app   = dev.escarp.chrome.slot<s> when launched with --cua-apps
            os_window_id = <calibrated at daemon startup outside CUA app mode>

Status

v1.2.0.

Claims that hold:

  • Each native-CUA slot can have a stable per-slot app bundle identity on macOS (dev.escarp.chrome.slotN).
  • Same-bundle slots still have an actively-verifiable OS-window identity (macOS today). escarp window <slot> queries it live.
  • Two agents on different slots drive their own leased CfTs over CDP without colliding.
  • Two native Codex CUA agents can drive different slots concurrently when the pool is launched with --cua-apps.
  • Killing an agent 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.

Claims that do NOT hold (and aren't claimed):

  • Two concurrent native CUA agents on two windows from the same app bundle. CUA addresses by app, not by window; use --cua-apps for native CUA concurrency.

Not in 1.1: delegated and supervised identity tiers, cross-machine pooling, Linux/Windows OS-window calibration.

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.2.0.tar.gz (50.0 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.2.0-py3-none-any.whl (55.6 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for escarp-1.2.0.tar.gz
Algorithm Hash digest
SHA256 bd629f5112e819d9066849dd7aef7ca36c4d1e0233d2f3a82702e7688022c8b5
MD5 852e855b66e10eea453af6f77f1ae063
BLAKE2b-256 34176250fbc56af91dc4f083211b050b720d13be75c4bca3e2ef34be7f119c01

See more details on using hashes here.

Provenance

The following attestation bundles were made for escarp-1.2.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.2.0-py3-none-any.whl.

File metadata

  • Download URL: escarp-1.2.0-py3-none-any.whl
  • Upload date:
  • Size: 55.6 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.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ef0ec05fde95385facac7290c1b168735af671f140916898a6f306bef4cd9bb9
MD5 3604be20bc446554c2722b0889cffecc
BLAKE2b-256 86c88727961101ccabcda8c3ce71996cb5d5f8f5a3fcab70cb35f34054ce0156

See more details on using hashes here.

Provenance

The following attestation bundles were made for escarp-1.2.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