# 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.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.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