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:
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).
|
||||
@@ -0,0 +1,91 @@
|
||||
# OQ-66: `alknet/register` Wire Protocol
|
||||
|
||||
- **Origin**: `docs/architecture/decisions/089-alknetclient-native-dial-seam.md`
|
||||
§6; `docs/architecture/crates/client/README.md` §"`alknet/register`".
|
||||
- **Status**: deferred(scope)
|
||||
- **Door type**: one-way (the wire protocol — once native workers and
|
||||
hubs exchange registration frames, the format is compatibility-locked)
|
||||
- **Priority**: medium
|
||||
- **Impacts**: Blocks native worker registration over QUIC/TCP+TLS
|
||||
without HTTP. A worker that has no HTTP client (a minimal native
|
||||
worker, an iroh-only deployment) cannot enroll its key with a hub
|
||||
until this is specced and implemented. The HTTP registration
|
||||
endpoint (OQ-58) remains the first implementation, so this does NOT
|
||||
block the first hub deployment (web + native uses HTTP registration
|
||||
for worker provisioning). It blocks the *native-only* registration
|
||||
path and the no-HTTP minimal-hub case.
|
||||
- **Blocked on**: OQ-58's token model (one-time vs. refresh,
|
||||
single-use vs. multi-use, rotation). The `alknet/register` wire
|
||||
protocol and the HTTP registration endpoint (OQ-58) share the
|
||||
enrollment semantics and the token model — they should converge on
|
||||
one model before either's wire format is locked. OQ-58 is open
|
||||
(resolvable now); this OQ is blocked on its resolution. The
|
||||
frame-format fork (reuse `EventEnvelope` vs. standalone) and the
|
||||
enrollment-trait shape (point 5) are independently workable but
|
||||
not independently decidable — they depend on the token model.
|
||||
- **What is decided (ADR-089 §6)**: `alknet/register` is a dialable
|
||||
ALPN — an **entry point** (ADR-086 §2) accepted without an
|
||||
established peer identity. Two registration cases exist, both hub
|
||||
concerns and both optional:
|
||||
- **Token registration** — a freshly-provisioned worker (docker,
|
||||
vast.ai, runpod) generates its local identity, dials the hub on
|
||||
`alknet/register`, presents a one-time registration token, and
|
||||
enrolls its key. The hub creates a `PeerEntry` (mixed-fingerprint
|
||||
shape, ADR-034 §3) and returns a session credential.
|
||||
- **No-token (open) registration** — a hub that hosts public
|
||||
services over channels, or a relay/gateway, accepts registration
|
||||
without a token. The enrollment creates a `PeerEntry` with no
|
||||
token requirement.
|
||||
The dial is the same as any other ALPN (`AlknetClient::dial_quic` /
|
||||
`dial_tcp_tls` on `b"alknet/register"`); the difference is the
|
||||
handshake protocol on the resulting `Connection`.
|
||||
- **What is open**: the **wire protocol** — the handshake on the
|
||||
`Connection` after the dial. Specifically:
|
||||
1. **Frame format** — does `alknet/register` reuse the call
|
||||
protocol's `EventEnvelope` framing (length-prefixed JSON), or
|
||||
does it have its own minimal framing? Registration is a one-shot
|
||||
request/response (send public key + token, receive session
|
||||
credential), not a long-lived call session. Reusing `EventEnvelope`
|
||||
ties the register ALPN to `alknet-call` (a dependency); a minimal
|
||||
own-format keeps it standalone but adds a second wire format.
|
||||
2. **Token model** — one-time vs. refresh, single-use vs. multi-use,
|
||||
rotation. This is the same open question as OQ-58 (the HTTP
|
||||
registration endpoint's token model). The two paths should share
|
||||
the token model — a token issued by the hub works over both HTTP
|
||||
and `alknet/register`.
|
||||
3. **No-token policy** — who decides whether a hub accepts
|
||||
no-token registration? Is it a `DynamicConfig` flag, an
|
||||
`IdentityProvider` policy, or a hub-crate config? What `PeerEntry`
|
||||
shape does an open-registration enrollment produce (no
|
||||
`auth_token_hash`, fingerprint-only)?
|
||||
4. **Session credential return** — what does the hub return on
|
||||
successful registration? A `PeerEntry`? A session token? Both?
|
||||
How does the returned credential feed into the subsequent
|
||||
`alknet/channels` connection's `CallCredentials`?
|
||||
5. **Relationship to OQ-58** — the HTTP registration endpoint
|
||||
(OQ-58) and `alknet/register` share the enrollment semantics
|
||||
(create `PeerEntry`, return credential) but differ in transport
|
||||
(HTTP vs. raw ALPN). Should they share an enrollment trait in
|
||||
`alknet-hub`, with the HTTP endpoint and the `alknet/register`
|
||||
handler as two transport-specific front-ends? Or are they
|
||||
separate handlers that happen to call the same `IdentityStore`
|
||||
write path?
|
||||
- **Resolution**: Not yet decidable. The token model (point 2) is the
|
||||
shared dependency with OQ-58 — both paths should converge on one
|
||||
model. The frame format (point 1) is a genuine fork: reusing
|
||||
`EventEnvelope` adds an `alknet-call` dependency to the register
|
||||
path (which may be fine — the hub already depends on
|
||||
`alknet-call`); a standalone format keeps register minimal but
|
||||
diverges. The relationship to OQ-58 (point 5) is the shape question
|
||||
that determines whether the register handler lives in `alknet-hub`
|
||||
(alongside the HTTP endpoint) or in a separate crate. These need a
|
||||
dedicated ADR that works through the token model, the frame format,
|
||||
and the enrollment-trait shape together — they are not independently
|
||||
decidable.
|
||||
- **Cross-references**: ADR-089 (§6 — names the ALPN and its
|
||||
entry-point role; defers the wire protocol to this OQ), OQ-58
|
||||
(worker registration flow — the HTTP path; shares the token model),
|
||||
ADR-086 (§2 — entry-point vs. endpoint ALPN distinction),
|
||||
ADR-034 (§3 — the mixed-fingerprint `PeerEntry` shape token
|
||||
registration creates), ADR-072 (channel 0 identity resolution —
|
||||
the path the session credential feeds into).
|
||||
Reference in new issue
Block a user