DEV Community

Cover image for ~1 MB to orbit: your agent's browser ships with your OS

~1 MB to orbit: your agent's browser ships with your OS

Sam LABBE on October 01, 2026

Houston, we deleted Chromium. https://slabbdev.github.io/navette/ Every AI agent that touches the web today drags the same luggage: headless Ch...
Collapse
 
jabinpou profile image
Mahdi JabinPour •

The changelog is more useful than the binary-size headline for judging this: the cookie-isolation assertion and the reported native/synthetic input mode make important boundaries inspectable. One benchmark I'd like alongside navigate → read is 'the app is ready for the next action.' A page can finish navigation before its hydrated controls work. Would a small fixture set with delayed hydration and SPA route changes help compare that readiness contract across the three engines?

Collapse
 
slabb profile image
Sam LABBE •

The readiness contract is the honest next axis, and you've named the gap precisely: navigate resolves at the WebView's navigation callback — document-complete, not interaction-complete. Hydration and SPA route changes live after that signal (SPA routes often never fire it at all), which is exactly why wait and evaluate exist as manual gap-fillers today. A fixture set with delayed hydration and client-side routing would measure the thing a click-heavy agent actually needs: the time from navigate-returns to controls-actually-responding — the difference between "the document loaded" and "the app is ready for the next action."

And the three-engine comparison is where it gets sharp, because the engines don't agree on what "finished" means: WebKit fires didFinish, Chromium has its own load semantics, Lightpanda leans on network-idle heuristics — the same hydration-delayed fixture would expose three different readiness contracts, and that disagreement is a real selection criterion for agent harnesses, not a benchmark footnote. It goes on the bench's list; the fixture is the easy half, the interesting output is whether the three engines can even agree on what done means.

Collapse
 
jabinpou profile image
Mahdi JabinPour •

Thanks for spelling out the contract. For that fixture, I would make success observable: a click must increment a counter or complete a route change, rather than just find an enabled button. Logging missed and duplicate actions alongside readiness time would show whether a faster return actually helps the agent. That should make the cross-engine comparison easier to interpret.

Thread Thread
 
slabb profile image
Sam LABBE •

Agreed — the counter is the sensor and the enabled button is its costume: "looks clickable" is what the DOM claims, "the route changed and the counter moved" is what the app did. Making success observable turns the fixture from a stopwatch into a verdict. And logging missed and duplicate actions beside the readiness time is the metric that actually matters: a fast return that costs the agent three duplicate clicks is slower in the only clock that counts — wall-clock-to-task-done. The cross-engine table then reads itself: readiness time, misses, duplicates — three columns, and whichever engine wins has to win at keeping the agent's hands off the retry button.

Collapse
 
slabb profile image
Sam LABBE •

v1.7 is out — native keyboard input (attempt-first OS-level events, automatic synthetic fallback, the mode always reported) and visible session windows for one-time human logins — 18 MCP tools.

Release notes: github.com/slabbdev/navette/releas...

Collapse
 
slabb profile image
Sam LABBE • • Edited

v1.9.0 is out — native key input on macOS, verified end-to-end on an AZERTY machine: show_window is the focus gate (the Window Server confirms isKeyWindow before any CGEvent is posted, so a keystroke can never land in someone else's app), and single characters ride with an attached unicode string, so e.key is what the agent asked for on any keyboard layout — 5/5 trusted keystrokes live (a, Enter, Z, Tab, ArrowLeft). All three engines now ship attempt-first native input: SendInput (Windows, verified end-to-end), XTEST (Linux/X11), CGEvents (macOS). Plus NAVETTE_EVAL_TIMEOUT_SECS for slow or contended environments.

Release notes: github.com/slabbdev/navette/releas...

And since the last update here: the no-API recipes grew a second member — a demo that logs in the human way, keeps the session in the OS keychain, and reads pro seller stats off two routes. Also new in v1.9.0's wake: 19 MCP tools, including login — the login form is filled straight from the OS keychain and the agent never sees the password (#8).

Collapse
 
slabb profile image
Sam LABBE • • Edited

Update — v1.2.0 (Oct 2):

  • Two new MCP tools shipped: state_export / state_import — cookies and storage out of the browser and back in, the Playwright storageState equivalent. Your agent can now save an authenticated session and resume it tomorrow. 11 tools total.
  • The Windows (WebView2) and Linux (WebKitGTK) backends are aboard and in the v1.2.0 tag — same HTTP/MCP surface, same 8 primitives, +32 KB of binary. macOS remains the fully hardened, benchmark-carrying backend: Windows passed its first end-to-end runtime smoke on GitHub's runners; Linux works under headless X and is hardening.
  • Honest sizing update: the macOS binary is now 626 KB (the title's 594 KB was v1.0.0) — still ~350× smaller than Playwright's browser download.
Collapse
 
slabb profile image
Sam LABBE • • Edited

Update — v1.2.1 (Oct 3):

  • CI is green on all three OSes: the Windows (WebView2) and Linux (WebKitGTK) backends pass the same navigate→read smoke on GitHub's runners — same binary, same MCP surface, +32 KB. Getting there surfaced five real root causes (fold-result routing, URL-matched completion, a missing D-Bus session bus in headless Linux, WebView2's file:// stall) — all fixed, each proven with a breadcrumb or a backtrace. macOS remains the benchmark-carrying backend; screenshots and cookie state on wry land next.
  • Two new MCP tools: state_export / state_import — cookies and storage out of the browser and back in, the Playwright storageState equivalent. Your agent can now save an authenticated session and resume it tomorrow. 11 tools total.
  • Boot-time session pre-warm reached parity with macOS: first navigate on a cold CI VM went 38 s → 83 ms. Honest sizing update: the macOS binary is now 626 KB (the title's 594 KB was v1.0.0) — still ~350× smaller than Playwright's browser download. Release notes: github.com/slabbdev/navette/releas...
Collapse
 
slabb profile image
Sam LABBE •

Updates — v1.3.0 & v1.4.0 (Oct 4):

  • Full platform parity: screenshots (native capture per engine), cookie state export/import, viewport control, and the resident daemon now work on Windows and Linux too. CI verifies a valid PNG screenshot and a cookie round-trip on all three OSes. JS dialogs are auto-handled in-page — agents can no longer deadlock on a hidden modal. T- he agent loop is complete: file upload (in-memory content through a page-side DataTransfer — no OS dialog, identical JS on all three engines) and scroll joined. 16 MCP tools now. One bug worth mentioning: the eval wrapper on Windows/Linux silently discarded its value — every /evaluate and /read outside the fold path returned undefined; fixed, and CI tests it explicitly.
  • Install it in one command: cargo install navette-browser — or grab the release binaries (macOS arm64 658 KB, Windows x64 / Linux x64 ~1.2 MB — still ~180× smaller than Playwright's download): github.com/slabbdev/navette/releas...
  • A bench workflow now measures navigate/read/screenshot on all three engines (median/p95, in the run summary). Still missing, stated plainly: OS-level keyboard/mouse input, request/response network interception, OS file-dialog automation.
Collapse
 
slabb profile image
Sam LABBE •

New measurement out: I sent a vanilla agent to 200 real URLs across five categories — 82.5% readable, 72 of 200 hosts blocking every tracked AI crawler in robots.txt, zero x402 signals in the wild yet, and one named site (The Intercept) running a declared paywall + full AI-bot blocks + a wide-open front door, all at once. The dataset is CC BY 4.0 in the repo.

dev.to/slabb/my-agent-met-the-real...

Collapse
 
slabb profile image
Sam LABBE •

v1.8.1 is out — cross-session cookie isolation is now a build-breaking CI assertion on all three engines (named WebView2 profiles + InPrivate on Windows, non-persistent stores on macOS, isolated contexts on Linux), plus loopback-API hardening: constant-time token checks, a DNS-rebinding guard, credential-redacted proxy logs. The full posture is now documented: docs/SECURITY.md.

Release notes: github.com/slabbdev/navette/releas...

And since the last update here: I pointed the resident daemon at 200 real URLs and measured the walled web — 82.5% readable by a vanilla agent, 72 hosts blocking every tracked AI crawler, zero x402 signals yet, and one named site (The Intercept) running a declared paywall + full AI-bot blocks + an open front door at the same time. Dataset is CC BY 4.0: dev.to/slabb/my-agent-met-the-real...

Collapse
 
sinarezaei profile image
Sina Rezaei •

The interesting part for me is actually the session layer, not the 594 KB number.

Once an agent becomes a long-running worker, browser state stops being a convenience and becomes part of the execution model. Think about an agent that monitors a deployment dashboard, checks a private Sentry project, and then verifies a feature flag in an internal admin panel across separate runs. Re-authenticating on every run isn't just slower, it makes the agent much more brittle.

That's where state_export/state_import gets interesting. You're essentially moving toward the same idea as Playwright's storageState, but with the browser engine living underneath the agent instead of being shipped alongside it. The important distinction is that the state can include more than a cookie jar: WebKit and WebView2 both have persistent website data stores/profiles that cover things like cookies, cache, and DOM storage.

I'd be curious how far you want to take that abstraction across the three backends. Keeping MCP semantics identical is one thing; keeping session isolation, persistence, and failure recovery identical when the underlying WebView data models differ is where this gets really interesting.

Collapse
 
slabb profile image
Sam LABBE •

This is exactly the right question, and it's where I've tried to be most deliberate.

The contract I committed to is: state is explicit data the agent owns, not ambient profile the browser owns. Every session runs in a fresh webview with a non-persistent store (on macOS explicitly a WKWebsiteDataStore nonPersistent per session) — isolation is by construction, nothing hits disk. What survives a run is what the agent chose to export: state_export serializes the cookie jar as JSON, stored outside the engine, re-importable after a crash, a daemon restart, or an idle-release reap. Failure recovery falls out of that shape — a crashed WebKit helper loses nothing the agent can't re-import idempotently, and CI round-trips the cookie jar on all three engines.

The honest scope: the jar is cookies only today. And your instinct about the divergence is right — persistent profiles (cache + DOM storage + credentials on disk) bring engine-specific data schemas and state rot that differ across WebKit / WebView2 / WebKitGTK, so I've kept them out rather than inheriting the differences by accident. I've opened the roadmap discussion as an issue, including the "what does each engine actually persist by default" audit: github.com/slabbdev/navette/issues/6 — the options on the table are ephemeral-only, a jar that grows past cookies, or a deliberate named-session persistent profile. Would love your take there.

Collapse
 
sinarezaei profile image
Sina Rezaei •

Yeah, keeping the jar deliberately small makes sense to me. Otherwise state_export can quietly turn into “serialize whatever this engine happens to consider a profile,” and the abstraction starts leaking pretty quickly.

The interesting edge case I'd test next is a long-lived session that gets rotated while the agent is in the middle of a workflow.

For example, an agent authenticates to an internal admin app, exports its cookie state, then the WebView is reaped. On the next run it imports the jar, but the application has rotated one of the session cookies or invalidated the server-side session in the meantime. You can end up with a perfectly valid-looking exported state that restores successfully at the browser layer but is already unusable at the application layer.

That makes me think state_export probably needs to be treated as a recoverable snapshot rather than proof that a session is still valid. A small distinction, but it could matter a lot once you start doing daemon restarts and automatic recovery.

I'd definitely keep the engine-specific persistent profile out for now. The cookie-only contract gives you a much cleaner portability boundary across the three backends.

Thread Thread
 
slabb profile image
Sam LABBE •

"Recoverable snapshot, not proof of validity" is the right contract name — and it's your own TOCTOU point arriving at the browser layer: a jar valid at export time says nothing about validity at use time, and cookies have no resourceVersion to precondition on. The application-layer check is only reachable by an actuator, so the honest shape is a three-event chain: the export sealed with its timestamp, the import sealed, and a cheap authenticated probe sealed after import — a session_verify tool: import, hit one known-authenticated URL, report alive or rotated instead of silently handing the agent a jar that restores green and reads 401.

Your rotated-session case becomes the CI fixture: export, reap, rotate server-side, import, probe, expect "rotated" — the failure surfaces as a named finding instead of a mystery 401 mid-workflow. And agreed on keeping the engine-specific persistent profile out: cookie-only is the portability boundary, and everything above it is the application's problem to verify.