Files
alkcall/CHANGELOG.md
T
glm-5.3-flash 54c2a3f941 fix(review-008 audit): adopted-entry drop guard; explicit-ALPN guards; review 009
Post-landing audit of the 0.7.1 -> 0.8.0 remediation diff: two
hardening guards, one rejection-posture fix, two log/message
corrections, and the deferred coverage debt filed as review 009.

- RelayPlan owns the producer-leg ChannelManager and reclaims the
  adopted spoke channel_id via a Drop guard (replaces the pump
  handler's post-pump_bidi explicit reclaim). Closes the leak
  windows the pump's normal path cannot reach: the wrapper's
  establishment bound expiring after the adopt, and the pump
  handler's early-return arms (plan absent, downcast failure,
  try_unwrap failure, accept_bi failure). The send-half drop still
  EOFs the spoke leg via the mux pump's implicit-EOF sentinel, so
  the spoke-side cascade is unchanged. ADR-051 §6 documents the
  closed post-adopt window (the pre-adopt §6 window and the
  inside-adopt_channel cancellation point stay as documented).
- rebuild_spec_for trims and rejects empty/whitespace
  channel_open_alpn strings — an empty explicit string previously
  overrode a sane name-derived ALPN.
- op_name_is_standard_channel_open_shape applies the same
  empty-segment guard as the derivation: channels//sub no longer
  serializes boolean-only and then reconstructs unmarked (silent
  stub for a marked op); the explicit string rides instead.
- reserved_reply_key_call_error interpolates RESERVED_REPLY_KEY;
  the establisher-bug log fires at warn! (programming error).
- Regression tests: the plan drop guard, the empty-ALPN fallback,
  the empty-segment shape check (672 tests, 3 new).
- CHANGELOG [Unreleased] entry for the audit fixes.
- docs/reviews/009 — the audit's deferred test-coverage gaps
  (template failure arms, filtered/only, batch reserved key,
  wire failure path, golden pins, derivation edge shapes, builder
  overwrite semantics), each with the test to add and gates.

Verification: cargo test 672 passed; clippy --all-targets -D
warnings clean; fmt --check clean; doc --no-deps clean; wasm32
check clean.
2026-09-18 06:19:56 +00:00

34 KiB

Changelog

All notable changes to this crate are documented here. The format is based on Keep a Changelog, and this crate adheres to Semantic Versioning.

[Unreleased]

Post-landing audit of review 008's remediation (0.7.1 → 0.8.0) — two hardening guards, two log/error-message corrections, one rejection-posture fix, and the ADR-051 §6 residual update. No wire changes; all behavior deltas are failure-path.

Fixed

  • 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 nameschannels//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!.

[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). 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/<seg>/(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<CallConnection> + 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).

Verified

  • 669 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 jsonschemaidna, 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<Identity> (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_tokenServingConfig.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<ChannelPlan> } with ChannelPlan = Arc<dyn Any + Send + Sync> — 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<ChannelPlan>, 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<String> 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<ServingConfig> (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<HandlerRegistration> (was Option<&HandlerRegistration>) and list_operations returns Vec<OperationSpec> (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 throughoutOperationRegistry::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/<name> to alk/<name>: alknet/callalk/call, alknet/channelsalk/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/<alpn> from channels/<alpn>/(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.