Agent-First HTTP v0.6.0: A 200 Isn't Proof
v0.6.0 is about not getting fooled. afhttp now recognizes a bot wall or security challenge on the cheap HTTP path — Cloudflare, Turnstile, generic access-denied — surfaces it as a `page_kind` plus warning, and auto-escalates to a real browser instead of handing the agent a 200 that isn't the page. The fetch trace gets honest too: per-stage timing, `current_stage`, `capture_reason`, `wait_mode`, so an agent can see where a fetch spent its time and why it stopped. Plus readiness tuning, a `takeover prepare` subcommand, a named display-provider abstraction, and a skill rewrite that tells the agent to reach for afhttp first.
The failure mode that quietly wastes an agent’s run isn’t a timeout or a 500 —
those are obvious, and afhttp already returns them as typed errors the agent can
branch on. The expensive failure is a status: 200 that isn’t the page: a
Cloudflare interstitial, a Turnstile challenge, an “access denied” wall. The
bytes arrived, the transport succeeded, and an agent that trusts the status code
summarizes the bot wall as if it were the content. Nobody catches it, because
nothing looked wrong.
Agent-First HTTP v0.6.0 is about not getting fooled by that.
The fast path learned to recognize a wall
afhttp’s cheap HTTP path now classifies what came back before handing it over. It
detects bot walls and security challenges — Cloudflare, Turnstile, generic
access-denied pages — and instead of returning a confident 200, it labels the
result with a page_kind (bot_wall_detected, security_challenge_detected),
attaches a warning, and auto-escalates to the real browser path on its own.
The agent doesn’t have to notice the wall; afhttp noticed it.
{
"code": "fetch",
"status": 200,
"page_kind": "security_challenge_detected",
"warning": "fast-path response looks like a security challenge; escalated to browser",
"final_url": "https://app.example.com/"
}
This is the reliability headline: the cheap path stops being a trap. The class
of bug where an agent reads a captcha and reports its contents as fact is closed
by default, because status: 200 is no longer treated as “this is the page.”
The trace stopped being a black box
When a browser-backed fetch takes a few seconds, an agent’s only previous option
was to guess why. v0.6.0 makes the fetch legible. The trace now carries
per-stage timing and status, plus current_stage, capture_reason,
wait_mode, wait_satisfied_by, and timeout_ms — so an agent (or a human
reading the log) can see exactly where a fetch spent its time and what made it
decide to capture:
{
"trace": {
"current_stage": "capture",
"capture_reason": "readiness_stable",
"wait_mode": "auto",
"wait_satisfied_by": "network_idle",
"timeout_ms": 30000,
"stages": [
{"stage": "http", "status": "escalated", "duration_ms": 180},
{"stage": "render", "status": "ok", "duration_ms": 1640},
{"stage": "capture", "status": "ok", "duration_ms": 90}
]
}
}
A slow fetch is now a thing the agent can reason about instead of a mystery to retry blindly.
Tune the wait when “done” is ambiguous
Knowing when a JavaScript page is finished is the hard part of rendering. The auto-wait heuristics are now adjustable per fetch, so a page that settles slowly or paints text in stages doesn’t get captured half-built:
--readiness-idle-ms— how long the network must be quiet before “settled”.--readiness-stable-ms— how long the DOM must stop changing.--readiness-min-text-bytes— a floor on visible text before a capture counts.
And --timeout is now --timeout-ms, because a timeout flag that doesn’t say
its unit is a flag that gets set wrong. The default per-response network-body
capture cap also rose from 1 MiB to 10 MiB, so data-heavy XHR/GraphQL responses
stop getting truncated under the old limit.
Preparing a human handoff, ahead of the wall
Some walls a browser can’t clear on its own — a manual login, a captcha, 2FA. A
new afhttp takeover prepare <url> subcommand opens a persistent host tab for a
human and returns a recommended_url to hand off, with --hard-site to prefer
real-display takeover for input-sensitive pages. Real-display takeover now routes
through a named --display-provider abstraction (currently backed by
KasmVNC) rather than a hardcoded backend, so the display layer is a plug-in
point, not a fixed dependency.
The skill tells the agent to reach for afhttp first
The embedded Agent Skill was rewritten around one rule: when the user names a
concrete URL, fetch it — don’t web-search it first. The old guidance let an
agent answer a “read this page” request from search results; the new skill
documents the default fetch flow, the direct-access and takeover flows, and
requires afhttp container status before container install so the agent
reuses a host instead of stacking new ones. Container install itself was hardened
for display-capable hosts (rebuild, --from-source, --with kasmvnc), and
container is now accepted as a CLI alias for the Apple runtime.
Where this fits
v0.5.0 gave afhttp a browser so it could reach pages a shell request can’t. v0.6.0 makes it honest about what it reached: a fast-path 200 that’s really a bot wall gets caught and escalated, and every fetch carries a trace that says where the time went and why it captured. An agent-first tool’s job isn’t just to get bytes — it’s to never quietly hand the agent the wrong page.
brew install agentfirstkit/tap/afhttp # macOS / Linux
scoop bucket add agentfirstkit https://github.com/agentfirstkit/scoop-bucket
scoop install afhttp # Windows
cargo install agent-first-http # any platform