- 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
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). AdditiveOption<String>op description (builder:with_description), disclosed byservices/listandservices/list-peers(local listings) when set, carried in theservices/schemawire shape (spec_to_json_pubemit /rebuild_spec_forparse — the field survivesfrom_calldiscovery andop/registerannouncement). 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_establisherregisters 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, elseESTABLISHMENT_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_closeun-increment — the allocation and teardown balance) and replieschannel:open_failedwithdetails: { reason, message }, reason ∈dial_failed/unknown_resource/resource_shortage/handler_error/timeout. The SSH contract holds consumer-visibly: a failed open never returns achannel_id.register_openableis unchanged (no establisher = always-OK, existing registrations compile and behave identically); the establisher takes(input, auth)— the channel's yield-onceBiStreambelongs exclusively to the pump handler. Additive wire surface (new error-code string +detailsshape).
Changed
ChannelClient::open_channelreturns a typed error (ADR-049 §4 — review 006 N-1, breaking at 0.5.0). The open op'sCallErroris carried verbatim inChannelOpenError::CallFailedinstead of being flattened into a debug-formatted string, so consumers branch onchannel:open_failed's typed reason (establishment_reason()). Other variants:MissingChannelId(malformed success reply),AdoptFailed(local adoption failure). Mechanical for consumers — theStringwas 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
OpenHandlercan 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_payloadnow parks up to 64 payloads per unknownchannel_idin a bounded early-arrival buffer andadopt_channeldrains them into the new receiver in order; chunks beyond the cap drop with the pre-existing debug log anddropped_unknown_chunkscounter (the counter now means "early-arrival overflow or genuinely unknown channel", not just the latter).clear_alldrops 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_schemais enforced by the registry on every dispatch. Until now the schema was advertise-only:services/schemadisclosed it, the wire and gateway paths disclosed it, but no dispatch entry point consulted it (the only schema alkcall actually enforced waspublish_schemaper-chunk on Pub ops, P-03). The schema now compiles once at registration (same fail-closed rule aspublish_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, andinvoke_sink(viaresolve_sink_handler, preserving the P-08 single-source-of-truth property). Violations returnCallError::invalid_input("input failed input_schema validation")with the input echoed indetails. Raw-JSON-Schema semantics (permissive on unknown keys) — adapters that want closed-by-default enforcement keep doing their own hardening, as alkhttp'sCompiledInputSchemaalready does; the two checks compose. Built-in ops are unaffected: alkcall's own specs declare the empty object schema (accepts anything) andservices/schemaalready returnedINVALID_INPUTfor a missingnamebefore its handler ran. Motivated by alktty's code review #001 (L1): the channels open-op wrapper hands the registry-checkedinputto theOpenHandleras the authoritative params, which requires the registry to actually validate it.OperationRegistrygained aninput_validatorcache (compiled at registration, mirrored onforkand inOperationRegistryBuilderlike the publish validators);input_validator()ispub(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-peersshows each peer's operations (UP-03, surfaced by alkhttp's review 006).PeerCompositeEnvoverrodepeer_idsonly, sopeer_operationsfell to the trait default (Vec::new()) and every peer listed with an empty operations array — the ADR-022 amendment's "announced op is discoverable viaservices/list-peers" promise never resolved on the wire. ADR-030 prescribed the fix but it had never been ported into alkcall; the existinglist-peersunit tests passed because they mockpeer_operationswith hand-rolled envs. Implemented per ADR-030:OperationEnv::list_operation_names(defaultVec::new())- overrides on
OverlayOperationEnv(overlay map keys),PeerCompositeEnv(peer_operationsdelegates to the peer's overlay; its own aggregate mirrorscontains(): session + connections + base),LocalOperationEnv(registry names), andChannelsSessionEnv(delegates to base). Gate:announced_op_is_discoverable_via_services_list_peersexercises the exactcompose_root_envshape (announce viaop/register, then assert both thepeer_operationsprobe and theservices/list-peerswire 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.
- overrides on
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 anArccan receive registrations after the dispatcher was built —install_bootstrap_discoveryrelies 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.requestedframes. NewChannelClient::from_connection_with_servingtakesOption<ServingConfig>(registry+identity_provider); withSome(..)the read pump becomes the full-duplex serving loop (Dispatcher::serve_single_stream), so a connected peer can call the consumer's ops.from_connectionkeeps 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_streamandDispatcher::dispatch_startStartedDispatch** — 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_registermodule — theop/registerbootstrap 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 viato_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 (whenreplaceis set) but never the serving side's own registrations — a name present on the serving registry rejects with the newCallError::already_exists(ALREADY_EXISTS, non-retryable), regardless ofreplace. The collision gate checks the serving registry, the session fork, and the connection overlay itself.install_bootstrap_discovery(review 004 F-06) — registersservices/list,services/list-peers, andservices/schemaagainst a registry with handlers closed over that sameArc, 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 includesservices/list-peers(review 005 G-05 — doc alignment; the code had installed it since the amendment landed) plusop/register(ADR-022 amendment).spec_to_json_pub— public serialization of anOperationSpecinto theservices/schemawire shape (the shapeop/registerannounces with andservices/schemaserves).resource_id_pathnow survives the spec wire round-trip (review 005 G-04): both the serializer and theop/registerparser carry the field.CallConnection::overlay_contains/CallConnection::overlay_registration— read access to the connection overlay, forop/registerreplace semantics and composition reachability checks.
Changed
OperationRegistry::registrationreturnsOption<HandlerRegistration>(wasOption<&HandlerRegistration>) andlist_operationsreturnsVec<OperationSpec>(wasVec<&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
&selfthroughout —OperationRegistry::register,ChannelOperations::register_on, andChannelCore::register_openabletake&OperationRegistry(was&mut). Source-compatible for callers (reborrow); this is what makes post-construction registration on a forked,Arc-shared registry possible. from_callcomposition reachability — the forwarding-handler path declares reachable namespaces on the composing handler'sscoped_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
gatewayfeature: the transport-neutral dispatch spine (ADR-048). Newalkcall::gatewaymodule behind the opt-ingatewaycargo feature (default off; adds no dependencies).GatewayDispatchis the deadline-bounded, re-rooted-context invoke spine overOperationRegistry(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_denialis the shared is-internal + ACL check forservices/schemainner-op-name disclosure (one implementation so transports cannot drift; ACL denial returnsFORBIDDEN, 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_schemavalues are rejected at registration time (CF-003).OperationRegistry::registerand everyOperationRegistryBuildermethod now returnErrwhen a Pub op'spublish_schemafails 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/schemano longer discloses Internal or ACL-restricted op specs (CF-004). The schema handler now applies the same gates asinvoke()— Internal-visibility rejection andAccessControl::checkwith 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
TooLargeskip 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 untrustedu32length (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 thecall.requestedframe 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-retryableINTERNAL(delivery ambiguous). The new code string is additive; theretryableflag is the machine-readable signal for consumers.
Fixed (continued)
- Doc hygiene. Resolved the broken intra-doc link on
lib.rs's feature-gatedgatewaymention (rustdoc warned on default-feature builds), and re-pointed 40src/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>toalk/<name>:alknet/call→alk/call,alknet/channels→alk/channels. The rename landed before the first published consumer (alktty), so no interop break exists.CHANNELS_ALPNis nowb"alk/channels";CallAdapter::alpn()returnsb"alk/call";derive_alpn_from_op_namederivesalk/<alpn>fromchannels/<alpn>/(sub|pub)op names. ADR-004 amended with the rename rationale. See Review 003. alktypedependency dropped. The only usage (alktype::validation::build_validator) was a thin wrapper overjsonschema::options().build(); the call site now usesjsonschemadirectly. The crate is leaner — noalktype(and no transitiveindexmap/hashbrown 0.17) in the dependency tree.tokiofeatures slimmed.features = ["full"]replaced with["rt", "sync", "time", "io-util", "macros"]— the crate uses no net/fs/process features. This unblockswasm32-unknown-unknown.- WASM support. The crate now compiles for
wasm32-unknown-unknown(verified withcargo checkandcargo clippyon the wasm target).uuidgained therng-getrandomfeature andgetrandom 0.4thewasm_jsfeature so request-ID generation works on wasm.
Added
- BAST document for the chunk header.
docs/architecture/chunk-header.bast.jsonis 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; thealktypecrate compiles it into readers/writers/validators, and future codegen derives language-specific implementations. Embedded in the crate aschannels::wire::CHUNK_HEADER_BASTso downstream Rust crates can consume it without a file lookup. The hand-rolledparse_header/write_headerfunctions 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.