Electron Stagewright docs

ADR-016: Network capture plugin via a transport capture seam

Status: Accepted (renderer capture + stubbing + response-body capture on both the Playwright and CDP transports)

Context

An agent driving an Electron app through Stagewright sees the DOM (snapshot, find, read, interact) but not the network calls the app makes underneath it. "Did saving the form POST to /api/save, and what did it return?" is unanswerable from the DOM alone. Network capture is the fourth differentiation plugin (after trace, IPC, and production); it ships the observe half of the dormant canIntercept capability ADR-003 reserved.

Two questions had to be settled: HOW a plugin observes network traffic (it is neither a DOM surface nor, unlike IPC, reachable through main-process eval — protocol-level network is invisible to an in-page or main-process evaluate), and what SECURITY posture gates that observation.

Decision

1. A dedicated network-capture seam on the transport, not eval

TransportSession gains three methods — startNetworkCapture(filter), networkEvents({ clear? }), stopNetworkCapture() — mirroring the existing always-on console/dialog buffers, but ARMED: the listeners record only between start and stop. The default Playwright transport implements them with page.on('requestfinished' | 'requestfailed'), recording one NetworkEvent per terminal request (method, url, status, request/response headers, failure, duration) into a per-session capped ring, filtered at record time to an explicit URL allowlist (+ optional method filter). The listeners attach alongside the console/dialog ones (covering current and future windows with no extra bookkeeping) and stay inert until armed, so stopNetworkCapture simply clears the filter — no fragile per-page detach.

@electron-stagewright/plugin-network drives that seam: network_capture_start { urls, methods? }, network_captured { clear? }, network_capture_stop. The plugin keeps the orchestration (allowlist relay, per-session capture state, header redaction, error envelopes) in TypeScript; the transport owns the actual listeners and buffer.

A seam — not the eval approach the IPC plugin uses — because eval cannot see protocol-level network traffic at all, and because network observation is not arbitrary JavaScript, so it should not inherit the eval threat model or the eval opt-in.

2. Gated by canIntercept, bounded by an allowlist, NOT by --allow-eval

Captured headers can carry secrets (auth, cookies, tokens). The plugin redacts authorization, cookie, and set-cookie by default (redactSecureDefaults, configurable off), redactHeaders adds more, and request/response BODIES are not captured in this increment — headers and metadata only — to limit the secret surface.

Rationale

Alternatives considered

Consequences

References

Status Update — 2026-06-16: Response stubbing (the modify half)

The deferred "modify half" named above now ships. TransportSession gains stubNetwork(stub) / clearNetworkStubs(url?), and the plugin gains network_stub / network_unstub — gated on the same canIntercept capability and bounded to the same explicit URL allowlist as capture, and likewise NOT --allow-eval gated.

Status Update — 2026-06-17: Response-body capture (the deferred opt-in)

The body opt-in deferred in Alternatives now ships. Capture still records headers + metadata by default; captureBodies opts into bodies, bounded by the same allowlist + canIntercept gate (and likewise NOT --allow-eval gated).

Status Update — 2026-06-18: CDP-transport network seam (the deferred broader path)

The deferred CDP-transport coverage now ships, so the whole seam — capture, bodies, and stubbing — works on the attach-mode transport too, and canIntercept flips honestly true on CDP (amends ADR-003). The plugin and the NetworkEvent / NetworkStub types are unchanged; this is a second transport implementation of the same seam.