Agent-First HTTP v0.11.0: The Bugs Headless Cannot Have
Two defects lived on the takeover display and nowhere else: a permanent warning bar eating a strip of every session, and a native save dialog that swallowed downloads and blocked the display until a human clicked it. Both are invisible headless, and one of them the last release explicitly measured wrong. This release fixes both, drops the chrome-headless-shell backend, moves the images to Debian trixie, and adds a test that runs every CLI shape through its own handler.
The previous release’s deployment guide said this, in writing:
--no-sandboxis not a fingerprinting concern… it is invisible to page JavaScript, and Chromium 151 shows no “unsupported command-line flag” warning for it (measured — the browser chrome is the same height with the sandbox on and off).
That was wrong. Chromium 151 raises the bar, Brave raises the bar, and it had been sitting across the top of every takeover session the whole time.
Why a measurement can be right and still wrong
The measurement was taken against --no-sandbox. The container passes
--no-sandbox --disable-setuid-sandbox, and Chromium names exactly one
offending flag in that bar — the first one. So removing --no-sandbox from the
launch line changed nothing visible, because --disable-setuid-sandbox simply
took its place at the front of the queue. Same bar, different name, and a
conclusion drawn from watching the wrong flag.
The image now suppresses it with a managed policy —
CommandLineFlagSecurityWarningsEnabled: false, written into all three browser
families’ policy directories by a new install-browser-policy.sh. That script
is installed by both the runtime image and the test image, the way
install-backends.sh already is, so a takeover test sees the browser chrome a
deployed host actually shows.
The policy changes no launch flag and no sandbox posture. The flags stay, the container stays the isolation boundary, only the notification goes away.
--test-type, the flag everyone reaches for first, is recorded in the script as
a thing not to try: it makes Brave abort on startup with a SIGTRAP, and CDP
never comes up.
A download that stopped at a dialog
The second defect is the same shape. Start a download on the takeover display,
and Chromium put up a native GTK “wants to save” dialog pointing at
~/Downloads. Nothing reached the profile’s download directory, and the dialog
blocked the display until a human clicked through it.
The host did issue Browser.setDownloadBehavior at launch, which is why this
looked covered. It never did anything: the host opened a throwaway CDP
connection, sent the command, and closed it — and Chromium scopes that override
to the DevTools client that set it. The override died before anything could
download.
Nor was CDP ever the right layer here. A human clicking a link on the takeover
display is not a DevTools client at all. Two profile preferences are what
actually survive both: download.prompt_for_download = false, and
download.default_directory / savefile.default_directory pointed at the
profile’s own downloads/. The dead host-level call is gone; each fetch still
sets its own download behavior on its own live connection, where it needs the
events.
Testing what only fails when a human is there
Both defects had a way of passing tests while broken, which is the interesting part.
The download regression test cannot run --headless=new — headless never shows
a save dialog, so the assertion passes with the fix reverted. It cannot go
through Client::fetch either, because that pipeline sets its own download
behavior on its own live connection and succeeds even with the bug fully
present.
So it runs headful on a real KasmVNC display and drives a bare CDP session — navigating to an attachment, the way a human clicking a link does — and waits for a file to land in the profile. That is the only arrangement where this fails when regressed, and it is now in the release gate.
One backend fewer
chrome-headless-shell is removed: --browser and container install --with
no longer accept it, in either spelling.
It was Google’s slimmer headless distribution of the same engine, with an identical CDP surface to the chromium backend the image always installs. For that it cost a pinned Chrome-for-Testing version to keep current, a linux64-only download that made arm64 images asymmetric, and a test that silently asserted nothing on arm64 while reporting a clean pass. It bought no capability the default backend did not already have.
Seven named backends remain, each of which does something the others cannot: chromium, chrome, edge, brave, fingerprint-chromium, camoufox, and lightpanda.
Debian trixie
The runtime image, the builder stage, the release-download stage, and the test image all move from bookworm to trixie, and KasmVNC now installs its trixie package.
One measurement worth carrying forward: the broad font list matters more on trixie, not less. A bare trixie image resolves about 8 font families where bookworm resolved about 46; with the list, trixie reaches about 473. Font enumeration is a standard fingerprinting dimension, and a browser that sees eight families does not read as a desktop.
Every shape, through its own handler
afhttp’s CLI is compiled from afdata’s cli-spec-v1 registry: 23 leaf commands
across 34 legal shapes, each shape declaring the arguments it supplies. A
handler reads those by id — and if it asks for an id its own shape does not
declare, the read used to degrade to an empty string and the command would run
on it.
There is now a test that walks every declared shape through its own handler with strict reads. A mismatch names itself in the test rather than shipping. It is safe to run because these handlers only project values; nothing in that path opens a socket, launches a host, or touches a profile.
This arrives with agent-first-data 0.31, which also splits the bound registry’s
outcome type (BoundOutcome) from the unbound one and moves execution onto the
invocation itself.
Getting it
$ brew install agentfirstkit/tap/afhttp
$ cargo install agent-first-http
Rebuild your container image with afhttp container install --rebuild to pick
up trixie and the browser policy.