docs(arch): TlsError shape — single enum, owned by alknet-tls (ADR-088, resolves OQ-63)

Grounded in the actual error-producing call sites (endpoint.rs server
side, call_client.rs client side) and the dependency-crate sources read
from the cargo cache (rustls 0.23.41, rustls-pemfile 2.2.0, rcgen 0.13.2,
quinn-proto 0.11.15, rustls-acme 0.12.1).

Decision: single #[non_exhaustive] enum, one variant per failure
category, owned by alknet-tls (not re-exported from core). Six variants:
CertLoad(io::Error), SelfSigned(rcgen::Error), Rustls(rustls::Error),
VerifierBuild(VerifierBuilderError), QuinnWrap(NoInitialCipherSuite)
[quinn-gated], AcmeConfig(String).

Three findings drove single-enum over thin wrapper: (1) for_quinn()
fails with NoInitialCipherSuite, not rustls::Error — a rustls::Error
wrapper cannot represent the for_quinn() failure; (2) rustls_pemfile::Error
is not a std::error::Error (no Display, no Error impl) so #[from] would
not compile — pemfile BufRead APIs return io::Error; (3)
WebPkiServerVerifier::build() returns VerifierBuilderError, not
rustls::Error — a thin wrapper cannot represent empty-CA-root-store as
a first-class failure.

Deliberately NOT variants: ACME EventError/OrderError (stream events,
logged not returned from new); unknown-raw-key fail-closed (handshake-
time rejection at dial time, not a config-construction error —
corrects OQ-63's original framing); provider init (infallible); resolver
construction (infallible).
This commit is contained in:
glm-5.2 committed 2026-07-15 09:22:24 +00:00
1 parent 5941280bca
commit 43b8179304
5 files changed
+573 -23

No files matched your search

@@ -6,7 +6,7 @@
crate's public signatures, but is never sketched or defined); ADR-082
(same — `TlsError` in signatures, no shape); ADR-087 (extends the
surface to `TlsClientConfig::new` — the client-side error variants).
- **Status**: open
- **Status**: resolved (ADR-088)
- **Door type**: one-way (the error type is the public API surface of
`alknet-tls`; changing it after consumers exist is a breaking change
to every assembly-layer call site)
@@ -18,9 +18,12 @@
in their signatures. Blocks the hub's assembly-layer wiring (which
calls both). This is the next decision needed before the TLS crate
can be implemented.
- **Resolution**: Not yet decided. The shape needs to cover the failure
modes across all server identity paths **and** the client verifier
paths (ADR-087):
- **Resolution**: **Decided (ADR-088).** `TlsError` is a single
`#[non_exhaustive]` enum with one variant per failure category, owned
by `alknet-tls` (not re-exported from core). The variants, grounded in
the actual error-producing call sites and the dependency-crate sources
(rustls 0.23.41, rustls-pemfile 2.2.0, rcgen 0.13.2, quinn-proto
0.11.15, rustls-acme 0.12.1):
- **Cert/key loading** (`X509`: file read + PEM parse;
`SelfSigned`: rcgen generation) — currently `io::Error`-wrapped in
@@ -46,22 +49,43 @@
overlap with the server-side rustls-build errors but have
client-specific context (verifier selection inputs).
The open question is the granularity: a single `TlsError` enum with
The open question was the granularity: a single `TlsError` enum with
variants per failure category (cert-load, rustls-build, quinn-wrap,
acme-disabled) vs a thin wrapper around `rustls::Error` /
`io::Error`. The single-enum shape gives callers matchable context
(the assembly layer can distinguish "cert file missing" from "quinn
rejected the rustls config"); the thin wrapper is less code but
loses the distinction. This is an API-surface decision — it needs an
ADR or at minimum a sketch in the TLS README before implementation.
`io::Error`. **Resolved: the single-enum shape.** The thin wrapper is
actively wrong here — three findings from the dependency-crate source
drive the decision: (1) `for_quinn()` fails with
`NoInitialCipherSuite`, a distinct type from `rustls::Error` — a thin
`rustls::Error` wrapper cannot represent the `for_quinn()` failure;
(2) `rustls_pemfile::Error` is not a `std::error::Error` (no
`Display`, no `Error` impl), so pemfile's BufRead APIs return
`io::Error` — a `#[from] rustls_pemfile::Error` would not compile;
(3) `WebPkiServerVerifier::build()` returns `VerifierBuilderError`,
not `rustls::Error` — a thin wrapper cannot represent "empty CA root
store" as a first-class failure. The six variants: `CertLoad(io::Error)`,
`SelfSigned(rcgen::Error)`, `Rustls(rustls::Error)`,
`VerifierBuild(VerifierBuilderError)`, `QuinnWrap(NoInitialCipherSuite)`
(quinn-gated), `AcmeConfig(String)`. ACME `EventError`/`OrderError`
are deliberately absent — they are stream events, logged not returned
from `new`. The unknown-raw-key fail-closed is deliberately absent —
it is a handshake-time rejection (dial time), not a
config-construction error (`new` time). See ADR-088 for the full
rationale, the variant definitions, and the "what is NOT a variant"
list.
Subsidiary question: does `TlsError` live in `alknet-tls` (owned by
the crate that produces it) or is it re-exported from `alknet-core`?
Likely `alknet-tls` (it's the crate's own error), but worth
confirming so `alknet-core`'s `EndpointError` (which no longer has a
`TlsConfig` variant after ADR-083) doesn't need to know about it.
- **Cross-references**: ADR-082 (the extraction that introduces
**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).
- **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
surface), `crates/alknet-core/src/endpoint.rs` (the current
`io::Error`-wrapping pattern the new type replaces)
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),
`crates/alknet-call/src/client/call_client.rs` (the current
`String`-wrapping pattern on the client side)