{"id":50584739,"url":"https://github.com/mentiora-ai/loom","last_synced_at":"2026-06-05T05:02:07.234Z","repository":{"id":355642845,"uuid":"1228848879","full_name":"mentiora-ai/loom","owner":"mentiora-ai","description":"Agent-first browser automation runtime — deterministic Chromium sessions with replay-equal hash chains, MCP-native tools, and a content-addressed action store.","archived":false,"fork":false,"pushed_at":"2026-05-29T08:43:37.000Z","size":2046,"stargazers_count":1,"open_issues_count":4,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-29T09:24:49.016Z","etag":null,"topics":["agent-tools","browser-automation","chromium","cli","content-addressable-storage","determinism","mcp","rust","wasm"],"latest_commit_sha":null,"homepage":"https://github.com/mentiora-ai/loom","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mentiora-ai.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE-APACHE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-04T12:49:52.000Z","updated_at":"2026-05-29T08:39:23.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/mentiora-ai/loom","commit_stats":null,"previous_names":["j-mentiora/loom","mentiora-ai/loom"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/mentiora-ai/loom","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mentiora-ai%2Floom","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mentiora-ai%2Floom/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mentiora-ai%2Floom/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mentiora-ai%2Floom/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mentiora-ai","download_url":"https://codeload.github.com/mentiora-ai/loom/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mentiora-ai%2Floom/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33930311,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-05T02:00:06.157Z","response_time":120,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent-tools","browser-automation","chromium","cli","content-addressable-storage","determinism","mcp","rust","wasm"],"created_at":"2026-06-05T05:02:06.387Z","updated_at":"2026-06-05T05:02:07.228Z","avatar_url":"https://github.com/mentiora-ai.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Loom\n\n[![CI](https://github.com/mentiora-ai/loom/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/mentiora-ai/loom/actions/workflows/ci.yml)\n[![Release](https://img.shields.io/github/v/release/mentiora-ai/loom?include_prereleases\u0026sort=semver)](https://github.com/mentiora-ai/loom/releases/latest)\n[![npm](https://img.shields.io/npm/v/@mentiora-ai/loom-sdk?label=%40mentiora-ai%2Floom-sdk\u0026color=cb3837)](https://www.npmjs.com/package/@mentiora-ai/loom-sdk)\n[![License](https://img.shields.io/badge/license-Apache--2.0%20OR%20MIT-blue)](#license)\n[![Rust](https://img.shields.io/badge/rust-1.92%2B-orange)](https://www.rust-lang.org/)\n[![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux-lightgrey)](#install)\n\n**Agent-first browser automation runtime.** A local daemon + CLI + MCP\nserver that drives a real Chromium subprocess through a deterministic\naction store. Playwright and CDP were built for humans testing their own\nsites; Loom is shaped for the case where the caller is an LLM agent that\nneeds to browse, fill forms, run JavaScript, and reliably know whether each\naction worked — with replay-equal hash chains, content-addressed artifacts,\nand a typed error wire shape that doesn't leak generic `internal_error`\nstrings.\n\n```\n┌─────────────┐         ┌──────────────┐         ┌────────────────┐\n│ loom (CLI)  │  ────►  │ loom-daemon  │  ────►  │ loom-shim-     │  ────► Chromium\n│ loom-mcp    │  ◄────  │  (sessions,  │  ◄────  │  chromium      │\n│ Claude/etc  │  JSON   │   replay,    │ CBOR    │  (CDP via WS)  │\n└─────────────┘  RPC    │   manifest)  │         └────────────────┘\n                        └──────────────┘\n```\n\n**Platforms.** macOS arm64/x86_64 and Linux x86_64/arm64. Windows is not supported on v0.9.9.\n\n## If you've hit the vibe-coding testing wall\n\nYou shipped the feature in two evenings. You want browser tests so you\ncan stop manually clicking through every release. You asked the agent to\nwire up Playwright. The suite passed — once. Now half the runs flake on\ntiming, a quarter flake on a popup the agent doesn't know about, and\nevery \"just fix the flake\" eats two hours and ends with three more\n`waitForTimeout(2000)` calls you're not proud of.\n\nThese don't break for *random* reasons — they break because the tooling\nunderneath was built for a human watching a screen. The agent has no\nreplay, no typed feedback, no budget. When something goes wrong it goes\nwrong silently, and you debug by re-running until you get lucky.\n\nLoom is the runtime you actually want for this:\n\n| You hit                                                | Loom answer                                                                |\n|--------------------------------------------------------|----------------------------------------------------------------------------|\n| Flake from timing / animations / `Math.random`         | Clocks, RNG, and animations frozen at session-create                       |\n| \"Passed yesterday, fails today\" with no useful diff    | Hash-chain WAL — `loom session diff` finds the one action that diverged    |\n| `Error: Timeout 30000ms exceeded` strings              | Typed errors (`kind: \"wait_predicate_false\"`, …) the agent can branch on   |\n| Glue code between Claude Code and Playwright           | Bundled MCP server — actions show up as native tools, no shim              |\n| Agent stuck in a loop melting the laptop               | Per-session budgets on wall-clock, network bytes, DOM nodes, JS heap       |\n| OAuth tokens passed around as cookie jars              | Scoped grants tied to (session, origin, scopes, ttl); agent never sees the token |\n\nIf you're three hours into a flaky test session right now, the\n[5-minute quickstart](#5-minute-quickstart) below is a fair deal.\n\n## What makes loom different\n\n- **Deterministic replay.** Every action goes into a manifest WAL with a\n  SHA-256 hash chain. `loom session replay \u003cid\u003e` reproduces the action\n  chain bit-for-bit (excluding screenshot blobs, by design).\n- **Typed errors over the wire.** `kind: \"http_status\"` with a real status\n  code, `kind: \"dns_failure\"` with the chromium error name, `kind:\n  \"wait_predicate_false\"` when a `web.wait` selector never appears — not\n  a generic 500.\n- **MCP-native.** `loom-mcp serve` exposes `loom.web.navigate`,\n  `loom.web.click`, `loom.web.evaluate`, etc. as MCP tools with implicit\n  session management — no `session_id` boilerplate in the client.\n- **WASM-isolated surfaces.** The Chromium driver lives in a separate\n  process; the WIT-based surface API is loaded as a wasmtime component,\n  so a hostile page can't reach the host process directly.\n- **Content-addressed everything.** DOM snapshots, screenshots, and\n  exported tarballs all live in CAS keyed by SHA-256.\n- **Per-session resource budgets.** Hard limits on wall-clock, network\n  bytes, DOM nodes, and JS heap. Breaching any of them kills the session\n  and returns a typed `budget-exceeded` error, so a runaway agent can't\n  open 400 tabs and melt the host. Configurable at session-create:\n  `loom session create --budget network=10MB,wall_clock=30s`.\n- **Scoped OAuth credential vault.** Grants are tied to (`session_id`,\n  `origin`, `scopes`, `ttl`); the agent process never sees the token, the\n  vault attaches it origin-scoped at request time, and every use lands in\n  the session's audit log. Backed by the OS keychain on macOS.\n- **Time-travel inspect.** `loom session inspect \u003cid\u003e --at-action N`\n  reconstructs the session state at any point in the action chain — DOM,\n  screenshots, network, console — without re-running anything.\n\n## How loom compares\n\nHonest matrix versus the other tools you'd reach for. Loom is not trying to\nwin on every axis — it's trying to be obviously the right pick when\nreplay-equality, MCP-native ergonomics, or process isolation actually matter,\nand the wrong pick when you need cross-browser coverage, Windows, or the\nbiggest community.\n\n|                                              | **loom**            | Playwright                | Puppeteer        | Browserbase + Stagehand    | browser-use            | Anthropic Computer Use |\n|----------------------------------------------|---------------------|---------------------------|------------------|----------------------------|------------------------|------------------------|\n| Bit-equal deterministic replay               | ✓ hash-chain WAL    | trace viewer (descriptive)| —                | —                          | —                      | —                      |\n| MCP server, no `session_id` plumbing         | ✓ bundled           | external¹                 | —                | partial²                   | —                      | —                      |\n| Typed errors (`kind: …`) for LLM consumers   | ✓                   | string messages           | string messages  | partial                    | string messages        | n/a                    |\n| WASM-isolated page driver                    | ✓                   | —                         | —                | —                          | —                      | —                      |\n| Local, no per-minute cloud bill              | ✓                   | ✓                         | ✓                | ✗ cloud                    | ✓                      | API per call           |\n| Approach                                     | DOM + CDP           | DOM + CDP                 | DOM + CDP        | DOM + CDP (cloud)          | DOM + vision           | pure vision            |\n| Browsers                                     | Chromium            | Chromium / Firefox / WebKit | Chromium       | Chromium                   | Chromium / Firefox / WebKit | any (vision)      |\n| Windows                                      | ✗                   | ✓                         | ✓                | ✓                          | ✓                      | ✓                      |\n| SDKs                                         | Rust + Python + TS  | very wide                 | Node             | Node + Python              | Python                 | any (HTTP)             |\n| Maturity                                     | 0.9 (pre-1.0)       | 5+ yrs, 1.x               | 7+ yrs, 1.x      | GA                         | growing                | GA                     |\n\n¹ Microsoft ships `@playwright/mcp` as a separate package — it isn't part of\ncore Playwright and runs as its own process.\n² Stagehand exposes MCP-style integration, but session lifecycle lives in\nBrowserbase's cloud rather than locally.\n\n**When to pick which:**\n\n- **Playwright / Puppeteer** — you're writing a test suite, need\n  cross-browser, or want the largest community and the most\n  StackOverflow answers.\n- **Browserbase + Stagehand** — you don't want to run anything locally\n  and are fine paying per browser-minute for someone else's infra +\n  fleet management.\n- **browser-use** — you're in pure Python and want fast LLM-driven\n  scraping with vision fallback, and don't need replay or typed errors.\n- **Anthropic Computer Use** — the surface has no usable DOM at all\n  (canvas-heavy apps, native apps over screen share, anti-bot pages\n  where DOM access is the problem).\n- **loom** — pick if any of this is your day:\n  - you're vibe-coding in Claude Code or Cursor and want the agent to\n    *actually* drive a real browser as a native tool — not write a\n    Playwright script you then run by hand\n  - your AI-driven browser tests flake on half the runs and you want\n    \"same prompt → same run,\" with a hash you can diff when one breaks\n  - you're automating pages from sources you don't fully trust\n    (LLM-supplied URLs, hostile sites, anti-bot territory) and would\n    rather a malicious page not reach your host\n\nIf none of those is your day, Playwright is probably the better call and\nwe'd say so. Loom earns its keep on determinism + MCP + isolation, not on\nfeature breadth.\n\n## Install\n\nPick whichever fits your environment. The three CLI methods below all end at the\nsame `loom postinstall` step, which downloads + verifies the pinned Chromium\nbuild (~150 MB, one-time) and AOT-compiles the WASM surfaces.\n\n\u003e **What runs where.** loom runs entirely locally; it adds no telemetry.\n\u003e Chromium is downloaded from GitHub Releases over TLS and SHA-256-verified\n\u003e before execution.\n\n### Packages at a glance\n\n| Component | Registry | Package | Install |\n|-----------|----------|---------|---------|\n| CLI + runtime | Homebrew | `mentiora-ai/loom/loom` | `brew install mentiora-ai/loom/loom` |\n| CLI + runtime | Release installer | — | `curl -fsSL …/loom-cli-installer.sh \\| sh` |\n| CLI + runtime | Cargo (from git) | `loom-cli` | `cargo install --git … --tag v0.9.9 loom-cli` |\n| CLI + runtime | crates.io | — | **not published** ([why](#why-no-cratesio-package)) — use a method above |\n| TypeScript SDK | npm | [`@mentiora-ai/loom-sdk`](https://www.npmjs.com/package/@mentiora-ai/loom-sdk) | `npm install @mentiora-ai/loom-sdk` |\n| Python SDK | PyPI | `mentiora-loom` | published on the next tagged release ([details](#python)) |\n\nThe detailed steps for each CLI method follow; SDK install is under\n[Client SDKs](#client-sdks).\n\n#### Why no crates.io package\n\nloom isn't published to crates.io. `loom` is taken (tokio-rs's concurrency\ncrate), and `loom-cli`/`loom-core` are owned by an unrelated project, so a real\npublish would mean renaming and republishing the whole ~10-crate workspace under\na namespace — out of proportion to the benefit when the binary already installs\nvia Homebrew, the release installer, and `cargo install --git`. The decision is\nencoded as `publish = false` in [`loom-cli/Cargo.toml`](loom-cli/Cargo.toml), so\n`cargo publish` refuses and no release can leak a crate by accident. **Rollback:**\nthere is no registry artifact to `cargo yank`, so a bad Rust build is recovered by\na patch-forward release (tag the next version); the npm and PyPI SDK packages\nfollow the yank / patch-forward procedure documented in their publish workflows.\n\n### Homebrew — macOS arm64/x64, Linux x64\n\n```bash\nbrew install mentiora-ai/loom/loom\nloom postinstall\nloom doctor\n```\n\n### `cargo install` — any platform with Rust 1.92+\n\n```bash\ncargo install --git https://github.com/mentiora-ai/loom --tag v0.9.9 loom-cli\nloom postinstall\nloom doctor\n```\n\n`--tag` is required: `loom postinstall` fetches `loom-daemon`, `loom-mcp`, and\n`loom-shim-chromium` from the GitHub Release matching the installed crate\nversion, so the tag must point at an existing release. (Substitute the latest\nrelease version for `v0.9.9`.)\n\n### Manual download — pre-built tarball\n\n```bash\ncurl -fsSL https://github.com/mentiora-ai/loom/releases/latest/download/loom-cli-installer.sh | sh\nloom postinstall\nloom doctor\n```\n\nThe installer drops all four binaries into `~/.cargo/bin` (or `~/.local/bin` if\nCargo isn't installed).\n\n### After install: Gatekeeper on macOS\n\nThe release artifacts aren't notarized yet. On first run macOS may quarantine\n`loom-shim-chromium`:\n\n```bash\nxattr -d com.apple.quarantine $(which loom-shim-chromium)\n```\n\nNotarization is tracked as a follow-up. Windows isn't supported — see\n[Known limitations](#known-limitations) below.\n\n### Build from source\n\n```bash\ngit clone https://github.com/mentiora-ai/loom\ncd loom\nrustup target add wasm32-wasip2\ncargo build --release\n./target/release/loom postinstall\n```\n\nSource builds skip the vendored WASM artifact and compile the surface from\nscratch, so they need the `wasm32-wasip2` target installed. The `cargo install`\npath uses the vendored bytes and works without it.\n\n### Uninstall\n\nFrom a clone, [`scripts/uninstall.sh`](scripts/uninstall.sh) removes everything\n`loom postinstall` put on disk. It prints a plan and asks before deleting:\n\n```bash\nscripts/uninstall.sh --dry-run   # preview; deletes nothing\nscripts/uninstall.sh             # remove (with confirmation)\n```\n\nIt clears, in order:\n\n- **Binaries** — `loom` (from `~/.cargo/bin` or `~/.local/bin`) and the three\n  auxiliary binaries (`loom-daemon`, `loom-mcp`, `loom-shim-chromium`).\n- **macOS LaunchDaemon** — stops and removes\n  `/Library/LaunchDaemons/com.loom.daemon.plist` (prompts for `sudo`).\n- **Chromium cache + compiled surfaces** — `~/.config/loom` (the pinned\n  ~150 MB Chromium plus the AOT-compiled WASM surfaces and schemas).\n- **Session + blob store** — `~/Library/Application Support/loom` on macOS, or\n  `~/.local/share/loom` on Linux. Pass `--keep-data` to preserve recorded\n  sessions while removing everything else.\n\nIf you installed via Homebrew, also run `brew uninstall loom`. Custom locations\nset through `LOOM_DATA_ROOT` / `LOOM_CHROMIUM_PATH`, and any vault secrets stored\nin the macOS Keychain, are left untouched — the script reports them for manual\nremoval. To do it entirely by hand, delete the four binaries, `~/.config/loom`,\nthe data dir above, and (macOS) the LaunchDaemon plist.\n\n## Client SDKs\n\nThe CLI is the surface most users want, but if you're driving Loom from\nyour own code, language SDKs talk to the same daemon over the same\nJSON-RPC socket.\n\n### TypeScript / JavaScript\n\n```bash\nnpm install @mentiora-ai/loom-sdk\n```\n\n```ts\nimport { Session } from \"@mentiora-ai/loom-sdk\";\n\nconst session = await Session.create();\ntry {\n  const receipt = await session.navigate(\"https://example.com\");\n  console.log(receipt.action_hash);\n} finally {\n  await session.close();\n}\n```\n\nRequires Node ≥ 20 and a running `loom serve`. Full surface (every\n`web.*` verb, vault, replay, export) and connection options are in\n[typescript-sdk/README.md](typescript-sdk/README.md).\n\n### Python\n\nThe Python SDK lives in [python-sdk/](python-sdk/) — same surface as\nthe TypeScript SDK, async-first. It publishes to PyPI as `mentiora-loom`\non the next tagged release (the publish workflow is wired but hasn't run\nagainst a tag yet). Until that lands, install from git:\n\n```bash\npip install \"git+https://github.com/mentiora-ai/loom@v0.9.9#subdirectory=python-sdk\"\n```\n\nOnce the next release is out: `pip install mentiora-loom` (the import name\nstays `loom`).\n\n```python\nimport loom\n\nwith loom.Session.create() as session:\n    session.navigate(\"https://example.com\")\n```\n\n### Rust\n\nThere's no separate published crate ([loom isn't on crates.io](#why-no-cratesio-package)) —\ncall `loom-rpc` directly from a path/git dependency, or shell out to\n`loom action ...` from your Rust code. The crate layout under\n[Architecture → Crate map](#crate-map) tells you which crate provides what.\n\n## 5-minute quickstart\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/assets/session-diff-demo.gif\" width=\"840\"\n       alt=\"loom in ~30s: a real agent browser flow, then what loom adds over a plain browser — deterministic replay you can diff (field_diffs empty, exit 0), a scoped credential vault, WASM isolation, per-session budgets, and typed errors\" /\u003e\n\u003c/p\u003e\n\n\u003e loom in ~30s — a real agent browser flow, then what loom gives you that a plain\n\u003e browser can't: **deterministic replay** you can `diff` (`field_diffs: []`, exit 0),\n\u003e a **scoped credential vault** (the agent never sees your token), **WASM isolation**,\n\u003e **per-session budgets**, and **typed errors**. Per-verb determinism (`Math.random`,\n\u003e `Date.now`) is Stable; full manifest src↔replay equality is Beta.\n\n```bash\n# Start the daemon (foreground; ^C to stop)\nloom serve\n\n# In another terminal: drive a real browser\nSESSION=$(loom session create --profile standard | jq -r .session_id)\nloom action web.navigate --session $SESSION --url https://example.com\nloom action web.evaluate --session $SESSION --expression 'document.title'\nloom session close $SESSION\n\n# Inspect what just happened\nloom session inspect $SESSION\nloom session validate $SESSION   # PASS — hash chain + blob presence verified\n\n# Replay it bit-for-bit\nNEW=$(loom session replay $SESSION | jq -r .session_id)\nloom session diff $SESSION $NEW   # field_diffs: []\n```\n\n## MCP server (Claude Desktop / Cursor / etc.)\n\nAdd to your MCP client config:\n\n```json\n{\n  \"mcpServers\": {\n    \"loom\": {\n      \"command\": \"loom-mcp\",\n      \"args\": [\"serve\"]\n    }\n  }\n}\n```\n\nThe server exposes the `loom.web.*` family. The client doesn't need to\nknow about session ids — `loom-mcp` lazily creates a session on first\ntool call and reuses it across the conversation.\n\n**One agent, one browser.** Each `loom-mcp` process drives a *single* browser\nsession — there is no parallel-browser model over one MCP connection. The\nlazily-created session is reused for every tool call, so concurrent\n`loom.web.*` calls operate on the same page, not separate tabs. If you need\nmultiple browsers at once, run multiple `loom-mcp` processes (one per MCP\nserver entry) or drive the CLI directly — each `loom session create` is\nindependent.\n\n| Tool                       | Args                                                            |\n|----------------------------|-----------------------------------------------------------------|\n| `loom.web.navigate`        | `url: string`                                                   |\n| `loom.web.click`           | `selector: string`                                              |\n| `loom.web.type`            | `selector: string, text: string`                                |\n| `loom.web.select`          | `selector: string, value: string`                               |\n| `loom.web.hover`           | `selector: string`                                              |\n| `loom.web.scroll`          | `selector: string, delta_y?: int`                               |\n| `loom.web.wait`            | `selector: string, timeout_ms?: int`                            |\n| `loom.web.evaluate`        | `expression: string`                                            |\n| `loom.web.set_input_files` | `selector: string, paths: [string]`                             |\n| `loom.web.screenshot`      | `selector?: string`                                             |\n| `loom.web.snapshot`        | (no args)                                                       |\n| `loom.web.set_cookies`     | `source: CookieSource ({inline,cookies}|{grant,grant_id})`      |\n| `loom.web.get_cookies`     | `urls?: [string]`                                               |\n| `loom.web.clear_cookies`   | (no args)                                                       |\n| `loom.web.delete_cookies`  | `name: string, url?: string, domain?: string, path?: string`    |\n\n## Verbs\n\n| Action          | What it does                                                            |\n|-----------------|-------------------------------------------------------------------------|\n| `web.click`     | Click an element by CSS selector.                                       |\n| `web.evaluate`  | Run a JavaScript expression in the page and return the value.           |\n| `web.hover`     | Dispatch a mouseover event at a CSS selector.                           |\n| `web.navigate`  | Load a URL, follow redirects, capture DOM and screenshot.               |\n| `web.screenshot`| Capture a PNG screenshot of the page or a selected element.             |\n| `web.scroll`    | Scroll an element by a (delta_x, delta_y) offset.                       |\n| `web.select`    | Set the value of a `\u003cselect\u003e` element and dispatch `change`.            |\n| `web.set_input_files` | Upload local files into an `\u003cinput type=file\u003e` via CDP `DOM.setFileInputFiles`. Paths gated by the `LOOM_UPLOAD_ROOT` allow-list (fail-closed, canonicalized, ≤20 files / ≤100 MiB each / ≤200 MiB total). |\n| `web.snapshot`  | Capture a full DOM snapshot of the active page.                         |\n| `web.type`      | Focus an input and type text into it.                                   |\n| `web.wait`      | Wait until a CSS selector resolves (or until timeout).                  |\n| `web.set_cookies`    | Inject cookies into the browser's network stack via CDP `Network.setCookies`. Source XOR: inline cookies or vault grant.       |\n| `web.get_cookies`    | Read cookies from the browser's cookie jar (CDP `Network.getCookies`). Operator-facing — receipt carries raw values per D7.    |\n| `web.clear_cookies`  | Clear ALL cookies in the browser's cookie jar. Audit-before-CDP per D9.                                                        |\n| `web.delete_cookies` | Delete a single cookie scoped by `(name, url?, domain?, path?)`. Returns `matched: bool` via getCookies peek before/after.     |\n\nFull per-action signatures (parameters, return shape, examples, typed\nerrors, profile guards) live in [docs/actions.md](docs/actions.md). At the\nCLI you can also run `loom action --help` for the list and `loom action\n\u003cname\u003e --help` for any single action's detailed signature. After\n`loom postinstall` the same content is available offline as `man loom`\nand `man loom-action`.\n\nURL allowlist (for navigate): `http`, `https`, `about:blank`. Profiles:\n\n- `safe` (default) — blocks `window.location` writes + similar\n  destructive evaluate patterns; confines downloads.\n- `standard` — no evaluate denylist; default download dir.\n- `full` — no guards.\n\n## Determinism\n\nEvery session has a `seed: u64` and an `epoch_ms: u64`, both fixed at\nsession-create time. Inside the page:\n\n- `Math.random()` is sfc32 seeded from `seed`.\n- `Date.now()`, `performance.now()`, `performance.timeOrigin` all\n  return `epoch_ms`.\n- `requestAnimationFrame` ticks at 16ms intervals (no real timing).\n- All animations + transitions are 0s.\n\nThe determinism script is injected via `Page.addScriptToEvaluateOnNewDocument`\n*and* explicitly run on the about:blank context, so `web.evaluate` works\ndeterministically even before the first navigate.\n\n`loom session replay \u003cid\u003e` reuses the source session's `seed` + `epoch_ms`\n+ `started_at_ms`, so the replay session's manifest hash chain is bit-equal\nto the source's at every action_receipt entry.\n\n## Security\n\n- Path-traversal-safe session IDs (26 lowercase ASCII alphanumeric chars,\n  ULID format). Anything else is rejected before any `fs::join`.\n- WASM surface isolation: hostile JS in the page can't reach the host\n  process — only the surface module's `host` interface is exposed, and\n  it's a curated set (clock, RNG, blob_put/get, net_request, shim_call).\n- `Browser.setDownloadBehavior(allowAndName, downloadPath=\u003csession-dir\u003e)`\n  for safe-profile sessions, so downloads can't escape the session\n  directory.\n- Vault (OAuth token storage) requires explicit grants tied to a\n  session ID + origin + scopes; tokens never enter the WASM guest's\n  address space.\n\n## Architecture\n\nReplay equality, typed errors, isolation, the Linux secondary build —\nnone of those are features bolted onto a normal browser-driver codebase.\nThey fall out of a small set of architectural commitments that reinforce\neach other. Worth understanding before depending on Loom or contributing\nto it.\n\n### Trust zones at runtime\n\n```mermaid\nflowchart LR\n    A[\"Agent\u003cbr/\u003e(Claude Code · Cursor · SDK)\"]\n    D[\"loom-daemon\u003cbr/\u003e(trusted host)\u003cbr/\u003erpc · host · core\"]\n    W[\"loom-surface-web\u003cbr/\u003e(WASM · sandboxed)\"]\n    S[\"loom-shim-chromium\u003cbr/\u003e(subprocess + supervisor)\"]\n    C[\"Chromium\"]\n\n    A \u003c--\u003e|\"JSON-RPC\u003cbr/\u003e(Unix socket)\"| D\n    D \u003c--\u003e|\"WIT calls\u003cbr/\u003ecurated capabilities\"| W\n    D \u003c--\u003e|\"CBOR pipe\u003cbr/\u003eshim_protocol\"| S\n    S \u003c--\u003e|\"CDP / WebSocket\"| C\n\n    style D fill:#dbeafe,stroke:#1d4ed8,stroke-width:2px\n    style W stroke-dasharray:5 5\n    style S stroke-dasharray:5 5\n    style C stroke-dasharray:5 5\n```\n\nThe daemon (blue) is the only fully trusted process. Everything dashed\nruns at arm's length: the WASM guest gets only the host capabilities\nthe WIT contract grants it, the shim is a separate subprocess under a\nsupervisor with a typed restart budget, and Chromium is, well,\nChromium. A renderer compromise has to traverse two protocol boundaries\n(CDP → shim_protocol, then through `shim_call` via the host) to reach\nanything in the daemon, and never crosses into agent code.\n\n### Six commitments\n\n**1. The session is an append-only, hash-chained WAL of immutable receipts.**\nThere's no mutable session state. Every action emits an `ActionReceipt`\nwhose `prev_hash` points at the previous receipt; the chain anchors back\nto the session's `seed` and `epoch_ms`. Replay just re-executes the action\nlist against the recorded content store; the replay's hash chain is\nbit-equal to the source's *by construction*. `loom session diff` is\nliterally \"compare two manifests.\"\n\n```mermaid\nflowchart LR\n    SEED[\"session_create\u003cbr/\u003eseed · epoch_ms\"]\n    R0[\"Receipt[0]\u003cbr/\u003eprev_hash: 0x00…\u003cbr/\u003eaction: web.navigate\u003cbr/\u003edom_after: h_A\u003cbr/\u003escreenshot: h_B\"]\n    R1[\"Receipt[1]\u003cbr/\u003eprev_hash: H(R0)\u003cbr/\u003eaction: web.click\u003cbr/\u003edom_after: h_C\"]\n    R2[\"Receipt[2]\u003cbr/\u003eprev_hash: H(R1)\u003cbr/\u003eaction: web.evaluate\u003cbr/\u003econsole: h_D\"]\n\n    SEED --\u003e R0 --\u003e R1 --\u003e R2\n\n    subgraph CAS[\"content store · SHA-256 keyed\"]\n        BA[\"h_A · DOM\"]\n        BB[\"h_B · PNG\"]\n        BC[\"h_C · DOM\"]\n        BD[\"h_D · JSON\"]\n    end\n\n    R0 -.-\u003e BA\n    R0 -.-\u003e BB\n    R1 -.-\u003e BC\n    R2 -.-\u003e BD\n```\n\n**2. Every artifact is content-addressed by SHA-256.** DOM snapshots,\nscreenshots, exported tarballs — keyed by content hash, stored in one\nCAS, referenced from the manifest by hash. Replay equality, artifact\ndeduplication, and `loom gc`'s reference protection are all the same\nmechanism, not three subsystems that have to agree.\n\n**3. Errors are part of the wire schema, not strings.** `LoomErrorCode`\nis a stable kebab-case enum (`session-not-found`, `wait-predicate-false`,\n`budget-exceeded`, `replay-divergence`, ~25 codes total) shared across\nevery crate, every process boundary, and every SDK. Adding a code is\nSemVer-minor; removing one is major. A linter walks every\n`LoomError::` constructor and asserts the variant is in the documented\n`errors.json` schema, so an emitter can't invent a new error string\nwithout it surfacing in the wire contract.\n\n**4. Untrusted code runs out-of-process or in WASM — never in the daemon.**\nChromium runs in `loom-shim-chromium`, a separate process speaking\nCBOR-framed `shim_protocol` over a pipe; the daemon never imports CDP\nand a renderer crash can't take it down. The `web.*` surface guest is a\nWASM cdylib loaded as a wasmtime component, with a curated host\ninterface (clock, RNG, `blob_put/get`, `net_request`, `shim_call`) — a\nhostile page that compromises the renderer can't reach the host's\nfilesystem or network without going through that interface. The\nshim itself runs under a supervisor with a typed restart budget; an\nexhausted budget surfaces as `kind: \"shim-failure\"`, not a hang.\n\n**5. The core is platform-agnostic.** `loom-core` (sessions, manifest,\nreplay, vault, content store) imports zero macOS or Linux symbols.\nAnything platform-specific lives behind a stable seam in a sibling\ncrate: `loom-keychain` for the OS keychain, `loom-shims` for the\nChromium driver process, `loom-surfaces` for verb implementations.\nThis is why the Linux x86_64/arm64 build doesn't fork — it's the same\ncore with one fewer adapter linked. It's also why a hypothetical\nfuture `native.*` (macOS apps), `shell.*`, or `fs.*` surface would\nbe a new shim + WASM guest pair against `loom-host`'s WIT contract,\nnot a `loom-core` rewrite.\n\n**6. The action registry is the single source of truth.** Every `web.*`\naction is declared once in [`loom-rpc/src/action_registry/action_registry.rs`](loom-rpc/src/action_registry/action_registry.rs).\nDocs (`docs/actions.md`, the man pages, CLI `--help`) and the JSON-RPC\nrouter both derive from it. A unit test in\n[`interface_tests.rs`](loom-rpc/src/action_registry/interface_tests.rs)\nasserts the registry's required-param set equals the router's, and a\nCI gate fails any PR where the generated docs are out of sync — so\ndispatch, documentation, and the CLI surface can't drift from each\nother even by accident.\n\n### Crate map\n\n| Crate                | What lives here                                                |\n|----------------------|----------------------------------------------------------------|\n| `loom-shared`        | Wire-protocol types (CBOR), session IDs, `shim_protocol`, `LoomError` |\n| `loom-keychain`      | Platform keychain access (macOS Keychain Services)             |\n| `loom-core`          | Session state, manifest WAL, content store, vault, replay engine |\n| `loom-host`          | wasmtime runtime, WIT bindings, `host_function_table`          |\n| `loom-rpc`           | JSON-RPC dispatch, schema validator, request router, action registry |\n| `loom-cli`           | `loom` binary — CLI commands + RPC client                      |\n| `loom-daemon`        | `loom-daemon` binary — long-lived daemon that owns sessions    |\n| `loom-mcp`           | `loom-mcp` binary — MCP server (stdio transport)               |\n| `loom-surfaces`      | Cross-target surface verb implementations                      |\n| `loom-shims`         | `loom-shim-chromium` binary + supervisor — out-of-process Chromium driver |\n| `loom-surface-web`   | WASM cdylib — the `web.*` surface guest                        |\n\nLayout convention: each module's source is `\u003cmodule\u003e/\u003cmodule\u003e.rs`, glue\nlives in `\u003cmodule\u003e/mod.rs`, tests live in `interface_tests.rs`. This\nisn't aesthetic — it's why `grep -r \"fn dispatch_action\"` lands at one\nfile, not eleven.\n\n## Status\n\nloom is **0.9.9** — pre-1.0. The matrix below is the stability\ncontract: breaking changes to **Stable** rows bump the major version\nwhen 1.0 ships; **Beta** rows may change without notice.\n\n| Surface | Status | Notes |\n|---|---|---|\n| Receipt schema (`ActionReceipt`, `SessionManifest` wire format) | **Stable** | Hash chain + canonical bytes frozen. Breaking changes bump major. |\n| Action / blob store (content-addressed SHA-256) | **Stable** | On-disk layout frozen; `loom gc` reference protection covers it. |\n| Determinism harness (`Math.random`, `Date.now`, `performance.now`) | **Stable** | Seeded at session-create; reproduced bit-for-bit on replay. |\n| Deterministic replay (manifest hash-chain bit-equality src ↔ replay) | **Beta** | Source/replay equality is not yet bulletproof — gated on real-Chromium subprocess wiring. |\n| `web.navigate`, `web.evaluate`, `web.wait`, `web.type` | **Stable** | Covered by replay-equality tests. |\n| `web.click` | **Beta** | DOM coordinate edge cases — gated on the hit-test refinements still in progress. |\n| `web.set_input_files` | **Beta** | New in 0.9.8. CDP `DOM.setFileInputFiles` behind the `LOOM_UPLOAD_ROOT` allow-list (fail-closed). Real-Chromium FileList coverage via the e2e harness. \u003c!-- version-check-ignore --\u003e |\n| `loom-mcp` server (implicit session, tool surface) | **Stable** | Hardened in 0.9.0 (path-traversal-safe IDs, typed errors, lazy session). \u003c!-- version-check-ignore --\u003e |\n| CLI surface (`loom session`, `loom action`, `loom export`, `loom import`) | **Stable** | Flags pinned. `--version` format pinned: `loom \u003cver\u003e (\u003csha\u003e \u003cdate\u003e)`. |\n| `import.playwright` RPC | **Stable** | End-to-end wired through facade, adapter, handlers, router. |\n| `request.cancel`, `session.kill`, `daemon.health` RPCs | **Beta** | Daemon-stall fix; structured concurrency via `SessionScope` + per-request 30s watchdog (`LOOM_REQUEST_TIMEOUT_MS`). Wire shape may evolve based on consumer feedback. |\n\n**1.0 promotion criteria:** real-Chromium subprocess wiring + the `web.click` hit-test refinements land, matrix CI\ngreen across the four release targets, no Beta rows remaining.\n\n### Known limitations\n\n- macOS arm64/x86 + linux x86/arm64 only. Windows isn't tested.\n- macOS binaries are not notarized. First run may need\n  `xattr -d com.apple.quarantine $(which loom-shim-chromium)` —\n  see the install section above. Notarization is a follow-up.\n- Chromium pinned at version 132 (Playwright build 1153). Newer\n  Chromium revisions may require a `chromium_pin.rs` update.\n- `loom-mcp`'s implicit session is single-session-per-process. Power\n  users who need multiple parallel browsers per MCP connection should\n  use the CLI directly.\n- `loom postinstall` requires network access to fetch Chromium (and,\n  for `cargo install` users, the auxiliary loom binaries). Air-gapped\n  installs work via the manual-download tarball, which bundles all four\n  binaries — only Chromium needs to be vendored separately on those hosts.\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md). Short version: clone,\n`cargo test --workspace -- --test-threads=1`, send a PR with a typed\nerror mode for any new failure path.\n\n### Keeping docs in sync\n\nThe action surface (`web.click`, `web.evaluate`, …) is documented from a\ncanonical Rust registry at\n[loom-rpc/src/action_registry/action_registry.rs](loom-rpc/src/action_registry/action_registry.rs).\nEdits to the registry — adding actions, renaming params, expanding\ndescriptions — must be paired with regenerated docs:\n\n```bash\njust gen-docs              # or: cargo run --example gen-docs -p loom-cli\n```\n\nThe CI gate fails any PR that desyncs `docs/actions.md` or the\n`docs/loom*.1` man pages from the registry. A unit test in\n[loom-rpc/src/action_registry/interface_tests.rs](loom-rpc/src/action_registry/interface_tests.rs)\nalso asserts the registry's required-param set equals the JSON-RPC\nrouter's, so the registry and the dispatch path can't drift either.\n\n## License\n\nDual-licensed under MIT or Apache-2.0 at your option. See\n[LICENSE-MIT](LICENSE-MIT) and [LICENSE-APACHE](LICENSE-APACHE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmentiora-ai%2Floom","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmentiora-ai%2Floom","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmentiora-ai%2Floom/lists"}