Commit Graph
227 Commits
Author SHA1 Message Date
glm-5.2 c819d99f1d docs(typedef): ADRs 099-102 — Int64/Uint64, aligned-mode restrictions, read factory
ADR-099: Int64/Uint64 as first-class kinds. The POC targets (SFTP
offset: u64, metatensor data_offsets: u64) require 64-bit integers.
The prior Uint64 addition was removed because it was half-finished;
this ADR specifies the complete addition (layout + validator + API).

ADR-100: Reject non-final inline length-prefixed variable fields in
aligned mode. The OffsetMap reserves only 4 bytes (the length prefix),
but write_string writes prefix+data inline — clobbering subsequent
fields. Non-final variable fields must use maxLength or offset-indirect.

ADR-101: Packed-mode read API — engine as SequentialReader factory.
engine.sequential_reader() returned &SequentialReader but read_next
needs &mut self — dead API. Now returns an owned fresh reader.

ADR-102: Reject TUnion in aligned mode for v1. The aligned-mode union
code had three bugs (no variant offsets, first-variant discriminator
offset, misaligned variant region). Unions are the protocol pattern;
mmap formats use structs/arrays. Reversible when a consumer needs it.
2026-07-22 09:36:55 +00:00
glm-5.2 7603f98799 docs(typedef): sync specs with the implemented API surface
The code introduced concrete public types and a unified API during
implementation that the specs described only conceptually. Sync the
specs to match the code:

- schema-layer.md: document the TypeDefKind enum and its inherent methods;
  add a Schema-Layer Public API section (get_typedef_kind vs
  get_typedef_kind_loose, annotation parsers, Endian/VariableEncoding/
  DiscriminatorKind, normalize_refs/resolve_ref/resolve_ref_or_inline);
  fix the TRecord layout (values are encoded by their declared kind, not
  universally value_len-prefixed); fix the alignment default list.
- layout-engine.md: document the LayoutMode enum and the ByteRange/
  FieldPosition/PackedLayout/OffsetMap public types with their actual
  signatures; update the LayoutBuilder/SequentialReader/OffsetMap
  component descriptions with the real new/build/compute signatures.
- data-access.md: document the FieldValue enum; add a Higher-level
  read/write section (TypedefEngine::read_field/write_field,
  SequentialReader::read_next/read_field); fix primitive signatures to
  include field_path and Endian; replace the wrong
  read_union_discriminator pseudo-code with the actual tunion module API
  (read_byte_discriminator/read_field_discriminator/resolve_variant/
  discriminator_size) and the UnionDispatch struct.
- validation.md: fix the TypedefEngine struct (add endian/schema fields,
  mark Layout as private); add the real compile signature (&mut Value,
  LayoutMode) and mode-appropriate accessors; fix engine.validate(buffer)
  -> engine.validate_json(&Value)/is_valid_json (the validator operates
  on serde_json::Value, not byte buffers — matches ADR-098).
- overview.md: remove the stale ~1,900 lines / 26 tests line count.
- ADR-097 §3a: correct the TRecord layout (no separate value_len prefix;
  the value is encoded by its declared TypeDef:* kind).
2026-07-21 13:40:12 +00:00
deepseek-v4-pro dd232c3d47 fix(typedef): fix code examples and cross-doc inconsistencies from second review
- Fix code examples hardcoding little-endian: read_string, write_string,
  read_string_indirect now take endian parameter and use match on Endian.
- Fix TUnion dispatch examples: remove undefined functions (read_u8,
  read_field, read_struct, read_f32_raw), remove Value returns
  (contradicts 'no intermediate Value tree'), add endian-aware
  discriminator reading for Uint8/Uint16/Uint32.
- Fix read_f32 example: inline the endian-aware conversion instead of
  calling undefined read_f32_raw; document it as aligned-mode only.
- Fix architecture README: 16→17 kinds in schema-layer and validation
  descriptions.
- Fix ADR-095: clarify validation operates on Value instances, not raw
  byte buffers directly. Fix 'defense in depth' paragraph.
- Fix ADR-097: add §3a defining TRecord 'values' property shape.
- Fix ADR-098: TTimestamp format ISO 8601→RFC 3339.
- Tighten OQ-071 impacts field: state what IS blocked, not just what
  isn't.
2026-07-21 08:42:13 +00:00
deepseek-v4-pro 01cc3a0367 docs(typedef): clarify OQ-070 WASM vs no_std distinction
WASM (wasm32-unknown-unknown) has std via wasm-bindgen and is not
blocked by this OQ. The crate is WASM-clean by construction. This OQ
is about bare-metal embedded targets only.
2026-07-21 08:05:24 +00:00
deepseek-v4-pro c6ab00d141 fix(typedef): resolve spec inconsistencies from sanity check
- Remove TEnum 'always LE' exception (no POC basis, contradicts ADR-097).
  TEnum now follows schema endianness like all other fixed-size types.
- Document TEnum design change: u32 index is a deliberate deviation from
  TypeBox's string enum for binary efficiency.
- Document TBytes as alknet-typedef addition (not in TypeBox typedef.ts).
- Fix kind count inconsistency: all docs now consistently say 17 kinds.
- Fix TTimestamp validation contradiction: clarify data-access layer vs
  jsonschema validator responsibility.
- Add TEnum read/write coverage to data-access.md.
- Add TEnum endianness cross-reference to layout-engine.md.
- Clarify TBytes binary-vs-JSON representation in validation.md.
2026-07-21 07:44:58 +00:00
deepseek-v4-pro a941d86c3a docs(typedef): add $ref normalization step for TypeBox interop
TypeBox generates bare-name $ref values ("$ref": "Read") within
$defs blocks. The jsonschema crate requires full JSON Pointer paths
("$ref": "#/$defs/Read"). Verified by generating actual TypeBox
output and testing against jsonschema v0.46.5 — bare-name refs fail
with 'Resource is not present in a registry'.

Add a ~20-line normalize_refs() pre-processing step that rewrites
bare-name refs to full JSON Pointer paths at schema load time. The
normalization is idempotent — full paths pass through unchanged.
2026-07-20 12:27:52 +00:00
deepseek-v4-pro 85c5590001 docs(architecture): add alknet-typedef crate specs, ADRs 095-098, and OQs 069-071
Add the alknet-typedef architecture specification — the binary struct
engine that takes JSON Schema with TypeDef:* custom keywords and produces
offset maps, read/write functions, and validation.

Specs (docs/architecture/crates/typedef/):
- overview.md: purpose, 'schema is the format' principle, consumers, scope
- schema-layer.md: 17 TypeDef:* kinds, jsonschema integration, annotations
- layout-engine.md: two layout modes, three variable-length strategies
- data-access.md: read/write, TUnion dispatch, field paths, zero-copy
- validation.md: custom keyword validators, TypedefError, TypedefEngine

ADRs:
- 095: Purpose, scope, and the jsonschema engine
- 096: Two layout modes — packed sequential vs aligned static
- 097: Schema annotations — endianness, alignment, encoding, TUnion
- 098: Error handling and validation strategy

OQs (deferred(scope)):
- 069: Arrays of variable-length-element structs
- 070: no_std + alloc support
- 071: Builder API for schema construction

Index updates: README doc table + ADR table, open-questions.md theme
table + Deferred/Blocked section, overview.md crate graph.

Grounded in the alknet-typedef POC (26 tests passing) and the
call-channels-unification research. Reviewed by architecture-reviewer;
all critical issues, warnings, and suggestions addressed.
2026-07-20 11:57:03 +00:00
deepseek-v4-pro bf0f827bf4 docs(research): add FFI and WASM ABI section to alknet-typedef findings
Covers schema-driven cross-language interfaces, WASM linear memory
model fit, cross-language schema portability, defense in depth
(Rust + WASM sandbox + schema validation), and implications for
the call crate's WASM-friendliness.
2026-07-20 09:49:21 +00:00
deepseek-v4-pro 076d5adfec docs(research): add alknet-typedef findings with POC results
POC 1 (core offset computation) and POC 2 (russh-sftp round-trip)
are complete with 26 passing tests. Key architectural finding:
two layout modes are needed — packed sequential for protocol wire
formats (LayoutBuilder/SequentialReader) and aligned static for
mmap-friendly formats (OffsetMap).

The jsonschema crate's custom keyword API handles all 16 TypeDef:*
kinds. TUnion byte-offset discriminator dispatch confirmed against
russh-sftp's own serialization byte-for-byte.
2026-07-20 09:27:21 +00:00
deepseek-v4-pro 3543c1bb7a findings: known gaps, wire format family, and alknet-typedef unification
- Add Known gaps section (A-G): Pub handler shape, hub broker spec,
  from_call relay wrapper, channel_id allocation in Pub case,
  OperationEnv coupling, channel_open wire format, resource_id_path
  ACL vs handler ownership
- Add Wire format family section: call JSON, call binary, channels,
  TTY all share [discriminant][length][payload] shape; binary call
  frame is 9 bytes vs JSON's ~80+
- Add alknet-typedef section: JSON Schema with TypeDef:* custom
  keywords as the binary struct engine, replacing per-protocol serde
  structs, typebox-rs, and per-handler wire format parsers
- Cross-reference Gap E resolution in open questions
2026-07-19 20:21:54 +00:00
deepseek-v4-pro 7cdff8c127 findings: Pub/Sub replaces open/expose — OperationType::Pub/Sub carries direction, ChannelDirection enum dissolves, marker simplifies to just alpn, hub is the broker matching Pub↔Sub by (op_name, params_hash) 2026-07-19 17:57:26 +00:00
deepseek-v4-pro 9d855a774d findings: dissolve the third category — TTY/tunnel/etc are just call apps, not a separate TLS-layer category; the endpoint determines framing, not the app 2026-07-19 17:10:32 +00:00
deepseek-v4-pro 90a8fa7328 findings: reframe — channels is call with a binary data plane, not 'call + data channels'; retire 'call++' for 'binary-stream call apps'; two modes: default call (JSON) and channels (binary framing) 2026-07-19 17:09:16 +00:00
deepseek-v4-pro 8ca2d0632d findings: expose is specced, not deferred — subscription model as matching mechanism, (op_name, params_hash) pubsub, stream dedup, step-by-step proxy walk-through 2026-07-19 16:50:02 +00:00
deepseek-v4-pro 0ef277730f findings: reframe relay flow with producer/consumer terminology, hub-as-proxy pattern, concrete opencode-on-remote example, N-consumer fan-out sketch 2026-07-19 16:33:17 +00:00
deepseek-v4-pro 7c1af0d71f findings: add terminology section — three role axes (deployment/call/data-plane), retire ALPN-server/client for producer/consumer, define assembly layer 2026-07-19 15:00:24 +00:00
deepseek-v4-pro 9a58714519 findings: address review gaps — per-connection plumbing, wire-visible marker, FromCall relay wrapper, defer expose, fix discovery filtering, WS dual-mode, nits 2026-07-19 14:46:04 +00:00
alkimiadev f73c6035b5 doc(fix) fixed factual error regarding model license 2026-07-19 14:21:39 +00:00
glm-5.2 74c1007bdf docs(research): call-channels-unification findings — openable ALPNs are operations
Iterating in docs/research/ per the stream-unification pattern; syncs
to docs/architecture/ and the ADRs only after it settles.

Working through the `channel/open` ACL granularity gap surfaced a
larger unification: channels is "call + data channels" ("call++"),
and the ALPN crates served under channels are call-consuming apps in
the same shape alknet-docker is a call-consuming app. The lineage
(call → docker → tty → channels) closes here.

Records three gaps from an outside review + the ALPN-category tangle
that fell out of working the first one:

- Gap 1: `channel/open` ACL granularity underspecified. Resolved by
  "an openable ALPN is an operation" — per-ALPN ops in
  `channels/<alpn>/open` and `.../expose` on the call
  OperationRegistry, with a `channel_open` marker on OperationSpec
  (registry metadata, not auth machinery). Two verbs (open/expose)
  give the two direction values separate ACLs. Avoids re-committing
  ADR-028's parallel-authorization structural miss one layer down.
- Gap 2: quota accounting leaks (ADR-094). Responder-initiated close
  decrements the wrong ledger; transport drop never decrements.
  Fix: per-connection opener ledger in channels-call (channels-core
  stays auth-blind), decremented on every teardown path. The trait
  shape survives.
- Gap 3: connection-count DoS is an unowned layer. New OQ against
  alknet-endpoint, deferred(scope) — named to stop the re-tangle.
- Gap 4: ALPN category blur (ADR-086 §4). The "channels data-channel
  ALPNs" category reframes to "call++ apps" — they inherit call's
  auth by construction; the data-channel part is the channel_open
  marker. SSH stays distinct.

Includes the hub-relay + worker-expose flow walked end-to-end under
the new model (both hold), the ADR plan (ADR-095 + amendments to
073/094/086/048/057 + clarification to 058), per-crate changes, and
six OQs with concrete resolution paths.

ChannelCore seam (wrapper shape) is the architecture decision; the
exact API shape is POC-flagged.
2026-07-19 14:12:44 +00:00
glm-5.2 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.
2026-07-19 11:17:57 +00:00
glm-5.2 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.
2026-07-19 04:07:49 +00:00
glm-5.2 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).
2026-07-18 18:10:17 +00:00
glm-5.2 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.
2026-07-18 17:02:59 +00:00
glm-5.2 859ad35896 fix(http/test-helper): branch on op_type in full_registry_with_ops (ADR-049 kind validation)
The to_mcp test helper full_registry_with_ops always registered ops
with HandlerKind::Once(make_echo_handler()) regardless of op_type.
When the search_returns_access_control_filtered_ops_excluding_subscriptions
test passed OperationType::Subscription for "events/stream", the
registry's kind validation (tightened in commit 9c81129, ADR-049)
rejected it with "handler kind mismatch: Subscription requires
HandlerKind::Stream (got HandlerKind::Once)" — panicking in
register().unwrap() before the test could run.

This was a pre-existing test-helper bug (predates Phase 6; verified by
stashing Phase 6 and reproducing on the develop baseline) but it
blocked Phase 9's 'Done when' criterion (cargo test -p alknet-http
passes).

Fix: added a handler_kind_for(op_type) helper that branches on op_type
(HandlerKind::Stream(make_echo_streaming_handler()) for Subscription,
HandlerKind::Once(make_echo_handler()) for Query/Mutation) and used
it in both register loops of full_registry_with_ops. The streaming
echo handler yields the input back as a single call.responded frame —
sufficient because the test only verifies that the MCP search tool
*excludes* Subscription ops from its listing; it never invokes the
handler.

Result: cargo test --workspace --all-features is fully green (1008
tests, 0 failures). Phase 9's 'Done when' criterion is met. The
findings doc's Phase 9 entry is updated to record the fix.

Closes Phase 9.
2026-07-18 16:14:39 +00:00
glm-5.2 5902c8aca9 docs(research): mark Phase 6 done, Phase 9 subsumed; note pre-existing to_mcp test failure
Phase 6 (BiStream unification) is complete (commit b60a584). Update the
findings doc to reflect what actually shipped and how it overlaps with
the remaining phases:

- Phase 6: marked Done. Added a 'What was done (cross-crate)' section
  listing the actual changes per crate (alknet-core, alknet-http,
  alknet-tty, alknet-call), so the doc records the implementation
  shape not just the plan.

- Phase 9: marked Done — subsumed by Phase 6. ADR-092's migration
  step 2 includes the alknet-http call-site update (drop QuicStream),
  so Phase 6's call-site work landed Phase 9's deliverable. grep
  confirms no QuicStream/QuicStreamDuplex remains.

- Phase 9: added a 'Pre-existing test failure to fix in a follow-up'
  note for the to_mcp::tests::search_returns_access_control_filtered_ops_excluding_subscriptions
  failure. The bug is in the test helper full_registry_with_ops
  (to_mcp.rs:501-516) — it always uses HandlerKind::Once even for
  OperationType::Subscription, which the registry's kind validation
  (tightened in commit 9c81129, ADR-049) rejects. Predates Phase 6
  (verified by stashing); blocks Phase 9's 'Done when' criterion
  (cargo test -p alknet-http passes) and needs a small follow-up.

- Intermediate-states table: updated rows 6, 7, 8, 9. Phase 6 and 9
  marked Done; Phase 7 and 8 noted as unchanged by Phase 6 (Phase 7's
  work is in wire.rs/control.rs which Phase 6 didn't touch; Phase 8 is
  docs-only).
2026-07-18 16:09:09 +00:00
glm-5.2 249370345f docs(research): add phases 6-9 — stream unification, TTY control fix, channels spec, http fix
Insert four new phases between the call prune (5) and the old http fix:
- Phase 6: Core stream unification (BiStream as handler leaf, ADR-092)
- Phase 7: TTY control-channel bidirectionality fix (STREAM_CTRL_IN/OUT)
- Phase 8: Channels spec cleanup (8-byte wire format, no stream_type)
- Phase 9: HTTP fix — drop QuicStream wrapper (now unnecessary after BiStream)

The old Phase 6 (http fix, deferred) is replaced — BiStream makes the
QuicStream wrapper dead code. Total: 10 phases (0-9).
2026-07-18 15:00:35 +00:00
glm-5.2 f03e38326c docs(research): resolve 8-vs-9-byte question — channels wire format is 8 bytes
Settle the open question: channels header is [channel_id:u32][length:u32]
(8 bytes) with opaque payload. The 9-byte alternative (including
stream_type in the channels header) is rejected — it leaks a handler
concept into the channels layer. The handler owns its framing entirely
within the payload. TTY's 5-byte format composes as payload bytes;
total header for TTY inside channels is 13 bytes (8 + 5).
2026-07-18 14:04:44 +00:00
glm-5.2 073bbba06a docs(research): rewrite stream-unification findings — channels as pure channel multiplexing
The previous framing ('mod 2 vs mod 3 vs mod 4 for the stream_type
space within a channel') was a symptom. The actual question is the
separation of concerns between the channels layer and the handler.

Resolution: the channels layer routes by channel_id only; handlers
own their sub-multiplexing on the BiStream they receive. Every
channel is a BiStream. The 'pass a stream to/from any ALPN' objective
becomes universal, not qualified.

The wire formats compose by construction: the 9-byte channels header
is the 5-byte TTY header with channel_id:u32 prepended. The channels
layer adds channel_id on write, strips it on read, hands the inner
5 bytes to the TTY handler. TTY's wire.rs works as-is. The
'double-chunking' objection (ADR-077's reason for rejecting
sub-multiplex inside channels) was about a 14-byte double-header; the
actual composition is 9 bytes total, shared across both layers because
the length prefix is shared.

This dissolves:
- The mod 2/3/4 question at the channels layer (the channels layer
  has no stream_type concept).
- The 'control isn't actually bidirectional' TTY flaw (TTY owns its
  sub-streams; stream_type 3 = ctrl_in, 4 = ctrl_out at the TTY layer).
- The 'into_sub_streams() as a second-class accessor' (removed;
  accept_bi is the only accessor, yields one BiStream per channel).
- The recursive composition question (made cleaner — strip a prefix
  at every level, uniform shape).
- The 'merge and split stderr' confusion (stderr is a handler concern;
  the channels layer carries bytes; TTY owns the stdout/stderr
  distinction).

ADR-077 is reversed: TTY always uses its 5-byte format, the channels
layer carries it transparently. The two-mode TTY design is preserved
but differs only in BiStream source, not in parsing.

No production constraint (develop branch is a rewrite, no one is
using this version yet). The decision is purely 'what's cleanest.'

One open sub-question: 8 bytes vs 9 bytes for the channels wire format.
9 bytes preserves TTY's wire.rs via literal strip/add; 8 bytes is more
uniform across inner layers but requires rewriting TTY's format.
Default assumption: 9 bytes (the strip/add property is the elegant one).

ADR-093 is ready to draft. The structural question is resolved.
2026-07-18 07:32:53 +00:00
glm-5.2 b5397f61aa docs(research): rewrite stream-unification findings — focus on the multiplexing layer
The previous draft was mixing two layers (transport leaf and stream_type
multiplexing) and including side-topics (WsBidiStream home, etc.) that
weren't load-bearing, which confused agents into conflating tokio::io::
join/split (ADR-092's layer, settled) with the demux/mux stream_type
layer (this doc's layer, in progress).

Rewrite to be focused:

- Layering section upfront separates transport leaf (ADR-092, settled),
  multiplexing (this doc), and channel protocol (ADR-072/073, settled).
  The two questions that got conflated are explicitly separated.
- Drop the ADR-092 recap (it's in the ADR, not this doc's concern).
- Drop the 'five abstractions' table (ADR-092's framing, not this doc's).
- Drop the WS open question (irrelevant to multiplexing).
- Record that POC 1 (stderr split/recombine) is already answered by the
  existing POC evidence: per-stream_type independent demux/mux (verified
  in demux.rs:91-109, 161-181 and mux.rs:58-63, 152-177) means the
  'unused write half' is an idle mpsc channel, not a wart. The mod-2
  framing is trivially clean. No new POC needed.
- The mod-2-vs-mod-3 question is settled by existing evidence; ADR-093
  is ready to draft.
- The one open question that benefits from a POC is POC 2
  (TTY-direct-as-channels, for the format-convergence / retire-5-byte
  call). POC 3 (recursive composition) is low leverage, deferred.
- The TTY control channel flaw is flagged as implementation-lag, not a
  design question (fix specified in ADR-077, subsumed by mod-4 instance
  framing).

This drops the scope to what the doc is actually about: the stream_type
convention and the TTY/channels convergence.
2026-07-18 05:51:54 +00:00
glm-5.2 909935ded3 docs(research): add stream-unification findings — the leaf, the instance, the convergence
Captures the deep dive that started as the alknet-crate-extraction
Phase 6 tangle and surfaced a layered issue:

1. Transport leaf split (ADR-092, drafted+pushed separately) — accept_bi
   returns BiStream, from_stream removed, from_bidi is the only public
   stream constructor. Load-bearing, separable.
2. Control channel 'isn't actually bidirectional' in TTY code (wire.rs
   STREAM_CONTROL=3 one stream both sides write). ADR-071 already fixes
   at wire-format level (3=ctrl_in, 4=ctrl_out); TTY code lags.
3. ADR-071's mod-3 stream_type decomposition is structural but
   asymmetric (stderr baked into group shape). Cleaner framing is
   mod 2/mod 4 by instance: an instance is a bidirectional unit addressed
   as a contiguous block of stream_types. No control: 128
   instances/channel (mod 2). With control: 64 instances/channel
   (mod 4). Combined address space ~255*128 or ~255*64.
4. TTY and channels should converge on one format. Channels was
   written after TTY as a natural extension (5-byte + channel_id:u32 =
   9-byte). Whether TTY-direct retires the 5-byte format is a bigger
   call — backward compat — flagged as POC candidate.
5. Recursive multiplexing follows: each instance can be a channels
   connection. Unbounded, uniform per level via the instance framing.

Following the research-then-sync pattern: iterate here, fix
inter-document drift, sync to specs only after it settles. ADR-092 is
pushed because it's load-bearing and separable; ADR-093 (multiplexing
redesign) and ADR-094 (retire 5-byte format) draft after POCs validate.

POC candidates (ordered by leverage):
- POC 1: stderr split/recombine (load-bearing for mod 2 vs mod 3)
- POC 2: TTY-direct-as-channels (load-bearing for format convergence)
- POC 3: recursive composition (low leverage, deferred)
2026-07-18 04:59:23 +00:00
glm-5.2 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.
2026-07-18 03:37:22 +00:00
glm-5.2 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.
2026-07-18 03:09:38 +00:00
glm-5.2 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 (c6eef73).
2026-07-17 15:32:04 +00:00
glm-5.2 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.
2026-07-17 14:51:28 +00:00
deepseek-v4-pro ddc577cd3e docs: update Phase 6 findings — QuicStream wrapper is necessary, deferred
SendStream implements only AsyncWrite, RecvStream implements only
AsyncRead. The QuicStream wrapper (44 lines) is a necessary adapter
that combines the split pair from accept_bi() into a single
AsyncRead+AsyncWrite type. Phase 6 is deferred — removing it would
require a new BidiStream abstraction or restructuring HttpAdapter::handle.
2026-07-17 14:09:21 +00:00
deepseek-v4-pro 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.
2026-07-17 08:54:04 +00:00
glm-5.2 ec46fc6d59 docs(research): fix ConnectionCredentials/CallCredentials wording in Phase 5 prune + source map
The Phase 5 'call_client.rs after prune' list inaccurately described
ConnectionCredentials as 'removed from call_client.rs' — it is a new
type in alknet-core, not a pre-existing type in call. Reworded to:
RemoteIdentity is removed (moved to core in Phase 0); CallCredentials
is restructured (transport dimensions leave for ConnectionCredentials
in core, auth_token stays). The source map table row for lines 40-88
is also corrected: the destination is a split, not a uniform move to
alknet-core.
2026-07-17 06:25:04 +00:00
glm-5.2 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
2026-07-17 05:56:47 +00:00
glm-5.2 16ae3cf6c9 docs(research): resolve remaining migration OQs — accept_bi semantics confirmed, integration test home decided 2026-07-16 12:44:32 +00:00
glm-5.2 bb60efebfa docs(research): resolve migration OQs — Phase 0 (credentials to core), test audit, integration test plan 2026-07-16 12:10:18 +00:00
glm-5.2 2a2a116a3c docs(research): alknet-crate-extraction migration findings — 6 phases, source map, ordering rationale 2026-07-16 11:41:29 +00:00
glm-5.2 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.
2026-07-16 11:33:18 +00:00
glm-5.2 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
2026-07-16 09:18:56 +00:00
glm-5.2 f46482253b docs(research): iroh proxy support — force relay-only PoC, resolves OQ-67
Option 2 (force relay-only via clear_ip_transports + addr_filter(relay_only)
+ proxy_url) validated end-to-end: a relay-only proxied iroh client connects
through an HTTP CONNECT proxy to a local relay; selected path is relay, no
direct IP path established, 45-byte echo completes (5/5 runs clean).

Option 1 (SOCKS5 UDP ASSOCIATE over iroh's direct path, the quinn-PoC
analogue) is not feasible without forking iroh: iroh exposes no
socket-injection hook for the IP/direct transport (noq_endpoint is
pub(crate), IpTransport binds its own netwatch::UdpSocket), and the only
public injection surface (unstable-custom-transports CustomTransport)
operates on a separate CustomAddr address space that iroh's hole-punching
does not route through.

Correction to OQ-67 premises: iroh's proxy_url covers the relay WebSocket
(HTTP CONNECT) only, not pkarr/DoH. Recommendation: Option 2 as default,
Option 1 deferred unless a direct-path-privacy use case justifies a fork.

PoC at /workspace/iroh-proxy-poc.
2026-07-16 09:04:14 +00:00
glm-5.2 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
2026-07-16 08:42:33 +00:00
glm-5.2 ed40f95d96 docs(research): quinn QUIC over SOCKS5 proxy via UDP ASSOCIATE — PoC-validated
Resolves the 'quinn has no proxy support' blocker for QUIC client
connections in alknet-call. Quinn routes every network byte through the
public quinn::AsyncUdpSocket trait, and Endpoint::new_with_abstract_socket
accepts any impl — so a SOCKS5 UDP ASSOCIATE tunnel wrapped as that trait
gives full QUIC-through-proxy support with no fork.

An end-to-end PoC (/workspace/quinn-proxy-poc) confirms a quinn client can
complete a QUIC handshake and exchange stream data through a SOCKS5 proxy
with UDP support (5/5 runs clean, clippy clean). The load-bearing impl is
~250 lines. Integration into alknet-call is ~30 lines in connect() plus one
new module, behind a new optional socks5 feature flag.

Limitations: ECN is lost across the proxy (quinn falls back to non-ECN), and
the proxy must support UDP ASSOCIATE (ssh -D does not). Both are acceptable
for alknet's call-protocol use.
2026-07-16 07:48:43 +00:00
glm-5.2 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).
2026-07-15 13:19:30 +00:00
glm-5.2 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).
2026-07-15 12:50:13 +00:00
glm-5.2 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.
2026-07-15 10:38:28 +00:00
glm-5.2 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).
2026-07-15 09:22:24 +00:00