diff --git a/docs/architecture/README.md b/docs/architecture/README.md index e1e6572..300fa49 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -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` — 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 diff --git a/docs/architecture/crates/call/README.md b/docs/architecture/crates/call/README.md index 89413f5..6449314 100644 --- a/docs/architecture/crates/call/README.md +++ b/docs/architecture/crates/call/README.md @@ -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). \ No newline at end of file diff --git a/docs/architecture/crates/call/call-protocol.md b/docs/architecture/crates/call/call-protocol.md index 71c98be..f80e500 100644 --- a/docs/architecture/crates/call/call-protocol.md +++ b/docs/architecture/crates/call/call-protocol.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 diff --git a/docs/architecture/crates/call/client-and-adapters.md b/docs/architecture/crates/call/client-and-adapters.md index dbf9383..c2bdb25 100644 --- a/docs/architecture/crates/call/client-and-adapters.md +++ b/docs/architecture/crates/call/client-and-adapters.md @@ -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 diff --git a/docs/architecture/crates/channels/README.md b/docs/architecture/crates/channels/README.md index 5a4731f..bf3c44e 100644 --- a/docs/architecture/crates/channels/README.md +++ b/docs/architecture/crates/channels/README.md @@ -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) | diff --git a/docs/architecture/crates/channels/channel-client.md b/docs/architecture/crates/channels/channel-client.md index d3a021f..aeb7525 100644 --- a/docs/architecture/crates/channels/channel-client.md +++ b/docs/architecture/crates/channels/channel-client.md @@ -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 diff --git a/docs/architecture/crates/channels/overview.md b/docs/architecture/crates/channels/overview.md index d67a9a8..12049f5 100644 --- a/docs/architecture/crates/channels/overview.md +++ b/docs/architecture/crates/channels/overview.md @@ -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 diff --git a/docs/architecture/crates/client/README.md b/docs/architecture/crates/client/README.md index eacce47..6df444a 100644 --- a/docs/architecture/crates/client/README.md +++ b/docs/architecture/crates/client/README.md @@ -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 diff --git a/docs/architecture/crates/core/README.md b/docs/architecture/crates/core/README.md index c51a1ec..0cf350c 100644 --- a/docs/architecture/crates/core/README.md +++ b/docs/architecture/crates/core/README.md @@ -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 diff --git a/docs/architecture/crates/core/core-types.md b/docs/architecture/crates/core/core-types.md index c051060..2d3e09f 100644 --- a/docs/architecture/crates/core/core-types.md +++ b/docs/architecture/crates/core/core-types.md @@ -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) -> Self; diff --git a/docs/architecture/crates/core/endpoint.md b/docs/architecture/crates/core/endpoint.md index c1e0ea5..0e5a17c 100644 --- a/docs/architecture/crates/core/endpoint.md +++ b/docs/architecture/crates/core/endpoint.md @@ -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`, diff --git a/docs/architecture/crates/endpoint/README.md b/docs/architecture/crates/endpoint/README.md index 7c44cbf..266d9d5 100644 --- a/docs/architecture/crates/endpoint/README.md +++ b/docs/architecture/crates/endpoint/README.md @@ -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); - 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) }` enum. Both variants are vestigial after +ADR-083: -```rust -pub enum EndpointError { - BindFailed(io::Error), - HandlerNotFound(Vec), // 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) diff --git a/docs/architecture/crates/hub/README.md b/docs/architecture/crates/hub/README.md index f229ec2..658591b 100644 --- a/docs/architecture/crates/hub/README.md +++ b/docs/architecture/crates/hub/README.md @@ -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) — diff --git a/docs/architecture/crates/tls/README.md b/docs/architecture/crates/tls/README.md index df42d59..6cccabe 100644 --- a/docs/architecture/crates/tls/README.md +++ b/docs/architecture/crates/tls/README.md @@ -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); - 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). diff --git a/docs/architecture/decisions/010-alpn-router-and-endpoint.md b/docs/architecture/decisions/010-alpn-router-and-endpoint.md index 50812a3..9ada83e 100644 --- a/docs/architecture/decisions/010-alpn-router-and-endpoint.md +++ b/docs/architecture/decisions/010-alpn-router-and-endpoint.md @@ -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 diff --git a/docs/architecture/decisions/017-call-protocol-client-and-adapter-contract.md b/docs/architecture/decisions/017-call-protocol-client-and-adapter-contract.md index 47e5617..ffd7600 100644 --- a/docs/architecture/decisions/017-call-protocol-client-and-adapter-contract.md +++ b/docs/architecture/decisions/017-call-protocol-client-and-adapter-contract.md @@ -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. \ No newline at end of file diff --git a/docs/architecture/decisions/069-from-call-manual-free-function.md b/docs/architecture/decisions/069-from-call-manual-free-function.md index 0bfdd8a..1b6dd1e 100644 --- a/docs/architecture/decisions/069-from-call-manual-free-function.md +++ b/docs/architecture/decisions/069-from-call-manual-free-function.md @@ -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 diff --git a/docs/architecture/decisions/080-channelclient.md b/docs/architecture/decisions/080-channelclient.md index 19a2ca5..25d796f 100644 --- a/docs/architecture/decisions/080-channelclient.md +++ b/docs/architecture/decisions/080-channelclient.md @@ -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) diff --git a/docs/architecture/decisions/082-alknet-tls-extraction.md b/docs/architecture/decisions/082-alknet-tls-extraction.md index 7874864..40a694b 100644 --- a/docs/architecture/decisions/082-alknet-tls-extraction.md +++ b/docs/architecture/decisions/082-alknet-tls-extraction.md @@ -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) diff --git a/docs/architecture/decisions/083-endpoint-as-accept-loop-runner.md b/docs/architecture/decisions/083-endpoint-as-accept-loop-runner.md index cc35472..e729156 100644 --- a/docs/architecture/decisions/083-endpoint-as-accept-loop-runner.md +++ b/docs/architecture/decisions/083-endpoint-as-accept-loop-runner.md @@ -223,9 +223,12 @@ impl AlknetEndpoint { pub async fn run(self: Arc); /// 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 diff --git a/docs/architecture/decisions/087-tlsclientconfig-not-blocked-on-dial.md b/docs/architecture/decisions/087-tlsclientconfig-not-blocked-on-dial.md index 38e999b..adde025 100644 --- a/docs/architecture/decisions/087-tlsclientconfig-not-blocked-on-dial.md +++ b/docs/architecture/decisions/087-tlsclientconfig-not-blocked-on-dial.md @@ -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 diff --git a/docs/architecture/decisions/088-tlserror-shape.md b/docs/architecture/decisions/088-tlserror-shape.md index d8f0f38..614fc74 100644 --- a/docs/architecture/decisions/088-tlserror-shape.md +++ b/docs/architecture/decisions/088-tlserror-shape.md @@ -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 diff --git a/docs/architecture/decisions/089-alknetclient-native-dial-seam.md b/docs/architecture/decisions/089-alknetclient-native-dial-seam.md index ca072a6..ed9c624 100644 --- a/docs/architecture/decisions/089-alknetclient-native-dial-seam.md +++ b/docs/architecture/decisions/089-alknetclient-native-dial-seam.md @@ -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 diff --git a/docs/architecture/decisions/090-client-dial-socks5-proxy-seam.md b/docs/architecture/decisions/090-client-dial-socks5-proxy-seam.md index 3fd224b..dcd249f 100644 --- a/docs/architecture/decisions/090-client-dial-socks5-proxy-seam.md +++ b/docs/architecture/decisions/090-client-dial-socks5-proxy-seam.md @@ -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 diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 282f889..e312a7f 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -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 | diff --git a/docs/architecture/questions/015-call-protocol-client-and-adapter-contract.md b/docs/architecture/questions/015-call-protocol-client-and-adapter-contract.md index 1c65da3..5fc750d 100644 --- a/docs/architecture/questions/015-call-protocol-client-and-adapter-contract.md +++ b/docs/architecture/questions/015-call-protocol-client-and-adapter-contract.md @@ -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) diff --git a/docs/architecture/questions/055-alknetclient-establishment-extraction.md b/docs/architecture/questions/055-alknetclient-establishment-extraction.md index ffe0b05..ba5bcc3 100644 --- a/docs/architecture/questions/055-alknetclient-establishment-extraction.md +++ b/docs/architecture/questions/055-alknetclient-establishment-extraction.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 diff --git a/docs/architecture/questions/063-tlserror-shape.md b/docs/architecture/questions/063-tlserror-shape.md index 599ba22..5f5cd1a 100644 --- a/docs/architecture/questions/063-tlserror-shape.md +++ b/docs/architecture/questions/063-tlserror-shape.md @@ -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), diff --git a/docs/architecture/questions/064-client-side-tls-helper.md b/docs/architecture/questions/064-client-side-tls-helper.md index 95a5571..32b1344 100644 --- a/docs/architecture/questions/064-client-side-tls-helper.md +++ b/docs/architecture/questions/064-client-side-tls-helper.md @@ -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.