Protocol Reference

Reflects the v0.6.0 implementation. architecture.md is the canonical contract; this file documents the on-wire JSON shapes the CLI and SDK actually emit. Coverage of the ten artifact tokens and 11 CLI commands matches src/shared/, src/sdk/, and src/host/.

All command outputs are AFDATA protocol-v1 JSON events. With the default --output-to split, successes go to stdout and failures go to stderr; --output-to stdout|stderr selects one ordered event stream. afhttp host streams rather than answering once, so it defaults to --output-to stdout. Successes are {"kind":"result","result":...,"trace":{}}; artifact files are referenced by absolute *_file paths inside result. Failures are {"kind":"error","error":...,"trace":{}}.

Every failure error payload carries the standard fields below. afhttp fetch execution failures also include trace; parse/configuration failures from other commands may omit it.

FieldDescription
error.codeStable machine-readable enum. Agents match on this field, not error.message.
error.messageHuman-readable detail for logs.
error.retryableWhether retrying the same operation may help.
traceBest-effort timings and phase details on fetch execution failures.

Fetch Result

afhttp fetch returns one object when a URL acquisition attempt reaches an HTTP response or browser-rendered page state.

{"kind":"result","result":{"code":"fetch","request_id":"req","body_file":"/tmp/afhttp-out/req/body.html","rendered_html_file":"/tmp/afhttp-out/req/rendered.html","network_file":"/tmp/afhttp-out/req/network.json"},"trace":{}}
FieldPresentDescription
request_idalways on successPer-fetch id used in the default artifact directory.
request_urlalways on successURL originally requested by the agent.
statusif HTTP response existsFinal HTTP status code. 4xx/5xx are successful transport responses, not error envelopes.
final_urlalways on successURL after redirects/navigation.
tab_idwhen a browser tab was usedCDP target/tab id for follow-up afhttp cdp or afhttp fetch --tab.
content_typewhen the response carried oneResponse Content-Type verbatim. Prefer it over guessing from body_file’s extension: the extension comes from a lossy table and unknown types land in body.bin.
body_filewhen body requested and body existsRaw HTTP response body path.
rendered_html_filewhen producedSerialized post-JS DOM path.
text_filewhen produceddocument.body.innerText path.
content_filewhen producedAgent-oriented composed page view (content.md); the artifact to read first.
content_json_filewhen producedStructured form of content with link/action candidates (content.json).
screenshot_filewhen producedFull-page PNG path.
network_filewhen producedDeep network log path.
console_filewhen producedConsole-event log path.
observation_filewhen producedAgent-readable accessibility/DOM snapshot path.
storage_filewhen storage requested and producedlocalStorage/sessionStorage/IndexedDB-name snapshot path.
download_filewhen navigation becomes a downloadCaptured browser download path inside the active profile.
download_byteswith download_fileCaptured file size in bytes.
download_filenamewith download_fileBrowser-selected filename.
download_urlwith download_fileURL that triggered the download.
download_statewith download_fileMechanical state, currently "completed".
warningsif non-emptyPer-artifact or per-entry non-fatal failures.
next_actionfetch --takeover, when human takeover is neededPresent only when --takeover could not reach the target with the warmed profile. See Next action.
tracealwaysRender decision, readiness, phase timings, bytes, and escalation signals.

Example:

{
  "code": "fetch",
  "request_id": "req",
  "request_url": "https://example.com/",
  "status": 200,
  "final_url": "https://example.com/",
  "tab_id": "page-1",
  "body_file": "/tmp/afhttp-out/req/body.html",
  "content_file": "/tmp/afhttp-out/req/content.md",
  "content_json_file": "/tmp/afhttp-out/req/content.json",
  "rendered_html_file": "/tmp/afhttp-out/req/rendered.html",
  "text_file": "/tmp/afhttp-out/req/text.txt",
  "screenshot_file": "/tmp/afhttp-out/req/page.png",
  "network_file": "/tmp/afhttp-out/req/network.json",
  "console_file": "/tmp/afhttp-out/req/console.json",
  "observation_file": "/tmp/afhttp-out/req/observation.json",
  "storage_file": "/tmp/afhttp-out/req/storage.json",
  "trace": {
    "render_decision": "browser",
    "render_mode": "auto",
    "render_used": true,
    "main_request_observed": true,
    "current_stage": "complete",
    "duration_ms": 820,
    "timeout_ms": 30000,
    "navigation_duration_ms": 540,
    "wait_mode": "auto",
    "wait_satisfied_by": "network_quiet_dom_text_stable",
    "network_quiet": true,
    "dom_stable": true,
    "text_stable": true,
    "capture_reason": "wait_satisfied",
    "stages": [
      {"name": "navigate", "status": "ok", "duration_ms": 340},
      {"name": "wait_readiness", "status": "ok", "duration_ms": 180},
      {"name": "capture_rendered_html", "status": "ok", "duration_ms": 25}
    ]
  }
}

Warnings

Warnings do not fail the whole fetch.

FieldDescription
artifactArtifact token, for example screenshot, network, or observation.
codeStable warning/error code such as backend_unsupported or artifact_capture_failed.
detailHuman-readable detail.
request_idOptional network request id when the warning applies to one network entry.

Next action

afhttp fetch --takeover emits next_action only when the warmed profile did not reach the target and a human must drive the browser. When the profile already reaches the target, the result carries the usual artifacts and no next_action.

FieldDescription
kindCurrently "human_takeover".
takeover_url_secretComplete short-lived takeover display capability URL with handoff_secret=... for a human to open in a local browser. It is intentionally revealed only in explicit takeover/panel results.
takeover_url_expires_at_rfc3339Expiry time for the handoff URL. Default TTL is 900 seconds.
takeover_url_ttl_sHandoff TTL in seconds.
takeover_url_scopeScope for the capability; currently takeover, valid only under /takeover/*.
recommended_commandA ready-to-run afhttp fetch that re-fetches the same --tab once the human has cleared the wall.
{
  "code": "fetch",
  "final_url": "https://example.com/login",
  "tab_id": "page-7",
  "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 \"https://example.com/login\" --tab page-7 --endpoint-url ws://host.mesh.internal:9222 …"
  }
}

Trace

duration_ms, timeout_ms, current_stage, and stages are always present once fetch execution begins. Successful and failed fetch envelopes use the same trace shape.

FieldDescription
duration_msTotal wall-clock time.
timeout_msOverall fetch budget from --timeout-ms.
current_stageStage active at snapshot time; complete on successful completion.
render_decisionhttp_only when the HTTP fast path was used, browser when a CDP-driven render was used.
render_modeRequested render mode (none, auto, or always).
render_usedConvenience boolean mirroring whether the browser path actually ran.
escalation_reasonStable token describing why the browser path was taken. Values: "empty_html_shell" (HTTP returned a JS-bootstrap with no visible text), "http_status_NNN" (HTTP returned status NNN), "http_failed_<code>" (transport error, <code> is error.code).
main_request_observedWhether the main document request was observed by the active fetch path. HTTP-only successes set this true; browser-internal URLs like about:blank or cancelled navigations may set it false.
navigation_duration_msBrowser-path only: wall-clock from Page.navigate to the wait condition resolving.
wait_modeBrowser-path wait mode (auto, load, idle, selector, selector_visible, or ms).
wait_satisfied_byMechanical readiness condition that allowed capture, such as network_quiet_dom_text_stable, load, selector, or network_idle_event.
network_quiet / dom_stable / text_stable--wait auto booleans showing whether afhttp’s own network collector and DOM/text stabilization checks were satisfied at capture time.
capture_reasonWhy artifacts were captured, for example wait_satisfied, readiness_timeout, or download.
cookie_jar_fileAbsolute cookie jar path used for this fetch, when a jar was resolved.
cookie_jar_warningStructured note when /profile was unavailable and implicit cookie-jar persistence was disabled.
sensitive_captureNon-empty when --no-network-redact, --capture-ws, or --capture-sse may write tokens/PII into artifacts.
stages[]Ordered stage timings. Each stage has name, status, and duration_ms; status is one of ok, error, timeout, or started.

Failure envelopes for afhttp fetch also include this trace:

{
  "kind": "error",
  "error": {
    "code": "navigation_timeout",
    "message": "fetch timed out after 90000ms during capture_text",
    "retryable": true
  },
  "trace": {
    "render_decision": "browser",
    "render_mode": "always",
    "render_used": true,
    "current_stage": "capture_text",
    "duration_ms": 90001,
    "timeout_ms": 90000,
    "main_request_observed": true,
    "stages": [
      {"name": "navigate", "status": "ok", "duration_ms": 929},
      {"name": "wait_readiness", "status": "ok", "duration_ms": 5000},
      {"name": "capture_text", "status": "timeout", "duration_ms": 84072}
    ]
  }
}

Artifact Schemas

body_file

Raw main-resource response body. The file is not redacted or transformed except decompression when requested by the fetch path.

rendered_html_file

UTF-8 HTML serialization of the post-JS DOM. It is a browser artifact, not a readability or markdown conversion.

text_file

UTF-8 text from document.body.innerText. This is mechanical visible text extraction and does not include summarization.

screenshot_file

Full-page PNG. Missing screenshots produce a warning, usually backend_unsupported.

console_file

JSON object with schema_version: 2 and an events array:

{
  "schema_version": 2,
  "events": [
    {
      "timestamp_epoch_ms": 123,
      "level": "warn",
      "text": "deprecated API",
      "source_url": "https://example.com/app.js",
      "line_number": 10
    }
  ]
}

observation_file

Agent-readable page snapshot. It is intentionally smaller and more action-oriented than rendered.html, but it remains a mechanical projection.

Nodes include native interactive elements, explicit interaction markers (role, tabindex, contenteditable=true), iframes, and a bounded set of non-semantic elements whose computed cursor is pointer. Observation traverses open shadow roots and same-origin iframes. Cross-origin iframes are represented only by their iframe node plus frame_ref/frames[] metadata because their contents are not readable from the embedding page.

Top-level fields:

FieldDescription
schema_versionObservation schema version.
page_urlPage URL at capture time. Credential-bearing URL components are redacted.
titleDocument title.
viewportWidth, height, device scale factor.
framesFrame list with frame_id and frame_url.
nodesInteractive and meaningful visible accessibility/DOM nodes.
formsMechanical form ownership and control refs.
focused_refref of focused node, if any.
truncatedPresent when the global node or scan cap stopped traversal.

Node fields:

FieldDescription
refSnapshot-scoped opaque id. Not durable across observations.
frame_idOwning frame id.
roleAccessibility role or mechanical DOM role.
nameAccessible name when available.
textVisible text snippet when available.
visibleWhether the node is visible.
enabledWhether interaction is enabled.
bboxCSS-pixel bounding box.
actionsMechanical possible actions such as click, fill, select, check, focus.
href / srcURL-bearing attributes when present.
frame_refOn iframe nodes, the matching frames[].frame_id for the child frame entry.
value_redactedTrue when an input has a value that was intentionally not emitted.
selector_hintOptional best-effort selector hint for CDP resolution in the node’s context; iframe nodes use frame_ref, iframe children use frame-relative selectors, and shadow nodes use host >> shadow >> inner chains.
selector_hint_uniquePresent when selector_hint is present; true when it matches exactly one element in that node’s actual document/shadow context.

Traversal caps are global across the main document, open shadow roots, and same-origin iframe documents. When truncated is present it reports the mechanical reason and the node/scan limits; no truncation is silent.

Forbidden fields: intent labels, importance scores, page-type guesses, recommended actions, or captcha/paywall/login classification.

network_file

Deep network artifact. Top-level shape:

{
  "schema_version": 2,
  "main_request_id": "req-1",
  "entries": [],
  "summary": {
    "requests_total": 12,
    "responses_total": 11,
    "finished_total": 10,
    "failed_total": 1,
    "incomplete_total": 1,
    "inflight_total_at_capture": 1,
    "pending_by_resource_type": {"XHR": 1},
    "captured_body_files": 2,
    "redacted": true
  }
}

Each entries[] item may include:

FieldDescription
request_idStable request id from the browser backend.
stateMechanical lifecycle state: pending, responded, finished, or failed.
redirect_from_request_idPrior request id for redirect chains.
frame_id / loader_idCDP frame/loader ids when known.
resource_typeDocument, XHR, Fetch, Script, Stylesheet, Image, etc.
initiatorCDP initiator type and stack when available.
request_urlRequest URL. URL userinfo and known secret query parameters are redacted by default.
methodHTTP request method.
status / mime_typeResponse status and MIME type when a response was observed.
request_headers / response_headersCaptured headers, with credential-bearing values redacted by default.
request_post_data_present / request_post_data_size_bytesPresence and size metadata; request bodies are not copied into the entry.
timing.start_monotonic_ms / timing.end_monotonic_msMonotonic offsets for ordering and elapsed-time calculations. They are not wall-clock timestamps.
failureFailure text and cancellation status when the resource failed.
body_fileOptional captured response body path under network-bodies/.
hintsMechanical payload hints such as JSON and GraphQL metadata.

Sensitive request/response headers and credential-bearing URL components are redacted by default in network.json: cookies, authorization values, token/secret-like header names, URL userinfo, and known secret query parameters. --no-network-redact disables this for trusted local debugging and may write raw tokens, cookies, and PII into network.json; trace.sensitive_capture records that opt-in.

Network body capture modes:

ModeBehavior
offMetadata only.
xhrCapture text/JSON XHR/fetch bodies up to the configured per-body limit.
allAttempt every exposed response body up to the configured per-body limit.

Body capture failures become warnings, not fetch failures. --capture-ws and --capture-sse write WebSocket/SSE payloads to frame/event files and may expose bearer tokens, session identifiers, chat content, or other PII.

When --wait auto is used, XHR/fetch/EventSource body capture is enabled by default so the network artifact can explain pages whose visible DOM is only a shell. Pending requests are not hidden: network.summary.incomplete_total, network.summary.inflight_total_at_capture, and network.summary.pending_by_resource_type stay non-zero, and entries remain in state: "pending" or state: "responded".

CDP Result

afhttp cdp wraps the raw CDP method result under result so CDP fields cannot collide with the top-level command envelope:

{"kind":"result","result":{"code":"cdp","result":{"result":{"type":"number","value":42}}},"trace":{}}

CDP method errors return the standard error event with error.code: "cdp_error" or error.code: "cdp_timeout".

Health Result

Authenticated /health and afhttp health return:

FieldDescription
codeAlways health.
statusok, starting, or degraded.
versionafhttp version.
uptime_sHost uptime in seconds.
backendBrowser family/version/connected status.
backend_errorStructured backend/CDP error summary when status is degraded.
profileCurrent profile kind/name/lock summary.
tabs_activeCurrent browser page target count from Target.getTargets.
capabilities_urlRelative URL for capabilities.

Unauthenticated public health, when enabled, returns only status.

Capabilities Result

/capabilities and afhttp capabilities return:

FieldDescription
codeAlways capabilities.
backendBrowser family/version.
artifactsPer-artifact supported booleans and notes.
wait_modesSupported wait modes.
takeoverHuman-takeover panel support: backend_capable (whether the backend can expose a panel at all, true for Chromium-family and camoufox, false for lightpanda), supported (whether this host has a panel enabled now), and panel_url/provider (the concrete screen-share method, e.g. kasmvnc) when enabled.
profilePersistent/ephemeral support.
featuresImplemented feature support such as selector_visible, network_body_capture, capture_ws, capture_sse, takeover, recent_requests, and profile_persistence; risky captures include a risk string.
limitsDefaults and hard limits relevant to fetch planning.

Capabilities describe support; they do not guarantee a later page-specific artifact capture will succeed.

UI Takeover Events

afhttp ui takeover holds the panel open and blocks, so it is a stream of two events rather than one result. It emits a progress event once the panel is resolved and announced, immediately before it starts waiting:

{"kind":"progress","progress":{"code":"ui_takeover","panel_url":"http://127.0.0.1:9222/takeover/panel","session":"watch","mode":"window"},"trace":{}}

and one terminal result when the session ends:

{"kind":"result","result":{"code":"ui_takeover","panel_url":"http://127.0.0.1:9222/takeover/panel","session":"watch","mode":"window","outcome":"closed","open_s":184},"trace":{}}
FieldDescription
panel_urlWhere the upstream panel is served, without its credential. The takeover credential never appears in these events.
sessionAlways watch. A VNC canvas has no submit control, so there is nothing for the person to confirm and no typed result to collect.
modeWhich of AFUI’s three deliveries this run resolved to: window, link, or session. --mode names it; with no flag, AFUI_DELIVERY does, and failing that it is window.
link_urlPresent only for link: the AFUI-owned one-session LAN page. It is a bearer capability — anyone holding it reaches the panel — published here because handing it to a person is what link is for, and published under a name no redactor masks for the same reason. The upstream takeover credential remains private behind AFUI’s proxy.
session_idThe identity to use with afui session open, serve, or close.
idle_timeout_s, grace_period_sPresent only for link: the global AFUI attention policy applied to that remote page.
outcomeclosed when the local window closed, expired when an AFUI Link’s attention policy lapsed, and stopped when the command was interrupted. A registered Session has no AFUI deadline.
open_sHow long the session was open.

External Runtime Dependencies

The distributed afhttp binary does not bundle browser engines or KasmVNC. It locates external tools on PATH (or explicit flags where available) and spawns them as separate processes:

DependencyUsed byNotes
Chromium/Chrome/Edge/Brave/fingerprint-chromiumBrowser-backed fetch, screenshots, real-display takeoverSet --browser-bin to override discovery for the primary browser binary.
lightpanda--browser lightpandaRendering subset; no display takeover.
foxbridge + camoufox--browser camoufox--browser-bin may point at foxbridge; camoufox is discovered separately on PATH.
KasmVNC Xvnc--takeover-provider kasmvncGPLv2 external process only. Install it in the container and ensure Xvnc plus the KasmVNC web root are present; optional env overrides are AFHTTP_KASMVNC_BIN and AFHTTP_KASMVNC_WEB_ROOT.
matchbox-window-manager (or openbox)KasmVNC display providerOptional. Keeps the headful browser maximized so the client can resize the framebuffer to the operator’s window (resize=remote); absent, the panel falls back to scaled rendering. Discovered on PATH.

Profile Results

Profile lifecycle commands are local-only.

profile list

{
  "code": "profile_list",
  "profile_root": "/Users/me/.local/share/afhttp/profiles",
  "profiles": [
    {
      "backend": "brave",
      "name": "work",
      "path": "/Users/me/.local/share/afhttp/profiles/brave/work",
      "locked": true,
      "metadata_present": true,
      "last_used_at_rfc3339": "2026-05-27T01:23:45Z",
      "size_bytes": 123456789
    }
  ]
}

profile info

Returns one profile object with backend, metadata, lock owner when known, approximate size, path, and browser-family hints. Pass --backend when multiple backend scopes contain the same logical profile name.

profile lock-status

Returns locked, and when available owner_pid, owner_started_at_rfc3339, and owner_command.

profile downloads

Read-only listing of files the browser captured in the profile download directory. Completed files report state:"completed"; Chromium partial files ending in .crdownload report state:"in_progress".

{
  "code": "profile_downloads",
  "backend": "brave",
  "name": "work",
  "download_dir": "/Users/me/.local/share/afhttp/profiles/brave/work/downloads",
  "downloads": [
    {
      "filename": "report.csv",
      "path": "/Users/me/.local/share/afhttp/profiles/brave/work/downloads/report.csv",
      "size_bytes": 12345,
      "state": "completed"
    }
  ]
}

profile delete / profile prune

profile delete returns the deleted profile name. profile prune returns the resolved profile_root, dry_run, and the matching profile entries. Locked profiles are skipped; missing profiles return profile_not_found.

Error Codes

Agents should branch on error.code, not the human-readable error.message. Examples below are representative error / Chromium errorText strings.

CodeExample detailAgent should
navigation_timeoutWait::Load: readyState never became completeRetry with a longer timeout or weaker wait condition; preserve artifacts already written.
wait_selector_unmatchedselector "#ready" did not appear before --timeout-msDistinguish from navigation_timeout: page itself loaded fine, only the CSS selector never matched. Verify the selector against the captured observation.json rather than blind-retrying.
render_unavailablebrowser fetch requires --endpoint-url pointing at an afhttp hostStart/connect a browser host or use --render none if HTTP-only is enough.
host_unreachableCDP connect ws://127.0.0.1:9222/cdp: connection refusedCheck the afhttp host endpoint/token and retry after the host is reachable.
dns_resolution_failednet::ERR_NAME_NOT_RESOLVEDCheck spelling/DNS/network; retry later only if DNS may recover.
target_unreachablenet::ERR_CONNECTION_REFUSEDCheck target service/firewall/port; retry when the target is reachable.
tls_errornet::ERR_CERT_AUTHORITY_INVALIDDo not blind-retry; fix trust/certificate settings or choose an HTTP-safe route.
tab_crashedTarget.detachedFromTarget: target crashedReopen the tab and retry the operation.
profile_lockedprofile "work" is already locked by pid 1234Reuse that host/profile or wait for the lock holder to exit.
browser_launch_failedchromium exited before DevTools endpoint appearedInspect browser path/dependencies/display and retry after fixing launch.
cdp_unavailablewait_event: events channel closedVerify the endpoint speaks afhttp CDP and reconnect.
cdp_errorCDP error -32000: No target with given id foundFix the method/params/tab id; retry only if the target may reappear.
cdp_timeoutwait_event: timed outRetry with a longer timeout or different event/wait strategy.
backend_unsupportedPage.captureScreenshot not supported by backendDrop that artifact/action or switch to a backend that supports it.
artifact_capture_failedDOM.getOuterHTML: missing outerHTMLUse other artifacts if sufficient; retry if the page/backend state changed.
artifact_capture_timeoutfetch timed out after 30000ms during capture_screenshotUse the partial result or retry with a larger --timeout-ms / smaller artifact set.
artifact_emptytext artifact was empty after trimming whitespaceCheck trace readiness fields and network.json; the page may still be a shell.
artifact_tinytext artifact was 12 bytes; threshold is 32Inspect network bodies or retry with a larger timeout if the page is still rendering.
bot_wall_detecteddetected bot wall/security challengeTreat the page as unverified target content; use next_action / fetch --takeover instead of answering from the challenge page.
security_challenge_detecteddetected security challenge/access denied pageTreat the page as unverified target content; use human takeover or report that verification failed.
network_not_idle1 request(s) were still pending/responded at captureInspect pending entries and captured XHR bodies before deciding whether to retry.
pending_xhr_at_capturepending XHR/fetch/EventSource at captureTreat status: 200 as incomplete acquisition until network payloads are checked.
observation_emptyobservation contained zero projected nodesFall back to rendered/text/network artifacts or retry after readiness improves.
readiness_timeout--wait auto captured before all readiness signals settledPartial capture succeeded; inspect warnings/trace/network before bounded retry.
network_body_truncatedbody for 1234.1 truncated to 10485760 bytesIncrease --network-body-max-bytes if the omitted suffix matters.
profile_not_foundprofile "work" does not existCreate/select an existing profile name.
profile_delete_lockedprofile "work" is locked; refusing deleteStop the owning host before delete/prune.
profile_invalid_nameprofile name "../work" is invalidUse a simple non-hidden profile name without path separators.
profile_root_unavailableprofile root cannot be created: permission deniedFix filesystem permissions/path or choose another profile root.
invalid_argument--takeover-quality-percent: expected an integer from 0 to 100, got 140Correct the CLI/SDK argument before retrying.
invalid_endpointendpoint must start with ws://, http://, or unix:Correct endpoint syntax.
io_errorwrite /out/body.html: permission deniedFix local filesystem permissions/space/path and retry.
internal_errorserialize observation: ...Treat as a bug; capture logs and file an issue if reproducible.

Argv rejections

The CLI is compiled from a closed cli-spec-v1 registry, so an argv that does not match exactly one registered shape is rejected before any work starts. The rejection names the rule it broke in error.code, carries a hint pointing at the command’s --help, and exits 2.

error.codeMeaning
cli_unknown_commandThe command path is not registered.
cli_unknown_argumentThe argument is not registered for that command.
cli_unregistered_combinationEvery argument is known, but the mix is not a registered shape.
cli_missing_argument_valueAn argument that takes a value was given none.
cli_invalid_argument_valueThe value is outside the argument’s declared type or value set.
cli_duplicate_argumentA non-repeatable argument appeared more than once.
cli_unexpected_positionalMore positionals were supplied than the command declares.
cli_invalid_utf8An argv token is not valid UTF-8.