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
@@ -71,9 +71,8 @@ data channels byte-forwarded with `channel_id` rewrite; the hub never runs
|
|||||||
protocol-specific handlers),
|
protocol-specific handlers),
|
||||||
[ADR-080](decisions/080-channelclient.md) (`ChannelClient`,
|
[ADR-080](decisions/080-channelclient.md) (`ChannelClient`,
|
||||||
transport-agnostic `from_connection` primary + `connect_quic` convenience,
|
transport-agnostic `from_connection` primary + `connect_quic` convenience,
|
||||||
bidirectionality preserved; `AlknetClient` dial-seam extraction stays
|
bidirectionality preserved; `AlknetClient` dial-seam extracted as
|
||||||
deferred per OQ-55 — blocked on a second *transport's* dial, not a second
|
`alknet-client` per ADR-089, resolving OQ-55),
|
||||||
client),
|
|
||||||
[ADR-081](decisions/081-channels-subcrate-decomposition.md) (sub-crate
|
[ADR-081](decisions/081-channels-subcrate-decomposition.md) (sub-crate
|
||||||
decomposition — `channels-core` (pure multiplexer, depends on alknet-core
|
decomposition — `channels-core` (pure multiplexer, depends on alknet-core
|
||||||
only, no call dependency) / `channels-call` (channel 0 pre-negotiation +
|
only, no call dependency) / `channels-call` (channel 0 pre-negotiation +
|
||||||
@@ -188,6 +187,7 @@ adapter location map is now consistent: all HTTP-backed adapters
|
|||||||
| [crates/vault/protocol.md](crates/vault/protocol.md) | stable | DerivedKey redaction, KeyType, serialization behavior |
|
| [crates/vault/protocol.md](crates/vault/protocol.md) | stable | DerivedKey redaction, KeyType, serialization behavior |
|
||||||
| [crates/hub/README.md](crates/hub/README.md) | draft | alknet-hub crate — composes a subset of three endpoint types (web/native/iroh — ADR-086), channels substrate (ADR-079 relay), worker registration flow (OQ-58), identity over transports, aggregated peer env, connection lifecycle, service discovery |
|
| [crates/hub/README.md](crates/hub/README.md) | draft | alknet-hub crate — composes a subset of three endpoint types (web/native/iroh — ADR-086), channels substrate (ADR-079 relay), worker registration flow (OQ-58), identity over transports, aggregated peer env, connection lifecycle, service discovery |
|
||||||
| [crates/tls/README.md](crates/tls/README.md) | reviewed | alknet-tls crate — shared TLS config (`TlsServerConfig` + `TlsClientConfig`) shared across quinn + TCP+TLS + iroh; one cert, one ACME state machine, N transports; split ALPN lists per endpoint type (ADR-086, resolves OQ-62); fixes cert-reuse welding in `alknet-core/endpoint.rs` (ADR-082) |
|
| [crates/tls/README.md](crates/tls/README.md) | reviewed | alknet-tls crate — shared TLS config (`TlsServerConfig` + `TlsClientConfig`) shared across quinn + TCP+TLS + iroh; one cert, one ACME state machine, N transports; split ALPN lists per endpoint type (ADR-086, resolves OQ-62); fixes cert-reuse welding in `alknet-core/endpoint.rs` (ADR-082) |
|
||||||
|
| [crates/client/README.md](crates/client/README.md) | draft | alknet-client crate — the native client dial seam (`AlknetClient`), client-side analogue of `AlknetEndpoint`; three dials (QUIC + TCP+TLS via `TlsClientConfig`, iroh via key); produces `Connection` for `CallClient`/`ChannelClient` take-over; `alknet/register` named (wire protocol deferred, OQ-66) |
|
||||||
| [crates/channels/README.md](crates/channels/README.md) | draft | alknet-channels crate — multiplexing proxy, 9-byte chunk format, N channels over one transport stream |
|
| [crates/channels/README.md](crates/channels/README.md) | draft | alknet-channels crate — multiplexing proxy, 9-byte chunk format, N channels over one transport stream |
|
||||||
| [crates/channels/overview.md](crates/channels/overview.md) | draft | Crate purpose, the multiplexing collapse, dependencies, transport agnosticism, WASM, relationship to existing crates |
|
| [crates/channels/overview.md](crates/channels/overview.md) | draft | Crate purpose, the multiplexing collapse, dependencies, transport agnosticism, WASM, relationship to existing crates |
|
||||||
| [crates/channels/channels-wire.md](crates/channels/channels-wire.md) | draft | 9-byte chunk format, stream types, sentinels, framing disambiguation, wire-level invariants (REQ-CH-01..05) |
|
| [crates/channels/channels-wire.md](crates/channels/channels-wire.md) | draft | 9-byte chunk format, stream types, sentinels, framing disambiguation, wire-level invariants (REQ-CH-01..05) |
|
||||||
@@ -288,6 +288,7 @@ adapter location map is now consistent: all HTTP-backed adapters
|
|||||||
| [086](decisions/086-endpoint-types-and-entry-points.md) | Endpoint Types and Entry Points | Accepted |
|
| [086](decisions/086-endpoint-types-and-entry-points.md) | Endpoint Types and Entry Points | Accepted |
|
||||||
| [087](decisions/087-tlsclientconfig-not-blocked-on-dial.md) | `TlsClientConfig` Not Blocked on Dial Seam | Accepted |
|
| [087](decisions/087-tlsclientconfig-not-blocked-on-dial.md) | `TlsClientConfig` Not Blocked on Dial Seam | Accepted |
|
||||||
| [088](decisions/088-tlserror-shape.md) | `TlsError` Shape — Single Enum, Owned by `alknet-tls` | Accepted |
|
| [088](decisions/088-tlserror-shape.md) | `TlsError` Shape — Single Enum, Owned by `alknet-tls` | Accepted |
|
||||||
|
| [089](decisions/089-alknetclient-native-dial-seam.md) | AlknetClient — Native Client Dial Seam | Accepted (resolves OQ-55) |
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,9 @@ impl CallClient {
|
|||||||
/// Feature-gated on `quinn` (the dial is QUIC-specific). Additive
|
/// Feature-gated on `quinn` (the dial is QUIC-specific). Additive
|
||||||
/// and two-way-door — `connect_tcp_tls`, `connect_webtransport`,
|
/// and two-way-door — `connect_tcp_tls`, `connect_webtransport`,
|
||||||
/// etc. join it as transports are added, without touching the
|
/// etc. join it as transports are added, without touching the
|
||||||
/// `spawn_dispatch` contract.
|
/// `spawn_dispatch` contract. This convenience is a thin wrapper
|
||||||
|
/// over `AlknetClient::dial_quic` (ADR-089) — a caller that needs
|
||||||
|
/// transport selection uses `AlknetClient` directly.
|
||||||
#[cfg(feature = "quinn")]
|
#[cfg(feature = "quinn")]
|
||||||
pub async fn connect(
|
pub async fn connect(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ protocol work itself.
|
|||||||
| [077](../../decisions/077-tty-inside-channels.md) | TTY Inside Channels — Sub-Streams, Not Wire Format | TTY's two modes (direct vs channels); 5 sub-streams; control bidirectional via 3/4; amends ADR-052 scope |
|
| [077](../../decisions/077-tty-inside-channels.md) | TTY Inside Channels — Sub-Streams, Not Wire Format | TTY's two modes (direct vs channels); 5 sub-streams; control bidirectional via 3/4; amends ADR-052 scope |
|
||||||
| [078](../../decisions/078-two-pump-shutdown-on-completion.md) | Two-Pump Shutdown-on-Completion Pattern | The two-pump deadlock contract; handler-level, not channels-layer |
|
| [078](../../decisions/078-two-pump-shutdown-on-completion.md) | Two-Pump Shutdown-on-Completion Pattern | The two-pump deadlock contract; handler-level, not channels-layer |
|
||||||
| [079](../../decisions/079-hub-relay-translate-not-forward.md) | Hub Relay — Translate, Not Transparently Forward | The hub translates channel 0, byte-forwards data channels with ID rewrite |
|
| [079](../../decisions/079-hub-relay-translate-not-forward.md) | Hub Relay — Translate, Not Transparently Forward | The hub translates channel 0, byte-forwards data channels with ID rewrite |
|
||||||
| [080](../../decisions/080-channelclient.md) | ChannelClient — the Client Side of a Channels Connection | `ChannelClient`, transport-agnostic `from_connection` primary + `connect_quic` convenience; `AlknetClient` dial-seam deferred (OQ-55) |
|
| [080](../../decisions/080-channelclient.md) | ChannelClient — the Client Side of a Channels Connection | `ChannelClient`, transport-agnostic `from_connection` primary + `connect_quic` convenience; `AlknetClient` dial-seam extracted (ADR-089, resolves OQ-55) |
|
||||||
| [081](../../decisions/081-channels-subcrate-decomposition.md) | channels Sub-Crate Decomposition | `channels-core` (pure multiplexer) / `channels-call` (call coupling + ChannelClient); hub and worker are consumers, not sub-crates |
|
| [081](../../decisions/081-channels-subcrate-decomposition.md) | channels Sub-Crate Decomposition | `channels-core` (pure multiplexer) / `channels-call` (call coupling + ChannelClient); hub and worker are consumers, not sub-crates |
|
||||||
| [070](../../decisions/070-bidistreamsource-trait.md) | BidiStreamSource Trait | The `Connection` extension point `ChannelBidiStreamSource` implements |
|
| [070](../../decisions/070-bidistreamsource-trait.md) | BidiStreamSource Trait | The `Connection` extension point `ChannelBidiStreamSource` implements |
|
||||||
| [065](../../decisions/065-connection-from-stream-generic-single-stream.md) | `Connection::from_stream` | The transport-agnostic `Connection` the channels layer rides on |
|
| [065](../../decisions/065-connection-from-stream-generic-single-stream.md) | `Connection::from_stream` | The transport-agnostic `Connection` the channels layer rides on |
|
||||||
@@ -52,7 +52,7 @@ protocol work itself.
|
|||||||
|
|
||||||
| OQ | Title | Status | Relevance |
|
| OQ | Title | Status | Relevance |
|
||||||
|----|-------|--------|-----------|
|
|----|-------|--------|-----------|
|
||||||
| OQ-55 | AlknetClient / Client Establishment Extraction | deferred(scope) | `ChannelClient`'s API is decided (ADR-080): transport-agnostic `from_connection` primary, `connect_quic` convenience. `AlknetClient` core extraction stays deferred — blocked on a second *transport's* dial (the shared dial+TLS seam), not a second client |
|
| OQ-55 | AlknetClient / Client Establishment Extraction | resolved (ADR-089) | `ChannelClient`'s API is decided (ADR-080): transport-agnostic `from_connection` primary, `connect_quic` convenience. `AlknetClient` core extraction is now resolved — the native dial seam is `alknet-client` (ADR-089) |
|
||||||
| OQ-56 | Full channel-level flow-control windowing | deferred(scope) | Bounded-buffer is decided (ADR-076); full windowing is an extension blocked on "a real deployment observes HOL blocking on a saturated channel where bounded buffer is insufficient" |
|
| OQ-56 | Full channel-level flow-control windowing | deferred(scope) | Bounded-buffer is decided (ADR-076); full windowing is an extension blocked on "a real deployment observes HOL blocking on a saturated channel where bounded buffer is insufficient" |
|
||||||
| OQ-57 | Two-pump helper extraction to alknet-core | deferred(scope) | The shutdown-on-completion *contract* is decided (ADR-078); the *helper* extraction is blocked on a second two-pump handler existing (shape convergence) |
|
| OQ-57 | Two-pump helper extraction to alknet-core | deferred(scope) | The shutdown-on-completion *contract* is decided (ADR-078); the *helper* extraction is blocked on a second two-pump handler existing (shape convergence) |
|
||||||
|
|
||||||
|
|||||||
@@ -159,27 +159,24 @@ populates what operations they expose).
|
|||||||
name follows the `CallClient` convention (the side that dialed), not a
|
name follows the `CallClient` convention (the side that dialed), not a
|
||||||
request/response role.
|
request/response role.
|
||||||
|
|
||||||
## Relationship to `AlknetClient` (OQ-55 — deferred)
|
## Relationship to `AlknetClient` (ADR-089 — resolved)
|
||||||
|
|
||||||
`ChannelClient`'s *API* is transport-agnostic — `from_connection` takes a
|
`ChannelClient`'s *API* is transport-agnostic — `from_connection` takes a
|
||||||
pre-established `Connection`. What is deferred (OQ-55) is the shared
|
pre-established `Connection`. The shared *dial+TLS* seam
|
||||||
*dial+TLS* seam (`AlknetClient`): the transport-specific work each dial
|
(`AlknetClient`, OQ-55) is now extracted: [`alknet-client`](../client/README.md)
|
||||||
helper does — open a socket, run the TLS handshake, apply ADR-034's
|
provides `AlknetClient` with three dial methods (`dial_quic` /
|
||||||
verifier-selection rule, produce a `Connection`. That dial is genuinely
|
`dial_tcp_tls` / `dial_iroh`), each producing a `Connection` that
|
||||||
transport-specific (QUIC, TCP+TLS, WebTransport, raw TCP, SSH), and we have
|
`from_connection` consumes. The dial is transport-specific (QUIC,
|
||||||
one shape implemented (QUIC, in `connect_quic`). Extracting a QUIC-shaped
|
TCP+TLS, iroh); the take-over (`from_connection`) is
|
||||||
connector now and naming it `AlknetClient` would bake QUIC in as *the*
|
transport-agnostic. The two concerns are separated.
|
||||||
establishment shape — the same welding ADR-065 unwound on the server side.
|
|
||||||
|
|
||||||
This is why `from_connection` is the one-way-door surface and
|
`connect_quic` becomes a thin wrapper over `AlknetClient::dial_quic` —
|
||||||
`connect_quic` is a two-way-door convenience over it. `AlknetClient` (when
|
dial QUIC, then `from_connection`. A caller that needs transport
|
||||||
extracted, after a second transport's dial exists) becomes the shared
|
selection (QUIC with TCP+TLS fallback) uses `AlknetClient` directly;
|
||||||
*dial*; `from_connection` stays the shared *channels-take-over*. The two
|
the fallback policy is a caller concern. See
|
||||||
concerns are separated now, before the one-way-door API is cast.
|
[ADR-089](../../decisions/089-alknetclient-native-dial-seam.md) for the
|
||||||
|
full decision and [OQ-55](../../questions/055-alknetclient-establishment-extraction.md)
|
||||||
The friction while `AlknetClient` is deferred is duplicated
|
(resolved).
|
||||||
verifier-selection boilerplate across dial helpers (~20 lines each) — not
|
|
||||||
duplicated capability and not a QUIC-welded client API.
|
|
||||||
|
|
||||||
## Design Decisions
|
## Design Decisions
|
||||||
|
|
||||||
@@ -187,14 +184,15 @@ All design decisions are documented as ADRs in [decisions/](../../decisions/).
|
|||||||
|
|
||||||
| ADR | Decision | Summary |
|
| ADR | Decision | Summary |
|
||||||
|-----|----------|---------|
|
|-----|----------|---------|
|
||||||
| [080](../../decisions/080-channelclient.md) | ChannelClient | Client side; transport-agnostic `from_connection` primary, `connect_quic` convenience; `AlknetClient` dial-seam deferred (OQ-55) |
|
| [080](../../decisions/080-channelclient.md) | ChannelClient | Client side; transport-agnostic `from_connection` primary, `connect_quic` convenience; `AlknetClient` dial-seam extracted (ADR-089, resolves OQ-55) |
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
- **OQ-55** (deferred(scope)): `AlknetClient` core **dial+TLS seam**
|
- **OQ-55** (resolved by ADR-089): `AlknetClient` core **dial+TLS seam**
|
||||||
extraction — blocked on a second *transport's* dial. `ChannelClient`'s
|
— extracted as `alknet-client` with three dial methods.
|
||||||
API is transport-agnostic (`from_connection`); the deferred part is the
|
`ChannelClient`'s API is transport-agnostic (`from_connection`); the
|
||||||
shared *dial* across transports, not the channels protocol.
|
dial is the shared seam, now extracted. See
|
||||||
|
[ADR-089](../../decisions/089-alknetclient-native-dial-seam.md).
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ All design decisions are documented as ADRs in [decisions/](../../decisions/).
|
|||||||
| [077](../../decisions/077-tty-inside-channels.md) | TTY Inside Channels | Two modes (direct vs channels); 5 sub-streams; control bidirectional via 3/4 |
|
| [077](../../decisions/077-tty-inside-channels.md) | TTY Inside Channels | Two modes (direct vs channels); 5 sub-streams; control bidirectional via 3/4 |
|
||||||
| [078](../../decisions/078-two-pump-shutdown-on-completion.md) | Two-Pump Pattern | Shutdown-on-completion contract; handler-level |
|
| [078](../../decisions/078-two-pump-shutdown-on-completion.md) | Two-Pump Pattern | Shutdown-on-completion contract; handler-level |
|
||||||
| [079](../../decisions/079-hub-relay-translate-not-forward.md) | Hub Relay | Translate channel 0, byte-forward data channels with ID rewrite |
|
| [079](../../decisions/079-hub-relay-translate-not-forward.md) | Hub Relay | Translate channel 0, byte-forward data channels with ID rewrite |
|
||||||
| [080](../../decisions/080-channelclient.md) | ChannelClient | Client side; transport-agnostic `from_connection` primary, `connect_quic` convenience; `AlknetClient` dial-seam deferred (OQ-55) |
|
| [080](../../decisions/080-channelclient.md) | ChannelClient | Client side; transport-agnostic `from_connection` primary, `connect_quic` convenience; `AlknetClient` dial-seam extracted (ADR-089, resolves OQ-55) |
|
||||||
| [081](../../decisions/081-channels-subcrate-decomposition.md) | Sub-Crate Decomposition | `channels-core` (pure multiplexer) / `channels-call` (call coupling + ChannelClient); hub and worker are consumers |
|
| [081](../../decisions/081-channels-subcrate-decomposition.md) | Sub-Crate Decomposition | `channels-core` (pure multiplexer) / `channels-call` (call coupling + ChannelClient); hub and worker are consumers |
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
@@ -257,10 +257,11 @@ All design decisions are documented as ADRs in [decisions/](../../decisions/).
|
|||||||
Open questions are tracked in [open-questions.md](../../open-questions.md).
|
Open questions are tracked in [open-questions.md](../../open-questions.md).
|
||||||
Key questions affecting this crate:
|
Key questions affecting this crate:
|
||||||
|
|
||||||
- **OQ-55** (deferred(scope)): `AlknetClient` core **dial+TLS seam**
|
- **OQ-55** (resolved by ADR-089): `AlknetClient` core **dial+TLS seam**
|
||||||
extraction — blocked on a second *transport's* dial. `ChannelClient`'s
|
— extracted as `alknet-client` with three dial methods.
|
||||||
API is transport-agnostic (`from_connection`); `AlknetClient` is the
|
`ChannelClient`'s API is transport-agnostic (`from_connection`); the
|
||||||
shared *dial* across transports, not the channels protocol.
|
dial is the shared seam, now extracted. See
|
||||||
|
[ADR-089](../../decisions/089-alknetclient-native-dial-seam.md).
|
||||||
- **OQ-56** (deferred(scope)): Full channel-level flow-control windowing —
|
- **OQ-56** (deferred(scope)): Full channel-level flow-control windowing —
|
||||||
bounded-buffer is decided (ADR-076); full windowing is an extension
|
bounded-buffer is decided (ADR-076); full windowing is an extension
|
||||||
blocked on a real HOL-blocking deployment observation.
|
blocked on a real HOL-blocking deployment observation.
|
||||||
|
|||||||
@@ -0,0 +1,549 @@
|
|||||||
|
---
|
||||||
|
status: draft
|
||||||
|
last_updated: 2026-07-15
|
||||||
|
---
|
||||||
|
|
||||||
|
# alknet-client
|
||||||
|
|
||||||
|
The native client dial seam — the client-side analogue of
|
||||||
|
`AlknetEndpoint`. A multi-transport dialer that takes pre-built
|
||||||
|
transport handles (quinn, TCP+TLS, iroh), dials a remote `AlknetEndpoint`
|
||||||
|
on a chosen ALPN, and produces a `Connection` for the protocol
|
||||||
|
take-overs (`CallClient::spawn_dispatch`,
|
||||||
|
`ChannelClient::from_connection`) to consume. It is the Rust native
|
||||||
|
client; it does not run protocols, manage peer lifecycle, or supervise
|
||||||
|
reconnection. It dials and produces a `Connection`.
|
||||||
|
|
||||||
|
## What
|
||||||
|
|
||||||
|
`AlknetClient` is the dial. Before this crate, each protocol client
|
||||||
|
(`CallClient::connect`, `ChannelClient::connect_quic`) built its own
|
||||||
|
QUIC dial inline — building a `TlsClientConfig`, constructing a
|
||||||
|
`quinn::Endpoint`, calling `connect_with`, wrapping as a `Connection`.
|
||||||
|
The dial boilerplate was duplicated, and there was no place for a
|
||||||
|
second transport's dial (TCP+TLS, iroh) to live without each protocol
|
||||||
|
client growing its own per-transport dial helper.
|
||||||
|
|
||||||
|
`alknet-client` extracts the dial the same way ADR-083 extracted the
|
||||||
|
accept loop on the server side: one type that takes pre-built transport
|
||||||
|
handles and produces a `Connection`, with the transport choice as a
|
||||||
|
parameter. The protocol take-overs are unchanged — they consume the
|
||||||
|
`Connection` and do not know `AlknetClient` produced it.
|
||||||
|
|
||||||
|
### The three concept layers (ADR-089 §"The tangle this ADR also names")
|
||||||
|
|
||||||
|
Three concept levels were conflated throughout the initial development.
|
||||||
|
`AlknetClient` is the fix for one of them (the establishment side);
|
||||||
|
naming all three is what makes the fix legible.
|
||||||
|
|
||||||
|
| Layer | Concepts | What it determines |
|
||||||
|
|-------|----------|--------------------|
|
||||||
|
| **Deployment role** | Hub / Worker / Hub-Worker | Who accepts, who dials — which side(s) you instantiate |
|
||||||
|
| **Establishment side** | `AlknetEndpoint` (server) / `AlknetClient` (client) | Accept-and-resolve-identity vs. dial-and-present-identity |
|
||||||
|
| **ALPN-level category** | Endpoint ALPN / Entry-point ALPN (ADR-086 §2) | Whether identity is required at the TLS layer vs. per-request |
|
||||||
|
|
||||||
|
The layers are orthogonal. A **hub** uses an `AlknetEndpoint` (server)
|
||||||
|
AND an `AlknetClient` (client, when dialing workers). A **worker** uses
|
||||||
|
an `AlknetClient` (client) AND may use an `AlknetEndpoint` (server, if
|
||||||
|
it accepts inbound). The role determines which side(s) you instantiate,
|
||||||
|
not what the side IS. `AlknetClient` is the client-side establishment
|
||||||
|
type — Layer 2 — independent of the deployment role that uses it and of
|
||||||
|
the ALPN-level category of the ALPN it dials.
|
||||||
|
|
||||||
|
### What `AlknetClient` IS
|
||||||
|
|
||||||
|
The client-side analogue of `AlknetEndpoint`: a multi-transport dialer
|
||||||
|
that produces `Connection`s for the protocol take-overs to consume.
|
||||||
|
Narrowed to the **native case**: dialing native endpoint types (QUIC +
|
||||||
|
TCP+TLS, both rustls-consuming via `TlsClientConfig`; iroh as the
|
||||||
|
key-not-config exception) over the native ALPNs (`alknet/register`,
|
||||||
|
`alknet/call`, `alknet/channels`).
|
||||||
|
|
||||||
|
### What `AlknetClient` is NOT
|
||||||
|
|
||||||
|
- **Not a protocol implementation.** It does not run the call protocol
|
||||||
|
or the channels protocol. It produces a `Connection`;
|
||||||
|
`CallClient`/`ChannelClient` take over from there. Analogue:
|
||||||
|
`AlknetEndpoint` dispatches by ALPN; the handler runs the protocol.
|
||||||
|
- **Not a hub or worker.** Hub/Worker are deployment roles that *use*
|
||||||
|
`AlknetClient` (and `AlknetEndpoint`). `AlknetClient` has no peer
|
||||||
|
lifecycle, no aggregated env, no supervision loop, no relay. The
|
||||||
|
hub's `supervise_worker` takes a `dial` closure that can call
|
||||||
|
`AlknetClient` internally — the hub does not need to know
|
||||||
|
`AlknetClient` exists.
|
||||||
|
- **Not the web/browser client.** Browsers dial via WebSocket/HTTP
|
||||||
|
(ADR-044/048) — a different client surface (the JS SDK / wasm), not
|
||||||
|
`AlknetClient`. `AlknetClient` is the Rust native client.
|
||||||
|
- **Not a replacement for `CallClient`/`ChannelClient`.** Those are
|
||||||
|
the protocol take-overs. `AlknetClient` is the dial that feeds them.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
The dial was deferred (OQ-55) because extracting a QUIC-shaped
|
||||||
|
connector would bake QUIC in as *the* establishment shape. ADR-089
|
||||||
|
resolves the deferral — three decisions (ADR-086, ADR-087, ADR-083)
|
||||||
|
collapsed the blocking conditions. See
|
||||||
|
[ADR-089](../../decisions/089-alknetclient-native-dial-seam.md) §"Why
|
||||||
|
the deferral has collapsed" for the full rationale.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### `AlknetClient`
|
||||||
|
|
||||||
|
The central type. Holds pre-built transport handles, all optional — the
|
||||||
|
client dials with whichever transport the remote endpoint type implies.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct AlknetClient {
|
||||||
|
#[cfg(feature = "quinn")]
|
||||||
|
quinn: Option<quinn::Endpoint>,
|
||||||
|
#[cfg(feature = "tcp")]
|
||||||
|
tcp_connector: Option<tokio_rustls::TlsConnector>,
|
||||||
|
#[cfg(feature = "iroh")]
|
||||||
|
iroh: Option<iroh::Endpoint>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AlknetClient {
|
||||||
|
pub fn new() -> Self;
|
||||||
|
|
||||||
|
#[cfg(feature = "quinn")]
|
||||||
|
pub fn with_quinn(mut self, endpoint: quinn::Endpoint) -> Self;
|
||||||
|
|
||||||
|
#[cfg(feature = "tcp")]
|
||||||
|
pub fn with_tcp_tls(mut self, connector: tokio_rustls::TlsConnector) -> Self;
|
||||||
|
|
||||||
|
#[cfg(feature = "iroh")]
|
||||||
|
pub fn with_iroh(mut self, endpoint: iroh::Endpoint) -> Self;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The builder mirrors `AlknetEndpoint`'s `with_quinn` / `with_iroh` /
|
||||||
|
`with_tcp_tls` (ADR-083) — the assembly layer builds the transport
|
||||||
|
handles and hands them to the client via builder methods. A native
|
||||||
|
client that needs QUIC-with-TCP+TLS-fallback holds both a quinn
|
||||||
|
endpoint and a TCP+TLS connector; a minimal iroh-only client holds only
|
||||||
|
the iroh endpoint.
|
||||||
|
|
||||||
|
### The three dials
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl AlknetClient {
|
||||||
|
/// QUIC dial. Builds a `TlsClientConfig` from `credentials`
|
||||||
|
/// (ADR-034 verifier selection + ADR-084 provider), dials `addr`
|
||||||
|
/// on `alpn`, returns a `Connection` via
|
||||||
|
/// `Connection::from_quinn_with_alpn`. The `server_name` is the
|
||||||
|
/// TLS SNI / name (for X.509; ignored for raw-key pinning).
|
||||||
|
/// Feature-gated on `quinn`.
|
||||||
|
#[cfg(feature = "quinn")]
|
||||||
|
pub async fn dial_quic(
|
||||||
|
&self,
|
||||||
|
addr: SocketAddr,
|
||||||
|
server_name: &str,
|
||||||
|
alpn: &[u8],
|
||||||
|
credentials: &CallCredentials,
|
||||||
|
) -> Result<Connection, ClientDialError>;
|
||||||
|
|
||||||
|
/// TCP+TLS dial. Builds a `TlsClientConfig` from `credentials`,
|
||||||
|
/// connects a `TcpStream` to `addr`, wraps with `TlsConnector`
|
||||||
|
/// using `host` as the SNI, returns a `Connection` via
|
||||||
|
/// `Connection::from_bidi` (ADR-065). Feature-gated on `tcp`.
|
||||||
|
#[cfg(feature = "tcp")]
|
||||||
|
pub async fn dial_tcp_tls(
|
||||||
|
&self,
|
||||||
|
host: &str,
|
||||||
|
addr: SocketAddr,
|
||||||
|
alpn: &[u8],
|
||||||
|
credentials: &CallCredentials,
|
||||||
|
) -> Result<Connection, ClientDialError>;
|
||||||
|
|
||||||
|
/// Iroh dial. Dials `node_id` on `alpn` via the iroh endpoint.
|
||||||
|
/// The iroh path does NOT use `TlsClientConfig` — iroh has its
|
||||||
|
/// own TLS (shares the `Ed25519SecretKey`, not the rustls config —
|
||||||
|
/// ADR-087 §3, ADR-089 §3). The verifier is iroh's `NodeId` match
|
||||||
|
/// (fingerprint pin by another name — ADR-034 §3). An unknown
|
||||||
|
/// iroh remote fails closed (no CA). Feature-gated on `iroh`.
|
||||||
|
#[cfg(feature = "iroh")]
|
||||||
|
pub async fn dial_iroh(
|
||||||
|
&self,
|
||||||
|
node_id: iroh::NodeId,
|
||||||
|
alpn: &[u8],
|
||||||
|
local_key: &alknet_core::config::Ed25519SecretKey,
|
||||||
|
) -> Result<Connection, ClientDialError>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The two rustls dials (`dial_quic`, `dial_tcp_tls`) share
|
||||||
|
`TlsClientConfig::new` — the ADR-034 verifier selection (fingerprint
|
||||||
|
pin for a known peer, CA-verify for an unknown X.509 remote, fail-closed
|
||||||
|
for an unknown raw-key remote) and the ADR-084 crypto provider
|
||||||
|
(`aws_lc_rs`). The iroh dial is the exception: iroh has its own TLS and
|
||||||
|
takes the `Ed25519SecretKey` directly, not a `rustls::ClientConfig`.
|
||||||
|
The consistency is in the rule (ADR-034), not in the type — the same
|
||||||
|
exception as the server side (ADR-082, ADR-087 §3).
|
||||||
|
|
||||||
|
### What the dial does NOT do
|
||||||
|
|
||||||
|
- **No protocol take-over.** The dial returns a `Connection`; the
|
||||||
|
caller hands it to `CallClient::spawn_dispatch` or
|
||||||
|
`ChannelClient::from_connection`. `AlknetClient` does not spawn the
|
||||||
|
dispatch loop or install channel 0.
|
||||||
|
- **No identity resolution.** The client *presents* its identity (via
|
||||||
|
the client cert in `TlsClientConfig`) and *verifies* the remote (via
|
||||||
|
the ADR-034 verifier). It does not resolve the remote's identity into
|
||||||
|
a `PeerId` — that happens inside the protocol take-over (the
|
||||||
|
`CallAdapter` / `CallConnection` resolves the fingerprint via
|
||||||
|
`IdentityProvider`).
|
||||||
|
- **No reconnection / supervision.** The dial is one-shot. A caller
|
||||||
|
that needs reconnect-with-backoff wraps the dial in a supervision
|
||||||
|
loop (the hub's `supervise_worker` pattern — a closure that produces
|
||||||
|
a `Connection`).
|
||||||
|
- **No transport fallback.** A caller that needs
|
||||||
|
QUIC-with-TCP+TLS-fallback dials QUIC, catches the error, and dials
|
||||||
|
TCP+TLS. `AlknetClient` provides both dials; the fallback policy is a
|
||||||
|
caller concern (or a future `dial_with_fallback` helper —
|
||||||
|
two-way-door).
|
||||||
|
|
||||||
|
### Credentials
|
||||||
|
|
||||||
|
`AlknetClient`'s dials take a `CallCredentials` bundle — the existing
|
||||||
|
type from `alknet-call` (the local `TlsIdentity`, the optional
|
||||||
|
`auth_token`, and the `RemoteIdentity` for verifier selection). The
|
||||||
|
credentials come from `Capabilities` (ADR-014), never from environment
|
||||||
|
variables — the no-env-vars invariant. The assembly layer derives them
|
||||||
|
from the vault at startup and passes them to each dial. The credential
|
||||||
|
type's crate location is a two-way-door detail — see
|
||||||
|
[ADR-089](../../decisions/089-alknetclient-native-dial-seam.md) §"What
|
||||||
|
this does NOT change" and the Dependencies section below.
|
||||||
|
|
||||||
|
### The dialable ALPNs
|
||||||
|
|
||||||
|
`AlknetClient` dials any ALPN the remote endpoint advertises. The
|
||||||
|
native ALPNs (ADR-086):
|
||||||
|
|
||||||
|
| ALPN | Category (ADR-086 §2) | Protocol take-over | Identity |
|
||||||
|
|------|----------------------|--------------------|----------|
|
||||||
|
| `alknet/register` | entry point | (registration handshake — deferred, OQ-66) | None at TLS; per-request token (or open) |
|
||||||
|
| `alknet/call` | endpoint | `CallClient::spawn_dispatch` | Fingerprint (raw key / client cert) or bearer token (first frame) |
|
||||||
|
| `alknet/channels` | endpoint | `ChannelClient::from_connection` | Fingerprint or bearer token (resolved on channel 0 — ADR-072) |
|
||||||
|
|
||||||
|
The dial is the same for all three — the difference is the protocol that
|
||||||
|
runs on the resulting `Connection`. For `alknet/register`, the protocol
|
||||||
|
is the registration handshake (deferred — see "`alknet/register`"
|
||||||
|
below). For `alknet/call` and `alknet/channels`, the protocol is the
|
||||||
|
call / channels take-over, which the caller invokes after the dial.
|
||||||
|
|
||||||
|
### `alknet/register`
|
||||||
|
|
||||||
|
The native registration entry point, parallel to HTTP registration
|
||||||
|
(OQ-58) but without the HTTP layer. A native worker that has no HTTP
|
||||||
|
client dials `alknet/register` directly. The connection is an **entry
|
||||||
|
point** (ADR-086 §2): accepted without an established peer identity,
|
||||||
|
authenticated per-request by the registration token (or open for
|
||||||
|
no-token registration). The dial produces the `Connection`; the
|
||||||
|
registration handshake runs on it.
|
||||||
|
|
||||||
|
Two registration cases (token / no-token), both hub concerns and both
|
||||||
|
optional — see [ADR-089](../../decisions/089-alknetclient-native-dial-seam.md)
|
||||||
|
§6 for the full description.
|
||||||
|
|
||||||
|
The `alknet/register` **wire protocol** (the handshake on the
|
||||||
|
`Connection` — what frames the client sends, what the hub returns) ties
|
||||||
|
into the call crate's ACL and the OQ-58 enrollment model. It is
|
||||||
|
**deferred** to a dedicated ADR. This spec names the ALPN and its
|
||||||
|
entry-point role; the HTTP registration endpoint (OQ-58) remains the
|
||||||
|
first implementation. See OQ-66.
|
||||||
|
|
||||||
|
### Relationship to `CallClient` / `ChannelClient`
|
||||||
|
|
||||||
|
The dial produces a `Connection`; the protocol take-overs consume it:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Dial QUIC, take over as channels:
|
||||||
|
let conn = client.dial_quic(addr, "alknet", b"alknet/channels", &creds).await?;
|
||||||
|
let channels = ChannelClient::from_connection(conn).await?;
|
||||||
|
|
||||||
|
// Dial TCP+TLS, take over as call:
|
||||||
|
let conn = client.dial_tcp_tls("hub.example", addr, b"alknet/call", &creds).await?;
|
||||||
|
let call = CallClient::new(registry, idp).spawn_dispatch(conn);
|
||||||
|
```
|
||||||
|
|
||||||
|
The existing convenience constructors (`CallClient::connect`,
|
||||||
|
`ChannelClient::connect_quic`) become thin wrappers over
|
||||||
|
`AlknetClient::dial_quic` — they build an ephemeral `AlknetClient` (or
|
||||||
|
accept one), dial QUIC, and call `spawn_dispatch` / `from_connection`.
|
||||||
|
They remain for the "I just want QUIC, no `AlknetClient` wiring" case.
|
||||||
|
A caller that needs transport selection (QUIC with TCP+TLS fallback)
|
||||||
|
uses `AlknetClient` directly. See
|
||||||
|
[ADR-089](../../decisions/089-alknetclient-native-dial-seam.md) §5.
|
||||||
|
|
||||||
|
### Iroh — shares the key, not the config (client side too)
|
||||||
|
|
||||||
|
The iroh client dial, like the iroh server side (ADR-082, ADR-087 §3),
|
||||||
|
does not consume a `rustls::ClientConfig`. It takes the
|
||||||
|
`Ed25519SecretKey` directly and feeds it to
|
||||||
|
`iroh::SecretKey::from_bytes`. Iroh handles TLS internally. The
|
||||||
|
verifier is iroh's `NodeId` match — the remote's `NodeId` (Ed25519
|
||||||
|
public key) is verified against the expected `NodeId`, which is
|
||||||
|
fingerprint-pinning by another name. An unknown iroh remote fails
|
||||||
|
closed (no CA to fall back to — ADR-034 §3, Assumption 1).
|
||||||
|
|
||||||
|
The `dial_iroh` method takes the `Ed25519SecretKey` as a parameter
|
||||||
|
rather than pulling it from `CallCredentials` because iroh does not use
|
||||||
|
the `TlsClientConfig` path. The assembly layer reads the key from
|
||||||
|
`StaticConfig` (in core) and passes it directly, same as the server
|
||||||
|
side's iroh endpoint construction.
|
||||||
|
|
||||||
|
### Non-Rust native clients (out of scope)
|
||||||
|
|
||||||
|
The wire protocols (channels 9-byte chunk format — ADR-071; call
|
||||||
|
`EventEnvelope` — ADR-012/064) are language-agnostic. When the endpoint
|
||||||
|
uses X.509 (the web endpoint type, or a native endpoint with X.509
|
||||||
|
instead of raw keys), non-Rust native clients (Node/Deno/Bun, Python,
|
||||||
|
wasm) can negotiate TLS with standard library TLS stacks and implement
|
||||||
|
the wire protocols directly. A wasm implementation of the wire
|
||||||
|
protocols is reusable both in-browser and server-side (Deno, etc.),
|
||||||
|
reducing the need for per-language native adapters.
|
||||||
|
|
||||||
|
`AlknetClient` is the **Rust** native client — one of several possible
|
||||||
|
native clients sharing the same wire protocols. The non-Rust clients
|
||||||
|
are out of scope for this crate; they implement the wire protocols in
|
||||||
|
their own languages. The X.509 endpoint type is what makes this
|
||||||
|
possible — raw-key (RFC 7250) endpoints require a TLS stack that
|
||||||
|
supports raw public keys, which most non-Rust runtimes do not (browsers
|
||||||
|
definitely do not — ADR-086).
|
||||||
|
|
||||||
|
### `ClientDialError`
|
||||||
|
|
||||||
|
The error type for all three dial methods. A single
|
||||||
|
`#[non_exhaustive]` enum, one variant per failure category:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum ClientDialError {
|
||||||
|
/// TLS config construction (TlsClientConfig::new failure —
|
||||||
|
/// verifier build, cert load, provider init). Wraps `TlsError`
|
||||||
|
/// from alknet-tls.
|
||||||
|
#[error("TLS config construction: {0}")]
|
||||||
|
TlsConfig(#[from] alknet_tls::TlsError),
|
||||||
|
|
||||||
|
/// Transport connect failure — quinn connect, TcpStream::connect,
|
||||||
|
/// or iroh connect. The transport's own error type, stringified.
|
||||||
|
#[error("transport connect: {0}")]
|
||||||
|
Connect(String),
|
||||||
|
|
||||||
|
/// TLS handshake failure — the handshake started but failed
|
||||||
|
/// (rejected cert, ALPN mismatch, unknown raw-key remote
|
||||||
|
/// fail-closed). Distinct from TlsConfig (which is pre-handshake).
|
||||||
|
#[error("TLS handshake: {0}")]
|
||||||
|
Handshake(String),
|
||||||
|
|
||||||
|
/// No transport handle configured for the requested dial — e.g.,
|
||||||
|
/// `dial_quic` called but `with_quinn` was not set.
|
||||||
|
#[error("no transport handle configured for {transport}")]
|
||||||
|
NoTransport { transport: &'static str },
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`TlsConfig` wraps `alknet_tls::TlsError` (ADR-088) — the config
|
||||||
|
construction errors. `Connect` and `Handshake` are transport-level
|
||||||
|
failures (pre- and post-handshake). `NoTransport` is a wiring error
|
||||||
|
(calling a dial without the matching `with_*`).
|
||||||
|
|
||||||
|
**`Handshake` resolves an ADR-088 §6 deferral.** ADR-088 §6 explicitly
|
||||||
|
scoped `TlsError` to *config-construction* errors and deferred the
|
||||||
|
handshake-error surfacing question to "the dial-seam ADR" (OQ-55, then
|
||||||
|
deferred). ADR-089 is that ADR. `ClientDialError::Handshake` is the
|
||||||
|
resolution: handshake-time errors (rejected cert, ALPN mismatch,
|
||||||
|
unknown-raw-key fail-closed) surface through the dial's error type as
|
||||||
|
`Handshake(String)`, not through `TlsError` (which stays
|
||||||
|
config-construction-only). This keeps ADR-088's scope boundary intact
|
||||||
|
while giving the dial a single error enum for all failure categories.
|
||||||
|
|
||||||
|
`Connect(String)` and `Handshake(String)` take `String` rather than
|
||||||
|
wrapping the concrete transport error types (`quinn::ConnectError`,
|
||||||
|
`io::Error`, `rustls::Error`) because the three transports' error types
|
||||||
|
are non-unifiable — the dial is transport-polymorphic, and there is no
|
||||||
|
single source type that covers quinn, tokio-rustls, and iroh. The
|
||||||
|
category is in the variant (`Connect` vs `Handshake`); the detail is in
|
||||||
|
the string. This differs from ADR-088's `TlsError` (which wraps concrete
|
||||||
|
types via `#[from]`) because `TlsError` has one source crate (rustls +
|
||||||
|
pemfile + rcgen), while `ClientDialError` spans three transport crates.
|
||||||
|
The variant granularity is decided; the exact string contents are an
|
||||||
|
implementation detail.
|
||||||
|
|
||||||
|
### Feature gates
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
quinn = ["dep:quinn", "alknet-tls/quinn"]
|
||||||
|
tcp = ["dep:tokio-rustls", "alknet-tls/tcp"]
|
||||||
|
iroh = ["dep:iroh"]
|
||||||
|
```
|
||||||
|
|
||||||
|
A deployment that dials QUIC only enables `quinn`. A deployment that
|
||||||
|
dials TCP+TLS enables `tcp`. A deployment that dials iroh enables
|
||||||
|
`iroh`. A full native client (QUIC + TCP+TLS fallback + iroh) enables
|
||||||
|
all three. The `quinn` and `tcp` features pull the corresponding
|
||||||
|
features on `alknet-tls` (for `TlsClientConfig::for_quinn` /
|
||||||
|
`for_tcp_tls`). The `iroh` feature does not pull `alknet-tls` features
|
||||||
|
— iroh has its own TLS.
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
```
|
||||||
|
alknet-client
|
||||||
|
├── alknet-core (Connection, CallCredentials/RemoteIdentity if
|
||||||
|
│ moved here, Ed25519SecretKey, types)
|
||||||
|
├── alknet-tls (TlsClientConfig — for quinn + tcp dials)
|
||||||
|
├── quinn (optional — dial_quic)
|
||||||
|
├── tokio-rustls (optional — dial_tcp_tls)
|
||||||
|
├── tokio (TcpStream, spawn)
|
||||||
|
├── iroh (optional — dial_iroh)
|
||||||
|
└── thiserror (ClientDialError)
|
||||||
|
```
|
||||||
|
|
||||||
|
`alknet-client` depends on `alknet-tls` (for `TlsClientConfig`) and
|
||||||
|
`alknet-core` (for `Connection` and types). It does **not** depend on
|
||||||
|
`alknet-call` or `alknet-channels-call` — the dial is below the
|
||||||
|
protocol. If `CallCredentials` / `RemoteIdentity` stay in
|
||||||
|
`alknet-call`, `alknet-client` depends on `alknet-call` for the type
|
||||||
|
only; the cleaner option (moving the credential type to `alknet-core`
|
||||||
|
or `alknet-client`) keeps the dial below the protocol. See
|
||||||
|
[ADR-089](../../decisions/089-alknetclient-native-dial-seam.md) §5 —
|
||||||
|
this is a two-way-door implementation detail.
|
||||||
|
|
||||||
|
## Crate dependencies (in the dep graph)
|
||||||
|
|
||||||
|
```
|
||||||
|
alknet-client
|
||||||
|
├── alknet-tls (TlsClientConfig)
|
||||||
|
│ └── alknet-core
|
||||||
|
└── alknet-core (Connection, types)
|
||||||
|
|
||||||
|
alknet-hub (uses AlknetClient for outbound worker dials)
|
||||||
|
├── alknet-client (the dial — the hub's dial_worker closure calls it)
|
||||||
|
├── alknet-channels-call (ChannelClient — the take-over)
|
||||||
|
├── alknet-call (CallAdapter, Dispatcher)
|
||||||
|
└── alknet-core (AlknetEndpoint)
|
||||||
|
|
||||||
|
alknet-worker (uses AlknetClient to dial a hub)
|
||||||
|
├── alknet-client (the dial)
|
||||||
|
├── alknet-channels-call (ChannelClient — the take-over)
|
||||||
|
└── alknet-core (AlknetEndpoint — if the worker accepts inbound)
|
||||||
|
```
|
||||||
|
|
||||||
|
`alknet-call` and `alknet-channels-call` do **not** depend on
|
||||||
|
`alknet-client`. Their take-over APIs (`spawn_dispatch`,
|
||||||
|
`from_connection`) consume a `Connection` from any source —
|
||||||
|
`AlknetClient` is one producer, but a test can hand them a
|
||||||
|
`Connection::from_stream` directly. The dependency direction is:
|
||||||
|
`alknet-client → alknet-tls → alknet-core`; the protocol crates are
|
||||||
|
parallel, not downstream of the dial.
|
||||||
|
|
||||||
|
## Assembly layer integration
|
||||||
|
|
||||||
|
A downstream worker or hub uses `alknet-client` like this:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 1. Build the transport handles (assembly layer — same pattern as
|
||||||
|
// the server side's AlknetEndpoint builder).
|
||||||
|
let quinn_endpoint = quinn::Endpoint::client("0.0.0.0:0".parse()?)?;
|
||||||
|
|
||||||
|
// 2. Build the AlknetClient with the transport handles it needs.
|
||||||
|
let client = AlknetClient::new()
|
||||||
|
.with_quinn(quinn_endpoint);
|
||||||
|
// .with_tcp_tls(tls_connector) — if TCP+TLS fallback is needed
|
||||||
|
// .with_iroh(iroh_endpoint) — if iroh is needed
|
||||||
|
|
||||||
|
// 3. Derive credentials from the vault (ADR-014 — no env vars).
|
||||||
|
let creds = CallCredentials::new()
|
||||||
|
.with_tls_identity(TlsIdentity::RawKey(local_key))
|
||||||
|
.with_remote_identity(RemoteIdentity {
|
||||||
|
fingerprint: hub_fingerprint, // known peer → fingerprint pin
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Dial the hub on alknet/channels, take over as channels.
|
||||||
|
let conn = client
|
||||||
|
.dial_quic(hub_addr, "alknet", b"alknet/channels", &creds)
|
||||||
|
.await?;
|
||||||
|
let channels = ChannelClient::from_connection(conn).await?;
|
||||||
|
|
||||||
|
// 5. Discover the hub's operations via from_call on channel 0.
|
||||||
|
let bundles = from_call(channels.call(), FromCallConfig::new()).await?;
|
||||||
|
channels.register_imported_all(bundles);
|
||||||
|
```
|
||||||
|
|
||||||
|
The hub's `supervise_worker` (hub README §"Dial") takes a `dial`
|
||||||
|
closure that produces a `Connection`. That closure can call
|
||||||
|
`client.dial_quic(...)` internally — the hub does not need to know
|
||||||
|
`AlknetClient` exists. The closure seam is preserved; `AlknetClient` is
|
||||||
|
the recommended dial producer for it.
|
||||||
|
|
||||||
|
## Design Decisions
|
||||||
|
|
||||||
|
All design decisions are documented as ADRs in
|
||||||
|
[decisions/](../../decisions/).
|
||||||
|
|
||||||
|
| ADR | Decision | Summary |
|
||||||
|
|-----|----------|---------|
|
||||||
|
| [089](../../decisions/089-alknetclient-native-dial-seam.md) | AlknetClient — native client dial seam | New crate `alknet-client`; client-side analogue of `AlknetEndpoint`; three dials (QUIC + TCP+TLS via `TlsClientConfig`, iroh via key); resolves OQ-55; `alknet/register` named, wire protocol deferred |
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
See [open-questions.md](../../open-questions.md) for full details.
|
||||||
|
|
||||||
|
- **OQ-55** (resolved by ADR-089): `AlknetClient` native dial seam —
|
||||||
|
the transport-polymorphic dial is extracted. The deferral's blocking
|
||||||
|
condition (a second transport's real dial) is met within the native
|
||||||
|
endpoint type (QUIC + TCP+TLS + iroh). The web/browser client
|
||||||
|
(WebSocket, HTTP) was never in scope. See
|
||||||
|
[ADR-089](../../decisions/089-alknetclient-native-dial-seam.md).
|
||||||
|
- **OQ-66** (deferred(scope)): `alknet/register` wire protocol — the
|
||||||
|
native registration handshake (token / no-token, the frames,
|
||||||
|
`PeerEntry` creation, session credential return). Named as a
|
||||||
|
dialable ALPN by ADR-089; the wire protocol ties into the call
|
||||||
|
crate's ACL and OQ-58 (the token model is the shared blocker) and
|
||||||
|
needs a dedicated ADR. The HTTP registration endpoint (OQ-58)
|
||||||
|
remains the first implementation.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR-089](../../decisions/089-alknetclient-native-dial-seam.md) —
|
||||||
|
the decision this spec implements
|
||||||
|
- [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md) —
|
||||||
|
`AlknetEndpoint` (the server-side shape this spec mirrors)
|
||||||
|
- [ADR-086](../../decisions/086-endpoint-types-and-entry-points.md) —
|
||||||
|
endpoint types (native = QUIC + TCP+TLS + iroh); entry-point vs.
|
||||||
|
endpoint ALPN distinction
|
||||||
|
- [ADR-087](../../decisions/087-tlsclientconfig-not-blocked-on-dial.md)
|
||||||
|
— `TlsClientConfig` (the prerequisite the dial consumes)
|
||||||
|
- [ADR-082](../../decisions/082-alknet-tls-extraction.md) —
|
||||||
|
`TlsServerConfig` / `TlsClientConfig` in `alknet-tls`
|
||||||
|
- [ADR-065](../../decisions/065-connection-from-stream-generic-single-stream.md)
|
||||||
|
— `Connection::from_stream` / `from_bidi` (the `Connection`
|
||||||
|
constructors the dials use)
|
||||||
|
- [ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md)
|
||||||
|
— client-side verifier selection (fingerprint pin vs CA vs
|
||||||
|
fail-closed)
|
||||||
|
- [ADR-084](../../decisions/084-aws-lc-rs-crypto-provider.md) —
|
||||||
|
aws-lc-rs crypto provider
|
||||||
|
- [ADR-080](../../decisions/080-channelclient.md) —
|
||||||
|
`ChannelClient::from_connection` (the take-over the dial feeds)
|
||||||
|
- [ADR-017](../../decisions/017-call-protocol-client-and-adapter-contract.md)
|
||||||
|
— `CallClient::spawn_dispatch` (the take-over the dial feeds)
|
||||||
|
- [`crates/core/endpoint.md`](../core/endpoint.md) — `AlknetEndpoint`
|
||||||
|
(the server-side complement)
|
||||||
|
- [`crates/tls/README.md`](../tls/README.md) — `TlsClientConfig`
|
||||||
|
- [`crates/call/client-and-adapters.md`](../call/client-and-adapters.md)
|
||||||
|
— `CallClient` (the protocol take-over)
|
||||||
|
- [`crates/channels/channel-client.md`](../channels/channel-client.md)
|
||||||
|
— `ChannelClient` (the protocol take-over)
|
||||||
|
- [`crates/hub/README.md`](../hub/README.md) §"Dial (outbound workers)"
|
||||||
|
— the hub-as-client case (the `supervise_worker` closure that calls
|
||||||
|
the dial)
|
||||||
|
- OQ-55 (resolved) — `AlknetClient` / client establishment extraction
|
||||||
|
- OQ-58 — worker registration flow (the HTTP path; `alknet/register`
|
||||||
|
is the native analogue)
|
||||||
|
- OQ-66 (deferred) — `alknet/register` wire protocol
|
||||||
@@ -48,7 +48,7 @@ Core library for ALPN-based protocol dispatch. Every handler crate depends on al
|
|||||||
| OQ-35 | ~~API key asymmetry~~ | dissolved | `PeerEntry` supports multiple credential paths; `ApiKeyEntry` is for tokens that ARE the identity |
|
| OQ-35 | ~~API key asymmetry~~ | dissolved | `PeerEntry` supports multiple credential paths; `ApiKeyEntry` is for tokens that ARE the identity |
|
||||||
| OQ-36 | Concrete persistence adapter shapes | resolved by ADR-035 | Read-sync / write-async split (`IdentityStore`); SQLite adapter caches in memory, honker NOTIFY for no-restart cache invalidation; `alknet-store-sqlite` crate |
|
| OQ-36 | Concrete persistence adapter shapes | resolved by ADR-035 | Read-sync / write-async split (`IdentityStore`); SQLite adapter caches in memory, honker NOTIFY for no-restart cache invalidation; `alknet-store-sqlite` crate |
|
||||||
| OQ-37 | X.509 outgoing-only case | resolved by ADR-034 | Three remote roles (public X.509 endpoint, transport relay, hub); `PeerEntry` asymmetry correct; client-side verifier by `PeerEntry` presence (CA vs fingerprint pin) |
|
| OQ-37 | X.509 outgoing-only case | resolved by ADR-034 | Three remote roles (public X.509 endpoint, transport relay, hub); `PeerEntry` asymmetry correct; client-side verifier by `PeerEntry` presence (CA vs fingerprint pin) |
|
||||||
| OQ-55 | AlknetClient / Client Establishment Extraction | deferred(scope) | Blocked on a second *transport's* real **dial** (not a second QUIC dial); extracting a QUIC-shaped connector now would bake QUIC in as *the* establishment shape — the welding ADR-065 unwound on the server side. The client take-over APIs (`CallClient::spawn_dispatch`, `ChannelClient::from_connection` — ADR-080) are transport-agnostic and decided; only the shared dial+TLS seam is deferred. |
|
| OQ-55 | AlknetClient / Client Establishment Extraction | resolved by ADR-089 | The native dial seam is extracted as `alknet-client` — the client-side analogue of `AlknetEndpoint`. Three dial methods (QUIC + TCP+TLS via `TlsClientConfig`, iroh via key). The client take-over APIs (`CallClient::spawn_dispatch`, `ChannelClient::from_connection` — ADR-080) are transport-agnostic and decided; the shared dial is now extracted. |
|
||||||
|
|
||||||
## Key Design Principles
|
## Key Design Principles
|
||||||
|
|
||||||
|
|||||||
@@ -320,12 +320,16 @@ taking over the `Connection`, it runs `from_call` on channel 0 to
|
|||||||
discover the worker's operations, registers the discovered bundles in
|
discover the worker's operations, registers the discovered bundles in
|
||||||
the connection's Layer 2 overlay, and attaches the peer to the
|
the connection's Layer 2 overlay, and attaches the peer to the
|
||||||
aggregated env. `connect_quic_worker` is the "I just want QUIC"
|
aggregated env. `connect_quic_worker` is the "I just want QUIC"
|
||||||
convenience — it builds a `TlsClientConfig` (ADR-087) with the
|
convenience — it dials QUIC (via `AlknetClient::dial_quic`, ADR-089,
|
||||||
worker's fingerprint pinned, calls `ChannelClient::connect_quic`, then
|
which builds the `TlsClientConfig` with the worker's fingerprint
|
||||||
`dial_worker_connection`. A future `connect_tcp_tls_worker` builds a
|
pinned), calls `ChannelClient::from_connection`, then
|
||||||
`TlsClientConfig` and dials TCP+TLS the same way. The one-way-door
|
`dial_worker_connection`. A future `connect_tcp_tls_worker` dials
|
||||||
surface is `dial_worker_connection`; the dial helpers are two-way-door
|
TCP+TLS via `AlknetClient::dial_tcp_tls` the same way. The
|
||||||
conveniences.
|
one-way-door surface is `dial_worker_connection`; the dial helpers are
|
||||||
|
two-way-door conveniences over `AlknetClient` (ADR-089). The hub's
|
||||||
|
`supervise_worker` (below) takes a `dial` closure that can call
|
||||||
|
`AlknetClient` internally — the hub does not need to know
|
||||||
|
`AlknetClient` exists; the closure seam is preserved.
|
||||||
|
|
||||||
#### Accept (inbound workers and browsers) — transport-agnostic
|
#### Accept (inbound workers and browsers) — transport-agnostic
|
||||||
|
|
||||||
@@ -787,6 +791,7 @@ into `CallAdapter::with_aggregated_env`.
|
|||||||
| Channel lifecycle operations | [ADR-073](../../decisions/073-channel-lifecycle-operations.md) | `channel/open`/`close`/`control`/`resources/subscribe` — what the hub translates |
|
| Channel lifecycle operations | [ADR-073](../../decisions/073-channel-lifecycle-operations.md) | `channel/open`/`close`/`control`/`resources/subscribe` — what the hub translates |
|
||||||
| Endpoint types and entry points | [ADR-086](../../decisions/086-endpoint-types-and-entry-points.md) | Three endpoint types (web/native/iroh); entry-point vs. endpoint ALPN distinction; split ALPN lists per endpoint type |
|
| Endpoint types and entry points | [ADR-086](../../decisions/086-endpoint-types-and-entry-points.md) | Three endpoint types (web/native/iroh); entry-point vs. endpoint ALPN distinction; split ALPN lists per endpoint type |
|
||||||
| `TlsClientConfig` for outbound dials | [ADR-087](../../decisions/087-tlsclientconfig-not-blocked-on-dial.md) | `alknet-tls` provides client-side TLS config; hub-as-client is a first-class use case; not blocked on the dial-seam extraction (OQ-55) |
|
| `TlsClientConfig` for outbound dials | [ADR-087](../../decisions/087-tlsclientconfig-not-blocked-on-dial.md) | `alknet-tls` provides client-side TLS config; hub-as-client is a first-class use case; not blocked on the dial-seam extraction (OQ-55) |
|
||||||
|
| `AlknetClient` native dial seam | [ADR-089](../../decisions/089-alknetclient-native-dial-seam.md) | New crate `alknet-client`; the hub's outbound worker dials use `AlknetClient` (via the `supervise_worker` closure or the `connect_quic_worker` convenience); resolves OQ-55 |
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
|
|||||||
@@ -461,12 +461,12 @@ TLS setup and cert sharing, not transport accept logic.
|
|||||||
A hub dials out to workers it supervises and to other hubs
|
A hub dials out to workers it supervises and to other hubs
|
||||||
(hub-as-client); `alknet-worker` dials a hub. Both need a
|
(hub-as-client); `alknet-worker` dials a hub. Both need a
|
||||||
`rustls::ClientConfig` with ADR-034's verifier selection and ADR-084's
|
`rustls::ClientConfig` with ADR-034's verifier selection and ADR-084's
|
||||||
crypto provider. `TlsClientConfig` centralizes this — it is not a
|
crypto provider. `TlsClientConfig` centralizes this — it is a
|
||||||
future extraction deferred behind the dial seam (OQ-55); it is a
|
present prerequisite for the first hub deployment, consumed by
|
||||||
present prerequisite for the first hub deployment.
|
`AlknetClient`'s QUIC and TCP+TLS dials (ADR-089).
|
||||||
|
|
||||||
There are exactly two clients in the alknet client surface as far as
|
There are exactly two clients in the alknet client surface as far as
|
||||||
`TlsClientConfig` and a future `AlknetClient` are concerned — **call**
|
`TlsClientConfig` and `AlknetClient` are concerned — **call**
|
||||||
(`CallClient`) and **channels** (`ChannelClient`, which is a proxy over
|
(`CallClient`) and **channels** (`ChannelClient`, which is a proxy over
|
||||||
many ALPNs via channel 0). Both must support all three transport
|
many ALPNs via channel 0). Both must support all three transport
|
||||||
accessors below; the TLS config is shared across them, the dial is
|
accessors below; the TLS config is shared across them, the dial is
|
||||||
@@ -536,22 +536,21 @@ both server and client errors) is decided — see
|
|||||||
[`TlsError`](#tlserror) section below.
|
[`TlsError`](#tlserror) section below.
|
||||||
|
|
||||||
`TlsClientConfig` produces a `rustls::ClientConfig`; the caller (the
|
`TlsClientConfig` produces a `rustls::ClientConfig`; the caller (the
|
||||||
transport-specific dial helper — `CallClient::connect_quic`, a future
|
transport-specific dial helper — `AlknetClient::dial_quic` /
|
||||||
`connect_tcp_tls`, etc.) passes it to the transport's connector. The
|
`dial_tcp_tls`, ADR-089) passes it to the transport's connector. The
|
||||||
config is transport-agnostic; the dial is not. This is the client-side
|
config is transport-agnostic; the dial is not. This is the client-side
|
||||||
analogue of ADR-065's server-side separation: the take-over
|
analogue of ADR-065's server-side separation: the take-over
|
||||||
(`spawn_dispatch` / `from_connection`, transport-agnostic) is built
|
(`spawn_dispatch` / `from_connection`, transport-agnostic) is built
|
||||||
now; the dial (transport-specific) is per-transport. The
|
now; the dial (transport-specific) is per-transport. The
|
||||||
transport-polymorphic dial extraction (`AlknetClient::dial()`) remains
|
transport-polymorphic dial is now extracted as `alknet-client`
|
||||||
deferred (OQ-55) — it is about picking the transport and calling the
|
(ADR-089, resolves OQ-55) — `AlknetClient` builds the `TlsClientConfig`
|
||||||
right connector, not about the TLS config.
|
per-dial and calls the transport's connector.
|
||||||
|
|
||||||
The client-side accessor API mirrors the server side: `for_quinn()`
|
The client-side accessor API mirrors the server side: `for_quinn()`
|
||||||
/ `for_tcp_tls()` / `rustls_config()` — three transports, same
|
/ `for_tcp_tls()` / `rustls_config()` — three transports, same
|
||||||
pattern. Iroh is the exception (see below). Both `CallClient` and
|
pattern. Iroh is the exception (see below). `AlknetClient` (ADR-089)
|
||||||
`ChannelClient` consume `TlsClientConfig` via these accessors; the
|
consumes `TlsClientConfig` via these accessors for the QUIC and TCP+TLS
|
||||||
shared dial seam (OQ-55) is about collapsing the per-transport dial
|
dials; the iroh dial is the key-not-config exception.
|
||||||
boilerplate, not about the TLS config.
|
|
||||||
|
|
||||||
### Iroh — shares the key, not the config (client side too)
|
### Iroh — shares the key, not the config (client side too)
|
||||||
|
|
||||||
@@ -722,44 +721,39 @@ See [open-questions.md](../../open-questions.md) for full details.
|
|||||||
and the "what is NOT a variant" list. The `TlsError` sketch is in the
|
and the "what is NOT a variant" list. The `TlsError` sketch is in the
|
||||||
[TlsError](#tlserror) section below.
|
[TlsError](#tlserror) section below.
|
||||||
- **OQ-64** (resolved): `alknet-tls` provides `TlsClientConfig`
|
- **OQ-64** (resolved): `alknet-tls` provides `TlsClientConfig`
|
||||||
(ADR-087). Not blocked on the dial-seam extraction (OQ-55) — the TLS
|
(ADR-087). Not blocked on the dial-seam extraction — the TLS
|
||||||
config is a prerequisite for the dial, not a consequence of it.
|
config is a prerequisite for the dial, not a consequence of it.
|
||||||
Centralizes ADR-034 verifier selection + ADR-084 provider; the
|
Centralizes ADR-034 verifier selection + ADR-084 provider; the
|
||||||
hub-as-client requirement makes it a prerequisite for the first hub
|
hub-as-client requirement makes it a prerequisite for the first hub
|
||||||
deployment. The dial seam (OQ-55) remains deferred; the TLS config
|
deployment. The dial seam is now extracted as `alknet-client`
|
||||||
does not.
|
(ADR-089, OQ-55 resolved); `TlsClientConfig` is consumed by
|
||||||
|
`AlknetClient`'s QUIC and TCP+TLS dials.
|
||||||
|
|
||||||
- **OQ-55** (deferred(scope)): `AlknetClient::dial()` — the
|
- **OQ-55** (resolved by ADR-089): `AlknetClient::dial()` — the
|
||||||
transport-polymorphic dial seam. Remains deferred (blocked on a
|
transport-polymorphic dial seam. Extracted as a new crate
|
||||||
second transport's real dial). `TlsClientConfig` (OQ-64, resolved)
|
`alknet-client` with three dial methods (`dial_quic` /
|
||||||
is not blocked on this; the dial helpers (`CallClient::connect_quic`,
|
`dial_tcp_tls` / `dial_iroh`). `TlsClientConfig` (OQ-64, resolved)
|
||||||
a future `connect_tcp_tls`, etc.) each build a `TlsClientConfig` and
|
is the prerequisite the dial consumes. See
|
||||||
call their transport's connector standalone. See
|
[`crates/client/README.md`](../client/README.md) and
|
||||||
[OQ-55](../../questions/055-alknetclient-establishment-extraction.md).
|
[ADR-089](../../decisions/089-alknetclient-native-dial-seam.md).
|
||||||
|
|
||||||
### Next session — client shape
|
### Next session — client shape
|
||||||
|
|
||||||
The client can now be defined. There are exactly two clients in the
|
The client is now specced. [`crates/client/README.md`](../client/README.md)
|
||||||
alknet client surface as far as `TlsClientConfig` and a future
|
defines `AlknetClient` — the native client dial seam (ADR-089, resolves
|
||||||
`AlknetClient` are concerned: **call** (`CallClient`) and **channels**
|
OQ-55). There are exactly two clients in the alknet client surface as
|
||||||
(`ChannelClient`, a proxy over many ALPNs via channel 0). Both consume
|
far as `TlsClientConfig` and `AlknetClient` are concerned: **call**
|
||||||
`TlsClientConfig` via the same three accessors (`for_quinn`,
|
(`CallClient`) and **channels** (`ChannelClient`, a proxy over many
|
||||||
`for_tcp_tls`, `rustls_config`); iroh is the exception (shares the key,
|
ALPNs via channel 0). Both consume `TlsClientConfig` via the same three
|
||||||
not the config). The connect details across `register`, `call`, and
|
accessors (`for_quinn`, `for_tcp_tls`, `rustls_config`); iroh is the
|
||||||
`channels` are the same at the TLS layer — an endpoint is one of a few
|
exception (shares the key, not the config). `AlknetClient` is the dial
|
||||||
well-specified types now (ADR-086), and the `TlsClientConfig` shape is
|
that feeds them — it produces a `Connection` and the protocol
|
||||||
the same regardless of which ALPN the client dials.
|
take-overs (`spawn_dispatch`, `from_connection`) consume it. The
|
||||||
|
existing `CallClient::connect` / `ChannelClient::connect_quic`
|
||||||
The client spec was previously deferred because there was no scope for
|
convenience constructors become thin wrappers over
|
||||||
it — the endpoint-types tangle (uncovered during the channels spec work)
|
`AlknetClient::dial_quic`. The `alknet/register` ALPN (native
|
||||||
had to be pulled apart first. That tangle is resolved (ADR-086); the
|
registration entry point, parallel to HTTP registration in OQ-58) is
|
||||||
endpoint types are well-specified, and the client shape collapses to
|
named by ADR-089; its wire protocol is deferred (OQ-66).
|
||||||
"same as `CallClient`, different ALPN." The next session should stop
|
|
||||||
hedging the client and work out the details: the `AlknetClient` dial
|
|
||||||
surface (OQ-55, unblocked once two transport dials exist), the
|
|
||||||
`CallClient` / `ChannelClient` shared dial pattern, and the `register`
|
|
||||||
ALPN's entry-point dial. This is a prerequisite for the first hub
|
|
||||||
deployment (the hub dials workers; workers dial a hub).
|
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,413 @@
|
|||||||
|
# ADR-089: AlknetClient — the Native Client Dial Seam
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted (resolves OQ-55)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
### The deferral and why it was valid
|
||||||
|
|
||||||
|
OQ-55 deferred `AlknetClient::dial()` — the transport-polymorphic client
|
||||||
|
dial seam — blocked on "a second transport's real dial existing." The
|
||||||
|
reasoning (recorded in the OQ-55 file and in
|
||||||
|
[channel-client.md](../crates/channels/channel-client.md) §"Relationship
|
||||||
|
to `AlknetClient`") was sound at the time: extracting a QUIC-shaped
|
||||||
|
connector and naming it `AlknetClient` would bake QUIC in as *the*
|
||||||
|
establishment shape — the same welding ADR-065 unwound on the server
|
||||||
|
side. Only one transport dial existed (`CallClient::connect` /
|
||||||
|
`ChannelClient::connect_quic`, both QUIC). The transport-polymorphic
|
||||||
|
seam was not extractable from two *different* transport implementations;
|
||||||
|
it was guessable from one.
|
||||||
|
|
||||||
|
### Why the deferral has collapsed
|
||||||
|
|
||||||
|
Three decisions landed since the deferral, each removing a blocker:
|
||||||
|
|
||||||
|
1. **ADR-086 (endpoint types)** named the native endpoint type and gave
|
||||||
|
it **two rustls-consuming transports**: QUIC (primary) and TCP+TLS
|
||||||
|
(fallback when UDP is blocked). Both consume `TlsClientConfig`; both
|
||||||
|
produce a `Connection` via `Connection::from_quinn_with_alpn` /
|
||||||
|
`Connection::from_bidi`. The native endpoint type also includes iroh
|
||||||
|
(key-based, not rustls-consuming). That is **three dial shapes within
|
||||||
|
one endpoint type** — two sharing `TlsClientConfig`, one using the
|
||||||
|
raw key directly. The OQ-55 blocking condition ("a second
|
||||||
|
transport's real dial existing") is met *within one endpoint type*,
|
||||||
|
not across two unrelated transports.
|
||||||
|
|
||||||
|
2. **ADR-087 (`TlsClientConfig`)** broke the circular hedge that linked
|
||||||
|
the TLS config to the dial. The client-side TLS config is extracted
|
||||||
|
and buildable today; it is a **prerequisite** for the dial, not a
|
||||||
|
consequence of it. Each transport-specific dial helper builds a
|
||||||
|
`TlsClientConfig` and passes it to its transport's connector. The
|
||||||
|
dial no longer waits on the TLS config; the TLS config is shared.
|
||||||
|
|
||||||
|
3. **ADR-083 (endpoint as accept-loop runner)** made the server side a
|
||||||
|
clean accept-loop runner that takes pre-built transports via
|
||||||
|
`with_quinn` / `with_iroh` / `with_tcp_tls`. The client-side analogue
|
||||||
|
— a dialer that takes pre-built transport handles and produces a
|
||||||
|
`Connection` — is now guessable by symmetry, not a shot in the dark.
|
||||||
|
The server side separates "build the transport" (assembly layer)
|
||||||
|
from "run the accept loop" (endpoint); the client side separates
|
||||||
|
"build the transport handle" (assembly layer) from "dial + produce
|
||||||
|
`Connection`" (`AlknetClient`).
|
||||||
|
|
||||||
|
The three together remove every blocker the deferral named. The dial
|
||||||
|
seam is extractable from two different rustls-consuming transport
|
||||||
|
implementations (QUIC + TCP+TLS) plus the key-based iroh path — three
|
||||||
|
real shapes, not one. The TLS config is shared. The server-side shape
|
||||||
|
gives the client-side shape by symmetry.
|
||||||
|
|
||||||
|
### The tangle this ADR also names
|
||||||
|
|
||||||
|
Three concept levels were conflated throughout the initial development,
|
||||||
|
contributing to the confusion that made `AlknetClient` hard to spec:
|
||||||
|
|
||||||
|
1. **Deployment role** — Hub / Worker / Hub-Worker. *Who accepts, who
|
||||||
|
dials, in the hub-and-spoke topology.* A hub accepts inbound and may
|
||||||
|
dial outbound (hub-as-client). A worker dials outbound and may
|
||||||
|
accept inbound (a hub-worker). A pure worker only dials.
|
||||||
|
2. **Establishment side** — `AlknetEndpoint` (server) / `AlknetClient`
|
||||||
|
(client). *Server-side accept vs. client-side dial.* The endpoint
|
||||||
|
accepts connections and resolves identity from the incoming
|
||||||
|
connection; the client dials and presents identity (client cert)
|
||||||
|
while verifying the remote (ADR-034).
|
||||||
|
3. **ALPN-level category** — endpoint ALPN / entry-point ALPN
|
||||||
|
(ADR-086 §2). *Identity-gated vs. bootstrap, at the TLS layer.*
|
||||||
|
|
||||||
|
These are orthogonal. A hub *uses* an `AlknetEndpoint` (server side) AND
|
||||||
|
*uses* an `AlknetClient` (client side, when dialing workers). A worker
|
||||||
|
*uses* an `AlknetClient` (client side) AND *may* use an `AlknetEndpoint`
|
||||||
|
(server side, if it accepts inbound). The role determines which side(s)
|
||||||
|
you instantiate, not what the side IS. `AlknetClient` is the client-side
|
||||||
|
establishment type — Layer 2 — independent of the deployment role that
|
||||||
|
uses it and of the ALPN-level category of the ALPN it dials.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### 1. `alknet-client` is a new crate
|
||||||
|
|
||||||
|
`AlknetClient` lives in a new crate `alknet-client`, not in
|
||||||
|
`alknet-core` or `alknet-tls`. The dependency profile rules out the
|
||||||
|
alternatives:
|
||||||
|
|
||||||
|
- **`alknet-core` is ruled out by a cycle.** `AlknetClient` needs
|
||||||
|
`TlsClientConfig` from `alknet-tls`, and `alknet-tls` depends on
|
||||||
|
`alknet-core`. Putting `AlknetClient` in core creates
|
||||||
|
`alknet-core → alknet-tls → alknet-core` — a circular dependency.
|
||||||
|
- **`alknet-tls` is the wrong scope.** That crate is "TLS config + cert
|
||||||
|
sharing" (`TlsServerConfig` / `TlsClientConfig`), not "dial +
|
||||||
|
transport establishment." The dial calls `quinn::Endpoint::connect`,
|
||||||
|
`TcpStream::connect` + `TlsConnector::connect`, and
|
||||||
|
`iroh::Endpoint::connect` — transport-connection establishment, not
|
||||||
|
TLS config. Putting the dial in `alknet-tls` would weld transport
|
||||||
|
establishment to cert config, the same conflation ADR-082 untangled
|
||||||
|
on the server side.
|
||||||
|
- **Folding into `alknet-hub` / `alknet-worker`** would make both
|
||||||
|
depend on each other or duplicate the dial. Both roles need the dial;
|
||||||
|
the dial is not a role concern.
|
||||||
|
|
||||||
|
`alknet-client` depends on `alknet-core` (for `Connection`,
|
||||||
|
`CallCredentials`, `RemoteIdentity`, types) + `alknet-tls` (for
|
||||||
|
`TlsClientConfig`) + transport crates (quinn, tokio-rustls, iroh —
|
||||||
|
feature-gated). The DAG is clean:
|
||||||
|
`alknet-client → alknet-tls → alknet-core`. `alknet-hub` and
|
||||||
|
`alknet-worker` (and any assembly layer) depend on `alknet-client` for
|
||||||
|
the dial; `alknet-call` and `alknet-channels-call` do not — their
|
||||||
|
take-over APIs (`spawn_dispatch`, `from_connection`) consume the
|
||||||
|
`Connection` the dial produces, without knowing `AlknetClient` produced
|
||||||
|
it.
|
||||||
|
|
||||||
|
### 2. `AlknetClient` is the client-side analogue of `AlknetEndpoint`
|
||||||
|
|
||||||
|
`AlknetEndpoint` (ADR-083) is a multi-transport **accept-loop runner**:
|
||||||
|
it takes pre-built transport endpoints and runs their accept loops,
|
||||||
|
dispatching by ALPN. `AlknetClient` is a multi-transport **dialer**: it
|
||||||
|
takes pre-built transport handles and dials a remote endpoint on a
|
||||||
|
chosen ALPN, producing a `Connection` for the protocol take-overs to
|
||||||
|
consume.
|
||||||
|
|
||||||
|
The symmetry:
|
||||||
|
|
||||||
|
| Concern | `AlknetEndpoint` (server) | `AlknetClient` (client) |
|
||||||
|
|---------|---------------------------|-------------------------|
|
||||||
|
| Transports | `with_quinn` / `with_iroh` / `with_tcp_tls` — pre-built by the assembly layer | `with_quinn` / `with_iroh` / `with_tcp_tls` — pre-built by the assembly layer |
|
||||||
|
| Per-connection work | Accept → extract ALPN + fingerprint → `Connection` → `dispatch` | Dial → TLS handshake → `Connection` (ALPN + fingerprint carried) |
|
||||||
|
| Identity | Resolved *from* the incoming connection (fingerprint from client cert, or token on channel 0) | *Presented* (local `TlsIdentity` as client cert) + remote *verified* (ADR-034 — fingerprint pin or CA) |
|
||||||
|
| What it does NOT do | Run protocols — handlers do | Run protocols — `CallClient` / `ChannelClient` do |
|
||||||
|
| Config | `TlsServerConfig` (per endpoint type, built by assembly) | `TlsClientConfig` (per-dial, built from `CallCredentials`) |
|
||||||
|
|
||||||
|
`AlknetClient` produces a `Connection`; the protocol take-overs
|
||||||
|
(`CallClient::spawn_dispatch`, `ChannelClient::from_connection`) take
|
||||||
|
over from there. This is the exact analogue of `AlknetEndpoint`
|
||||||
|
producing a `Connection` for `ProtocolHandler::handle`.
|
||||||
|
|
||||||
|
### 3. Three dial methods, one per transport family
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct AlknetClient {
|
||||||
|
// Pre-built transport handles, all optional — the client dials
|
||||||
|
// with whichever transport the remote endpoint type implies.
|
||||||
|
#[cfg(feature = "quinn")]
|
||||||
|
quinn: Option<quinn::Endpoint>,
|
||||||
|
#[cfg(feature = "tcp")]
|
||||||
|
tcp_connector: Option<tokio_rustls::TlsConnector>,
|
||||||
|
#[cfg(feature = "iroh")]
|
||||||
|
iroh: Option<iroh::Endpoint>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AlknetClient {
|
||||||
|
/// QUIC dial. Builds a `TlsClientConfig` from `credentials`
|
||||||
|
/// (ADR-034 verifier selection + ADR-084 provider), dials `addr`
|
||||||
|
/// on `alpn`, returns a `Connection` via
|
||||||
|
/// `Connection::from_quinn_with_alpn`. Feature-gated on `quinn`.
|
||||||
|
#[cfg(feature = "quinn")]
|
||||||
|
pub async fn dial_quic(
|
||||||
|
&self,
|
||||||
|
addr: SocketAddr,
|
||||||
|
server_name: &str,
|
||||||
|
alpn: &[u8],
|
||||||
|
credentials: &CallCredentials,
|
||||||
|
) -> Result<Connection, ClientDialError>;
|
||||||
|
|
||||||
|
/// TCP+TLS dial. Builds a `TlsClientConfig` from `credentials`,
|
||||||
|
/// connects `TcpStream`, wraps with `TlsConnector`, returns a
|
||||||
|
/// `Connection` via `Connection::from_bidi`. Feature-gated on `tcp`.
|
||||||
|
#[cfg(feature = "tcp")]
|
||||||
|
pub async fn dial_tcp_tls(
|
||||||
|
&self,
|
||||||
|
host: &str,
|
||||||
|
addr: SocketAddr,
|
||||||
|
alpn: &[u8],
|
||||||
|
credentials: &CallCredentials,
|
||||||
|
) -> Result<Connection, ClientDialError>;
|
||||||
|
|
||||||
|
/// Iroh dial. Dials `node_id` on `alpn` via the iroh endpoint. The
|
||||||
|
/// iroh path does NOT use `TlsClientConfig` — iroh has its own TLS
|
||||||
|
/// (shares the `Ed25519SecretKey`, not the rustls config —
|
||||||
|
/// ADR-087 §3). The verifier is iroh's `NodeId` match (fingerprint
|
||||||
|
/// pin by another name). Feature-gated on `iroh`.
|
||||||
|
#[cfg(feature = "iroh")]
|
||||||
|
pub async fn dial_iroh(
|
||||||
|
&self,
|
||||||
|
node_id: iroh::NodeId,
|
||||||
|
alpn: &[u8],
|
||||||
|
local_key: &alknet_core::config::Ed25519SecretKey,
|
||||||
|
) -> Result<Connection, ClientDialError>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The three dials share `CallCredentials` (the local identity + remote
|
||||||
|
identity + auth token bundle, from `Capabilities`). The two rustls dials
|
||||||
|
(QUIC, TCP+TLS) build a `TlsClientConfig` from the credentials; the
|
||||||
|
iroh dial uses the raw `Ed25519SecretKey` directly. This mirrors the
|
||||||
|
server side's "iroh shares the key, not the config" (ADR-082, ADR-087
|
||||||
|
§3) — the consistency is in the rule (ADR-034 verifier selection), not
|
||||||
|
in the type.
|
||||||
|
|
||||||
|
### 4. The dial is transport-polymorphic across the native endpoint type
|
||||||
|
|
||||||
|
The native endpoint type (ADR-086) has QUIC + TCP+TLS (both
|
||||||
|
rustls-consuming) + iroh (key-based). `AlknetClient` dials all three.
|
||||||
|
The two rustls dials share `TlsClientConfig::new`; the iroh dial is the
|
||||||
|
exception. The dial is transport-polymorphic within the native endpoint
|
||||||
|
type — a native client can reach a native endpoint over QUIC, TCP+TLS
|
||||||
|
(when UDP is blocked), or iroh (relay-assisted p2p). The transport
|
||||||
|
choice is the caller's, driven by network conditions and the remote
|
||||||
|
endpoint's reachability.
|
||||||
|
|
||||||
|
### 5. `CallClient::connect` / `ChannelClient::connect_quic` delegate
|
||||||
|
|
||||||
|
The existing QUIC convenience constructors on `CallClient` and
|
||||||
|
`ChannelClient` (`connect` / `connect_quic`) become thin wrappers over
|
||||||
|
`AlknetClient::dial_quic`. They build an ephemeral `AlknetClient` (or
|
||||||
|
accept one), dial QUIC, and call `spawn_dispatch` / `from_connection`.
|
||||||
|
The one-way-door surface is the `AlknetClient` dial + take-over pattern;
|
||||||
|
the per-protocol convenience constructors are two-way-door sugar over
|
||||||
|
it. This does not break the existing APIs — they remain for the "I just
|
||||||
|
want QUIC, no `AlknetClient` wiring" case. A caller that needs
|
||||||
|
transport selection (QUIC with TCP+TLS fallback) uses `AlknetClient`
|
||||||
|
directly.
|
||||||
|
|
||||||
|
### 6. `alknet/register` is a dialable ALPN (entry point, wire protocol deferred)
|
||||||
|
|
||||||
|
`AlknetClient::dial_quic` / `dial_tcp_tls` can dial the `alknet/register`
|
||||||
|
ALPN — the native registration entry point, parallel to HTTP
|
||||||
|
registration (OQ-58) but without the HTTP layer. The connection is an
|
||||||
|
**entry point** (ADR-086 §2): accepted without an established peer
|
||||||
|
identity, authenticated per-request by the registration token (or open
|
||||||
|
for no-token registration). The dial is the same as any other ALPN; the
|
||||||
|
difference is the protocol that runs on the resulting `Connection`.
|
||||||
|
|
||||||
|
Two registration cases, 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 the one-time registration token, and
|
||||||
|
enrolls its key. The hub creates a `PeerEntry` 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 `alknet/register` **wire protocol** (the handshake on the
|
||||||
|
`Connection` after the dial — what frames the client sends, what the
|
||||||
|
hub returns) ties into the call crate's ACL and the OQ-58 enrollment
|
||||||
|
model. It is **deferred** to a dedicated ADR — this ADR names the ALPN
|
||||||
|
and its entry-point role; it does not specify the wire protocol. The
|
||||||
|
HTTP registration endpoint (OQ-58) remains the first implementation;
|
||||||
|
`alknet/register` is the native analogue that removes the HTTP
|
||||||
|
dependency for workers that have no HTTP client.
|
||||||
|
|
||||||
|
### 7. OQ-55 is resolved for the native dial
|
||||||
|
|
||||||
|
OQ-55's blocking condition ("a second transport's real dial existing")
|
||||||
|
is met: the native endpoint type has two rustls-consuming transports
|
||||||
|
(QUIC + TCP+TLS) + iroh (key-based) — three dial shapes, two sharing
|
||||||
|
`TlsClientConfig`. The transport-polymorphic dial seam is extractable
|
||||||
|
from two different transport implementations. `AlknetClient` is that
|
||||||
|
seam, for the native case.
|
||||||
|
|
||||||
|
The **web/browser client** (WebSocket, HTTP — the browser bidirectional
|
||||||
|
path per ADR-044/048) was never what OQ-55 was about. The browser path
|
||||||
|
is a different client surface (the JS SDK / wasm), not a Rust dial. It
|
||||||
|
does not use `AlknetClient`; it negotiates TLS via the browser's
|
||||||
|
network stack and speaks the wire protocol over WebSocket. OQ-55
|
||||||
|
deferred the Rust transport-polymorphic dial; the browser path is out
|
||||||
|
of scope and always was. The non-Rust native clients (Node/Deno/Bun,
|
||||||
|
Python, wasm) that can negotiate TLS against an X.509 endpoint and
|
||||||
|
implement the wire protocols directly are also out of scope for
|
||||||
|
`AlknetClient` — `AlknetClient` is the Rust native client, one of
|
||||||
|
several possible native clients sharing the same wire protocols.
|
||||||
|
|
||||||
|
## What this does NOT change
|
||||||
|
|
||||||
|
- **`AlknetEndpoint` (ADR-083)** — the server side is unchanged. The
|
||||||
|
client is a new type, not a modification to the endpoint.
|
||||||
|
- **`TlsClientConfig` (ADR-087)** — the client-side TLS config is
|
||||||
|
unchanged. `AlknetClient` calls `TlsClientConfig::new` per-dial; the
|
||||||
|
config is a prerequisite, not a consequence of the dial (the
|
||||||
|
relationship ADR-087 established).
|
||||||
|
- **`CallClient::spawn_dispatch` / `ChannelClient::from_connection`**
|
||||||
|
— the take-over APIs are unchanged. They consume the `Connection`
|
||||||
|
the dial produces; they do not know `AlknetClient` produced it.
|
||||||
|
- **`CallCredentials` / `RemoteIdentity`** — unchanged. The credential
|
||||||
|
bundle `AlknetClient` takes is the existing type from `alknet-call`
|
||||||
|
(or moved to `alknet-client` / `alknet-core` as a shared type — an
|
||||||
|
implementation detail; the shape is decided).
|
||||||
|
- **The channels substrate (ADR-071)** — unchanged. The dial produces a
|
||||||
|
`Connection`; the channels protocol runs on it.
|
||||||
|
- **ADR-086 (endpoint types / entry points)** — the endpoint-type model
|
||||||
|
is unchanged. `AlknetClient` is the client-side consumer of the
|
||||||
|
native endpoint type. The entry-point vs. endpoint ALPN distinction
|
||||||
|
(§2) governs which ALPNs the client can dial and whether identity is
|
||||||
|
required — `AlknetClient` dials both; the protocol on the resulting
|
||||||
|
`Connection` differs.
|
||||||
|
- **The hub's `supervise_worker` (hub README §"Dial")** — the hub's
|
||||||
|
supervision loop takes a `dial` closure that produces a
|
||||||
|
`Connection`. That closure can call `AlknetClient::dial_quic` /
|
||||||
|
`dial_tcp_tls` internally. The hub does not need to know
|
||||||
|
`AlknetClient` exists — the closure seam is preserved. The hub spec
|
||||||
|
is updated to note `AlknetClient` as the recommended dial producer
|
||||||
|
for the closure.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
**Positive:**
|
||||||
|
|
||||||
|
- **OQ-55 is resolved.** The transport-polymorphic dial seam is
|
||||||
|
extracted, for the native case. The duplicated dial boilerplate
|
||||||
|
(each convenience constructor rebuilding `TlsClientConfig::new` +
|
||||||
|
its transport's connector) is centralized in `AlknetClient`. The
|
||||||
|
friction the deferral accepted is removed.
|
||||||
|
- **The client-side shape is symmetric with the server side.** A
|
||||||
|
reader who understands `AlknetEndpoint` (accept + dispatch) can
|
||||||
|
understand `AlknetClient` (dial + produce `Connection`) by
|
||||||
|
symmetry. The concept layers (role / side / ALPN-category) are
|
||||||
|
named, reducing the tangle that made the client hard to spec.
|
||||||
|
- **The hub-as-client case is first-class.** A hub that dials workers
|
||||||
|
(or another hub) uses `AlknetClient` — the same type a worker uses
|
||||||
|
to dial a hub. The role asymmetry (hub vs. worker) does not produce
|
||||||
|
a type asymmetry; both use the same client.
|
||||||
|
- **Transport selection is the caller's.** A native client that needs
|
||||||
|
QUIC-with-TCP+TLS-fallback dials QUIC first, falls back to TCP+TLS
|
||||||
|
on connection failure. `AlknetClient` provides both dials; the
|
||||||
|
fallback policy is a caller concern (or a future `dial_with_fallback`
|
||||||
|
helper — two-way-door).
|
||||||
|
- **`alknet/register` is named.** The native registration entry point
|
||||||
|
has a home in the ALPN registry, parallel to HTTP registration. The
|
||||||
|
wire protocol is deferred, but the ALPN and its entry-point role are
|
||||||
|
decided — a worker that has no HTTP client can register natively.
|
||||||
|
|
||||||
|
**Negative:**
|
||||||
|
|
||||||
|
- **A new crate.** `alknet-client` is one more crate in the workspace.
|
||||||
|
The cost is low (the dial is narrow), and the dependency profile
|
||||||
|
rules out the alternatives, but it is a new entry in the crate
|
||||||
|
graph.
|
||||||
|
- **The iroh dial is the exception.** It does not use
|
||||||
|
`TlsClientConfig` — iroh has its own TLS. The dial helper applies
|
||||||
|
the same ADR-034 rule via iroh's API (NodeId match). The
|
||||||
|
consistency is in the rule, not in the type. This is the same
|
||||||
|
exception as the server side (ADR-082, ADR-087 §3) — unavoidable,
|
||||||
|
and isolated to one dial method.
|
||||||
|
- **The `alknet/register` wire protocol is still deferred.** This ADR
|
||||||
|
names the ALPN and its role; the handshake protocol (token/no-token,
|
||||||
|
the frames, the `PeerEntry` creation, the session credential return)
|
||||||
|
is a separate ADR tied to OQ-58. A worker cannot register natively
|
||||||
|
until that ADR lands; the HTTP path (OQ-58) remains the first
|
||||||
|
implementation.
|
||||||
|
- **The ADR-086 entry-point/endpoint terminology is under-specified
|
||||||
|
as a general abstraction.** This ADR uses the current terms
|
||||||
|
(entry-point = no identity at TLS; endpoint = identity required) but
|
||||||
|
does not re-litigate them. The broader abstraction — that all
|
||||||
|
top-level ALPNs are "entry points to the endpoint," each handling
|
||||||
|
auth in its own way — is a separate conceptual refinement, not
|
||||||
|
this ADR's scope.
|
||||||
|
|
||||||
|
## Door type
|
||||||
|
|
||||||
|
**One-way (crate existence + dial seam).** `alknet-client` as the
|
||||||
|
shared client dial crate is structural — every outbound-dialing role
|
||||||
|
(hub, worker, hub-worker) depends on it. Reversing would mean
|
||||||
|
re-distributing the dial across crates, reintroducing the duplicated
|
||||||
|
boilerplate. The three-dial API (`dial_quic` / `dial_tcp_tls` /
|
||||||
|
`dial_iroh`) is one-way — changing the signatures after consumers exist
|
||||||
|
is a rewrite. The internal implementation (how `CallCredentials` feeds
|
||||||
|
`TlsClientConfig::new`, how the iroh dial maps the `Ed25519SecretKey`)
|
||||||
|
is two-way. The `alknet/register` ALPN name is one-way (wire
|
||||||
|
compatibility); its wire protocol is two-way until the dedicated ADR
|
||||||
|
lands.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- OQ-55 (resolved by this ADR) — `AlknetClient` / client establishment
|
||||||
|
extraction
|
||||||
|
- [ADR-083](083-endpoint-as-accept-loop-runner.md) — `AlknetEndpoint`
|
||||||
|
as multi-transport accept-loop runner; the server-side shape this
|
||||||
|
ADR mirrors on the client side
|
||||||
|
- [ADR-086](086-endpoint-types-and-entry-points.md) — endpoint types
|
||||||
|
(native has QUIC + TCP+TLS + iroh); entry-point vs. endpoint ALPN
|
||||||
|
distinction (§2)
|
||||||
|
- [ADR-087](087-tlsclientconfig-not-blocked-on-dial.md) —
|
||||||
|
`TlsClientConfig` not blocked on the dial seam; breaks the circular
|
||||||
|
hedge; the TLS config is a prerequisite for the dial
|
||||||
|
- [ADR-082](082-alknet-tls-extraction.md) — `TlsServerConfig` /
|
||||||
|
`TlsClientConfig` in `alknet-tls`; "iroh shares the key, not the
|
||||||
|
config"
|
||||||
|
- [ADR-065](065-connection-from-stream-generic-single-stream.md) —
|
||||||
|
`Connection::from_stream` / `from_bidi`; the server-side
|
||||||
|
generalization whose client-side analogue this ADR completes
|
||||||
|
- [ADR-034](034-outgoing-only-x509-and-three-peer-roles.md) —
|
||||||
|
client-side verifier selection (fingerprint pin vs CA vs fail-closed)
|
||||||
|
- [ADR-084](084-aws-lc-rs-crypto-provider.md) — aws-lc-rs crypto
|
||||||
|
provider on all paths
|
||||||
|
- [ADR-080](080-channelclient.md) — `ChannelClient::from_connection`
|
||||||
|
(the take-over `AlknetClient` feeds)
|
||||||
|
- [ADR-017](017-call-protocol-client-and-adapter-contract.md) —
|
||||||
|
`CallClient::spawn_dispatch` (the take-over `AlknetClient` feeds)
|
||||||
|
- OQ-58 — worker registration flow (the HTTP path; `alknet/register`
|
||||||
|
is the native analogue)
|
||||||
|
- `docs/architecture/crates/channels/channel-client.md` §"Relationship
|
||||||
|
to `AlknetClient`" — the deferral this ADR resolves
|
||||||
@@ -91,7 +91,7 @@ Door type is separate from whether a decision is made. A two-way door is a decis
|
|||||||
| [OQ-12](questions/012-tls-identity-provisioning-in-alknetendpoint.md) | TLS Identity Provisioning in AlknetEndpoint | resolved | one | high |
|
| [OQ-12](questions/012-tls-identity-provisioning-in-alknetendpoint.md) | TLS Identity Provisioning in AlknetEndpoint | resolved | one | high |
|
||||||
| [OQ-13](questions/013-operation-path-format-and-routing-scope.md) | Operation Path Format and Routing Scope | resolved | two | med |
|
| [OQ-13](questions/013-operation-path-format-and-routing-scope.md) | Operation Path Format and Routing Scope | resolved | two | med |
|
||||||
| [OQ-14](questions/014-batch-operation-semantics.md) | Batch Operation Semantics | resolved | two | low |
|
| [OQ-14](questions/014-batch-operation-semantics.md) | Batch Operation Semantics | resolved | two | low |
|
||||||
| [OQ-55](questions/055-alknetclient-establishment-extraction.md) | AlknetClient / Client Establishment Extraction | deferred(scope) | two | med |
|
| [OQ-55](questions/055-alknetclient-establishment-extraction.md) | AlknetClient / Client Establishment Extraction | resolved | one | med |
|
||||||
| [OQ-59](questions/059-fingerprint-module-location.md) | Should `fingerprint.rs` Stay in Core or Move to `alknet-tls`? | resolved | two | med |
|
| [OQ-59](questions/059-fingerprint-module-location.md) | Should `fingerprint.rs` Stay in Core or Move to `alknet-tls`? | resolved | two | med |
|
||||||
| [OQ-60](questions/060-transport-construction-location.md) | Where Does Transport Construction Live? | resolved | one | high |
|
| [OQ-60](questions/060-transport-construction-location.md) | Where Does Transport Construction Live? | resolved | one | high |
|
||||||
| [OQ-61](questions/061-multi-owner-shutdown-coordination.md) | Multi-Owner Shutdown Coordination | dissolved | two | med |
|
| [OQ-61](questions/061-multi-owner-shutdown-coordination.md) | Multi-Owner Shutdown Coordination | dissolved | two | med |
|
||||||
@@ -203,6 +203,12 @@ Door type is separate from whether a decision is made. A two-way door is a decis
|
|||||||
| [OQ-63](questions/063-tlserror-shape.md) | `TlsError` Shape | resolved | one | high |
|
| [OQ-63](questions/063-tlserror-shape.md) | `TlsError` Shape | resolved | one | high |
|
||||||
| [OQ-64](questions/064-client-side-tls-helper.md) | Should `alknet-tls` Provide a Client-Side TLS Config Helper? | resolved | one | high |
|
| [OQ-64](questions/064-client-side-tls-helper.md) | Should `alknet-tls` Provide a Client-Side TLS Config Helper? | resolved | one | high |
|
||||||
|
|
||||||
|
### alknet-client
|
||||||
|
|
||||||
|
| OQ | Title | Status | Door | Pri |
|
||||||
|
|----|-------|--------|------|-----|
|
||||||
|
| [OQ-66](questions/066-alknet-register-wire-protocol.md) | `alknet/register` Wire Protocol | deferred(scope) | one | med |
|
||||||
|
|
||||||
## Deferred / Blocked
|
## Deferred / Blocked
|
||||||
|
|
||||||
The safe-exit visibility surface. These questions are parked because the
|
The safe-exit visibility surface. These questions are parked because the
|
||||||
@@ -269,22 +275,34 @@ filtering the tables above.
|
|||||||
|
|
||||||
### OQ-55: AlknetClient / Client Establishment Extraction
|
### OQ-55: AlknetClient / Client Establishment Extraction
|
||||||
|
|
||||||
- **Blocked on**: a **second transport's** real dial existing (not just a
|
- **Resolved** (ADR-089): `AlknetClient` is extracted as a new crate
|
||||||
second QUIC dial). The dial is transport-specific (QUIC, HTTP, TCP+TLS,
|
`alknet-client` — the client-side analogue of `AlknetEndpoint`
|
||||||
WebTransport, raw TCP); we have one shape implemented (QUIC —
|
(ADR-083). Three dial methods (`dial_quic` / `dial_tcp_tls` /
|
||||||
`CallClient::connect` and `ChannelClient::connect_quic`). Extracting a
|
`dial_iroh`) produce a `Connection` for the protocol take-overs
|
||||||
QUIC-shaped connector now would bake QUIC in as *the* establishment
|
(`CallClient::spawn_dispatch`, `ChannelClient::from_connection`) to
|
||||||
shape — the same welding ADR-065 unwound on the server side. The blocking
|
consume. The deferral's blocking condition (a second transport's
|
||||||
condition is met when a non-QUIC dial (SSH raw-TCP, HTTP-wrapped call,
|
real dial) is met within the native endpoint type (ADR-086): QUIC +
|
||||||
TCP+TLS) exists, so the transport-polymorphic dial+TLS seam is
|
TCP+TLS (both via `TlsClientConfig`, ADR-087) + iroh (key-based) —
|
||||||
extractable from two *different* transport implementations. Note: the
|
three dial shapes, two sharing `TlsClientConfig`. The web/browser
|
||||||
*client APIs* are already transport-agnostic — `CallClient::spawn_dispatch`
|
client (WebSocket, HTTP) was never in scope. See
|
||||||
and `ChannelClient::from_connection` (ADR-080) take a pre-established
|
[ADR-089](decisions/089-alknetclient-native-dial-seam.md) and
|
||||||
`Connection`. What is deferred is the shared *dial*, not the client
|
[OQ-55](questions/055-alknetclient-establishment-extraction.md).
|
||||||
protocol surface.
|
|
||||||
- **Priority**: medium
|
|
||||||
- **Full file**: [OQ-55](questions/055-alknetclient-establishment-extraction.md)
|
- **Full file**: [OQ-55](questions/055-alknetclient-establishment-extraction.md)
|
||||||
|
|
||||||
|
### OQ-66: `alknet/register` Wire Protocol
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
- **Priority**: medium
|
||||||
|
- **Impacts**: Blocks native worker registration over QUIC/TCP+TLS
|
||||||
|
without HTTP (the native-only / no-HTTP-minimal-hub case). Does NOT
|
||||||
|
block the first hub deployment (web + native uses HTTP registration
|
||||||
|
for worker provisioning per OQ-58).
|
||||||
|
- **Full file**: [OQ-66](questions/066-alknet-register-wire-protocol.md)
|
||||||
|
|
||||||
### OQ-56: Full Channel-Level Flow-Control Windowing
|
### OQ-56: Full Channel-Level Flow-Control Windowing
|
||||||
|
|
||||||
- **Blocked on**: a real deployment observes head-of-line blocking on a
|
- **Blocked on**: a real deployment observes head-of-line blocking on a
|
||||||
|
|||||||
@@ -99,13 +99,14 @@ alknet-vault (standalone — foundational to ACL: key derivation, identity)
|
|||||||
│ │ StaticConfig, DynamicConfig
|
│ │ StaticConfig, DynamicConfig
|
||||||
│ ├── alknet-tls TlsServerConfig + TlsClientConfig — shared TLS config across quinn + TCP+TLS + iroh (ADR-082/087)
|
│ ├── alknet-tls TlsServerConfig + TlsClientConfig — shared TLS config across quinn + TCP+TLS + iroh (ADR-082/087)
|
||||||
│ ├── alknet-call CallAdapter on alknet/call, CallClient, OperationRegistry, adapters
|
│ ├── alknet-call CallAdapter on alknet/call, CallClient, OperationRegistry, adapters
|
||||||
│ └── alknet-channels
|
│ ├── alknet-channels
|
||||||
│ ├── alknet-channels-core pure multiplexer (wire format, demux/mux) — ADR-081
|
│ │ ├── alknet-channels-core pure multiplexer (wire format, demux/mux) — ADR-081
|
||||||
│ └── alknet-channels-call channel 0 pre-negotiation + lifecycle ops — ADR-081
|
│ │ └── alknet-channels-call channel 0 pre-negotiation + lifecycle ops — ADR-081
|
||||||
|
│ └── alknet-client AlknetClient — native client dial seam (QUIC + TCP+TLS + iroh); produces Connection for CallClient/ChannelClient take-over (ADR-089)
|
||||||
│
|
│
|
||||||
├── Deployment shapes
|
├── Deployment shapes
|
||||||
│ ├── alknet-hub channels hub — accepts workers, relays, aggregates (ADR-079)
|
│ ├── alknet-hub channels hub — accepts workers, relays, aggregates (ADR-079); dials workers via AlknetClient
|
||||||
│ └── alknet-worker channels worker — dials out to a hub [not yet specced]
|
│ └── alknet-worker channels worker — dials out to a hub via AlknetClient [not yet specced]
|
||||||
│
|
│
|
||||||
├── Foundational handlers (inside channels as data-channel ALPNs, or on the endpoint)
|
├── Foundational handlers (inside channels as data-channel ALPNs, or on the endpoint)
|
||||||
│ ├── alknet-tty alknet/tty — specced (ADR-052–057), implemented
|
│ ├── alknet-tty alknet/tty — specced (ADR-052–057), implemented
|
||||||
@@ -371,6 +372,7 @@ All design decisions are documented as ADRs in [decisions/](decisions/).
|
|||||||
| [085](decisions/085-workspace-scope-core-vs-consumer-repos.md) | Workspace Scope — Core vs. Consumer Repos | Core mono-repo (substrate + deployment shapes + foundational handlers + vault) vs. consumer repos (docker, agent) |
|
| [085](decisions/085-workspace-scope-core-vs-consumer-repos.md) | Workspace Scope — Core vs. Consumer Repos | Core mono-repo (substrate + deployment shapes + foundational handlers + vault) vs. consumer repos (docker, agent) |
|
||||||
| [086](decisions/086-endpoint-types-and-entry-points.md) | Endpoint Types and Entry Points | Three endpoint types (web/native/iroh); entry-point vs. endpoint ALPN distinction; split ALPN lists per endpoint type (resolves OQ-62) |
|
| [086](decisions/086-endpoint-types-and-entry-points.md) | Endpoint Types and Entry Points | Three endpoint types (web/native/iroh); entry-point vs. endpoint ALPN distinction; split ALPN lists per endpoint type (resolves OQ-62) |
|
||||||
| [087](decisions/087-tlsclientconfig-not-blocked-on-dial.md) | `TlsClientConfig` Not Blocked on Dial Seam | `alknet-tls` provides client-side TLS config; not deferred behind OQ-55; breaks the circular hedge; hub-as-client is a first-class use case |
|
| [087](decisions/087-tlsclientconfig-not-blocked-on-dial.md) | `TlsClientConfig` Not Blocked on Dial Seam | `alknet-tls` provides client-side TLS config; not deferred behind OQ-55; breaks the circular hedge; hub-as-client is a first-class use case |
|
||||||
|
| [089](decisions/089-alknetclient-native-dial-seam.md) | AlknetClient — Native Client Dial Seam | New crate `alknet-client`; client-side analogue of `AlknetEndpoint`; three dials (QUIC + TCP+TLS via `TlsClientConfig`, iroh via key); resolves OQ-55; `alknet/register` named (wire protocol deferred, OQ-66) |
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
|
|||||||
@@ -5,83 +5,51 @@
|
|||||||
§"Issues Surfaced" #1 (the `BidiStreamSource` finding that motivates
|
§"Issues Surfaced" #1 (the `BidiStreamSource` finding that motivates
|
||||||
separating the client-extraction question from the `Connection` extension
|
separating the client-extraction question from the `Connection` extension
|
||||||
question).
|
question).
|
||||||
- **Status**: deferred(scope)
|
- **Status**: resolved (ADR-089)
|
||||||
- **Door type**: two-way
|
- **Door type**: one-way (crate existence + dial seam — ADR-089)
|
||||||
- **Priority**: medium
|
- **Priority**: medium
|
||||||
- **Impacts**: Does NOT block individual transport dials — each
|
- **Resolution**: `AlknetClient` is extracted as a new crate
|
||||||
transport-specific dial helper builds its `TlsClientConfig` (ADR-087)
|
`alknet-client` — the client-side analogue of `AlknetEndpoint`
|
||||||
and its own connector standalone. Blocks only the *shared*
|
(ADR-083). Three dial methods: `dial_quic` / `dial_tcp_tls` (both
|
||||||
`AlknetClient::dial()` extraction (one entry point that picks the
|
build a `TlsClientConfig` via ADR-087 and call their transport's
|
||||||
transport and calls the right connector). Low impact until a second
|
connector) + `dial_iroh` (the key-not-config exception, shares the
|
||||||
transport's dial exists and the duplicated dial boilerplate becomes
|
`Ed25519SecretKey`). The dial produces a `Connection` for the
|
||||||
worth extracting.
|
protocol take-overs (`CallClient::spawn_dispatch`,
|
||||||
- **Blocked on**: a **second transport's** real dial existing, not just a
|
`ChannelClient::from_connection`) to consume — it does not run
|
||||||
second QUIC dial. The blocking condition is met when, e.g., the SSH
|
protocols.
|
||||||
crate's raw-TCP dial or the HTTP-wrapped call dial exists — so the
|
- **Why the deferral collapsed**: the blocking condition ("a second
|
||||||
transport-polymorphic dial+TLS seam is extractable from two
|
transport's real dial existing") is met *within the native endpoint
|
||||||
*different* transport implementations, not two QUIC variants.
|
type* (ADR-086): QUIC + TCP+TLS (both rustls-consuming via
|
||||||
`ChannelClient`'s `connect_quic` does not unblock this; it is a second
|
`TlsClientConfig`) + iroh (key-based) — three dial shapes, two
|
||||||
*client* but the same *transport shape*. `ChannelClient`'s
|
sharing `TlsClientConfig`. ADR-087 broke the circular hedge
|
||||||
`from_connection` (the transport-agnostic take-over, ADR-080) is decided
|
(`TlsClientConfig` is a prerequisite, not a consequence of the dial).
|
||||||
and is not the thing being deferred — the shared *dial* is. The deferral
|
ADR-083 made the server-side shape a clean accept-loop runner, giving
|
||||||
is on transport-polymorphism of the dial, not on client count or on the
|
the client-side shape by symmetry. The dial seam is extractable from
|
||||||
channels protocol's API.
|
two different transport implementations, not guessable from one.
|
||||||
- **What is NOT deferred (amended by ADR-087)**: the client-side TLS
|
- **Scope of the resolution**: the **native** transport-polymorphic
|
||||||
config (`TlsClientConfig`) is **not** part of this deferral. OQ-64
|
dial. The web/browser client (WebSocket, HTTP — the browser
|
||||||
is resolved — `alknet-tls` provides `TlsClientConfig` now, unblocked
|
bidirectional path per ADR-044/048) was never what this OQ was
|
||||||
by this OQ. The TLS config is a **prerequisite** for the dial, not a
|
about — it is a different client surface (the JS SDK / wasm), not a
|
||||||
consequence of it; it is transport-agnostic (ADR-034 verifier
|
Rust dial, and does not use `AlknetClient`. Non-Rust native clients
|
||||||
selection + ADR-084 provider, both already decided). Each
|
(Node/Deno/Bun, Python, wasm) that negotiate TLS against an X.509
|
||||||
transport-specific dial helper builds its `TlsClientConfig` and
|
endpoint and implement the wire protocols directly are also out of
|
||||||
passes it to its transport's connector. This OQ defers only the
|
scope — `AlknetClient` is the Rust native client, one of several
|
||||||
*transport-polymorphic dial extraction* — the shared `AlknetClient::dial()`
|
possible native clients sharing the same wire protocols.
|
||||||
that picks the transport and calls the right connector. When a second
|
- **What does NOT block on this (unchanged)**: each crate building its
|
||||||
transport's dial exists, the dial seam is extractable; the TLS config
|
own client standalone with a shared `TlsClientConfig` (ADR-087), and
|
||||||
is already shared by then.
|
each transport-specific dial helper. `CallClient`'s transport-agnostic
|
||||||
- **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
|
|
||||||
take-over (`spawn_dispatch`) and `ChannelClient`'s transport-agnostic
|
take-over (`spawn_dispatch`) and `ChannelClient`'s transport-agnostic
|
||||||
take-over (`from_connection`, ADR-080) are decided; `connect_quic`
|
take-over (`from_connection`, ADR-080) are decided; the existing
|
||||||
dials QUIC and calls `from_connection`. The SSH crate's TCP client,
|
`connect` / `connect_quic` convenience constructors become thin
|
||||||
the HTTP call client, a `connect_tcp_tls` / `connect_webtransport`
|
wrappers over `AlknetClient::dial_quic` (ADR-089 §5).
|
||||||
helper — each builds its `TlsClientConfig` (ADR-087) and its own dial
|
- **Cross-references**: ADR-089 (the resolution — `AlknetClient` native
|
||||||
standalone. Core already permits all of this —
|
dial seam), ADR-083 (the server-side shape mirrored), ADR-086
|
||||||
`Connection::from_stream` / `from_bidi` (ADR-065) handles the
|
(endpoint types — native has QUIC + TCP+TLS + iroh), ADR-087
|
||||||
non-QUIC transport on the server side, and nothing prevents a client
|
(`TlsClientConfig` — the prerequisite the dial consumes), ADR-034
|
||||||
from constructing a `Connection` the same way after its own
|
(verifier selection — centralized in `TlsClientConfig::new`), ADR-065
|
||||||
transport-specific dial. The friction is the duplicated dial
|
(server-side transport generalization — the client-side analogue
|
||||||
boilerplate (each dial helper calls its transport's connector), not
|
this OQ's deferral avoided preempting, now completed), ADR-070 (the
|
||||||
duplicated TLS config (that is shared via ADR-087). The
|
`BidiStreamSource` extension point — orthogonal to the client
|
||||||
bidirectionality criterion (a crate needs a Client type when (a) the
|
establishment question), OQ-CH-14 in
|
||||||
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
|
|
||||||
`docs/research/alknet-channels/phase-0-findings.md` (the research-scope
|
`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