docs(arch): tighten tls/endpoint/client specs — remove connect/connect_quic, move CallCredentials to core, shed alknet-call TLS deps

Review of the three new crates (alknet-tls, alknet-endpoint, alknet-client)
+ revised core found compile-blocking inconsistencies, stale claims, and
dep-graph contradictions. All resolved:

Critical:
- C1: CallClient::connect / ChannelClient::connect_quic REMOVED (not
  delegated) — keeping them as thin wrappers over AlknetClient::dial_quic
  would make protocol crates depend on alknet-client, contradicting the
  dep graph. Callers compose dial + take-over (2 lines).
- C2: alknet-client feature gates now pull alknet-core/quinn +
  alknet-core/iroh (for Connection::from_quinn_with_alpn / from_iroh).
- C3: rustls-native-certs + webpki-roots added to alknet-tls deps
  (always-present, not feature-gated — CA-verify path is transport-agnostic).

Warning:
- W1: CallCredentials/RemoteIdentity moved to alknet-core (from
  alknet-call) — the dial must not depend on the call protocol; not a
  two-way-door, it determines the dep graph.
- W2: webpki-roots fallback implemented in spec (ADR-088 §5 added) —
  the code claimed a fallback that never existed; now the store is never
  empty, NoRootAnchors unreachable, containerized deployments work.
- W3: EndpointError removed entirely (BindFailed + HandlerNotFound both
  vestigial after ADR-083); shutdown() is now infallible.
- W4: FingerprintPinVerifier moved to alknet-tls (from alknet-call) —
  alknet-call sheds quinn/rustls/rustls-pemfile/rustls-native-certs
  entirely; CallClient becomes a pure protocol crate.

Plus: ClientError removed (only produced by removed connect); S1
(CallCredentials → ClientVerifierContext mapping + auth_token stripped
at TLS boundary documented); amendment notes on ADR-017, ADR-069,
ADR-080, ADR-082, ADR-087, ADR-090; overview crate graph + README index
updated.

29 files, consistency-reviewed.
This commit is contained in:
glm-5.2 committed 2026-07-16 11:33:18 +00:00
1 parent 34e3be2801
commit bf6ce957c0
29 files changed
+524 -267

No files matched your search

@@ -152,7 +152,16 @@ This reframes the connectivity model. The quinn and iroh paths are not distingui
### Error taxonomy
> **`EndpointError` is removed** per ADR-083 (Amendment 2026-07-15 +
> the `EndpointError`-removal amendment). `BindFailed` is vestigial
> (the endpoint takes pre-bound transports); `TlsConfig` is removed
> (the endpoint takes no TLS config); `HandlerNotFound` is swallowed
> by `dispatch` (close + log, not an error). `shutdown()` is
> infallible. The sketch below is the historical shape; it does not
> survive into `alknet-endpoint`. `HandlerError` is unchanged.
```rust
// HISTORICAL — removed per ADR-083. See the note above.
pub enum EndpointError {
BindFailed(io::Error),
TlsConfig(io::Error),
@@ -167,8 +176,11 @@ pub enum HandlerError {
}
```
- `EndpointError`: Problems starting or running the endpoint. Fatal — the endpoint cannot accept connections.
- `HandlerError`: Problems within a handler's `handle()` method. Non-fatal — the connection is closed, but the endpoint keeps running.
- ~~`EndpointError`~~: **removed** (ADR-083). The endpoint takes
pre-built transports and swallows no-handler matches; `shutdown()` is
infallible.
- `HandlerError`: Problems within a handler's `handle()` method.
Non-fatal — the connection is closed, but the endpoint keeps running.
## Consequences
@@ -2,7 +2,7 @@
## Status
Accepted (amended 2026-06-26 and 2026-07-13 — see "Amendments" below)
Accepted (amended 2026-06-26, 2026-07-13, and 2026-07-16 — see "Amendments" below; the 2026-07-16 amendment per ADR-089 §5 removes `CallClient::connect`)
## Context
@@ -468,17 +468,21 @@ existing code (which already has the right structure —
shared dispatch loop, returns a live `CallConnection`. Mirrors the
server-side `CallAdapter::handle(Connection)` and
`ChannelClient::from_connection` (ADR-080).
- **`CallClient::connect(addr, credentials)`** — a QUIC convenience
constructor: dial QUIC (feature-gated on `quinn`), then
`spawn_dispatch`. Additive and two-way-door. `connect_tcp_tls`,
`connect_webtransport`, etc. join it as transports are added,
without touching the `spawn_dispatch` contract.
- **`CallClient::connect(addr, credentials)`** — ~~a QUIC convenience
constructor~~ **REMOVED per ADR-089 §5 (2026-07-16)**. The dial is
centralized in `AlknetClient` (`alknet-client`); `connect` is
deleted, not retained as a two-way-door convenience, to avoid
`alknet-call` depending on `alknet-client` and to let `alknet-call`
shed its TLS/transport deps. Callers compose
`AlknetClient::dial_quic(...).await?` +
`CallClient::new(...).spawn_dispatch(conn)`.
The door-type classification is unchanged: `spawn_dispatch` is one-way
(the handler-facing surface), `connect` is two-way (additive
convenience). The `AlknetClient` extraction (OQ-55 — the shared
dial+TLS seam) remains deferred; what is deferred is the shared *dial*,
not a QUIC-welded client API. `spawn_dispatch` is decided now.
The door-type classification is updated: `spawn_dispatch` is one-way
(the handler-facing surface); ~~`connect` is two-way (additive
convenience)~~ `connect` is **removed** (ADR-089 §5). The
`AlknetClient` extraction (OQ-55) is **resolved** by ADR-089 — the
shared dial seam is `alknet-client`; `spawn_dispatch` is the
protocol-crate take-over that consumes the dial's `Connection`.
See [client-and-adapters.md](../crates/call/client-and-adapters.md)
§"CallClient" for the reframed operational spec.
@@ -2,7 +2,11 @@
## Status
Proposed
Proposed (amended 2026-07-16 by ADR-089 §5: `CallClient::connect()` is
removed; `from_call` is now called by the assembly layer after
`AlknetClient::dial_*` + `CallClient::new(...).spawn_dispatch(conn)`,
not after `connect()`. The manual-free-function decision stands; the
`connect()` references in the body are the pre-ADR-089 shape.)
## Context
@@ -2,7 +2,23 @@
## Status
Accepted (amended 2026-07-12 — see "Amendment: transport-agnostic API" below)
Accepted (amended 2026-07-12 — see "Amendment: transport-agnostic API"
below; amended 2026-07-16 — `connect_quic` removed per ADR-089 §5, see
"Amendment: `connect_quic` removed" below)
## Amendment: `connect_quic` removed (2026-07-16, per ADR-089 §5)
The `connect_quic(addr, credentials)` convenience constructor is
**removed**. Keeping it as a thin wrapper over
`AlknetClient::dial_quic` would make `alknet-channels-call` depend on
`alknet-client`, contradicting the dep graph (the protocol crates are
parallel to the dial, not downstream of it). Callers compose
`AlknetClient::dial_quic(...).await?` + `ChannelClient::from_connection(conn).await?`.
The `from_connection` primary constructor (the 2026-07-12 amendment
below) is unchanged and remains the one-way-door surface. The
`connect_quic` references in the body of this ADR are the historical
shape; they do not survive into the implementation. See ADR-089 §5 for
the full rationale and the breaking-change acknowledgment.
## Amendment: transport-agnostic API (2026-07-12)
@@ -130,8 +130,8 @@ impl TlsServerConfig {
| `TlsIdentity` enum | `alknet-core/config.rs` | Config type — `StaticConfig` holds it |
| `Ed25519SecretKey` | `alknet-core/config.rs` | Config type — iroh reads it directly |
| `AcmeDirectory` | `alknet-core/config.rs` | Config type |
| `fingerprint.rs` | `alknet-core` | Shared by server (endpoint) and client (`alknet-call`'s `FingerprintPinVerifier`) — moving it would create a dep edge from `alknet-call` to `alknet-tls`. Production code uses `sha2` + manual DER only; `rustls` is test-only. See OQ-59. |
| `AlknetEndpoint` | `alknet-core` | The endpoint struct stays; it takes no TLS config (see ADR-083) |
| `fingerprint.rs` | `alknet-core` | Shared by server (endpoint) and client (`FingerprintPinVerifier`, now in `alknet-tls` per ADR-089 §5 — the original dep-edge concern from `alknet-call` is dissolved) — production code uses `sha2` + manual DER only; `rustls` is test-only. See OQ-59. |
| `AlknetEndpoint` | `alknet-endpoint` | Extracted from `alknet-core` per ADR-083 Am. 2026-07-15; takes no TLS config (see ADR-083) |
### Feature gates
@@ -240,9 +240,10 @@ that compiles and passes type-checks but silently changes TLS behavior:
[ADR-083](083-endpoint-as-accept-loop-runner.md)). This is expected —
it is the point of the extraction.
- `alknet-core` may keep a narrow `rustls` dep if `fingerprint.rs` stays
(OQ-59). If `fingerprint.rs` moves to `alknet-tls`, `alknet-call`'s
client-side `FingerprintPinVerifier` gains a dep on `alknet-tls`. The
trade-off is documented in OQ-59.
(OQ-59). (Resolved by ADR-089 §5: `FingerprintPinVerifier` moved to
`alknet-tls`, so the `alknet-call` → `alknet-tls` dep-edge concern that
drove this trade-off is moot — `alknet-call` shed its `rustls` dep
entirely.)
- A new crate in the dep graph. Small, focused, one clear
responsibility.
@@ -278,8 +279,10 @@ details that can change without breaking the contract.
- ADR-080 — `ChannelClient::from_connection` (transport-agnostic
client; the pattern this ADR mirrors on the TLS side)
- OQ-59 — resolved: `fingerprint.rs` stays in `alknet-core` (the
client-side `FingerprintPinVerifier` in `alknet-call` must not depend
on `alknet-tls`; the `rustls` dep in core is test-only).
client-side `FingerprintPinVerifier` is now in `alknet-tls` per
ADR-089 §5, so the `alknet-call` → `alknet-tls` dep-edge concern that
drove this resolution is dissolved; the `rustls` dep in core is
test-only).
- `crates/alknet-core/src/endpoint.rs` — the code being extracted
- `crates/alknet-core/src/config.rs` — `TlsIdentity`, `Ed25519SecretKey`
(staying in core)
@@ -223,9 +223,12 @@ impl AlknetEndpoint {
pub async fn run(self: Arc<Self>);
/// Signal all owned accept loops to stop, drain in-flight handlers
/// for `drain_timeout`, then close. One owner, one shutdown — no
/// external loop coordination needed.
pub async fn shutdown(&self) -> Result<(), EndpointError>;
/// for `drain_timeout`, then close. Infallible — the endpoint owns
/// all its accept loops (quinn, iroh, TCP+TLS) and takes pre-built
/// pre-bound transports, so there is no bind failure path and no
/// external loop coordination. No-handler matches are swallowed by
/// `dispatch` (close + log). One owner, one shutdown.
pub async fn shutdown(&self);
}
```
@@ -386,13 +389,13 @@ live in the hub crate, not a generic transport crate.
| `load_cert_chain()` / `load_private_key()` | `alknet-tls` (ADR-082) |
| `build_quinn_server_config_from_rustls()` | `alknet-tls` (`for_quinn()`, ADR-082) |
| `build_iroh_endpoint()` | Assembly layer (inlined; 15 lines of iroh API calls) |
| `AlknetEndpoint`, `HandlerRegistry`, `EndpointError`, all dispatch/loop/extraction code | `alknet-endpoint` (this ADR, §"Amendment 2026-07-15") |
| `AlknetEndpoint`, `HandlerRegistry`, all dispatch/loop/extraction code | `alknet-endpoint` (this ADR, §"Amendment 2026-07-15") |
| `EndpointError` | **removed** — `BindFailed` and `HandlerNotFound` are both vestigial (endpoint takes pre-bound transports; `dispatch` swallows no-handler matches); `shutdown()` is infallible |
### What stays in `alknet-endpoint` (the new crate)
- `AlknetEndpoint` struct (multi-transport accept-loop runner + public `dispatch`)
- `HandlerRegistry`
- `EndpointError`
- `TcpTlsListener` (the `(TcpListener, TlsAcceptor)` tuple for the TCP+TLS field)
- `dispatch` (public — ACME guard, handler lookup, `build_auth_context`, spawn)
- `dispatch_quinn` / `dispatch_iroh` / `dispatch_tcp_tls` (private — transport-specific extraction, then call `dispatch`)
@@ -418,8 +421,8 @@ The endpoint is extracted from `alknet-core` into a new crate
identity_provider, drain_timeout)` + `with_quinn` / `with_iroh` /
`with_tcp_tls` + public `dispatch` + `run` / `shutdown` — is **unchanged**
by this amendment. Only the *location* changes: `endpoint.rs`,
`HandlerRegistry`, and `EndpointError` move from `alknet-core` to
`alknet-endpoint`.
`HandlerRegistry` move from `alknet-core` to `alknet-endpoint`
(`EndpointError` is removed — see above).
#### Why extract (the dependency data)
@@ -530,7 +533,7 @@ refactor.
| `fingerprint.rs` | ~260 | yes — shared by server + client (OQ-59) |
| `ownership.rs` (`OwnershipStore`, `OwnershipProvider`) | ~280 | yes — shared |
| `store.rs` (`CredentialStore`, `EncryptedData`) | ~200 | yes — shared |
| `endpoint.rs` (`AlknetEndpoint`, `HandlerRegistry`, `EndpointError`) | ~1600 | **no** — moves to `alknet-endpoint` |
| `endpoint.rs` (`AlknetEndpoint`, `HandlerRegistry`) | ~1600 | **no** — moves to `alknet-endpoint` (`EndpointError` is removed, not moved) |
Core loses ~1600 LOC (the endpoint) and 5 heavy deps (`quinn`, `iroh`,
`rcgen`, `rustls-pemfile`, `rustls-acme`). The remaining ~3200 LOC is
@@ -123,22 +123,30 @@ sketched lightly here; the full variant-granularity of `TlsError` is
OQ-63 (the next session).
This is **not** the dial. `TlsClientConfig` produces a
`rustls::ClientConfig`; the caller (the transport-specific dial helper,
or `CallClient::connect_quic`, or a future `connect_tcp_tls`) passes
it to the transport's connector. The config is transport-agnostic; the
dial is not.
`rustls::ClientConfig`; the caller (the transport-specific dial helper
— now `AlknetClient::dial_quic` / `dial_tcp_tls` per ADR-089; the
per-protocol `CallClient::connect` / `ChannelClient::connect_quic`
convenience constructors are removed per ADR-089 §5) passes it to the
transport's connector. The config is transport-agnostic; the dial is
not.
### 2. The dial seam (OQ-55) is unaffected
### 2. The dial seam (OQ-55) — subsequently resolved by ADR-089
> **Update (2026-07-16):** OQ-55 is now resolved by ADR-089. The text
> below is the original (pre-ADR-089) framing, preserved for context.
> `AlknetClient` is the extracted dial seam; the per-protocol
> convenience constructors are removed, not retained as wrappers.
OQ-55 (the `AlknetClient` transport-polymorphic dial extraction)
remains deferred. The deferral is about the *dial* —
transport-specific connection establishment — not about the TLS config.
With `TlsClientConfig` in `alknet-tls`, each transport-specific dial
helper (`CallClient::connect_quic`, a future `connect_tcp_tls`, a
future `connect_iroh`) builds its `TlsClientConfig` and passes it to
its transport's connector. The friction (each dial helper calls
`TlsClientConfig::new` + its transport's connect) is real but narrow —
it's duplicated `TlsClientConfig::new` calls, not duplicated verifier
~~remains deferred~~ **is resolved by ADR-089**. The deferral was about
the *dial* — transport-specific connection establishment — not about
the TLS config. With `TlsClientConfig` in `alknet-tls`, each
transport-specific dial helper (now `AlknetClient::dial_quic` /
`dial_tcp_tls` / `dial_iroh`, ADR-089) builds its `TlsClientConfig`
and passes it to its transport's connector. The friction (each dial
helper calls `TlsClientConfig::new` + its transport's connect) is real
but narrow — it's duplicated `TlsClientConfig::new` calls, not
duplicated verifier
selection logic. When a second transport's dial exists, the dial
seam is extractable (OQ-55 unblocked); the TLS config is already
shared by then.
@@ -207,12 +215,12 @@ the same way.
establishing that `Connection`, not about the take-over.
- **`FingerprintPinVerifier`** — the existing verifier in
`alknet-call` is the current implementation of ADR-034's
fingerprint-pin path. `TlsClientConfig::new` centralizes the
verifier construction (including the CA-verify and fail-closed
paths that `FingerprintPinVerifier` does not cover). The
`FingerprintPinVerifier` type may move to `alknet-tls` or stay in
`alknet-call` and be constructed by `TlsClientConfig::new` — an
implementation detail, not an architecture decision.
fingerprint-pin path. It **moves to `alknet-tls`** (amended by
ADR-089 §5): with `CallClient::connect` removed, it has no remaining
home in `alknet-call`, and it is a TLS concern (implements
`rustls::client::danger::ServerCertVerifier`). `TlsClientConfig::new`
constructs it. Moving it lets `alknet-call` shed its direct `rustls`
dep entirely — `CallClient` becomes a pure protocol crate.
## Consequences
@@ -67,7 +67,7 @@ The decision is grounded in two sources, not memory:
| `FingerprintPinVerifier::new(...)` | **infallible** | stores the fingerprint + supported algorithms; the known-peer path |
| `WebPkiServerVerifier::builder_with_provider(...).build()` | **`VerifierBuilderError`** | `rustls::webpki::VerifierBuilderError` — `NoRootAnchors`, `InvalidCrl`; the unknown-X.509-remote path |
| `rustls::RootCertStore::add(cert)` | `rustls::Error` | adding a native root cert (maps `webpki::Error` → `InvalidCertificate(...)`) |
| `rustls-native-certs::load_native_certs()` errors | logged, not returned | the current client code logs and continues; an empty store falls back to built-in webpki-roots |
| `rustls-native-certs::load_native_certs()` errors | logged, not returned | native-certs load errors are logged and the load continues; the built-in `webpki-roots` are merged into the store when the platform store is empty (see §5), so the store is never empty and `NoRootAnchors` is unreachable in practice |
| Client-auth cert resolver — RawKey | **infallible** | builds `CertifiedKey` in-memory |
| Client-auth cert resolver — X.509 | `io::Error` (load) + `rustls::Error` (`CertifiedKey::from_der`) | cert/key file load + key parse |
| "ACME TLS identity is server-only; cannot be used for client auth" | `io::Error`-shape | the `TlsIdentity::Acme` as client-identity guard |
@@ -287,7 +287,37 @@ The enum is `#[non_exhaustive]` so adding variants (e.g., a future
`TcpWrap` if `for_tcp_tls` ever gains a failure path — it does not today)
is not a breaking change.
### 5. ACME — errors are stream events, not `TlsError` variants
### 5. Root store fallback — `webpki-roots` when the platform store is empty
The unknown-X.509-remote CA-verification path in `TlsClientConfig::new`
loads the platform's native root certs (`rustls-native-certs`). The
current code's comment claims a fallback to built-in `webpki-roots` when
the platform store is empty, but the code does no such thing — an empty
store produces `NoRootAnchors` from `WebPkiServerVerifier::build()`,
surfacing as `TlsError::VerifierBuild`. This is a latent bug for
containerized deployments (no system CA bundle) dialing public X.509
endpoints — a real deployment shape.
The extraction to `alknet-tls` is the moment to make the claim true.
`TlsClientConfig::new`'s CA-verify path merges the built-in
`webpki-roots` into the `RootCertStore` when the platform store comes
back empty (or unconditionally — the built-in roots are a superset of
trust, and native certs are mostly the same CAs). `webpki-roots` is an
always-present dep in `alknet-tls` (not feature-gated — the CA-verify
path is transport-agnostic). Native-certs *load* errors are still
logged, not returned (preserved behavior); the fallback guarantees the
store is non-empty regardless, so `NoRootAnchors` is unreachable in
practice. The `VerifierBuild` variant remains in `TlsError` for the
`InvalidCrl` case and forward-compatibility, but the empty-store case
is eliminated.
This is a behavior change from the current code (which silently fails on
empty stores), but it is the behavior the docs already claimed — making
the claim true, not preserving a latent bug. See ADR-089 §5 for the
broader principle: preserve behavior only when it makes sense; a claim
without code is a bug, not a contract.
### 6. ACME — errors are stream events, not `TlsError` variants
`AcmeConfig::new()` and `AcmeConfig::state()` are infallible (confirmed
in the rustls-acme 0.12.1 source). `TlsServerConfig::new`'s ACME branch
@@ -310,7 +340,7 @@ the caller (rather than log them), that is a new ADR — it changes the
`TlsServerConfig` API (the ACME handle would need an error channel) and
is out of scope for the error-shape decision.
### 6. The fail-closed distinction
### 7. The fail-closed distinction
OQ-63's framing listed "unknown-remote fail-closed (not an error to
return — it's a `Result::Err` the caller gets for trying to connect to
@@ -334,9 +364,10 @@ distinction:
`TlsError` is the **config-construction** error type. Handshake-time
errors are the transport's error type. This keeps `TlsError` scoped to
what `new` and `for_quinn` can actually fail on, and avoids pretending
handshake outcomes are config-construction errors. A future ADR that
introduces a dial helper (the OQ-55 dial seam) would define how
handshake errors are surfaced then; that is not this ADR.
handshake outcomes are config-construction errors. The dial seam
(ADR-089, `AlknetClient`) surfaces handshake errors as
`ClientDialError::Handshake(String)` — see the client spec's
`ClientDialError` section.
## Consequences
@@ -216,18 +216,68 @@ type — a native client can reach a native endpoint over QUIC, TCP+TLS
choice is the caller's, driven by network conditions and the remote
endpoint's reachability.
### 5. `CallClient::connect` / `ChannelClient::connect_quic` delegate
### 5. `CallClient::connect` / `ChannelClient::connect_quic` are removed
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.
`ChannelClient` (`connect` / `connect_quic`) are **removed**, not
delegated. Keeping them as thin wrappers over `AlknetClient::dial_quic`
would make `alknet-call` / `alknet-channels-call` depend on
`alknet-client` — contradicting the dep graph (§1: the protocol crates
are parallel to the dial, not downstream of it) and re-coupling every
`CallClient` user to `quinn` + `rustls` + the TLS verifier machinery,
the exact welding the extraction undoes. Duplicating the dial inline in
each convenience constructor would preserve the dep graph but defeats
the point of centralizing the dial.
The dial is a distinct concern from the protocol take-over.
`AlknetClient` is the single home for the dial; `CallClient` /
`ChannelClient` are the single home for the take-over. A caller that
wants the old one-liner shape composes two lines:
`client.dial_quic(...).await?` then
`CallClient::new(...).spawn_dispatch(conn)` (or
`ChannelClient::from_connection(conn).await?`). The one-way-door
surface is the `AlknetClient` dial + take-over pattern; the
per-protocol convenience constructors are gone, not retained as
two-way-door sugar.
This is a breaking change to `CallClient` / `ChannelClient`'s public
APIs. It is expected — the develop branch is a total rewrite addressing
issues not feasible to fix inline against `main`; there are no external
consumers to preserve compatibility for. The migration plan handles
the call-site updates.
**Consequence: `CallCredentials` / `RemoteIdentity` move to
`alknet-core`.** These types were in `alknet-call` because
`CallClient::connect` consumed them. With `connect` removed, the dial
(`AlknetClient`) is the consumer, and the dial must not depend on
`alknet-call` (§1). Both the call and channels clients need them (the
channels client takes `CallCredentials` for its own removed
`connect_quic`, and the dial takes them for all three dials). They move
to `alknet-core` — the shared-types crate, alongside `TlsIdentity` and
`AuthToken` which already live there. This is the cleaner of the two
options the original ADR-089 draft called a "two-way-door
implementation detail"; it is not implementation detail — it determines
the dep graph, and the dep graph requires it.
**Consequence: `FingerprintPinVerifier` moves to `alknet-tls`.** With
`connect` removed and the verifier-selection logic centralized in
`TlsClientConfig::new` (ADR-087), `FingerprintPinVerifier` has no
remaining home in `alknet-call`. It is a TLS concern (it implements
`rustls::client::danger::ServerCertVerifier`); moving it to
`alknet-tls` lets `alknet-call` shed its direct `rustls`,
`rustls-pemfile`, and `rustls-native-certs` deps entirely — `CallClient`
becomes a pure protocol crate (`{registry, identity_provider}` +
`spawn_dispatch`). See ADR-087 §5 (amended).
**Consequence: `ClientError` is removed.** The existing
`ClientError { Transport, TlsSetup, ConnectionClosed }` was produced
only by `connect` (`Transport` and `TlsSetup`) and by no current
`spawn_dispatch` path (`ConnectionClosed` is a `FrameError`/`StreamError`
variant internal to the dispatch loop, not a `CallClient` API error).
With `connect` gone, `ClientError` has no producing call site. It is
removed rather than left as a vestigial enum. If `spawn_dispatch` ever
gains a failure path, a fresh error type is cleaner than retrofitting
this one.
### 6. `alknet/register` is a dialable ALPN (entry point, wire protocol deferred)
@@ -292,10 +342,11 @@ several possible native clients sharing the same wire protocols.
- **`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).
- **`CallCredentials` / `RemoteIdentity`** — **moved to `alknet-core`**
(see §5). The shape is unchanged; the location changes from
`alknet-call` to `alknet-core` so the dial does not depend on the
call protocol. Both the call and channels clients consume them from
core.
- **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
@@ -318,9 +369,16 @@ several possible native clients sharing the same wire protocols.
- **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 removed convenience constructors each rebuilt
`TlsClientConfig::new` + their transport's connector) is centralized
in `AlknetClient`. The friction the deferral accepted is removed.
- **`alknet-call` becomes a pure protocol crate.** With `connect`
removed and `FingerprintPinVerifier` moved to `alknet-tls` (§5),
`alknet-call` sheds its direct `quinn`, `rustls`, `rustls-pemfile`,
and `rustls-native-certs` deps. `CallClient` is `{registry,
identity_provider}` + `spawn_dispatch` — no TLS, no transport. Every
handler crate that uses `CallClient` stops transitively linking the
TLS/transport stack.
- **The client-side shape is symmetric with the server side.** A
reader who understands `AlknetEndpoint` (accept + dispatch) can
understand `AlknetClient` (dial + produce `Connection`) by
@@ -342,6 +400,15 @@ several possible native clients sharing the same wire protocols.
**Negative:**
- **Breaking change: `CallClient::connect` / `ChannelClient::connect_quic`
removed; `CallCredentials` / `RemoteIdentity` / `FingerprintPinVerifier`
relocated; `ClientError` removed.** Call sites that used the
convenience constructors must switch to `AlknetClient::dial_*` +
`spawn_dispatch` / `from_connection`. Import paths for
`CallCredentials` / `RemoteIdentity` change from `alknet_call` to
`alknet_core`. This is expected — the develop branch is a total
rewrite; there are no external consumers to preserve compatibility
for. The migration plan handles the call-site + import updates.
- **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
@@ -135,11 +135,13 @@ pub struct Socks5Credentials {
}
```
The config's crate location is an implementation detail (it can live in
`alknet-client`, `alknet-core`, or `alknet-tls` alongside the other
client config types), same as the `CallCredentials` location is an
implementation detail per ADR-089 §5. The shape is decided; the crate
home is two-way-door.
The config's crate location is an implementation detail (it can live
in `alknet-client`, `alknet-core`, or `alknet-tls` alongside the other
client config types) — the shape is decided; the crate home is
two-way-door. (Note: `CallCredentials`'s location is **not** a
two-way-door — it is decided as `alknet-core` per ADR-089 §5, because it
determines the dep graph; `Socks5ProxyConfig`'s location does not have
that dep-graph consequence.)
### 2. `AlknetClient` holds the proxy; each dial applies it