docs(arch): AlknetClient native dial seam — resolves OQ-55 (ADR-089)

Extract the deferred AlknetClient as a new crate alknet-client — the
client-side analogue of AlknetEndpoint. Three dial methods (QUIC +
TCP+TLS via TlsClientConfig, iroh via key) produce a Connection for
CallClient::spawn_dispatch / ChannelClient::from_connection to consume.
The deferral collapsed because ADR-086 gave the native endpoint type
three dial shapes within one endpoint type, ADR-087 broke the circular
hedge, and ADR-083 gave the server-side shape to mirror by symmetry.

Names the three concept layers that were tangled throughout the initial
development (deployment role / establishment side / ALPN-level category)
so the fix is legible. Names alknet/register as a dialable entry-point
ALPN (native registration, parallel to HTTP registration in OQ-58); its
wire protocol is deferred to OQ-66 (blocked on OQ-58's token model).

Cross-references updated across 11 existing docs (README, overview,
open-questions, OQ-55, tls, hub, core, channels README/overview/
channel-client, call client-and-adapters) to reflect OQ-55 resolved and
the new alknet-client crate. Architecture review passed (2 critical, 7
warnings — all addressed).
This commit is contained in:
glm-5.2 committed 2026-07-15 12:50:13 +00:00
1 parent 1291a751b0
commit ce7de57973
14 files changed
+1224 -182

No files matched your search

@@ -5,83 +5,51 @@
§"Issues Surfaced" #1 (the `BidiStreamSource` finding that motivates
separating the client-extraction question from the `Connection` extension
question).
- **Status**: deferred(scope)
- **Door type**: two-way
- **Status**: resolved (ADR-089)
- **Door type**: one-way (crate existence + dial seam — ADR-089)
- **Priority**: medium
- **Impacts**: Does NOT block individual transport dials — each
transport-specific dial helper builds its `TlsClientConfig` (ADR-087)
and its own connector standalone. Blocks only the *shared*
`AlknetClient::dial()` extraction (one entry point that picks the
transport and calls the right connector). Low impact until a second
transport's dial exists and the duplicated dial boilerplate becomes
worth extracting.
- **Blocked on**: a **second transport's** real dial existing, not just a
second QUIC dial. The blocking condition is met when, e.g., the SSH
crate's raw-TCP dial or the HTTP-wrapped call dial exists — so the
transport-polymorphic dial+TLS seam is extractable from two
*different* transport implementations, not two QUIC variants.
`ChannelClient`'s `connect_quic` does not unblock this; it is a second
*client* but the same *transport shape*. `ChannelClient`'s
`from_connection` (the transport-agnostic take-over, ADR-080) is decided
and is not the thing being deferred — the shared *dial* is. The deferral
is on transport-polymorphism of the dial, not on client count or on the
channels protocol's API.
- **What is NOT deferred (amended by ADR-087)**: the client-side TLS
config (`TlsClientConfig`) is **not** part of this deferral. OQ-64
is resolved — `alknet-tls` provides `TlsClientConfig` now, unblocked
by this OQ. The TLS config is a **prerequisite** for the dial, not a
consequence of it; it is transport-agnostic (ADR-034 verifier
selection + ADR-084 provider, both already decided). Each
transport-specific dial helper builds its `TlsClientConfig` and
passes it to its transport's connector. This OQ defers only the
*transport-polymorphic dial extraction* — the shared `AlknetClient::dial()`
that picks the transport and calls the right connector. When a second
transport's dial exists, the dial seam is extractable; the TLS config
is already shared by then.
- **Resolution**: Not yet decidable. The shared substance across
TLS-carrying transports is ADR-034's verifier-selection rule (PeerEntry
presence → fingerprint pin : CA-verify / fail-closed) and the
`rustls::ClientConfig` construction — now centralized in
`TlsClientConfig::new` (ADR-087, OQ-64 resolved). What remains
transport-specific is the *dial* itself — `quinn::Endpoint::connect`,
`TcpStream::connect` + `TlsConnector::connect`, iroh's
`Endpoint::connect`. We have one of ~5 shapes implemented (QUIC; the
others being HTTP, TCP+TLS, WebTransport, raw TCP). Extracting a
QUIC-shaped connector to core and naming it `AlknetClient` would bake
QUIC in as *the* establishment shape — the same welding ADR-065
unwound on the server side, repeated on the client side. The dial is
transport-polymorphic; the shared TLS config is narrow and now
extracted (ADR-087). Until a second transport's dial exists, the seam
between "dial" (per-transport) and "spawn the dispatcher" (per-crate)
is not extractable from two real shapes — it's guessable from one.
- **What does NOT block on this**: each crate building its own client
standalone with a shared `TlsClientConfig` (ADR-087), and each
transport-specific dial helper. `CallClient`'s transport-agnostic
- **Resolution**: `AlknetClient` is extracted as a new crate
`alknet-client` — the client-side analogue of `AlknetEndpoint`
(ADR-083). Three dial methods: `dial_quic` / `dial_tcp_tls` (both
build a `TlsClientConfig` via ADR-087 and call their transport's
connector) + `dial_iroh` (the key-not-config exception, shares the
`Ed25519SecretKey`). The dial produces a `Connection` for the
protocol take-overs (`CallClient::spawn_dispatch`,
`ChannelClient::from_connection`) to consume — it does not run
protocols.
- **Why the deferral collapsed**: the blocking condition ("a second
transport's real dial existing") is met *within the native endpoint
type* (ADR-086): QUIC + TCP+TLS (both rustls-consuming via
`TlsClientConfig`) + iroh (key-based) — three dial shapes, two
sharing `TlsClientConfig`. ADR-087 broke the circular hedge
(`TlsClientConfig` is a prerequisite, not a consequence of the dial).
ADR-083 made the server-side shape a clean accept-loop runner, giving
the client-side shape by symmetry. The dial seam is extractable from
two different transport implementations, not guessable from one.
- **Scope of the resolution**: the **native** transport-polymorphic
dial. The web/browser client (WebSocket, HTTP — the browser
bidirectional path per ADR-044/048) was never what this OQ was
about — it is a different client surface (the JS SDK / wasm), not a
Rust dial, and does not use `AlknetClient`. Non-Rust native clients
(Node/Deno/Bun, Python, wasm) that negotiate TLS against an X.509
endpoint and implement the wire protocols directly are also out of
scope — `AlknetClient` is the Rust native client, one of several
possible native clients sharing the same wire protocols.
- **What does NOT block on this (unchanged)**: each crate building its
own client standalone with a shared `TlsClientConfig` (ADR-087), and
each transport-specific dial helper. `CallClient`'s transport-agnostic
take-over (`spawn_dispatch`) and `ChannelClient`'s transport-agnostic
take-over (`from_connection`, ADR-080) are decided; `connect_quic`
dials QUIC and calls `from_connection`. The SSH crate's TCP client,
the HTTP call client, a `connect_tcp_tls` / `connect_webtransport`
helper — each builds its `TlsClientConfig` (ADR-087) and its own dial
standalone. Core already permits all of this —
`Connection::from_stream` / `from_bidi` (ADR-065) handles the
non-QUIC transport on the server side, and nothing prevents a client
from constructing a `Connection` the same way after its own
transport-specific dial. The friction is the duplicated dial
boilerplate (each dial helper calls its transport's connector), not
duplicated TLS config (that is shared via ADR-087). The
bidirectionality criterion (a crate needs a Client type when (a) the
endpoint has protocol-level authority — e.g., channels' id allocation
— or (b) the protocol needs a reliable establishment interface) is
met by each crate independently; `AlknetClient` is the eventual
*shared dial seam*, not a prerequisite for any single client to
exist.
- **Cross-references**: ADR-034 (verifier selection — centralized in
`TlsClientConfig::new` per ADR-087), ADR-087 (`TlsClientConfig` is
not blocked on this OQ — the TLS config is extracted; only the dial
remains deferred), ADR-065 (server-side transport generalization —
the client-side analogue this OQ's deferral avoids preempting),
ADR-070 (the `BidiStreamSource` extension point, which is the
*Connection* opening and is orthogonal to the *client* establishment
question), OQ-CH-14 in
take-over (`from_connection`, ADR-080) are decided; the existing
`connect` / `connect_quic` convenience constructors become thin
wrappers over `AlknetClient::dial_quic` (ADR-089 §5).
- **Cross-references**: ADR-089 (the resolution — `AlknetClient` native
dial seam), ADR-083 (the server-side shape mirrored), ADR-086
(endpoint types — native has QUIC + TCP+TLS + iroh), ADR-087
(`TlsClientConfig` — the prerequisite the dial consumes), ADR-034
(verifier selection — centralized in `TlsClientConfig::new`), ADR-065
(server-side transport generalization — the client-side analogue
this OQ's deferral avoided preempting, now completed), ADR-070 (the
`BidiStreamSource` extension point — orthogonal to the client
establishment question), OQ-CH-14 in
`docs/research/alknet-channels/phase-0-findings.md` (the research-scope
question this core-scope OQ carries forward).
question this core-scope OQ carried forward).