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

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