Commit Graph
617 Commits
Author SHA1 Message Date
glm-5.2 f41387eeed feat(typedef/validation): implement custom keyword validators for all 17 TypeDef kinds 2026-07-21 10:01:53 +00:00
glm-5.2 28d4068e0e feat(typedef): re-export schema layer public API from lib.rs
Re-export Endian, VariableEncoding, DiscriminatorKind, and the parse_*
functions plus normalize_refs from the schema module so they're accessible
at the crate root alongside TypedefError.
2026-07-21 09:57:08 +00:00
glm-5.2 ac36a1cee3 feat(typedef/schema-types): implement TypeDef kind detection, annotations, Endian, $ref normalization 2026-07-21 09:56:40 +00:00
glm-5.2 109d743f9f feat(typedef/error-type): implement TypedefError enum with Schema, Offset, Access, Validation variants 2026-07-21 09:55:46 +00:00
glm-5.2 44f221e8cc feat(typedef/crate-init): initialize alknet-typedef crate skeleton
Created crates/alknet-typedef/ with Cargo.toml depending on jsonschema 0.46
(default-features = false for WASM-cleanliness) and serde_json with
preserve_order. Added src/lib.rs with module declarations for all 9 modules
and skeleton source files for each. Added crate to workspace members list.

Verified: cargo check, clippy -D warnings, and build --workspace all succeed.
Dependency tree confirmed free of tokio/reqwest/rustls (WASM-clean).
2026-07-21 09:54:05 +00:00
deepseek-v4-pro db100f9849 feat(typedef): add implementation task decomposition (12 tasks, 7 generations)
Break the alknet-typedef architecture specs into atomic, dependency-ordered
implementation tasks covering crate init, error types, schema layer, data
access, both layout modes (aligned static + packed sequential), TUnion
dispatch, jsonschema custom keyword validators, TypedefEngine integration,
comprehensive tests, and a final review checkpoint.

Validated: 12 tasks, 0 cycles, 7 parallel generations.
2026-07-21 09:36:30 +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 b60a5844ba refactor(core,http,tty,call): unify stream leaf — BiStream as the handler leaf (Phase 6)
Implement ADR-092 across the workspace: accept_bi/open_bi return BiStream
(a concrete AsyncRead + AsyncWrite + Send + Unpin newtype), not the split
(SendStream, RecvStream) pair. The join moves into core's BidiStreamSource
impls (quinn/iroh via tokio::io::join, single-stream via boxed AsyncReadWrite);
handlers receive the joined BiStream and never see the pair.

Core (alknet-core/src/types.rs):
- Add concrete BiStream struct boxing Box<dyn AsyncReadWrite + Unpin>,
  with AsyncRead + AsyncWrite impls. from_joined (pub, for downstream
  crates that produce split halves naturally — channels reassembly, tests)
  and from_bidi (pub(crate), for Connection::from_bidi) constructors.
- Change BidiStreamSource::accept_bi/open_bi return types from
  (SendStream, RecvStream) to BiStream. Update QuinnBidiStreamSource,
  IrohBidiStreamSource, StreamBidiStreamSource impls to do the join once.
- Collapse SendStream/RecvStream to thin newtypes over
  Box<dyn Async* + Send + Unpin>. Remove SendStreamKind/RecvStreamKind
  enums and the quinn/iroh per-call dispatch (the join happens once in the
  BidiStreamSource impl now). Keep SendStream::from_stream /
  RecvStream::from_stream per-half boxing for into_sub_streams() (ADR-074)
  and the future channels reassembly path.
- Remove Connection::from_stream (split-pair constructor). Promote
  Connection::from_bidi to the only public stream constructor (the rule:
  the split never crosses a crate boundary as part of a constructor).
- Update Connection::accept_bi/open_bi to return BiStream. Update
  from_source_tests and tests modules to use from_bidi and BiStream;
  add a SinkEmpty test helper (AsyncRead EOF + AsyncWrite discard) for
  Connection-level-only test connections.

alknet-http (server/adapter.rs):
- Drop the 44-line QuicStream wrapper — accept_bi returns BiStream which
  is already AsyncRead + AsyncWrite. HttpAdapter::handle becomes 4 lines.
- Drop the 38-line QuicStreamDuplex test helper — tests use a single
  tokio::io::duplex whose ends are each AsyncRead + AsyncWrite natively.
- Remove unused std::io / std::pin::Pin imports.

alknet-tty (adapter.rs):
- TtyAdapter::handle splits the BiStream from accept_bi via
  tokio::io::split for drive_session's separate AsyncWrite/AsyncRead args
  (the stdlib idiom for TcpStream-style duplex streams).

alknet-call (protocol/*, client/*):
- Dispatcher::run_loop accept_bi site: take BiStream, pass to handle_stream.
- Dispatcher::handle_stream signature: take BiStream, split internally via
  tokio::io::split (was: take SendStream + RecvStream separately).
- CallConnection::call_with_payload / subscribe_with_payload / write_envelope:
  split the BiStream from open_bi via tokio::io::split at the call site.
- write_request / read_stream_until_closed: generic over AsyncWrite/AsyncRead
  (were: concrete SendStream/RecvStream) — accepts the ReadHalf/WriteHalf
  from tokio::io::split directly.
- Add protocol/test_support.rs with sink_empty_connection() (replaces the
  5 duplicated stub_connection() fns that used Connection::from_stream).
- Update all test stubs (call_client.rs, protocol/connection.rs,
  protocol/dispatch.rs, protocol/adapter.rs, client/from_call.rs) to use
  Connection::from_bidi + the shared sink_empty_connection() helper.
- Test handle_stream call sites: build BiStream::from_joined(recv, send)
  from the existing BufReader<Cursor> + duplex pair.

Workspace test status: all 9 crates pass (116 + 307 + 18 + 3 + 17 + 301 +
34 + 61 + 23 + 5 + 6 + 8 + 82 + 4 + 3 + 6 + 12 + 1 = 1007 tests pass). One
pre-existing failure remains in alknet-http
(adapters::to_mcp::tests::search_returns_access_control_filtered_ops_excluding_subscriptions
— handler kind mismatch, unrelated to Phase 6, fails on develop baseline).
2026-07-18 15:59:01 +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 4fc6854846 chore(call): prune connect, TLS helpers, CallCredentials, and dead paths (Phase 5)
- Remove RemoteIdentity, CallCredentials, ClientError, connect() from call_client.rs
- Remove all TLS helpers (build_quinn_client_config, build_client_auth,
  select_server_verifier, FingerprintPinVerifier, RawKeyClientCertResolver,
  NoClientCertResolver, Ed25519SigningKey, cert/key loaders)
- Remove credentials_auth_token dead path from from_call.rs
  (OpSummary field, build_bundles, make_forwarding_handler,
  make_streaming_forwarding_handler, build_forwarded_payload)
- Remove 2 dead-path tests (build_forwarded_payload_sets_auth_token,
  streaming_forwarding_handler_sets_auth_token)
- Update mod.rs re-exports to only CallClient
- Remove quinn feature and TLS deps from Cargo.toml
- Delete tests/two_node_call.rs (used connect() + CallCredentials)
- alknet-call is now a pure protocol crate
2026-07-17 14:01:51 +00:00
deepseek-v4-pro a0dbe4fd3c chore(core): prune endpoint.rs and heavy deps (Phase 4)
- Delete crates/alknet-core/src/endpoint.rs (1606 lines)
- Remove pub mod endpoint from lib.rs, update doc comment
- Remove rcgen, rustls-pemfile, rustls-acme deps from Cargo.toml
- Remove acme feature
- Update fingerprint.rs doc comment references to alknet-endpoint/alknet-tls
2026-07-17 13:21:54 +00:00
deepseek-v4-pro 83a94ec03f chore(client): mark all client tasks as completed
Phase 3 complete: alknet-client crate implemented with:
- AlknetClient struct with builder methods (with_quinn, with_tcp_tls, with_iroh, with_socks5_proxy)
- ClientDialError enum with 5 variants
- dial_quic, dial_tcp_tls, dial_iroh methods
- SOCKS5 proxy support (Socks5ProxyConfig, Socks5Credentials, Socks5UdpSocket)
- Unit tests (18) + integration test (3)
- All feature combos compile and test clean
- Workspace build and tests pass
2026-07-17 12:49:50 +00:00
deepseek-v4-pro 2a75724dde style(client): cargo fmt 2026-07-17 12:49:40 +00:00
deepseek-v4-pro 759c627ca4 test(client): add unit tests and integration test for alknet-client
- AlknetClient construction tests: new(), Default, Send+Sync, Debug
- ClientDialError tests: #[from] conversion, display formatting, Send+Sync
- dial_quic error path tests: NoTransport, TlsConfig on invalid creds
- dial_tcp_tls error path tests: NoTransport
- dial_iroh error path tests: NoTransport, extract_iroh_endpoint_id
- SOCKS5 proxy error display test (feature-gated)
- Integration test: dial_and_takeover.rs

Task: client/tests
2026-07-17 12:47:40 +00:00
deepseek-v4-pro 82ee37e206 feat(client): implement alknet-client crate — AlknetClient, dial methods, error type, SOCKS5 proxy
- Initialize alknet-client crate with Cargo.toml, deps, feature flags
- Implement ClientDialError enum with 5 variants (TlsConfig, Connect, Handshake, NoTransport, Proxy)
- Implement AlknetClient struct with builder methods (with_quinn, with_tcp_tls, with_iroh, with_socks5_proxy)
- Implement dial_quic — QUIC dial via quinn, producing a Connection
- Implement dial_tcp_tls — TCP+TLS dial via tokio-rustls, producing a Connection
- Implement dial_iroh — iroh dial, producing a Connection
- Implement SOCKS5 proxy support — Socks5ProxyConfig, Socks5Credentials, Socks5UdpSocket, proxy integration
- Add into_rustls_config() to TlsClientConfig in alknet-tls
- Add alknet-client to workspace members

Tasks: client/crate-init, client/error-type, client/client-core,
client/dial-quic, client/dial-tcp-tls, client/dial-iroh, client/socks5-proxy
2026-07-17 12:42:47 +00:00
deepseek-v4-pro b476182d64 feat(tasks): decompose Phase 3 alknet-client into 9 atomic tasks
Phase 3 of the crate extraction (per findings.md): create the
alknet-client crate — the native client dial seam, client-side
analogue of AlknetEndpoint. Three dial methods (dial_quic,
dial_tcp_tls, dial_iroh) unified on &ConnectionCredentials (ADR-091),
pre-built transports via builder methods, optional SOCKS5 proxy
support (ADR-090).

9 tasks, 7 generations, no cycles:
- client/crate-init: Cargo.toml, feature flags, module skeleton
- client/error-type: ClientDialError enum (5 variants)
- client/client-core: AlknetClient struct + builder methods
- client/dial-quic: QUIC dial via quinn
- client/dial-tcp-tls: TCP+TLS dial via tokio-rustls
- client/dial-iroh: Iroh dial (key-not-config)
- client/socks5-proxy: Socks5ProxyConfig, Socks5UdpSocket, proxy integration
- client/tests: Unit tests + integration test
- client/review-client: Review checkpoint

Depends on: tls/review-tls, endpoint/review-endpoint (both completed).
Purely additive — old CallClient::connect stays until Phase 5 prune.
2026-07-17 12:07:08 +00:00
deepseek-v4-pro ab56acae69 feat(endpoint): complete review and mark all tasks as completed
- Fix formatting in endpoint.rs
- Mark tests.md and review-endpoint.md as completed
- All 9 endpoint tasks are now complete
- Workspace: all tests pass, clippy clean, fmt clean
2026-07-17 10:54:50 +00:00
deepseek-v4-pro 5467c30892 feat(endpoint): add tests and mark tasks 1-7 as completed
- Add 7 registry tests (handler_registry_*) to registry.rs
- Add 4 dispatch tests (build_auth_context_*, dispatch_decision_logic_*) to dispatch.rs
- Add 6 endpoint tests to endpoint.rs:
  - debug_for_alknet_endpoint_is_implemented_without_panicking
  - endpoint_constructs_with_iroh_raw_key_identity (adapted for new API)
  - iroh_endpoint_runs_accept_loop_and_shutdown (adapted for new API)
  - with_iroh_sets_field (replaces has_iroh_identity_true_for_raw_key)
  - without_iroh_field_is_none (replaces has_iroh_identity_false_for_x509)
  - endpoint_works_without_iroh (replaces has_iroh_identity_false_when_no_identity)
- Add async-trait as dev-dependency
- All 17 tests pass with iroh feature
- All 8 tests pass without features
- Workspace tests all pass (no breakage)
- Mark tasks 1-7 as completed
2026-07-17 10:53:25 +00:00
deepseek-v4-pro 2755b995c6 feat(endpoint): initialize alknet-endpoint crate with all modules
- Create crates/alknet-endpoint/ with Cargo.toml, feature flags (quinn, iroh, tcp, acme)
- Implement HandlerRegistry (registry.rs) — extracted from core/endpoint.rs
- Implement AlknetEndpoint (endpoint.rs) — fresh build against ADR-083 shape
  - new() takes no StaticConfig, no TLS config
  - with_quinn/with_iroh/with_tcp_tls builder methods
  - run() spawns accept loops for each active transport
  - shutdown() is infallible (no Result)
  - No EndpointError type
- Implement dispatch (dispatch.rs) — shared dispatch_connection free function
  - Transport-agnostic: takes pre-extracted alpn, fingerprint, remote_addr
  - ACME guard (acme-tls/1) behind acme feature
  - build_auth_context resolves identity from fingerprint
- Implement accept loops:
  - accept/quinn.rs — quinn accept loop with ALPN + fingerprint extraction
  - accept/iroh.rs — iroh accept loop with ALPN negotiation + fingerprint
  - accept/tcp_tls.rs — TCP+TLS accept loop (new code, not in old endpoint.rs)
- Add alknet-endpoint to workspace members
- All feature combos compile cleanly (no features, quinn, iroh, tcp, all three)
- Workspace cargo check passes
2026-07-17 10:48:39 +00:00