Commit Graph
4 Commits
Author SHA1 Message Date
Sander Spilleman 9ae36764da feat(stealth): many stealth improvements 2026-06-03 08:21:14 +00:00
SGavrl fd2421940b Perf/cdp fetch cache and cookie alloc (#216)
* perf: cache HTTP clients + selector parsing + exponential wait backoff

Three low-risk wins discovered while reading the hot paths.

1. op_fetch_url cached its reqwest::Client. Previously every JS-side
   fetch(), XHR, and dynamic script load built a fresh client, which
   re-loaded TLS roots and started with an empty connection pool. On
   an asset-heavy page with 30+ subresources this added ~150ms of
   wasted setup with zero connection reuse. The cache is a process-
   wide map keyed by proxy URL (empty string for no-proxy), so the
   first fetch on each proxy pays the build cost once and every
   subsequent fetch reuses the same pool.
   Also bumped pool_idle_timeout to 5 minutes and tcp_keepalive to
   60s so connections stay warm for SPA-heavy workloads that hit
   the same origin dozens of times across a navigation.

2. DomTree::parse_selector now caches parsed selectors in a thread-
   local HashMap (cap 256). querySelector / querySelectorAll re-
   parsed the selector string every call before; batch DOM access
   patterns (an agent scraping a table row-by-row, a framework
   polling for elements) paid the cssparser cost N times.

3. browser_wait_for / browser_wait_for_text now exponentially back
   off (5 -> 10 -> 20 -> ... -> 200 ms). The old fixed 200ms tick
   meant a selector that appeared 30ms after the call still took
   the next full 200ms tick to be detected. Worst case is still
   200ms per check, but the median wait drops sharply for fast
   appearances.

28/28 integration tests pass. Asset-heavy bench (rust-lang/rust,
docs.stripe.com, etc) doesn't regress.

* perf: module loader client cache, fetch concurrency cap, snapshot hide list, cookie alloc

Round two of perf wins after a deep code audit.

1. ES module loader (module_loader.rs) was building a fresh
   reqwest::Client on every dynamic import. Modern SPAs (Next, Vite)
   load 20-50 chunks via dynamic import; each one used to spin up an
   empty connection pool with fresh TLS init. Now reuses the
   process-wide FETCH_CLIENT_CACHE that op_fetch_url already uses
   (the cached_request_client helper). Saves ~30-100ms on chunk-
   heavy first navigations.

2. Script and CSS fetches in execute_scripts used futures::join_all
   with unbounded concurrency. A 100-script page would open 100 TCP
   sockets at once, exhausting the connection pool and the OS
   ephemeral port range. Capped at 16 in-flight (well above the per-
   host pool ceiling real Chrome uses).

3. bootstrap.js __obscura_init() ran Object.keys(globalThis).filter()
   on every page setup to find internals to hide. The set of keys
   is static at snapshot-build time, so we now precompute it once
   (top-level statement just below the function) and __obscura_init
   just iterates the precomputed array. Was 5-40ms per page on heavy
   SPAs (1000+ globals); now ~0.2ms per page.

4. CookieJar::domain_matches allocated 2 lowercase Strings and a
   format! per call. The lookup runs per fetch per cookie domain;
   on a heavily-cookied site with 100 cookies across 30 domains
   that was thousands of allocations per page. Now zero-alloc with
   eq_ignore_ascii_case and char-boundary checks.
2026-06-01 00:00:22 +02:00
SGavrl 171dba23e7 Feat/mcp agent skill tier1 (#215)
* feat(mcp): 10 new agent tools + element refs (12 -> 22 tools)

Brings obscura-mcp from a Playwright-style core (navigate/click/fill/
type/snapshot/wait_for/evaluate/select/press_key/network_requests/
console_messages/close) up to a full agent surface.

New tools:
- browser_markdown: render the page as Markdown (reuses the
  HTML_TO_MARKDOWN_JS converter behind `fetch --dump markdown`).
  Token-dense alternative to browser_snapshot.
- browser_links: every <a href> with anchor text, JSON-per-line.
  Optional internal_only and limit args.
- browser_interactive_elements: list every clickable / typeable
  element with a stable ref (e1, e2, ...) and a one-line label.
- browser_back / browser_forward / browser_reload: history nav,
  uses the Page history stack from the engine-tier1 commits.
- browser_get_cookies / browser_set_cookie / browser_clear_cookies:
  full cookie jar ops so an agent can skip a login flow when it
  already has a session token.
- browser_wait_for_text: wait for a substring to appear anywhere
  in the visible page text. Complements browser_wait_for which
  only checks for selectors.

Element refs (the playwright-mcp pattern, agent UX win):
- BrowserState now keeps an interactive_refs HashMap built on each
  browser_snapshot / browser_interactive_elements call.
- Each interactive element gets a data-obscura-ref="eN" attribute
  written into the DOM so refs survive across calls.
- browser_click / browser_fill / browser_type accept `ref` (preferred)
  or `selector` (fallback). Removes the agent's 'guess a CSS
  selector then retry' loop.
- Refs are cleared on every navigation / reload / back / forward / close.

Output size limits:
- browser_snapshot, browser_markdown accept max_chars (default 4000).
- browser_links, browser_interactive_elements accept limit (default 100).
- Truncated responses say '...(truncated, N more chars)' so the agent
  can ask for more if needed.

Verified end-to-end via /tmp/mcp-test.sh: tools/list returns 22 tools,
ref-based click on a real element works, markdown converts H1+P+UL
correctly, cookies round-trip. 28/28 integration tests pass.

* feat(mcp): 10 more agent tools, multi-tab, structured extract (22 -> 32 tools)

Tier 2: closes the gaps in agentic flows the Tier 1 surface still
needed multi-call workarounds for (login forms, paginated lists,
multi-step research across multiple destinations).

Multi-tab:
- BrowserState now holds a BTreeMap of tabs (Page per tab) with an
  active_tab cursor. page_mut() auto-creates a default tab so every
  legacy single-tab tool keeps working.
- browser_tab_new: open and optionally navigate.
- browser_tab_list: list all open tabs with active marker.
- browser_tab_switch: change which tab subsequent tools operate on.
- browser_tab_close: close (default: active tab), promotes another.

Form ergonomics:
- browser_detect_forms: returns each <form> with action, method, and
  every field's tag, type, name, current value, label (from <label for>
  or aria-label or placeholder), and option list for selects. Agents
  call this once to see the whole form shape instead of guessing
  selectors field by field.
- browser_fill_form: fill N inputs in one call. Each entry chooses
  type='text' (default value-set), 'check'/'uncheck', or 'select'
  (matches by value OR visible text). Optional submit_ref clicks
  after filling.

Structured extraction:
- browser_extract: pass {field: 'selector'} and get a JSON object back.
  Selector suffix '@attr' reads attribute (e.g. 'a@href'). Field
  suffix '[]' returns an array (queries all matches). One call returns
  the whole structured object instead of an evaluate per field.
- browser_count: existence / pagination probe.
- browser_get_attribute: read href / src / value / data-* etc.

Page control:
- browser_scroll: 'top'|'bottom'|'up'|'down'|'left'|'right' with
  optional amount, or scroll an element into view. Dispatches a real
  'scroll' event on window AND document so infinite-scroll handlers
  fire even though our viewport isn't a real layout.

Other:
- Bumped #![recursion_limit = "512"] because the tools/list JSON
  literal is now too big for the default macro recursion limit.
- browser_count: coerce f64 -> u64 since V8 numbers come back as floats.

Verified end-to-end: 32 tools registered, multi-tab open/list/switch/
close all work, detect_forms returns proper JSON, fill_form fills
text/check/select in one call, extract returns the structured object
with arrays and attribute reads. 28/28 integration tests pass.

* feat(mcp): search + storage state export/restore (32 -> 35 tools)

Three additions that close the long-tail agentic flows:

- browser_search: substring search in visible page text, returns each
  match with N chars of surrounding context (default 80). Lets an
  agent confirm content exists before scraping and locate a section
  without pulling the full page into its window. case_sensitive +
  limit + context_chars args.

- browser_storage_state: export the full auth surface (cookies +
  localStorage + sessionStorage for the current origin) as one JSON
  object. Agents stash this between runs to skip a login flow.

- browser_set_storage_state: apply a previously-exported state. Sets
  cookies via the cookie jar; writes localStorage / sessionStorage
  via evaluate on the current page (so a navigate to the right origin
  must come first).

Tool count: 35. 28/28 integration tests pass.
2026-05-31 23:21:07 +02:00
Jordi Llonch bce055fd2a MCP support (#65)
* add mcp support

* add MCP docs to README

* wire up mcp subcommand in cli

* fix(stealth): propagate stealth feature to obscura-mcp
2026-05-13 21:38:10 +00:00