docs(arch): tighten tls/endpoint/client specs — remove connect/connect_quic, move CallCredentials to core, shed alknet-call TLS deps
Review of the three new crates (alknet-tls, alknet-endpoint, alknet-client) + revised core found compile-blocking inconsistencies, stale claims, and dep-graph contradictions. All resolved: Critical: - C1: CallClient::connect / ChannelClient::connect_quic REMOVED (not delegated) — keeping them as thin wrappers over AlknetClient::dial_quic would make protocol crates depend on alknet-client, contradicting the dep graph. Callers compose dial + take-over (2 lines). - C2: alknet-client feature gates now pull alknet-core/quinn + alknet-core/iroh (for Connection::from_quinn_with_alpn / from_iroh). - C3: rustls-native-certs + webpki-roots added to alknet-tls deps (always-present, not feature-gated — CA-verify path is transport-agnostic). Warning: - W1: CallCredentials/RemoteIdentity moved to alknet-core (from alknet-call) — the dial must not depend on the call protocol; not a two-way-door, it determines the dep graph. - W2: webpki-roots fallback implemented in spec (ADR-088 §5 added) — the code claimed a fallback that never existed; now the store is never empty, NoRootAnchors unreachable, containerized deployments work. - W3: EndpointError removed entirely (BindFailed + HandlerNotFound both vestigial after ADR-083); shutdown() is now infallible. - W4: FingerprintPinVerifier moved to alknet-tls (from alknet-call) — alknet-call sheds quinn/rustls/rustls-pemfile/rustls-native-certs entirely; CallClient becomes a pure protocol crate. Plus: ClientError removed (only produced by removed connect); S1 (CallCredentials → ClientVerifierContext mapping + auth_token stripped at TLS boundary documented); amendment notes on ADR-017, ADR-069, ADR-080, ADR-082, ADR-087, ADR-090; overview crate graph + README index updated. 29 files, consistency-reviewed.
This commit is contained in:
1 parent
34e3be2801
commit
bf6ce957c0
29 files changed
+524
-267
No files matched your search
@@ -14,7 +14,7 @@ Structured RPC: operations, request/response, streaming subscriptions, and servi
|
||||
|----------|--------|-------------|
|
||||
| [call-protocol.md](call-protocol.md) | draft | CallAdapter, hand-rolled EventEnvelope framing (no irpc — ADR-064), stream model, PendingRequestMap, bidirectional calls |
|
||||
| [operation-registry.md](operation-registry.md) | draft | OperationSpec, Handler, OperationRegistry, AccessControl, service discovery, hand-rolled framing (no irpc — ADR-064) |
|
||||
| [client-and-adapters.md](client-and-adapters.md) | draft | CallClient (transport-agnostic `spawn_dispatch` primary, `connect` QUIC convenience — ADR-017 Am. 2026-07-13), from_call, OperationAdapter trait, adapter location map, no-env-vars invariant, exchange-of-operations pattern (from_jsonschema moved to alknet-http per ADR-066) |
|
||||
| [client-and-adapters.md](client-and-adapters.md) | draft | CallClient (transport-agnostic `spawn_dispatch` primary; `connect` removed per ADR-089 §5 — dial extracted to `AlknetClient`), from_call, OperationAdapter trait, adapter location map, no-env-vars invariant, exchange-of-operations pattern (from_jsonschema moved to alknet-http per ADR-066) |
|
||||
|
||||
## Applicable ADRs
|
||||
|
||||
@@ -81,7 +81,7 @@ Structured RPC: operations, request/response, streaming subscriptions, and servi
|
||||
8. **Abort cascades to descendants**: `call.aborted` for a parent request cascades to all non-terminal descendants. Default `abort-dependents`; `continue-running` opt-in. See ADR-016.
|
||||
9. **Internal calls switch authority context, not skip ACL**: The `internal` flag marks composition-originated calls. ACL runs against the handler's composition authority, not the caller's and not as a blanket skip. Operations have External/Internal visibility. Scoped composition env bounds reachability. See ADR-015, ADR-022.
|
||||
10. **Provenance determines composition capability**: Only `Local` and `Session` ops can compose. Leaves (`FromOpenAPI`, `FromMCP`, `FromCall`, `FromJsonSchema`) are forwarding stubs — they don't get composition authority or a scoped env. The assembly layer is the sole grantor of composition authority. See ADR-022. (`FromJsonSchema` is now a real HTTP-forwarding leaf per ADR-066, not a schema-only placeholder.)
|
||||
11. **Connection direction is independent of call direction**: Who opens the connection is a connection-layer concern, not a protocol-layer concern. Both sides can call each other once connected. The `CallAdapter` accepts connections; the `CallClient` takes them over (`spawn_dispatch` primary, `connect` QUIC convenience — ADR-017 Am. 2026-07-13); both produce the same `CallConnection` and dispatch through the same loop. See ADR-017, [client-and-adapters.md](client-and-adapters.md).
|
||||
11. **Connection direction is independent of call direction**: Who opens the connection is a connection-layer concern, not a protocol-layer concern. Both sides can call each other once connected. The `CallAdapter` accepts connections; the `CallClient` takes them over (`spawn_dispatch` primary; `connect` removed per ADR-089 §5 — dial extracted to `AlknetClient`); both produce the same `CallConnection` and dispatch through the same loop. See ADR-017, [client-and-adapters.md](client-and-adapters.md).
|
||||
12. **Peer authorization via `AccessControl`**: A remote peer's call is authorized by `AccessControl::check(peer_identity)` against the op's `AccessControl` — the same mechanism that gates every other call. No `remote_safe` flag, no `trusted_peer` bypass. An op with `AccessControl::default()` is callable by any peer; an op with `required_scopes` is callable only by peers whose `Identity.scopes` satisfy them; an op with `Visibility::Internal` is never callable from the wire. See ADR-029.
|
||||
13. **Adapter trait lives with the types; implementations live with their transport**: `OperationAdapter` is in `alknet-call`; `from_call` is in `alknet-call` (QUIC); `from_jsonschema`/`from_openapi`/`from_mcp`/`to_openapi`/`to_mcp` are in `alknet-http` (reqwest / axum). `alknet-call` stays lean — no HTTP client, no HTTP server. (`from_jsonschema` was originally in `alknet-call` as a schema-only placeholder; ADR-066 moved it to `alknet-http` as a real HTTP-backed adapter.) See [client-and-adapters.md](client-and-adapters.md).
|
||||
14. **No handler reads outbound credentials from any source other than `OperationContext.capabilities`** (no-env-vars invariant): the credential injection path is vault → assembly layer → `Capabilities` → `HandlerRegistration.capabilities` → `OperationContext.capabilities` → handler. Downstream consumers' `std::env::var` reads are unreachable because the assembly layer never calls `Default::default()`. See ADR-014, [client-and-adapters.md](client-and-adapters.md).
|
||||
@@ -114,7 +114,7 @@ operations discovered when the connection was established.
|
||||
/// opened). Holds the connection's Layer 2 overlay (imported ops).
|
||||
pub struct CallConnection {
|
||||
/// The underlying transport Connection (from endpoint.accept,
|
||||
/// CallClient::spawn_dispatch, or CallClient::connect). May be QUIC,
|
||||
/// CallClient::spawn_dispatch, or AlknetClient::dial_*). May be QUIC,
|
||||
/// TCP+TLS, WebTransport, SSH, or any Connection::from_stream source
|
||||
/// (ADR-065).
|
||||
connection: Connection,
|
||||
@@ -176,10 +176,12 @@ The adapter:
|
||||
6. Manages the `PendingRequestMap` for outgoing calls
|
||||
|
||||
The dispatch loop is **shared** with `CallClient` (ADR-017 §1): both
|
||||
`CallAdapter::handle` (accept path) and `CallClient::connect` (connect path)
|
||||
construct a `Dispatcher` (`protocol/dispatch.rs`) and call `run_loop` — the
|
||||
dispatch half is one implementation, the connection-establishment half differs
|
||||
(accept vs dial). Peer authorization flows through the existing
|
||||
`CallAdapter::handle` (accept path) and `CallClient::spawn_dispatch`
|
||||
(connect path — the dial is now `AlknetClient::dial_*` per ADR-089 §5;
|
||||
`CallClient::connect` is removed) construct a `Dispatcher`
|
||||
(`protocol/dispatch.rs`) and call `run_loop` — the dispatch half is one
|
||||
implementation, the connection-establishment half differs (accept vs
|
||||
dial). Peer authorization flows through the existing
|
||||
`AccessControl::check(peer_identity)` — no `RemoteFilter`/`remote_safe` gate
|
||||
(ADR-029 §3). The composition env is peer-keyed (`PeerCompositeEnv`,
|
||||
ADR-029 §1) to handle head→N-workers routing. See
|
||||
|
||||
@@ -22,11 +22,11 @@ This document specifies three components, all in `alknet-call`:
|
||||
|
||||
1. **`CallClient`** — takes over an established transport `Connection`
|
||||
on ALPN `alknet/call`, spawns the shared dispatch loop, and produces
|
||||
a `CallConnection`. Transport-agnostic (`spawn_dispatch` primary,
|
||||
`connect` QUIC convenience); the dispatch loop is shared with the
|
||||
server-side `CallAdapter` (ADR-017 §1); `CallClient` is the
|
||||
connection-establishment + credential-handling half, not a parallel
|
||||
protocol implementation.
|
||||
a `CallConnection`. Transport-agnostic (`spawn_dispatch` primary;
|
||||
`connect` removed per ADR-089 §5 — dial extracted to `AlknetClient`);
|
||||
the dispatch loop is shared with the server-side `CallAdapter`
|
||||
(ADR-017 §1); `CallClient` is the connection-take-over half, not a
|
||||
parallel protocol implementation.
|
||||
2. **`from_call`** — discovers operations on a remote call-protocol endpoint
|
||||
via `services/list` + `services/schema` (already implemented in
|
||||
`registry/discovery.rs`) and registers them in the connection's Layer 2
|
||||
@@ -124,15 +124,13 @@ impl CallClient {
|
||||
/// a transport.
|
||||
pub fn spawn_dispatch(&self, connection: Connection) -> CallConnection;
|
||||
|
||||
/// QUIC convenience constructor. Dials a QUIC connection to `addr`
|
||||
/// on ALPN `alknet/call` (using `credentials` for the TLS handshake
|
||||
/// — ADR-034 verifier selection), then calls `spawn_dispatch`.
|
||||
/// Feature-gated on `quinn` (the dial is QUIC-specific). Additive
|
||||
/// and two-way-door — `connect_tcp_tls`, `connect_webtransport`,
|
||||
/// etc. join it as transports are added, without touching the
|
||||
/// `spawn_dispatch` contract. This convenience is a thin wrapper
|
||||
/// over `AlknetClient::dial_quic` (ADR-089) — a caller that needs
|
||||
/// transport selection uses `AlknetClient` directly.
|
||||
/// **REMOVED per ADR-089 §5.** The dial is extracted into
|
||||
/// `AlknetClient` (`alknet-client`); `connect` is deleted, not
|
||||
/// delegated, to avoid `alknet-call` depending on `alknet-client`
|
||||
/// and to let `alknet-call` shed its TLS/transport deps entirely.
|
||||
/// Callers compose `AlknetClient::dial_quic(...).await?` +
|
||||
/// `CallClient::new(...).spawn_dispatch(conn)`. `ClientError` is
|
||||
/// removed (it was produced only by `connect`).
|
||||
#[cfg(feature = "quinn")]
|
||||
pub async fn connect(
|
||||
&self,
|
||||
@@ -192,19 +190,23 @@ authorization machinery that gates every other call. No `RemoteFilter`, no
|
||||
primary constructor — it takes a pre-established `Connection`,
|
||||
constructs a `CallConnection`, builds a `Dispatcher`, spawns the
|
||||
dispatch task, and returns the live `CallConnection`. `connect()` is
|
||||
the QUIC convenience over it: dial QUIC (feature-gated on `quinn`),
|
||||
then `spawn_dispatch`. Tests use `spawn_dispatch` directly to wire
|
||||
mock/loopback connections. A future `connect_tcp_tls` /
|
||||
`connect_webtransport` would dial their transport and call
|
||||
`spawn_dispatch` the same way. The one-way-door surface is
|
||||
`spawn_dispatch`; the dial helpers are two-way-door conveniences.
|
||||
**removed** per ADR-089 §5: the dial is extracted into `AlknetClient`
|
||||
(`alknet-client`), and keeping a QUIC convenience constructor on
|
||||
`CallClient` would make `alknet-call` depend on `alknet-client`,
|
||||
contradicting the dep graph (the protocol crates are parallel to the
|
||||
dial, not downstream of it). Callers compose `AlknetClient::dial_quic`
|
||||
+ `spawn_dispatch` — two lines, the dial then the take-over. Tests use
|
||||
`spawn_dispatch` directly to wire mock/loopback connections. The
|
||||
one-way-door surface is `spawn_dispatch`; the dial lives in
|
||||
`alknet-client`.
|
||||
|
||||
This mirrors `ChannelClient::from_connection` / `connect_quic`
|
||||
(ADR-080) and is the client-side analogue of the server-side
|
||||
generalization ADR-065 made. The call protocol, like the channels
|
||||
protocol, is transport-agnostic — `Connection::from_stream` /
|
||||
`from_bidi` (ADR-065) accept any `AsyncRead + AsyncWrite`, and
|
||||
`spawn_dispatch` takes the resulting `Connection` unchanged.
|
||||
This mirrors `ChannelClient::from_connection` (ADR-080; its
|
||||
`connect_quic` is likewise removed per ADR-089 §5) and is the
|
||||
client-side analogue of the server-side generalization ADR-065 made.
|
||||
The call protocol, like the channels protocol, is transport-agnostic —
|
||||
`Connection::from_stream` / `from_bidi` (ADR-065) accept any
|
||||
`AsyncRead + AsyncWrite`, and `spawn_dispatch` takes the resulting
|
||||
`Connection` unchanged.
|
||||
|
||||
#### Peer-keyed composition env (ADR-029)
|
||||
|
||||
@@ -244,8 +246,10 @@ attribution, filtered by the calling peer's authorization). See
|
||||
|
||||
### Credential sources for connections
|
||||
|
||||
`CallClient::connect()` takes a `CallCredentials` bundle. Credentials come
|
||||
from `Capabilities` (ADR-014), never from environment variables. The three
|
||||
`CallCredentials` (now in `alknet-core`, moved from `alknet-call` per
|
||||
ADR-089 §5 so the dial does not depend on the call protocol) carries
|
||||
the three credential dimensions (ADR-017 §7). Credentials come from
|
||||
`Capabilities` (ADR-014), never from environment variables. The three
|
||||
credential dimensions (ADR-017 §7):
|
||||
|
||||
```rust
|
||||
|
||||
Reference in new issue
Block a user