docs(arch): TCP+TLS as first-class owned transport — resolves OQ-60, dissolves OQ-61

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).
This commit is contained in:
glm-5.2 committed 2026-07-14 08:58:50 +00:00
1 parent 81bde6f28f
commit 2abe8f1872
10 files changed
+524 -308

No files matched your search

@@ -2,49 +2,69 @@
- **Origin**: `docs/architecture/decisions/083-endpoint-as-accept-loop-runner.md`
(the endpoint refactor commits to the boundary — construction is not
in the endpoint — but not the location of `build_iroh_endpoint`).
- **Status**: open
- **Door type**: one-way (where `build_iroh_endpoint` lives determines
who depends on `iroh` for transport construction; moving it later
churns the dep graph and every binary's assembly code)
in the endpoint — but not the location of transport construction).
- **Status**: resolved
- **Door type**: one-way (where `build_iroh_endpoint` and the TCP+TLS
loop live determines who depends on `iroh` / `tokio-rustls` for
transport construction; the dep-graph shape is structural)
- **Priority**: high (the hub is the first multi-transport consumer;
its assembly code sets the pattern)
- **Blocked on**: nothing structural. The three options are clear; the
decision is a trade-off, not a missing capability.
- **Scope**: This OQ covers **`build_iroh_endpoint`** — the function
that reads `StaticConfig` and builds an `iroh::Endpoint`.
`build_quinn_server_config_from_rustls` is **decided** (it moves to
`alknet-tls` as `TlsServerConfig::for_quinn()` per ADR-082 — it's a
thin wrapper over a `rustls::ServerConfig`, which `alknet-tls` owns).
Only `build_iroh_endpoint` is genuinely undecided: it reads
`StaticConfig` (not a rustls config), builds an `iroh::Endpoint` from
an `Ed25519SecretKey` + relay URL, and doesn't fit the `alknet-tls`
cert-provider boundary.
- **Resolution**: Not yet decided. The options:
- **Resolution**: Split answer (resolved by ADR-083 revision, 2026-07-14):
**Option A: In the assembly layer (the binary).** Each binary reads
`StaticConfig` and hand-assembles quinn/iroh/TCP+TLS. Pro: maximal
flexibility, core stays lean. Con: every binary duplicates the
"build an iroh endpoint from an `Ed25519SecretKey` + relay URL"
boilerplate; a 10-step procedure is copy-pasted per binary.
**TCP+TLS accept loop → `alknet-core` behind a `tcp` feature (owned
by the endpoint).** The endpoint takes a `TcpListener` +
`TlsAcceptor` via `with_tcp_tls(listener, acceptor)` and runs the
accept loop inside `run()` alongside the quinn and iroh loops. TCP+TLS
is a listener transport — same shape as quinn and iroh (accept →
extract ALPN + fingerprint → `Connection::from_bidi` → `dispatch`).
Making it owned gives the endpoint a single uniform ownership model:
it owns all its accept loops, `shutdown()` stops them all. The
multi-owner shutdown problem (OQ-61) does not arise. Any node that
wants TCP+TLS enables the `tcp` feature and calls `with_tcp_tls` — no
hub dependency. This reverses ADR-010's "TCP is not an endpoint struct
concern": the reason TCP was excluded (the endpoint built transports
internally, TCP+TLS couldn't fit) is gone; the endpoint is now a
multi-transport accept-loop runner and TCP+TLS fits the same shape.
**Option B: In `alknet-tls` as convenience helpers.** `alknet-tls`
gains a `build_iroh_endpoint` helper. Pro: one place. Con: `alknet-tls`
then depends on `iroh`, bloats a crate whose stated job is "TLS setup,
not transport endpoint construction" (ADR-082 scopes `alknet-tls` to
cert config and its accessors — `build_iroh_endpoint` doesn't touch
certs, it touches iroh's relay/secret-key APIs).
**Builder functions (`build_iroh_endpoint`, `build_quinn_endpoint`,
`build_tcp_tls`) → inlined by the assembly layer.** These are trivial
API calls (2-15 lines each, pure configuration, no shared logic). The
assembly layer (the deployment binary — today primarily the hub
crate's composition code) inlines them. No helper crate or module —
20 lines total across all three. A `alknet-transport` crate was
considered and rejected: it would contain only trivial builders (the
real component, the TCP+TLS loop, is in core). Hub-specific transport
helpers, if they accumulate, live in the hub crate, not a generic
transport crate.
**Option C: A new crate or module — `alknet-transport` / an
`alknet-core::transport` module — that owns transport construction.**
Pro: clean separation (TLS = certs, transport = endpoints, endpoint =
dispatch). Con: another layer in the dep graph.
**`dispatch` stays public** — but for genuinely external shapes (SSH
channels, future WebTransport streams), not for TCP+TLS. The
distinction: listener transports (quinn, iroh, TCP+TLS) produce
connections from an accept loop the endpoint owns; multiplexing
transports (SSH, WT) produce connections from within an existing
connection, and the endpoint can't own their accept loop.
The ADR-082 boundary ("`alknet-tls` is the cert provider, not the
transport constructor") is a strong argument against Option B. Option C
is the cleanest separation but adds a layer. Option A is simplest but
risks per-binary duplication that drifts.
- **Cross-references**: ADR-083 (endpoint refactor), ADR-082
(`alknet-tls` — the cert provider boundary; `for_quinn()` is in scope,
`build_iroh_endpoint` is not), ADR-010 (original endpoint design,
where construction was welded to the endpoint)
**Why not the hub crate for the TCP+TLS loop:** "the assembly layer"
is, in practice, usually the hub. But a hub-worker serving TCP+TLS
shouldn't depend on the hub crate for a transport loop. The loop
belongs in core (behind a feature), where any node can use it. The hub
crate owns hub-specific *composition* (wiring adapters, relay, peer
lifecycle), not transport runtimes.
**Why not `alknet-tls`:** `alknet-tls`'s stated job is TLS setup —
`rustls::ServerConfig`, cert resolvers, ACME (ADR-082). The TCP+TLS
accept loop is transport runtime, not TLS setup. It calls
`endpoint.dispatch()`, which is core's API. Adding it to `alknet-tls`
would make a cert-provider crate depend on the endpoint's dispatch API
and own a transport runtime — a category error.
**Why not `alknet-transport` (new crate):** The real component (the
TCP+TLS loop) is in core. What's left for a transport crate is trivial
builder functions. A crate for 20 lines of API calls doesn't earn its
existence. If future transport runtimes accumulate that don't fit
core, a transport crate can be created then — but not speculatively.
- **Cross-references**: ADR-083 (endpoint refactor — the revision that
resolved this), ADR-082 (`alknet-tls` — the cert provider boundary),
ADR-010 (original endpoint design — "TCP is not an endpoint struct
concern" is revised), OQ-61 (dissolved — the multi-owner shutdown
problem does not arise with TCP+TLS owned by the endpoint)
@@ -2,41 +2,28 @@
- **Origin**: `docs/architecture/decisions/083-endpoint-as-accept-loop-runner.md`
(the endpoint owns dispatched handlers; the assembly layer owns
spawned accept loops — coordination between them on shutdown is
spawned accept loops — coordination between them on shutdown was
unspecified).
- **Status**: open
- **Door type**: two-way (the coordination mechanism is an
implementation detail; the ownership boundary — endpoint owns
dispatched handlers, assembly layer owns spawned accept loops — is
the one-way part, committed in ADR-083)
- **Priority**: medium (matters for clean hub shutdown; doesn't block
the endpoint-shape decision or the TLS extraction)
- **Blocked on**: nothing structural. The question is which
coordination primitive (shared `shutdown_sender`, separate channel,
drain semantics) — a design choice, not a missing capability.
- **Resolution**: Not yet decided. The boundary is clear:
- **Status**: dissolved
- **Door type**: two-way (the coordination mechanism was an
implementation detail)
- **Priority**: medium
- **Resolution**: Dissolved by ADR-083 revision (2026-07-14). The
problem this OQ tracked — coordinating shutdown between the endpoint's
owned loops and external TCP+TLS accept loops — does not arise. The
TCP+TLS accept loop is now an owned transport (via `with_tcp_tls`),
not an external sibling. The endpoint owns all its accept loops
(quinn, iroh, TCP+TLS); `shutdown()` stops them all and drains all
dispatched handlers. One owner, one shutdown. The `dispatch` method
stays public for SSH channels and future WebTransport streams, but
those are connection-internal multiplexing callers, not listener
loops with independent shutdown needs — they call `dispatch` on
connections the endpoint already accepted and dispatched a handler
for.
- The endpoint owns shutdown of **dispatched handlers** (it spawned
them via `tokio::spawn` in `dispatch`).
- The assembly layer owns shutdown of the **accept loops it spawned**
(the TCP+TLS listeners).
The open question is the **coordination mechanism**:
- Does the assembly layer use the endpoint's `shutdown_sender()` to
signal the TCP+TLS loops, or a separate channel?
- Does `endpoint.shutdown()` drain in-flight dispatched handlers
(including those from TCP+TLS loops), or only the quinn/iroh ones
it spawned via `run()`?
- What happens to in-flight `dispatch` calls after the endpoint is
shut down — are they rejected, or do they complete?
A likely shape: the assembly layer uses the endpoint's
`shutdown_sender()` for all its accept loops (one signal, all loops
stop accepting); the endpoint's `shutdown()` drains all dispatched
handlers regardless of which transport spawned them (the endpoint
owns them all once `dispatch` is called). But this needs to be
written down and verified against the drain semantics.
- **Cross-references**: ADR-083 (endpoint refactor — ownership
boundary), ADR-010 (original shutdown design — single-owner, the
simpler case being generalized)
The premise of this OQ (external TCP+TLS loop + endpoint-owned
dispatch = multi-owner shutdown) was a consequence of TCP+TLS being
external. ADR-083's revision made TCP+TLS internal, retiring the
premise.
- **Cross-references**: ADR-083 (endpoint refactor — TCP+TLS is now
owned), OQ-60 (resolved — the TCP+TLS loop lives in core)