Agent-First HTTP v0.13.1
The prebuilt container install was broken on every platform, and the error message asserted a cause it had never checked — sending people to a build two orders of magnitude slower, to fix a file that had simply not been staged.
afhttp container install did not work. Not on Apple Silicon, not on Linux, not
anywhere — the default, documented path failed for everyone, while
--from-source kept working. The fast route was the broken one.
That is worth writing down not for the bug, which is small, but for what the failure said about it.
A context the binary carries
The install builds the host image from an embedded context: the Dockerfile and its helper scripts are compiled into the binary, written to a cache directory, and handed to the builder. Nothing is read from a checkout, which is the point — the download path needs no source tree and no Rust toolchain.
Three files were embedded. The Dockerfile copied four. The build died on:
failed to calculate checksum of ref …:
"/container/docker/install-browser-policy.sh": not found
--from-source was unaffected because it builds from a real tree, where the file
is simply present. So the one path that never exercised the embedded context was
the one that worked.
The fix is one include_str!. The guard is the part worth keeping: a test reads
the Dockerfile, collects every container/docker/ file it copies, and asserts
each one is staged. The next file added to the Dockerfile fails there, rather
than in somebody’s build.
The error that sent you the wrong way
Here is what that failure printed:
image build failed. If v0.13.0 has no published release asset for aarch64-unknown-linux-gnu, build from a source checkout instead:
afhttp container install --from-source
Every clause of that is a guess, and the load-bearing one is false. The asset was published. It was downloadable. The build had not failed to fetch anything — it had failed to copy a file that was never staged. The code threw the builder’s real message away and substituted a cause it had not checked:
exec_to_log(runtime.bin(), &build, &log_file)
.map_err(|_| build_failed_error(target, &log_file))?;
And the advice was expensive. Building from source compiles afhttp and its browser stack inside a 2-CPU VM — roughly an hour. The actual fix, once the context was complete, took fifteen seconds. Someone following that message spends an afternoon compiling, to solve a problem that was never about compiling, chasing a release asset that was sitting there the whole time.
A wrong cause stated confidently is worse than no cause at all, because it is the one that gets acted on. A bare “build failed, here is the log” would have been more useful than a specific, plausible, wrong explanation.
So the failure now reports what the builder actually said, and names the source-build routes only when the download is the thing that failed — the one case where they are the answer. The narrowness is deliberate: over-claiming is the bug being replaced, so anything not positively identified as a failed fetch is left to the builder’s own words.
Two ways to stop a host that had already started
Both specific to the Apple runtime, and both reported as something other than what they were.
The volume. Docker seeds an empty named volume from the image path, ownership
included, so the image’s build-time chown carries over. Apple mounts it
root-owned. The image runs as a non-root user, so the host died with
mkdir: cannot create directory '/data/afhttp': Permission denied before it
could write its token. The install now hands the volume to the image’s own
account first — by name, since the account is defined once in the Dockerfile and
a uid repeated in Rust would be a second definition free to drift.
The state check. Apple’s inspect has no State.Running. That is Docker’s
shape; Apple answers status.state as a string. Reading only the first shape
returned None for every container — and the fallback then ran container ps,
a subcommand Apple does not have (Plugin 'container-ps' not found). Between
them, every container on that runtime read as stopped. install would report
container exited before /health became ready
about a host that was running, serving, and had already logged host_ready. The
comment above that fallback claimed it “works the same on Docker and Apple”. It
never did.
The shape of all four
Each of these was diagnosed by looking rather than reasoning, and two of them
were not what they first appeared. The container path looked like a missing
release asset, because that is what the tool said; it was a missing embed. The
readiness failure looked like an authenticated /health being misread as
unready; it was a schema mismatch plus a subcommand that does not exist.
Guessing would have been wrong both times, and each wrong guess costs a full build cycle to disprove. When the environment is one you cannot fully reproduce, spending that cycle on a diagnostic that dumps raw state — instead of on a fix — is not the slow path. It is the only one that converges.
Backends move with this release too: camoufox v152.0.4-beta.28 → beta.29,
which the pre-release third-party check required before anything could ship.
Getting it
$ brew install agentfirstkit/tap/afhttp # macOS and Linux
$ scoop bucket add agentfirstkit https://github.com/agentfirstkit/scoop-bucket && scoop install afhttp # Windows
$ cargo install agent-first-http --locked