Houston, we deleted Chromium.
https://slabbdev.github.io/navette/
Every AI agent that touches the web today drags the same luggage: headless Ch...
For further actions, you may consider blocking this person and/or reporting abuse
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?
The readiness contract is the honest next axis, and you've named the gap precisely:
navigateresolves 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 whywaitandevaluateexist 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.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.
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.
v1.7 is out — native keyboard input (attempt-first OS-level events, automatic synthetic fallback, the
modealways reported) and visible session windows for one-time human logins — 18 MCP tools.Release notes: github.com/slabbdev/navette/releas...
v1.9.0 is out — native key input on macOS, verified end-to-end on an AZERTY machine:
show_windowis the focus gate (the Window Server confirmsisKeyWindowbefore any CGEvent is posted, so a keystroke can never land in someone else's app), and single characters ride with an attached unicode string, soe.keyis 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). PlusNAVETTE_EVAL_TIMEOUT_SECSfor 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).Update — v1.2.0 (Oct 2):
Update — v1.2.1 (Oct 3):
Updates — v1.3.0 & v1.4.0 (Oct 4):
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...
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...
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_importgets interesting. You're essentially moving toward the same idea as Playwright'sstorageState, 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.
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
WKWebsiteDataStorenonPersistent per session) — isolation is by construction, nothing hits disk. What survives a run is what the agent chose to export:state_exportserializes 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.
Yeah, keeping the jar deliberately small makes sense to me. Otherwise
state_exportcan 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_exportprobably 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.
"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.