Files
wraith/docs/architecture/decisions/007-napi-single-stream.md
glm-5.1 dad8224686 Add architecture specification for wraith SSH tunnel tool
Docs:
- README.md: index with doc table, ADR table, lifecycle definitions
- overview.md: purpose, exports, dependencies, constraints
- transport.md: Transport trait, TCP/TLS/iroh implementations, stream join
- client.md: SOCKS5 server, port forwarding, channel manager, reconnection
- server.md: auth, channel handling, stealth mode, outbound proxy
- tun-shim.md: separate privileged process, virtual DNS, --unshare mode
- napi-and-pubsub.md: NAPI wrapper, pubsub event target adapter

ADRs:
- 001: Pluggable transport via AsyncRead+AsyncWrite trait
- 002: TUN shim as separate process
- 003: iroh stream via tokio::io::join
- 004: SSH runs over transport, not alongside
- 005: SOCKS5 as primary interface, TUN as add-on
- 006(007): NAPI exposes single duplex stream

Open questions: 11 items covering TLS certs, iroh relay defaults,
Windows TUN, auth expansion, NAPI surface, TCP reconstruction
2026-06-01 15:01:45 +00:00

1.7 KiB

ADR-006: NAPI Exposes Single Duplex Stream

Status

Accepted

Context

The NAPI wrapper for wraith could expose different granularity levels:

  1. Full SSH API: Expose channel multiplexing, open_direct_tcpip, tcpip_forward, session management. The TypeScript layer would manage channels.
  2. Single duplex stream: The NAPI wrapper establishes one SSH channel and returns it as a Node.js Duplex stream. TypeScript multiplexing (if needed) happens at the pubsub layer.

Decision

Option 2: NAPI exposes a single duplex stream.

The NAPI wrapper's job is to get a reliable, authenticated byte stream from A to B. It handles transport (TCP/TLS/iroh), SSH authentication, and channel setup, then hands the caller a single Duplex stream that just works.

If the TypeScript consumer needs multiplexing (e.g., multiple concurrent tool calls over operations), pubsub handles that at the EventEnvelope level. Multiple call.requested / call.responded events flow over the same stream, distinguished by their id fields. This is how the existing WebSocket adapter works.

Consequences

  • Positive: Minimal NAPI surface — one function, one return type. Small binary, small FFI boundary.
  • Positive: The TypeScript side doesn't need to understand SSH at all. It gets a stream and sends/receives EventEnvelope JSON.
  • Positive: No need to expose russh types in NAPI. The SSH complexity stays in Rust.
  • Negative: If a consumer wants multiple isolated channels (e.g., one for events, one for file transfer), they'd need multiple connect() calls (multiple SSH sessions). This is acceptable for the expected use case (pubsub events over a single stream).

References