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 single JSON objects on stdout unless otherwise stated. Artifact files are referenced by absolute *_file paths in those JSON envelopes.

Every failure envelope 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.
errorHuman-readable detail for logs.
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.

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.
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_urlComplete short-lived takeover display URL with handoff=... for a human to open in a local browser.
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": "http://host.mesh.internal:9222/takeover/panel?handoff=…",
    "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 the 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:

{
  "code": "error",
  "error_code": "navigation_timeout",
  "error": "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 array of console/runtime events:

[
  {
    "timestamp_ms": 123,
    "level": "warning",
    "type": "log",
    "text": "deprecated API",
    "url": "https://example.com/app.js",
    "line": 10,
    "column": 5
  }
]

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.
urlPage URL at capture time.
titleDocument title.
viewportWidth, height, device scale factor.
framesFrame list with ids and urls.
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": 1,
  "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.
requestMethod, URL, headers, post-data metadata, and timing.
responseStatus, status text, URL, headers, mime type, protocol, remote address, sizes, and timing.
cacheCache/service-worker flags.
failureFailure text and cancellation status when the resource failed.
body_fileOptional captured response body path under network-bodies/.
body_base64_fileOptional captured binary body path when bytes are not UTF-8.
payload_hintsMechanical hints such as json_valid, json_top_level_type, graphql_operation_name, graphql_operation_type.

Sensitive request/response headers are redacted by default in network.json: cookie, authorization, proxy-authorization, set-cookie, and token/secret-like header names. --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:

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

CDP method errors return the standard error envelope 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.

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. 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--render: unknown mode "sometimes"Correct 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.