The earlier draft kept from_stream as an 'escape hatch' for already-split
transports. That bakes the split into the constructor API — the same
split-leaf shape pushed one step earlier. The cleaner normalization: the
split never crosses a crate boundary as part of a constructor.
- Connection::from_bidi is the only public stream constructor.
- Connection::from_stream(send, recv, ...) is removed.
- The channels reassembly path joins MpscSendStream/MpscRecvStream itself
via tokio::io::join (one line) and calls from_bidi.
- The call crate's 5 test stub sites do tokio::io::split(x) then
from_stream — they become from_bidi(x) directly. The split was always
gratuitous at the call site.
- SendStream::from_stream / RecvStream::from_stream (per-half boxing for
into_sub_streams() and the SubStreamHandle leaves) are retained — not
constructors that feed Connection.
The crate-extraction findings Phase 6 deferred the alknet-http rework
on the grounds that the QuicStream wrapper (44 lines) is a necessary
adapter. The finding was right about the symptom, wrong about the
cause: the root is that the leaf type is split, so every consumer
re-joins or bypasses. Five abstractions exist for one concept
(BiStream trait vestigial in code, Connection, SendStream/RecvStream,
WsStream, MpscSendStream/MpscRecvStream).
ADR-092 resurrects ADR-007's BiStream as a concrete newtype leaf
(the bounds survive, the trait becomes a concrete struct for
Pin<&mut Self> projection), moves the join into core's quinn/iroh
BidiStreamSource impls once, and removes the per-handler wrappers:
- HttpAdapter::handle drops QuicStream (44 lines) and QuicStreamDuplex
(38 lines); serve_io is unchanged.
- WebSocket runs through Connection::from_bidi + the call-protocol
handler. WsBidiStream (~50-80 lines) implements AsyncRead/AsyncWrite
over axum WS binary messages. WsStream trait, drive_ws_session loop,
and ~150 lines of dispatch glue removed. ADR-044/048's 'WS message
stream is BiStream-satisfying' becomes literal.
- Tunnel/SSH handlers call tokio::io::split(bidi) for their two pumps
(same stdlib idiom as TcpStream/TlsStream). ADR-078 preserved.
- SendStream/RecvStream collapse to thin newtypes (quinn enum dispatch
gone); retained for ADR-074 into_sub_streams() and channels
reassembly (ADR-071 unidirectional sub-streams).
- 'VPN-like without being a VPN' over WS in v1 becomes real: the
webtransport.md path, over WS, now. WASM SSH parser implements
BiStream over a WS-message adapter on the browser side.
- WebTransport h3 extraction recorded as a future channels-variant
move enabled by the unification (out of scope per ADR-044).
Amends ADR-065 (from_bidi primary, from_stream escape hatch),
ADR-070 (accept_bi returns BiStream), ADR-074
(ChannelBidiStreamSource::accept_bi returns BiStream; into_sub_streams
unchanged). ADR-077 two-mode TTY design preserved. Resolves the
findings.md Phase 6 deferral.
Three open questions recorded with defaults (WsBidiStream home,
SendStream/RecvStream long-term home, from_stream vs from_bidi
primacy) — none blocking.
ADR-091 decided `ConnectionCredentials.local_identity`; the code implemented
`tls_identity` (tasks/core/connection-credentials.md deferred the rename as
"path of least resistance" during the extraction). The tangle that made the
rename hard no longer exists, so align the code with the decision.
Scope is the `ConnectionCredentials` field + builder only:
- alknet-core/credentials.rs: field, with_local_identity, doc, test
- alknet-tls/client.rs: field access in TlsClientConfig::new, test builders, docs
- alknet-client/dial/quinn.rs: test builder
NOT renamed (distinct concepts sharing the words):
- StaticConfig.tls_identity (server-side static config; ADR-082/027/083)
- TlsIdentity enum type name
- alknet-tls server fn params named tls_identity (&TlsIdentity value)
Also fixes dial_iroh.rs doc comments that claimed the local key is extracted
from creds.local_identity — the key is actually on the pre-built iroh endpoint
(set at with_iroh time); the dial reads only creds.remote_identity and ignores
creds.local_identity (per client/README.md §iroh).
Architecture specs updated to match (call/client-and-adapters.md, tls/README.md,
client/README.md). Historical ADR context describing the old CallCredentials
field stays as-is; tasks/ and docs/research/ are historical artifacts.
Resolves follow-up #2 from the post-extraction spec sync (c6eef73).
The crate-extraction migration (phases 0-5) is complete in the code;
the specs still carried forward/migration framing ("was welded",
"after the refactor", "currently duplicated", "does not exist yet",
"What moves from X to Y" tables, "Implementation ordering") that
described the migration rather than the resulting state. Updated 10
spec files to describe the current state cleanly.
Spec/code mismatches fixed:
- core/README.md: a stale paragraph said CallCredentials "stays in
alknet-call" while ADR-091 Am. 2026-07-17 removed it. Now consistent.
- tls/README.md: TlsClientConfig API described a planned
ClientVerifierContext + for_tcp_tls(&self) + rustls_config(&self);
the actual code is new(&ConnectionCredentials, alpn) +
for_quinn(self) + into_rustls_config(self). Updated to match.
- client/README.md, call/client-and-adapters.md: ConnectionCredentials
field is tls_identity / with_tls_identity in the code, not
local_identity / with_local_identity. Updated the specs describing
the current API (ADR-091 body keeps local_identity as the decided
name).
- client/README.md: dial_iroh description said the local key is
"extracted from creds.local_identity" — the code uses the pre-built
iroh endpoint's key (set at with_iroh time) and reads only
creds.remote_identity for the NodeId. Fixed.
- overview.md: said core has "no quinn/iroh deps" — core keeps
quinn/iroh for Connection::from_quinn/from_iroh. Fixed.
- call/client-and-adapters.md: a /// doc-comment block and
pub struct RemoteIdentity were floating outside any code fence
(orphaned closing backticks). Fixed.
- tls/README.md: TlsError sketch shows the full ADR-088 6-variant
enum; the code has a simplified 3-variant enum. Added an
implementation note flagging the divergence; ADR-088 shape kept as
target.
- call/README.md: review note said "ADR-029 migration pending" (stale
— migration landed). Updated to reflect phase 5 completion (pure
protocol crate, no TLS/transport deps, verified against Cargo.toml).
Migration framing removed (present-state descriptions instead):
- tls/README.md: "What moves from" tables -> module-contents tables;
"Implementation ordering / greenfield" section removed; "after the
refactor" section -> "What AlknetEndpoint does"; references to
extraction-source files (alknet-core/src/endpoint.rs,
alknet-call/src/client/call_client.rs) replaced with current file
locations (alknet-tls/src/{server,client,pem,signing}.rs).
- endpoint/README.md: "was two things welded" framing removed;
"after the extraction" section -> "What alknet-core looks like".
- core/endpoint.md: "Historical summary" section removed; clean
deprecation pointer.
- README.md, overview.md, open-questions.md: dates + present-tense
cleanup.
SendStream implements only AsyncWrite, RecvStream implements only
AsyncRead. The QuicStream wrapper (44 lines) is a necessary adapter
that combines the split pair from accept_bi() into a single
AsyncRead+AsyncWrite type. Phase 6 is deferred — removing it would
require a new BidiStream abstraction or restructuring HttpAdapter::handle.
- Fix formatting in endpoint.rs
- Mark tests.md and review-endpoint.md as completed
- All 9 endpoint tasks are now complete
- Workspace: all tests pass, clippy clean, fmt clean
- Add 7 registry tests (handler_registry_*) to registry.rs
- Add 4 dispatch tests (build_auth_context_*, dispatch_decision_logic_*) to dispatch.rs
- Add 6 endpoint tests to endpoint.rs:
- debug_for_alknet_endpoint_is_implemented_without_panicking
- endpoint_constructs_with_iroh_raw_key_identity (adapted for new API)
- iroh_endpoint_runs_accept_loop_and_shutdown (adapted for new API)
- with_iroh_sets_field (replaces has_iroh_identity_true_for_raw_key)
- without_iroh_field_is_none (replaces has_iroh_identity_false_for_x509)
- endpoint_works_without_iroh (replaces has_iroh_identity_false_when_no_identity)
- Add async-trait as dev-dependency
- All 17 tests pass with iroh feature
- All 8 tests pass without features
- Workspace tests all pass (no breakage)
- Mark tasks 1-7 as completed
All six tasks are done:
- core/connection-credentials: ConnectionCredentials + RemoteIdentity in alknet-core
- tls/crate-init: alknet-tls crate skeleton with deps and feature flags
- tls/server-extract: server-side TLS code extracted into alknet-tls
- tls/client-extract: client-side TLS code extracted into alknet-tls
- tls/tests: 34 TLS tests moved and adapted into alknet-tls
- tls/review-tls: spec conformance review passed, all feature combos green
Phase 1, Task 3 of crate extraction. Extracts client-side TLS setup code
from alknet-call/call_client.rs into alknet-tls:
- client.rs: TlsClientConfig, build_client_auth, select_server_verifier,
load_platform_root_cert_store, FingerprintPinVerifier,
RawKeyClientCertResolver, NoClientCertResolver
- load_platform_root_cert_store includes webpki-roots fallback (ADR-088 §5)
- Reuses shared Ed25519SigningKey from signing.rs and load_cert_chain/
load_private_key from pem.rs
- All error returns use TlsError (not String)
- webpki-roots 0.26 with TrustAnchor-based fallback
Old code in call_client.rs stays (duplicated). No breakage.
Phase 0 of crate extraction. Purely additive — adds two small types
(ConnectionCredentials, RemoteIdentity) to a new credentials.rs module
in alknet-core. No other crates touched. All workspace tests pass.
ADR-091 amended 2026-07-17: CallCredentials removed (not retained in
alknet-call). Trace showed CallCredentials.auth_token had no reader
(connect() read only tls_identity + remote_identity; spawn_dispatch
takes no credentials; from_call's credentials_auth_token was a
different type, always None, never connected). auth_token is a
per-request payload field — browsers send it in the WS payload; the
HTTP gateway resolves bearer → Identity at its boundary.
from_call's credentials_auth_token dead path removed in the same pass
(OpSummary field, handler params, build_forwarded_payload param, and
the two tests asserting the never-exercised Some path).
ADR-089 §5 further amended, ADR-080 noted, all spec READMEs and
overview updated. Migration plan (findings.md) corrected: Phase 5
prune now includes CallCredentials removal + from_call dead-path
removal; test audit corrected (4 unchanged + 2 move to core, not 6
unchanged); integration-test split documented; all 'or' hedges
resolved.
The Phase 5 'call_client.rs after prune' list inaccurately described
ConnectionCredentials as 'removed from call_client.rs' — it is a new
type in alknet-core, not a pre-existing type in call. Reworded to:
RemoteIdentity is removed (moved to core in Phase 0); CallCredentials
is restructured (transport dimensions leave for ConnectionCredentials
in core, auth_token stays). The source map table row for lines 40-88
is also corrected: the destination is a split, not a uniform move to
alknet-core.
ADR-091: ConnectionCredentials — decouple the dial credential bundle
from the call protocol. CallCredentials (call-protocol-level, carries
auth_token) was being used as the dial's credential type, coupling the
dial to the call protocol. The auth_token is a hub-layer identity
correlation mechanism (browsers, alknet/register), not a transport
credential. ConnectionCredentials (transport-level: local_identity +
remote_identity) is the dial's credential bundle; all three dial
signatures unify on &ConnectionCredentials; dial_iroh's node_id
parameter is derived from remote_identity.fingerprint. CallCredentials
stays in alknet-call with auth_token. The shape is validated by the
future dial_ssh pattern (russh's check_server_key + authenticate_publickey
consume the same two dimensions).
Amends ADR-089 §3 (dial signatures) and §5 (move consequence), ADR-087
(input framing). Updates client/tls/call/core crate specs and the
extraction plan's Phase 0/3/4/5.
Migration plan cleanups (findings.md):
- Remove duplicated ordering-rationale bullets (copy-paste artifact)
- TL;DR: six phases -> seven (Phase 0 promoted); four compilable
intermediate states -> each phase leaves workspace compilable
- Remove inline 'Wait — that's 10, not 8' self-correction; fix
Category B header to (10 tests)
- Replace contradictory line ranges in Net Phase 5 test impact with
name-based references
- Decide quinn feature fate: removed (not no-op)
- Note webpki-roots always-present per ADR-088 §5 in Phase 1 dep list
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.
The iroh-proxy POC (/workspace/iroh-proxy-poc, 5/5 runs clean) settled
OQ-67: iroh does NOT expose a socket-injection hook for the IP/direct
transport (noq_endpoint() is pub(crate), the IP transport binds its own
netwatch::UdpSocket, CustomTransport operates on a separate CustomAddr
address space iroh's hole-punching doesn't route through). The quinn
POC's Socks5UdpSocket does not transfer to iroh. The decision: force
relay-only when a proxy is configured, via three stable public iroh
Builder knobs — clear_ip_transports() + addr_filter(relay_only) +
proxy_url. The peer sees the relay's IP; the relay sees the proxy's IP;
the client's real IP is hidden on both surfaces. No iroh fork required.
Because iroh's proxy_url expects an HTTP CONNECT proxy (not SOCKS5),
the integration runs a tiny local HTTP-to-SOCKS5 bridge (~80 lines) so
a single Socks5ProxyConfig covers all three dials uniformly: UDP
ASSOCIATE for dial_quic, CONNECT for dial_tcp_tls, force-relay-only +
HTTP-to-SOCKS5 bridge for dial_iroh.
The POC also corrected a factual error: iroh's proxy_url proxies the
relay WebSocket only, not pkarr/DoH (those use pkarr/hickory-resolver
directly). Acceptable for the force-relay-only config (QAD disabled);
spec text corrected.
Force relay-only forgoes iroh's direct-path latency advantage (negligible
for the hub deployment, which runs its own relay) and makes relay
availability a hard dependency — the intended privacy/availability
tradeoff; a caller that prefers availability over privacy for the iroh
path simply does not set the proxy.
- ADR-090 §5 amended: iroh force-relay-only decision + proxy_url
coverage correction + HTTP-to-SOCKS5 bridge
- OQ-67: resolved (force relay-only)
- client README: iroh proxy row, bridge, limitations, ADR/OQ entries
- README/open-questions: OQ-67 resolved, Current State amendment
Option 2 (force relay-only via clear_ip_transports + addr_filter(relay_only)
+ proxy_url) validated end-to-end: a relay-only proxied iroh client connects
through an HTTP CONNECT proxy to a local relay; selected path is relay, no
direct IP path established, 45-byte echo completes (5/5 runs clean).
Option 1 (SOCKS5 UDP ASSOCIATE over iroh's direct path, the quinn-PoC
analogue) is not feasible without forking iroh: iroh exposes no
socket-injection hook for the IP/direct transport (noq_endpoint is
pub(crate), IpTransport binds its own netwatch::UdpSocket), and the only
public injection surface (unstable-custom-transports CustomTransport)
operates on a separate CustomAddr address space that iroh's hole-punching
does not route through.
Correction to OQ-67 premises: iroh's proxy_url covers the relay WebSocket
(HTTP CONNECT) only, not pkarr/DoH. Recommendation: Option 2 as default,
Option 1 deferred unless a direct-path-privacy use case justifies a fork.
PoC at /workspace/iroh-proxy-poc.
AlknetClient gains an optional SOCKS5 proxy (with_socks5_proxy) so a
native client can hide its real IP from the hub. dial_quic routes QUIC
through SOCKS5 UDP ASSOCIATE (validated by the /workspace/quinn-proxy-poc
PoC — quinn's AsyncUdpSocket + new_with_abstract_socket is the extension
point, 5/5 runs clean); dial_tcp_tls routes through SOCKS5 CONNECT. The
proxy is invisible above the dial (Connection, dispatch, credentials,
TLS config all proxy-unaware) and the no-proxy path is the zero-cost
default (socks5 feature + fast-socks5 dep are opt-in).
SOCKS5 is the sole proxy protocol (covers both TCP and UDP, so no HTTP
CONNECT variant needed). The two distinct SOCKS5 concepts — the
client-dial proxy (ADR-090, transport-layer privacy) and the planned
alknet-socks5 channels data-channel handler (ADR-085 scope, a service
one side offers the other) — compose at the SOCKS5 protocol level
without alknet-type-level coupling.
iroh is the exception: dial_iroh does not consume Socks5ProxyConfig —
iroh's proxy_url covers the relay-exposure surface, but the
direct-connection peer-exposure case is OQ-67 (deferred(unclear) — the
pieces exist but the iroh socket-stack composition isn't clear; does
not block the first hub deployment, which uses QUIC/TCP+TLS).
- ADR-090: Client-Dial SOCKS5 Proxy Seam
- OQ-67: iroh Proxy Support (Direct-Connection Peer Exposure)
- client README: proxy section, struct/builder, Proxy error variant,
socks5 feature gate, deps, assembly example, decisions/open-questions
- README/open-questions: index entries, Current State, OQ count 67/20
Resolves the 'quinn has no proxy support' blocker for QUIC client
connections in alknet-call. Quinn routes every network byte through the
public quinn::AsyncUdpSocket trait, and Endpoint::new_with_abstract_socket
accepts any impl — so a SOCKS5 UDP ASSOCIATE tunnel wrapped as that trait
gives full QUIC-through-proxy support with no fork.
An end-to-end PoC (/workspace/quinn-proxy-poc) confirms a quinn client can
complete a QUIC handshake and exchange stream data through a SOCKS5 proxy
with UDP support (5/5 runs clean, clippy clean). The load-bearing impl is
~250 lines. Integration into alknet-call is ~30 lines in connect() plus one
new module, behind a new optional socks5 feature flag.
Limitations: ECN is lost across the proxy (quinn falls back to non-ECN), and
the proxy must support UDP ASSOCIATE (ssh -D does not). Both are acceptable
for alknet's call-protocol use.
Amend ADR-083 with the crate-extraction decision: the endpoint moves
from alknet-core into a new crate alknet-endpoint, mirroring the
alknet-client extraction (ADR-089). The ADR's shape (new + builder
methods + public dispatch + run/shutdown) is unchanged; only the
location changes.
The extraction is structural pruning, not an inline refactor. The
endpoint is a leaf consumer of core's shared types (zero handler crates
import it; 124 import sites for the other core modules). Extracting it
lets core shed quinn/iroh/rcgen/rustls-acme — handler crates no longer
transitively link those. A pure worker (client-only) does not pull
alknet-endpoint at all. The dep graph is symmetric: alknet-core is the
shared types crate; alknet-endpoint and alknet-client are the
server-side and client-side establishment crates.
New spec: crates/endpoint/README.md (the canonical endpoint spec).
core/endpoint.md is deprecated to a stub. Cross-references updated
across 8 docs (README, overview, tls, hub, client, core README, ADR-083,
ADR-089 references). Architecture review passed (3 critical, 9 warnings
— all addressed).
Extract the deferred AlknetClient as a new crate alknet-client — the
client-side analogue of AlknetEndpoint. Three dial methods (QUIC +
TCP+TLS via TlsClientConfig, iroh via key) produce a Connection for
CallClient::spawn_dispatch / ChannelClient::from_connection to consume.
The deferral collapsed because ADR-086 gave the native endpoint type
three dial shapes within one endpoint type, ADR-087 broke the circular
hedge, and ADR-083 gave the server-side shape to mirror by symmetry.
Names the three concept layers that were tangled throughout the initial
development (deployment role / establishment side / ALPN-level category)
so the fix is legible. Names alknet/register as a dialable entry-point
ALPN (native registration, parallel to HTTP registration in OQ-58); its
wire protocol is deferred to OQ-66 (blocked on OQ-58's token model).
Cross-references updated across 11 existing docs (README, overview,
open-questions, OQ-55, tls, hub, core, channels README/overview/
channel-client, call client-and-adapters) to reflect OQ-55 resolved and
the new alknet-client crate. Architecture review passed (2 critical, 7
warnings — all addressed).
TLS spec review before task decomposition. The client side was
under-specified relative to the server side — fixed:
- W1+W2: TlsClientConfig gets the full accessor API (for_quinn,
for_tcp_tls, rustls_config) mirroring TlsServerConfig, plus the
local TlsIdentity input for client-auth cert presentation. Both
clients (call + channels) consume it via the same three accessors.
- W3: Client-side extraction table for call_client.rs items that move
to alknet-tls, including the Ed25519SigningKey / load_cert_chain /
load_private_key duplicates that consolidate into one copy.
- W5: Server-side rustls_config() doc comment no longer claims iroh
uses it (iroh reads the key directly).
- S6: Dropped 'remote cert type' from ClientVerifierContext — it
doesn't drive any construction decision.
- S7: Implementation ordering note (tls first, then endpoint refactor,
then assembly) — the call sites don't exist until step 2/3.
- N8: Noted alknet-core's acme feature + deps become vestigial.
- Flagged client spec work for the next session (two clients: call +
channels; same TlsClientConfig shape; prerequisite for first hub).
- Advanced ADR-082/083 to Accepted; TLS README to reviewed.
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).
Address the root cause of rework-causing hedging: the architect was
put in a logical bind where it couldn't express justified uncertainty
('the pieces exist but the shape isn't clear yet'). The only options
were 'decide now' (premature) or 'deferred(scope)' (false — the
information isn't missing, it's un-synthesized). The agent picked
deferred(scope) with a circular blocking condition (OQ-64 blocked on
OQ-55, OQ-55 needs OQ-64) because there was no honest way to say 'I
can see the pieces but I can't see the shape.'
Changes to the architect role spec:
- Add deferred(unclear) state: the pieces exist but the composition
isn't clear; resolution requires investigation (work through
examples, POC), not waiting. Has an investigation target and an
impacts field.
- Add 'Impacts' field to the OQ format: what does this block
downstream? Be specific ('blocks the first hub deployment because
the hub dials workers' not 'blocks the hub crate'). The triage
signal that makes deferral urgency visible — the field that would
have made the AlknetClient circular hedge visible.
- Add circular-reasoning guard to self-review: 'check that your
blocking condition isn't a prerequisite of the thing you're
deferring.'
- Trim anti-patterns #9-#11 (hedging synonyms catalog, ~40 lines):
detection belongs in the reviewer, not the architect's self-review.
The architect is too close to its own reasoning to see its own
circular hedges.
- Trim door-types section (30→10 lines): keep the one-paragraph
summary, cut the elaboration.
Changes to the architecture-reviewer role spec:
- Add Decision Quality (F) category: false-deferral check
distinguishing three cases — (1) hedging on a resolved decision, (2)
false deferral / circular hedge (the blocking condition is a
prerequisite of the thing being deferred), (3) legitimate deferral.
- Add Impacts Field Coverage (G) category: check that unresolved OQs
have specific impacts fields.
- Note: the Decision Quality category is often the highest-value
check on poorly-defined projects — the architect cannot self-review
it (circular reasoning is invisible from inside the circle).
Retrofit existing OQs:
- Add Impacts field to all 16 unresolved OQs (10 deferred, 6 open).
- Update OQ-63 (TlsError shape) to reflect ADR-087's client-side
addition — the error type now covers both server and client
variants.
- Move OQ-65 (WebSocket carrying channels) to alknet-http theme
(done in prior commit; this commit adds its impacts field).
- Verified: no circular reasoning found in existing deferrals. The
AlknetClient hedge (OQ-64) was the circular one; it's already
resolved by ADR-087.
OQ-65 is about the WebSocket browser path — an alknet-http concern. It
surfaced during the TLS/ALPN-list discussion in passing (because the
web config's ALPN list needed to account for whether alknet/channels
is advertised), but the question itself lives with the WebSocket
spec, not with TLS or the hub. Remove from alknet-hub and alknet-tls
theme tables; add to alknet-http.
OQ-64 and OQ-55 were linked in a circular dependency: the client-side
TLS config was deferred behind the dial seam (OQ-55), but the dial
needs the TLS config. No second transport can dial until it has a TLS
config; the TLS config was deferred until a second transport dials.
Schrödinger's code — required and not required until observed.
ADR-087 breaks the circle by separating two concerns that were
conflated as 'the same seam':
1. TlsClientConfig — rustls::ClientConfig + ADR-034 verifier selection
+ ADR-084 crypto provider. Transport-agnostic. All decisions made.
Buildable today. A PREREQUISITE for any dial, not a consequence of it.
2. The dial (AlknetClient::dial()) — transport-specific connection
establishment. Extracting a transport-polymorphic dial from one
shape (QUIC) would bake QUIC in. Legitimate deferral (OQ-55,
unchanged).
The hub makes this non-optional: a hub dials out to workers it
supervises and to other hubs (hub-as-client). The first hub deployment
(web + native) dials workers over QUIC with the worker's fingerprint
pinned. There is no 'later' for the TLS config — it is on the critical
path for the first hub and for alknet-worker.
Changes:
- ADR-087: TlsClientConfig in alknet-tls, not blocked on OQ-55
- OQ-64: resolved (yes, alknet-tls provides TlsClientConfig)
- OQ-55: amended — only the dial seam is deferred; TLS client config
is explicitly NOT part of the deferral
- TLS README: 'Server-only (for now)' section replaced with
TlsClientConfig section; crate is no longer server-only
- Hub README: dial/supervision section references TlsClientConfig for
outbound connections
Name the three-endpoint-type model (web/native/iroh) and the
entry-point vs. endpoint ALPN distinction. This untangles the
hub/endpoint/ALPN-config confusion that OQ-62 hinted at:
- A hub composes a SUBSET of three endpoint types (web, native, iroh),
each with its own identity model, auth model, and transport(s). A
full hub runs all three; a minimal hub runs iroh alone (no public
IP required). The first real use case is web + native. Corrects the
hub README's 'must support TCP+TLS and QUIC' framing.
- ALPNs split into entry points (accepted without identity — h2,
http/1.1, future alknet/register) and endpoints (identity required
before dispatch — alknet/channels, alknet/call, alknet/ssh). This
resolves OQ-62: split ALPN lists by endpoint type (Option B), because
each endpoint type serves a different client class with different
negotiable ALPNs. The assembly-layer wiring pattern is now guessable.
- Foundational handlers are two categories, not one: channels
data-channel ALPNs (tunnel, socks5, fs, sftp — gated by channels,
not in any TLS ALPN list) vs. SSH (an endpoint ALPN that wraps
channels inside it, RFC 7250 keys, legacy compat, comes later).
Files OQ-65 (WebSocket carrying channels — the browser story update)
filed as a one-way-door question, open. ADR-048 is not superseded;
OQ-65 may extend it. The web config advertises alknet/channels by
default so the hub is ready if OQ-65 resolves to 'WebSocket carries
channels.'
ADR-085 records the actual workspace scope: the mono-repo is the core
networking toolkit (substrate: core, tls, call, channels; deployment
shapes: hub, worker; foundational handlers: tty, http, ssh, tunnel,
socks5, fs, sftp; vault). Consumer repos (docker, agent) are separate
repos depending on the published core crates. The overview's crate
graph had been describing the wrong scope since ADR-003 — a flat
~12-crate workspace including DNS/messaging/NAPI while omitting
channels, hub, worker, and tls. This stale scope was a causal factor
in the 'assembly layer' hedging pattern: when the overview implies
everything lives in one repo but the architecture needs a hub/worker
composition layer not in the graph, the gap gets filled with
'assembly layer' as an escape hatch. The overview is rewritten to
match the real boundary.
TLS spec review fixes (from architecture review):
- C3: hub/worker/hub-worker terminology pointers (tls README + endpoint.md)
- W1: server-only statement + OQ-64 (client-side TLS helper, deferred)
- W2: ACME task lifecycle semantics (returns immediately, no first-cert await)
- W3: remove stale EndpointError::TlsConfig variant
- W4: update stale ALPN section for two-config hub
- W5: add alknet-tls to hub dep graph (assembly-layer dep)
- W6: trim inline rationale -> point to ADR-084
- W7: ADR-084 status dependency note
New open questions:
- OQ-62: ALPN list sharing for two-config hub (open, high)
- OQ-63: TlsError shape (open, high)
- OQ-64: client-side TLS helper (deferred, blocked on OQ-55)
OQ-59 resolved to Option A: 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 parsing; the
rustls::sign usage is a test helper only. alknet-tls re-exports the
fingerprint functions for convenience.
ADR-084: aws-lc-rs as the TLS crypto provider on all server + client
config paths. Records the decision that was already in the code (to
match iroh's tls-aws-lc-rs feature) but had no ADR. FIPS-capable, broad
platform support, consistent across quinn/iroh/TCP+TLS/client. Switching
to ring or process-default requires a new ADR. ADR-082's
behavior-preservation invariant now references ADR-084 for the decision
record.
ADR-083 revised: TCP+TLS moves from an external sibling loop calling
public dispatch to a first-class owned transport via
with_tcp_tls(listener, acceptor), running inside run() alongside the
quinn and iroh accept loops. The endpoint owns all its accept loops;
shutdown() stops them all. The multi-owner shutdown problem (OQ-61)
does not arise — dissolved.
The reason TCP+TLS was structurally excluded (ADR-010 Am. 1: the
endpoint built transports internally, TCP+TLS couldn't fit) is gone
after ADR-083 — the endpoint no longer builds transports; it runs
accept loops on whatever it's given. TCP+TLS is a listener transport,
same shape as quinn and iroh. ADR-010 Amendment 2 supersedes Am. 1's
struct-level exclusion.
dispatch stays public — but for genuinely external shapes (SSH channels,
future WebTransport streams), which are connection-internal multiplexing,
not listener transports. The listener-vs-multiplexing distinction is now
explicit.
OQ-60 resolved: the TCP+TLS loop lives in alknet-core behind a tcp
feature (owned by the endpoint); builder functions are inlined by the
assembly layer. A alknet-transport crate was rejected — it would contain
only trivial builders; the real component (the loop) is in core. Hub-
specific composition lives in the hub crate; transport runtimes that any
node might need live in core.
Updated: ADR-010 (Amendment 2), ADR-082 (TCP+TLS loop location), ADR-083
(revised), core/endpoint.md (struct + dispatch + shutdown), hub/README.md
(transport table + assembly example + stale sibling references),
tls/README.md (endpoint section + TCP+TLS loop location + references),
open-questions.md (OQ-60 resolved, OQ-61 dissolved).
Review: zero critical issues, five warnings fixed (stale hub README
prose, stale core endpoint.md struct/dispatch listings, stale ADR-082
TCP+TLS loop location, stale TLS README reference entry, hub front-matter
date).
ADR-083: AlknetEndpoint becomes a pure accept-loop runner with a public
dispatch method. Transport construction moves out of the endpoint — the
assembly layer reads StaticConfig, builds transports from
TlsServerConfig(s), and hands pre-built quinn/iroh endpoints to the
endpoint via with_quinn/with_iroh. TCP+TLS dispatch is first-class (same
dispatch path as quinn/iroh); ADR-010 Amendment 1's duplicated-dispatch
workaround is retired. StaticConfig stays in core as the assembly-layer
config; the endpoint takes only drain_timeout.
The acme-tls/1 guard moves from dispatch_quinn to the shared dispatch
method — ACME TLS-ALPN-01 challenges arrive over TCP (CAs validate via
TCP to port 443, not QUIC), so the guard's quinn-specific location was a
latent bug once TCP+TLS exists. The guard is transport-agnostic; the
rationale (no handler, silent close) is unchanged. ADR-027 §5 amended.
OQ-60: where build_iroh_endpoint lives (assembly layer / alknet-tls
helper / transport module). build_quinn_server_config_from_rustls is
decided — it moves to alknet-tls as for_quinn() per ADR-082; only
build_iroh_endpoint is genuinely undecided.
OQ-61: multi-owner shutdown coordination. Boundary committed (endpoint
owns dispatched handlers; assembly layer owns spawned accept loops);
mechanism open.
ADR-082 amended: drops the Arc<TlsServerConfig> endpoint signature
(superseded by ADR-083); keeps its scope as the alknet-tls crate's
TlsServerConfig + accessors.
Review: zero critical issues, five warnings fixed (build_iroh_endpoint
destination contradiction, undeclared shutdown_sender, missing ADR-027
amendment marker, underspecified dispatch no-match behavior, stale
door-type timing clause).
The endpoint refactor findings doc relegated TCP+TLS to a 'sibling
accept loop' that duplicated the endpoint's dispatch logic at the
assembly layer. That was ADR-010 Amendment 1's workaround for the
endpoint being welded to quinn — not a deliberate design.
The endpoint now exposes a public dispatch() method so every transport
(quinn, iroh, TCP+TLS, future SSH, future WebTransport) calls the same
dispatch path: handler lookup, build_auth_context, spawn. The
transport-specific parts (ALPN extraction, fingerprint extraction,
Connection construction, no-handler close) happen before dispatch is
called. No duplicated logic at the assembly layer.
Also: the acme-tls/1 guard is quinn-specific (ACME challenges arrive
over QUIC); a TCP+TLS listener serving HTTPS does not advertise
acme-tls/1. build_auth_context becomes a private helper called by
dispatch, not a standalone function.
Captures the full picture of the AlknetEndpoint refactor interleaved
with the alknet-tls extraction:
- AlknetEndpoint becomes a pure accept-loop runner (no transport
construction, no StaticConfig, no tls_identity reading)
- A hub holds one or two TlsServerConfigs (raw key + X.509), not one;
cert-reuse is within each identity, shared across that identity's
transports
- TCP fallback for native clients uses the raw-key config (no X.509
needed); raw-key/X.509 mixing works in the TLS handshake
- iroh and SSH share the Ed25519SecretKey, not the TlsServerConfig
- The reverse-proxy as reference for ACME lifecycle and live renewal
- The remaining tls spec review items (C2-C6, W3-W5) carried forward