The hub README predates channels (2026-07-09) and was built on the
call-protocol-directly-over-QUIC model: 'One QUIC connection per peer
carrying the call protocol,' CallClient::connect as the dial path,
QUIC as the only transport. The channels crate replaced that substrate
(ADR-071/079/080), and the hub's primary use case (worker provisioning
on docker/vast.ai/runpod) requires TCP+TLS for registration and QUIC
or TCP+TLS for the ongoing session — coexisting on one hub, not as
alternatives.
Rewrite:
- Multi-transport stated as decided: 'A hub MUST support TCP+TLS and
QUIC endpoints simultaneously.' TCP+TLS accept loop (ADR-010 Am. 1)
lives in the hub, shares the HandlerRegistry with the quinn endpoint.
- Channels substrate: the hub uses ChannelsAdapter/ChannelClient
(from_connection primary, connect_quic convenience — ADR-080),
not CallClient/CallAdapter directly. One channels connection per
peer; CallAdapter runs on channel 0 (ADR-072).
- Transport-agnostic dial/accept: dial_worker_connection takes a
Connection (one-way door); connect_quic_worker is a two-way-door
convenience. supervise_worker takes a dial closure, not a SocketAddr.
- Identity over transports: fingerprint path (QUIC+raw-key,
X.509 client cert) via resolve_from_fingerprint; bearer-token path
(TCP+TLS no client cert, WebTransport, WebSocket) via
resolve_from_token on the call first frame. Both resolve to the
same PeerEntry (ADR-030/034).
- Worker registration flow (6 steps): provision → token → download →
key gen → HTTP POST over TCP+TLS → channels connect. Step 4 is HTTP
on HttpAdapter; step 6 is channels over QUIC or TCP+TLS. The hub
creates a mixed-fingerprint PeerEntry (ADR-034 §3) at registration.
- OQ-58: worker registration flow — enrollment-token model,
endpoint shape, register_worker API. Open (decision-ready, not
blocked), one-way door, high priority.
- ADR-081: fix stale references to channels-hub/channels-worker
sub-crates (the ADR's Decision says they don't exist; the References
section said they did).
Review (architecture-reviewer): 3 critical (ADR-081 stale refs;
bearer-token extraction flow; assembly example QUIC-only +
unspecced into_connection), 7 warnings (registration 'or'→'both',
channel/control translation, OQ-52 interim, HubError variant naming,
RegistrationError definition, X.509-client-cert prose, adapter
ownership phrasing), 5 suggestions. All criticals and warnings #4/#7/#8
addressed; #5/#6/#9/#10 addressed; #11-15 noted as optional.
Each open question lives in its own file under questions/,
named NNN-slug.md (mirroring the ADR convention). This file is the index:
theme-grouped tables for scannability, plus a cross-theme
Deferred / Blocked section that surfaces the
safe-exit deferrals with their blocking conditions inline — so "what's
currently parked and why" is answerable at a glance.
Status values:
open — Needs to be resolved now. Has a clear path to resolution.
resolved — Decided. The resolution is stated cleanly, without caveats about how it could be changed later.
deferred(scope) — Cannot be resolved yet. The information doesn't exist. Has a concrete blocking condition (e.g., "blocked on: alknet-agent crate spec"). Not a failure — scope management.
partially resolved — Some aspects decided, others deferred or open.
dissolved — The question was reframed out of existence (e.g., superseded by an ADR that retires the premise). Kept for reference.
Door type classifications follow ADR-009 — they describe reversal cost (how expensive it is to undo), not urgency:
One-way door: Reversal requires rewriting significant code or permanently closes a capability. Getting it wrong is expensive — requires ADR before implementation.
Two-way door: Reversal is cheap or additive. Getting it wrong is recoverable — decide, implement, revert if needed.
Door type is separate from whether a decision is made. A two-way door is a decision you make now and can revert later, not a decision to defer. See ADR-009 §"What this framework is NOT."
The safe-exit visibility surface. These questions are parked because the
information needed to resolve them does not exist yet — each has a concrete
blocking condition. They are not failures; they are scope management. See
ADR-009 §"Safe Exit: Deferred Decisions." This section exists so "what's
currently blocking the architect" is answerable at a glance, not by
filtering the tables above.
OQ-09: WASM Target Boundaries
Blocked on: A concrete server-side WASM use case, or a deliberate confirmation that WASM stays a client-side design constraint. Tracked as architecture/oq-09-wasm-server-use-case in tasks/architecture/.
Priority: low
Amendment (2026-07-09): The Connection door is now open via Connection::from_stream (ADR-065) — a Connection can be constructed from any wasm-compatible stream. What remains closed is the accept-loop runtime (tokio::spawn does not run on WASM; PendingRequestMap/CallAdapter use tokio channels). The blocking condition (a concrete server-side WASM use case) is unchanged.
OQ-10: Git Adapter Scope — Smart Protocol Only or Full Server?
Blocked on: Speccing the alknet-git crate — resolve this when that crate is specified, not deferred past it. Tracked as architecture/oq-10-git-adapter-spec in tasks/architecture/.
Blocked on: A handler that needs stream operators and finds the existing combinators (Box::pin(stream::iter(...)), async_stream::stream!, futures::stream) insufficient. The operators library is a convenience, not a prerequisite for any handler.
Blocked on: a concrete use case for network or volume management over the call protocol. Dev containers use the default bridge network; hosted services declare networks/volumes in docker compose.
Blocked on: v1 implementation — the create input JSON Schema is finalized when register_docker_ops is written and tested against bollard's Config struct. An architectural decision (ADR-060 §5), not a deferral past implementation.
Blocked on: a second transport's real dial existing (not just a
second QUIC dial). The dial is transport-specific (QUIC, HTTP, TCP+TLS,
WebTransport, raw TCP); we have one shape implemented (QUIC —
CallClient::connect and ChannelClient::connect_quic). Extracting a
QUIC-shaped connector now would bake QUIC in as the establishment
shape — the same welding ADR-065 unwound on the server side. The blocking
condition is met when a non-QUIC dial (SSH raw-TCP, HTTP-wrapped call,
TCP+TLS) exists, so the transport-polymorphic dial+TLS seam is
extractable from two different transport implementations. Note: the
client APIs are already transport-agnostic — CallClient::spawn_dispatch
and ChannelClient::from_connection (ADR-080) take a pre-established
Connection. What is deferred is the shared dial, not the client
protocol surface.
Blocked on: a real deployment observes head-of-line blocking on a
saturated channel where the bounded-buffer's stop-reading mitigation is
insufficient (e.g., a high-throughput file transfer over a tunnel that
saturates a channel and causes frequent demux stalls affecting other
channels). The intended use cases (TTY, SSH, tunnels) are not
high-throughput in the HOL-blocking sense; the trigger requires a
high-throughput use case.
Blocked on: a second two-pump handler existing (the tunnel handler is
the first; SSH direct-tcpip will be the second), so the shape
convergence is observable. Extracting the helper from one consumer would
bake in a shape that the second might not fit. The shutdown-on-completion
contract is decided (ADR-078); only the helper extraction is deferred.