Skip to content

docs(design): wire observability and debug host - #315

Open
decrypto21 wants to merge 6 commits into
mainfrom
nidish/debugger-rfc
Open

docs(design): wire observability and debug host#315
decrypto21 wants to merge 6 commits into
mainfrom
nidish/debugger-rfc

Conversation

@decrypto21

@decrypto21 decrypto21 commented Jul 25, 2026

Copy link
Copy Markdown

Adds a design doc for the wire observability layer of @parity/truapi: a payload-blind debug tap in
the Rust host (truapi-server), correlated by the wire requestId, streamed to a debugger app the host
dials out to. Frame bytes are opaque to the core (never decoded), so it leaks no payload or key
material and the product package is untouched.

Placed under docs/design/ (not docs/rfcs/) because it specifies the host-side tap and TS debugger
tooling over the existing wire, with no change to the Rust protocol interface.

@decrypto21
decrypto21 requested review from a team July 25, 2026 06:04
@decrypto21
decrypto21 force-pushed the nidish/debugger-rfc branch 2 times, most recently from 727e0cc to cccd69d Compare July 25, 2026 06:10
@decrypto21 decrypto21 changed the title docs(rfc): add 0022 wire observability and debug host docs(design): wire observability and debug host Jul 25, 2026
@decrypto21
decrypto21 removed the request for review from a team July 25, 2026 06:11
Comment thread docs/design/wire-observability-debug-host.md Outdated
Comment thread docs/design/wire-observability-debug-host.md Outdated
Comment thread docs/design/wire-observability-debug-host.md Outdated
Comment thread docs/design/wire-observability-debug-host.md Outdated
Comment thread docs/design/wire-observability-debug-host.md Outdated
Comment thread docs/design/wire-observability-debug-host.md Outdated
Comment thread docs/design/wire-observability-debug-host.md Outdated

@TarikGul TarikGul left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think generally the content of the doc is good - and the direction is nice. But I think we should reframe it more like a specification. Currently its very easy for the doc to get out of sync with the implementation PR.

I think we can really slim this down and take out some of the unnecessary sections that dont add much to the actual core design of what we are building.

@pgherveou

pgherveou commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Two points on the design. Both are about where the debugger lives.

1. Topology: the host dials the debugger

The doc's model is a passive observe callback on createTransport (L78), with the debugger notified
off to the side (L65-74), plus a separate relay service to reach another process. I'd replace both
with one rule: the host opens a WebSocket to the debugger app and streams frames to it.

This is not a preference, it's forced. Only the inside can initiate:

  • ws_bridge binds 127.0.0.1 on the device. A debugger app on the dev machine cannot reach it,
    and cannot dial into the device. (It works in the iOS Simulator only because the simulator shares
    the Mac's network stack, which is an accident of the simulator, not a design.)
  • In web, nothing outside the browser can dial into a Worker.

So the debugger app is always the server, and every host dials outward:

 WEB                                      NATIVE / DESKTOP

 ┌─ browser ──────────────────┐           ┌─ device ───────────────────┐
 │  product (iframe)          │           │  product (webview)         │
 │      │ MessagePort         │           │      │ ws loopback         │
 │      ▼                     │           │      ▼                     │
 │  host worker ── tap        │           │  ws_bridge ── tap          │
 │      │        rust wasm    │           │      │       rust core     │
 └──────┼─────────────────────┘           └──────┼─────────────────────┘
        │ ws, dialed outward                     │ ws, dialed outward
        └──────────────┬─────────────────────────┘
                       ▼
           ┌──────────────────────┐
           │    debugger app      │   ws server on the dev machine
           └──────────────────────┘
A CALL, END TO END

 product                host edge               rust core           debugger app
(iframe / webview)        (tap)                                       (remote)
    │                       │                       │                     │
    │  getAccount()         │                       │                     │
    ├─ frame{ p:1, id=22 } ─▶                       │                     │
    │                       ├┈┈┈┈┈┈┈┈┈┈┈┈ { dir: out, frame } ┈┈┈┈┈┈┈┈┈┈┈┈▶
    │                       ├ delivered immediately ▶                     │
    │                       │                       │                     │  LOG  ▲ out  p:1
    │                       │                       │  ctx.requestId = "p:1"
    │                       ◀─ frame{ p:1, id=23 } ─┤                     │
    │                       ├┈┈┈┈┈┈┈┈┈┈┈┈ { dir: in, frame } ┈┈┈┈┈┈┈┈┈┈┈┈┈▶
    ◀ delivered immediately ┤                       │                     │
    │  Ok(response)         │                       │                     │
    │                       │                       │                     │  LOG  ▼ in   p:1

Two properties to state as invariants, neither currently in the doc:

  • In the path, not in the critical path. The tap sees every frame because it is carrying them, and
    it forwards before it emits. A slow, absent, or crashed debugger cannot stall a session. Only the
    trace is lost.
  • Emit, never wait. No reply is read from the debugger socket. That is what makes the first
    property true.

Worth noting that the second invariant is also the extension point. If we later want mocking or
mutation, the socket and the envelope stay as they are and the tap starts reading a reply on the
frames it cares about: deliver unchanged is today's behaviour, deliver modified is mutation,
respond is a mock. Nothing about the topology or the event shape has to change, which is the reason
to be comfortable shipping the one-way version first.

What this removes from the current design: the relay service (RelayRouter, createRelayProvider,
the { v, role, sessionId, productId, frame } envelope, join-order buffering, the Bun reference
server) is unnecessary, because the debugger app is the server. The envelope collapses to:

 host → debugger    { channelId, dir: "out" | "in", frame: bytes }

Two things this forces that the doc should state. The debugger URL must be configurable, not
loopback-derived: with the app on a device and the debugger on a Mac, 127.0.0.1 is wrong and the
link is LAN.

And that LAN hop is why wss:// is the right call. It keeps
hosts/ios/polkadot-app/Info.plist as it is (an empty NSAppTransportSecurity already requires TLS,
and wss:// satisfies it, so no exception is needed) and it means the trace does not cross the
office network in the clear. The cost moves to certificates: iOS still evaluates trust, so a
self-signed debugger cert is rejected until its CA is installed and switched on under Settings →
General → About → Certificate Trust Settings. Worth naming that as a setup step in the doc, since
"use wss" reads as free and isn't.


2. The tap belongs in truapi-server, behind a sink trait

The doc puts the seam in the TypeScript transport (js/packages/truapi/src/client.ts) and turns it on
from the product side via @parity/truapi/sandbox reading ?debug=wire (L244-252). That is why the
doc has to claim host-agnosticism (L46-47) while shipping a browser-only, product-package integration,
and why it needs a dead-code-elimination argument (L129-131) to keep the decode path out of production
bundles.

Put it in the Rust host instead. truapi-server already has exactly two choke points, and both web
and native funnel through them
:

Direction Choke point
inbound (product → core) ProductRuntime::receive_frame()host_core.rs:422
outbound (core → product) FrameSink::emit_frame()host_core.rs:40

Native reaches these through ws_bridge.rs (WsFrameSink), web through wasm.rs:851. So one tap
implementation covers both platforms, and nothing in @parity/truapi changes at all. That is the test
for whether the product is genuinely untouched, and the current design fails it.

A sink trait, not a hardcoded socket

Worth defining the flush target as a trait rather than wiring a WebSocket into the core, both to keep
the transport out of truapi-server and because wire frames are not the only thing worth observing.
dotli's own debugger also surfaces host-internal events like SSO, and those have no wire frame to hang
off. An enum event keeps room for them without reopening the design:

/// Dev-only sink for host debug events. A host that does not enable the
/// debugger leaves it unset and the tap is inert.
pub trait DebugSink: Send + Sync {
    /// Hand one event to the sink. Fire-and-forget by construction: it cannot
    /// block the frame path and cannot fail the operation that produced it.
    fn emit(&self, event: DebugEvent);
}

/// One observable host event.
pub enum DebugEvent {
    /// A SCALE wire frame crossing a product channel.
    Frame {
        /// Which product channel on this host.
        channel_id: ChannelId,
        /// Product → core, or core → product.
        dir: FrameDirection,
        /// Untouched `ProtocolMessage` bytes; the debugger app decodes.
        bytes: Vec<u8>,
    },
}

Comment thread docs/design/wire-observability-debug-host.md Outdated
@decrypto21
decrypto21 force-pushed the nidish/debugger-rfc branch from 333b792 to 13048f9 Compare July 28, 2026 08:46
@decrypto21
decrypto21 force-pushed the nidish/debugger-rfc branch from 13048f9 to 4f5c8f7 Compare July 28, 2026 09:04
@decrypto21
decrypto21 requested a review from TarikGul July 28, 2026 10:35
@decrypto21
decrypto21 requested review from Imod7 and pgherveou July 28, 2026 20:07
@decrypto21

Copy link
Copy Markdown
Author

Moved the wire debug tap into the Rust host (truapi-server's DebugSink) - tapping both frame choke points

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants