1f06253959439afd08dd79f5b4b9bf8e8b6cb273
12 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 1f06253959 |
fix: Unit 4 — backpressure fixes (C-04, C-05, C-07, C-16, C-17, C-25 #2 #3 #6)
C-04 [critical]: route_payload is now async — uses send().await instead of try_send, so the demux stalls on a full buffer instead of dropping chunks. Lossless bounded-buffer backpressure per ADR-040 REQ-CH-05. C-05 [critical]: DEFAULT_BUFFER_CAP changed from 1,048,576 (messages) to 64 (messages). The old value counted messages, not bytes, giving a ~16 TiB per-channel bound instead of the intended 1 MiB. The new value is a reasonable message-count bound; the actual memory bound is enforced by the 16 MiB MAX_CHUNK_LEN per message. C-07 [critical]: demux loop now skips the payload bytes on ChunkError::TooLarge before continuing. The parsed length is in the error variant; the demux reads and discards that many bytes, then resyncs on the next 8-byte header. Previously it continued without skipping, causing permanent stream desync. C-16 [major]: MpscSendStream switched from tokio::sync::mpsc to futures::channel::mpsc, which exposes poll_ready for proper async backpressure in poll_write. The ~50 lines of abandoned deliberation comments are removed. The mux pump now uses futures::StreamExt::next instead of tokio recv. C-17 [major]: mux pump writes an EOF chunk when the receiver ends without a sentinel (handler dropped without shutdown). Previously the pump exited silently on recv→None, leaving the remote handler hanging until full transport close. Tests added: - C-25 #2: demux_resyncs_after_oversized_chunk - C-25 #3: backpressure_slow_reader_no_data_loss_other_channel_unaffected - C-25 #6: mux_pump_writes_eof_on_implicit_close Verification: 441 tests pass (was 439; +3), clippy clean, fmt clean, doc warnings unchanged (2 pre-existing, Unit 6 long-tail). |
|||
| 8066d08395 |
fix: Unit 3 — register_openable + per-connection ChannelCore (C-02, C-03, C-09)
A channel-open op could not be registered or invoked (C-02):
ChannelCore::register_openable did not exist, and resolve_channel_manager
(C-03) was a stub returning None — the ADR-047 §4 dynamic-resolution
shape (downcast context.env to &dyn ChannelOperationEnv) was unworkable
as written: context.env is a PeerCompositeEnv, not a single concrete
type that can be downcast to a channels-backed env.
The fix is per-connection registration (ADR-047 §4 amendment,
2026-08-13): a ChannelCore is constructed per channels connection (in
the install_channel_zero hook, which already runs per-connection and
already receives the ChannelManager), and register_openable is called on
that connection's overlay OperationRegistry (Layer 2 per ADR-019). The
wrapper closes over the per-connection ChannelCore and uses
ChannelCore::manager() directly — no context.env downcast. This
preserves every invariant ADR-047 §4 was written to protect (layering,
per-connection resolution) without adding as_any() to OperationEnv
(which would close the session/connection overlay patterns from
ADR-024, AGENTS.md §6).
Changes:
- ChannelCore::register_openable wraps the ALPN's OpenHandler with the
ACL→check_open→open_channel→spawn→respond flow (ADR-047 §3). Branches
on spec.op_type: Query/Mutation→Once, Sub→Stream (emits { channel_id }
and completes; data plane on the channel's BiStream), Pub→Sink (stub:
channel:pub_open_not_implemented — requires the channel-adoption path,
C-08/Unit 5). The OpenHandler receives (input, Connection, AuthContext)
and spawns the ALPN's protocol on the channel's BiStream, returning a
JoinHandle for teardown.
- ChannelManager::set_handler_task installs the spawned OpenHandler's
JoinHandle after open_channel (which allocates the channel first to
get the BiStream halves, then the handler is spawned, then the task is
recorded for abort on channel/close / connection drop).
- channel:too_many_channels / channel:allocation_failed error codes
mapped to CallError with details (channel:forbidden is the ACL's
FORBIDDEN, already handled by the registry before the wrapper).
- resolve_channel_manager stub removed (C-03); ChannelOperationEnv trait
and ChannelsSessionEnv retained as a two-way-door implementation detail
for future per-connection routing (not on the open-op path). The
tautology filler test (C-22 env.rs) removed.
- ADR-047 §4 amendment records the per-connection-registration decision
(two-way door: the ADR's door-type section explicitly marks the wrapper
shape as a two-way-door implementation detail; the one-way decisions
— per-ALPN op names, channel_open marker, removal of channel/open —
are unchanged).
Acceptance gate (C-02/C-03): one end-to-end test wires ChannelClient ↔
ChannelsAdapter over a real tokio::io::duplex carrying the channels
8-byte chunk header wire format. The accept side's install_channel_zero
hook builds a per-connection ChannelCore, registers a no-op open op
(channels/tty/sub) via register_openable, and runs the dispatch loop.
The client calls call_open_op("channels/tty/sub") on channel 0; the
wrapper does check_open→open_channel→spawn→respond. Asserts the
response carries a non-zero channel_id and that the per-identity quota
was reserved (policy count for the caller incremented to 1).
Verification: 438 tests pass (was 437; +1 e2e), clippy clean, fmt clean,
doc warnings 2 (was 4; fixed the 2 register_openable broken-link
warnings — C-09; the remaining default_policy and env module/macro
warnings are Unit 6 long-tail items).
|
|||
| 495e04ed43 |
fix: Unit 2 — channel 0 single-stream call mode (C-01, C-25 #1)
Channel 0 was dead in both directions (C-01): the call protocol's stream-per-request model (open_bi per call) is incompatible with channel 0's single yield-once BiStream — every call_open_op failed with StreamClosed on the connect side, and channel 0's Connection was a black hole on the accept side. The fix is single-stream call mode (ADR-036 amendment): all EventEnvelope frames are multiplexed on channel 0's one BiStream. Changes: - CallConnection gains single_stream_writer: Option<Arc<SharedFrameWriter>> and new_single_stream() constructor. call_with_payload, subscribe_with_payload, publish_with_payload, and abort branch on is_single_stream() — in single-stream mode they write frames through the shared writer (mutex-serialized) instead of opening a fresh open_bi per call. - Dispatcher::run_loop_single_stream reads frames off channel 0's read half, dispatches call.requested, writes responses through the shared writer, and routes in-flight call.published/call.completed/ call.aborted to the matching Pub sink's chunk_tx by request_id. - ChannelsAdapter::handle's InstallChannelZero hook now receives channel 0's Connection (built by the adapter) and runs the single-stream dispatch loop on it — closing the accept-side black hole. Mux runner is spawned BEFORE install_channel_zero so mux.register(0) can complete. - ChannelClient::from_connection uses CallConnection::new_single_stream and spawns a read pump (read_single_stream_until_closed) that routes channel-0 response frames into the PendingRequestMap via dispatch_envelope — closing the connect-side StreamClosed path. - ADR-036 amendment records the single-stream-mode decision (two-way door: implementation detail, wire format unchanged). Acceptance gate (C-25 #1): three end-to-end tests wire ChannelClient ↔ ChannelsAdapter over a real tokio::io::duplex pair carrying the channels 8-byte chunk header wire format: - channel_0_end_to_end_call_round_trip: a Query op round-trip - channel_0_end_to_end_unknown_op_returns_not_found: NOT_FOUND - channel_0_end_to_end_publish_delivers_chunks: a Pub op with 3 chunks Verification: 437 tests pass (was 434; +3 e2e), clippy clean, fmt clean, doc warnings unchanged (4, pre-existing C-09). |
|||
| 502488cc72 |
fix: Unit 1 — make publish() work end-to-end (P-01, P-02, P-05, P-08, P-09, P-12 #1)
Fixes the Pub operation's client→responder path so a publish() actually works on any transport. Previously the wire was broken in three compounding ways and the unit tests couldn't see it. P-01 [critical] — publish chunks were written to a *fresh* open_bi stream (stream B) while the responder read chunks from the stream that carried call.requested (stream A). Stream B's frames were silently discarded by the dispatch loop's "ignoring non-requested event" branch. On single-stream transports (TCP+TLS, SSH) the second open_bi returns StreamClosed outright, so every publish failed. Fix: pump call.requested + call.published + call.completed on the *same* write half via the new pump_publish_to_wire free function; the read half is pumped concurrently for the single call.responded. P-02 [major] — publish() / publish_with_payload() now take Pin<Box<dyn Stream<Item = Value> + Send>> per ADR-046 §8, not Vec<Value>. The Vec shape buffered the entire publish in memory and made the ADR's flagship use cases (telemetry ingest, live upload, drag-drop file streaming) impossible. The wire format is unchanged; this corrects API drift (no deployments exist). P-05 [major] — publish now registers with timeout: None (like subscribe), not DEFAULT_CALL_TIMEOUT (30s). A publish whose stream took >30s wall-clock got a spurious client-side TIMEOUT while the responder kept consuming. PendingEntry::Call.timeout is now Option<Instant> so the sweeper never evicts unbounded calls; all register_call callers updated (Some(...) for call(), None for publish()). P-08 [major] — the dispatch Pub branch re-implemented invoke_sink's not-found / visibility / ACL / handler-kind checks inline; invoke_sink was only ever called from its own tests. Any future fix in invoke_sink (e.g. the missing publish_schema validation, P-03) wouldn't reach the wire path. Fix: extract OperationRegistry::resolve_sink_handler (pub(crate)) as the single source of truth for the sink dispatch checks; both invoke_sink and Dispatcher::dispatch (Pub branch) call it. The two paths can no longer diverge. P-09 [major, side-effect] — make_sink_forwarding_handler in from_call.rs was store-and-forward (collect into Vec) and silently discarded Err items (filter_map(|item| item.ok())), contradicting the SinkHandler contract that an Err terminates the stream. Now that publish_with_payload takes a Stream, the forwarding handler passes the stream through directly (streamed, not buffered) and uses take_while(Ok) + filter_map to terminate on Err. P-09 was listed in Unit 6 but depends on P-01/P-02, so it falls out naturally here. P-12 #1 [acceptance gate] — adds the end-to-end publish test that the review identifies as the gate for this unit: publish_end_to_end_delivers_chunks_and_returns_response wires CallConnection::publish ↔ Dispatcher::run_loop over a real tokio::io::duplex pair (new duplex_connection_pair test helper + SingleStreamSource BidiStreamSource impl), publishes 3 chunks, and asserts the responder saw all 3 and returned the right result. A second test covers the unknown-op → NOT_FOUND path. These tests would have caught P-01 immediately. Verification: - cargo test --lib → 434 passed, 0 failed (was 432; +2 e2e tests) - cargo clippy --all-targets -- -D warnings → clean - cargo fmt --check → clean - cargo doc --no-deps → 4 warnings (pre-existing C-09, unchanged) |
|||
| f1d6796436 |
docs: consolidated review 001 — Pub (ADR-046) + channels integration (ADR-047)
Consolidates three code-review passes (one main summary + sub-review A
on ADR-046 + sub-review B on channels ADRs 034–043/047) into a single
verified document under docs/reviews/. Every finding was re-verified
directly against the source at
|
|||
| f305f8c0a5 |
feat: implement channels protocol + ADR-047 (openable ALPNs are operations)
ADR-047: the unifying decision that dissolves into per-ALPN ops (, ) with a marker on . Each openable ALPN registers its own ops with their own , , , and the marker. The field is replaced by (Sub/Pub). The generic ops (channel/close, channel/control, channel/resources/subscribe) stay, keyed by channel_id. Resolves Gaps A-G from the research findings (Gap B broker named out-of-scope for alkcall; Gap C relay wrapper is consumer concern; Gap D connection-owner allocates; Gap E extension trait; Gap F boolean marker on wire; Gap G ACL/ownership complementary). ADR-037 amended: dissolves; removed; generic ops stay; preview dropped from resources/subscribe. Spec docs updated: channel-operations.md (unified model, opener ledger, ACL flow), operation-registry.md (channel_open marker, ChannelOpenSpec), README.md (ADR-047), open-questions.md (OQ-31..38 resolved). Source changes: - spec.rs: ChannelOpenSpec struct, channel_open field on OperationSpec, with_channel_open builder, 3 tests - discovery.rs: spec_to_json emits channel_open boolean, operation_spec_schema includes channel_open, 2 tests - from_call.rs: rebuild_spec_for parses channel_open marker, derive_alpn_from_op_name helper, 6 tests Channels module (src/channels/, 10 files, ~2400 lines): - wire.rs: 8-byte chunk header (ChunkHeader, parse/write_header, read_header/write_chunk/write_eof async helpers), 12 tests - reassembly.rs: MpscRecvStream (tokio::mpsc::Receiver<Bytes> → AsyncRead), MpscSendStream (AsyncWrite → tokio::mpsc::Sender<Bytes>), REQ-CH-01 shutdown sentinel, REQ-CH-02 sender-drop EOF, 10 tests - mux.rs: MuxHandle (clone-able, register(channel_id)), MuxRunner (per-channel pump tasks, exits when handles drop), OpenerLedger (ADR-047 §7), 4 tests - manager.rs: ChannelManager (channel map, open_channel, install_channel_zero, route_payload, teardown_channel, clear_all), 11 tests - source.rs: ChannelBidiStreamSource (yield-once accept_bi), channel_source helper, 4 tests - adapter.rs: ChannelsAdapter (ProtocolHandler for alknet/channels, demux loop, install_channel_zero hook), 1 test - operations.rs: ChannelOperations (registers channel/close, channel/control, channel/resources/subscribe), ChannelCore (check_open/on_close wrappers), 4 tests - policy.rs: ChannelLifecyclePolicy trait, NoCap, PerIdentityChannelPolicy (default 256, per_identity_caps override), default_policy, 8 tests - env.rs: ChannelOperationEnv extension trait (ADR-047 §4), ChannelsSessionEnv impl, 2 tests - client.rs: ChannelClient (from_connection, call_open_op, take_call_connection), 1 test Verification: 432 tests pass (66 new channels + 10 marker + 356 existing), clippy clean, fmt clean, cargo doc generates. Cargo.toml: +bytes dependency. |
|||
| ea66398c88 |
feat: add Pub operation type, HandlerKind::Sink, call.published wire event (ADR-046)
The call protocol had Subscription (server→client streaming) but lacked the directional complement: client→server streaming, where the initiator produces a stream and the responder's handler consumes it. This gap was inherited from the @alkdev/pubsub EventEnvelope prior art, which has subscribe but no wire-level publish. ADR-046 adds the Pub primitive: - OperationType::Pub (client→server streaming) - OperationType::Subscription renamed to Sub (wire: "subscription" → "sub") - SinkHandler type + HandlerKind::Sink variant - PublishStream type alias (Stream<Item = Result<Value, CallError>>) - OperationRegistry::invoke_sink() dispatch path - call.published wire event (sixth event type, additive) - OperationSpec.publish_schema (Option<Value>, validates per-chunk input) - DispatchResult::Sink + SinkDispatch (handler future + chunk channel) - Dispatcher::pump_sink (feeds call.published chunks from wire to handler) - CallConnection::publish() / publish_with_payload() client methods - from_call sink forwarding handler (make_sink_forwarding_handler) - make_sink_handler() helper Fan-out/broker (one producer, N consumers, topic matching) is deferred to the channels session — the call protocol is point-to-point; the broker is a routing concern that sits above it. The Pub primitive is the load-bearing piece the broker will compose on. - 23 new tests (366 total, up from 343) - clippy clean, fmt clean Verification: cargo test — 366 passed cargo clippy --all-targets -- -D warnings — clean cargo fmt --check — clean |
|||
| cc470a363a |
docs: port architecture specs + 45 ADRs from alknet, renumbered
Port the call + channels architecture documentation from the alknet mono-repo into docs/architecture/, renumbered as alkcall ADR-001..045. Renumbering map (alknet -> alkcall): Core: 001,002,004,006,007,011,065,070,092,014,050,091 -> 001-012 Call: 005,064,012,023,015,022,024,016,049,017,028,029,030,032,066,069,067,068 -> 013-030 Shared: 003,009,013 -> 031-033 Channels: 071,093,072,073,074,075,076,094,079,080,081,089 -> 034-045 3 superseded/reversed ADRs kept for historical trail: - ADR-013 (irpc foundation, superseded by ADR-014) - ADR-023 (peer-scoped filtering, superseded by ADR-024) - ADR-077 (TTY inside channels, reversed by ADR-035 — not ported, TTY-only) Ported docs (11 spec files + README + open-questions): - call-README.md, call-protocol.md, operation-registry.md, client-and-adapters.md - channels-README.md, channels-overview.md, channels-wire.md, channels-connection.md, channels-adapter.md, channel-operations.md, channel-client.md - README.md (index with doc table, ADR table grouped by category, key principles) - open-questions.md (lean — 30 OQs, renumbered OQ-01..030; includes new OQ-22 for the pub/sub gap) Cross-reference rewriting: - All ADR-NNN references rewritten single-pass (no chaining bug) - Markdown link paths fixed - Title lines aligned with filenames - Non-ported ADR refs (052, 082, 086, etc.) left as-is with README note The open-questions.md includes OQ-22 (new): the call protocol pub/sub gap — subscribe exists but pub does not, needed for channels channel/resources/subscribe fan-out. This is the next ADR to write (alkcall ADR-046). |
|||
| 1ceb9b785d |
docs: rename pass — update crate refs from alknet-* to alkcall
Update doc comments referencing the old alknet-* crate names and spec paths to reflect the alkcall home. ALPN strings (alknet/call, alknet/test) are wire-stable and unchanged. Sibling crate refs (alknet-http, alknet-core historical context) kept as-is — those are accurate references to crates that still exist or will be reworked later. Also fixes the pre-existing broken intra-doc link [`CallAdapter`] in dispatch.rs (now [`super::adapter::CallAdapter`]) — the only doc warning carried over from the extraction. Verification: - cargo test: 343 passed (0 failed) - cargo clippy --all-targets -- -D warnings: clean - cargo fmt --check: clean - cargo doc --no-deps: clean (0 warnings, was 1) |
|||
| 4bc7a19695 |
feat: extract alknet-call into alkcall, vendor core types
Phase 1 of the alknet-call + alknet-channels unification. The call crate
is extracted verbatim from /workspace/@alkdev/alknet/crates/alknet-call
and the needed alknet-core types are vendored into src/core/ — alkcall is
the home for these types going forward (no separate alkcore crate).
Vendored core types (src/core/):
- auth.rs: Identity, AuthToken, AuthContext, IdentityProvider
- ownership.rs: OwnershipProvider, OwnershipStore, InMemoryOwnershipStore,
OwnershipError
- types.rs: ProtocolHandler, Connection, BiStream, BidiStreamSource,
SendStream, RecvStream, HandlerError, StreamError, Capabilities, Secret,
IdentityAlreadySet
- Stripped: quinn/iroh/rustls deps, Connection::from_quinn/from_iroh
(the dial lives in the consumer per ADR-089), config/credentials/
fingerprint/store modules, ConfigIdentityProvider/IdentityStore
Call crate (src/client, src/protocol, src/registry/):
- Copied verbatim from alknet-call; alknet_core::{auth,types,ownership}
rebound to crate::core::{auth,types,ownership}
- ALPN strings unchanged (alknet/call — wire-stable per ADR-006)
- No behavioral changes
Verification:
- cargo test: 343 passed (0 failed)
- cargo clippy --all-targets -- -D warnings: clean
- cargo fmt --check: clean
- cargo doc --no-deps: 1 pre-existing broken intra-doc link
(CallAdapter in dispatch.rs — Phase 2 cleanup)
Next: Phase 2 — channels greenfield implementation + ALPN rename +
architecture doc porting.
|
|||
| a779dd0d0d |
docs: replace alkvault scaffold with alkcall project conventions
- AGENTS.md: full rewrite for the call+channels RPC crate (async/tokio, vendored core types, alktype dep, producer/consumer framing, the two stable wire formats, no-env-vars invariant, OperationEnv trait, abort cascade, AccessControl peer auth, ADR index for both halves) - implementation-specialist.md: replace vault crypto conventions (OsRng, zeroize, AES-GCM) with 15 alkcall protocol conventions matching AGENTS.md - docs/sdd_process.md: fix alkvault -> alkcall reference Verification: no code changed (docs-only) commit 0a031cd..HEAD |
|||
| 0a031cd378 | init |