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

+12 -11
View File
@@ -125,7 +125,8 @@ translates `channel/open` on channel 0 with `forwarded_for` — ADR-032;
data channels byte-forwarded with `channel_id` rewrite; the hub never runs
protocol-specific handlers),
[ADR-080](decisions/080-channelclient.md) (`ChannelClient`,
transport-agnostic `from_connection` primary + `connect_quic` convenience,
transport-agnostic `from_connection` primary; `connect_quic` removed
per ADR-089 §5 (dial extracted to `AlknetClient`),
bidirectionality preserved; `AlknetClient` dial-seam extracted as
`alknet-client` per ADR-089, resolving OQ-55),
[ADR-081](decisions/081-channels-subcrate-decomposition.md) (sub-crate
@@ -209,7 +210,7 @@ adapter location map is now consistent: all HTTP-backed adapters
|----------|--------|-------------|
| [overview.md](overview.md) | draft | Workspace-level overview, crate graph (core mono-repo scope per ADR-085), hub/worker model, shared types, design principles |
| [open-questions.md](open-questions.md) | draft | OQ index — theme-grouped tables + Deferred/Blocked section; per-OQ files in [`questions/`](questions/) |
| [crates/core/README.md](crates/core/README.md) | draft | alknet-core crate index — shared types + auth + config (endpoint extracted to `alknet-endpoint` per ADR-083 Am. 2026-07-15) |
| [crates/core/README.md](crates/core/README.md) | draft | alknet-core crate index — shared types + auth + config (endpoint extracted to `alknet-endpoint` per ADR-083 Am. 2026-07-15; `CallCredentials`/`RemoteIdentity` moved here from `alknet-call` per ADR-089 §5) |
| [crates/core/core-types.md](crates/core/core-types.md) | draft | ProtocolHandler, HandlerError, Connection (`Box<dyn BidiStreamSource>` — ADR-070), BidiStreamSource trait, BiStream, StreamError |
| [crates/core/endpoint.md](crates/core/endpoint.md) | deprecated | Endpoint spec — **moved to `alknet-endpoint`** (ADR-083 Am. 2026-07-15); see [`crates/endpoint/README.md`](crates/endpoint/README.md) |
| [crates/core/auth.md](crates/core/auth.md) | draft | AuthContext (incl. `anonymous` constructor), Identity, IdentityProvider, AuthToken, resolution flow |
@@ -217,7 +218,7 @@ adapter location map is now consistent: all HTTP-backed adapters
| [crates/call/README.md](crates/call/README.md) | draft | alknet-call crate index |
| [crates/call/call-protocol.md](crates/call/call-protocol.md) | draft | CallAdapter, hand-rolled EventEnvelope framing (no irpc — ADR-064), stream model, PendingRequestMap, bidirectional calls, streaming subscribe example |
| [crates/call/operation-registry.md](crates/call/operation-registry.md) | draft | OperationSpec, Handler, OperationRegistry, AccessControl, capability injection, service discovery (hand-rolled, no irpc) |
| [crates/call/client-and-adapters.md](crates/call/client-and-adapters.md) | draft | CallClient (transport-agnostic `spawn_dispatch` primary, `connect` QUIC convenience — ADR-017 Am. 2026-07-13), from_call, OperationAdapter trait, adapter location map, no-env-vars invariant, exchange-of-operations pattern (from_jsonschema moved to alknet-http per ADR-066) |
| [crates/call/client-and-adapters.md](crates/call/client-and-adapters.md) | draft | CallClient (transport-agnostic `spawn_dispatch` primary; `connect` removed per ADR-089 §5 — dial extracted to `AlknetClient`), from_call, OperationAdapter trait, adapter location map, no-env-vars invariant, exchange-of-operations pattern (from_jsonschema moved to alknet-http per ADR-066) |
| [crates/http/README.md](crates/http/README.md) | draft | alknet-http crate index |
| [crates/http/overview.md](crates/http/overview.md) | draft | Crate purpose, two roles (server + client host), dependencies, adapter location map |
| [crates/http/http-server.md](crates/http/http-server.md) | draft | HttpAdapter for h2/http1.1 + WebSocket upgrade route, axum over QUIC, Bearer auth, stealth, /healthz |
@@ -241,16 +242,16 @@ adapter location map is now consistent: all HTTP-backed adapters
| [crates/vault/service.md](crates/vault/service.md) | stable | VaultServiceHandle lifecycle, direct dispatch, cache, error model |
| [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/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); optional SOCKS5 proxy (ADR-090 — UDP ASSOCIATE for QUIC, CONNECT for TCP+TLS, force-relay-only + HTTP-to-SOCKS5 bridge for iroh; OQ-67 resolved); produces `Connection` for `CallClient`/`ChannelClient` take-over; `alknet/register` named (wire protocol deferred, OQ-66) |
| [crates/endpoint/README.md](crates/endpoint/README.md) | draft | alknet-endpoint crate — the server-side accept-loop runner (`AlknetEndpoint`), extracted from `alknet-core` (ADR-083 Am. 2026-07-15); takes pre-built transports via `with_quinn`/`with_iroh`/`with_tcp_tls`; public `dispatch` for SSH/WT; handler crates no longer transitively link quinn/iroh |
| [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); `FingerprintPinVerifier` moved here from `alknet-call` (ADR-089 §5); `webpki-roots` fallback for empty platform stores (ADR-088 §5); 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); optional SOCKS5 proxy (ADR-090 — UDP ASSOCIATE for QUIC, CONNECT for TCP+TLS, force-relay-only + HTTP-to-SOCKS5 bridge for iroh; OQ-67 resolved); produces `Connection` for `CallClient`/`ChannelClient` take-over; `CallClient::connect`/`ChannelClient::connect_quic` removed (dial centralized here); `alknet/register` named (wire protocol deferred, OQ-66) |
| [crates/endpoint/README.md](crates/endpoint/README.md) | draft | alknet-endpoint crate — the server-side accept-loop runner (`AlknetEndpoint`), extracted from `alknet-core` (ADR-083 Am. 2026-07-15); takes pre-built transports via `with_quinn`/`with_iroh`/`with_tcp_tls`; public `dispatch` for SSH/WT; `EndpointError` removed (vestigial); handler crates no longer transitively link quinn/iroh |
| [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/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-connection.md](crates/channels/channels-connection.md) | draft | `ChannelBidiStreamSource` (implements `BidiStreamSource`), `into_sub_streams()` typed accessor, recursive composition |
| [crates/channels/channels-adapter.md](crates/channels/channels-adapter.md) | draft | `ChannelsAdapter`, `ChannelManager`, demux/mux contracts (REQ-CH-01..04), two-pump pattern (ADR-078) |
| [crates/channels/channel-operations.md](crates/channels/channel-operations.md) | draft | `channel/open`/`close`/`control`/`resources/subscribe`, ACL flow, `direction` semantics, hub relay contract (ADR-079) |
| [crates/channels/channel-client.md](crates/channels/channel-client.md) | draft | `ChannelClient` — client side of a channels connection, transport-agnostic `from_connection` primary + `connect_quic` convenience, bidirectionality preserved |
| [crates/channels/channel-client.md](crates/channels/channel-client.md) | draft | `ChannelClient` — client side of a channels connection, transport-agnostic `from_connection` primary; `connect_quic` removed per ADR-089 §5 (dial extracted to `AlknetClient`); bidirectionality preserved |
## ADR Table
@@ -338,13 +339,13 @@ adapter location map is now consistent: all HTTP-backed adapters
| [080](decisions/080-channelclient.md) | ChannelClient — the Client Side of a Channels Connection | Accepted |
| [081](decisions/081-channels-subcrate-decomposition.md) | channels Sub-Crate Decomposition | Accepted |
| [082](decisions/082-alknet-tls-extraction.md) | alknet-tls Crate Extraction | Accepted (amended — endpoint signature superseded by ADR-083) |
| [083](decisions/083-endpoint-as-accept-loop-runner.md) | Endpoint as Multi-Transport Accept-Loop Runner with Public Dispatch | Accepted (revised — TCP+TLS is an owned transport, not external; amended 2026-07-15 — endpoint extracted from `alknet-core` into `alknet-endpoint`) |
| [083](decisions/083-endpoint-as-accept-loop-runner.md) | Endpoint as Multi-Transport Accept-Loop Runner with Public Dispatch | Accepted (revised — TCP+TLS is an owned transport, not external; amended 2026-07-15 — endpoint extracted from `alknet-core` into `alknet-endpoint`; `EndpointError` removed — both variants vestigial, `shutdown()` infallible) |
| [084](decisions/084-aws-lc-rs-crypto-provider.md) | aws-lc-rs as the TLS Crypto Provider | Accepted |
| [085](decisions/085-workspace-scope-core-vs-consumer-repos.md) | Workspace Scope — Core vs. Consumer Repos | 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 |
| [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) |
| [087](decisions/087-tlsclientconfig-not-blocked-on-dial.md) | `TlsClientConfig` Not Blocked on Dial Seam | Accepted (§5 amended by ADR-089 — `FingerprintPinVerifier` moves to `alknet-tls`; `alknet-call` sheds TLS deps) |
| [088](decisions/088-tlserror-shape.md) | `TlsError` Shape — Single Enum, Owned by `alknet-tls` | Accepted (§5 added — `webpki-roots` fallback when platform store is empty; §7 references ADR-089 for handshake-error surfacing) |
| [089](decisions/089-alknetclient-native-dial-seam.md) | AlknetClient — Native Client Dial Seam | Accepted (resolves OQ-55; `CallClient::connect` / `ChannelClient::connect_quic` removed; `CallCredentials`/`RemoteIdentity` moved to `alknet-core`; `FingerprintPinVerifier` moved to `alknet-tls`; `ClientError` removed; `alknet-call` sheds TLS deps) |
| [090](decisions/090-client-dial-socks5-proxy-seam.md) | Client-Dial SOCKS5 Proxy Seam | Accepted (§5 amended 2026-07-16 — OQ-67 resolved: iroh force-relay-only + HTTP-to-SOCKS5 bridge) |
## Open Questions
+2 -2
View File
@@ -14,7 +14,7 @@ Structured RPC: operations, request/response, streaming subscriptions, and servi
|----------|--------|-------------|
| [call-protocol.md](call-protocol.md) | draft | CallAdapter, hand-rolled EventEnvelope framing (no irpc — ADR-064), stream model, PendingRequestMap, bidirectional calls |
| [operation-registry.md](operation-registry.md) | draft | OperationSpec, Handler, OperationRegistry, AccessControl, service discovery, hand-rolled framing (no irpc — ADR-064) |
| [client-and-adapters.md](client-and-adapters.md) | draft | CallClient (transport-agnostic `spawn_dispatch` primary, `connect` QUIC convenience — ADR-017 Am. 2026-07-13), from_call, OperationAdapter trait, adapter location map, no-env-vars invariant, exchange-of-operations pattern (from_jsonschema moved to alknet-http per ADR-066) |
| [client-and-adapters.md](client-and-adapters.md) | draft | CallClient (transport-agnostic `spawn_dispatch` primary; `connect` removed per ADR-089 §5 — dial extracted to `AlknetClient`), from_call, OperationAdapter trait, adapter location map, no-env-vars invariant, exchange-of-operations pattern (from_jsonschema moved to alknet-http per ADR-066) |
## Applicable ADRs
@@ -81,7 +81,7 @@ Structured RPC: operations, request/response, streaming subscriptions, and servi
8. **Abort cascades to descendants**: `call.aborted` for a parent request cascades to all non-terminal descendants. Default `abort-dependents`; `continue-running` opt-in. See ADR-016.
9. **Internal calls switch authority context, not skip ACL**: The `internal` flag marks composition-originated calls. ACL runs against the handler's composition authority, not the caller's and not as a blanket skip. Operations have External/Internal visibility. Scoped composition env bounds reachability. See ADR-015, ADR-022.
10. **Provenance determines composition capability**: Only `Local` and `Session` ops can compose. Leaves (`FromOpenAPI`, `FromMCP`, `FromCall`, `FromJsonSchema`) are forwarding stubs — they don't get composition authority or a scoped env. The assembly layer is the sole grantor of composition authority. See ADR-022. (`FromJsonSchema` is now a real HTTP-forwarding leaf per ADR-066, not a schema-only placeholder.)
11. **Connection direction is independent of call direction**: Who opens the connection is a connection-layer concern, not a protocol-layer concern. Both sides can call each other once connected. The `CallAdapter` accepts connections; the `CallClient` takes them over (`spawn_dispatch` primary, `connect` QUIC convenience — ADR-017 Am. 2026-07-13); both produce the same `CallConnection` and dispatch through the same loop. See ADR-017, [client-and-adapters.md](client-and-adapters.md).
11. **Connection direction is independent of call direction**: Who opens the connection is a connection-layer concern, not a protocol-layer concern. Both sides can call each other once connected. The `CallAdapter` accepts connections; the `CallClient` takes them over (`spawn_dispatch` primary; `connect` removed per ADR-089 §5 — dial extracted to `AlknetClient`); both produce the same `CallConnection` and dispatch through the same loop. See ADR-017, [client-and-adapters.md](client-and-adapters.md).
12. **Peer authorization via `AccessControl`**: A remote peer's call is authorized by `AccessControl::check(peer_identity)` against the op's `AccessControl` — the same mechanism that gates every other call. No `remote_safe` flag, no `trusted_peer` bypass. An op with `AccessControl::default()` is callable by any peer; an op with `required_scopes` is callable only by peers whose `Identity.scopes` satisfy them; an op with `Visibility::Internal` is never callable from the wire. See ADR-029.
13. **Adapter trait lives with the types; implementations live with their transport**: `OperationAdapter` is in `alknet-call`; `from_call` is in `alknet-call` (QUIC); `from_jsonschema`/`from_openapi`/`from_mcp`/`to_openapi`/`to_mcp` are in `alknet-http` (reqwest / axum). `alknet-call` stays lean — no HTTP client, no HTTP server. (`from_jsonschema` was originally in `alknet-call` as a schema-only placeholder; ADR-066 moved it to `alknet-http` as a real HTTP-backed adapter.) See [client-and-adapters.md](client-and-adapters.md).
14. **No handler reads outbound credentials from any source other than `OperationContext.capabilities`** (no-env-vars invariant): the credential injection path is vault → assembly layer → `Capabilities` → `HandlerRegistration.capabilities` → `OperationContext.capabilities` → handler. Downstream consumers' `std::env::var` reads are unreachable because the assembly layer never calls `Default::default()`. See ADR-014, [client-and-adapters.md](client-and-adapters.md).
@@ -114,7 +114,7 @@ operations discovered when the connection was established.
/// opened). Holds the connection's Layer 2 overlay (imported ops).
pub struct CallConnection {
/// The underlying transport Connection (from endpoint.accept,
/// CallClient::spawn_dispatch, or CallClient::connect). May be QUIC,
/// CallClient::spawn_dispatch, or AlknetClient::dial_*). May be QUIC,
/// TCP+TLS, WebTransport, SSH, or any Connection::from_stream source
/// (ADR-065).
connection: Connection,
@@ -176,10 +176,12 @@ The adapter:
6. Manages the `PendingRequestMap` for outgoing calls
The dispatch loop is **shared** with `CallClient` (ADR-017 §1): both
`CallAdapter::handle` (accept path) and `CallClient::connect` (connect path)
construct a `Dispatcher` (`protocol/dispatch.rs`) and call `run_loop` — the
dispatch half is one implementation, the connection-establishment half differs
(accept vs dial). Peer authorization flows through the existing
`CallAdapter::handle` (accept path) and `CallClient::spawn_dispatch`
(connect path — the dial is now `AlknetClient::dial_*` per ADR-089 §5;
`CallClient::connect` is removed) construct a `Dispatcher`
(`protocol/dispatch.rs`) and call `run_loop` — the dispatch half is one
implementation, the connection-establishment half differs (accept vs
dial). Peer authorization flows through the existing
`AccessControl::check(peer_identity)` — no `RemoteFilter`/`remote_safe` gate
(ADR-029 §3). The composition env is peer-keyed (`PeerCompositeEnv`,
ADR-029 §1) to handle head→N-workers routing. See
@@ -22,11 +22,11 @@ This document specifies three components, all in `alknet-call`:
1. **`CallClient`** — takes over an established transport `Connection`
on ALPN `alknet/call`, spawns the shared dispatch loop, and produces
a `CallConnection`. Transport-agnostic (`spawn_dispatch` primary,
`connect` QUIC convenience); the dispatch loop is shared with the
server-side `CallAdapter` (ADR-017 §1); `CallClient` is the
connection-establishment + credential-handling half, not a parallel
protocol implementation.
a `CallConnection`. Transport-agnostic (`spawn_dispatch` primary;
`connect` removed per ADR-089 §5 — dial extracted to `AlknetClient`);
the dispatch loop is shared with the server-side `CallAdapter`
(ADR-017 §1); `CallClient` is the connection-take-over half, not a
parallel protocol implementation.
2. **`from_call`** — discovers operations on a remote call-protocol endpoint
via `services/list` + `services/schema` (already implemented in
`registry/discovery.rs`) and registers them in the connection's Layer 2
@@ -124,15 +124,13 @@ impl CallClient {
/// a transport.
pub fn spawn_dispatch(&self, connection: Connection) -> CallConnection;
/// QUIC convenience constructor. Dials a QUIC connection to `addr`
/// on ALPN `alknet/call` (using `credentials` for the TLS handshake
/// — ADR-034 verifier selection), then calls `spawn_dispatch`.
/// Feature-gated on `quinn` (the dial is QUIC-specific). Additive
/// and two-way-door — `connect_tcp_tls`, `connect_webtransport`,
/// etc. join it as transports are added, without touching the
/// `spawn_dispatch` contract. This convenience is a thin wrapper
/// over `AlknetClient::dial_quic` (ADR-089) — a caller that needs
/// transport selection uses `AlknetClient` directly.
/// **REMOVED per ADR-089 §5.** The dial is extracted into
/// `AlknetClient` (`alknet-client`); `connect` is deleted, not
/// delegated, to avoid `alknet-call` depending on `alknet-client`
/// and to let `alknet-call` shed its TLS/transport deps entirely.
/// Callers compose `AlknetClient::dial_quic(...).await?` +
/// `CallClient::new(...).spawn_dispatch(conn)`. `ClientError` is
/// removed (it was produced only by `connect`).
#[cfg(feature = "quinn")]
pub async fn connect(
&self,
@@ -192,19 +190,23 @@ authorization machinery that gates every other call. No `RemoteFilter`, no
primary constructor — it takes a pre-established `Connection`,
constructs a `CallConnection`, builds a `Dispatcher`, spawns the
dispatch task, and returns the live `CallConnection`. `connect()` is
the QUIC convenience over it: dial QUIC (feature-gated on `quinn`),
then `spawn_dispatch`. Tests use `spawn_dispatch` directly to wire
mock/loopback connections. A future `connect_tcp_tls` /
`connect_webtransport` would dial their transport and call
`spawn_dispatch` the same way. The one-way-door surface is
`spawn_dispatch`; the dial helpers are two-way-door conveniences.
**removed** per ADR-089 §5: the dial is extracted into `AlknetClient`
(`alknet-client`), and keeping a QUIC convenience constructor on
`CallClient` would make `alknet-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`
+ `spawn_dispatch` — two lines, the dial then the take-over. Tests use
`spawn_dispatch` directly to wire mock/loopback connections. The
one-way-door surface is `spawn_dispatch`; the dial lives in
`alknet-client`.
This mirrors `ChannelClient::from_connection` / `connect_quic`
(ADR-080) and is the client-side analogue of the server-side
generalization ADR-065 made. The call protocol, like the channels
protocol, is transport-agnostic — `Connection::from_stream` /
`from_bidi` (ADR-065) accept any `AsyncRead + AsyncWrite`, and
`spawn_dispatch` takes the resulting `Connection` unchanged.
This mirrors `ChannelClient::from_connection` (ADR-080; its
`connect_quic` is likewise removed per ADR-089 §5) and is the
client-side analogue of the server-side generalization ADR-065 made.
The call protocol, like the channels protocol, is transport-agnostic —
`Connection::from_stream` / `from_bidi` (ADR-065) accept any
`AsyncRead + AsyncWrite`, and `spawn_dispatch` takes the resulting
`Connection` unchanged.
#### Peer-keyed composition env (ADR-029)
@@ -244,8 +246,10 @@ attribution, filtered by the calling peer's authorization). See
### Credential sources for connections
`CallClient::connect()` takes a `CallCredentials` bundle. Credentials come
from `Capabilities` (ADR-014), never from environment variables. The three
`CallCredentials` (now in `alknet-core`, moved from `alknet-call` per
ADR-089 §5 so the dial does not depend on the call protocol) carries
the three credential dimensions (ADR-017 §7). Credentials come from
`Capabilities` (ADR-014), never from environment variables. The three
credential dimensions (ADR-017 §7):
```rust
+3 -3
View File
@@ -24,7 +24,7 @@ protocol work itself.
| [channels-connection.md](channels-connection.md) | draft | `ChannelBidiStreamSource` (implements `BidiStreamSource` — ADR-070/074), `into_sub_streams()` typed destructure, recursive composition |
| [channels-adapter.md](channels-adapter.md) | draft | `ChannelsAdapter` (`ProtocolHandler` on `alknet/channels`), `ChannelManager`, demux/mux contracts (REQ-CH-01..04), the two-pump pattern (ADR-078) |
| [channel-operations.md](channel-operations.md) | draft | `channel/open`, `channel/close`, `channel/control`, `channel/resources/subscribe` — call-protocol operations on channel 0, ACL flow, `direction` semantics, the hub relay contract (ADR-079) |
| [channel-client.md](channel-client.md) | draft | `ChannelClient` — the client side of a channels connection; transport-agnostic `from_connection` primary, `connect_quic` convenience; bidirectionality preserved |
| [channel-client.md](channel-client.md) | draft | `ChannelClient` — the client side of a channels connection; transport-agnostic `from_connection` primary; `connect_quic` removed per ADR-089 §5 (dial extracted to `AlknetClient`); bidirectionality preserved |
## Applicable ADRs
@@ -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 |
| [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 |
| [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) |
| [080](../../decisions/080-channelclient.md) | ChannelClient — the Client Side of a Channels Connection | `ChannelClient`, transport-agnostic `from_connection` primary; `connect_quic` removed per ADR-089 §5 (dial extracted to `AlknetClient`); `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 |
| [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 |
@@ -52,7 +52,7 @@ protocol work itself.
| OQ | Title | Status | Relevance |
|----|-------|--------|-----------|
| 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-55 | AlknetClient / Client Establishment Extraction | resolved (ADR-089) | `ChannelClient`'s API is decided (ADR-080): transport-agnostic `from_connection` primary; `connect_quic` removed (ADR-089 §5). `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-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) |
@@ -55,6 +55,12 @@ impl ChannelClient {
/// it is additive over `from_connection` and is a two-way door —
/// `connect_tcp_tls`, `connect_webtransport`, etc. can be added
/// alongside it without touching the one-way-door surface.
///
/// **REMOVED per ADR-089 §5.** The dial is extracted into
/// `AlknetClient` (`alknet-client`); `connect_quic` is deleted,
/// not delegated, to avoid `alknet-channels-call` depending on
/// `alknet-client`. Callers compose `AlknetClient::dial_quic` +
/// `from_connection`. See "Relationship to `AlknetClient`" below.
pub async fn connect_quic(
addr: SocketAddr,
credentials: CallCredentials,
@@ -130,19 +136,19 @@ a WebTransport `BiStream`, an SSH `direct-tcpip` channel wrapped via
ADR-044) — all produce a `Connection` that `from_connection` accepts
unchanged. This mirrors the server side's `ChannelsAdapter::handle(Connection)`, which is substrate-agnostic by the same mechanism.
`connect_quic(addr, credentials)` is a **convenience** constructor — dial
QUIC, then `from_connection`. It is additive and two-way-door: transport-specific dial helpers (`connect_tcp_tls`, `connect_webtransport`, …)
join it as transports are added, none of which touch the `from_connection`
contract. The dial helper set is open-ended by design.
`connect_quic(addr, credentials)` was a **convenience** constructor —
dial QUIC, then `from_connection`. It is **removed** per ADR-089 §5:
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?` — two lines, the dial
then the take-over.
The credential/verifier-selection rule (ADR-034) lives in the transport's
own dial path, not in `from_connection` — `from_connection` receives an
already-established, already-authenticated `Connection`, exactly as
`ChannelsAdapter::handle` does on the server side. The ~20 lines of
verifier-selection boilerplate each dial helper rebuilds is the known
duplicated cost of not having `AlknetClient` (OQ-55) extracted yet;
`from_connection` keeps that boilerplate on the *transport-specific dial*
side, not on the channels protocol's one-way-door surface.
The credential/verifier-selection rule (ADR-034) lives in the dial
(`AlknetClient`), not in `from_connection` — `from_connection` receives
an already-established, already-authenticated `Connection`, exactly as
`ChannelsAdapter::handle` does on the server side.
## Bidirectionality preserved
@@ -170,8 +176,8 @@ provides `AlknetClient` with three dial methods (`dial_quic` /
TCP+TLS, iroh); the take-over (`from_connection`) is
transport-agnostic. The two concerns are separated.
`connect_quic` becomes a thin wrapper over `AlknetClient::dial_quic` —
dial QUIC, then `from_connection`. A caller that needs transport
`connect_quic` is removed (see above) — `AlknetClient::dial_quic` is the
dial that feeds `from_connection`. A caller that needs transport
selection (QUIC with TCP+TLS fallback) uses `AlknetClient` directly;
the fallback policy is a caller concern. See
[ADR-089](../../decisions/089-alknetclient-native-dial-seam.md) for the
@@ -184,7 +190,7 @@ All design decisions are documented as ADRs in [decisions/](../../decisions/).
| ADR | Decision | Summary |
|-----|----------|---------|
| [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) |
| [080](../../decisions/080-channelclient.md) | ChannelClient | Client side; transport-agnostic `from_connection` primary; `connect_quic` convenience **removed** per ADR-089 §5 (dial extracted to `AlknetClient`); `AlknetClient` dial-seam extracted (ADR-089, resolves OQ-55) |
## Open Questions
@@ -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 |
| [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 |
| [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) |
| [080](../../decisions/080-channelclient.md) | ChannelClient | Client side; transport-agnostic `from_connection` primary; `connect_quic` removed per ADR-089 §5 (dial extracted to `AlknetClient`); `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 |
## Open Questions
+66 -38
View File
@@ -26,7 +26,10 @@ 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.
client growing its own per-transport dial helper. Those convenience
constructors are removed (see "Relationship to `CallClient` /
`ChannelClient`" below); `AlknetClient` is the single dial home, and
the protocol crates shed their TLS/transport deps entirely.
`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
@@ -325,15 +328,25 @@ the alknet type level — see ADR-090 §"Two distinct SOCKS5 uses".
### 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.
`AlknetClient`'s dials take a `CallCredentials` bundle — the shared
type from `alknet-core` (moved out of `alknet-call` so the dial does
not depend on the call protocol; see ADR-089 §5). It carries the local
`TlsIdentity`, the optional call-protocol-level `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 `auth_token` is stripped at the TLS boundary.** `TlsClientConfig`
and `ClientVerifierContext` are TLS-level types and do not carry the
call-protocol auth token. The dial extracts the TLS-relevant fields
from `CallCredentials` — `tls_identity` → the client-cert
`local_identity`, `remote_identity` → the fingerprint-pin input to
`ClientVerifierContext` — and builds a `TlsClientConfig` from those
alone. The `auth_token` travels with the `Connection` into the
protocol take-over (`spawn_dispatch` / `from_connection`), where it is
sent as the first call-protocol frame when the protocol requires it.
This keeps `alknet-tls` free of any call-protocol coupling.
### The dialable ALPNs
@@ -387,13 +400,18 @@ let conn = client.dial_tcp_tls("hub.example", addr, b"alknet/call", &creds).awai
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
The per-protocol QUIC convenience constructors that previously lived on
`CallClient` / `ChannelClient` (`connect` / `connect_quic`) are
**removed**. They welded the dial into the protocol crate — every
`CallClient` user transitively pulled `quinn` + `rustls` + the TLS
verifier machinery, and the convenience constructor's existence made
`alknet-call` / `alknet-channels-call` depend on `alknet-client` (or
duplicate the dial), contradicting the dep graph below. The dial is a
distinct concern from the protocol take-over; `AlknetClient` is the
single home for it. 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?`). See
[ADR-089](../../decisions/089-alknetclient-native-dial-seam.md) §5.
### Iroh — shares the key, not the config (client side too)
@@ -509,9 +527,9 @@ implementation detail.
```toml
[features]
default = []
quinn = ["dep:quinn", "alknet-tls/quinn"]
quinn = ["dep:quinn", "alknet-tls/quinn", "alknet-core/quinn"]
tcp = ["dep:tokio-rustls", "alknet-tls/tcp"]
iroh = ["dep:iroh"]
iroh = ["dep:iroh", "alknet-core/iroh"]
socks5 = ["dep:fast-socks5"] # enables the proxied dial paths (ADR-090)
```
@@ -520,21 +538,27 @@ 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. The `socks5` feature (ADR-090) is independent
of the transport features — it enables the proxy code path that
`dial_quic` (UDP ASSOCIATE) and `dial_tcp_tls` (CONNECT) use when a
proxy is configured. Enabling `socks5` without `quinn` or `tcp` is a
no-op; enabling `quinn` + `socks5` enables proxied QUIC; `tcp` +
`socks5` enables proxied TCP+TLS. The `fast-socks5` dep is behind
`socks5`, so deployments that don't use a proxy don't pay the dep.
`for_tcp_tls`). The `quinn` and `iroh` features also pull the
corresponding features on `alknet-core` — `dial_quic` produces a
`Connection` via `Connection::from_quinn_with_alpn` and `dial_iroh`
via `Connection::from_iroh`, both of which live in `alknet-core`'s
`types.rs` behind core's `quinn` / `iroh` features (the "quinn feature
split" from ADR-083 §"The `quinn` feature split"). The `iroh` feature
does not pull `alknet-tls` features — iroh has its own TLS. The
`socks5` feature (ADR-090) is independent of the transport features —
it enables the proxy code path that `dial_quic` (UDP ASSOCIATE) and
`dial_tcp_tls` (CONNECT) use when a proxy is configured. Enabling
`socks5` without `quinn` or `tcp` is a no-op; enabling `quinn` +
`socks5` enables proxied QUIC; `tcp` + `socks5` enables proxied
TCP+TLS. The `fast-socks5` dep is behind `socks5`, so deployments that
don't use a proxy don't pay the dep.
### Dependencies
```
alknet-client
├── alknet-core (Connection, CallCredentials/RemoteIdentity if
│ moved here, Ed25519SecretKey, types)
├── alknet-core (Connection, CallCredentials, RemoteIdentity,
│ Ed25519SecretKey, types)
├── alknet-tls (TlsClientConfig — for quinn + tcp dials)
├── quinn (optional — dial_quic)
├── tokio-rustls (optional — dial_tcp_tls)
@@ -545,14 +569,16 @@ alknet-client
```
`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.
`alknet-core` (for `Connection`, `CallCredentials`, `RemoteIdentity`,
and types). It does **not** depend on `alknet-call` or
`alknet-channels-call` — the dial is below the protocol.
`CallCredentials` and `RemoteIdentity` live in `alknet-core` (moved
from `alknet-call` per ADR-089 §5 — both the call and channels clients
need them, and the dial must not depend on the call protocol; see
ADR-089 §5). `FingerprintPinVerifier` lives in `alknet-tls` (moved from
`alknet-call` per ADR-087 §5 — it is a TLS concern, and
`TlsClientConfig::new` constructs it; moving it lets `alknet-call` shed
its direct `rustls` dep entirely).
## Crate dependencies (in the dep graph)
@@ -580,7 +606,9 @@ alknet-worker (uses AlknetClient to dial a hub)
`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.
parallel, not downstream of the dial. `CallCredentials` and
`RemoteIdentity` live in `alknet-core` (not `alknet-call`), so the dial
does not depend on the call protocol for the credential type.
## Assembly layer integration
+11 -7
View File
@@ -8,13 +8,17 @@ last_updated: 2026-07-15
Shared types, auth, config, and identity for ALPN-based protocol
dispatch. Every handler crate depends on `alknet-core` for
`ProtocolHandler`, `Connection`, `AuthContext`, `IdentityProvider`, and
config types. The endpoint (`AlknetEndpoint`, `HandlerRegistry`,
`EndpointError`) has been extracted to
[`alknet-endpoint`](../endpoint/README.md) (ADR-083 Amendment
2026-07-15); core no longer carries the accept-loop runner or its
transport deps (quinn, iroh, rcgen, rustls-acme). `Connection::from_quinn`
/ `from_iroh` stay in core's `types.rs` as shared constructors (gated
on core's `quinn` / `iroh` features).
config types. The endpoint (`AlknetEndpoint`, `HandlerRegistry`) has
been extracted to [`alknet-endpoint`](../endpoint/README.md) (ADR-083
Amendment 2026-07-15; `EndpointError` is removed — both variants were
vestigial); core no longer carries the accept-loop runner or its
transport deps (quinn, iroh, rcgen, rustls-acme).
`Connection::from_quinn` / `from_iroh` stay in core's `types.rs` as
shared constructors (gated on core's `quinn` / `iroh` features).
`CallCredentials` and `RemoteIdentity` move to `alknet-core` (from
`alknet-call`, per ADR-089 §5) — both the call and channels clients
need them, and the dial (`alknet-client`) must not depend on the call
protocol for the credential type.
## Documents
+2 -2
View File
@@ -67,8 +67,8 @@ impl Connection {
pub fn from_quinn(conn: quinn::Connection) -> Self;
/// Construct from a quinn connection with an explicit ALPN (feature-gated
/// on quinn). Used by the client path (`CallClient::connect`) and the
/// endpoint's quinn accept loop.
/// on quinn). Used by the client path (`AlknetClient::dial_quic` per
/// ADR-089) and the endpoint's quinn accept loop.
#[cfg(feature = "quinn")]
pub fn from_quinn_with_alpn(conn: quinn::Connection, alpn: Vec<u8>) -> Self;
+5 -4
View File
@@ -5,10 +5,11 @@ last_updated: 2026-07-15
# Endpoint (moved to `alknet-endpoint`)
> **This document is deprecated.** The `AlknetEndpoint`,
> `HandlerRegistry`, and `EndpointError` types have been extracted from
> `alknet-core` into a new crate `alknet-endpoint` (ADR-083 Amendment
> 2026-07-15). The canonical spec is now
> **This document is deprecated.** The `AlknetEndpoint` and
> `HandlerRegistry` types have been extracted from `alknet-core` into a
> new crate `alknet-endpoint` (ADR-083 Amendment 2026-07-15).
> `EndpointError` is removed (both variants were vestigial). The
> canonical spec is now
> [`crates/endpoint/README.md`](../endpoint/README.md).
>
> The shared types the endpoint imports (`ProtocolHandler`,
+35 -23
View File
@@ -27,9 +27,10 @@ assembly layer does both (transports from `alknet-tls`'s
endpoint is a leaf consumer of core's shared types (it imports `auth`,
`config`, `types`; nothing in core imports from it), depended on by a
different audience (the assembly layer) than the shared types (every
handler crate). No handler crate imports `AlknetEndpoint`,
`HandlerRegistry`, or `EndpointError` — they depend on `alknet-core`
for `ProtocolHandler`, `Connection`, `AuthContext`, and types only.
handler crate). No handler crate imports `AlknetEndpoint` or
`HandlerRegistry` — they depend on `alknet-core` for
`ProtocolHandler`, `Connection`, `AuthContext`, and types only.
(`EndpointError` is removed — see below.)
## Why
@@ -89,7 +90,16 @@ impl AlknetEndpoint {
);
pub async fn run(self: Arc<Self>);
pub async fn shutdown(&self) -> Result<(), EndpointError>;
/// Signal all owned accept loops to stop and drain in-flight handlers
/// for `drain_timeout`. Infallible — the accept loops are owned by
/// the endpoint (quinn, iroh, TCP+TLS), so there is no external
/// coordination and no bind failure path (the assembly layer binds
/// before handing pre-built transports to the endpoint via
/// `with_quinn` / `with_iroh` / `with_tcp_tls`). No-handler matches
/// are swallowed by `dispatch` (ADR-083: close + log, not an error).
/// One owner, one shutdown — no external loop coordination needed.
pub async fn shutdown(&self);
}
```
@@ -131,25 +141,25 @@ Registration is static at startup (ADR-010, OQ-04). The assembly layer
builds a `HandlerRegistry`, inserts all handlers, and passes it to
`AlknetEndpoint::new()`.
### `EndpointError`
### `EndpointError` — removed
The error type for `AlknetEndpoint::shutdown` and listener bind
failures. Lives in `alknet-endpoint` (moves with the endpoint from
core).
The endpoint previously had an `EndpointError { BindFailed(io::Error),
HandlerNotFound(Vec<u8>) }` enum. Both variants are vestigial after
ADR-083:
```rust
pub enum EndpointError {
BindFailed(io::Error),
HandlerNotFound(Vec<u8>), // ALPN string with no registered handler
}
```
- `BindFailed` — the endpoint takes pre-built, pre-bound transports
(the assembly layer does the binding); the endpoint performs no bind,
so it cannot produce a bind error.
- `HandlerNotFound` — `dispatch` swallows no-handler matches (close +
log per ADR-083), so this variant is never returned.
After ADR-083, the endpoint takes no TLS config and constructs no
transports — TLS config errors surface as `TlsError` in `alknet-tls` /
the assembly layer, not as `EndpointError`. The `TlsConfig(io::Error)`
variant that existed when the endpoint built TLS internally is removed.
`BindFailed` covers listener bind failures (quinn, iroh, TCP+TLS
`TcpListener::bind`).
The enum is removed. `shutdown()` is infallible (`async fn shutdown(&self)`,
no `Result`). If a future requirement adds a real failure path to
shutdown or dispatch, a fresh error type is cleaner than retrofitting
this one. The `EndpointError` type, its `TlsConfig` variant (already
removed by ADR-083), and the `BindFailed`/`HandlerNotFound` variants all
move out of the codebase with the endpoint extraction — none survives
into `alknet-endpoint`.
### `TcpTlsListener`
@@ -254,9 +264,11 @@ alknet-endpoint
`alknet-endpoint` depends on `alknet-core` (for `Connection`,
`ProtocolHandler`, `AuthContext`, `IdentityProvider`, `DynamicConfig`).
`HandlerRegistry` and `EndpointError` live in `alknet-endpoint` (they
move with the endpoint from core). The endpoint does **not** depend on
`alknet-tls` — it takes pre-built transports, so TLS config
`HandlerRegistry` lives in `alknet-endpoint` (it moves with the
endpoint from core). `EndpointError` is removed (both variants were
vestigial — see "`EndpointError` — removed" above). The endpoint does
**not** depend on `alknet-tls` — it takes pre-built transports, so TLS
config
construction stays at the assembly layer.
### Crate dependencies (in the dep graph)
+13 -8
View File
@@ -287,15 +287,18 @@ another hub (A) is a client from A's perspective. The dial needs a
client-side TLS config (`TlsClientConfig`, ADR-087) for the outbound
connection's `rustls::ClientConfig` (verifier selection per ADR-034:
fingerprint pin for the worker's known key). The dial path mirrors the
`from_connection` / `connect_quic` split (ADR-080):
`from_connection` primary (ADR-080; `ChannelClient::connect_quic` is
removed per ADR-089 §5 — the dial lives in `AlknetClient`):
```rust
impl Hub {
/// Take over a pre-established channels `Connection` as a worker
/// connection. Transport-agnostic — the caller (or a transport
/// helper) produces the `Connection`. This is the primary path;
/// `connect_quic_worker` and future `connect_tcp_tls_worker` are
/// conveniences over it.
/// `connect_quic_worker` (a hub-level convenience, distinct from
/// the removed `ChannelClient::connect_quic` per-protocol
/// constructor — ADR-089 §5) and future `connect_tcp_tls_worker`
/// are conveniences over it.
pub async fn dial_worker_connection(
&self,
connection: Connection,
@@ -610,8 +613,8 @@ pub enum HubError {
NoPeerIdentity,
#[error("from_call discovery failed: {0}")]
Discovery(#[from] AdapterError),
#[error("call client error: {0}")]
Client(#[from] ClientError),
#[error("dial error: {0}")]
Dial(#[from] alknet_client::ClientDialError),
#[error("channel error (client or adapter path): {0}")]
Channel(#[from] ChannelError),
#[error("registration failed: {0}")]
@@ -654,7 +657,8 @@ alknet-hub (Hub struct deps)
├── alknet-channels-call (ChannelClient, ChannelsAdapter, ChannelManager,
│ ChannelBidiStreamSource)
├── alknet-call (CallAdapter, Dispatcher, PeerCompositeEnv,
│ from_call, FromCallConfig, AdapterError, ClientError)
│ from_call, FromCallConfig, AdapterError)
├── alknet-client (AlknetClient, ClientDialError — for outbound worker dials)
├── alknet-http (HttpAdapter — for the registration endpoint and browser access)
├── alknet-core (IdentityProvider, Connection, OperationRegistry,
│ AuthContext)
@@ -787,7 +791,7 @@ into `CallAdapter::with_aggregated_env`.
| Peer-graph routing model | [ADR-029](../../decisions/029-peer-graph-routing-model.md) | Peer-keyed overlays, `PeerRef` routing, `AccessControl`-based peer auth |
| PeerEntry and Identity.id | [ADR-030](../../decisions/030-peerentry-and-identity-id-decoupling.md) | `PeerId` = `Identity.id` = `PeerEntry.peer_id` (stable) |
| Three peer roles | [ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md) | Hub = role-3 `PeerEntry` (mixed fingerprints); browsers not peers; bearer-token identity over TCP/WebTransport |
| ChannelClient — transport-agnostic | [ADR-080](../../decisions/080-channelclient.md) | `from_connection` primary, `connect_quic` convenience; the dial path the hub uses |
| ChannelClient — transport-agnostic | [ADR-080](../../decisions/080-channelclient.md) | `from_connection` primary; `connect_quic` removed per ADR-089 §5 (dial extracted to `AlknetClient`); the dial path the hub uses |
| Channels transport-agnostic | [ADR-071](../../decisions/071-channels-wire-format.md) | Substrate modes; `Connection::from_stream`/`from_bidi` (ADR-065) — the substrate the hub relays |
| TCP+TLS as first-class owned transport | [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md) | `with_tcp_tls(listener, acceptor)` — TCP+TLS is owned by the endpoint, not a sibling loop; supersedes ADR-010 Am. 1 |
| Channel 0 pre-negotiated | [ADR-072](../../decisions/072-channel-0-pre-negotiated-call.md) | Channel 0 = `alknet/call`; the `CallAdapter` runs here |
@@ -832,7 +836,8 @@ See [open-questions.md](../../open-questions.md) for full details.
## References
- [channel-client.md](../channels/channel-client.md) — `ChannelClient`
(`from_connection` / `connect_quic` — the dial path)
(`from_connection` — the take-over; `connect_quic` removed per
ADR-089 §5, dial now via `AlknetClient`)
- [channels-adapter.md](../channels/channels-adapter.md) —
`ChannelsAdapter`, `ChannelManager`, the accept path
- [channel-operations.md](../channels/channel-operations.md) —
+65 -26
View File
@@ -121,7 +121,7 @@ transports the deployment runs.
| `AcceptAnyCertVerifier` | `alknet-core/endpoint.rs` (`#[cfg(feature = "quinn")]`) | `alknet-tls` |
| `SelfSignedCert` / `generate_self_signed_cert()` | `alknet-core/endpoint.rs` (`#[cfg(feature = "quinn")]`) | `alknet-tls` |
| `load_cert_chain()` / `load_private_key()` | `alknet-core/endpoint.rs` | `alknet-tls` (consolidates with the `alknet-call` duplicate — see client table) |
| `fingerprint.rs` | `alknet-core/fingerprint.rs` | **stays in core** (shared by server + client; client is in `alknet-call`; production code uses `sha2` + manual DER only — `rustls` is test-only. See OQ-59.) |
| `fingerprint.rs` | `alknet-core/fingerprint.rs` | **stays in core** (shared by server + client; the client-side `FingerprintPinVerifier` is now in `alknet-tls` per ADR-089 §5, so both consumers are co-located; production code uses `sha2` + manual DER only — `rustls` is test-only. See OQ-59 — the original dep-edge concern that motivated keeping `fingerprint.rs` in core is dissolved by ADR-089 §5.) |
### What moves from `alknet-call` to `alknet-tls` (client side)
@@ -137,12 +137,12 @@ extraction is the client-side analogue of the server-side
| `build_client_auth()` | `alknet-call/client/call_client.rs` | `alknet-tls` (client-auth cert resolver construction inside `TlsClientConfig::new`) |
| `select_server_verifier()` | `alknet-call/client/call_client.rs` | `alknet-tls` (ADR-034 verifier selection inside `TlsClientConfig::new`) |
| `load_platform_root_cert_store()` | `alknet-call/client/call_client.rs` | `alknet-tls` (the unknown-X.509-remote CA path inside `TlsClientConfig::new`) |
| `FingerprintPinVerifier` | `alknet-call/client/call_client.rs` | `alknet-tls` or stays in `alknet-call` (implementation detail — ADR-087 §5; `TlsClientConfig::new` constructs it either way) |
| `FingerprintPinVerifier` | `alknet-call/client/call_client.rs` | `alknet-tls` (moved — it is a TLS concern; `TlsClientConfig::new` constructs it; moving it lets `alknet-call` shed its direct `rustls` dep entirely per ADR-089 §5) |
| `Ed25519SigningKey` (client-side copy) | `alknet-call/client/call_client.rs` | `alknet-tls` (consolidates with the `endpoint.rs` duplicate — one copy in `alknet-tls`) |
| `RawKeyClientCertResolver` | `alknet-call/client/call_client.rs` | `alknet-tls` |
| `NoClientCertResolver` | `alknet-call/client/call_client.rs` | `alknet-tls` |
| `load_cert_chain()` / `load_private_key()` (client-side copies) | `alknet-call/client/call_client.rs` | `alknet-tls` (consolidates with the `endpoint.rs` duplicate — one copy in `alknet-tls`) |
| `CallClient::connect` | `alknet-call/client/call_client.rs` | **stays in `alknet-call`** (the dial; calls `TlsClientConfig::new` + `for_quinn()` instead of `build_quinn_client_config`) |
| `CallClient::connect` | `alknet-call/client/call_client.rs` | **removed** (ADR-089 §5 — the dial is extracted to `AlknetClient`; `CallClient` keeps only `spawn_dispatch`, shedding its TLS/transport deps) |
**Consolidation note.** `Ed25519SigningKey` and
`load_cert_chain`/`load_private_key` are currently **duplicated** across
@@ -156,11 +156,14 @@ updated to import from `alknet-tls`.
types — `StaticConfig` holds a `TlsIdentity`, and config types belong in
core. `alknet-tls` re-exports them for convenience. `fingerprint.rs` stays
in core because it's shared by both the server path (endpoint extracts
fingerprint from the client cert) and the client path (`FingerprintPinVerifier`
in `alknet-call` matches the server's cert against a pinned fingerprint).
fingerprint from the client cert) and the client path
(`FingerprintPinVerifier` — now in `alknet-tls` per ADR-089 §5 —
matches the server's cert against a pinned fingerprint).
The production code in `fingerprint.rs` uses only `sha2` and manual DER
parsing — the `rustls::sign` usage is in the test helper only. See OQ-59
for the full trade-off.
(the original dep-edge concern that motivated keeping `fingerprint.rs`
in core is dissolved by ADR-089 §5 — `FingerprintPinVerifier` moved to
`alknet-tls`, so its consumers are co-located).
### `TlsServerConfig`
@@ -319,10 +322,16 @@ present (it's the core TLS library).
```
alknet-tls
├── alknet-core (TlsIdentity, Ed25519SecretKey, fingerprint — re-exported)
├── rustls (ServerConfig, cert types — always present)
├── rustls (ServerConfig, ClientConfig, cert types — always present)
├── rustls-pki-types (CertificateDer, PrivateKeyDer, etc. — via rustls re-export
│ or direct dep; core lists it directly)
├── rustls-pemfile (cert/key file loading — always present)
├── rustls-native-certs (platform root cert store — always present; the
│ unknown-X.509-remote CA path in `TlsClientConfig::new`)
├── webpki-roots (built-in CA roots fallback — always present; merged
│ into the root store when the platform store is empty,
│ so a containerized deployment with no system CA bundle
│ can still verify public X.509 remotes — see ADR-088 §5)
├── rcgen (self-signed cert generation — always present)
├── ed25519-dalek (Ed25519 signing key — always present, via core)
├── sha2 (fingerprint computation — always present, via core)
@@ -334,6 +343,14 @@ alknet-tls
└── rustls-acme (optional — ACME state machine)
```
`rustls-native-certs` and `webpki-roots` are always-present deps (not
feature-gated) because the unknown-X.509-remote CA-verification path in
`TlsClientConfig::new` is needed by any client dialing a public X.509
endpoint, regardless of transport (QUIC or TCP+TLS). In the
pre-extraction code these lived in `alknet-call` behind the `quinn`
feature; the extraction (ADR-089 §5) moves them to `alknet-tls` ungated,
and `alknet-call` sheds the deps entirely.
`alknet-core` loses `rustls-pemfile`, `rcgen`, and `rustls-acme` from
its dependencies — the cert-loading, self-signed generation, and ACME
machinery move to `alknet-tls`. Core's `acme` feature
@@ -431,7 +448,7 @@ impl AlknetEndpoint {
);
pub async fn run(self: Arc<Self>);
pub async fn shutdown(&self) -> Result<(), EndpointError>;
pub async fn shutdown(&self);
}
```
@@ -537,11 +554,29 @@ impl TlsClientConfig {
The `ClientVerifierContext` carries the inputs to ADR-034's verifier
selection (whether a `PeerEntry` exists for the remote, the expected
fingerprint). The exact struct shape is an implementation detail; the
decisions are in ADR-034. The `TlsError` variant granularity (covering
both server and client errors) is decided — see
decisions are in ADR-034. `ClientVerifierContext` is derived from
`CallCredentials` (in `alknet-core`) at the dial site — `AlknetClient`
extracts the TLS-relevant fields (`tls_identity` → `local_identity`,
`remote_identity` → fingerprint-pin input) and builds a
`ClientVerifierContext` from those. The call-protocol `auth_token` is
**stripped at the TLS boundary** — it never reaches `TlsClientConfig` or
`ClientVerifierContext`; it travels with the `Connection` into the
protocol take-over. The `TlsError` variant granularity (covering both
server and client errors) is decided — see
[ADR-088](../../decisions/088-tlserror-shape.md) and the
[`TlsError`](#tlserror) section below.
**Root store fallback (ADR-088 §5).** The unknown-X.509-remote
CA-verification path loads the platform's native root certs
(`rustls-native-certs`). If the platform store is empty (e.g. a
containerized deployment with no system CA bundle), the built-in
`webpki-roots` are merged in so the store is never empty. This makes
the `NoRootAnchors` failure mode unreachable in practice — a
containerized worker dialing a public X.509 hub succeeds without
requiring the operator to mount a CA bundle. Native-certs *load* errors
are logged, not returned (preserved behavior); the fallback guarantees
the store is non-empty regardless. See ADR-088 §5.
`TlsClientConfig` produces a `rustls::ClientConfig`; the caller (the
transport-specific dial helper — `AlknetClient::dial_quic` /
`dial_tcp_tls`, ADR-089) passes it to the transport's connector. The
@@ -642,10 +677,11 @@ arrive asynchronously and are logged (ADR-082 §"Behavior-preservation
invariants").
**Ownership.** `TlsError` lives in `alknet-tls`, owned by the crate
that produces it. It is not re-exported from `alknet-core`; core's
`EndpointError` has no `TlsConfig` variant after ADR-083 and does not
need to know about `TlsError`. The assembly layer (hub/worker) depends
on `alknet-tls` directly and gets `TlsError` from that dependency.
that produces it. It is not re-exported from `alknet-core`; `EndpointError`
is removed entirely after ADR-083 (both variants were vestigial), so
core has no endpoint error type and does not need to know about
`TlsError`. The assembly layer (hub/worker) depends on `alknet-tls`
directly and gets `TlsError` from that dependency.
## Crate dependencies (in the dep graph)
@@ -653,11 +689,12 @@ on `alknet-tls` directly and gets `TlsError` from that dependency.
alknet-tls
├── alknet-core (TlsIdentity, Ed25519SecretKey, fingerprint)
alknet-core (loses TLS setup code)
alknet-core (loses TLS setup code + endpoint)
├── (rustls — only for fingerprint.rs types, if kept)
alknet-call (client-side verifier — unchanged)
├── alknet-core (fingerprint.rs)
alknet-call (pure protocol crate — no TLS/transport deps per ADR-089 §5)
└── alknet-core (ProtocolHandler, Connection, types; CallCredentials/RemoteIdentity
moved to core per ADR-089 §5)
alknet-hub (multi-transport endpoint)
├── alknet-tls (TlsServerConfig — shared across quinn + TCP)
@@ -696,12 +733,13 @@ All design decisions are documented as ADRs in
See [open-questions.md](../../open-questions.md) for full details.
- **OQ-59** (resolved): `fingerprint.rs` stays in `alknet-core`. The
client-side `FingerprintPinVerifier` (in `alknet-call`) uses fingerprint
functions and must not depend on `alknet-tls` (which would pull TLS
setup infra into client-only deployments). The `rustls` dep in core is
narrow — production fingerprint code uses only `sha2` + manual DER; the
`rustls::sign` usage is a test helper. `alknet-tls` re-exports the
fingerprint functions for convenience.
client-side `FingerprintPinVerifier` (now in `alknet-tls` per
ADR-089 §5 — the original `alknet-call` → `alknet-tls` dep-edge
concern that motivated keeping `fingerprint.rs` in core is
dissolved). `fingerprint.rs` stays in core because `alknet-core`'s
own `Identity`/fingerprint code uses it; `alknet-tls` re-exports.
The `rustls` dep in core is narrow — production fingerprint code uses
only `sha2` + manual DER; the `rustls::sign` usage is a test helper.
- **OQ-60** (resolved): Where does transport construction live? The
TCP+TLS accept loop lives in `alknet-endpoint` behind a `tcp` feature
as an owned endpoint transport (`with_tcp_tls`). Builder functions are
@@ -758,9 +796,10 @@ accessors (`for_quinn`, `for_tcp_tls`, `rustls_config`); iroh is the
exception (shares the key, not the config). `AlknetClient` is the dial
that feeds them — it produces a `Connection` and the protocol
take-overs (`spawn_dispatch`, `from_connection`) consume it. The
existing `CallClient::connect` / `ChannelClient::connect_quic`
convenience constructors become thin wrappers over
`AlknetClient::dial_quic`. The `alknet/register` ALPN (native
per-protocol QUIC convenience constructors (`CallClient::connect` /
`ChannelClient::connect_quic`) are **removed** per ADR-089 §5 — the
dial is centralized in `AlknetClient`, and the protocol crates shed
their TLS/transport deps. The `alknet/register` ALPN (native
registration entry point, parallel to HTTP registration in OQ-58) is
named by ADR-089; its wire protocol is deferred (OQ-66).
@@ -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
+8 -6
View File
@@ -95,15 +95,17 @@ alknet-vault (standalone — foundational to ACL: key derivation, identity)
│
├── Substrate
│ alknet-core ProtocolHandler, Connection, BidiStreamSource, AuthContext,
│ │ IdentityProvider, StaticConfig, DynamicConfig, fingerprint
│ │ IdentityProvider, StaticConfig, DynamicConfig, fingerprint,
│ │ CallCredentials, RemoteIdentity
│ │ (endpoint extracted to alknet-endpoint; core is now lightweight
│ │ types+auth+config — no quinn/iroh/rcgen deps)
│ ├── alknet-tls TlsServerConfig + TlsClientConfig — shared TLS config across quinn + TCP+TLS + iroh (ADR-082/087)
│ ├── alknet-call CallAdapter on alknet/call, CallClient, OperationRegistry, adapters
│ │ types+auth+config — no quinn/iroh/rcgen deps; CallCredentials
│ │ moved here from alknet-call per ADR-089 §5)
│ ├── alknet-tls TlsServerConfig + TlsClientConfig + FingerprintPinVerifier — shared TLS config across quinn + TCP+TLS + iroh (ADR-082/087; FingerprintPinVerifier moved from alknet-call per ADR-089 §5)
│ ├── alknet-call CallAdapter on alknet/call, CallClient (spawn_dispatch only — connect removed per ADR-089 §5), OperationRegistry, adapters (no TLS/transport deps)
│ ├── alknet-channels
│ │ ├── alknet-channels-core pure multiplexer (wire format, demux/mux) — 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)
│ ├── alknet-client AlknetClient — native client dial seam (QUIC + TCP+TLS + iroh); produces Connection for CallClient/ChannelClient take-over (ADR-089; CallClient::connect/ChannelClient::connect_quic removed — dial centralized here)
│ └── alknet-endpoint AlknetEndpoint — multi-transport accept-loop runner, extracted from core (ADR-083 Am. 2026-07-15); takes pre-built transports; public dispatch for SSH/WT
│
├── Deployment shapes
@@ -354,7 +356,7 @@ All design decisions are documented as ADRs in [decisions/](decisions/).
| [014](decisions/014-secret-material-flow-and-capability-injection.md) | Secret Material Flow and Capability Injection | Capabilities carry outbound credentials; call protocol carries no secret material |
| [015](decisions/015-privilege-model-and-authority-context.md) | Privilege Model and Authority Context | `internal` = authority switch not ACL skip; External/Internal visibility; handler identity + scoped env |
| [016](decisions/016-abort-cascade-for-nested-calls.md) | Abort Cascade for Nested Calls | `call.aborted` cascades to descendants; default `abort-dependents`, `continue-running` opt-in |
| [017](decisions/017-call-protocol-client-and-adapter-contract.md) | Call Protocol Client and Adapter Contract | `CallClient` takes over connections (`spawn_dispatch` transport-agnostic primary, `connect` QUIC convenience); `from_call` imports remote ops; connection direction independent of call direction |
| [017](decisions/017-call-protocol-client-and-adapter-contract.md) | Call Protocol Client and Adapter Contract | `CallClient` takes over connections (`spawn_dispatch` transport-agnostic primary; `connect` removed per ADR-089 §5 — dial extracted to `AlknetClient`); `from_call` imports remote ops; connection direction independent of call direction |
| [018](decisions/018-vault-standalone-crate.md) | Vault as Standalone Crate | Zero alknet crate dependencies; vault defines own types and errors |
| [019](decisions/019-vault-assembly-layer-only.md) | Vault Assembly-Layer-Only Access | The assembly layer (CLI binary) is the sole direct caller; handlers never hold a vault reference |
| [020](decisions/020-hd-derivation-for-encryption-keys.md) | HD Derivation for Encryption Keys | SLIP-0010 derivation from seed, not PBKDF2; salt field unused in v2 |
@@ -4,5 +4,5 @@
- **Status**: resolved
- **Door type**: One-way
- **Priority**: high
- **Resolution**: `CallClient` takes over transport connections (`spawn_dispatch` transport-agnostic primary, `connect` QUIC convenience — ADR-017 Am. 2026-07-13) and shares the dispatch loop with `CallAdapter` — both sides can send and receive `call.requested` once connected. Connection direction (who opened the connection) is independent of call direction (who calls whom). `from_call` adapter discovers remote operations via `services/list` + `services/schema` and registers them with forwarding handlers — same pattern as `from_openapi` and `from_mcp`. `to_openapi` and `to_mcp` project local operations to external protocols. Adapter contract trait (`OperationAdapter`) produces `HandlerRegistration` bundles. Cross-node call tree: abort cascade (ADR-016) propagates across node boundaries through `from_call` handlers. Credentials for connections come from capabilities (ADR-014). Adapter-registered operations are `Internal` by default (ADR-015). See ADR-017.
- **Resolution**: `CallClient` takes over transport connections (`spawn_dispatch` transport-agnostic primary; `connect` removed per ADR-089 §5 — dial extracted to `AlknetClient`) and shares the dispatch loop with `CallAdapter` — both sides can send and receive `call.requested` once connected. Connection direction (who opened the connection) is independent of call direction (who calls whom). `from_call` adapter discovers remote operations via `services/list` + `services/schema` and registers them with forwarding handlers — same pattern as `from_openapi` and `from_mcp`. `to_openapi` and `to_mcp` project local operations to external protocols. Adapter contract trait (`OperationAdapter`) produces `HandlerRegistration` bundles. Cross-node call tree: abort cascade (ADR-016) propagates across node boundaries through `from_call` handlers. Credentials for connections come from capabilities (ADR-014). Adapter-registered operations are `Internal` by default (ADR-015). See ADR-017.
- **Cross-references**: ADR-005, ADR-013, ADR-014, ADR-015, ADR-016, ADR-017, [call-protocol.md](crates/call/call-protocol.md), [operation-registry.md](crates/call/operation-registry.md)
@@ -39,9 +39,10 @@
own client standalone with a shared `TlsClientConfig` (ADR-087), and
each transport-specific dial helper. `CallClient`'s transport-agnostic
take-over (`spawn_dispatch`) and `ChannelClient`'s transport-agnostic
take-over (`from_connection`, ADR-080) are decided; the existing
`connect` / `connect_quic` convenience constructors become thin
wrappers over `AlknetClient::dial_quic` (ADR-089 §5).
take-over (`from_connection`, ADR-080) are decided; the
`connect` / `connect_quic` convenience constructors are **removed**
per ADR-089 §5 (not delegated — the dial is centralized in
`AlknetClient`, and the protocol crates shed their TLS/transport deps).
- **Cross-references**: ADR-089 (the resolution — `AlknetClient` native
dial seam), ADR-083 (the server-side shape mirrored), ADR-086
(endpoint types — native has QUIC + TCP+TLS + iroh), ADR-087
@@ -76,14 +76,14 @@
Subsidiary question: does `TlsError` live in `alknet-tls` (owned by
the crate that produces it) or is it re-exported from `alknet-core`?
**Resolved: `alknet-tls`, owned by the crate that produces it.**
`alknet-core`'s `EndpointError` no longer has a `TlsConfig` variant
after ADR-083 (the endpoint takes no TLS config); core does not need to
know about `TlsError`. Re-exporting from core would invert the
ownership (core re-exporting a type from a crate that depends on it).
`EndpointError` is removed entirely after ADR-083 (both variants
vestigial); core has no endpoint error type and does not need to know
about `TlsError`. Re-exporting from core would invert the ownership
(core re-exporting a type from a crate that depends on it).
- **Cross-references**: ADR-088 (the resolution — single enum, owned by
`alknet-tls`, six variants), ADR-082 (the extraction that introduces
`TlsError`), ADR-083 (the endpoint refactor that removes
`EndpointError::TlsConfig`, making `TlsError` the sole TLS error
`EndpointError` entirely, making `TlsError` the sole TLS error
surface), ADR-087 (extends the surface to client-side variants),
`crates/alknet-core/src/endpoint.rs` (the current
`io::Error`-wrapping pattern the new type replaces),
@@ -32,8 +32,10 @@
`TlsClientConfig::new` takes a verifier context (the inputs to
ADR-034's rule: `PeerEntry` presence, expected fingerprint, remote
cert type) and returns a `rustls::ClientConfig`. The caller
(transport-specific dial helper, `CallClient::connect_quic`, a future
`connect_tcp_tls`) passes it to the transport's connector. Iroh is
(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. Iroh is
the exception — it has its own TLS and does not consume a
`rustls::ClientConfig`; the iroh dial helper applies the same
ADR-034 rule via iroh's `NodeId` verification API.