Agent-First HTTP v0.10.0: The Browser Was Never the Variable

by Agent-First Kit Contributors

afhttp was launching Chromium with a flag that removed WebGL entirely, forcing a second flag that made the browser announce itself as automated, and shipping an image with two dozen font families. Then it measured which browser cleared bot walls better and blamed the browser. This release fixes all three, retracts the conclusion, and makes takeover backend-agnostic — plus a closed-world CLI registry and the response Content-Type on FetchResult.

A previous post here claimed that Brave draws a non-interactive Cloudflare challenge where Chrome gets a clickable checkbox. That claim was wrong, and the way it was wrong is the story of this release.

Three flags, all ours

The measurement was real. It was taken on a host carrying three defects that afhttp had put there itself.

The GPU flag. The chromium launcher pushed --disable-gpu unconditionally. That flag does not merely disable hardware acceleration — it switches off the GPU process wholesale and takes WebGL down with it. A host-launched browser reported webgl=false, webgl2=false, and zero WebGL extensions.

No real browser looks like that. Every anti-bot scorer reads a WebGL-less browser as automation, which makes the flag actively hostile to the one job the takeover host exists to do. Measured against a Cloudflare Turnstile wall, the no-WebGL profile never cleared — not even with a human clicking the checkbox.

It now passes --enable-unsafe-swiftshader instead, which only permits the software fallback. Hardware GL still wins wherever it exists, so this is a no-op on a machine with a usable GPU; containers without one get full WebGL 1 and 2. The software path does report a SwiftShader renderer string — a weaker signal of its own — but present-and-software grades far better than absent. Anyone who wants the old behaviour can still pass --browser-arg=--disable-gpu, which lands after the defaults and wins.

The automation flag. The takeover preset forced --disable-blink-features=AutomationControlled, and the install validator required it, so a caller could not opt out. It bought nothing: the host never launches Chromium with --enable-automation in the first place, so navigator.webdriver already reported false — measured identical with the flag and without it.

What it did buy was Chromium listing it under “unsupported command-line flag” — the browser announcing on screen that something automated launched it, on the one path where a human is looking at the window. A takeover session exists to be looked at by a person. It now ships without the flag.

The fonts. The image resolved about 46 font families. Font enumeration is a standard fingerprinting dimension, and a browser that sees two dozen families reads as a bare server rather than a desktop. The wider set takes it to roughly 486, and stops CJK and emoji from rendering as tofu in captured screenshots.

The retraction

Re-measured against the same site, on a host with all three fixed: Brave draws the ordinary checkbox and clears it, same as Chrome.

The browser was never the variable. The earlier claim is withdrawn.

Brave stays the default takeover backend, but on its original merits rather than a fabricated one — a default install that pulls no proprietary vendor binary, and Shields keeping captured content clear of ads and trackers. Chrome remains available, now described by what actually distinguishes it: the web’s most common fingerprint, and no Shields to break a login flow’s third-party auth scripts.

Takeover stops naming a vendor

Takeover was pinned to Brave in three independent places — the container install preflight, the fetch --takeover capability gate, and the built-image component probe, which shelled out for brave-browser by name. The assumption behind the pin was that Linux arm64 had no Chrome; Google shipped native arm64 builds in July 2026, so it no longer holds.

All three now follow the selected --browser through a single TAKEOVER_BACKENDS set. What takeover actually requires is a headful CDP browser plus the KasmVNC display, not one vendor. Chrome is added as an opt-in backend (--with chrome, WITH_CHROME=1), installed from Google’s apt repository the way Brave already is; being proprietary, it never lands in a redistributed artifact, only in an image built locally.

Both backends now have tests of their own, which neither had before — pinning each backend’s family string against the takeover table, and asserting the browser exposes the WebGL that the --disable-gpu removal was for.

A closed-world CLI

Separately, and breaking: afhttp’s CLI is now compiled from afdata’s cli-spec-v1 registry. One declaration — the root, four command groups (container, profile, skill, tabs), 23 leaf commands, 34 legal shapes — is the single source for argv parsing, typed values, legal combinations, output contracts, --help, and docs/cli.md. clap is dropped, along with two hand-rolled scanners that used to walk raw argv before the real parser ran.

Combinations that were runtime errors are now simply absent from the grammar: fetch --takeover with --render none, --data with --form, host --takeover-provider kasmvnc with --display headless, and container logs --follow without --raw. Two arguments that were previously accepted and silently ignored — --context without --from-source, and --rebuild with it — are now rejected rather than quietly doing nothing.

Content-Type, reported rather than guessed

FetchResult now carries the origin’s response Content-Type verbatim.

body_file’s extension was always a lossy derivative of it: types outside the extension table land in body.bin, so a caller reconstructing the type from the filename silently loses everything the table does not cover. A downstream fetch service did exactly that, and dropped the content type of every gif, pdf, zip and docx it downloaded. Both pipelines already had the value — HTTP reads it off the response headers, the browser path off the main network entry — and simply never put it in the result.

Getting it

$ brew install agentfirstkit/tap/afhttp
$ cargo install agent-first-http

Container pins move with this release too: chrome-for-testing 151.0.7922.47 → .71, and KasmVNC 1.4.0 → 1.5.0.