Grip-browser

Latest version: v0.8.2

Safety actively analyzes 993455 Python packages for vulnerabilities to keep your Python projects secure.

Scan your dependencies

Page 1 of 2

0.8.2

Packaging only. No change to any code path: `grip/` is byte-identical to

0.8.1

Measurement release. Two things the site called "unmeasured" got measured,
and the measuring found real bugs.

Fixed

- **The slider solver never worked.** `SLIDER_PROBE_JS` resolved the track
with `handle.closest('[class*="slider"]')`, which matched the handle
itself (its own class contains "slider"), so the drag distance collapsed
to roughly zero and every attempt timed out — including on markup
matching grip's own documented geetest example. The track is now resolved
from a genuine ancestor wider than the handle, and bails with a stated
reason instead of silently dragging nothing. Benchmarked 0/5 → 5/5.
- **Two challenge classifier false positives**, both deterministic: ordinary
prose containing "solve a captcha puzzle" classified as a challenge, and a
documentation page merely quoting the `cf-turnstile` embed snippet as text
classified as Turnstile. False-positive rate on the negative fixtures went
10/40 → 0/40.
- **The stealth user agent was pinned to a Chrome version that was not
running.** `_STEALTH_UA` hardcoded `Chrome/149` while the launched binary
was 151 — self-inconsistent, and worse with every Chrome release. It is
now derived from `Browser.getVersion()` at runtime, applied via
`Network.setUserAgentOverride` so it fixes the outgoing request header and
not only `navigator.userAgent`, and re-applied to popup targets.

Added

- **`benchmarks/bench_challenges.py`** with 26 local fixtures: measures
classification accuracy (including negatives, so false positives count),
solve rate, and whether a "solved" claim is ever false. Across 30 solve
runs, zero false-solved — the "reports solved only when verified" claim
holds up.
- **`benchmarks/bench_stealth_signals.py`**, which reads bot.sannysoft's own
57-row result table rather than regexing page text.

Measured

On real Chrome for Testing 151, macOS arm64, `--headless=new`: 5 of 57
sannysoft signals failed with stealth off, 0 of 57 with it on. All five were
user-agent related. Every other commonly-cited leak — plugins, mimeTypes,
languages, WebGL vendor/renderer, permissions consistency, window.chrome,
screen dimensions — already passed before any change, on this host. No
page-world JS shims were added: patching signals that already pass is how
you manufacture a new tell.

TLS/JA3 was never a gap. grip drives real Chromium, so the handshake is
Chromium's own.

Still true

`navigator.userAgentData` is left undefined under UA override rather than
fabricated. A page that detects the DevTools session itself is below
anything an injected script can reach — unfixable for any CDP-based tool.
IP reputation remains an egress problem. Challenge solve rates outside
these synthetic fixtures remain unmeasured, and Cloudflare's public test
sitekeys short-circuit the real widget flow, so production Turnstile's
click path is still untested here.

0.8.0

Three audits of the agent-facing surface (ergonomics, browser capability
coverage, robustness), then the fixes. Most of these are defects an
autonomous agent hits and a human driver routes around.

Security

- **Typed passwords no longer reach snapshot text, the model, or trace
output.** `gripOwnText` fell through to `el.value` for password inputs.
The value-capture path and the accessible-text path now share one
exclusion set, so the two cannot drift apart.
- **Page-authored element handles can no longer collide with grip's.**
A page could pre-author `data-grip-h` to shadow a live handle or break
selector resolution; handles are only trusted if grip minted them this
session, and selectors go through `CSS.escape`.
- **Downloads landing outside the configured directory are dropped.**

Fixed — actions that reported success while doing nothing

- **`click()` returned ok on disabled, off-screen and overlay-covered
elements.** It now hit-tests at the element centre and names the
occluding element when something is in the way.
- **`type()` bypassed React/Vue value trackers** (raw `el.value` assignment,
no key events, so typeahead never fired) and still reported success. It
now uses the native value setter, brackets the write with key events, and
verifies the value took.
- **Every action snapshotted before the page settled**, so a click that
navigated returned the pre-click page and the agent clicked again.
- **`CONTENT` was truncated at 2000 characters with no marker**, so the
agent believed it had read the whole page.
- **`page_error` and `prompt_injection` were computed and then dropped** in
rendering: an agent blocked by an anti-bot wall or auth wall was told
nothing at all.

Fixed — wrong element, wrong conclusion

- **`click("Save")` could hit "Save draft".** Exact match now wins outright;
genuine ambiguity raises `AMBIGUOUS_TARGET` listing the candidates instead
of silently guessing.
- **A stale ref from a previous document silently resolved to a different
element.** Refs from a superseded snapshot are now rejected with a
re-snapshot hint.
- **Failed navigations returned as success.** `Page.navigate`'s `errorText`
was ignored and the load timeout was swallowed, so DNS failures and
connection refusals surfaced as a loaded page with status 0.
- **A browser crash was re-classified by string match into
`ELEMENT_NOT_FOUND`**, looping the agent straight back into a dead
connection. Typed errors are no longer re-classified.

Added — state and content agents could not see

- **Element state in the snapshot**: `disabled`, `required`, `checked`,
`selected` and `value` (password values excluded). Verifying that a box
is ticked or a submit button is enabled is most of task verification.
- **Inputs labelled only by sibling text are now addressable by label**,
via an inference chain (label-for/wrapping label, `aria-label`,
placeholder, title, sibling text, humanized name/id).
- **Scroll position and page height**, plus `scroll()` targeting the
nearest scrollable ancestor rather than only the window, so inner panes,
virtual lists and infinite scroll can be reached.
- **Closed shadow roots**, via an `attachShadow` patch installed before
navigation. They were previously invisible with no signal.
- **iframes** surfaced as rows (no cross-frame traversal yet), **canvas**
rects and **labelled SVG**, and **comboboxes** with their options.

Added — capabilities

- **JavaScript dialogs are handled by policy.** Nothing subscribed to
`Page.javascriptDialogOpening`, so an `alert()`/`confirm()` froze the tab
until timeout.
- **`wait_for()`** for a text/ref condition, and same-document navigation
now invalidates the cached snapshot, so `pushState` no longer leaves a
stale one behind.
- **`hover()`**, for menus that only open on pointer events.
- **`select()` falls back to open, re-snapshot, pick** for non-native
comboboxes instead of failing outright.
- **Conservative cookie-banner dismissal**, once per navigation.
- **File-chooser interception**, for drop zones that create the input on
click, and **popup adoption** for OAuth flows.
- **Viewport and device emulation**, and **permission control** with
notifications and geolocation denied by default so a prompt cannot stall
a task.

Fixed — MCP

- **The server died on every tool call when an LLM SDK was not installed.**
The adapter was resolved eagerly at browser construction, so `open` and
`click` — which need no model — failed with an `ImportError` on any host
that sets provider keys without grip's optional extras. It is now
resolved lazily, only for the `run` tool.
- **`press`, `upload`, `links`, `popups_blocked`, `wait_for`, `hover` and
`scroll` exposed**; several existed in the Python API but not over MCP.
- **Error recovery hints now reach the client** instead of being dropped at
the MCP boundary.
- **`screenshot` returns an image block** rather than raw base64 as tool
text.
- **Overlapping tool calls could act on the wrong tab**; calls are
serialized.

Fixed — robustness

- **The Chrome process and temp profile leaked whenever `kill()` timed
out**, and on any teardown path that was not an explicit `close()`.
- **CDP commands had a fixed 30s timeout with no override.**
- **Crashes and socket drops surfaced as a generic `ConnectionError`**;
both now map to a typed `BROWSER_CRASHED`.
- **`Fetch.enable` paused every subresource** to enforce a policy that only
concerns navigations.
- **The trace grew unbounded** on a long-lived server.

Still not done

Cross-origin iframe traversal (iframes are surfaced, not entered), TLS/JA3
fingerprint parity (below the DevTools Protocol, unreachable from a Python
client), and challenge solve rates remain unmeasured. `enable_downloads` is
deliberately not exposed over MCP: it would let a client create a directory
at an arbitrary server-side path.

0.7.0

Added

- **Non-semantic clickable element discovery.** Elements with a JS click
listener but no interactive tag or ARIA role (e.g. a `<div>` with
`addEventListener('click')`) are now found and clickable by description.
Detection uses CDP `DOMDebugger.getEventListeners`, bounded by a cheap
scoring pass (`PRE_RANK_LIMIT=150`) with listener probes only on survivors
(`MAX_LISTENER_PROBE_NODES=40`), issued concurrently. Measured snapshot
latency cost: Wikipedia 8.5ms → 10.7ms, Hacker News 9.1ms → 12.5ms, no
token-count change on pages without such elements.
- **`allow_popups` on `NavigationPolicy`** (default `False` — existing secure
behaviour unchanged). Opting in disables popup blocking; the popup's CDP
target has no Fetch interception, so `NavigationPolicy` is not enforced
inside it.

Changed

- **Popup blocking is now an explicit `NavigationPolicy` option, not an
unreviewed side effect of v0.6.0.** `window.open()`/`target="_blank"` still
fails under the default policy — that has not changed — but it is now a
documented choice (`NavigationPolicy(allow_popups=True)` / `Browser(...,
allow_popups=True)`) rather than a silent one.
- A blocked popup is no longer silent. It now logs a `WARNING` naming the URL
and the flag, is counted on `Page.popups_blocked`, and each block is
recorded as a `"popup_blocked"` entry in `Page`'s `Trace`.

Known limitations

- Click listeners attached to an ancestor (event delegation) are not
detected; `getEventListeners` at the node cannot see them.
- `click_at` remains unregistered as an agent tool by design — it takes
coordinates, which a model without vision cannot usefully produce.

0.6.0

Security

- **`NavigationPolicy` was enforced only in `Browser.open()`.** `Page.goto()`
had no check, and redirects were never re-checked, so `allow_private=False`
was bypassable. It is now enforced via CDP Fetch interception at
`RequestStage.Request` — a refused request fails before Chrome resolves DNS
or opens a connection — and is armed for the page lifetime rather than
re-applied per navigation.
- **The private-address block missed IPv4 spellings Chrome accepts but
Python's `ipaddress` rejects:** `2130706433`, `0177.0.0.1`, `0x7f000001`,
`127.1`. All are now canonicalized before the check.
- **Popup targets (`window.open`) are closed on attach under a restrictive
policy,** since a new CDP target carries none of the parent's interception.
- Known limits, stated rather than hidden: WebSocket handshakes are not
intercepted (CDP's Fetch domain does not cover them); DNS rebinding remains
out of scope.

Changed — breaking

- `window.open()` now fails under a restrictive policy, which is the default.
- Session file format changed to `{"cookies": [...], "origins": {...}}`. Old
bare-list (cookie-only) files still load.

Added

- `grip` CLI: `open`, `snapshot`, `read`, `screenshot`, `run`, `doctor`.
- File upload/download: `Page.upload()`, `Page.enable_downloads()`,
`Page.wait_for_download()`.
- localStorage in `save_session`/`load_session` (was cookies-only).
- `Browser.pages` / `Browser.get_page()`; MCP `list_tabs`/`switch_tab`/`close_tab`.
- Gemini adapter; `base_url` on the OpenAI adapter for Ollama/vLLM/LM
Studio/OpenRouter; `adapter_from_env()`.
- MCP: `goto`, `screenshot`, `run` tools (5 -> 8), install docs at `docs/mcp.md`.

Fixed

- The MCP server never closed the `Browser` — the clean stdio-exit path
stranded Chrome and its profile dir.
- The snapshot/delta protocol was implemented twice against private page
state; now one `Page.payload()`.
- `enable_downloads()` registered its listener after enabling events, so a
download completing in that window was dropped and `wait_for_download()`
timed out with the file already on disk.
- Adapter selection was duplicated in `cli.py` and `mcp/server.py`, so `grip
run` and the MCP run tool could not reach Gemini or OpenAI-compatible
endpoints.
- `assert self._llm is not None` in `Browser.run()` vanished under `python -O`.
- CI: stealth tests pinned Chrome-build-dependent booleans; the workflow
claimed offline while seven tests hit live URLs.

0.5.1

Fixed

- **A delta could cost more than the page it replaced.** On a click-driven
navigation where the reported URL trailed the document, `build_delta` did not
see a URL change, diffed two unrelated pages, and emitted a wholesale
replacement — 5,701 tokens where the full snapshot was 2,963, in 6 of 22 runs.
Two independent guards now bound it: a delta that is not meaningfully smaller
than the snapshot loses to it at the point the payload is chosen, and a
restamped-document check compares the elements behind shared handles, since a
handle stamped per document names a different element after a restart. Found
by grip's own benchmark, not by a user.
- **A launch failure said only "timed out".** Chrome's stderr went to
`/dev/null`, so a failed launch could not say why. The error now carries the
process state, exit code, port-file path and the tail of Chrome's own
complaint, and distinguishes a Chrome that died before writing the port from
one still running when the deadline passed. The deadline is configurable via
`GRIP_CHROME_LAUNCH_TIMEOUT`, because ten seconds is tight for a cold Chrome
on a loaded CI runner.
- **A reused profile kept the previous run's `DevToolsActivePort`,** so
`_read_port` could return a dead port instead of waiting for the one Chrome
was about to write — a defect in the persistent-profile feature added in
0.5.0.
- **`find_chrome()` trusted `CHROME_EXECUTABLE` without checking it existed,**
so a stale value produced an opaque `Popen` failure instead of the clear
"Chrome/Chromium not found" error.

Changed — measured claims corrected

Page 1 of 2

© 2026 Safety CLI Cybersecurity Inc. All Rights Reserved.