f73c6035b5f979204b01cb41dd559749f38c6317
135
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0fcd5bc322 |
docs(adr): 094 — per-identity channel cap as DoS defense
ADR-076 framed the per-connection max_channels=256 cap as the DoS defense, but a peer can open an unbounded number of transport connections, so a per-connection cap bounds a connection's reassembly-buffer cost, not a peer's total channels. The only coherent unit for a channel DoS defense is the identity. ADR-094 records the corrected design: a ChannelLifecyclePolicy trait in channels-call (where the identity is already on OperationContext), consulted by the channel/open handler (after AccessControl::check, before allocation) and the channel/close handler (after the drain completes). Default is PerIdentityChannelPolicy::new(256) — 256 per PeerId across all the peer's connections, shared via Arc across every channels connection a peer accepts. The cap is a peer concern (not hub-specific), symmetric (both sides enforce), and lives in channels-call because the channels layer is auth-blind by design (ADR-075) — that is what makes it WASM-compatible, transport-agnostic, and ALPN-blind. For the hub-relay path (ADR-079), the spoke sees the hub as the direct caller (ADR-032 — forwarded_for is metadata, not authority, for the cap as for AccessControl::check), so the spoke caps the hub, not the browser. A spoke serving a high-fan-out hub sets the hub peer's cap higher via with_per_identity_caps — the spoke's own policy, not the hub's. Recursive channels do not bypass the cap (the same policy can be wired into the inner ChannelOperations). ADR-076 is amended: the per-connection max_channels is reframed as a per-connection memory bound (still returns channel:too_many_channels when hit), the "DoS defense summary" table is removed, and the "per-connection, not per-peer" line (the channels layer confessing a hole and hoping the layer above would fill it) is corrected. Spec docs updated to reference ADR-094: channel-operations.md gains a "Per-identity channel cap" section (trait, default, enforcement point, relay consequence, recursion); channels-adapter.md adds the policy check as step 3 of the channel/open handler and the decrement in channel/close; hub README adds the channel_policy field on Hub, the with_channel_policy builder, a dedicated subsection, and the inbound-peer-vs-hub-as-caller distinction; channels/README.md adds ADR-094 to the Applicable ADRs table and a 9th Key Design Principle; docs/architecture/README.md adds a Current State note and the ADR table row. |
||
|
|
762d9c7bd2 |
docs(architecture): remove removed-thing remnants from spec docs
Spec docs should describe WHAT IS, not WHAT WAS. ADRs and OQ files are historical records by design and are left alone; the ADR-index Status column records ADR status (stable fact). What changed in the specs: - API code blocks no longer list removed methods. ChannelClient::connect_quic (channel-client.md) and CallClient::connect (client-and-adapters.md) are gone from the impl blocks; surrounding prose describes the current from_connection / spawn_dispatch primary + AlknetClient dial shape. - Amendment (ADR-093): stream_types field is removed blockquotes dropped from channel-client.md and channel-operations.md (the code already reflects the current state). - Historical is-removed / reversed-by / amended-by prose rewritten to current state across channels crate, call crate, hub, tls, core, endpoint, client READMEs, and the top-level README/overview. - Dropped the EndpointError — removed subsection from endpoint/README.md (the type doesn't exist anymore, so it shouldn't have a subsection). - Replaced stale connect() references in flow descriptions with the dial (in AlknetClient) since connect() is no longer a method. - Removed strikethrough ADR-028 / from_jsonschema-clause rows from client-and-adapters.md and operation-registry.md ADR tables. - Rewrote the ADR-066 update blockquote in operation-registry.md to describe FromJsonSchema's current shape. 19 files modified, net -116 lines. No ADRs or OQ files touched. |
||
|
|
a3cb44968e |
docs(adr): 093 — channels pure channel multiplexing (8-byte header, no stream_type)
Prune the channels spec to reflect the stream-unification resolution (docs/research/stream-unification/findings.md): the channels wire format goes from 9 bytes to 8 bytes, the channels layer no longer carries a stream_type concept, into_sub_streams() is removed, and TTY always uses its 5-byte format (carried transparently in the channels payload). ADR-093 is the umbrella decision (the channels-layer consequence of ADR-092's BiStream handler leaf): every channel is a BiStream, the handler owns its sub-stream multiplexing, the channels layer routes by channel_id only. Amends ADR-071 (8-byte header, no stream_type), ADR-074 (into_sub_streams removed, accept_bi yields BiStream), reverses ADR-077 (TTY always 5-byte), and the channels-facing clauses of ADR-072/073/075/076/080/081. Adds ADR-092 forward-reference note (into_sub_streams preservation subsequently reversed by ADR-093) and the missing ADR-092 cross-reference on ADR-070. Adds OQ-68 (add/strip API shape — built-in vs utility; the contract is decided in ADR-093, the function surface is open; two-way door, low priority, decision-ready when the channels crate's implementation begins). Rewrites the 7 channels spec docs (README, overview, channels-wire, channels-connection, channels-adapter, channel-operations, channel-client) to describe the post-amendment shape as current, with the 8-byte header, the add/strip composition, single accept_bi accessor, BiStream per channel, and TTY-always-5-byte. Touch-up cross-references in hub README, client README, ADR-085, and the OQ-45/47/65 question files (TTY-internal stream_type 3 → STREAM_CTRL_IN; channels 9-byte → 8-byte). |
||
|
|
c2b7055a64 |
refactor(tty): split control channel into STREAM_CTRL_IN/STREAM_CTRL_OUT halves (Phase 7)
The single STREAM_CONTROL = 3 was documented as bidirectional but the
adapter had to ignore Exit from the client because the two directions
were indistinguishable on the same stream_type — half-duplex in
disguise. Phase 7 splits it into two halves so the bidirectionality is
literal on the wire.
Changes:
- wire.rs: STREAM_CTRL_IN = 3 (client→server), STREAM_CTRL_OUT = 4
(server→client); InvalidStreamType bound > 3 → > 4; Chunk::control
→ Chunk::ctrl_in/ctrl_out; ChunkWriter::write_control_json →
write_ctrl_in_json/write_ctrl_out_json; tests split accordingly.
- control.rs: ControlMessage doc updated with the stream_type column;
JSON shape unchanged.
- adapter.rs: pump_client_to_backend dispatches on STREAM_CTRL_IN
(Resize/Signal/Eof; Exit on ctrl_in is a protocol violation,
ignored); send_exit_chunk emits on STREAM_CTRL_OUT; STREAM_CTRL_OUT
from the client is a protocol violation, ignored. 3 new tests for
the direction enforcement; existing tests updated to the new
stream_types.
- negotiation.rs: framing-disambiguation doc updated (server-sent
stream_type set is {1, 2, 4}).
- alknet-tty-local/tests: common/mod.rs, pty.rs, pipe.rs updated to
the new constants.
Specs:
- ADR-052 amended (§4a 'Control channel split (Phase 7 amendment)').
- tty-wire.md + tty-adapter.md updated (last_updated 2026-07-18).
Verification:
- cargo test -p alknet-tty: 65 passed (was 61; +4 new tests).
- cargo test -p alknet-tty-local: 19 passed.
- cargo test --workspace --all-features: 1017 passed, 0 failed.
- cargo clippy --workspace --all-features: clean.
- cargo fmt --all: clean.
|
||
|
|
528cfa0367 |
docs(adr): 092 — remove Connection::from_stream, from_bidi is the only public constructor
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. |
||
|
|
f8d4650dce |
docs(adr): 092 — BiStream as the handler leaf, unify split-pair accept_bi
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. |
||
|
|
3b10fc1817 |
refactor(core,tls,client): align ConnectionCredentials field name with ADR-091 (tls_identity -> local_identity)
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 (
|
||
|
|
c6eef730e4 |
docs(architecture): sync specs to post-extraction state (phases 0-5)
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.
|
||
|
|
e91d943857 |
docs: remove CallCredentials — dead field, dead from_call path, auth_token is per-request payload
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. |
||
|
|
613bf680cd |
docs(arch): ConnectionCredentials decouples dial from call protocol (ADR-091) + migration plan cleanups
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 |
||
|
|
bf6ce957c0 |
docs(arch): tighten tls/endpoint/client specs — remove connect/connect_quic, move CallCredentials to core, shed alknet-call TLS deps
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. |
||
|
|
34e3be2801 |
docs(arch): resolve OQ-67 — iroh proxy force-relay-only + HTTP-to-SOCKS5 bridge (ADR-090 §5 amended)
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 |
||
|
|
b7d67e5a5f |
docs(arch): client-dial SOCKS5 proxy seam — ADR-090, OQ-67
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 |
||
|
|
8669594661 |
docs(arch): extract AlknetEndpoint into alknet-endpoint (ADR-083 Am. 2026-07-15)
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). |
||
|
|
ce7de57973 |
docs(arch): AlknetClient native dial seam — resolves OQ-55 (ADR-089)
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). |
||
|
|
1291a751b0 |
docs(arch): alknet-tls spec sanity-check fixes — client-side accessors, extraction tables, ordering
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. |
||
|
|
43b8179304 |
docs(arch): TlsError shape — single enum, owned by alknet-tls (ADR-088, resolves OQ-63)
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). |
||
|
|
5941280bca |
fix(agents): break the hedging-at-the-root pattern — deferred(unclear), impacts field, reviewer detection
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.
|
||
|
|
bd9ae3cb68 |
docs(arch): move OQ-65 (WebSocket carrying channels) to alknet-http theme
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. |
||
|
|
77321a7e84 |
docs(arch): break the AlknetClient circular hedge — TlsClientConfig not blocked on dial (ADR-087, resolves OQ-64)
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 |
||
|
|
7d9b1ebad9 |
docs(arch): endpoint types and entry points (ADR-086) — resolves OQ-62
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.' |
||
|
|
7610ec1f31 |
docs(arch): workspace scope correction (ADR-085) + tls spec review fixes
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) |
||
|
|
34729c7846 |
docs(arch): resolve OQ-59 (fingerprint stays in core) + ADR-084 (aws-lc-rs crypto provider)
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. |
||
|
|
2abe8f1872 |
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). |
||
|
|
81bde6f28f |
docs(arch): endpoint as pure accept-loop runner + acme-tls/1 guard relocation (ADR-083, OQ-60/61)
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). |
||
|
|
837f94f2aa |
docs(arch): alknet-tls review fixes — dep claim, Clone clarity, dedup, ADR refs
C1: Correct dep-change claim — only rustls-pemfile, rcgen, rustls-acme leave core; quinn, iroh, ed25519-dalek stay (endpoint struct, accept loops, and Ed25519SecretKey remain in core). W1: Trim duplicated 'Why' section in README to summary + ADR-082 link; keep the three-use-cases table as reference. W2: Clarify TlsServerConfig is not Clone (holds JoinHandle); share via Arc, accessors clone the inner rustls::ServerConfig. Fix in README and ADR-082. W6: Resolve futures dep inconsistency — acme-gated in both README and ADR. S1: Behavior-preservation invariants now reference ADR-027 as their origin. S2: Document for_quinn fallibility (QuicServerConfig::try_from can fail) vs for_tcp_tls infallibility (TlsAcceptor::new cannot fail). |
||
|
|
192bd0a5a2 |
docs(arch): spec alknet-tls crate — shared TLS config across quinn + TCP+TLS + iroh (ADR-082, OQ-59)
The TLS setup in alknet-core is welded to quinn: build_rustls_server_config produces a rustls::ServerConfig, then build_quinn_server_config_from_rustls consumes it into quinn::ServerConfig — the rustls config is moved, not shareable. ACME is worse: the AcmeState task is spawned inside the quinn endpoint, so a TCP+TLS listener would need a second ACME state machine (two orders for the same domain, two cert caches, Let's Encrypt rate-limit risk). alknet-tls extracts the TLS setup into a shareable TlsServerConfig: - TlsServerConfig::new(identity, alpns) builds the rustls::ServerConfig once (including the ACME state machine if ACME) - for_quinn() clones it for quinn (QuicServerConfig::try_from) - for_tcp_tls() clones it for tokio-rustls (TlsAcceptor::from) - rustls_config() borrows it for any other consumer - One cert, one ACME state machine, N transports TlsIdentity and Ed25519SecretKey stay in core (config types). fingerprint.rs stays in core (shared by server endpoint + client FingerprintPinVerifier in alknet-call — OQ-59 tracks whether it should move; likely stays because moving would force alknet-call to depend on alknet-tls, pulling TLS infra into client-only deployments). iroh shares the key, not the rustls config — iroh has its own TLS built into the Endpoint (takes iroh::SecretKey, not rustls::ServerConfig). alknet-tls has no for_iroh() method; the assembly layer passes Ed25519SecretKey to iroh directly. Feature gates: quinn / tcp / acme as independent opt-ins. rustls always present. tokio-rustls only when tcp. quinn only when quinn. rustls-acme only when acme. Files: - crates/tls/README.md: crate spec (TlsServerConfig API, what moves/ stays, feature gates, deps, behavior-preservation invariants) - decisions/082-alknet-tls-extraction.md: the ADR (Proposed) - questions/059-fingerprint-module-location.md: OQ-59 (should fingerprint.rs stay in core or move to alknet-tls? open, two-way, likely stays) - open-questions.md: OQ-59 added to alknet-core table - README.md: tls doc table row + ADR-082 row added Review (architecture-reviewer): 3 critical (missing build_quinn_server_config_from_rustls in README table, missing futures dep, two TlsServerConfig code blocks disagreed on feature gates), 7 warnings (behavior-preservation invariants: max_early_data_size, aws_lc_rs provider, verifier scheme list, acme-tls/1 ALPN; rustls-pki-types dep; fingerprint.rs rustls-usage imprecision; ADR missing acme-tls/1), 4 suggestions. All criticals and warnings addressed. |
||
|
|
b0cc0a01b7 |
docs(arch): unweld CallClient from QUIC — spawn_dispatch primary, connect convenience (ADR-017 Am. 2026-07-13)
Same fix as ADR-080 (ChannelClient) applied to the call crate. The existing code already had the right structure — spawn_dispatch is not feature-gated (transport-agnostic), connect is #[cfg(feature = "quinn")] — but the docs inverted the framing: connect was 'primary,' spawn_dispatch was 'lower-level.' That welding masked the call protocol's transport-agnosticism (ADR-012 EventEnvelope; ADR-065 from_stream/from_bidi accept any AsyncRead + AsyncWrite). Changes: - client-and-adapters.md: reframe spawn_dispatch as the transport- agnostic primary constructor (one-way door), connect as the QUIC convenience (two-way door, feature-gated on quinn). Mirror the from_connection / connect_quic pattern from ADR-080/channel-client.md. Fix all 'QUIC-backed' / 'over QUIC' / 'opens a QUIC connection' framing to be transport-agnostic. Fix from_call description, adapter location map, exchange-of-operations example, Constraints section. - call-protocol.md: 'runs over QUIC bidirectional streams' → 'runs over any ordered, reliable bidirectional stream.' Fix stream model, stream lifecycle (connection drop / stream reset now list all transports, not just QUIC). Fix CallConnection.connection doc comment. - operation-registry.md: FromCall provenance comment 'QUIC forwarding stub' → 'call-protocol forwarding stub.' Fix from_call description. - call README.md: fix client-and-adapters.md description, fix design principle #11 framing. - ADR-017: add Amendment (2026-07-13) documenting the spawn_dispatch / connect reframe, mirroring ADR-080's amendment. - overview.md, architecture README: update ADR-017 and client-and-adapters.md table summaries. - OQ-015, OQ-007: fix 'opens QUIC connections' / 'bidirectional QUIC streams' framing in resolution text. No code changes — spawn_dispatch and connect already exist with the right feature-gate structure. This is a documentation reframe. |
||
|
|
a26401aadd |
docs(arch): rewrite hub README for multi-transport + channels substrate; add OQ-58 (worker registration)
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. |
||
|
|
73c621bf21 |
docs(arch): unweld ChannelClient from QUIC — from_connection primary, connect_quic convenience (ADR-080 amendment)
The channels protocol is transport-agnostic by design (ADR-071 substrate
modes; ADR-065 unwound the server-side QUIC-welding via
Connection::from_stream/from_bidi). ADR-080 had welded the client-side
one-way-door API to QUIC and masked it as a deferral ('QUIC-only
initially', 'can be generalized later') — anti-patterns #8/#9/#11
(door-type-as-deferral + resolved-with-escape-hatch). 'Can be
generalized later' meant 'can be rewritten later' — the expensive
reversal the one-way-door classification exists to prevent.
Fix: split the constructor surface.
- from_connection(connection: Connection) — transport-agnostic primary,
one-way door. Mirrors server-side ChannelsAdapter::handle(Connection)
and the existing CallClient::spawn_dispatch pattern.
- connect_quic(addr, credentials) — QUIC convenience, two-way door,
additive. Future connect_tcp_tls / connect_webtransport join it
without touching the one-way-door surface.
OQ-55 reframed: the deferred thing is the shared dial+TLS seam
(AlknetClient), not a QUIC-welded client API. The client take-over APIs
(CallClient::spawn_dispatch, ChannelClient::from_connection) are
transport-agnostic and decided; only the shared dial across transports
is blocked on a second transport's dial existing.
Files:
- decisions/080-channelclient.md: amendment section, Decision code
block, Transport-agnostic by construction (replaces QUIC-only
initially), Consequences, Door type, subscribe_resources added to
Decision block (was referenced by Door type but missing)
- crates/channels/channel-client.md: from_connection primary API,
Transport-agnostic by construction section, OQ-55 relationship
- crates/channels/overview.md, crates/channels/README.md,
crates/core/README.md, README.md, open-questions.md,
questions/055-...md: cross-reference summaries aligned
|
||
|
|
3006e29afc |
docs(arch): control format is ALPN-specific (not JSON-binding); hub/worker are consumers not sub-crates
Two refinements from the review: 1. Control stream_types (3/4/5) carry ALPN-specific payloads, not JSON. The channels layer is blind to what control stream_types carry — it reassembles bytes and delivers them to the handler. TTY happens to use JSON for its control channel because its control messages map cleanly to JSON; another ALPN might use a binary format. The channels layer does not mandate JSON on control stream_types, the same way it doesn't mandate a format for data stream_types. This prevents the TTY/PTY JSON constraint from becoming a binding constraint on all future channel types. (ADR-071, channels-wire.md) 2. Hub and worker are consumers of channels, not sub-crates. The existing alknet-hub crate IS the channels hub — it depends on channels-call and uses channels as its substrate, with the relay logic (ADR-079) living in alknet-hub. A worker is any crate that uses ChannelClient to dial. There are no channels-hub or channels-worker sub-crates. The dependency direction is: alknet-hub → channels-call → channels-core → alknet-core; worker → channels-call → channels-core → alknet-core. The channels crate has no dependency on alknet-hub or any worker crate. (ADR-081, overview.md) |
||
|
|
fd83fc1685 |
docs(arch): channels substrate simplification + stream_type decomposition + sub-crate split
Three simplifications to the channels spec, all flowing from the review: 1. Substrate simplification (ADR-071 revised): the 9-byte chunk header is used in ALL substrates — in-line (TCP+TLS, WebTransport), native (QUIC bidi streams), and multi-connection. The ChannelsAdapter reads headers off every bidi stream it accepts; the transport's native multiplexing is a performance optimization (independent flow-control windows), not a protocol change. One wire format, one code path, one handler experience. The channel_id in the header is the correlation key across substrates. 2. Stream_type decomposition (ADR-071 revised): every stream_type is unidirectional. Bidirectionality is two stream_types (write + read), not one 'bidirectional' stream_type. Grouped in threes: 0/1/2 = data write/read/err, 3/4/5 = control write/read/err, % 3 formula. This resolves the TTY control channel's 'not actually bidirectional' flaw — control is now 3 (write, client→server) + 4 (read, server→client), each with its own reassembly buffer and EOF. Channel 0 uses [0,1] (call frames bidirectional via 0=in, 1=out). TTY uses [0,1,2,3,4]. ADR-072, 073, 074, 077 updated for the new stream_type assignments. 3. Sub-crate decomposition (ADR-081 new): channels-core (pure multiplexer — wire format, demux/mux, ChannelBidiStreamSource, ChannelManager; depends on alknet-core only, no call dependency, ALPN-blind) / channels-call (channel 0 pre-negotiation + lifecycle op registrations; depends on channels-core + alknet-call) / channels-hub (relay) / channels-worker (ChannelClient). Isolates the call-protocol coupling from the pure multiplexer so the dependency graph is honest. ADR-077 (TTY inside channels) updated: TTY now uses 5 sub-streams [0,1,2,3,4] with control properly bidirectional via 3/4; amends ADR-052's stream_type assignments for direct mode too (direct alknet/tty now uses 0-4, not 0-3). Spec docs updated: channels-wire, channels-connection, channels-adapter, channel-operations, overview, README. |
||
|
|
2313c51f12 |
docs(arch): add alknet-channels specs — ADRs 071-080, 7 spec docs, OQ-56/57
Phase 1 architecture for alknet-channels (multiplexing proxy on alknet/channels). Grounded in the completed de-risk POC (28 tests) and the landed ADR-070 (BidiStreamSource trait + Connection::from_source). ADRs: - 071: 9-byte chunk wire format (generalizes TTY's 5-byte) - 072: channel 0 pre-negotiated as alknet/call (no special control plane) - 073: channel lifecycle operations on the call protocol — channel/open, close, control, resources/subscribe; direction field pinned; subscribe from day one (not poll-for-v1 — StreamingHandler machinery exists) - 074: ChannelBidiStreamSource implements BidiStreamSource (ADR-070); into_sub_streams() typed accessor for TTY; accept_bi() generic path - 075: ChannelsAdapter + ChannelManager; REQ-CH-01..04 wire invariants - 076: bounded-buffer backpressure (1 MiB), 256-channel cap, monotonic IDs - 077: TTY inside channels uses sub-streams, not own wire format; amends ADR-052 scope to direct-connect TTY; channels feature on tty - 078: two-pump shutdown-on-completion contract (handler-level) - 079: hub relay translates channel 0, byte-forwards data channels - 080: ChannelClient (QUIC-only); AlknetClient core extraction deferred (OQ-55) Spec docs: overview, channels-wire, channels-connection, channels-adapter, channel-operations, channel-client. OQ-56 (full windowing) and OQ-57 (two-pump helper extraction) are genuine deferred(scope) deferrals with concrete blocking conditions; the contracts are decided, only the extensions are deferred. Hedging audit converted three research hedges into decisions: resources/subscribe (not poll), server-assigned IDs (not if-zero-RTT), bounded-buffer (not if-HOL-becomes-a-problem). |
||
|
|
02c5b9e039 |
docs(arch): add Connection::from_source to ADR-070 — the missing extension point
ADR-070 made Connection hold Box<dyn BidiStreamSource> so downstream crates can add connection shapes without editing core. The trait and three built-in impls landed, but the public constructor that lets a downstream crate construct a Connection from its own BidiStreamSource impl was never added — the source field is private with no from_source constructor. The channels POC update surfaced this when it tried to use the trait directly and found no way to build a Connection from a ChannelBidiStreamSource. This is a gap in the ADR-070 implementation, not a new decision. ADR-070 §Consequences says 'the channels crate implements ChannelBidiStreamSource in its own crate and constructs Connection from it' — the constructor for that path is what was missing. - ADR-070: add from_source to the Constructors table + reword the downstream-crates paragraph to make the two paths explicit (from_source for custom impls; from_quinn/from_iroh/from_stream for built-in impls) - core-types.md: add from_source to the impl Connection block, the built-in implementations table, and the design-decisions table entry - tasks/core/connection-from-source-constructor.md: the implementation task (one constructor + one test, scope narrow, risk low) |
||
|
|
f8f5f27ce0 |
docs(arch): land ADR-070 BidiStreamSource + OQ-55 AlknetClient deferral + impl task
Three core-crate changes surfaced by the alknet-channels POC: - ADR-070: extract BidiStreamSource trait so Connection holds Box<dyn BidiStreamSource> instead of a closed ConnectionKind enum; downstream crates (channels, future transports) implement the trait to add connection shapes without editing core. Public Connection API preserved verbatim. REQ-CORE-02 (close() params clippy warning under --no-default-features) folded in — the signature stays on the trait, non-QUIC impls ignore the args. - OQ-55: AlknetClient / client establishment extraction deferred(scope). Blocked on a second *transport's* real client (not a second QUIC client) — extracting a QUIC-shaped connector now would bake QUIC in as the establishment shape, the same welding ADR-065 unwound on the server side. Each crate builds its own client standalone for now. - core-types.md / auth.md / overview.md / README indexes updated to reflect ADR-070 and OQ-55. Architecture review: zero critical issues. - tasks/core/bidistreamsource-trait.md: the implementation task (Parts 1-3: BidiStreamSource refactor, close() fix, AuthContext::anonymous). |
||
|
|
deea6de38a
|
docs(arch): remove channels from hub spec — channels are research-phase, not specced
The channels concept (docs/research/alknet-channels/phase-0-findings.md) was developed in a separate session and is research-phase, not architecture. The hub spec was inadvertently built assuming a channel model that doesn't exist yet. Changes: - Remove all channels references from hub README (subtitle, channel model section, channel management bullet, channel proxying section, 'does NOT do' bullet, references) - Remove OQ-55 (channel/open operation) — channels research has its own question tracking (OQ-CH-01 through OQ-CH-07) - Hub spec now describes what it actually provides: peer lifecycle, aggregated operation env, service discovery. One QUIC connection per peer carrying the call protocol. No channels, no future multiplexing, no research references. |
||
|
|
5d56bae1ba
|
docs(arch): fix inbound worker hook — callback inside handle(), not post-hoc
- Replace on_worker_connected() post-hoc call with WorkerConnectedCallback that fires inside CallAdapter::handle(). handle() blocks until disconnect — there is no 'after handle accepts' point for the assembly layer to hook into. The callback carries both on_connected (from_call + attach_peer) and on_disconnected (detach_peer + drop channels). - Add CallAdapter::with_worker_connected_callback(callback) builder method. - Consolidate duplicate WorkerConnectedCallback struct definitions. - Fix channel role: 'channel proxying' → 'channel management'. The hub tracks channels; it does not proxy streams. What the caller does with the resulting channel is the assembly layer's business. - Resolve OQ-54: callback is the committed design. Update OQ file and open-questions.md table. |
||
|
|
b38b1d28a7
|
docs(arch): channel model — call protocol as control plane, any ALPN as data plane
Replace the 'future strategies' section with the channel model: - Channel 0 is the call protocol — the universal control plane. Handles operation discovery (from_call), operation routing (invoke_peer), service discovery, and channel negotiation (channel/open, channel/close, channel/list). - Channels 1..N carry any ALPN as data planes. Opened via channel/open on the call protocol. Each channel is a bidirectional QUIC stream, wrapped as Connection::from_bidi (ADR-065), and handed to the same ProtocolHandler::handle() that handles dedicated connections. The handler does not know it's on a multiplexed channel. - Channel negotiation is symmetric — either side can open a channel. Same pattern as from_call: bidirectional, symmetric, negotiated over the call protocol. - The hub's role for channels 1..N is transparent stream proxying. The hub does not interpret the protocol; the client and worker speak the ALPN directly. - Hub struct gains channel tracking (PeerId → ChannelId → ChannelInfo). HubError gains ChannelAlreadyOpen, ChannelNotFound, ChannelOpenFailed. - New OQ-55: channel/open operation spec (deferred to call-protocol implementation phase). |
||
|
|
ab963d0e46 |
docs(arch): resolve OQ-050 — include docker/system/events in v1
Resolve the deferred docker system events subscription question. docker/system/events is now a v1 Subscription operation using the same StreamingHandler pattern already wired for logs, exec, and image/pull. The internal ownership-store subscription for stale-entry cleanup on destroy events is a follow-up refinement. Scrub hedging language from ADR-060 and docker specs: - Remove 'marginal gain' / 'future feature is additive' framing - Replace 'no reaper' / 'not promptly cleaned up' with clean statement that events subscription provides the prompt-cleanup path - Remove stale-entry policy from ADR-060's two-way door classification |
||
|
|
11f531131c |
docs(arch): clean unwrap, remove stale ownership, protocol-agnostic hub core
- Replace .unwrap() with .expect() in spec pseudocode; remove .unwrap() from ADR-067 prose. Specs describe intent, not implementation details. - Remove 'ownership reaping' from hub's 'does NOT do' list. Ownership is an alknet-docker concern, not a hub concern. The other agent's hedging leaked into the spec; removed. - Restructure hub README: separate core peer lifecycle (protocol-agnostic) from call-protocol strategy (first strategy). Add extension points section for future tty/blobs/custom strategies. A QUIC connection can multiplex multiple protocols over separate streams; the hub's core manages the connection, each strategy manages its own protocol-specific state. The call-protocol strategy handles the majority of real-world use cases; the architecture keeps the door open for others. |
||
|
|
87b2c2a5ef |
docs(arch): hub-wiring cluster — aggregated env, peer_operations, from_call cleanup, alknet-hub crate spec
Three ADRs addressing gaps surfaced by the first hub consumer (alkapi): - ADR-067: Aggregated peer-env wiring — Dispatcher::with_aggregated_env hook so compose_root_env reads a shared PeerCompositeEnv across all calls, not a fresh per-call one. The hub-defining gap (alkapi OQ-08 / G.1). - ADR-068: PeerCompositeEnv::peer_operations override — adds list_operation_names() to OperationEnv, overrides on OverlayOperationEnv and PeerCompositeEnv. Fixes services/list-peers returning empty operation lists for non-local peers (alkapi G.6). - ADR-069: from_call is a manual free function, not auto-wired — reverses the aspirational OQ-27 resolution to match the implementation. Cleans the 'v1 default' hedging language in ADR-017 and client-and-adapters.md (alkapi G.4). New crate spec: alknet-hub — reusable hub pattern (aggregated env, connection lifecycle, worker supervision with backoff, service discovery). Spec fixes: builder API drift (with_local/with_local_streaming separation), ScopedOperationEnv → ScopedPeerEnv type name, OQ count 51→54. Three new OQs: OQ-52 (wait_for_close), OQ-53 (backoff defaults), OQ-54 (inbound hook placement). |
||
|
|
97456bc608 |
docs(arch): ADR-066 — move from_jsonschema to alknet-http as HTTP-backed single-endpoint adapter
from_jsonschema was in alknet-call as a schema-only placeholder with a NOT_FOUND handler — broken (an op in the registry needs a real handler) and in the wrong crate (alknet-call has no HTTP client; a useful from_jsonschema needs reqwest like from_openapi). ADR-066 moves it to alknet-http as a real reqwest-backed single-endpoint adapter for non-standard/non-OpenAPI REST endpoints, functionally similar to from_openapi but one endpoint at a time. FromJsonSchema provenance stays in alknet-call (now a handler-bearing leaf). - New ADR-066 (supersedes ADR-017 §5 from_jsonschema clause + ADR-022 FromJsonSchema row; both amended with strikethrough + pointer) - Updated specs: call/client-and-adapters, call/README, call/operation-registry, http/http-adapters (new from_jsonschema section), http/overview, http/README, arch README + overview - New task: tasks/http/adapters/from-jsonschema.md (depends on from-openapi; includes alknet-call cleanup of the broken placeholder) - Old task tasks/call/client/from-jsonschema.md marked superseded taskgraph validate: 116 tasks valid |
||
|
|
2aa6363e57 |
docs(arch): sync specs to transport generalization sweep (ADR-064, ADR-065)
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.
|
||
|
|
1ce11717d8 |
docs(arch): draft alknet-docker architecture specs (ADRs 058-063, OQs 048-051)
Greenfield architecture spec set for the alknet-docker crate — a thin, single-host bollard wrapper exposing docker container/image operations as call-protocol ops on the shared alknet/call ALPN, plus a DockerTtyBackend (impl TtyBackend) behind a tty feature for interactive terminal sessions into containers over alknet/tty. Six ADRs: - 058: docker ops on alknet/call (no separate ALPN; raw-carriage handoff dissolved by alknet-tty extraction — interactive attach moved to alknet/tty via DockerTtyBackend, no carriage field on call.requested) - 059: bollard 0.21 (verified current on crates.io) + feature selection (http+pipe+time; no ssl/ssh/websocket/buildkit) - 060: container resource model (ADR-050 application) — alknet.managed/ alknet.owner labels, list owned_only flag, hosted-services operator role via static-resource fallback, handler-driven revoke with autonomous-death tolerance, resource-action vocabulary - 061: DockerTtyBackend in alknet-docker behind tty feature (attach vs exec mode; POC drive_attach_raw as reference) - 062: Docker client + OwnershipStore injection via closure capture (not Capabilities, not OperationContext — matches from_openapi pattern) - 063: exit code on terminal call.responded for non-interactive exec (call.completed stays empty, ADR-012 unchanged) Four spec docs (crates/docker/): README, overview, docker-operations, docker-tty-backend. Four deferred-scope OQs (048-051): network/volume ops, buildkit, system events subscription, create options surface. Updates the tty-backend.md Backend implementations table (DockerTtyBackend row now specced) and the architecture README (doc table, ADR table, current-state paragraph, OQ count). Grounded in the alknet-docker POC (docs/research/alknet-docker/poc-summary.md) which validated the hard parts; the remaining lifecycle ops are mechanical bollard wrapping. Reviewed by architecture-reviewer subagent; criticals (the docker_client injection model conflicting with the Capabilities contract) resolved via ADR-062/063 before commit. |
||
|
|
dbac972aaa |
docs(arch): ADR-057 — alknet-tty does not depend on alknet-call (self-contained negotiation framing)
The earlier specs claimed alknet-tty reuses alknet-call's FrameFramedReader/FrameFramedWriter 'framing utility' (ADR-052 §6, ADR-003 Am. 1). A pre-implementation sanity check found this was unsound: FrameFramedReader::read_frame() is hardcoded to deserialize EventEnvelope — the length-prefix read and the type-specific deserialize are one entangled call, not a separable utility. alknet-tty's negotiation payload is a NegotiateRequest, not an EventEnvelope, so the claimed reuse did not exist in a usable form. ADR-057: alknet-tty implements its own ~30-line length-prefixed framing directly on tokio AsyncRead/AsyncWrite. The format coincides with alknet-call's framing by convention (both are length-prefixed JSON); the implementations are independent. alknet-tty depends on alknet-core only — no alknet-call dependency. The ~30 lines of framing is an idiom, not a domain abstraction worth a cross-crate dependency. Updates: - ADR-003 Amendment 2: clarifies alknet-tty does not depend on alknet-call; the Am. 1 protocol-foundation exception remains for alknet-http/agent/napi (actual type reuse), not alknet-tty. - ADR-052 §6: framing is self-contained in alknet-tty; format coincides by convention, not by code reuse. - overview.md: dependency block drops alknet-call; new 'Why no alknet-call dependency' subsection explains the unsound-reuse finding. - tty-wire.md, tty-backend.md, tty README, crates/tty/README.md, top-level README: ADR tables, Design Decisions tables, and dependency edges updated to reflect alknet-core-only dependency. - ADR-053, ADR-054: References updated to ADR-003 Am. 2 + ADR-057. |
||
|
|
25f0f55592 |
docs(arch): alknet-tty spec sanity-check fixes — TtyControlHandle newtype (C1), backend cleanup contract (W1, ADR-056), clarifications (S1-S5)
C1: Box<dyn TtyControl + Clone> does not compile (Clone is not object-safe). Replace with a TtyControlHandle newtype: #[derive(Clone)] wrapping Arc<dyn TtyControl + Send + Sync>. The trait stays object-safe; the newtype carries the Clone-ability. Updated across tty-backend.md, ADR-053, OQ-43, and the tty README. W1: 'Drop is the cleanup, threads exit on channel close' was false for the local PTY waiter thread (blocked in Child::wait(), does not observe channel close) — a child that ignores stdin EOF would be orphaned on session cancel. New ADR-056 commits a backend cleanup contract: dropping the exit_code future MUST kill the session target. The local backend implements it via a ChildKiller held in the exit_code future's Drop guard (disarmed on resolve). Corrected the lifecycle section in tty-adapter.md, added the mechanism + constraint to tty-local.md, and referenced ADR-056 from tty-backend.md, overview.md, both READMEs. S1: error-frame < 16 MiB stated as a wire-format invariant (not an assumption) so the 0x00-first-byte disambiguation trick is sound by construction. S2: carriage field validation (MUST be "raw" in v1, else malformed_negotiation) specified. S3: NegotiateRequest + TerminalParamsWire Rust structs added with serde defaults and validation rules. S4: modes field annotated 'backends MUST ignore content in v1' in both tty-backend.md and ADR-053. S5: AsyncWrite/Stream qualified with crate origins (tokio::io, futures_core) in the TtyHandle struct definitions. |
||
|
|
b90d717ac2 |
docs(arch): amend ADR-053 — opaque backend params + resource_id(), complete the inversion
The BackendParams typed enum was a spec bug: it created a partial inversion (trait inverted, params not), modeled the SSH channel as an input when it is an output (the backend opens it in allocate()), and created a dependency contradiction — SshChannelRef could not live in alknet-ssh (alknet-tty would depend on it) nor in alknet-tty (would pull in russh types) without violating the inversion ADR-053 establishes. Replaced with an opaque serde_json::Map: the adapter passes the negotiation frame's backend-specific fields through verbatim; each backend deserializes its own strongly-typed params struct. alknet-tty has zero knowledge of any backend's params shape. Added a resource_id() default method to TtyBackend so the ownership check (ADR-050) is backend-driven rather than the adapter hardcoding docker's container field extraction. The inversion is now complete — both the trait and the params are inverted; adding SSH (or any future backend) is purely additive: zero changes to alknet-tty, zero changes to existing call sites. |
||
|
|
db4da1e6f0 |
docs(arch): resolve OQ-45 — no application-level windowing; QUIC per-stream flow control suffices
The backpressure chain is complete by construction: QUIC flow control
→ bounded drainer channel → bounded stdout channel → OS pipe/PTY buffer
→ process write() blocks. Every link awaits its producer; no unbounded
buffer breaks the chain. The reversal path (an additive ControlMessage
variant, not a wire-format header change) is noted in ADR-052's
consequences as a two-way door, not the expected path.
Tightened ADR-052 assumption 1 from a hedge ('expected to work; a POC
would confirm') to a decided constraint. Updated all OQ-45 references
across the tty spec docs from 'open (low risk)' to 'resolved'.
|
||
|
|
df5d04af1d |
tasks(arch): adopt taskgraph for architecture work — blocker tasks for deferred OQs, level mapping, status fixes
Establishes the tasks/architecture/ blocker-task half of the Safe Exit protocol. Each deferred OQ now has an external-trigger tracker task representing its unblocking condition, and the OQ's Blocked on field names the tracker task ID — closing the gap where the deferral field existed but the task-graph half was unenforced. Changes: - Add 6 external-trigger tracker tasks for the deferred OQs (OQ-09, 10, 32, 41, 44, 46) under tasks/architecture/, tagged [external-trigger, deferred-oq] - Add structured Blocked on field to OQ-09 and OQ-10 (previously used legacy 'deferred' status with the reason in the Resolution prose); index now surfaces all 6 deferred OQs with concrete conditions, no placeholders - Document the architecture-task level mapping (level: implementation for ADRs/specs, decomposition for spec-to-ADR breakdown, planning for backlog seeding, research/review direct fits) and the deferred-OQ blocker-task pattern in docs/sdd_process.md - Mark architecture/safe-exit-blocker-task-mechanism and architecture/oq-09-10-blocking-conditions completed (implemented by this pass) - Fix 4 pre-existing ADR-050 implementation tasks: status: done → completed (taskgraph enum is 'completed', not 'done') — taskgraph validate now passes for all 100 tasks |
||
|
|
1baa619ce9 |
docs(arch): decompose open-questions.md into per-OQ files under questions/
The monolithic open-questions.md (1310 lines, 47 OQs) was large enough to be unmanageable, with high size variance (OQ-42 at 220 lines next to OQ-06 at 8). Decomposed into one file per OQ under docs/architecture/questions/ (NNN-slug.md, mirroring the ADR convention), with open-questions.md retained as the index: theme-grouped tables plus a cross-theme Deferred/Blocked section that surfaces the 6 deferred OQs with their Blocked-on conditions inline (the safe-exit visibility surface). Per-OQ content moved verbatim; all 62 inbound links stay valid (none used anchors). README's curated OQ summary dropped (now redundant with the index tables). Also seeds tasks/architecture/ with this task plus two follow-ups found during the decompose: OQ-09/10 missing structured Blocked-on fields, and the tasks/architecture/ blocker-task half of the Safe Exit protocol being unenforced. |