Skip to content

Rust CDP Transport Research — 2026-07-12

Context and method

This research grounds epic-rust-cdp-capture-foundation-cdp-transport-gate. Krometrail needs a browser-level Chrome DevTools Protocol (CDP) connection with flat target sessions, typed control operations, raw protocol escape hatches, and a screencast event path that Krometrail—not the library—can supervise and bound.

Evidence was frozen on 2026-07-12 from crates.io metadata, published/repository source at cdpkit 0.4.0 (15dd5e6d) and chromey 2.52.0 (6eeca5e8), their CI/tests, their public GitHub issues, and the final5 schema-v2 qualification. The final5 reports and generated decision were independently normalized, redaction-checked, decisively validated, and recomputed at exact revision a0e98ad6bd9c53d10385020bc43629f7ac246173. They preserve bounded sanitized canonical fixture, wire, and runtime material and recompute all summaries from it. The contract uses observed capture_elapsed_seconds and handoff_elapsed_seconds measurements and measures acknowledgement from returned frame to ack completion only. These artifacts describe the qualification spike and do not substitute for production runtime evidence; production lifecycle, reconnect supervision, capture ingestion, recording, and bounded handoff are implemented separately in krometrail-cdp.

Caller-aware research ran direct-read only: the caller prohibited subagents and questions, and the bounded candidate/source surfaces did not warrant delegation. Reversible uncertainties are therefore converted into explicit spike gates below.

Project gates

A viable transport must provide:

  1. compatible maintenance, license, and minimum supported Rust version (MSRV);
  2. typed Page, Target, Runtime, Accessibility, and Input commands;
  3. browser-level commands over the browser WebSocket;
  4. flat target attachment and correct sessionId command/event routing;
  5. arbitrary command send and named raw event subscription without waiting for regenerated bindings;
  6. Page.startScreencast, Page.screencastFrame, and prompt Page.screencastFrameAck;
  7. an explicit protocol-drift strategy;
  8. connection loss surfaced cleanly so Krometrail owns reconnect and target restoration; and
  9. deterministic fake-WebSocket tests plus real-browser qualification.

Evidence matrix

Gatecdpkit 0.4.0chromey 2.52.0Minimal owned transport
Health / license / MSRVMIT OR Apache-2.0; Rust 1.75. Released 2026-06-26, 8 releases since 2026-02, but only 406 total downloads, one contributor, one star: active but very young and single-maintainer.MIT OR Apache-2.0; Rust 1.70. Released 2026-07-03, 290k total downloads, 50 stars, mostly one maintainer: active with materially more field use.Krometrail's MIT / Rust 1.85 policy; tokio-tungstenite 0.30.0 itself requires Rust 1.85. Maintenance burden transfers to Krometrail.
Typed required domainsSource-generated modules expose Page, Target, Runtime, Accessibility, Input, and Browser commands, including GetFullAxTree, DispatchMouseEvent, Evaluate, GetVersion, and SetAutoAttach.Generated spider_chromiumoxide_cdp commands cover the same domains; public Browser::execute<T: Command> and Page::execute<T: Command> are typed.Must generate a pinned, narrow contract from official protocol JSON/PDL. Hand-copying a growing command set is not acceptable.
Browser-level connectionCDP is explicitly browser-level; typed and raw commands sent through &CDP omit sessionId. Direct browser-WebSocket and /json/version discovery APIs exist.Browser::connect discovers /json/version; Browser::execute submits commands without a session.Straightforward envelope routing, but discovery, limits, timeouts, close behavior, and pending-request cleanup become owned code.
Flat sessions / routingAttachToTarget and SetAutoAttach default flatten: Some(true) in 0.4.0. Session/OwnedSession insert sessionId; event listeners filter it. Source and mock tests pass, but real target ordering is unproved.Handler initialization sets flatten(true) and maintains session-to-target maps. It has substantial mock and real-Chrome tests, but issue #8 documents a prior target/session ordering hang fixed in March 2026.Full control and best observability, but Krometrail must correctly own correlation, detach races, and pending calls.
Raw commandSender::send_raw(method, Value) works at browser or session scope and returns Value.No direct method-string API, but public, unsealed Command and Method traits allow a local generic value command. This is an adapter shim, not a first-class API.Native by design.
Raw event subscriptionSender::event_stream::<Value>(name) subscribes before dispatch and session-filters, but returns only params; there is no wildcard/full-envelope stream. Typed decode failures are logged and skipped.CustomEvent supports named event subscriptions and CdpEvent::Other, but events pass through the generated event decoder first. Issue #5 is real protocol-drift evidence: one newly observed enum value caused WebSocket event deserialization failures until bindings changed. No wildcard/full-envelope public stream.Can preserve {method, sessionId, params} before optional typed decoding and is the strongest drift boundary.
Screencast / ackTyped StartScreencast, ScreencastFrame, and ScreencastFrameAck(i64) signatures exist. Per-subscription channels are intentionally unbounded since 0.3.2, so the spike must prove the reader can receive, immediately ack, then hand off to Krometrail's bounded queue without unbounded buildup; enqueue failure records a capture gap.Typed event and convenience Page::start_screencast / ack_screencast exist. Internal event listeners are also unbounded. Real sustained screencast behavior is not covered by the inspected tests.Can make receive → ack → bounded handoff explicit, measure ack only from returned frame to ack completion, and record failed enqueue as a capture gap, at the cost of transport code.
Protocol driftGenerated from the official tip-of-tree JSON, but 0.4.0 records only protocol version 1.3, not the source commit/date. Raw commands and Value events reduce binding lag; provenance remains weak.Generated PDL is checked in but published provenance is likewise not pinned. Issue #5 shows drift has broken event decoding in practice.Pin the official devtools-protocol commit in generated output and keep raw envelopes authoritative; regeneration and compatibility fixtures become Krometrail responsibilities.
Reconnect ownershipNo transparent reconnect. CloseReason, is_closed, pending-call draining, and closed event streams give Krometrail a usable supervision signal. This matches the architecture.Retry settings cover initial connection attempts. Runtime connection loss ends the handler; Krometrail still needs to reconstruct browser/target state.Explicitly Krometrail-owned and easiest to model, but also easiest to get subtly wrong.
TestabilityCompact connect_ws boundary and mock-WebSocket tests cover raw send, session routing, event filtering, disconnect, timeout, and concurrency. Release CI tests only mocks; no real Chrome gate was found.Rich fake-CDP and real-Chrome integration suites run under Xvfb in CI. The larger handler has more ordering/state behavior to understand and bypass.Best if designed around an injected connector/duplex; highest initial test-writing cost.

Source-level API shape

cdpkit has the most direct fit for Krometrail's adapter boundary:

rust
use cdpkit::{page, target, CDP, Sender};
use futures::StreamExt;

let cdp = CDP::connect_ws(browser_ws_url).await?;
let attached = target::methods::AttachToTarget::new(target_id).send(&cdp).await?;
let session = cdp.owned_session(attached.session_id);

let mut frames = page::events::ScreencastFrame::subscribe(&session);
page::methods::StartScreencast::new().send(&session).await?;
while let Some(frame) = frames.next().await {
    let ack_started = std::time::Instant::now();
    page::methods::ScreencastFrameAck::new(frame.session_id)
        .send(&session)
        .await?;
    let _ack_latency = ack_started.elapsed(); // returned frame → ack completion only
    // Only then attempt Krometrail's bounded handoff; failed enqueue is a capture gap.
}

let raw = session
    .send_raw("Page.getLayoutMetrics", serde_json::json!({}))
    .await?;

The important limitation is equally concrete: event_stream::<Value>("Domain.event") gives raw event parameters for one named event, not every incoming raw envelope. The spike must not accidentally claim broader escape-hatch semantics.

Selection outcome: exact cdpkit 0.4.0

Qualification evidence

The current reports and decision are decisive inputs after the trace-reconstructability repair. The prior 07b0990 reports and decision remain byte-for-byte preserved under docs/evidence/cdp-transport/v2/historical/final-v2-07b0990/; the earlier prior-v2 set remains under historical/.

  • docs/evidence/cdp-transport/v2/cdpkit-linux.json — SHA-256 c5ed8bfab9cb829f0d1e1622755667084abc09129ed1f2928cdc5f577d3761f8; Linux x86_64, Chrome 149.0.7827.155.
  • docs/evidence/cdp-transport/v2/cdpkit-macos.json — SHA-256 7b2d7c61d61400f47281423d35ea57d51b1292cc78a95c4d7cef3118476c2264; macOS aarch64, Chrome 149.0.7827.201, hosted run 29212145045.
  • docs/evidence/cdp-transport/v2/decision.json — generated solely by decide_from_files; SHA-256 dfbd51c9e7a1f8e051c173df35962bc6f443d2b5c28037e406c3a72beda6472a.

Both reports bind exact cdpkit 0.4.0 and Cargo checksum c3fdb566d913b31e0014391a94c0db4ed871dbb76577dd1b2f2c5f6df158bfaa to revision a0e98ad6bd9c53d10385020bc43629f7ac246173, configuration digest sha256:06388b5f8ad042093d22408dedb8d02d5a04a9e59d485158edc533334bab956e, source-attestation digest sha256:96acbed658fb89a71a90107ac0bfec0ab78860e57f95a374cc9e183d672a4c5a, browser fixture digest sha256sum-of-ordered-fixture-files:9b42ae730d12a95772a946bf55e4838a5443b6cb4c536424570219041b6e2a68:84ba666539a996012a781637c1a894d8c7a4789cfca84661bd7cf8b79efa2e13, and candidate-contract fixture/trace digests sha256:622fb296e0b50bf0dc81123c5f54a797040cdc48bd6b5f9ca96167bbe87fce76 / sha256:33ccc161726cc35f68e6a260c129a06f9050af4a616a76c8b957525f557a6e00 with 942 observations. The decision validates the complete 13-gate registry, canonical configuration, source attestation, redaction contract, and platform-labelled gates without waivers.

1. Exact cdpkit 0.4.0

The generated final5 decision selects exact cdpkit 0.4.0: its source API maps cleanly to Krometrail's replaceable transport adapter and leaves reconnect ownership in the right layer. The remediation harness derives candidate routing, ordering, detach, close, and rebuild assertions from one observed wire controller. Real-Chrome routing counts are derived from unique correlated command/event tokens. The sustained runs remain continuously drained RSS/counter proxies; they do not prove that cdpkit's hidden unbounded subscriber queue is bounded. Linux measured 3,601 frames in 60.015619792 seconds and macOS measured 3,566 frames in 60.012583042 seconds. In both reports the exact order is receive → ack completion → bounded handoff: ack p99/max were Linux 0.214389/0.889178 ms and macOS 0.582458/12.67025 ms, with handoff elapsed equal to the measured capture elapsed. Both runs passed the same 60-second, 1,000-frame, 10-second saturation, 100-attempt, 120-second hard-stop, ack, RSS, disconnect, redaction, and trace-reconstruction thresholds.

The selection does not broaden cdpkit's API. event_stream::<Value>(name) preserves parameters for one named event only; it is not wildcard or full-envelope receive. The subscriber remains unbounded and queue depth remains uninspectable. Krometrail must acknowledge before its bounded handoff, own backpressure and capture-gap policy, and own reconnect/session restoration.

2. Selection rules carried forward

  • Adopt cdpkit behind krometrail-cdp::transport only if every mandatory gate passes unchanged. A small adapter for domain errors and bounded handoff is expected; patching/forking its routing, event decoder, or lifecycle is a failure.
  • Spike chromey with the same harness only if cdpkit fails on demonstrated real-browser lifecycle, target ordering, or sustained-capture behavior that chromey's mature handler may solve. Adopt it only if typed commands, a local generic raw-command wrapper, named raw event subscription, and explicit reconnect ownership all pass without importing its crawling/network-policy behavior into core contracts.
  • Go directly to a minimal owned transport if either candidate loses unknown events before a raw boundary, cannot expose reliable session routing, requires a fork, or obscures prompt ack/backpressure. The owned fallback should use Tokio plus tokio-tungstenite, keep raw envelopes as the source of truth, and generate the supported typed command/event subset from a pinned official protocol revision.
  • Do not weaken a gate to select a library. If both libraries fail, the evidence justifies the owned cost.

Implementation constraints

  • cdpkit is unusually young and single-maintainer; pin exactly =0.4.0 and keep the production adapter narrow and replaceable.
  • Its event subscriber is unbounded. Krometrail's bounded ingestion does not bound memory upstream of that queue; the committed RSS result is a process-level trend proxy, not queue-depth proof.
  • Generated protocol version 1.3 is not meaningful provenance by itself. The selected adapter carries forward the explicit lack of a cdpkit source revision.
  • Reconnect is a browser-session state transition, not a WebSocket retry. Krometrail must recreate discovery, attachments, domain enablement, screencast state, and gap evidence.
  • The qualification spike remains feature-gated, non-default, and not root-wired. Its exact limitations remain binding: cdpkit's named-event subscription is not wildcard or full-envelope receive, its subscriber queue is unbounded and not depth-inspectable, and it does not own transparent reconnect or target reconstruction. Production lifecycle, capture, reconnect, backpressure, recording, and gap reporting are implemented by the krometrail-cdp adapter and supervisor.

References

Last updated: