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`.
An agent reads far more of a tool’s output than a human ever does, and it reads it literally. v0.9.0 is a pass over every byte afhttp emits, asking one question each time: can a consumer act on this without guessing? Not the pages it fetches — those were already faithful — but the envelopes, the artifacts, the help text, and the stream each one lands on.
--help is a surface you can explore, not a wall of text
afhttp --help used to print the whole CLI. Every command, every flag, every
caveat — thousands of tokens for an agent that wanted to know whether fetch
takes a --profile. Now bare help comes back as an AFDATA result scoped to a
single level:
afhttp --help
{
"kind": "result",
"result": {
"code": "help",
"help": {
"command_path": "afhttp",
"scope": "one_level",
"arguments": [...],
"subcommands": [{"name": "fetch", "about": "Fetch a URL"}, ...]
}
},
"trace": {}
}
Ask for one command and you get that command, with a note about what it inherits rather than a repeat of the globals:
afhttp fetch --help
Each flag carries a one-line summary at this level; the long-form caveats — when
--takeover needs a running host, why --cookie-jar is for forensics and not
production — live in the detail paragraph, which surfaces only when you ask for
it. --recursive expands the full tree when you genuinely want an index.
--output plain gives conventional human help, and --output markdown is what
generates this repository’s CLI reference.
Two rough edges went with it: clap’s help pseudo-subcommand is no longer
advertised as if it were a real command, and running afhttp with no command
returns a structured invalid_argument error instead of dumping usage text at
whatever was reading stdout.
Results on stdout, errors on stderr
Every afhttp command emits exactly one protocol-v1 event. Until now that event
always went to stdout, success or failure — which meant URL=$(afhttp fetch ... )
captured error JSON just as happily as a result, and a shell pipeline had to
parse before it could branch.
v0.9.0 adds a global --output-to:
afhttp fetch "$URL" # split (default): result → stdout, error → stderr
afhttp fetch "$URL" --output-to stdout # one ordered stream on stdout
afhttp fetch "$URL" --output-to stderr # one ordered stream on stderr
The default split means an exit status and a redirect are now enough to tell
the two apart. stdout and stderr keep every event on one ordered stream for
callers that would rather read a single channel. The routing is process-wide and
applies to the discovery output above as well, so --help and --version land
where the rest of the events do.
Secrets don’t ride along in URLs
afhttp already refused to write *_secret fields into its own output. But URLs
are where credentials actually hide in browsing work — a password in userinfo, a
?token=, an OAuth ?code= mid-flow — and afhttp writes URLs into a lot of
places: network.json entries, console event origins, observation frame lists,
storage snapshots, the host’s /recent-requests ring, and every *_url field in
a result or error envelope.
All of them now go through one redaction pass:
https://user:pass@example.test/callback?code=abc&state=ok
https://user:***@example.test/callback?code=***&state=ok
The parameter list is exact and deliberately narrow — token, access_token,
api_key, code, password, sig, signature, and their close relatives —
and it is applied only while parsing a URL, never to ordinary structured field
names, so a result whose code field says "fetch" stays readable. _secret
fields keep their existing treatment.
This does not touch captured page data. The body artifact, the composed
content.md, the response bytes — those are what you asked for, and they pass
through byte-for-byte. --no-network-redact is still the explicit opt-out for
trusted local debugging, and trace.sensitive_capture still records that you
took it.
The takeover URL is a secret that names itself
Human takeover hands a person a short-lived capability URL that drives the
agent’s browser. That is exactly the kind of value that should be redacted by
default — and it now is. The field is next_action.takeover_url_secret, the
query parameter inside it is handoff_secret=, and both follow afhttp’s
_secret convention wherever they appear:
{
"kind": "human_takeover",
"takeover_url_secret": "http://host:9222/takeover/panel?handoff_secret=…",
"takeover_url_expires_at_rfc3339": "2026-07-29T08:15:00Z",
"takeover_url_ttl_s": 900,
"recommended_command": "afhttp fetch https://… --takeover --tab page-7"
}
The narrow exception is the point of the whole feature: afhttp panel,
afhttp fetch --takeover, and the host’s handoff endpoint reveal that one
field — and only that field — because their entire job is putting it in front of
a human. Everything else in the same payload, including the long-lived host
token, stays masked.
Artifacts that say what they hold
Field names that were ambiguous under an agent’s literal reading are now
self-describing, and every affected artifact moves to schema_version: 2:
| Artifact | Was | Now |
|---|---|---|
observation.json | url, frames[].url | page_url, frames[].frame_url |
network.json | entries[].url | entries[].request_url |
network.json | timing.start_ms / end_ms | start_monotonic_ms / end_monotonic_ms |
console.json | timestamp_ms, url | timestamp_epoch_ms, source_url |
storage.json | url | page_url |
host_ready | listen | listen_address |
The timing rename is the one worth pausing on. Those numbers were never wall
clock — they are CDP monotonic offsets, good for ordering and elapsed time and
meaningless as timestamps. start_monotonic_ms says so; start_ms invited
exactly the wrong arithmetic. Console events, by contrast, really are epoch
milliseconds, so they say timestamp_epoch_ms.
Underneath
afhttp moves onto agent-first-data 0.26.1 — the emitter, the output routing, and the redaction policy above all come from the shared contract every spore in the kit speaks. base64 goes to 0.23, and the pinned chrome-for-testing backend advances to 151.0.7922.47. The Docker test harness now mounts the spore on its own with an optional sibling afdata checkout, so the local gate runs the same way inside the monorepo and in the standalone repository.
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