Files
alkcall/CHANGELOG.md
T
glm-5.3-flash f8dad9dbc8 feat(review 006 Unit 2): additive OperationSpec.description disclosed via discovery (E-02)
- OperationSpec gains description: Option<String> (builder
  with_description, defaults None; no struct-literal construction
  sites exist, so additive by construction)
- spec_to_json_pub emits description when set; rebuild_spec_for
  parses it back — the field survives from_call discovery and
  op/register announcement (same round-trip pattern as
  resource_id_path / publish_schema)
- services/list and the local-ops half of services/list-peers emit
  description when set; output-schema docs on both listing specs and
  operation_spec_schema advertise the field
- Tests: builder/default, emit/omit, listing emission, schema
  disclosure, schema-doc presence, round-trip + absent-stays-absent
  (8 new; 616 total)
- Docs: review 006 Unit 2 marked IMPLEMENTED; ADR-047 §6 amendment
  records the E-02 discovery decision (listing enrichment lands, the
  channel/resources/subscribe half stays deferred); OQ-40 gains the
  load-bearing note; operation-registry.md struct + listing docs;
  CHANGELOG

Verification: cargo test (616 pass), clippy -D warnings (host +
wasm32), fmt --check, cargo doc --no-deps, wasm32 check — all clean
2026-09-06 19:26:51 +00:00

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

[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 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/<name> to alk/<name>: 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/<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.