Agent-First HTTP

Give your AI agent its own private browser — so it reads the real page, past logins and bot walls, without ever touching yours.

Ask your agent: “Open this link and pull the pricing table out for me.”

What problem does this solve?

Agents are bad at opening pages. Ask one to read a specific URL and it tends to:

afhttp fixes this by loading the actual URL itself — falling back to a real browser of its own to render JavaScript when the page needs one — and returns the page as files the agent can inspect, so it answers from verified content instead of a guess.

That browser is fully isolated: it runs separately from the browser you use every day and never touches your cookies, logins, or history. When a page needs a login, captcha, or 2FA, you can take over that same isolated browser, clear the wall yourself, and let the agent continue — without ever mixing it into your own session.

The basics: hand it a URL, get the page back as data

Give afhttp a URL; it writes the page to disk and prints one line of JSON saying what it got:

$ afhttp fetch https://example.com
{"kind":"result","result":{"code":"fetch","request_url":"https://example.com","status":200,"final_url":"https://example.com/","body_file":"/tmp/afhttp-out/<id>/body.html"},"trace":{}}

That is the whole job: hand it a URL, get the page back as files an agent can read — never a terminal blob to scrape. CLI output follows AFDATA protocol v1: command data is in result; failures use kind: "error" with stable error.code values rather than guesses.

By default afhttp sends a plain HTTP request, returns the raw body_file, and only starts a real browser when the page actually needs one (--render none forces the fast path, --render always forces the browser, --render auto decides). A browser-backed fetch automatically captures more of what a human would look at — an agent-oriented composed page view (content.md, the one to read first), rendered HTML, a screenshot, a DOM observation, the network and console logs — each a flat *_file field on the same JSON, never nested:

{
  "kind": "result",
  "result": {
    "code": "fetch",
    "request_url": "https://example.com",
    "status": 200,
    "final_url": "https://example.com/",
    "body_file": "/tmp/afhttp-out/<id>/body.html",
    "content_file": "/tmp/afhttp-out/<id>/content.md",
    "content_json_file": "/tmp/afhttp-out/<id>/content.json",
    "rendered_html_file": "/tmp/afhttp-out/<id>/rendered.html",
    "text_file": "/tmp/afhttp-out/<id>/text.txt",
    "screenshot_file": "/tmp/afhttp-out/<id>/page.png",
    "network_file": "/tmp/afhttp-out/<id>/network.json",
    "console_file": "/tmp/afhttp-out/<id>/console.json",
    "observation_file": "/tmp/afhttp-out/<id>/observation.json"
  },
  "trace": {}
}

What afhttp covers

The hard part for an agent is not fetching bytes. It is that many useful URLs do not turn into a usable page from a simple shell request — they need JavaScript rendering, cookies, session state, or a real browser fingerprint. Where a human would open a browser and inspect, an agent needs those same facts as data it can branch on. afhttp covers the whole range:

The agent never has to parse a human-readable error message. Every CLI output is an AFDATA protocol-v1 JSON event, and every failure carries a stable error.code. See docs/architecture.md for the full contract.

Two roles: host and driver

afhttp splits into two roles that are independently locatable:

RoleCommandWhat it does
browser-hostafhttp hostLong-running foreground process. Holds Chromium + a profile. Exposes a CDP endpoint and optional real-display takeover.
agent-driverafhttp fetch, afhttp upload, afhttp cdp, afhttp panel, afhttp ui, afhttp health, afhttp capabilities, afhttp profile, afhttp tabs, or the Rust SDKShort-lived client. Connects to a host’s endpoint when needed, does work, writes artifacts locally.

Run the host where the browser needs to be (residential IP, GUI machine, datacenter); run the driver wherever the agent runs. Connectivity is your mesh’s problem, not afhttp’s. The CLI has 11 commands: host, fetch, upload, cdp, panel, health, capabilities, profile, tabs, skill, and container.

Running it: inline for a one-shot, a host for sessions

afhttp fetch <url> with no --endpoint-url runs inline — it spins up a sandboxed browser for that one fetch and tears it down. Zero setup; use it for stateless, one-shot acquisition.

For state that outlives a single fetch — a reused login, a warmed profile, human takeover — run a long-lived host in a container (the isolation boundary). One command builds the image from a recipe embedded in the binary and runs it (Docker, Podman, or Apple container, auto-detected — override with --runtime); it prints structured connection metadata and a ready-to-run driver command without exposing the long-lived host token by default:

afhttp container install

Once it is running, nothing has to be said about it again: every command that needs a host — fetch --takeover, fetch --profile, cdp, tabs, upload, health, capabilities, panel — discovers the standard local afhttp-host and reads its token from the container when --endpoint-url is omitted. No environment variable, no docker exec … cat.

container install is takeover-ready by default (Brave + KasmVNC + an ephemeral initial profile + a 2g /dev/shm). A takeover fetch switches to a persistent per-site profile derived from the URL, and lets a human clear a wall on the same browser the agent drives:

afhttp fetch "$URL" --takeover

If the warmed profile already reaches the target, fetch --takeover just returns the page. Otherwise it keeps a persistent tab open and returns a next_action with kind: "human_takeover", a takeover_url_secret for a human to open, and a recommended_command that re-fetches the same --tab once the wall is cleared. The capability URL is intentionally revealed only in this explicit takeover result; other _secret fields remain redacted. fetch --takeover needs a running host (auto-discovered locally, or supplied with --endpoint-url / AFHTTP_ENDPOINT_URL) and a browser render (--render auto or always); it does not auto-create containers.

Building from a source checkout instead? Use afhttp container install --from-source (or docker compose -f container/docker/compose.yaml up --build when driving the runtime directly). See docs/deployment.md for the full container setup, upgrades, and security posture.

Browser backends: meet each site with the engine it demands

afhttp is not “headless Chromium.” How hard a site fights back decides which engine actually reaches it, so afhttp drives a whole spectrum behind one CDP contract — pick one with --browser (or point --browser-bin at a binary):

Unsupported per-artifact operations return per-artifact warnings (backend_unsupported), not whole-fetch failures.

Human takeover: a person drives the same browser when a step needs it

When a fetch hits a login, captcha, or 2FA wall, afhttp fetch <url> --takeover keeps a persistent tab open on a takeover-ready host and hands back a complete short-lived takeover_url_secret a human opens to drive the same browser the agent is using, via real-display takeover backed by KasmVNC. Once the human is past the wall, the agent re-fetches the same tab to continue. Without --profile, takeover switches to a persistent profile derived from the URL’s registrable domain. The worked example below shows the full next_action payload.

Handing over a URL leaves the agent guessing when the person is finished. Where a window can open on the agent’s own machine, afhttp ui takeover opens the panel itself and blocks until they close it, so “done” is an event rather than a guess.

Worked examples

One-shot fetch, no host

The shortest path starts with pure HTTP. --render none never starts a browser. With the default --render auto, afhttp tries the same HTTP fast path first and only starts an inline ephemeral host if the response needs browser rendering.

afhttp fetch https://example.com
{
  "code": "fetch",
  "status": 200,
  "final_url": "https://example.com/",
  "body_file": "/tmp/afhttp-out/<id>/body.html",
  "trace": {
    "render_decision": "http_only",
    "render_mode": "auto",
    "render_used": false,
    "current_stage": "complete",
    "duration_ms": 120,
    "timeout_ms": 30000,
    "stages": [
      {"name": "navigate", "status": "ok", "duration_ms": 110},
      {"name": "capture_body", "status": "ok", "duration_ms": 12}
    ]
  }
}

Long-running host + remote fetch

For real workflows: start one afhttp host, drive it from anywhere.

# On the host machine (or in a systemd unit, tmux pane, docker container — your choice).
# A non-loopback listener (anything other than 127.0.0.1 / a unix: socket) serves
# full browser control over /cdp, so a --token-secret is required — the host refuses to
# bind otherwise:
mkdir -p ~/.afhttp && printf '{"work":{"token_secret":"%s"}}\n' \
  "$(head -c 32 /dev/urandom | base64 | tr '+/' '-_' | tr -d '=\n')" > ~/.afhttp/hosts.json
afhttp host --listen tcp:0.0.0.0:9222 --profile work --display headless \
            --token-secret file:$HOME/.afhttp/hosts.json#work.token_secret

# From the agent's machine. --token-secret names where the token is — a config
# file, an environment variable (env:NAME), or a local container
# (container:NAME) — so the secret never goes through the shell:
afhttp fetch --endpoint-url ws://host.mesh.internal:9222 \
             --token-secret file:$HOME/.afhttp/hosts.json#work.token_secret \
             --render auto --wait auto \
             --want rendered_html --want observation --want screenshot \
             --want network --want console \
             --network-bodies xhr \
             https://target.example.com/dashboard

The token secret gates /cdp, display takeover, and /profile; bind tcp:127.0.0.1:<port> or a unix: socket instead when the host and driver share a machine and you want to skip it. The profile persists across host restarts. Cookies and localStorage acquired in one fetch are available to the next.

Raw CDP escape hatch

When fetch is not enough — for example, evaluating arbitrary JavaScript in the target page:

# Against the local afhttp-host container, which is found rather than named:
afhttp cdp Runtime.evaluate \
  --tab abc123 \
  --params '{"expression":"document.querySelectorAll(\"a\").length","returnByValue":true}'
# {"result":{"type":"number","value":42}}

# Against a host elsewhere:
afhttp cdp Runtime.evaluate \
  --endpoint-url ws://host.mesh.internal:9222 \
  --token-secret file:$HOME/.afhttp/hosts.json#work.token_secret \
  --tab abc123 \
  --params '{"expression":"document.querySelectorAll(\"a\").length","returnByValue":true}'

No click / type / navigate wrappers. The agent talks raw CDP; afhttp only forwards.

Check health and capabilities

Before assigning work to a host, an agent or supervisor can ask what is alive and what the backend supports:

afhttp health --endpoint-url ws://host.mesh.internal:9222
# {"kind":"result","result":{"code":"health","status":"ok","backend":{"family":"chromium","connected":true},...},"trace":{}}

afhttp capabilities --endpoint-url ws://host.mesh.internal:9222
# {"kind":"result","result":{"code":"capabilities","artifacts":{"observation":{"supported":true},...},...},"trace":{}}

/health is for readiness. /capabilities is for planning artifact requests and avoiding predictable backend_unsupported warnings.

Human takes over (real-display takeover)

With the default local afhttp container install host running, fetch --takeover discovers its endpoint and token automatically:

afhttp fetch "$URL" --takeover

If the warmed profile already reaches the target, fetch --takeover just returns the content. Otherwise it keeps a persistent tab open and returns a next_action:

{
  "code": "fetch",
  "next_action": {
    "kind": "human_takeover",
    "takeover_url_secret": "http://host.mesh.internal:9222/takeover/panel?handoff_secret=…",
    "takeover_url_expires_at_rfc3339": "2026-06-11T08:15:00Z",
    "takeover_url_ttl_s": 900,
    "takeover_url_scope": "takeover",
    "recommended_command": "afhttp fetch \"$URL\" --tab page-7 --endpoint-url ws://host.mesh.internal:9222 …"
  }
}

A human opens the takeover_url_secret in a local browser and drives the real display (Brave on KasmVNC). Once they are past the wall, the agent runs the recommended_command to re-fetch the same tab and continue. The agent can stay CDP-attached the whole time. afhttp panel --endpoint-url … returns the same short-lived capability in takeover_url_secret. See docs/architecture.md §9 for the risk-control honest assessment.

Manage persistent profiles

Persistent browser profiles are local disk identities. Operators can inspect and clean them up without guessing which temp directory belongs to which host:

afhttp profile list
afhttp profile info work --backend brave
afhttp profile lock-status work --backend brave
afhttp profile downloads work --backend brave
afhttp profile prune --older-than 30d --dry-run
afhttp profile delete old-work --backend brave --confirm old-work

Profile lifecycle commands are local-only; downloads only lists captured files, and destructive commands refuse locked profiles. Profile names are logical and persistent storage is backend-scoped, so work under Brave and work under Chromium are different directories.

From Rust

The library is a thin SDK over the same endpoint protocol. It is not an embedded browser engine; it talks to a running afhttp host over CDP.

use afhttp::{Client, RenderMode, Wait, Artifact};

let client = Client::connect("ws://host.mesh.internal:9222")?;

let result = client.fetch("https://target.example.com")
    .render(RenderMode::Auto)
    .wait(Wait::Auto)
    .timeout(Duration::from_secs(30))
    .want([Artifact::RenderedHtml, Artifact::Observation, Artifact::Screenshot])
    .send()
    .await?;
// result.rendered_html_file -> path on this machine's disk
// result.observation_file -> agent-readable page snapshot

// Dev / test convenience: spawn a host subprocess, use it, kill on drop.
// Requires the `host` feature — pure `features = ["sdk"]` consumers
// connect to an afhttp host started separately.
let local = Client::inline_ephemeral().await?;

Consumers depend on the crate with default-features = false, features = ["sdk"] and link only the client weight — no Chromium, no chromiumoxide, no browser-launch code.

Cross-spore collaboration

afhttp does not operate in isolation. Here is how it fits with the rest of the agentfirstkit suite:

afmail: CAPTCHAs and mail-borne login flows

When a page requires an emailed verification link or OTP, hand off to afmail rather than polling IMAP yourself:

  1. afhttp fetch navigates to the login form and submits credentials.
  2. The page sends an email. The agent calls afmail triage (or afmail fetch) to find the message, extract the link or code.
  3. The agent feeds the link/code back to afhttp via --evaluate-after-wait or a subsequent afhttp fetch.

afhttp handles the browser-side state; afmail handles the mailbox-side state. They share no storage and are always driven by the agent — never by each other.

afpay: profile reuse for payment-gated pages

afhttp holds the browser session (cookies, localStorage) that proves the agent is a logged-in subscriber. afpay handles the wallet and transaction side. The coordination point is the persistent profile:

afdata: field naming alignment

afhttp response fields follow afdata suffix conventions (suffix-typed names: _file, _ms, _url). When an agent passes afhttp artifacts to afdata for extraction, the field shapes should be predictable without a schema lookup. If you add new fields to fetch responses, match the suffix table in the afdata SDK docs.

Adopt it: hand afhttp to your agent

Supported platforms: macOS, Linux, Windows.

The quickest way to find out whether afhttp earns a place in your toolkit is to let your agent read it and tell you. Paste this to your agent:

Read what Agent-First HTTP is at https://agentfirstkit.com/agent-first-http, then tell me in plain terms what it would do for me and whether it fits what I’m working on. If it’s a fit, install it — the prebuilt package for the quick path, or build from source after a quick security review of the repo if you’d rather read what you run — then run afhttp skill install so you follow its behavior rules.

If it’s a fit, install it — a prebuilt package, or from source if you want to read it first:

# prebuilt binary
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, from crates.io

# or build from source after reviewing the repo
git clone https://github.com/agentfirstkit/agent-first-http
cargo install --path agent-first-http

Then install the embedded Agent Skill so the agent follows afhttp’s behavior rules — when to escalate --render, when to reach for human takeover, how to read the artifacts. skill install targets Codex, Claude Code, opencode, and Hermes; skill status reports whether each install is present, valid, and current:

afhttp skill install
afhttp skill status

To remove it, run afhttp skill uninstall.

Docs

Agent-First HTTP v0.13.0: The Jar Believed the Sender

A profile's cookie jar holds the logged-in state of every site that profile visits, and it took each response's Domain attribute at its word — so a page anywhere could write, and by RFC identity replace, the session cookie of a site it had nothing to do with. That is fixed, along with Max-Age, three errors that quoted the credential they were given, and a takeover panel you can now hand to another device.

Agent-First HTTP v0.12.0: Two Ceremonies Before the Command

Reaching a local host cost two steps before the actual command: read the token out of the container by hand, then push it through the shell as an environment variable. This release makes discovery answer for every command, gives --token-secret a typed source grammar, and turns the takeover panel into a window whose closing is the signal.

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.

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

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.

Agent-First HTTP v0.9.0: Output You Can Trust

v0.9.0 reworks everything afhttp emits. `--help` is now a compact, progressively-scoped AFDATA result an agent reads one command at a time instead of a thousand-line dump. A new global `--output-to` routes results to stdout and errors to stderr by default, so a shell pipeline and a machine consumer finally agree. Credential-bearing URLs are scrubbed everywhere afhttp writes them — network.json, console origins, observation frames, storage snapshots, the host's recent-requests ring — with userinfo passwords and token-shaped query parameters masked before anything reaches disk. The human-takeover capability becomes `takeover_url_secret` behind a `handoff_secret=` parameter, redacted by default and revealed only by the one command that exists to hand it to a person. And every artifact moves to schema_version 2 with field names that say what they hold: `page_url`, `request_url`, `timestamp_epoch_ms`, `start_monotonic_ms`.

Agent-First HTTP v0.8.0: Every Build Says Exactly What It Is

v0.8.0 makes `afhttp --version` a structured event an agent can branch on — `name`, `display_name`, `version`, and a `build` field carrying the exact commit SHA the binary was built from, so a bug report or a capability check pins the precise tree, not just `0.8.0`. Under the hood it moves onto agent-first-data 0.22 and its protocol-v1 CLI contract, folds in the retired agent-first-config crate, and refreshes the stealth backends (chrome-for-testing 151, camoufox 152). The embedded skill moves to the standard `SKILL.md` layout and installs to Hermes alongside Codex, Claude Code, and opencode. And the README is now the single canonical guide — coverage map, host/driver roles, and worked examples end to end.

Agent-First HTTP v0.7.0: One Flag for the Hard Sites, Brave by Default

v0.7.0 collapses the hard-site dance into a single flag: `afhttp fetch <url> --takeover`. It auto-discovers the local host, gives each site its own isolated profile by default, and only asks a human to step in when a wall actually blocks it. `container install` is now takeover-ready out of the box on Brave — built-in ad/tracker blocking and the browser a human drives, no `--with` flags to remember. Secrets are redacted from agent-visible output by default, with an explicit `--reveal-token-secret` to opt in. The whole release is one thing: make afhttp simpler for an agent to use and harder to use wrong.

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.

Agent-First HTTP v0.5.0: One Command to a Browser Host, in Any Runtime

v0.5.0 adds `afhttp container`: bring up the browser host in one command — Docker, Podman, or Apple Container — from a recipe embedded in the binary, no source tree required.

Agent-First HTTP v0.5.0: When the Page Needs a Browser

v0.5.0 turns afhttp from an HTTP client into a full URL-acquisition tool. A single `afhttp fetch` covers the whole range — a plain HTTP request when that works, a real browser when it doesn't — and returns the page plus structured artifacts (rendered HTML, a DOM observation, a screenshot, network and console logs) an agent can branch on. It adds a browser-host / agent-driver split, a raw CDP escape hatch, deep network capture, an ops panel with optional real-display takeover for human login/captcha/2FA, and persistent profiles. The public contract converged in the process: flat `*_file` artifact paths, one profile per host, and no legacy aliases.

Agent-First HTTP v0.4.3: Release Hardening for Agent Tools

The v0.4.3 release moved afhttp into Agent-First Kit, tightened structured-output discipline, and hardened cross-platform packaging.

Agent-First HTTP v0.4: A Narrower Runtime with Complete Help

The v0.4 line removed the MCP server surface, generated CLI docs from clap definitions, and made --help complete for agents.

Agent-First HTTP v0.3.4: Output Formats Preserved Responses

The v0.3.4 update extended yaml/plain rendering to pipe mode while protecting server response bodies from formatter reinterpretation.

Agent-First HTTP v0.3.2: Requests Became Previewable

The v0.3.2 update added dry-run previews and actionable hints, so agents can inspect an HTTP request shape before it touches the network.

Agent-First HTTP v0.3: One Request, One JSON Line

The early Agent-First HTTP release line: a structured HTTP client for agents that turns requests, streaming bodies, and transport failures into stable JSON events.