loom-harness 0.1.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.
- loom_harness-0.1.0/LICENSE +21 -0
- loom_harness-0.1.0/PKG-INFO +230 -0
- loom_harness-0.1.0/README.md +191 -0
- loom_harness-0.1.0/pyproject.toml +108 -0
- loom_harness-0.1.0/src/loom/__init__.py +5 -0
- loom_harness-0.1.0/src/loom/__main__.py +4 -0
- loom_harness-0.1.0/src/loom/auth.py +183 -0
- loom_harness-0.1.0/src/loom/batch.py +113 -0
- loom_harness-0.1.0/src/loom/browser.py +608 -0
- loom_harness-0.1.0/src/loom/canvas.py +163 -0
- loom_harness-0.1.0/src/loom/cli/__init__.py +250 -0
- loom_harness-0.1.0/src/loom/cli/auth.py +64 -0
- loom_harness-0.1.0/src/loom/cli/batch.py +45 -0
- loom_harness-0.1.0/src/loom/cli/browser.py +176 -0
- loom_harness-0.1.0/src/loom/cli/canvas.py +65 -0
- loom_harness-0.1.0/src/loom/cli/clipboard.py +39 -0
- loom_harness-0.1.0/src/loom/cli/contexts.py +76 -0
- loom_harness-0.1.0/src/loom/cli/cookies.py +105 -0
- loom_harness-0.1.0/src/loom/cli/daemon.py +167 -0
- loom_harness-0.1.0/src/loom/cli/dashboard.py +597 -0
- loom_harness-0.1.0/src/loom/cli/dialog.py +28 -0
- loom_harness-0.1.0/src/loom/cli/dom.py +396 -0
- loom_harness-0.1.0/src/loom/cli/download.py +24 -0
- loom_harness-0.1.0/src/loom/cli/eval.py +31 -0
- loom_harness-0.1.0/src/loom/cli/events.py +169 -0
- loom_harness-0.1.0/src/loom/cli/frames.py +94 -0
- loom_harness-0.1.0/src/loom/cli/input.py +220 -0
- loom_harness-0.1.0/src/loom/cli/network.py +270 -0
- loom_harness-0.1.0/src/loom/cli/observe.py +50 -0
- loom_harness-0.1.0/src/loom/cli/page.py +661 -0
- loom_harness-0.1.0/src/loom/cli/storage.py +216 -0
- loom_harness-0.1.0/src/loom/cli/workers.py +42 -0
- loom_harness-0.1.0/src/loom/client.py +315 -0
- loom_harness-0.1.0/src/loom/clipboard.py +68 -0
- loom_harness-0.1.0/src/loom/connection.py +214 -0
- loom_harness-0.1.0/src/loom/console.py +83 -0
- loom_harness-0.1.0/src/loom/contexts.py +143 -0
- loom_harness-0.1.0/src/loom/daemon/__init__.py +1 -0
- loom_harness-0.1.0/src/loom/daemon/__main__.py +44 -0
- loom_harness-0.1.0/src/loom/daemon/cache.py +303 -0
- loom_harness-0.1.0/src/loom/daemon/events.py +201 -0
- loom_harness-0.1.0/src/loom/daemon/pool.py +156 -0
- loom_harness-0.1.0/src/loom/daemon/server.py +671 -0
- loom_harness-0.1.0/src/loom/dialog.py +67 -0
- loom_harness-0.1.0/src/loom/dom.py +919 -0
- loom_harness-0.1.0/src/loom/download.py +42 -0
- loom_harness-0.1.0/src/loom/errors.py +137 -0
- loom_harness-0.1.0/src/loom/evaluator.py +55 -0
- loom_harness-0.1.0/src/loom/expect.py +87 -0
- loom_harness-0.1.0/src/loom/form.py +95 -0
- loom_harness-0.1.0/src/loom/frames.py +214 -0
- loom_harness-0.1.0/src/loom/idb.py +220 -0
- loom_harness-0.1.0/src/loom/input.py +366 -0
- loom_harness-0.1.0/src/loom/introspect.py +67 -0
- loom_harness-0.1.0/src/loom/launch_server.js +49 -0
- loom_harness-0.1.0/src/loom/locators.py +151 -0
- loom_harness-0.1.0/src/loom/mcp/__init__.py +1 -0
- loom_harness-0.1.0/src/loom/mcp/schema.py +128 -0
- loom_harness-0.1.0/src/loom/mcp/server.py +94 -0
- loom_harness-0.1.0/src/loom/metrics.py +99 -0
- loom_harness-0.1.0/src/loom/network/__init__.py +0 -0
- loom_harness-0.1.0/src/loom/network/kinds.py +73 -0
- loom_harness-0.1.0/src/loom/network/query.py +104 -0
- loom_harness-0.1.0/src/loom/network/routes.py +650 -0
- loom_harness-0.1.0/src/loom/network/sink.py +369 -0
- loom_harness-0.1.0/src/loom/network/waits.py +194 -0
- loom_harness-0.1.0/src/loom/observe.py +213 -0
- loom_harness-0.1.0/src/loom/output.py +70 -0
- loom_harness-0.1.0/src/loom/pages.py +729 -0
- loom_harness-0.1.0/src/loom/paths.py +57 -0
- loom_harness-0.1.0/src/loom/pattern.py +41 -0
- loom_harness-0.1.0/src/loom/py.typed +0 -0
- loom_harness-0.1.0/src/loom/recording.py +249 -0
- loom_harness-0.1.0/src/loom/rpc/__init__.py +53 -0
- loom_harness-0.1.0/src/loom/rpc/codec.py +236 -0
- loom_harness-0.1.0/src/loom/rpc/registry.py +573 -0
- loom_harness-0.1.0/src/loom/screenshot.py +88 -0
- loom_harness-0.1.0/src/loom/session.py +130 -0
- loom_harness-0.1.0/src/loom/snap.py +530 -0
- loom_harness-0.1.0/src/loom/state.py +258 -0
- loom_harness-0.1.0/src/loom/waits.py +70 -0
- loom_harness-0.1.0/src/loom/workers.py +105 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thomas Caron
|
|
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.
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: loom-harness
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Stealth browser harness for autonomous LLM agents — Camoufox/Playwright wrapped behind a CLI, JSON-RPC daemon, and MCP server.
|
|
5
|
+
Keywords: browser,automation,playwright,camoufox,agent,llm,mcp,scraping,stealth
|
|
6
|
+
Author: Thomas Caron
|
|
7
|
+
Author-email: Thomas Caron <thomas.caron00@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: Operating System :: MacOS
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Topic :: Software Development :: Testing
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Dist: playwright>=1.49
|
|
22
|
+
Requires-Dist: camoufox[geoip]>=0.4
|
|
23
|
+
Requires-Dist: pydantic>=2.9
|
|
24
|
+
Requires-Dist: pyyaml>=6.0
|
|
25
|
+
Requires-Dist: typer>=0.12
|
|
26
|
+
Requires-Dist: websockets>=12
|
|
27
|
+
Requires-Dist: anyio>=4
|
|
28
|
+
Requires-Dist: mcp>=1.0
|
|
29
|
+
Requires-Dist: httpx>=0.27
|
|
30
|
+
Requires-Dist: pillow>=10
|
|
31
|
+
Requires-Dist: orjson>=3.11.8
|
|
32
|
+
Requires-Dist: pytesseract>=0.3 ; extra == 'ocr'
|
|
33
|
+
Requires-Python: >=3.12
|
|
34
|
+
Project-URL: Homepage, https://github.com/tomtom103/loom
|
|
35
|
+
Project-URL: Repository, https://github.com/tomtom103/loom
|
|
36
|
+
Project-URL: Issues, https://github.com/tomtom103/loom/issues
|
|
37
|
+
Provides-Extra: ocr
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# Loom
|
|
41
|
+
|
|
42
|
+
**Stealth browser harness for autonomous LLM agents.**
|
|
43
|
+
|
|
44
|
+
Loom gives an agent a fast, observable handle on a real browser — open a tab,
|
|
45
|
+
click things, read the DOM, watch the network, run JS, replay APIs, persist
|
|
46
|
+
auth — exposed three ways:
|
|
47
|
+
|
|
48
|
+
- A CLI: `loom <namespace> <verb>` (JSONL output, scriptable, debuggable).
|
|
49
|
+
- A long-lived JSON-RPC daemon: `loomd` (Unix socket at `~/.loom/loomd.sock`,
|
|
50
|
+
auto-spawned on the first command).
|
|
51
|
+
- An MCP server: `loom mcp` (stdio transport, ~50 tools auto-derived from the
|
|
52
|
+
RPC registry).
|
|
53
|
+
|
|
54
|
+
All three share the same daemon, so a session opened from the CLI is drivable
|
|
55
|
+
from MCP and vice versa.
|
|
56
|
+
|
|
57
|
+
The default engine is **[Camoufox](https://github.com/daijro/camoufox)**, a
|
|
58
|
+
stealth-patched Firefox build with BrowserForge fingerprints. Chromium,
|
|
59
|
+
Firefox, and WebKit are also supported by passing `--engine`.
|
|
60
|
+
|
|
61
|
+
## Why
|
|
62
|
+
|
|
63
|
+
Agent automation against real websites is a pile of small problems: bot
|
|
64
|
+
detection, flaky locators, capturing network traffic, persisting login state,
|
|
65
|
+
juggling multi-tab work, snapshotting state for the model to reason over.
|
|
66
|
+
Loom packages those into one fast, predictable harness so the agent can spend
|
|
67
|
+
its tokens on the actual task.
|
|
68
|
+
|
|
69
|
+
## Status
|
|
70
|
+
|
|
71
|
+
Beta. ~625 unit tests, 81% line coverage. The public RPC surface is stable;
|
|
72
|
+
internal layout still moves between releases.
|
|
73
|
+
|
|
74
|
+
## Install
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install loom-harness
|
|
78
|
+
playwright install firefox # for chromium/firefox/webkit engines
|
|
79
|
+
camoufox fetch # for the camoufox stealth engine
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
On headless Linux, Loom can run any engine **headed** against a virtual
|
|
83
|
+
display for better stealth. Install Xvfb once:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
sudo apt-get install xvfb # Debian/Ubuntu
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Loom auto-detects Xvfb on `PATH` and uses it when `--display auto` is set
|
|
90
|
+
(the default).
|
|
91
|
+
|
|
92
|
+
## Quickstart
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# Daemon starts itself the first time the CLI talks to it.
|
|
96
|
+
loom browser open --session s1 --display virtual --viewport 1280x720
|
|
97
|
+
loom page goto https://example.com -s s1
|
|
98
|
+
loom page snap -s s1 --max 30 | jq '.elements[0:5]'
|
|
99
|
+
loom dom click 'role:link[name="More information..."]' -s s1
|
|
100
|
+
loom page info -s s1
|
|
101
|
+
loom browser close -s s1
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`page snap` is the agent's primary "look" call — one payload combining a
|
|
105
|
+
screenshot, an element index, and stable, semantic locator strings (the same
|
|
106
|
+
grammar accepted by `dom.click`, `dom.type`, etc).
|
|
107
|
+
|
|
108
|
+
## What's in the box
|
|
109
|
+
|
|
110
|
+
| Namespace | Highlights |
|
|
111
|
+
|-------------|------------|
|
|
112
|
+
| `browser` | open / close / list; multi-engine (camoufox \| chromium \| firefox \| webkit); persistent profiles; emulation knobs (viewport, locale, timezone, geolocation, color scheme, proxy, UA); Camoufox stealth knobs (OS fingerprint, geoip, humanize, WebRTC/WebGL blocks). |
|
|
113
|
+
| `page` | goto / reload / back / forward; new / close / switch / list / info; snap (annotated screenshot + element index); screenshot / pdf / metrics / console; wait-{selector, text, function, url, idle, request, response}; trace start/stop. |
|
|
114
|
+
| `dom` | click / type / press / hover / select / check / scroll / upload / text / html / query — all locator-grammar aware. |
|
|
115
|
+
| `frame` | list / info / switch / reset — every primitive after `frame switch` runs inside the iframe. Shadow DOM is pierced automatically by Playwright locators. |
|
|
116
|
+
| `network` | query / block / unblock / mock / unmock / offline / cache-clear / export / set-extra-headers / execute (replay a captured request via httpx with the live cookies). |
|
|
117
|
+
| `cookies`, `storage`, `storage idb`, `clipboard` | full CRUD on cookies, localStorage / sessionStorage, IndexedDB, and the page clipboard. |
|
|
118
|
+
| `auth` | interactive (block while operator logs in, snapshot `storage_state`); list / status / delete; replay via `--auth-profile` on subsequent sessions. |
|
|
119
|
+
| `context` | list / new / switch / close — multiple isolated cookie/storage worlds inside one browser. |
|
|
120
|
+
| `mouse`, `keyboard` | coordinate-based mouse + per-key keyboard control. |
|
|
121
|
+
| `dialog`, `download`, `eval` | handle native dialogs, wait for downloads, evaluate JS in the page. |
|
|
122
|
+
| `worker`, `coverage` | service / dedicated worker control; JS+CSS coverage (chromium only). |
|
|
123
|
+
| `observe`, `batch` | snap-act-snap-diff helper; multi-step RPC chains in one daemon dispatch. |
|
|
124
|
+
| `daemon` | start / stop / status. |
|
|
125
|
+
|
|
126
|
+
Full agent-facing reference: [`AGENTS.md`](./AGENTS.md). A drop-in skill for
|
|
127
|
+
Claude Code lives at [`.claude/skills/loom-browser-harness/SKILL.md`](./.claude/skills/loom-browser-harness/SKILL.md).
|
|
128
|
+
|
|
129
|
+
## Live dashboard
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
loom dashboard [-s SESSION] [--port 9876]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Spins up a single-page web viewer at `http://127.0.0.1:9876/` showing a
|
|
136
|
+
live screenshot of the active page plus a scrolling timeline of every
|
|
137
|
+
RPC the daemon dispatches. Useful for demos and for watching long
|
|
138
|
+
autonomous runs from the browser instead of the CLI.
|
|
139
|
+
|
|
140
|
+
The same data is also available as a raw stream:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
loom events tail [-s SESSION] [--frames] [--save DIR]
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`--frames` opts in to base64 JPEG captures (~5fps per session, gated on
|
|
147
|
+
subscriber demand). `--save DIR` drops each frame to disk for replay.
|
|
148
|
+
|
|
149
|
+
## MCP
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
loom mcp # stdio transport — point any MCP client at this command
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Each RPC method is auto-exposed as one MCP tool with a JSON Schema derived
|
|
156
|
+
from its Python type hints. State is shared with the CLI through the daemon,
|
|
157
|
+
so an agent can mix MCP tool calls with shell `loom …` invocations freely.
|
|
158
|
+
|
|
159
|
+
## Layout
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
src/loom/
|
|
163
|
+
browser.py browser.open / close / list, engine launching
|
|
164
|
+
pages.py page.* primitives
|
|
165
|
+
dom.py dom.* primitives
|
|
166
|
+
snap.py page.snap — annotated screenshot + element index
|
|
167
|
+
locators.py semantic locator grammar (testid:, role:, text:, …)
|
|
168
|
+
frames.py frame.* primitives
|
|
169
|
+
network/ capture sink, route mocking, replay, waits
|
|
170
|
+
auth.py auth profile lifecycle
|
|
171
|
+
contexts.py multi-context per session
|
|
172
|
+
observe.py snap-act-snap-diff helper
|
|
173
|
+
batch.py multi-step RPC chains
|
|
174
|
+
daemon/ loomd: server, per-session worker pool, connection cache
|
|
175
|
+
rpc/ JSON-RPC codec + method registry
|
|
176
|
+
mcp/ stdio MCP server, schema generator
|
|
177
|
+
cli/ Typer entry points (one module per namespace)
|
|
178
|
+
client.py CLI ↔ daemon Unix-socket client (auto-spawn)
|
|
179
|
+
session.py session file CRUD (fcntl-locked updates)
|
|
180
|
+
paths.py ~/.loom layout (LOOM_HOME override)
|
|
181
|
+
errors.py structured error codes
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Development
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
git clone https://github.com/tomtom103/loom
|
|
188
|
+
cd loom
|
|
189
|
+
make install # uv sync
|
|
190
|
+
make lint # ruff check
|
|
191
|
+
make type-check # mypy --strict
|
|
192
|
+
make test # pytest (mocked, fast)
|
|
193
|
+
make test-all # add the integration tests (real browsers)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Requirements: Python 3.12+, [uv](https://docs.astral.sh/uv/) for the dev
|
|
197
|
+
setup, and one of the supported browser engines installed.
|
|
198
|
+
|
|
199
|
+
## Files Loom owns
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
~/.loom/loomd.sock daemon Unix socket
|
|
203
|
+
~/.loom/loomd.pid daemon pidfile (flock-guarded)
|
|
204
|
+
~/.loom/loomd.log daemon log
|
|
205
|
+
~/.loom/sessions/<name>.json session state (engine, ws_url, tabs, contexts, ...)
|
|
206
|
+
~/.loom/captures/<name>/ per-session network + console capture
|
|
207
|
+
~/.loom/downloads/<name>/ files saved via the page's download flow
|
|
208
|
+
~/.loom/profiles/<name>/ saved auth profiles (storage_state.json)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Override the root with `LOOM_HOME`.
|
|
212
|
+
|
|
213
|
+
## Opting out of the daemon
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
loom --no-daemon page goto https://example.com # in-process
|
|
217
|
+
LOOM_NO_DAEMON=1 loom page goto https://example.com # same, env-scoped
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Useful for tests, debugging, or scripts that don't want a daemon on disk.
|
|
221
|
+
You lose persistence between calls — the browser disconnects when the CLI
|
|
222
|
+
returns — but every primitive still works.
|
|
223
|
+
|
|
224
|
+
## Releases
|
|
225
|
+
|
|
226
|
+
See [CHANGELOG.md](./CHANGELOG.md) for the history of public changes.
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Loom
|
|
2
|
+
|
|
3
|
+
**Stealth browser harness for autonomous LLM agents.**
|
|
4
|
+
|
|
5
|
+
Loom gives an agent a fast, observable handle on a real browser — open a tab,
|
|
6
|
+
click things, read the DOM, watch the network, run JS, replay APIs, persist
|
|
7
|
+
auth — exposed three ways:
|
|
8
|
+
|
|
9
|
+
- A CLI: `loom <namespace> <verb>` (JSONL output, scriptable, debuggable).
|
|
10
|
+
- A long-lived JSON-RPC daemon: `loomd` (Unix socket at `~/.loom/loomd.sock`,
|
|
11
|
+
auto-spawned on the first command).
|
|
12
|
+
- An MCP server: `loom mcp` (stdio transport, ~50 tools auto-derived from the
|
|
13
|
+
RPC registry).
|
|
14
|
+
|
|
15
|
+
All three share the same daemon, so a session opened from the CLI is drivable
|
|
16
|
+
from MCP and vice versa.
|
|
17
|
+
|
|
18
|
+
The default engine is **[Camoufox](https://github.com/daijro/camoufox)**, a
|
|
19
|
+
stealth-patched Firefox build with BrowserForge fingerprints. Chromium,
|
|
20
|
+
Firefox, and WebKit are also supported by passing `--engine`.
|
|
21
|
+
|
|
22
|
+
## Why
|
|
23
|
+
|
|
24
|
+
Agent automation against real websites is a pile of small problems: bot
|
|
25
|
+
detection, flaky locators, capturing network traffic, persisting login state,
|
|
26
|
+
juggling multi-tab work, snapshotting state for the model to reason over.
|
|
27
|
+
Loom packages those into one fast, predictable harness so the agent can spend
|
|
28
|
+
its tokens on the actual task.
|
|
29
|
+
|
|
30
|
+
## Status
|
|
31
|
+
|
|
32
|
+
Beta. ~625 unit tests, 81% line coverage. The public RPC surface is stable;
|
|
33
|
+
internal layout still moves between releases.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install loom-harness
|
|
39
|
+
playwright install firefox # for chromium/firefox/webkit engines
|
|
40
|
+
camoufox fetch # for the camoufox stealth engine
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
On headless Linux, Loom can run any engine **headed** against a virtual
|
|
44
|
+
display for better stealth. Install Xvfb once:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
sudo apt-get install xvfb # Debian/Ubuntu
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Loom auto-detects Xvfb on `PATH` and uses it when `--display auto` is set
|
|
51
|
+
(the default).
|
|
52
|
+
|
|
53
|
+
## Quickstart
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Daemon starts itself the first time the CLI talks to it.
|
|
57
|
+
loom browser open --session s1 --display virtual --viewport 1280x720
|
|
58
|
+
loom page goto https://example.com -s s1
|
|
59
|
+
loom page snap -s s1 --max 30 | jq '.elements[0:5]'
|
|
60
|
+
loom dom click 'role:link[name="More information..."]' -s s1
|
|
61
|
+
loom page info -s s1
|
|
62
|
+
loom browser close -s s1
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`page snap` is the agent's primary "look" call — one payload combining a
|
|
66
|
+
screenshot, an element index, and stable, semantic locator strings (the same
|
|
67
|
+
grammar accepted by `dom.click`, `dom.type`, etc).
|
|
68
|
+
|
|
69
|
+
## What's in the box
|
|
70
|
+
|
|
71
|
+
| Namespace | Highlights |
|
|
72
|
+
|-------------|------------|
|
|
73
|
+
| `browser` | open / close / list; multi-engine (camoufox \| chromium \| firefox \| webkit); persistent profiles; emulation knobs (viewport, locale, timezone, geolocation, color scheme, proxy, UA); Camoufox stealth knobs (OS fingerprint, geoip, humanize, WebRTC/WebGL blocks). |
|
|
74
|
+
| `page` | goto / reload / back / forward; new / close / switch / list / info; snap (annotated screenshot + element index); screenshot / pdf / metrics / console; wait-{selector, text, function, url, idle, request, response}; trace start/stop. |
|
|
75
|
+
| `dom` | click / type / press / hover / select / check / scroll / upload / text / html / query — all locator-grammar aware. |
|
|
76
|
+
| `frame` | list / info / switch / reset — every primitive after `frame switch` runs inside the iframe. Shadow DOM is pierced automatically by Playwright locators. |
|
|
77
|
+
| `network` | query / block / unblock / mock / unmock / offline / cache-clear / export / set-extra-headers / execute (replay a captured request via httpx with the live cookies). |
|
|
78
|
+
| `cookies`, `storage`, `storage idb`, `clipboard` | full CRUD on cookies, localStorage / sessionStorage, IndexedDB, and the page clipboard. |
|
|
79
|
+
| `auth` | interactive (block while operator logs in, snapshot `storage_state`); list / status / delete; replay via `--auth-profile` on subsequent sessions. |
|
|
80
|
+
| `context` | list / new / switch / close — multiple isolated cookie/storage worlds inside one browser. |
|
|
81
|
+
| `mouse`, `keyboard` | coordinate-based mouse + per-key keyboard control. |
|
|
82
|
+
| `dialog`, `download`, `eval` | handle native dialogs, wait for downloads, evaluate JS in the page. |
|
|
83
|
+
| `worker`, `coverage` | service / dedicated worker control; JS+CSS coverage (chromium only). |
|
|
84
|
+
| `observe`, `batch` | snap-act-snap-diff helper; multi-step RPC chains in one daemon dispatch. |
|
|
85
|
+
| `daemon` | start / stop / status. |
|
|
86
|
+
|
|
87
|
+
Full agent-facing reference: [`AGENTS.md`](./AGENTS.md). A drop-in skill for
|
|
88
|
+
Claude Code lives at [`.claude/skills/loom-browser-harness/SKILL.md`](./.claude/skills/loom-browser-harness/SKILL.md).
|
|
89
|
+
|
|
90
|
+
## Live dashboard
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
loom dashboard [-s SESSION] [--port 9876]
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Spins up a single-page web viewer at `http://127.0.0.1:9876/` showing a
|
|
97
|
+
live screenshot of the active page plus a scrolling timeline of every
|
|
98
|
+
RPC the daemon dispatches. Useful for demos and for watching long
|
|
99
|
+
autonomous runs from the browser instead of the CLI.
|
|
100
|
+
|
|
101
|
+
The same data is also available as a raw stream:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
loom events tail [-s SESSION] [--frames] [--save DIR]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`--frames` opts in to base64 JPEG captures (~5fps per session, gated on
|
|
108
|
+
subscriber demand). `--save DIR` drops each frame to disk for replay.
|
|
109
|
+
|
|
110
|
+
## MCP
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
loom mcp # stdio transport — point any MCP client at this command
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Each RPC method is auto-exposed as one MCP tool with a JSON Schema derived
|
|
117
|
+
from its Python type hints. State is shared with the CLI through the daemon,
|
|
118
|
+
so an agent can mix MCP tool calls with shell `loom …` invocations freely.
|
|
119
|
+
|
|
120
|
+
## Layout
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
src/loom/
|
|
124
|
+
browser.py browser.open / close / list, engine launching
|
|
125
|
+
pages.py page.* primitives
|
|
126
|
+
dom.py dom.* primitives
|
|
127
|
+
snap.py page.snap — annotated screenshot + element index
|
|
128
|
+
locators.py semantic locator grammar (testid:, role:, text:, …)
|
|
129
|
+
frames.py frame.* primitives
|
|
130
|
+
network/ capture sink, route mocking, replay, waits
|
|
131
|
+
auth.py auth profile lifecycle
|
|
132
|
+
contexts.py multi-context per session
|
|
133
|
+
observe.py snap-act-snap-diff helper
|
|
134
|
+
batch.py multi-step RPC chains
|
|
135
|
+
daemon/ loomd: server, per-session worker pool, connection cache
|
|
136
|
+
rpc/ JSON-RPC codec + method registry
|
|
137
|
+
mcp/ stdio MCP server, schema generator
|
|
138
|
+
cli/ Typer entry points (one module per namespace)
|
|
139
|
+
client.py CLI ↔ daemon Unix-socket client (auto-spawn)
|
|
140
|
+
session.py session file CRUD (fcntl-locked updates)
|
|
141
|
+
paths.py ~/.loom layout (LOOM_HOME override)
|
|
142
|
+
errors.py structured error codes
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Development
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
git clone https://github.com/tomtom103/loom
|
|
149
|
+
cd loom
|
|
150
|
+
make install # uv sync
|
|
151
|
+
make lint # ruff check
|
|
152
|
+
make type-check # mypy --strict
|
|
153
|
+
make test # pytest (mocked, fast)
|
|
154
|
+
make test-all # add the integration tests (real browsers)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Requirements: Python 3.12+, [uv](https://docs.astral.sh/uv/) for the dev
|
|
158
|
+
setup, and one of the supported browser engines installed.
|
|
159
|
+
|
|
160
|
+
## Files Loom owns
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
~/.loom/loomd.sock daemon Unix socket
|
|
164
|
+
~/.loom/loomd.pid daemon pidfile (flock-guarded)
|
|
165
|
+
~/.loom/loomd.log daemon log
|
|
166
|
+
~/.loom/sessions/<name>.json session state (engine, ws_url, tabs, contexts, ...)
|
|
167
|
+
~/.loom/captures/<name>/ per-session network + console capture
|
|
168
|
+
~/.loom/downloads/<name>/ files saved via the page's download flow
|
|
169
|
+
~/.loom/profiles/<name>/ saved auth profiles (storage_state.json)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Override the root with `LOOM_HOME`.
|
|
173
|
+
|
|
174
|
+
## Opting out of the daemon
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
loom --no-daemon page goto https://example.com # in-process
|
|
178
|
+
LOOM_NO_DAEMON=1 loom page goto https://example.com # same, env-scoped
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Useful for tests, debugging, or scripts that don't want a daemon on disk.
|
|
182
|
+
You lose persistence between calls — the browser disconnects when the CLI
|
|
183
|
+
returns — but every primitive still works.
|
|
184
|
+
|
|
185
|
+
## Releases
|
|
186
|
+
|
|
187
|
+
See [CHANGELOG.md](./CHANGELOG.md) for the history of public changes.
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "loom-harness"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Stealth browser harness for autonomous LLM agents — Camoufox/Playwright wrapped behind a CLI, JSON-RPC daemon, and MCP server."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Thomas Caron", email = "thomas.caron00@gmail.com" }
|
|
10
|
+
]
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
keywords = [
|
|
13
|
+
"browser",
|
|
14
|
+
"automation",
|
|
15
|
+
"playwright",
|
|
16
|
+
"camoufox",
|
|
17
|
+
"agent",
|
|
18
|
+
"llm",
|
|
19
|
+
"mcp",
|
|
20
|
+
"scraping",
|
|
21
|
+
"stealth",
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 4 - Beta",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"Operating System :: POSIX :: Linux",
|
|
27
|
+
"Operating System :: MacOS",
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"Programming Language :: Python :: 3.12",
|
|
30
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
31
|
+
"Topic :: Internet :: WWW/HTTP :: Browsers",
|
|
32
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
33
|
+
"Topic :: Software Development :: Testing",
|
|
34
|
+
"Typing :: Typed",
|
|
35
|
+
]
|
|
36
|
+
dependencies = [
|
|
37
|
+
"playwright>=1.49",
|
|
38
|
+
"camoufox[geoip]>=0.4",
|
|
39
|
+
"pydantic>=2.9",
|
|
40
|
+
"pyyaml>=6.0",
|
|
41
|
+
"typer>=0.12",
|
|
42
|
+
"websockets>=12",
|
|
43
|
+
"anyio>=4",
|
|
44
|
+
"mcp>=1.0",
|
|
45
|
+
"httpx>=0.27",
|
|
46
|
+
"pillow>=10",
|
|
47
|
+
"orjson>=3.11.8",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[project.urls]
|
|
51
|
+
Homepage = "https://github.com/tomtom103/loom"
|
|
52
|
+
Repository = "https://github.com/tomtom103/loom"
|
|
53
|
+
Issues = "https://github.com/tomtom103/loom/issues"
|
|
54
|
+
|
|
55
|
+
[project.scripts]
|
|
56
|
+
loom = "loom.cli:main"
|
|
57
|
+
|
|
58
|
+
[project.optional-dependencies]
|
|
59
|
+
ocr = ["pytesseract>=0.3"]
|
|
60
|
+
|
|
61
|
+
[dependency-groups]
|
|
62
|
+
dev = [
|
|
63
|
+
"pytest>=8",
|
|
64
|
+
"ruff>=0.11",
|
|
65
|
+
"mypy>=1.15",
|
|
66
|
+
"types-pyyaml>=6.0",
|
|
67
|
+
"pytest-asyncio>=0.24",
|
|
68
|
+
"pytest-cov>=5",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
[build-system]
|
|
72
|
+
requires = ["uv_build>=0.10.10,<0.11.0"]
|
|
73
|
+
build-backend = "uv_build"
|
|
74
|
+
|
|
75
|
+
[tool.uv.build-backend]
|
|
76
|
+
module-name = "loom"
|
|
77
|
+
module-root = "src"
|
|
78
|
+
|
|
79
|
+
[tool.ruff]
|
|
80
|
+
target-version = "py312"
|
|
81
|
+
line-length = 100
|
|
82
|
+
|
|
83
|
+
[tool.ruff.lint]
|
|
84
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
85
|
+
|
|
86
|
+
[tool.ruff.lint.per-file-ignores]
|
|
87
|
+
# Inline HTML/CSS/JS for the dashboard ships as a single string — line-length
|
|
88
|
+
# wrapping would corrupt the rendered output, so allow long lines here.
|
|
89
|
+
"src/loom/cli/dashboard.py" = ["E501"]
|
|
90
|
+
"tests/test_dashboard.py" = ["E501"]
|
|
91
|
+
|
|
92
|
+
[tool.ruff.lint.flake8-bugbear]
|
|
93
|
+
extend-immutable-calls = ["typer.Option", "typer.Argument"]
|
|
94
|
+
|
|
95
|
+
[tool.mypy]
|
|
96
|
+
python_version = "3.12"
|
|
97
|
+
strict = true
|
|
98
|
+
|
|
99
|
+
[[tool.mypy.overrides]]
|
|
100
|
+
module = ["pytesseract"]
|
|
101
|
+
ignore_missing_imports = true
|
|
102
|
+
|
|
103
|
+
[tool.pytest.ini_options]
|
|
104
|
+
testpaths = ["tests"]
|
|
105
|
+
asyncio_mode = "auto"
|
|
106
|
+
markers = [
|
|
107
|
+
"integration: tests that launch a real browser (slow; opt-in).",
|
|
108
|
+
]
|