Agent-First HTTP v0.6.0: A 200 Isn't Proof

by Agent-First Kit Contributors

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:

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