# Changelog All notable changes to this crate are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this crate adheres to [Semantic Versioning](https://semver.org/). ## [0.8.1] - 2026-09-28 Bug-fix release: a duplicate `adopt_channel`/`open_channel` no longer destroys the live channel it collided with, found by the crate's new fuzzing harness (`fuzz/` workspace — five cargo-fuzz targets, committed seed corpora, three 10-minute campaigns clean). No API or wire-format change; the error is still `Err(ChannelExists)` — the difference is entirely in what the collision leaves behind. ### Fixed - **Duplicate adopt/open no longer replaces the live channel's state (found by the `manager_routing` fuzz target, `docs/research/fuzzing.md` §7.8).** `ChannelManager::open_channel` and `adopt_channel` used `HashMap::insert(...).is_some()` as the collision check — but `insert` *replaces* an occupied entry and returns the old value, so a duplicate on an in-use id installed the new `ChannelState`, dropped the live channel's `demux_sender` (spurious EOF to its readers; all subsequently routed chunks lost), and *still* returned `Err(ChannelExists)`. The mux write half kept framing onto the transport for a channel the demux no longer fed. Both functions now check `contains_key` and return before any map mutation — the live channel's routing state is untouched on collision. Reachable whenever an open-op response is replayed or a connection-owner race re-announces an id. Regression tests: `adopt_channel_duplicate_id_leaves_live_channel_intact`, `open_channel_duplicate_id_leaves_live_channel_intact` (both verify routing survives a rejected duplicate; the first also verifies the EOF sentinel remains the only EOF source). ### Added (development) - **Fuzzing harness (`fuzz/` workspace, `docs/research/fuzzing.md`).** Five cargo-fuzz targets over the two attacker-reachable wire formats and the stateful channel-routing surface: `chunk_header` (8-byte header no-panic/round-trip/accounting), `envelope_frame` (frame decode error-shape partition + allocation bound), `manager_routing` (`Arbitrary` op sequences over `ChannelManager` with exact counter models and the parked-bytes bound), `envelope_semantic` (all six event kinds: constructors → serde → framing round-trip), and `spec_parse` (attacker-shaped op-register schemas compile or reject cleanly at registration). Invariant logic lives in `fuzz/shared/` — a stable-toolchain crate replayed as plain `cargo test`, so the committed corpora stay executable without nightly. Campaigns (10 min per target, detached runner): ~25M executions total, zero crashes/hangs/OOMs. The `manager_routing` campaign found the 0.8.1 fix above within minutes — exactly the stateful interleaving class example-based tests cannot reach. `fuzz/` is excluded from the workspace and from the published package. ### Verified - 684 tests pass; `clippy --all-targets -- -D warnings`, `fmt --check`, `cargo doc`, wasm32-unknown-unknown check, `cargo semver-checks` (no API changes vs 0.8.0), and `cargo publish --dry-run` clean; corpus replay (`cargo test --manifest-path fuzz/shared/Cargo.toml`) green. ## [0.8.0] - 2026-09-18 Review 008's remediation lands in full — the graduation upstream asks (U-1 flavor-form open-op ids in discovery; U-2 the establisher → reply projection) plus the in-tree channel relay they demanded (`ChannelRelay`, the hub-leg install template, and the review's gate-2 e2e harness). A post-landing audit of that diff hardened the relay's adopted-entry teardown and the discovery ALPN guards, corrected two log/error messages, and review 009's coverage debt is paid in full — all failure-path; no wire or API changes. The channels data plane is untouched (ADR-034/035's one-way doors stay closed); the new surfaces are additive on the call-plane JSON and the registry seams. ### Added - **`Establishment` reply projection (review 008 U-2, ADR-049 amendment 3)** — `Establishment::with_reply_field` / `with_reply_fields` / `reply_fields()` (an optional map riding the typed-opaque plan carrier). The open-op wrapper merges the fields into the success output after reserving `channel_id` — an establisher supplying the reserved key fails loudly (`channel:open_failed`, reason `handler_error`), never shadows. Absent fields leave the reply byte-identical to the pre-amendment shape. - **`ChannelClient::open_channel_with_reply` (review 008 U-2)** — the full open-op success output (the extra reply fields, e.g. a bind-first listener's `bound`) alongside the streams; `open_channel` delegates with the fields discarded, its signature unchanged. - **Flavor-form open-op ids in discovery (review 008 U-1, ADR-047 amendment 3)** — `spec_to_json_pub` emits an explicit `channel_open_alpn` string alongside the boolean marker when the op name is not a standard `channels//(sub|pub)` shape (standard shapes stay byte-stable); `rebuild_spec_for` prefers the explicit string and generalizes the derivation to strip the LAST path segment when the marker is present. `channels/tunnel/direct`-shaped ops now reconstruct WITH the `channel_open` marker through discovery — hubs relaying through `from_call` never see them as plain forwarding stubs. - **`ChannelRelay` (review 008 Unit 3a, ADR-042 as amended by ADR-051)** — the in-tree relay component: `ProducerLeg` (`Arc` + producer-leg `ChannelManager` — not a `ChannelClient`, whose `take_call_ connection` detach is wrong for a hub with multiple connection claimants) and `ChannelRelay::register_relay_openable` (registers a from_call-imported marked spec on a consumer-leg registry via the full open-op wrapper: ACL, per-identity cap/ledger, establishment bound, teardown). The translate hop calls the producer leg with the forwarded payload (`forwarded_for` from the consumer's per-call identity, ADR-026 §3) and adopts the spoke-allocated id; the byte-forward hop is `pump_bidi` awaited inline — the hub never parses chunk framing, ids are per-connection (no rewrite exists). The spoke reply's `channel_id` is stripped (the consumer reply carries the hub-allocated id); the reply's other fields ride through (`bound` flows end-to-end, per-hop truthful). Reason mapping per ADR-051 §3 — the spoke's code and message are preserved (`timeout` maps to `dial_failed`, the one non-1:1 row). The adopted producer-leg channel entry reclaims when the relayed pump completes. Rejection postures are loud: unmarked and Pub-typed marked specs fail registration (`RelayError`), never silent stubs. - **`HubLegImports` + `HubLegTemplate` (review 008 Unit 3b, ADR-051 §5)** — the hub-leg install template as an in-tree export: the `Clone` discover/stash (from_call bundles split by the marker, with the per-consumer `filtered`/`only` subset filter) and the `install_channel_zero` hook composing fork + generic channel ops + bootstrap discovery closed over the fork (review 004 F-06) + plain bundles as-is + relay openables + serving-identity resolution (CF-005 precedence) + the single-stream dispatch loop. A Pub-typed (or unregistrable) marked spec ends the leg's install task — channel 0 never dispatches. Discovered bootstrap-discovery op names are skipped on re-registration (the template's own install supersedes the imported copies). ### Changed - **New module `src/channels/relay.rs` and `src/channels/hub_leg.rs` re-exported from `channels`**; the relay's teardown fix is part of this landing (no pre-relay behavior to preserve). ### Fixed Post-landing audit of the remediation diff — hardening guards, message corrections, and one rejection-posture fix; all failure-path. - **Adopted-entry teardown on every relay plan path** (ADR-051 §6) — the relay's `RelayPlan` now owns the producer-leg `ChannelManager` and reclaims the adopted spoke `channel_id` via a `Drop` guard, instead of the pump handler's post-`pump_bidi` explicit reclaim. This closes the leak windows the pump's normal path cannot reach: the open wrapper's establishment bound expiring after the adopt (the plan dropped before the pump ever spawns) and the pump handler's early-return arms (plan absent, downcast failure, `try_unwrap` failure, `accept_bi` failure). Dropping the plan's send half still EOFs the spoke side via the mux pump's implicit-EOF sentinel, so the spoke handler reclaims through the same cascade as before. Regression test: `relay_plan_drop_reclaims_the_adopted_producer_leg_entry`. - **Empty/whitespace `channel_open_alpn` rejected at rebuild** — an explicit string of `""` (or whitespace-only) previously overrode a sane name-derived ALPN, poisoning the marker for every consumer from one misconfigured producer; `rebuild_spec_for` now trims and rejects empties, falling back to the derivation. - **Empty-segment standard-shape names** — `channels//sub` was serialized boolean-only (the standard-shape check saw flavor `sub`) but reconstructed UNMARKED (the derivation's empty-segment guard returned `None`) — a silent stub for a marked op. The standard-shape check now applies the same empty-segment guard, so the explicit `channel_open_alpn` string rides the wire and the marker reconstructs. - **Reserved-reply-key error text and log level** — the `channel:open_failed` message interpolates `RESERVED_REPLY_KEY` instead of hardcoding `"channel_id"` (the two could drift), and the establisher-bug log fires at `warn!` (a programming error), not `debug!`. ### Testing Review 009's coverage debt (all seven findings, no wire or API changes; three as-filed errata recorded in the review): - **C-1** — the template's plain-bundle install-failure arm is pinned (un-compilable `input_schema` → the install task ends before the dispatch loop, channel 0 never dispatches); ADR-051 §5 carries the loud-install coverage note (relay-openable + plain-bundle arms pinned; generic-ops + bootstrap-discovery arms documented as best-effort-loud). - **C-2** — the `HubLegImports` filter: the `filtered` closure path and the `only` marked/plain partition unit-tested; the empty-stash e2e gate (serves only the generic ops + discovery, dropped ops resolve `NOT_FOUND`). - **C-3** — the batch-form reserved-reply-key rejection (`with_reply_fields` smuggling `channel_id`) pinned: reason `handler_error`, channel torn down, ledger decremented, pump never spawns. - **C-4** — `open_channel_with_reply`'s failure path pinned e2e: the typed error carries the `channel:open_failed` code and the full `details` shape (`reason` + `message`) through the new API. - **C-5** — both "byte-identical" claims are golden-pinned: the no-fields reply against the exact `{"channel_id": 2}` literal, and the standard-shape wire payload against the full 9-key literal. - **C-6** — the derivation's edge shapes pinned: `channels//sub` / `channels//direct` / `channels` → `None`; the 4-segment strict superset (`channels/x/sub/extra` → `Some("x/sub")`) annotated as the pre-amendment behavior change; the verbatim `channels/alk/tty/sub` case. - **C-7** — the builder's overwrite semantics pinned last-win (single and batch forms) with the doc sentence on `with_reply_field`. ### Verified - 682 tests pass (`cargo test`), `clippy --all-targets -- -D warnings` and `fmt --check` clean. The gate-2 e2e harness (`src/channels/gate2_tests.rs`) pins the review's gate 2: the full consumer → hub → producer relay through the template (hub-allocated id, `bound` surviving, data both directions with a fake 8-byte chunk header riding verbatim), hub-side disconnect tearing down both legs, the mid-establishment window's two reclaim signals (ADR-051 §6), and the `channels/tty/sub` standard-shape companion. ## [0.7.1] - 2026-09-09 The MSRV floor becomes honest: `rust-version` raises from 1.85 to 1.88. The 1.85 claim was already false at the dependency level — the resolved lockfile pulls icu 2.x (MSRV 1.86) and wasip2 1.87 transitively via `jsonschema` → `idna`, so no 1.85 toolchain could build 0.7.0. The raise aligns with the ecosystem floor (noq's 1.88, matching the QUIC path) and breaks no downstream that could build the crate before. No API or wire-format change; downstreams on caret requirements pick this up on their next `cargo update`. ### Changed - **`rust-version` 1.85 → 1.88.** Verified on a real 1.88 toolchain (full test suite, clippy). Two clippy lints promoted since 1.85 were fixed to keep `-D warnings` clean on the new floor: `collapsible_else_if` (`channels::mux`) and `uninlined_format_args` (`core::types`, `registry::registration`). Behavior-identical. ## [0.7.0] - 2026-09-07 The connect-side caller-identity seam (CF-005) plus the full sweep of every open alkcall finding from downstream consumers (CF-006, CF-007): a connect-side serving op can now authenticate the transport-authenticated peer by key-based identity (mTLS/QUIC), the open-op establisher/pump handler see the per-call opener identity, and the ADR-016 code list is current. The wire surface is unchanged. ### Added - **`ServingConfig.identity: Option` (CF-005 remediation (a))** — an explicit caller identity for the connect-side serving dispatch (`ChannelClient::from_connection_with_serving`). Wins over the propagated transport identity; the payload `auth_token` path still takes precedence over both (ADR-017 §7). **Semver-relevant at 0.x:** struct literals must add `identity: None`. - **`core::auth::NoopIdentityProvider`** — a public `IdentityProvider` that resolves nothing; the identity-less posture for ACL-free serving and the `ServingConfig::default()` provider (three private test copies of it existed across the crate). ### Changed - **The transport identity propagates to channel 0 (CF-005 remediation (b)).** `from_connection_with_serving` copies the transport `Connection::identity()` to the channel-0 connection via `set_identity` before the serving loop starts, mirroring the accept side (the adapter hands the install hook the transport `AuthContext`). The connect-side serving dispatch now resolves the caller identity in precedence order: payload `auth_token` → `ServingConfig.identity_provider`; `ServingConfig.identity`; transport identity. With none, the dispatch runs identity-less and `AccessControl::check` fails closed (`FORBIDDEN`) — unchanged. The identity resolution is process-local (`set_identity` on the internal channel-0 `Connection`); nothing new crosses the transport. - **The open-op establisher and pump handler see the per-call opener identity (CF-006 — the CF-005 corollary).** `run_open_wrapper` derives a per-call `AuthContext` — the opener's dispatch-resolved identity overlaid onto the install-time context — and passes it to both the establisher (ADR-049 §1) and the pump handler (`OpenHandler`). Identity-less calls keep the install-time identity; transport-truthful fields (`alpn`, `remote_addr`, `tls_client_fingerprint`) are never rewritten. Signatures are unchanged — behavior-only; on a per-connection registry (ADR-047 §4) this is identical to before, and on hub-forwarded opens the establisher now sees the end client instead of the hub. - **ADR-016 amended: the protocol-code list is eight codes (CF-007 — alkhttp review 006 Part C doc drift).** `ALREADY_EXISTS` (non-retryable, `op/register` collision) and `CONNECTION_CLOSED` (retryable, provably-undelivered call) are now in the Context paragraph, the §3 table, and the `from_openapi` collision rule; §2a documents the undelivered-vs-ambiguous write-failure distinction. Doc-only. - **`ChannelPlan` type doc carries the `Send + Sync` payload constraint** (alktunnels POC F-1 — a non-`Sync` boxed handle needs a wrapper; documented so the next consumer doesn't re-derive it by compiler error). - Regression gates: the four `cf005_*` tests in `src/channels/client.rs` (transport propagation, override precedence, identity-less failure, token fallback + precedence) and the two open-op identity-overlay tests in `src/channels/operations.rs`. Ledger: CF-005, CF-006, CF-007 → resolved (the ledger's Open section is now empty). Specs: ADR-022 §connect-side-serving, ADR-016 (code list), ADR-049 establisher doc note. ## [0.6.0] - 2026-09-07 The establishment follow-ups sweep (review 007): the `Establishment` plan payload lands (R-01), the `OpenHandler` lifetime contract is documented (R-02), and the two-pump helper is extracted (R-03). One breaking change (below); the wire surface is unchanged. ### Added - **`pump_bidi` (review 007 R-03 / ADR-050).** The two-pump helper — `channels::pump_bidi(channel, peer_read, peer_write)` — pumps a channel stream (`BiStream`-shaped) against a peer's split read/write halves: one pump per direction, each shutting down the opposite sink on completion (alknet ADR-078's shutdown-on- completion), joined, returning `(u64, u64)` copy counts. Errors are EOF-shaped by design (the POC + ADR-078 semantics — no `Err` state; the review sketch's `io::Result` return was dead code). The helper pins the contract in one place instead of three (alktunnels POC `pump_halves`, alktty's channels session, the next consumer). Purely additive. ### Changed - **`Establishment` carries the channel plan (review 007 R-01 — breaking at 0.6.0).** The reserved field is filled: `Establishment { plan: Option }` with `ChannelPlan = Arc` — typed-opaque, because the payload an establisher hands the pump handler is a live handle (a dialed socket, a TTY handle), not JSON. The wrapper threads `establishment.plan` to the `OpenHandler`'s new second parameter (`Fn(Value, Option, Connection, AuthContext) -> JoinHandle<()>`); `None` when no establisher is registered or it returned `Establishment::default()`. Process-local: establisher → wrapper → handler; nothing new crosses the transport. This kills the side-channel handoff the alktunnels POC shipped (resource-keyed slot + poll loop) with its concurrent same-resource race — each open's establisher result flows to its own handler. Migration: `Ok(Establishment {})` → `Ok(Establishment::default())` (or `Establishment::new(handle)` to deliver a handle); handler closures gain a `_plan` (or `plan`) parameter. - **`OpenHandler` lifetime contract documented (review 007 R-02).** Doc-only semantics note on the `OpenHandler` type and the registration entry points: the returned `JoinHandle` must track the data-plane lifetime — the wrapper awaits it and its completion triggers channel teardown (drop of the demux sender = EOF to the handler's read half); a handler that returns before its pumps finish tears the channel down at birth (await pumps inline, never spawn-and-forget). Plus a `debug!` telemetry line in `run_open_wrapper` when a handler exits without having accepted the channel's `BiStream` (the birth-teardown hint). ## [0.5.0] - 2026-09-06 The channel-open establishment phase (ADR-049 — review 006 E-01 + N-1). Additive establishment machinery; one breaking change (the `ChannelClient::open_channel` error type, below). ### Added - **`OperationSpec.description` (review 006 E-02).** Additive `Option` op description (builder: `with_description`), disclosed by `services/list` and `services/list-peers` (local listings) when set, carried in the `services/schema` wire shape (`spec_to_json_pub` emit / `rebuild_spec_for` parse — the field survives `from_call` discovery and `op/register` announcement). Describes the op, not the produced resource set (ADR-047 §6 amendment: the live resource-enumeration half stays deferred, OQ-40). Absent on the wire when unset — additive for all consumers. - **Channel-open establishment phase (ADR-049 — review 006 E-01).** `ChannelCore::register_openable_with_establisher` registers a per-ALPN open op with an establisher hook (`OpenEstablisher`): an awaited establishment phase — validate params semantically, dial the backend — bounded by the dispatch deadline or the registration's timeout override, else `ESTABLISHMENT_TIMEOUT` (10s; the earlier of the two). On establisher failure (error or deadline) the wrapper tears down the just-allocated channel (demux sender, opener-ledger take, `policy.on_close` un-increment — the allocation and teardown balance) and replies `channel:open_failed` with `details: { reason, message }`, reason ∈ `dial_failed` / `unknown_resource` / `resource_shortage` / `handler_error` / `timeout`. The SSH contract holds consumer-visibly: a failed open never returns a `channel_id`. `register_openable` is unchanged (no establisher = always-OK, existing registrations compile and behave identically); the establisher takes `(input, auth)` — the channel's yield-once `BiStream` belongs exclusively to the pump handler. Additive wire surface (new error-code string + `details` shape). ### Changed - **`ChannelClient::open_channel` returns a typed error (ADR-049 §4 — review 006 N-1, breaking at 0.5.0).** The open op's `CallError` is carried verbatim in `ChannelOpenError::CallFailed` instead of being flattened into a debug-formatted string, so consumers branch on `channel:open_failed`'s typed reason (`establishment_reason()`). Other variants: `MissingChannelId` (malformed success reply), `AdoptFailed` (local adoption failure). Mechanical for consumers — the `String` was a debug-formatting wrapper. ## [0.4.1] - 2026-09-05 Bug-fix release: chunks arriving for a not-yet-adopted channel are parked instead of dropped (the open-op response / first-data race). No API changes. ### Fixed - **Early-arrival chunks for un-adopted channels are parked, not dropped.** The connect side adopts a channel (installs local routing state) only after the open-op response arrives, but the accept side's `OpenHandler` can start pumping data the moment the channel opens — the two race. Previously the connect side's demux dropped those chunks (REQ-CH-04's lenient unknown-channel drop), silently losing the first chunks of any push-first producer (a TTY backend's banner or greeting, a sub protocol's initial frame). `route_payload` now parks up to 64 payloads per unknown `channel_id` in a bounded early-arrival buffer and `adopt_channel` drains them into the new receiver in order; chunks beyond the cap drop with the pre-existing debug log and `dropped_unknown_chunks` counter (the counter now means "early-arrival overflow or genuinely unknown channel", not just the latter). `clear_all` drops the parked buffers with the connection. Surfaced by alktty's consumer end-to-end test (review #001 L3): the session never resolved because the producer's first chunks (the stdout sentinel + exit chunk for an immediately-resolving backend) arrived before the adopt and were dropped. ## [0.4.0] - 2026-09-05 `input_schema` is now enforced at call time (the advertise-vs-enforce leg ADR-016 promised: `INVALID_INPUT` is the registry's schema-mismatch error code). Minor bump — behavioral break for any caller that was passing schema-violating inputs to ops declaring a non-trivial `input_schema` and relying on the validation being documentation-only. ### Changed - **`OperationSpec.input_schema` is enforced by the registry on every dispatch.** Until now the schema was advertise-only: `services/schema` disclosed it, the wire and gateway paths disclosed it, but no dispatch entry point consulted it (the only schema alkcall actually enforced was `publish_schema` per-chunk on Pub ops, P-03). The schema now compiles once at registration (same fail-closed rule as `publish_schema`/CF-003: an un-compilable schema is a registration error, never a silently-skipped contract) and is checked after the ACL gate in all three dispatch entry points — `invoke`, `invoke_streaming`, and `invoke_sink` (via `resolve_sink_handler`, preserving the P-08 single-source-of-truth property). Violations return `CallError::invalid_input("input failed input_schema validation")` with the input echoed in `details`. Raw-JSON-Schema semantics (permissive on unknown keys) — adapters that want closed-by-default enforcement keep doing their own hardening, as alkhttp's `CompiledInputSchema` already does; the two checks compose. Built-in ops are unaffected: alkcall's own specs declare the empty object schema (accepts anything) and `services/schema` already returned `INVALID_INPUT` for a missing `name` before its handler ran. Motivated by alktty's code review #001 (L1): the channels open-op wrapper hands the registry-checked `input` to the `OpenHandler` as the authoritative params, which requires the registry to actually validate it. - `OperationRegistry` gained an `input_validator` cache (compiled at registration, mirrored on `fork` and in `OperationRegistryBuilder` like the publish validators); `input_validator()` is `pub(crate)` — the public surface is unchanged. ## [0.3.1] - 2026-09-04 Bug-fix release: `services/list-peers` can now list peer-announced ops. No API breaks — `OperationEnv` gains one defaulted trait method (non-breaking for all implementors; `cargo semver-checks` 196 checks pass against the 0.3.0 baseline). ### Fixed - **`services/list-peers` shows each peer's operations** (UP-03, surfaced by alkhttp's review 006). `PeerCompositeEnv` overrode `peer_ids` only, so `peer_operations` fell to the trait default (`Vec::new()`) and every peer listed with an empty operations array — the ADR-022 amendment's "announced op is discoverable via `services/list-peers`" promise never resolved on the wire. ADR-030 prescribed the fix but it had never been ported into alkcall; the existing `list-peers` unit tests passed because they mock `peer_operations` with hand-rolled envs. Implemented per ADR-030: `OperationEnv::list_operation_names` (default `Vec::new()`) + overrides on `OverlayOperationEnv` (overlay map keys), `PeerCompositeEnv` (`peer_operations` delegates to the peer's overlay; its own aggregate mirrors `contains()`: session + connections + base), `LocalOperationEnv` (registry names), and `ChannelsSessionEnv` (delegates to base). Gate: `announced_op_is_discoverable_via_services_list_peers` exercises the exact `compose_root_env` shape (announce via `op/register`, then assert both the `peer_operations` probe and the `services/list-peers` wire shape attribute the op to the peer) — verified load-bearing (reverting the override fails the gate). ADR-030 status is now Accepted with the UP-03 provenance note. ## [0.3.0] - 2026-09-04 The connect side can serve (two-way ops over one channels connection), per-session fork registries, and the `op/register` bootstrap op — the remediation of reviews 004 and 005 (`docs/reviews/004-*.md`, `docs/reviews/005-*.md`). Two `OperationRegistry` methods changed their return types from borrowed to owned (source-breaking for annotated call sites; minor bump per 0.x semver rules) — verified with `cargo semver-checks` (196 checks pass against the published 0.2.0 baseline; the return-type changes were caught by manual diff review, as the tool has no lint for that pattern). ### Added - **`OperationRegistry::fork`** — deep-copies a registry (handlers, provenance, composition authority, capabilities, cached publish-schema validators) into an independently-mutable copy. The per-session shape: fork the deployment's base registry, register the session's ops on the fork, dispatch the session over the fork (ADR-047 §4 amendment, 2026-09-03). Internally mutable, so a fork shared as an `Arc` can receive registrations after the dispatcher was built — `install_bootstrap_discovery` relies on this. - **`OperationRegistryBuilder::from_registry`** — seeds a builder from an existing registry's registrations (the fork surface expressed through the builder; registration order is not preserved — HashMap iteration). - **Connect-side serving (two-way ops over one connection).** The channels connect side's channel-0 read pump previously resolved outbound responses only and silently dropped inbound `call.requested` frames. New `ChannelClient::from_connection_with_serving` takes `Option` (`registry` + `identity_provider`); with `Some(..)` the read pump becomes the full-duplex serving loop (`Dispatcher::serve_single_stream`), so a connected peer can call the consumer's ops. `from_connection` keeps the resolution-only pump (pure-consumer default). Serving is opt-in: the protocol is symmetric, the API is explicit (ADR-022 amendment, 2026-09-03). - **`Dispatcher::serve_single_stream` and `Dispatcher::dispatch_start` + `StartedDispatch`** — the shared serving-loop machinery. Both single-stream loops (accept side and connect side) now run the same concurrency model: Once invocations, Sub pumps, and sink response writers are spawned, so same-connection nested composition resolves concurrently instead of deadlocking until the 30 s sweeper (review 005 G-01). Handles are tracked and aborted at loop exit; no lock is held across an await. - **`registry::op_register` module — the `op/register` bootstrap op** (review 004 F-05). A connected peer announces the ops it serves over the wire: `OP_REGISTER_NAME` (`op/register`), `OpRegisterRequest` (wire round-trip via `to_json`/`from_json`), `op_register_spec` / `op_register_handler`. Announcements land in the connection overlay and are served through forwarding handlers, so hub→consumer import (`from_call`) and consumer→hub announce are symmetric. **Collision policy** (review 005 G-03): a peer-announced op may replace other peer-announced ops (when `replace` is set) but never the serving side's own registrations — a name present on the serving registry rejects with the new `CallError::already_exists` (`ALREADY_EXISTS`, non-retryable), regardless of `replace`. The collision gate checks the serving registry, the session fork, and the connection overlay itself. - **`install_bootstrap_discovery`** (review 004 F-06) — registers `services/list`, `services/list-peers`, and `services/schema` against a registry with handlers closed over that same `Arc`, so per-session forks are the discovery source for their own openables (handlers see every op the registry serves at call time). ACL filtering stays per-caller. The bootstrap-op set on channel 0 is closed and now includes `services/list-peers` (review 005 G-05 — doc alignment; the code had installed it since the amendment landed) plus `op/register` (ADR-022 amendment). - **`spec_to_json_pub`** — public serialization of an `OperationSpec` into the `services/schema` wire shape (the shape `op/register` announces with and `services/schema` serves). **`resource_id_path` now survives the spec wire round-trip** (review 005 G-04): both the serializer and the `op/register` parser carry the field. - **`CallConnection::overlay_contains` / `CallConnection::overlay_registration`** — read access to the connection overlay, for `op/register` replace semantics and composition reachability checks. ### Changed - **`OperationRegistry::registration` returns `Option`** (was `Option<&HandlerRegistration>`) and **`list_operations` returns `Vec`** (was `Vec<&OperationSpec>`). Source-breaking for call sites that annotate the borrowed types — clone-at-the-boundary instead. Minor bump per 0.x semver rules. - **Registration is `&self` throughout** — `OperationRegistry::register`, `ChannelOperations::register_on`, and `ChannelCore::register_openable` take `&OperationRegistry` (was `&mut`). Source-compatible for callers (reborrow); this is what makes post-construction registration on a forked, `Arc`-shared registry possible. - **`from_call` composition reachability** — the forwarding-handler path declares reachable namespaces on the composing handler's `scoped_env` (empty is deny-by-default) and the connection overlay is attached only for identity-carrying connections (ADR-030 §5). ## [0.2.0] - 2026-08-31 Consumer-findings remediation (CF-001..004 from `docs/reviews/consumer-findings-ledger.md` — alkhttp as the first real consumer), the promoted `gateway` dispatch spine (ADR-048), and a docs-hygiene pass. One behavior change noted below; otherwise additive. ### Added - **`gateway` feature: the transport-neutral dispatch spine** (ADR-048). New `alkcall::gateway` module behind the opt-in `gateway` cargo feature (default off; adds no dependencies). `GatewayDispatch` is the deadline-bounded, re-rooted-context invoke spine over `OperationRegistry` (`invoke` / `invoke_streaming` / `invoke_sink`) promoted from alkhttp's gateway after it proved transport-agnostic — hubs and spokes relaying calls (ADR-042 translate path) need the identical root-context discipline (`internal: false`, `forwarded_for: None`) without any HTTP. `schema_disclosure_denial` is the shared is-internal + ACL check for `services/schema` inner-op-name disclosure (one implementation so transports cannot drift; ACL denial returns `FORBIDDEN`, Internal visibility returns spec-404 — see ADR-048 for the split from the wire handler's conservative spec-404). The handler deadline is a constructor knob (`with_deadline`); the default remains 30 s. alkhttp migrates to this module in a follow-up and drops its local copy. ### Changed - **Un-compilable `publish_schema` values are rejected at registration time** (CF-003). `OperationRegistry::register` and every `OperationRegistryBuilder` method now return `Err` when a Pub op's `publish_schema` fails to compile. Previously the bad schema was accepted and the dispatch path failed **open** (warn log, chunks unvalidated). The compiled validator is now cached per-op (`OperationRegistry::publish_validator`) and consumed by the dispatch path — the per-request compile is gone. Code that registered un-compilable schemas will now get a registration error instead of a silently-unvalidated op. ### Fixed - **`services/schema` no longer discloses Internal or ACL-restricted op specs** (CF-004). The schema handler now applies the same gates as `invoke()` — Internal-visibility rejection and `AccessControl::check` with matching identity resolution. Restricted ops return spec-404 (`NOT_FOUND`), consistent with "restricted ops don't exist" elsewhere; no information leaks about the restricted surface. The gate is inside the handler, so every transport is covered. - **Demux `TooLarge` skip no longer allocates from the peer's header** (CF-002). The skip now streams through a fixed 64 KiB buffer, and a cumulative 256 MiB skipped-bytes budget tears down connections that loop oversized headers. Previously the buffer was sized from the untrusted `u32` length (up to ~4 GiB pinned per dribbling peer). - **Write failures before request delivery are retryable** (CF-001). New `CallError::connection_closed` (`CONNECTION_CLOSED`, `retryable: true`) is returned when the `call.requested` frame write fails on any consumer path (`call`/`subscribe`/`publish`, both stream modes) — the call provably never reached the producer, so reconnect/retry is safe. Mid-publish write failures and producer-side connection-teardown failures remain non-retryable `INTERNAL` (delivery ambiguous). The new code string is additive; the `retryable` flag is the machine-readable signal for consumers. ### Fixed (continued) - **Doc hygiene.** Resolved the broken intra-doc link on `lib.rs`'s feature-gated `gateway` mention (rustdoc warned on default-feature builds), and re-pointed 40 `src/` doc references from the old alknet mono-repo ADR numbering (ADR-049/050/052/065/070/074/092) to this crate's numbering (ADR-021/011/034/007/008/009/005) so published docs reference ADRs this crate actually carries. Failure-path coverage was added alongside: the CF-002 skipped-bytes teardown and skip-EOF arms, CF-001's subscribe write-failure sites (both stream modes), the publish request-frame failure in single-stream mode, and non-retryability pinning for mid-publish failures. ## [0.1.1] - 2026-08-17 A minor release that renames the ALPN prefix from `alknet/` to `alk/` and drops the `alktype` dependency. No public API changes — verified with `cargo semver-checks` (196 checks pass, no semver update required). ### Changed - **ALPN prefix renamed.** All ALPN strings changed from `alknet/` to `alk/`: `alknet/call` → `alk/call`, `alknet/channels` → `alk/channels`. The rename landed before the first published consumer (alktty), so no interop break exists. `CHANNELS_ALPN` is now `b"alk/channels"`; `CallAdapter::alpn()` returns `b"alk/call"`; `derive_alpn_from_op_name` derives `alk/` from `channels//(sub|pub)` op names. ADR-004 amended with the rename rationale. See Review 003. - **`alktype` dependency dropped.** The only usage (`alktype::validation::build_validator`) was a thin wrapper over `jsonschema::options().build()`; the call site now uses `jsonschema` directly. The crate is leaner — no `alktype` (and no transitive `indexmap`/`hashbrown 0.17`) in the dependency tree. - **`tokio` features slimmed.** `features = ["full"]` replaced with `["rt", "sync", "time", "io-util", "macros"]` — the crate uses no net/fs/process features. This unblocks `wasm32-unknown-unknown`. - **WASM support.** The crate now compiles for `wasm32-unknown-unknown` (verified with `cargo check` and `cargo clippy` on the wasm target). `uuid` gained the `rng-getrandom` feature and `getrandom 0.4` the `wasm_js` feature so request-ID generation works on wasm. ### Added - **BAST document for the chunk header.** `docs/architecture/chunk-header.bast.json` is the machine-readable wire-format spec for the channels 8-byte chunk header (`[channel_id: u32 BE][length: u32 BE]`). BAST is plain JSON — consumable by any language; the `alktype` crate compiles it into readers/writers/validators, and future codegen derives language-specific implementations. Embedded in the crate as `channels::wire::CHUNK_HEADER_BAST` so downstream Rust crates can consume it without a file lookup. The hand-rolled `parse_header`/`write_header` functions remain the hot path; the BAST document is the contract. ## [0.1.0] - 2026-08-14 Initial crates.io release. The unification of `alknet-call` (structured JSON RPC: operations, streaming subscriptions, service discovery) and `alknet-channels` (multiplexing proxy: N logical channels over one transport stream, channel 0 pre-negotiated as the call protocol). Vendored core types (`Connection`, `ProtocolHandler`, `BiStream`, `BidiStreamSource`, `AuthContext`, `IdentityProvider`, `Capabilities`, `OwnershipProvider`), the `Pub` operation type / `HandlerKind::Sink` (ADR-046), the channels protocol with openable-ALPNs-as-operations (ADR-047), and the `ChannelClient` transport-agnostic client. [0.8.1]: https://git.alk.dev/alkdev/alkcall/releases/tag/v0.8.1 [0.8.0]: https://git.alk.dev/alkdev/alkcall/releases/tag/v0.8.0 [0.7.1]: https://git.alk.dev/alkdev/alkcall/releases/tag/v0.7.1 [0.7.0]: https://git.alk.dev/alkdev/alkcall/releases/tag/v0.7.0 [0.6.0]: https://git.alk.dev/alkdev/alkcall/releases/tag/v0.6.0 [0.5.0]: https://git.alk.dev/alkdev/alkcall/releases/tag/v0.5.0 [0.4.1]: https://git.alk.dev/alkdev/alkcall/releases/tag/v0.4.1 [0.4.0]: https://git.alk.dev/alkdev/alkcall/releases/tag/v0.4.0 [0.3.1]: https://git.alk.dev/alkdev/alkcall/releases/tag/v0.3.1 [0.3.0]: https://git.alk.dev/alkdev/alkcall/releases/tag/v0.3.0 [0.2.0]: https://git.alk.dev/alkdev/alkcall/releases/tag/v0.2.0 [0.1.1]: https://git.alk.dev/alkdev/alkcall/releases/tag/v0.1.1 [0.1.0]: https://git.alk.dev/alkdev/alkcall/releases/tag/v0.1.0