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)
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).
Three code commits landed a clean sweep discovered when building an
external app against the crates. This syncs the architecture specs to
match the codebase and amends the affected decisions.
ADR-064 (supersedes ADR-005): irpc was never integrated — no .rs file in
the workspace ever imported it. The wire protocol (wire.rs) is hand-rolled
length-prefixed JSON; the EventEnvelope shape was derived from the
@alkdev/pubsub TypeScript prior art (ADR-013), not from irpc. ADR-005's
premise ('irpc as the call protocol foundation') was never implemented as
stated. Superseded with a clear header; the body is kept as historical
record.
ADR-065: Connection::from_stream / from_bidi — Connection now accepts any
AsyncRead + AsyncWrite pair via ConnectionKind::Stream (yield-once
accept_bi contract: QUIC yields many streams, everything else yields one
then ConnectionClosed). Unblocks TCP+TLS, SSH channel dispatch,
WebTransport streams, and wasm streams through the same HandlerRegistry
as QUIC connections, with zero handler code changes. MockConnection /
ConnectionKind::Mock removed (tests use from_stream with sink/empty).
Stream-level Mock variants renamed to Stream (they were already generic).
Amended ADRs: ADR-003 (irpc removed from dep table), ADR-007 (from_stream
opens the server-side door; MockConnection removed), ADR-010 (TCP+TLS can
now dispatch through the registry via from_bidi; iroh 1.0 migration noted).
Updated specs: core-types.md (Connection/SendStream/RecvStream sections),
endpoint.md (TCP section, iroh note), call-protocol.md, operation-registry
(irpc Integration section replaced), overview.md, http-server.md,
webtransport.md, call README. OQ-09 (WASM) resolution amended: the
Connection door is now open via from_stream; the accept-loop runtime door
remains closed (tokio doesn't run on WASM).
See docs/research/transport-generalization/findings.md for the full trace.
ADR-027 resolves the architectural gap surfaced when ACME integration
became a concrete target:
1. TlsIdentity::Acme variant — static config data (domains, cache_dir,
directory, contact) with async AcmeState constructed at endpoint
setup via two-phase TlsSetup (not stuffed into the Clone-able enum).
2. TlsIdentity::RawKey decoupled from the iroh feature — uses
Ed25519SecretKey (alknet-core-owned wrapper over ed25519_dalek)
instead of iroh::SecretKey. Raw-key TLS identity (RFC 7250, the
default for most alknet nodes) now works in quinn-only builds.
iroh transport converts via SecretKey::from_bytes.
3. ACME feature-gated behind new acme feature (rustls-acme optional
dep). Non-ACME builds don't compile it.
4. dispatch_quinn guard for acme-tls/1 challenge connections — TLS-ALPN-01
is handled at the rustls cert resolver layer during the handshake;
the guard closes challenge connections gracefully instead of logging
a misleading "no handler" warning.
Research confirmed QUIC (quinn) handles ACME challenges differently than
TCP (reverse-proxy): quinn gives no ClientHello peek hook, but the
challenge is fully answered at the cert resolution step before the
connection surfaces to the application. No handler registration needed.
Spec updates: config.md, endpoint.md, open-questions.md (OQ-12),
overview.md + README.md (ADR index), ADR-010 (cross-ref).
Tasks: core/rawkey-decouple-from-iroh (gen 1, no deps),
core/acme-integration (gen 2, depends on rawkey). Graph: 36 tasks.
Governance (Tier 2):
- Advance ADR-022 and ADR-023 from Proposed to Accepted (specs already
depend on their types as source of truth)
- Amend ADR-015: mark Decision 3 and Assumption 6 as superseded by ADR-022;
update handler_identity type to CompositionAuthority
- Amend ADR-002: note handle() signature revised by ADR-007 (BiStream → Connection)
- Amend ADR-004: note 'enrich/replace' AuthContext language superseded by
ADR-011's immutability model; update to describe set_identity on Connection
- Update main README ADR table to show ADR-022/023 as Accepted
Spec-ADR consistency (Tier 3):
- Add abort_policy: AbortPolicy field to OperationContext struct (ADR-016
Decision 6 mandated this but the spec omitted it)
- Define AbortPolicy enum (AbortDependents | ContinueRunning) with Default impl
- Add abort_policy to build_root_context and LocalOperationEnv::invoke()
- Define the OperationEnv trait explicitly with invoke() and
invoke_with_policy() methods (was referenced as 'must remain a trait'
but never defined)
- Specify From<StreamError> for HandlerError impl with exact variant mapping
- Add Connection::from_quinn() / from_iroh() constructors (was referenced
as Connection::new() but never defined)
- Remove undefined CertAuthorityEntry placeholder from AuthPolicy v1 (will
be added additively when alknet-ssh lands)
- Fix config.md key-differences table: rate limits are in DynamicConfig,
not StaticConfig
Mechanical fixes (Tier 1):
- overview.md: 'closes the QUIC stream' → 'closes the connection' (stale
from pre-ADR-007 model)
- overview.md: OQ-04 entry updated from stale 'defer to implementation'
to 'resolved: static at startup'
- mnemonic-derivation.md: remove duplicate helper functions block (incomplete
first copy, complete second copy)
- ADR-003: add iroh (feature-gated) to alknet-core dependency list, added
by ADR-010
- ADR-021: fix ambiguous 'W1 drift issue from the vault review' cross-reference
- ADR-022: rephrase FromCall 'leaf locally' to 'leaf in the local registry'
- ADR-017: add error_schemas to from_call mirror list and services/schema
step (inconsistency with ADR-023)
- ADR-016: fix self-referential citation ('ADR-016 Assumption 5' → 'Assumption 5')
- Add ScopedOperationEnv::empty(), allows(), new() and
CompositionAuthority::none(), new() impl blocks (referenced but undefined)
- Add call.completed clarification for non-subscription calls
- Add services/schema leading-slash normalization note
- Crate README ADR tables: add missing ADR-013 (call), ADR-015 (core),
ADR-006 + ADR-010 (vault)
- Vault README: add consolidated 'Known Source Drift' table tracking all
four drift items (OsRng, unwrap, CURRENT_KEY_VERSION, spawn bug) in one
place, including the two previously missing from README
- Rewrite OQ-12: separate two distinct TLS identity use cases (RFC 7250
raw keys as default for P2P, X.509 for domain-hosted/browsers) instead
of conflating them as 'file paths now, ACME later'. ACME is a proven
pattern from the reverse-proxy project, not speculative future work.
- Resolve OQ-13 and OQ-14: remove 'Phase 1' framing from core crate
specs. /{service}/{op} is the correct design for alknet-call, not a
simplification. Batch as correlated call.requested events is the correct
protocol design. Core crates need to be done right from the start.
- Add ADR-013: Rust as canonical implementation language. TypeScript
@alkdev/operations is a reference that informed the design, not a
parallel implementation. The only JS use case is browser SDK adaptation.
Five reasons: memory safety, LLM competence, supply chain attacks,
performance, browser-only JS.
- Add alknet-agent crate to the crate graph (depends on alknet-call, not
alknet-core). Agent service uses call protocol client for tool dispatch
and vault/derive for provider keys — no env vars for secrets. ALPN
alknet/agent added to the registry.
- Add OQ-15: call protocol client and adapter contract. alknet-call needs
both server (CallAdapter) and client (remote invocation over QUIC), plus
the adapter traits (from_*, to_*) that enable composition.
- Clarify alknet-napi as thin NAPI projection layer, not business logic.
- Fix bugs: ProtocolController → ProtocolHandler typo, OperationEnv
invoke() path format inconsistency, RateLimitConfig comment confusion.
- Update endpoint.md TLS section: comprehensive identity model comparison
table, RFC 7250 as default mode, ACME as proven pattern.
iroh uses RFC 7250 raw Ed25519 public keys for TLS instead of X.509
certificates. rustls already supports this. This means the quinn
endpoint can also use raw public keys — same key-based identity model
as iroh, but with direct QUIC over UDP. X.509 is optional, needed
only for domain-facing identity (browser/WebTransport clients).
Update StaticConfig with TlsIdentity enum (X509, RawKey, SelfSigned)
and add iroh_relay field. Remove 'iroh deferred' language — iroh is
a first-class connectivity mode.
iroh's Endpoint natively supports ALPN negotiation and set_alpns(). Our
HandlerRegistry dispatches exactly like iroh's own ProtocolMap/Router
pattern, but shared across both quinn and iroh connection sources. We
use iroh::Endpoint directly (not iroh::Router) because our HandlerRegistry
and AuthContext are shared across sources.
Correct the conflation of quinn/TLS/iroh as interchangeable transports.
They are complementary connectivity modes serving different deployment
contexts: quinn (public IP + TLS), iroh (NAT traversal via relay), TCP
(handler-specific, not core). Clarify that TLS cert = network identity,
not auth identity. Map stealth mode to HTTP handler on standard ALPNs
instead of byte-peeking. Resolve OQ-05 as one-way door. SendStream/
RecvStream now use internal enum dispatch for both quinn and iroh
streams.