27 KiB
alknet-crate-extraction — Migration from the welded core to the extracted crates
Status: Findings in progress — mapping the existing code to the
target shape, phase by phase, so the migration can be ordered to keep
the tree compilable at each step.
Date: 2026-07-16
Scope: The three new crates (alknet-tls, alknet-endpoint,
alknet-client) + the prune of alknet-core and alknet-call + the
alknet-http residual fix. The specs are confirmed tight (reviewed +
amended); this doc is the how — what code moves where, in what order,
with what intermediate states.
TL;DR
The migration is additive-then-subtractive: build all three new
crates first (no breakage), then prune the old code from core and call
(breakage confined to a single phase), then fix the http residual. Six
phases, four compilable intermediate states. The heaviest single file
(endpoint.rs, 1606 lines) is not a monolith — it's three concerns
welded together, each going to a different destination.
What exists now (the source map)
crates/alknet-core/src/endpoint.rs — 1606 lines, three concerns
The file is not 1600 lines of one thing. It's three concerns that the extraction separates:
| Lines | Concern | Destination | LOC |
|---|---|---|---|
| 1-492 | AlknetEndpoint struct, HandlerRegistry, dispatch_quinn/dispatch_iroh, accept loops, build_auth_context, has_iroh_identity |
alknet-endpoint |
~492 |
| 493-934 | TlsSetup, build_rustls_server_config, build_quinn_server_config_from_rustls, RawKeyCertResolver, Ed25519SigningKey, AcceptAnyCertVerifier, SelfSignedCert, generate_self_signed_cert, load_cert_chain, load_private_key, build_iroh_endpoint |
alknet-tls (TLS setup) / assembly layer (iroh builder) |
~442 |
| 935-1606 | 42 test functions | split by subject | ~671 |
The endpoint struct + dispatch + accept loops (the part that goes to
alknet-endpoint) is ~492 lines of implementation — not 1600. The TLS
setup code (~442 lines) goes to alknet-tls. The tests (~671 lines)
split by what they test. The file feels monolithic because the
#[cfg(feature = "quinn")] gates weave the three concerns together,
but the extraction unwinds that.
crates/alknet-call/src/client/call_client.rs — 930 lines
| Lines | Concern | Destination | LOC |
|---|---|---|---|
| 40-88 | RemoteIdentity, CallCredentials (struct + builder) |
alknet-core (new credentials.rs or auth.rs) |
~48 |
| 90-100 | ClientError enum |
removed (only produced by connect) |
~10 |
| 102-187 | CallClient struct + new + spawn_dispatch |
stays (the pure protocol take-over) | ~85 |
| 189-320 | build_quinn_client_config, build_client_auth, select_server_verifier, load_platform_root_cert_store, load_cert_chain, load_private_key, Ed25519SigningKey, RawKeyClientCertResolver, NoClientCertResolver, FingerprintPinVerifier |
alknet-tls |
~130 |
| 321-640 | CallConnection, Dispatcher wiring, wire-protocol helpers |
stays (protocol) | ~320 |
| 640-930 | Tests (use connect, CallCredentials, TLS helpers) |
rewrite to use spawn_dispatch directly or AlknetClient |
~290 |
The call crate's prune is ~140 lines of implementation + ~290 lines of
tests that need rewriting. What remains is the pure protocol: CallClient
CallConnection+Dispatcher+ the wire protocol.
crates/alknet-http/src/server/adapter.rs — the residual
The HttpAdapter::handle method does connection.accept_bi() → wraps
the send/recv pair in a hand-rolled QuicStream (lines 271-300, an
AsyncRead+AsyncWrite adapter) → feeds it to serve_io(). But
serve_io already accepts any AsyncRead+AsyncWrite (line 244). The
QuicStream wrapper is a hand-rolled version of what
Connection::from_bidi/from_stream (ADR-065, already landed) provides
natively.
The residual: handle assumes a quinn-style multi-stream connection
(accept_bi returns a fresh bidi stream). For a from_bidi connection
(TCP+TLS), accept_bi yields the single bidi stream once (ADR-070's
yield-once contract). The QuicStream wrapper works but is unnecessary
— the streams from accept_bi are already AsyncRead+AsyncWrite. The
fix is to drop QuicStream and use the streams directly (or via
TokioIo::new). This is a small change (~30 lines removed) but needs
verification against ADR-070's accept_bi semantics for from_bidi.
The QuicStreamDuplex test helper (lines 456-471) is the test-side
equivalent — it can be replaced with Connection::from_stream test
helpers (the MockConnection/ConnectionKind::Mock variants were
already removed per ADR-065; tests use from_stream with
tokio::io::sink/empty).
The seven phases
Phase 0: Move CallCredentials/RemoteIdentity to alknet-core (additive, no breakage)
What: Add CallCredentials + RemoteIdentity to a new
crates/alknet-core/src/credentials.rs (~50 lines). Update
alknet-core/src/lib.rs to pub mod credentials + re-export. Update
alknet-call/src/client/mod.rs to re-export from core instead of
defining locally. No other changes.
Why first: It's independent of the three new crates, purely
additive (core gains types, nothing breaks), and means alknet-client
(Phase 3) never has a temporary dep on alknet-call. The dep graph is
clean from the start.
Compilable state: cargo test passes across the workspace.
alknet-call re-exports the types from core; its own code + tests
import via alknet_call::client::CallCredentials (unchanged — the
re-export preserves the path).
Done when: cargo test passes, CallCredentials +
RemoteIdentity are defined in alknet-core, alknet-call re-exports
them.
Phase 1: Create alknet-tls (greenfield, additive)
What: New crate crates/alknet-tls/. Extract the TLS setup code
from alknet-core/endpoint.rs (lines 493-934) + the client-side TLS
helpers from alknet-call/client/call_client.rs (lines 189-320).
Types: TlsServerConfig, TlsClientConfig, TlsError,
FingerprintPinVerifier, RawKeyCertResolver,
RawKeyClientCertResolver, NoClientCertResolver, Ed25519SigningKey
(consolidated — one copy, used by both server + client),
AcceptAnyCertVerifier, SelfSignedCert, generate_self_signed_cert,
load_cert_chain, load_private_key, load_platform_root_cert_store
(+ the webpki-roots fallback — new, not extracted).
Deps: alknet-core (TlsIdentity, Ed25519SecretKey, fingerprint),
rustls, rustls-pemfile, rustls-native-certs, webpki-roots,
rcgen, tokio, optional quinn/tokio-rustls/rustls-acme.
Compilable state: alknet-tls builds and tests standalone. Core and
call are unchanged — the old code still exists (duplicated). No
breakage. The new crate's tests are the moved tests from
endpoint.rs (the TLS-setup tests) + the moved tests from
call_client.rs (the verifier/client-config tests).
Tests that move here (from endpoint.rs):
raw_key_cert_resolver_only_raw_public_keysself_signed_cert_generation_produces_cert_and_keyacme_directory_production_url/staging_url/custom_urltls_setup_x509_returns_no_acme_statebuild_rustls_server_config_raw_key_succeedsbuild_rustls_server_config_self_signed_succeedsbuild_quinn_server_config_from_rustls_succeedsload_private_key_returns_error_when_no_key_present/_file_missingload_cert_chain_returns_error_when_file_missingaccept_any_cert_verifier_*(4 tests)ed25519_signing_key_*(6 tests)raw_key_cert_resolver_debug_is_implemented
Tests that move here (from call_client.rs): the
FingerprintPinVerifier + build_quinn_client_config tests (need
adaptation — they currently test via connect, should test via
TlsClientConfig::new directly).
Done when: cargo test -p alknet-tls passes, the crate is
self-contained, no other crate changed.
Phase 2: Create alknet-endpoint (greenfield, additive)
What: New crate crates/alknet-endpoint/. Extract the endpoint
struct + dispatch + accept loops from alknet-core/endpoint.rs (lines
1-492), built fresh against the ADR-083 shape (new(handlers, dynamic, identity_provider, drain_timeout) + with_quinn/with_iroh/with_tcp_tls
- public
dispatch+run/shutdown). NoEndpointError(removed per the spec review).shutdown()is infallible.
Types: AlknetEndpoint, HandlerRegistry, TcpTlsListener,
private dispatch_quinn/dispatch_iroh/dispatch_tcp_tls,
build_auth_context, the extract_* helpers.
Deps: alknet-core (Connection, ProtocolHandler, AuthContext,
IdentityProvider, DynamicConfig), optional quinn/iroh/tokio-rustls,
tokio, arc-swap, tracing.
Module breakdown (the 492 lines split):
registry.rs—HandlerRegistry(~40 lines + tests)endpoint.rs—AlknetEndpointstruct,new, builder methods,run,shutdown(~120 lines)dispatch.rs—dispatch(public),build_auth_context, theacme-tls/1guard (~80 lines)accept/quinn.rs—dispatch_quinn,run_quinn_accept_loop,extract_quinn_alpn,extract_quinn_client_fingerprint(~80 lines)accept/iroh.rs—dispatch_iroh,run_iroh_accept_loop,extract_iroh_client_fingerprint(~50 lines)accept/tcp_tls.rs—dispatch_tcp_tls,run_tcp_tls_accept_loop,extract_tcp_tls_alpn,extract_tcp_tls_client_fingerprint(new, ~60 lines — not in the current code; written fresh per ADR-083)
This is a fresh build, not a move — the old endpoint.rs stays in core
until Phase 4. The new crate is written against the target shape, not
the old shape.
Compilable state: alknet-endpoint builds and tests standalone.
Core's endpoint.rs still exists (duplicate). No breakage.
Tests that move here (from endpoint.rs):
handler_registry_*(5 tests)build_auth_context_*(3 tests)dispatch_decision_logic_lookup_and_authhas_iroh_identity_*(3 tests)endpoint_constructs_with_iroh_raw_key_identityiroh_endpoint_runs_accept_loop_and_shutdowndebug_for_alknet_endpoint_is_implemented_without_panicking
Done when: cargo test -p alknet-endpoint passes, the crate is
self-contained, no other crate changed.
Phase 3: Create alknet-client (greenfield, additive)
What: New crate crates/alknet-client/. The AlknetClient dial
seam — three dial methods (dial_quic/dial_tcp_tls/dial_iroh),
consuming TlsClientConfig from alknet-tls + CallCredentials from
alknet-core. The SOCKS5 proxy path (ADR-090) is feature-gated.
Types: AlknetClient, ClientDialError, Socks5ProxyConfig,
Socks5Credentials (behind socks5 feature).
Deps: alknet-core (Connection, CallCredentials, RemoteIdentity,
Ed25519SecretKey), alknet-tls (TlsClientConfig), optional
quinn/tokio-rustls/iroh/fast-socks5.
Feature gates: quinn = ["dep:quinn", "alknet-tls/quinn", "alknet-core/quinn"], tcp = ["dep:tokio-rustls", "alknet-tls/tcp"],
iroh = ["dep:iroh", "alknet-core/iroh"], socks5 = ["dep:fast-socks5"].
Compilable state: alknet-client builds and tests standalone. No
breakage — the old CallClient::connect still exists in
alknet-call (duplicate dial). The new crate's tests use
AlknetClient::dial_* + spawn_dispatch/from_connection or test
the dial in isolation with mock transports.
Done when: cargo test -p alknet-client passes, the crate is
self-contained, no other crate changed.
Phase 4: Prune alknet-core (subtractive, breakage confined)
What: Delete endpoint.rs from core. Remove pub mod endpoint
from lib.rs. Remove the heavy deps (quinn, iroh, rcgen,
rustls-pemfile, rustls-acme) from Cargo.toml — but keep the
quinn/iroh features (they gate Connection::from_quinn/
from_iroh in types.rs). Add CallCredentials + RemoteIdentity
to core (from alknet-call).
The lib.rs change:
// Before:
pub mod endpoint; // ← removed
pub mod auth;
pub mod config;
// ... rest unchanged
// After:
pub mod auth;
pub mod config;
pub mod credentials; // ← new (CallCredentials, RemoteIdentity)
// ... rest unchanged
The Cargo.toml change: quinn/iroh/rcgen/rustls-pemfile/
rustls-acme leave [dependencies]. The quinn/iroh features stay
(gating types.rs constructors). The acme feature is removed
(vestigial — the ACME state machine is on TlsServerConfig in
alknet-tls now).
The fingerprint.rs doc comment (line 13) references
alknet_core::endpoint — update to reference alknet-endpoint /
alknet-tls.
Breakage: anything that imported alknet_core::endpoint breaks.
But per the source map, nothing does — no handler crate or call
imports the endpoint module. The only consumer is the future assembly
layer (hub/worker), which doesn't exist yet. So the prune is clean:
delete the file, update lib.rs, update Cargo.toml, fix the
fingerprint.rs comment. Core's own tests for endpoint.rs are gone
(moved to alknet-tls in Phase 1 and alknet-endpoint in Phase 2).
Compilable state: cargo test -p alknet-core passes (minus the
endpoint tests, which moved). The quinn/iroh features still work
(Connection::from_quinn/from_iroh in types.rs).
Done when: cargo test -p alknet-core passes, the crate is
lightweight (~3200 LOC, no heavy transport deps).
Phase 5: Prune alknet-call (subtractive, breakage confined)
What: Delete connect() + all TLS helpers + ClientError from
call_client.rs. Move CallCredentials/RemoteIdentity to core
(already done in Phase 4 — here we just remove the old definitions +
update imports). Update Cargo.toml to drop quinn/rustls/
rustls-native-certs/rustls-pemfile. Rewrite the tests that used
connect to use spawn_dispatch directly (with Connection::from_stream
mocks) or AlknetClient::dial_quic + spawn_dispatch.
The call_client.rs after prune:
CallClientstruct +new+registry+identity_provider+spawn_dispatch(~85 lines — unchanged)CallConnection+Dispatcherwiring (stays — protocol)RemoteIdentity/CallCredentials— removed (now inalknet-core, re-imported from there)ClientError— removedconnect+ allbuild_*/select_*/load_*/Ed25519SigningKey/RawKeyClientCertResolver/NoClientCertResolver/FingerprintPinVerifier— removed (now inalknet-tls)
The Cargo.toml after prune: quinn, rustls,
rustls-native-certs, rustls-pemfile all leave. The quinn feature
either disappears or becomes a no-op (it only gated connect + the
TLS helpers, both removed). alknet-call becomes a pure protocol crate.
Test impact (per the test audit below): the lib tests in
call_client.rs (16 tests) split into 6 that stay unchanged
(protocol-level, use spawn_dispatch(stub_connection())) and 10 that
move to alknet-tls in Phase 1 (TLS/verifier tests). Zero lib tests
need rewriting — no test in call_client.rs calls connect(). The
from_call.rs tests (27 tests) stay unchanged — they use
CallConnection directly. The one integration test file
(tests/two_node_call.rs, 2 tests) calls connect() twice — it moves
to alknet-client/tests/ (Phase 3) or is updated to use
AlknetClient::dial_quic + spawn_dispatch (Phase 5).
The implementation prune is mechanical: delete the error enum, delete
connect, delete the TLS helpers (~140 lines). The test work is: the
10 TLS tests already moved in Phase 1, the 6 protocol tests stay, the
integration test is handled separately.
Compilable state: cargo test -p alknet-call passes with the
rewritten tests. The crate has no TLS/transport deps.
Done when: cargo test -p alknet-call passes, the crate is a pure
protocol crate.
Phase 6: Fix alknet-http (small, additive)
What: Remove the QuicStream wrapper from server/adapter.rs.
The HttpAdapter::handle method does connection.accept_bi() →
wraps in QuicStream → feeds to serve_io. After the fix, it does
connection.accept_bi() → uses the streams directly (they're already
AsyncRead+AsyncWrite) → feeds to serve_io via TokioIo::new.
The QuicStream struct (lines 271-300) is deleted. The
QuicStreamDuplex test helper (lines 456-471) is replaced with
Connection::from_stream test helpers.
Verification needed: confirm that accept_bi() on a from_bidi
connection yields the single bidi stream per ADR-070's yield-once
contract, and that the yielded streams are directly usable as
AsyncRead+AsyncWrite (no wrapper needed). If the yield-once semantics
require a different path for single-stream connections, the fix is
slightly larger — but the serve_io signature already accepts any
AsyncRead+AsyncWrite, so the adapter is ready.
Compilable state: cargo test -p alknet-http passes. The crate no
longer has the hand-rolled QuicStream wrapper.
Done when: cargo test -p alknet-http passes, the QuicStream
wrapper is gone.
Intermediate states (compilable after each phase)
| After phase | State |
|---|---|
| 0 (credentials) | CallCredentials/RemoteIdentity in core; call re-exports; no breakage |
| 1 (tls) | alknet-tls builds standalone; core/call/http unchanged (old code duplicated) |
| 2 (endpoint) | alknet-endpoint builds standalone; core still has old endpoint.rs (duplicate) |
| 3 (client) | alknet-client builds standalone; call still has old connect (duplicate) |
| 4 (core prune) | core is lightweight; endpoint.rs gone; CallCredentials in core |
| 5 (call prune) | call is pure protocol; connect + TLS helpers gone; Category B tests already moved |
| 6 (http fix) | http has no QuicStream wrapper; clean accept_bi path |
Phases 0-3 are purely additive — no existing code breaks, no tests
break. Phases 4-5 are subtractive — the pruned code's callers don't
exist yet (no assembly layer), so the breakage is confined to the
crate's own tests (and per the test audit, the call prune breaks zero
tests — the TLS tests moved in Phase 1, the protocol tests use
spawn_dispatch directly). Phase 6 is a small fix.
Ordering rationale
The ordering is deps before dependents, additive before subtractive:
-
Phase 0 (
CallCredentialsto core) first because it's independent, additive, and makesalknet-client(Phase 3) never depend onalknet-call. ~50 lines moved, zero breakage. -
alknet-tls(Phase 1) because bothalknet-endpoint(indirectly — the assembly layer buildsTlsServerConfig) andalknet-client(directly —TlsClientConfig) depend on it. It has no dep on the other new crates. -
alknet-endpoint(Phase 2) depends only onalknet-core(already exists) — it doesn't needalknet-tls(the endpoint takes pre-built transports). It could go beforealknet-tls, but putting tls first means the assembly layer's transport-building code has a home from the start. -
alknet-client(Phase 3) depends onalknet-tls(TlsClientConfig) +alknet-core(CallCredentials— moved in Phase 0, so the dep is clean from the start). -
Phases 4-5 (the prunes) go last because they're subtractive. The new crates (0-3) must exist first so the pruned code's functionality has a home.
-
Phase 6 (http fix) goes last because it's independent of the extraction — it's a residual fix that could happen at any point after ADR-065 landed (which it did). Putting it last keeps the extraction phases clean.
-
Phases 4-5 (the prunes) go last because they're subtractive. The new crates (1-3) must exist first so the pruned code's functionality has a home.
-
Phase 6 (http fix) goes last because it's independent of the extraction — it's a residual fix that could happen at any point after ADR-065 landed (which it did). Putting it last keeps the extraction phases clean.
Resolved decisions
CallCredentials/RemoteIdentity move — Phase 0 (before Phase 1)
Decision: Move CallCredentials/RemoteIdentity to alknet-core
as a standalone additive step before any new crate is created. It's
independent of everything else, purely additive (core gains two small
types, nothing breaks), and alknet-call re-exports them from core
so its own code + tests don't change yet. This means alknet-client
(Phase 3) never depends on alknet-call — the dep graph is clean from
the start, no temporary dep to clean up later.
The move is ~50 lines (struct definitions + builder impls) into a new
crates/alknet-core/src/credentials.rs (or auth.rs — auth.rs
already holds AuthToken, so credentials.rs is cleaner to keep the
auth module from growing). alknet-call's client/mod.rs changes
pub use call_client::{CallCredentials, RemoteIdentity} to
pub use alknet_core::{CallCredentials, RemoteIdentity} (re-export).
No test changes — the tests import CallCredentials from
alknet_call::client, which still works via the re-export.
Phase 5 test audit — call_client.rs (16 tests)
The 16 tests in call_client.rs split into three categories:
Category A — protocol-level, stay in alknet-call, no rewrite
needed (6 tests):
These tests use spawn_dispatch(stub_connection()) or
CallCredentials directly. They don't touch connect or any TLS
helper. stub_connection() (line 582) uses
Connection::from_stream(tokio::io::channel(...)) — already
transport-agnostic. These survive the prune unchanged.
| Test | Line | What it tests |
|---|---|---|
call_credentials_builder_methods |
652 | CallCredentials builder |
external_op_dispatches_and_populates_capabilities |
665 | dispatch + capabilities |
unknown_op_returns_not_found |
679 | dispatch error path |
spawn_dispatch_returns_live_call_connection |
691 | spawn_dispatch + ALPN |
call_client_is_send_sync |
705 | trait bounds |
remote_identity_none_is_load_bearing_not_defaulted |
921 | CallCredentials::new() |
Category B — TLS/verifier tests, move to alknet-tls (8 tests):
These test FingerprintPinVerifier, build_client_auth,
select_server_verifier, and build_quinn_client_config directly.
They're #[cfg(feature = "quinn")]-gated and test the TLS helpers,
not the call protocol. They move to alknet-tls in Phase 1 (adapted
to test TlsClientConfig::new instead of the free functions).
| Test | Line | What it tests | Move target |
|---|---|---|---|
fingerprint_pin_verifier_matches_correct_ed25519_fingerprint |
750 | verifier accept | alknet-tls |
fingerprint_pin_verifier_rejects_wrong_ed25519_fingerprint |
769 | verifier reject | alknet-tls |
fingerprint_pin_verifier_matches_correct_sha256_fingerprint |
789 | verifier X.509 accept | alknet-tls |
fingerprint_pin_verifier_rejects_wrong_sha256_fingerprint |
806 | verifier X.509 reject | alknet-tls |
select_server_verifier_returns_ca_verifier_for_none |
822 | CA path | alknet-tls |
select_server_verifier_returns_fingerprint_pin_for_some |
839 | pin path | alknet-tls |
build_client_auth_presents_ed25519_raw_key_without_error |
857 | client cert resolver | alknet-tls |
build_client_auth_none_resolves_to_no_client_cert |
879 | no-cert resolver | alknet-tls |
build_quinn_client_config_with_raw_key_identity_builds_without_error |
893 | full config build | alknet-tls |
build_quinn_client_config_with_no_remote_identity_builds_without_error |
909 | CA-verify config | alknet-tls |
Wait — that's 10, not 8. The two build_quinn_client_config tests
test the full config build (verifier + client-auth + provider wired
together). They move to alknet-tls and are adapted to test
TlsClientConfig::new + for_quinn() instead of the free function.
Category C — connect integration test, remove (0 tests):
No test in call_client.rs actually calls connect(). The tests that
exercise the full QUIC dial path are in from_call.rs (which has 27
tests) and in the integration tests. call_client.rs's tests are all
either protocol-level (Category A) or TLS-helper-level (Category B).
This means the connect removal doesn't break any test in
call_client.rs itself — the tests that need a real connection already
use spawn_dispatch(stub_connection()).
The from_call.rs tests (27 tests): these use CallConnection
directly (constructed from stub_connection() or a mock), not
connect. They're protocol-level and stay in alknet-call unchanged.
The one reference to connect() is in a doc comment (line 76:
"the assembly layer calls from_call immediately after connect()")
— update the comment to say "after AlknetClient::dial_* +
spawn_dispatch".
Net Phase 5 test impact: 6 tests stay unchanged (Category A), 10
tests move to alknet-tls in Phase 1 (Category B), 0 tests need
rewriting. The from_call.rs tests (27) stay unchanged. The prune
of call_client.rs is mechanical: delete lines 90-640 (the error
enum + connect + TLS helpers), keep lines 102-187 (CallClient +
spawn_dispatch), update imports. The tests (lines 640-930) keep the
Category A tests, remove the Category B tests (moved in Phase 1), and
update the one doc comment.
This is much simpler than the initial estimate of "~290 lines of test
restructuring." The actual test work is: move 10 tests to alknet-tls
in Phase 1 (adapted to the new API), keep 6 tests unchanged, update one
doc comment. The connect removal breaks zero tests because no test
calls connect.
Open questions for the migration plan
- Phase 6
accept_bisemantics: doesaccept_bi()on afrom_bidiconnection yield the single bidi stream directly usable asAsyncRead+AsyncWrite, or does it need a wrapper? Needs verification against theBidiStreamSourceimpl forfrom_bidiintypes.rs. - Workspace
Cargo.toml: the three new crates need to be added to the workspace member list. Trivial but worth noting. - Integration tests (
tests/two_node_call.rs): the one integration test file (331 lines, 2 tests) callsclient.connect()twice and builds its own raw quinn server (build_raw_quinn_server, lines 58-94 — hand-rolled rustls + quinn server config, noAlknetEndpoint). Both tests (call_round_trips_over_quic_loopbackandfrom_call_discovers_and_forwards_over_quic_loopback) need theconnectpath replaced withAlknetClient::dial_quic+spawn_dispatch. The server side (build_raw_quinn_server) should eventually useAlknetEndpoint+TlsServerConfig, but that's a larger rewrite — for Phase 5, the minimal fix is to replace just the clientconnectcall with the dial + take-over composition. The server-side raw quinn setup can stay (it's a test helper, not production code). This integration test will needalknet-callto dev-depend onalknet-client(or the test moves toalknet-client's own integration tests). The cleaner option: movetwo_node_call.rstoalknet-client/tests/oncealknet-clientexists (Phase 3), since it's testing the dial + take-over composition, not just the protocol.