f362c9e596b0062c0080e4330c712bdeacab9f99
17 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| f362c9e596 |
fix: Unit 8 — Pub protocol semantics (P-03, P-04, P-07)
The publish path's decided-but-unimplemented behaviors land: per-chunk
schema validation, initiator call.error terminates the stream, and an
early handler return short-circuits the feed. alktype::validation::
build_validator is now used on both dispatch paths (the dependency was
declared and unused); jsonschema was added as a direct dependency
because its Validator type is part of alktype's public API.
P-03 — OperationSpec::publish_schema was declared, had a builder,
defaulted to None, and was never read. It now round-trips through
discovery: spec_to_json emits "publish_schema" when Some,
operation_spec_schema() advertises the property, and rebuild_spec_for
parses it back (null/absent treated as None, so an imported Pub op no
longer loses the schema). Both dispatch paths (pump_sink and
run_loop_single_stream) validate each call.published chunk's input
against the schema before yielding it to the SinkHandler; on
validation failure an Err(CallError::invalid_input(...)) with the
offending chunk in `details` is injected and the feed terminates,
matching the SinkHandler contract that an Err terminates the stream.
The validator is built once at dispatch time and carried in the
SinkDispatch (and, for the single-stream path, in a new InFlightSink
struct alongside chunk_tx); a schema that fails to compile logs a
warn and falls back to no validation. When publish_schema is None,
chunks are yielded as-is (unchanged behavior).
P-04 — pump_sink matched only call.published/call.completed/call.
aborted and dropped everything else into a debug-log ignore branch; a
call.error from the initiator was silently ignored. Both dispatch
paths now match call.error, parse the CallError from the payload,
inject it as Err(call_error) into the sink's chunk_tx, and terminate
the feed — the handler sees the initiator's error, not a synthetic
"aborted" message. A malformed payload falls back to
CallError::internal("publish error from initiator (malformed)").
P-07 — pump_sink used tokio::join!(feed_fut, handler) and only wrote
the response after both completed; an early-returning handler (e.g.
rejects after chunk 1) had its response deferred until the initiator
finished publishing or the next chunk_tx.send failed. Replaced with a
tokio::select! loop over handler.fuse() and reader.read_frame(): when
the handler completes first, the response is captured and the loop
breaks immediately, the feed is short-circuited (chunk_tx dropped,
the handler's PublishStream sees EOF), and the response is written
without waiting for the feed. The feed-wins branch (natural end /
abort / error / read failure) awaits the handler after the loop as
before. This removes the unbounded stall on a path ADR-046 intends
for long-lived streams.
Tests (15 new, 480 total):
- discovery: spec_to_json emits/omits publish_schema; operation_spec_
schema documents the property (3).
- from_call: rebuild_spec_for parses publish_schema (present/omitted/
null) and round-trips with spec_to_json (4).
- dispatch (stream-per-request): publish_schema rejects invalid chunk
+ passes valid chunks + no-schema yields as-is (3); initiator
call.error terminates with the initiator's error + malformed
fallback (2); early handler return short-circuits a slow feed and
the response is not deferred (1).
- channels/client (single-stream): publish_schema rejects invalid
chunk over channel 0; initiator call.error terminates the publish
over channel 0 (drives run_loop_single_stream directly with crafted
frames since the public publish() API takes Stream<Item = Value> and
cannot emit an initiator call.error) (2).
Verification:
- cargo test → 480 passed, 0 failed (was 465; +15 new tests)
- cargo clippy --all-targets -- -D warnings → clean
- cargo fmt --check → clean
- cargo doc --no-deps → 0 warnings
|
|||
| da12d65a03 |
fix: Unit 7 — small correctness fixes (C-19-misc, C-21, C-23, P-13)
No panic paths in library code, no misleading public fields, honest
error envelopes, latent bugs in derive_alpn_from_op_name and the mux
pump map fixed, per-discovery Box::leak removed, demux observability
counter added, resources-snapshot unbounded loop replaced with an
O(open channels) iterator.
C-21 — write_header (wire.rs) sliced out[..CHUNK_HEADER_LEN] and
panicked on short buffers; the doc comment declared the panic. Returns
Result<_, ChunkError::HeaderTooShort> instead (the variant already
existed for the parse side). write_chunk and all callers updated (they
pass [0u8; 8] today so the Result is always Ok, but the API contract
is typed, not panic-documented). Added a short-buffer test.
C-23 — dispatch_requested's Sink and Stream error arms used
String::new() as the request id (dispatch.rs), producing envelopes
with an empty id. The request_id is now cloned before the move into
dispatch and used in the error arms, so the envelope carries the real
id (matching the Once arm, which already did this correctly).
P-13 — SinkDispatch::chunk_tx was pub (dispatch.rs), exposing the
futures::mpsc::Sender type and the 64-slot buffer size as effective
public API. Made pub(crate); handle_stream and pump_sink (the only
readers) are in the same module, and the dispatch tests are in the
same module too.
C-19 (mux pump map leak + duplicate registration) — MuxRunner::run
inserted each pump's JoinHandle into self.pumps and never removed
finished ones (they accumulated for the connection's lifetime).
Duplicate register(channel_id) silently overwrote the old handle while
the old pump kept running (two pumps, one channel id). Fixed: before
insert, check for an existing entry; if it is_finished(), reap it and
proceed (leak fix); if it is still running, reject the registration by
dropping the responder without sending (the caller's receiver.await
yields RecvError, which ChannelManager maps to ChannelExists). Added
two tests: duplicate-rejected-while-running and
reap-finished-then-reregister.
C-19 (derive_alpn_from_op_name) — from_call.rs took only the first
path segment (rest.split('/').next()), so channels/custom/proto/sub
derived alknet/custom instead of the full ALPN custom/proto. The
segment.starts_with("alknet/") branch was dead (a single segment
cannot contain /), and segment == "alknet" yielded the nonsense ALPN
"alknet". Fixed: strip the known /sub or /pub suffix from rest
instead of taking the first segment, so multi-segment ALPNs survive.
The rule (ADR-047 Negative): alknet/*-prefixed segments and
multi-segment non-alknet/* ALPNs are returned as-is; single-segment
non-alknet names get the alknet/ prefix prepended. Added tests for the
multi-segment non-alknet case, the explicit alknet/ prefix case, the
/pub suffix, and the no-suffix (non-channel-open-op) case.
C-19 (Box::leak per discovery) — from_call.rs leaked a String to
'static on every leak_alpn call (bounded per unique ALPN in theory,
but leaks on every rediscovery of every marked op). Refactored
ChannelOpenSpec::alpn from &'static str to Cow<'static, str>
(spec.rs); the common case (ALPN crates register at compile time with a
&'static str literal) pays no allocation via Into<Cow>, while from_call
supplies an owned String without leaking. Removed leak_alpn. Amended
ADR-047 §2 to record the Cow<'static, str> shape (two-way-door type
detail; the wire format — a boolean channel_open marker — is
unchanged).
C-19 (REQ-CH-04 error counter) — the demux's lenient unknown-channel
drop (manager.rs) only debug!-logged; there was no counter or stats
surface. Added an AtomicU64 dropped_unknown_chunks counter to
ChannelManager, incremented on the unknown-channel drop in
route_payload, with a dropped_unknown_chunks() accessor for
observability. Added two tests: counter increments per drop, and
known-channel routing does not increment it.
C-19 (resources-snapshot unbounded loop) — operations.rs iterated
0..u32::MAX with a per-iteration mutex lock, breaking when
resources.len() >= manager.open_count(). With sparse ids (monotonic
after churn) or a channel closing mid-iteration, this could iterate
millions of times. Replaced with an iterator over ChannelManager::
channel_ids() — a new accessor that returns a point-in-time Vec<u32>
of open channel ids — so the snapshot is O(open channels). Added
channel_ids tests and behavioral tests for the resources/subscribe
handler (open channels emit their ALPNs; no channels emit an empty
set). Also added close/control handler tests (C-25 #8 coverage the
review noted was missing): close rejects channel 0, close on unknown
channel returns NOT_FOUND, control on unknown channel returns
NOT_FOUND, control missing channel_id returns INVALID_INPUT.
Verification:
- cargo test → 465 passed, 0 failed (was 449; +16 new tests)
- cargo clippy --all-targets -- -D warnings → clean
- cargo fmt --check → clean
- cargo doc --no-deps → 0 warnings
|
|||
| bfb265e31b |
fix: Unit 6 — convention + doc cleanup (C-09, C-20-rem, C-22-rem, C-24, P-10, P-11)
Conventions satisfied, `cargo doc` clean, no actively-wrong comments, producer/consumer naming consistent in the call/registry docs. P-10 — `pump_sink` matched the string literals "call.published", "call.completed", "call.aborted" (dispatch.rs) instead of the EVENT_PUBLISHED / EVENT_COMPLETED / EVENT_ABORTED constants the rest of the file imports. Replaced with the constants — pure refactor hazard, no behavior change. C-20 remainder — the wrong "SAFETY:" comment at from_call.rs:271 marked no `unsafe` block and was factually wrong (described a `'static` return that isn't what `derive_alpn_from_op_name` does — it returns `Option<String>`; the leak happens in `leak_alpn`). Reworded to a plain note about the `'static` lifetime requirement. Also reworded the abort-cancels claims in the `pump_sink` and `pump_stream` doc comments (dispatch.rs): both claimed `call.aborted` "cancels the task and drops the handler future" — the handler is actually `join!`-ed to completion and not yet cancelled (the abort-cancels-Pub mechanism is review 001 Unit 9). Trimmed step-numbered narration comments in adapter.rs / client.rs that restated what the code does, keeping the ordering-constraint and REQ-CH comments. The big reassembly.rs deliberation landed with Unit 4; this finishes the remainder. C-09 — fixed the 2 remaining `cargo doc` warnings (was 4; the register_openable links were fixed in Unit 3): - `unresolved link to default_policy` (operations.rs:50) — the [`default_policy`] intra-doc link resolves to super::policy::default_policy; used the full path. - `env is both a module and a macro` (channels/mod.rs:30) — the [`env`] link collided with the std `env!` macro; qualified as [`self::env`]. `cargo doc --no-deps` now emits 0 warnings. C-22 remainder — removed the filler `PhantomData` test at client.rs (`let _ = std::marker::PhantomData::<ChannelClient>;` — asserts nothing). The env.rs tautology was already removed in Unit 3. C-24 — replaced "client→server streaming" with "producer→consumer streaming" in call-protocol.md, operation-registry.md, README.md, and open-questions.md (4 occurrences). Per AGENTS.md §8 the convention is producer/consumer, not server/client. The remaining "client→server" references in channels ADRs 034/037 are in stream_type table contexts that Unit 10 (C-26) will handle as part of the spec-doc renumbering. P-11 — amended ADR-046 §3's SinkHandler type so the stream item type matches §6. §3 declared `Pin<Box<dyn Stream<Item = Value> + Send>>`; §6 declared `Pin<Box<dyn Stream<Item = Result<Value, CallError>> + Send>>`. The code uses §6's shape uniformly (registration.rs:32-40, aliased as PublishStream). §3's text and the Door-type section are amended to match §6; the Door-type section already marked the concrete stream item type a two-way-door detail, so this is a text correction, not a design change. Added an amendment note dated 2026-08-13. Verification: - cargo test --lib → 449 passed, 0 failed (was 450; -1 removed filler test) - cargo clippy --all-targets -- -D warnings → clean - cargo fmt --check → clean - cargo doc --no-deps → 0 warnings (was 2) |
|||
| 48564a8f49 |
docs(review 001): decompose Unit 6 into Units 6-10
The original Unit 6 was the largest unit by item count and mixed
trivial doc fixes with real protocol-semantics work (P-03/P-04/P-06/
P-07) in one commit. Decomposed into five focused units, each
independently shippable, ordered by risk and dependency:
- Unit 6 — convention + doc cleanup (C-09, C-20-rem, C-22-rem, C-24,
P-10, P-11): mechanical; `cargo doc` clean, producer/consumer
naming, ADR-046 §3 text amendment, EVENT_* constants in pump_sink.
- Unit 7 — small correctness fixes (C-19-misc, C-21, C-23, P-13):
write_header returns Result not panic, honest error envelopes,
mux pump map leak, derive_alpn_from_op_name dead branch, Box::leak
Arc<str> refactor, REQ-CH-04 counter, opaque-wrap chunk_tx.
- Unit 8 — Pub protocol semantics (P-03, P-04, P-07): publish_schema
per-chunk validation via alktype, initiator call.error terminates
the stream, early handler return short-circuits the feed.
- Unit 9 — abort-cancels-Pub (P-06): drop the handler future on
abort (not just join! to completion); needs a cancellation token
per in-flight sink; depends on Unit 8's pump_sink changes.
- Unit 10 — stubs + substrate + doc renumber (C-10, C-11, C-14,
C-15, C-26): honestly stub the deferred behaviors with OQs, remove
stale doc claims, renumber alknet ADR refs to alkcall ADR-001..047.
Also records which original Unit 6 items were already absorbed by
Units 1-5 (P-09, C-20's reassembly deliberation, C-22's env tautology,
C-19's next_id/demux_sender/mux-error-mapping) so the units below
cover only what remains.
Verified 2026-08-13 against tree
|
|||
| f25d0a6920 |
fix: Unit 5 — ledger decrement on all teardown paths + channel-id adoption (C-06, C-08, C-12, C-13, C-18, C-25 #4 #5)
- C-06: add ChannelLifecyclePolicy to ChannelsAdapter; demux loop decrements per-identity counts on connection drop (clear_all path). Handler-exit teardown: wrap handler tasks in run_open_wrapper to call teardown_channel + on_close on natural completion. Fix check_open leak: on_close on allocation failure in run_open_wrapper. - C-08: odd/even ID split (connect=1, accept=2, step=2) via ChannelSide enum. Add adopt_channel to ChannelManager for non-allocating side routing. Add ChannelClient::open_channel (call open op + adopt). - C-12: reject channel_id:0 in channel/close handler. - C-13: drain-before-close — await handler task (5s timeout) instead of abort, then decrement policy. - C-18: re-check max_channels on re-acquire after mux.register in open_channel (TOCTOU-safe). Tests added: policy_decremented_on_connection_drop, concurrent_opens_respect_max_channels, channel_close_rejects_channel_zero, channel_adoption_end_to_end_round_trip, odd_even_split_no_collision, adopt_channel_installs_routing, adopt_channel_duplicate_id_returns_channel_exists, open_channel_too_many_channels_rejected, connect_side_starts_at_1_accept_side_starts_at_2. Verification: 450 tests pass (was 441; +9), clippy clean, fmt clean. |
|||
| 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 |