- 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
387 lines
20 KiB
Markdown
387 lines
20 KiB
Markdown
# 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<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.
|
|
|
|
[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
|