Repository navigation
Releases: vernikr/browser-agent
Release list
v1.0.23
Changed
-
A private helper is no longer imported across module boundaries.
vision_agent.pyimported_extract_text_from_htmlfromchrome.py— a
leading underscore that crossed a module boundary, which is the exact shape
Python's underscore convention exists to prevent. The function is used by
two modules, so it is not internal to either; the underscore was a lie.Renamed to
extract_text_from_htmlat all five sites: the definition in
chrome.py, two call sites insidechrome.py, the import in
vision_agent.py, and the call site there. No behaviour change — pure
navigation.
v1.0.22
Changed
-
The package root no longer re-exports internal classes.
__init__.pyused to put
RealChromeSession,X11Display,bezier_curve,is_cloudflare_challengeand
is_warp_proxy_readyinto__all__alongsideBrowserClient, although nothing in
either repo imports any of them from the package root — every consumer uses a
submodule (browser_agent.chrome,browser_agent.x11_input, …). The README names
onlyBrowserClient.__all__is now["BrowserClient", "__version__"]. The modules stay importable by
their full paths, so no working code breaks.tests/test_public_surface.pypins the
published set and verifies the submodules still resolve, so the surface cannot grow
back silently the way it grew in the first place.
v1.0.21
Added
-
Five CLI subcommands that were missing:
goto,key,scroll,screenshot,html. The daemon has always
implemented all of them and the MCP bridge exposes them, butbrowser-agentcould only move, click and type. An
agent with SSH access and no bridge installed could act on whatever pagefetchhappened to leave behind and
nothing else — it could not navigate, press a key, scroll, look at the page, or read it back. The plan listed
four of the five;gotowas the one it missed, and it is the one that matters most, because a session you cannot
navigate is a session you cannot use.A test now reads
/action's own action set out of the daemon and the subcommand list out of the parser, and fails
when they diverge — so the gap cannot reopen silently the way it opened.screenshotwrites the PNG to a path (a temp file when none is given) and reports its size, and never puts the
base64 on stdout.htmlprints the page, or writes it to a file with--outand reports only its length — a
rendered page is routinely a megabyte.
Fixed
scroll --xwithout--yfailed in the daemon with a 500 naming a browser. The daemon validatesx/yas a
pair before it starts the browser, so the CLI now raises the same message before sending anything. Failing in the
CLI gives the caller the real reason instead of a browser-start failure they did not ask for.
Changed
-
gotoomitsuse_proxyunless--proxyis given, matching the bridge: sendingfalsewould forbid WARP and
drag a proxied session back to direct, and the daemon treats an absent field andnullidentically, so omitting
costs nothing and keeps the payload shape equal to what was actually requested. -
The new action subcommands share the existing "daemon must be running" behaviour rather than
fetch's in-process
fallback, and the reason is now written down: every one of them acts on a live session, so a one-shot session would
move the mouse in one Chrome and click in another.
v1.0.20
Added
-
tests/test_troubleshooting_doc.py— the other half of the P1-7 guard. The bridge's CI checks its
troubleshooting doc against its hint table; this asserts the daemon's doc keeps deferring, and that the advice a
CLI or API caller needs still lives where that caller reads it (docs/api.mdforuse_proxyand the
hibernation cold start,docs/deployment.mdfor the WARP route).Structural rather than vocabulary-level, and deliberately so: it catches a revert to the old shape, not a single
re-added row. The vocabulary check belongs with the hint table, because that is the only place the advice
actually is.Removing the duplicated rows from this doc cost a CLI user nothing, and this test is what says so — a CLI caller
gets no hint line, so the assertion that the advice survives elsewhere is the part that makes the removal safe.
v1.0.19
Documentation
-
The daemon's troubleshooting doc no longer restates the bridge's failure advice. The cold-start, bot-wall and
daemon-unreachable guidance lived in three places — the hint table inbrowser-agent-mcp/src/server.ts, the
bridge'sdocs/troubleshooting.md, and this repo's — so all three had to be updated together and nothing
failed when one was missed. The hint table is the one an agent actually reads: it is printed inline with the
error, at the moment the agent is still looking at the response.The daemon's doc now keeps only what the hint cannot know — symptoms of this host, and repairs that are not
about a single failed call: a unit that is down, memory pressure, a stale X lock, xdotool driven by hand, PDFs,
a profile directory that moved after an upgrade, a CDP port conflict. It opens by saying to read thehint:
line first, and why the duplication was removed.Nothing here was removed from the product. The advice still reaches an agent on every failure; it now reaches it
from exactly one place.
v1.0.18
Documentation
-
Volatile measurements removed from the evergreen docs. The repo's own
AGENTS.mdrule is to omit them,
and the docs had drifted a long way from it: resting RAM (~12 MBin three files), the55–125 mshold
range, the JS heap cap, the300–600 MBtransient fetch figure,OOMScoreAdjust=500, the-n 100/
-n 200log tails, the28×32checkbox inset and the~11%chrome band. Each was a number the code
owns and nobody re-measures, so each was a future drift ticket paid for in review attention.They are replaced with the behaviour and a pointer to the live source:
browser-agent doctorreports what
the daemon actually costs on the host it is running on, and the env table owns the config defaults. The
log-tail commands become time-bounded (--since "10 min ago"), which is better advice for a cold-start
problem than a line count anyway. -
docs/architecture.mddescribed a design that no longer exists. It explained that the viewport origin
is derived from the browser window's outer rectangle — which is the fallback, not the mechanism. Since
1.0.9 the origin is measured once per session by locating a calibration marker, and the geometric estimate
only runs when that cannot. An agent reading the old text would have looked for a chrome-height constant
that was deliberately removed, and would have concluded the calibration did not exist. Rewritten to describe
the measurement first and the estimate as its fallback, including why the estimate is a few pixels off and
when a calibration is trusted. -
AGENTS.mdno longer restates the ruff selectors and line length (they live in[tool.ruff]) or the size
of the Pillow wheel and trafilatura's dependency count.
v1.0.17
Added
-
The opt-in integration suite that
AGENTS.mdhas been documenting all along.
The guide promisedBROWSER_AGENT_IT=1 pytest -m integration. The marker was
registered inpyproject.toml, but nothing implemented the gate and no test carried
the marker — so the documented command selected zero tests while implying the
X11/Chrome layer was covered. The layer that actually breaks in production (Xvfb
start, xdotool, scrot, the CDP handshake) had no automated coverage anywhere.tests/conftest.pyimplements the gate:integration-marked tests skip unless
BROWSER_AGENT_ITis truthy, and skip again — with a different reason — when the
host lacks Xvfb, xdotool or a capture tool. A missing opt-in is the operator's
choice; a missing binary is a provisioning problem. Neither is a test failure,
because a check that fails for the wrong reason gets disabled.tests/integration/test_x11_smoke.pycovers X11 only, on a display that is not the
daemon's:99so it cannot drive a running daemon's cursor. The assertion that
matters reads the cursor back from the X server withgetmouselocationrather than
trustingmove_mouse's return value, which is the point it asked for and therefore
proves only that the method ran. Also covered: the geometry the daemon asked for,
a decodable PNG at the right size, the exit-0-with-stderr keysym quirk that P2-4 was
written for, and that the display is re-bindable afterwards. -
tests/test_integration_gate.py— the coverage claim is now itself asserted.
The marker is registered, at least one test carries it, the gate is off by default
and on for a truthy value, and a plain run reports the skip reason. Verified
negative in both directions: deleting the tests fails two of these, and deleting
the gate fails two others. A guard that needs Xvfb would itself be skippable, so
these four run everywhere and cost about a second.
Documentation
AGENTS.mdnow states what is and is not covered, in those words. The old line
said "real-Chrome tests are opt-in", which overstated it twice: the harness is X11
only, and Chrome, CDP and the Turnstile solver still have no automated coverage
anywhere — they are exercised against fakes, never against a real browser, and
browser-agent doctoris the only thing that touches them. A reader now learns
that from the guide instead of assuming the opposite.
v1.0.16
Added
/actionacceptsuse_proxy— and now says so. The daemon has read the field on
/actionfor its whole life without documenting it, which is exactly why nothing
used it: an undocumented parameter is one no caller dares depend on.docs/api.md
now documents it under/action, andtests/test_action_use_proxy.pypins the
pass-through together with the documentation, so the two cannot drift apart the way
the field itself did.
Documentation
-
Two consequences of the field that were previously invisible, both of them silent:
- Switching the route restarts Chrome. Cookies and in-page state are lost, so the
field belongs once per session, not once per click. use_proxy: truewith WARP not running degrades to direct without an error. The
route is chosen only whenis_warp_proxy_ready().gotoreturns{ok, url, title}
with noblockedfield, so an agent that asked for WARP and got a datacenter-ASN
wall sees a successful-looking response. The docs now say to check
warp_proxy_readyfromGET /statusfirst, rather than leaving the reader to
discover it.
This is a pre-existing property of the field, documented rather than changed: making a
forced route fail loudly would change/fetchbehaviour for callers that currently
passuse_proxy: trueon hosts without WARP, and that is a separate decision. - Switching the route restarts Chrome. Cookies and in-page state are lost, so the
-
use_proxyon/actionmeans something different fromuse_proxyon/fetch. A
fetch auto-retries a blocked page through WARP when the field is omitted; an action
cannot, because it is already driving a live session. Omitting it there means "leave
the session on its current route", andfalseactively forbids WARP. The tests pin
that the default isNoneand notFalse— collapsing the two would make every plain
clicksilently drag a proxied session back to direct.
v1.0.15
Added
-
/statusis now a pinned contract instead of prose. The endpoint both repos and
every script read lived only as a hand-maintained example indocs/api.mdand a
hand-copied object in the bridge's mocks, so nothing failed when either drifted —
the failure surfaced in production as a confusing 500 from a field that no longer
existed.tests/test_status_contract.pynow asserts the exact field set the daemon emits,
the type of each one, and thatdocs/api.mddocuments every field — in both
directions, so a documented field that is not emitted fails too. A rename fails
with the added and removed fields named.
v1.0.14
Added
-
doctornow hands the agent a runnable repair instead of a report. Every check
that fails carries afixfield with the exact command that clears it — the apt
package, the systemd unit to enable, the lock to remove, the key to set — and the
plain-text output prints it indented under the check it belongs to. A check that
passes carries no fix, because a repair for something that works is noise.This is what turns
doctor --jsonfrom something an agent has to interpret into
something it can act on, which is the difference between a diagnostic and a
repair script.
Fixed
-
The repair for a missing binary named the binary, not the package.
Xvfbthe
command ships in thexvfbpackage, soapt-get install -y Xvfbwould not have
installed it. A repair command that does not work is worse than none. -
browser-agent fetchsilently fell back to an in-process session when the daemon
was down. It launched a whole Chrome in-process and printed the results exactly
as if nothing had happened, so the agent never learned the daemon was down — and
every subsequent invocation paid the cold start again. One line now goes to stderr;
stdout stays the JSON contract.