strawmanus
by painbaba
README.md
# strawmanus
A browser-control agent that fuses **Manus's action layer** with **Strawberry's session, selector and reuse layer**.
Manus has the better *hands*: a flat 12-tool browser API, coordinate fallback when element detection fails, a JS console escape hatch, markdown-first perception, and a strict one-tool-per-iteration loop. Strawberry (Dendrite Systems) has the better *body and memory*: it runs in your real logged-in browser, resolves elements from natural language and caches the result so it self-heals when the site changes, saves successful tasks as replayable skills, and asks before doing anything irreversible.
Neither works alone. Manus re-pays the full model cost on every repeat of a task and, running in a disposable Linux sandbox, structurally cannot get past a login wall. Strawberry is closed and exposes no primitives to program against. `strawmanus` is the combination: Manus's action space, driven inside your own Chrome, with every element resolution cached.
---
## Install
```bash
pip install -r requirements.txt && playwright install chromium
```
`observe` works with no API key. Everything model-driven needs one:
```bash
export ANTHROPIC_API_KEY=sk-ant-...
```
Then check the environment:
```bash
python -m strawmanus doctor
```
## Use
```bash
# See exactly what the agent sees — no API key, no model call
python -m strawmanus observe --url https://github.com/trending
# Run a task
python -m strawmanus run "top 5 trending TypeScript repos today, 2 sources per fact" --expect 5
# Log in once by hand; later headless runs inherit the session
python -m strawmanus auth --url linkedin.com
# Save a successful run as a skill, then replay it with new arguments
python -m strawmanus run "search for Acme Corp" --save-skill find-co --param company="Acme Corp"
python -m strawmanus replay find-co --arg company="Globex Inc"
# Fan subtasks across tabs (they share the logged-in browser)
python -m strawmanus companions "price Linear" "price ClickUp" "price Asana"
# Score against the seat-free Strawberry benchmarks
python -m strawmanus bench --tasks B9,B12 --judge --csv scores.csv
```
Global flags: `--profile {cloned,real,fresh,attach}` · `--headless` · `--max-steps N` · `--auto-approve` · `--allow DOMAIN` · `--parallel N` · `--cdp URL`.
State lives in `~/.strawmanus/`: `sm.db` (selector cache, memory, run log), `skills/`, `runs/<stamp>-<slug>/{report.md,trace.json,guard.json}`, `profile/`.
---
## How it works
### 1. Perception is three layers in one payload
```
[page] url=https://github.com/trending | title='Trending repositories on GitHub today · GitHub' | viewport 1/3 | tabs=1
[interactive] (viewport only; use index, or coordinate_x/coordinate_y for anything missing)
0[:]<a>Homepage</a>
1[:]<button>Platform</button>
...
52[:]<a>obra / superpowers</a>
53[:]<a>275,441</a>
60[:]<a>You must be signed in to star a repository</a>
[markdown] (whole page, links/images stripped — if this answers the question, do not scroll)
# Trending
## RyanCodrai / turbovec
A vector index built on TurboQuant, written in Rust with Python bindings
348 stars today
...
```
Interactive elements are the viewport only, in Manus's exact `index[:]<tag>text</tag>` format, hit-tested with `elementFromPoint` so occluded controls are dropped. The markdown covers the **whole document**, including below the fold — which is the real cost lever, because most read-only questions get answered without a single scroll. Screenshots are opt-in, for when the DOM is genuinely ambiguous.
Password, OTP and card fields are indexed so they can be filled, but their text and value never enter the model's context:
```
4[:]<input type=password><redacted secret field></input>
```
### 2. Manus's 12 tools, verbatim — plus 5
`browser_view` · `browser_navigate` · `browser_restart` · `browser_click` · `browser_input` · `browser_move_mouse` · `browser_press_key` · `browser_select_option` · `browser_scroll_up` · `browser_scroll_down` · `browser_console_exec` · `browser_console_view`
The names are kept exactly because models already generalize to them, and Manus's published behavioural rules then transfer directly. Added on top:
| Tool | Why |
|---|---|
| `browser_find(description)` → index | Natural-language selector, cache-first |
| `browser_wait_for(description)` | Semantic waits instead of sleep loops |
| `browser_extract(description, schema)` | Typed extraction that auto-files its source URL |
| `browser_tabs(action, target)` | list / open / switch / close |
| `browser_takeover(reason)` | 2FA, captcha, payment — hand the keyboard back |
Click escalates rather than dead-ends: index → coordinates from the snapshot bbox → `browser_console_exec` → `browser_takeover`. Three consecutive failures on a page inject the escalation ladder into the loop.
### 3. The selector cache is the biggest win
`(domain, normalized description)` → `{selector, fingerprint{tag, role, text_hash}, hits, misses}` in SQLite.
Resolution order: **cache hit → verify the fingerprint → use it**, at zero model calls. On mismatch or miss, the model picks an index from the element listing and the cache is rewritten — Dendrite's self-heal, which Manus has no equivalent of. A replayed skill on a healthy page costs **no model calls at all**; `strawmanus cache` shows what has accumulated.
### 4. Real browser first
Default is a persistent Chrome profile `strawmanus` owns — real cookies, login walls already solved, no credentials stored anywhere. `--profile real` uses your actual profile (Chrome must be closed), `--profile attach` connects to a Chrome you started yourself on `--remote-debugging-port=9222`, `--profile fresh` gives a clean context for reproducible benchmarking. `strawmanus auth --url <site>` is the Dendrite pattern: you log in by hand once, the storage state is saved for later headless runs.
### 5. Nothing irreversible happens silently
Every action is classified **read** / **soft-write** / **irreversible** before dispatch. Send, submit, pay, delete, post, apply, subscribe, transfer, merge, push — and Enter pressed in a compose field — hold for confirmation with a preview of the target and payload:
```
⚠ irreversible action held for confirmation
what: click 'Send invoice' on x.com
why: click target text contains 'send'
approve? [y/N]
```
`--auto-approve` only ever applies to domains passed with `--allow`. Held actions are listed in the report under "Held for your approval (not performed)", so a blocked step is visible rather than silently missing. Every decision is written to `guard.json`.
### 6. A fact with no source cannot be printed
Strawberry's benchmark rubric is enforced at runtime instead of graded afterwards. Facts enter a ledger with the URL they were actually read from; a search-engine result page is rejected as a source and the model is told to open the real page. Confidence is mechanical — High (2+ official sources), Medium (1), Low (inferred) — and `report.render()` **raises** rather than emit a zero-source claim. Reports close with a fixed status line, blockers, telemetry, and the agent's self-rated autonomy:
```
**Status:** Complete | **Entities Found:** 5/5 | **Duration:** 6.2 min
```
### 7. Skills and companions
A successful trace is distilled into a parameterized skill: noise tools dropped, literal argument values replaced with `{{param}}`, element text kept so each step can be relocated on replay. Companions fan subtasks across tabs behind a semaphore, each with its own event stream, sharing the one logged-in browser.
---
## Hand the browser to Claude directly
Everything above drives the agent from the CLI. `serve-mcp` inverts it: the browser
becomes a tool Claude holds in conversation, one persistent Chrome that stays open
across turns, driven tool call by tool call with the reasoning happening in the chat
rather than in a scripted loop.
```bash
claude mcp add strawmanus -- python -m strawmanus serve-mcp
```
Hand-rolled JSON-RPC 2.0 over stdio, no extra dependency. All 17 browser tools plus 9
trading tools. Chrome starts on the first tool call, not at registration, so adding the
server does not pop a window. `browser_takeover` returns immediately rather than
blocking on `input()` — stdin is the protocol channel here, so blocking would deadlock
the server; instead the model stops and asks you in conversation.
The trading tools target Olymp Trade and come with an arithmetic layer, a journal that
records the reasoning behind every trade, and risk limits enforced in code *below* the
reasoning layer, so a refusal is final. The design assumption is that "no edge" is the
likely answer, and the demo-evidence gate exists so that answer is reachable: real
money requires the **lower** bound of the 95% Wilson interval to clear break-even, not
the point estimate. A 120-trade demo record at a 61.7% win rate, up 146.80, still
reports `edge proven NO`.
**[TRADING.md](TRADING.md)** has the full picture, including the ToS and structural
risks you should read before pointing this at real money.
---
## Benchmarks
`bench/tasks.json` carries the 9 tasks from Strawberry's published B1–B12 spec that need no paid seat (B6–B8 want LinkedIn Sales Navigator / CRM / ATS access), scored on its rubric — Data Accuracy 35, Source Quality 25, Completeness 25, Insight 10, Speed 5 — minus a penalty table topped by −20 per invented data point.
Two passes run per benchmark. The **mechanical** pass needs no model and catches what actually goes wrong: unsourced facts, single-sourced facts, entity shortfalls, overruns against the time estimate, hedged numeric language. The `--judge` pass then grades with a separate model call that sees only the emitted markdown — the agent never grades its own homework.
## Tests
```bash
python tests/test_core.py # 33 offline: no browser, no network, no API key
python tests/test_live.py # 6 end-to-end: real headless Chrome, scripted stub model
```
The live suite runs the whole stack against a real DOM with the model replaced by a script, which is what makes the invariants checkable without a key: the snapshot JS against real HTML, the guard refusing `Send invoice` while allowing `Show details` (asserted on the page's own state, not the return value), the cache going cold→warm with the model-call count staying flat, one tool call per iteration, a snippet source rejected and reported, and a skill replayed with a new argument.
## Layout
```
strawmanus/
cli.py config.py loop.py llm.py
prompts/system.md # agent loop, browser, information, safety, deliverable rules
browser/
session.py # CDP attach / persistent profile / fresh context, tabs, storage state
observe.py # the three-layer observation
actions.py # Manus's 12 + 5, on Playwright
selectors.py # NL selector → cache → fingerprint verify → self-heal
guard.py # risk classifier, confirm gate, secret redaction
memory/
store.py # SQLite: prefs, per-domain quirks, run log
skills.py # trace → parameterized skill; replay with relocation
research/
sourcing.py # citation ledger
report.py # the deliverable contract
bench/
tasks.json judge.py
```
## Not built yet
Triggers (firing a skill from a Gmail label or a GitHub PR), MCP connectors, and Manus's file tools are designed for but unimplemented. The model-driven loop has been exercised end-to-end against a scripted stub, not yet against a live API key.