escarp 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ *.egg-info/
6
+ .mypy_cache/
7
+ .ruff_cache/
8
+ .pytest_cache/
9
+
10
+ # v2: Chrome for Testing dropped here by @puppeteer/browsers during local dev.
11
+ # Production install path is ~/.escarp/chrome/ (daemon-managed on first run).
12
+ chrome/
13
+
14
+ # v2: ephemeral profile/state dirs used by the broker daemon.
15
+ .escarp/
16
+
escarp-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 David Gao
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
escarp-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,205 @@
1
+ Metadata-Version: 2.4
2
+ Name: escarp
3
+ Version: 1.0.0
4
+ Summary: Identity-aware runtime for parallel coding agents -- persistent browser pool + lease broker over CDP
5
+ Project-URL: Homepage, https://github.com/ddavidgao/escarp
6
+ Project-URL: Repository, https://github.com/ddavidgao/escarp
7
+ Project-URL: Issues, https://github.com/ddavidgao/escarp/issues
8
+ Author-email: David Gao <davidgao1345@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,ai,browser-pool,chrome-devtools-protocol,claude-code,codex,mcp,worktree
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Testing
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: aiohttp>=3.9
21
+ Requires-Dist: cryptography>=42.0
22
+ Requires-Dist: httpx>=0.28.1
23
+ Requires-Dist: mcp>=1.27.2
24
+ Provides-Extra: dev
25
+ Requires-Dist: mypy>=1.10; extra == 'dev'
26
+ Requires-Dist: playwright>=1.44; extra == 'dev'
27
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
28
+ Requires-Dist: pytest>=8.0; extra == 'dev'
29
+ Requires-Dist: ruff>=0.4; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # escarp
33
+
34
+ **Identity-aware runtime for parallel coding agents.** A single broker daemon
35
+ owns a pool of persistent Chrome for Testing windows and hands CDP endpoint
36
+ leases to coding agents (Claude Code, Codex) over MCP. N worktrees can run N
37
+ agents with N isolated browsers, no stale-lock hell.
38
+
39
+ > **Why?** Spawn-a-browser-per-tool-call leaks chromes on every chat exit.
40
+ > Per-agent lockfiles strand themselves when the agent dies. Driving the
41
+ > user's daily-driver browser pollutes cookies and session state. Escarp
42
+ > separates lifecycle (persistent, owned by escarp) from leases (ephemeral,
43
+ > owned by the agent). The browsers always exist; agents check them out.
44
+
45
+ ## Install
46
+
47
+ ```bash
48
+ pip install escarp
49
+ ```
50
+
51
+ Requirements: Python 3.11+, a Chrome for Testing binary on disk. Easiest way
52
+ to get one:
53
+
54
+ ```bash
55
+ npx @puppeteer/browsers install chrome@stable
56
+ export ESCARP_CFT_BINARY=".../Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"
57
+ ```
58
+
59
+ ## Quick start
60
+
61
+ ```bash
62
+ # 1. Spawn N persistent Chrome for Testing windows (one-shot, exits immediately).
63
+ # The chromes are detached and survive escarp restarts.
64
+ escarp launch-pool # default 4
65
+ ESCARP_POOL_SIZE=8 escarp launch-pool
66
+
67
+ # 2. Start the broker daemon. It discovers the chromes via /json/version,
68
+ # serves the lease HTTP API on 127.0.0.1:7878, and runs a reaper for
69
+ # expired leases. ^C releases the slot locks but leaves chromes alive.
70
+ escarp daemon &
71
+
72
+ # 3. Inspect the pool
73
+ curl -s http://127.0.0.1:7878/status | jq
74
+ ```
75
+
76
+ Drive one of the leased browsers in Python:
77
+
78
+ ```python
79
+ import asyncio, httpx
80
+ from playwright.async_api import async_playwright
81
+
82
+ async def main():
83
+ lease = httpx.post(
84
+ "http://127.0.0.1:7878/acquire",
85
+ json={"holder": "demo-script"},
86
+ ).json()
87
+ print("got slot", lease["slot"], "->", lease["cdp_ws_url"])
88
+
89
+ async with async_playwright() as pw:
90
+ browser = await pw.chromium.connect_over_cdp(lease["cdp_ws_url"])
91
+ page = browser.contexts[0].pages[0]
92
+ await page.goto("https://example.com")
93
+ await page.screenshot(path="/tmp/example.png")
94
+ await browser.close() # disconnect; broker-owned chrome stays alive
95
+
96
+ httpx.post(
97
+ "http://127.0.0.1:7878/release",
98
+ json={"lease_token": lease["lease_token"]},
99
+ ) # broker auto-resets the tab to about:blank
100
+
101
+ asyncio.run(main())
102
+ ```
103
+
104
+ ## Wire it into Claude Code / Codex via MCP
105
+
106
+ Register the bundled MCP shim so the model gets three first-class tools
107
+ (`escarp_status`, `escarp_acquire`, `escarp_release`) and never has to
108
+ curl the broker by hand:
109
+
110
+ ```bash
111
+ # Claude Code
112
+ claude mcp add escarp -- escarp-mcp
113
+
114
+ # Codex CLI
115
+ codex mcp add escarp escarp-mcp
116
+ ```
117
+
118
+ Auto-heartbeat lives in the shim, so a long-running session can't lose the
119
+ lease mid-task. On disconnect the lease releases and the slot returns to
120
+ the pool, reset to about:blank.
121
+
122
+ ## HTTP API (three verbs)
123
+
124
+ | Verb | Body | Returns |
125
+ | ---- | ---- | ------- |
126
+ | `GET /status` | -- | Pool snapshot, no lease tokens leaked |
127
+ | `POST /acquire` | `{"holder": str, "slot"?: int, "dev_port"?: int}` | `{slot, cdp_ws_url, lease_token, expires_at, ...}` |
128
+ | `POST /heartbeat` | `{"lease_token": str}` | Refreshed lease record |
129
+ | `POST /release` | `{"lease_token": str}` | Lease record in `state: free` |
130
+ | `GET /reaped` | -- | Last 50 TTL-expired reclamations (debug) |
131
+
132
+ ## Architecture (one paragraph)
133
+
134
+ **Control plane (escarp):** slot allocator with kernel-flock atomicity,
135
+ lease broker with TTL + reaper, HTTP API on 7878, MCP shim. **Data plane
136
+ (your agent's tools):** Playwright `connect_over_cdp`, `@playwright/mcp`
137
+ `--cdp-endpoint`, `chrome-devtools-mcp` `--browser-url`, or whatever CDP
138
+ client you want, attached directly to the leased `cdp_ws_url`. Escarp
139
+ provisions and points; it never proxies your clicks. If escarp ever shows
140
+ up in your per-click latency, that's a bug.
141
+
142
+ The persistence contract is the load-bearing trick: chromes are launched
143
+ detached (`start_new_session=True`) and reparent to launchd/init. The
144
+ daemon discovers them by GET `/json/version`; it never owns their
145
+ lifecycle. Kill the daemon, chromes stay up. Kill an agent mid-task, the
146
+ reaper reclaims its lease within one sweep interval (default 2 s). On every
147
+ release boundary the broker `PUT /json/new?about:blank`s a fresh tab and
148
+ closes the old ones, so no state inherits across holders.
149
+
150
+ See [V2_PLAN.md](V2_PLAN.md) for the full design and decision record.
151
+
152
+ ## Demos
153
+
154
+ The repo ships scripts that prove the headline claims end-to-end:
155
+
156
+ ```bash
157
+ # Two holders, two browsers, asyncio.gather'd lockstep concurrency.
158
+ # Steps fire within ~70 ms across both browsers; ~2x parallel speedup.
159
+ uv run python scripts/demo_two_holders_concurrent.py
160
+
161
+ # Lease-boundary reset: drive to YouTube, release, watch the tab snap back
162
+ # to about:blank. Proof that state does not leak across holders.
163
+ uv run python scripts/demo_reset_on_release.py
164
+ ```
165
+
166
+ ## Configuration
167
+
168
+ | Env var | Default | What |
169
+ | ------- | ------- | ---- |
170
+ | `ESCARP_POOL_SIZE` | `4` | Number of browser slots |
171
+ | `ESCARP_CDP_BASE` | `9222` | cdp port for slot 0; slot N uses base+N |
172
+ | `ESCARP_API_PORT` | `7878` | Broker HTTP API port (bind-and-shift on collision) |
173
+ | `ESCARP_LEASE_TTL_S` | `60` | Lease expiry; reaper reclaims past this |
174
+ | `ESCARP_CFT_BINARY` | autodetect | Path to Chrome for Testing binary |
175
+ | `ESCARP_BROKER_URL` | `http://127.0.0.1:7878` | Where the MCP shim looks for the broker |
176
+
177
+ ## Per-slot resource derivation
178
+
179
+ Each slot derives all its resources from a single index, so two worktrees
180
+ on different slots never collide on ports:
181
+
182
+ ```
183
+ slot s -> frontend = 3000 + s*10
184
+ backend = 8000 + s*10
185
+ postgres = 5432 + s*10
186
+ cdp_port = 9222 + s
187
+ user_data = ~/.escarp/profiles/<tier>/slot-<s>
188
+ ```
189
+
190
+ ## Status
191
+
192
+ v1.0.0. The four headline claims hold:
193
+
194
+ - Two agents on different slots drive their own leased CfTs, never collide.
195
+ - Killing an agent mid-task returns its browser within one reaper interval.
196
+ - Pool exhaustion returns a structured 409, not a hang.
197
+ - No lockfiles outside the broker's single source of truth.
198
+
199
+ What's not in 1.0: delegated and supervised identity tiers (v1.1 and v1.2),
200
+ cross-machine pooling (v2), Docker compose orchestration (hooks only for
201
+ now -- escarp doesn't own compose semantics).
202
+
203
+ ## License
204
+
205
+ MIT
escarp-1.0.0/README.md ADDED
@@ -0,0 +1,174 @@
1
+ # escarp
2
+
3
+ **Identity-aware runtime for parallel coding agents.** A single broker daemon
4
+ owns a pool of persistent Chrome for Testing windows and hands CDP endpoint
5
+ leases to coding agents (Claude Code, Codex) over MCP. N worktrees can run N
6
+ agents with N isolated browsers, no stale-lock hell.
7
+
8
+ > **Why?** Spawn-a-browser-per-tool-call leaks chromes on every chat exit.
9
+ > Per-agent lockfiles strand themselves when the agent dies. Driving the
10
+ > user's daily-driver browser pollutes cookies and session state. Escarp
11
+ > separates lifecycle (persistent, owned by escarp) from leases (ephemeral,
12
+ > owned by the agent). The browsers always exist; agents check them out.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pip install escarp
18
+ ```
19
+
20
+ Requirements: Python 3.11+, a Chrome for Testing binary on disk. Easiest way
21
+ to get one:
22
+
23
+ ```bash
24
+ npx @puppeteer/browsers install chrome@stable
25
+ export ESCARP_CFT_BINARY=".../Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"
26
+ ```
27
+
28
+ ## Quick start
29
+
30
+ ```bash
31
+ # 1. Spawn N persistent Chrome for Testing windows (one-shot, exits immediately).
32
+ # The chromes are detached and survive escarp restarts.
33
+ escarp launch-pool # default 4
34
+ ESCARP_POOL_SIZE=8 escarp launch-pool
35
+
36
+ # 2. Start the broker daemon. It discovers the chromes via /json/version,
37
+ # serves the lease HTTP API on 127.0.0.1:7878, and runs a reaper for
38
+ # expired leases. ^C releases the slot locks but leaves chromes alive.
39
+ escarp daemon &
40
+
41
+ # 3. Inspect the pool
42
+ curl -s http://127.0.0.1:7878/status | jq
43
+ ```
44
+
45
+ Drive one of the leased browsers in Python:
46
+
47
+ ```python
48
+ import asyncio, httpx
49
+ from playwright.async_api import async_playwright
50
+
51
+ async def main():
52
+ lease = httpx.post(
53
+ "http://127.0.0.1:7878/acquire",
54
+ json={"holder": "demo-script"},
55
+ ).json()
56
+ print("got slot", lease["slot"], "->", lease["cdp_ws_url"])
57
+
58
+ async with async_playwright() as pw:
59
+ browser = await pw.chromium.connect_over_cdp(lease["cdp_ws_url"])
60
+ page = browser.contexts[0].pages[0]
61
+ await page.goto("https://example.com")
62
+ await page.screenshot(path="/tmp/example.png")
63
+ await browser.close() # disconnect; broker-owned chrome stays alive
64
+
65
+ httpx.post(
66
+ "http://127.0.0.1:7878/release",
67
+ json={"lease_token": lease["lease_token"]},
68
+ ) # broker auto-resets the tab to about:blank
69
+
70
+ asyncio.run(main())
71
+ ```
72
+
73
+ ## Wire it into Claude Code / Codex via MCP
74
+
75
+ Register the bundled MCP shim so the model gets three first-class tools
76
+ (`escarp_status`, `escarp_acquire`, `escarp_release`) and never has to
77
+ curl the broker by hand:
78
+
79
+ ```bash
80
+ # Claude Code
81
+ claude mcp add escarp -- escarp-mcp
82
+
83
+ # Codex CLI
84
+ codex mcp add escarp escarp-mcp
85
+ ```
86
+
87
+ Auto-heartbeat lives in the shim, so a long-running session can't lose the
88
+ lease mid-task. On disconnect the lease releases and the slot returns to
89
+ the pool, reset to about:blank.
90
+
91
+ ## HTTP API (three verbs)
92
+
93
+ | Verb | Body | Returns |
94
+ | ---- | ---- | ------- |
95
+ | `GET /status` | -- | Pool snapshot, no lease tokens leaked |
96
+ | `POST /acquire` | `{"holder": str, "slot"?: int, "dev_port"?: int}` | `{slot, cdp_ws_url, lease_token, expires_at, ...}` |
97
+ | `POST /heartbeat` | `{"lease_token": str}` | Refreshed lease record |
98
+ | `POST /release` | `{"lease_token": str}` | Lease record in `state: free` |
99
+ | `GET /reaped` | -- | Last 50 TTL-expired reclamations (debug) |
100
+
101
+ ## Architecture (one paragraph)
102
+
103
+ **Control plane (escarp):** slot allocator with kernel-flock atomicity,
104
+ lease broker with TTL + reaper, HTTP API on 7878, MCP shim. **Data plane
105
+ (your agent's tools):** Playwright `connect_over_cdp`, `@playwright/mcp`
106
+ `--cdp-endpoint`, `chrome-devtools-mcp` `--browser-url`, or whatever CDP
107
+ client you want, attached directly to the leased `cdp_ws_url`. Escarp
108
+ provisions and points; it never proxies your clicks. If escarp ever shows
109
+ up in your per-click latency, that's a bug.
110
+
111
+ The persistence contract is the load-bearing trick: chromes are launched
112
+ detached (`start_new_session=True`) and reparent to launchd/init. The
113
+ daemon discovers them by GET `/json/version`; it never owns their
114
+ lifecycle. Kill the daemon, chromes stay up. Kill an agent mid-task, the
115
+ reaper reclaims its lease within one sweep interval (default 2 s). On every
116
+ release boundary the broker `PUT /json/new?about:blank`s a fresh tab and
117
+ closes the old ones, so no state inherits across holders.
118
+
119
+ See [V2_PLAN.md](V2_PLAN.md) for the full design and decision record.
120
+
121
+ ## Demos
122
+
123
+ The repo ships scripts that prove the headline claims end-to-end:
124
+
125
+ ```bash
126
+ # Two holders, two browsers, asyncio.gather'd lockstep concurrency.
127
+ # Steps fire within ~70 ms across both browsers; ~2x parallel speedup.
128
+ uv run python scripts/demo_two_holders_concurrent.py
129
+
130
+ # Lease-boundary reset: drive to YouTube, release, watch the tab snap back
131
+ # to about:blank. Proof that state does not leak across holders.
132
+ uv run python scripts/demo_reset_on_release.py
133
+ ```
134
+
135
+ ## Configuration
136
+
137
+ | Env var | Default | What |
138
+ | ------- | ------- | ---- |
139
+ | `ESCARP_POOL_SIZE` | `4` | Number of browser slots |
140
+ | `ESCARP_CDP_BASE` | `9222` | cdp port for slot 0; slot N uses base+N |
141
+ | `ESCARP_API_PORT` | `7878` | Broker HTTP API port (bind-and-shift on collision) |
142
+ | `ESCARP_LEASE_TTL_S` | `60` | Lease expiry; reaper reclaims past this |
143
+ | `ESCARP_CFT_BINARY` | autodetect | Path to Chrome for Testing binary |
144
+ | `ESCARP_BROKER_URL` | `http://127.0.0.1:7878` | Where the MCP shim looks for the broker |
145
+
146
+ ## Per-slot resource derivation
147
+
148
+ Each slot derives all its resources from a single index, so two worktrees
149
+ on different slots never collide on ports:
150
+
151
+ ```
152
+ slot s -> frontend = 3000 + s*10
153
+ backend = 8000 + s*10
154
+ postgres = 5432 + s*10
155
+ cdp_port = 9222 + s
156
+ user_data = ~/.escarp/profiles/<tier>/slot-<s>
157
+ ```
158
+
159
+ ## Status
160
+
161
+ v1.0.0. The four headline claims hold:
162
+
163
+ - Two agents on different slots drive their own leased CfTs, never collide.
164
+ - Killing an agent mid-task returns its browser within one reaper interval.
165
+ - Pool exhaustion returns a structured 409, not a hang.
166
+ - No lockfiles outside the broker's single source of truth.
167
+
168
+ What's not in 1.0: delegated and supervised identity tiers (v1.1 and v1.2),
169
+ cross-machine pooling (v2), Docker compose orchestration (hooks only for
170
+ now -- escarp doesn't own compose semantics).
171
+
172
+ ## License
173
+
174
+ MIT