Files
alkcall/CHANGELOG.md
T
glm-5.3-flash 3bda29c419 chore(release): 0.8.1 — duplicate adopt/open fix + fuzz harness bookkeeping
- version 0.8.0 -> 0.8.1 (bug-fix release; semver-checks 196 pass vs 0.8.0)
- CHANGELOG 0.8.1: the manager_routing-found duplicate adopt/open fix,
  the fuzz harness, and the verification summary
- publish exclude gains docs/research/ (internal research notes;
  fuzz/ was already excluded — package verified clean of both)
- fuzzing.md §7.9: standing no-hosted-CI policy — corpus replay is the
  release-verification fuzz gate; campaigns manual via the detached
  runner; OSS-Fuzz out; no workflow files in this repo
- AGENTS.md: corpus replay added to the verification checklist
- lockfiles: alkcall 0.8.1 (root + fuzz)

Verification: 684 tests; clippy -D warnings (main, wasm, fuzz/shared);
fmt clean; doc clean; wasm32-unknown-unknown check+clippy clean;
semver-checks 196 pass; publish --dry-run 116 files (no fuzz/research/
reviews/sdd/AGENTS in the tarball); fuzz corpus replay 5/5 green
2026-09-28 06:56:11 +00:00

753 lines
39 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.8.1] - 2026-09-28
Bug-fix release: a duplicate `adopt_channel`/`open_channel` no longer
destroys the live channel it collided with, found by the crate's new
fuzzing harness (`fuzz/` workspace — five cargo-fuzz targets, committed
seed corpora, three 10-minute campaigns clean). No API or wire-format
change; the error is still `Err(ChannelExists)` — the difference is
entirely in what the collision leaves behind.
### Fixed
- **Duplicate adopt/open no longer replaces the live channel's state
(found by the `manager_routing` fuzz target,
`docs/research/fuzzing.md` §7.8).** `ChannelManager::open_channel`
and `adopt_channel` used `HashMap::insert(...).is_some()` as the
collision check — but `insert` *replaces* an occupied entry and
returns the old value, so a duplicate on an in-use id installed the
new `ChannelState`, dropped the live channel's `demux_sender`
(spurious EOF to its readers; all subsequently routed chunks lost),
and *still* returned `Err(ChannelExists)`. The mux write half kept
framing onto the transport for a channel the demux no longer fed.
Both functions now check `contains_key` and return before any map
mutation — the live channel's routing state is untouched on
collision. Reachable whenever an open-op response is replayed or a
connection-owner race re-announces an id. Regression tests:
`adopt_channel_duplicate_id_leaves_live_channel_intact`,
`open_channel_duplicate_id_leaves_live_channel_intact` (both verify
routing survives a rejected duplicate; the first also verifies the
EOF sentinel remains the only EOF source).
### Added (development)
- **Fuzzing harness (`fuzz/` workspace, `docs/research/fuzzing.md`).**
Five cargo-fuzz targets over the two attacker-reachable wire formats
and the stateful channel-routing surface: `chunk_header` (8-byte
header no-panic/round-trip/accounting), `envelope_frame` (frame
decode error-shape partition + allocation bound), `manager_routing`
(`Arbitrary` op sequences over `ChannelManager` with exact counter
models and the parked-bytes bound), `envelope_semantic` (all six
event kinds: constructors → serde → framing round-trip), and
`spec_parse` (attacker-shaped op-register schemas compile or reject
cleanly at registration). Invariant logic lives in
`fuzz/shared/` — a stable-toolchain crate replayed as plain
`cargo test`, so the committed corpora stay executable without
nightly. Campaigns (10 min per target, detached runner): ~25M
executions total, zero crashes/hangs/OOMs. The `manager_routing`
campaign found the 0.8.1 fix above within minutes — exactly the
stateful interleaving class example-based tests cannot reach.
`fuzz/` is excluded from the workspace and from the published
package.
### Verified
- 684 tests pass; `clippy --all-targets -- -D warnings`, `fmt --check`,
`cargo doc`, wasm32-unknown-unknown check, `cargo semver-checks`
(no API changes vs 0.8.0), and `cargo publish --dry-run` clean;
corpus replay (`cargo test --manifest-path fuzz/shared/Cargo.toml`)
green.
## [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). A post-landing audit of that diff hardened the relay's
adopted-entry teardown and the discovery ALPN guards, corrected two
log/error messages, and review 009's coverage debt is paid in full —
all failure-path; no wire or API changes. 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).
### Fixed
Post-landing audit of the remediation diff — hardening guards,
message corrections, and one rejection-posture fix; all failure-path.
- **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 names** — `channels//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!`.
### Testing
Review 009's coverage debt (all seven findings, no wire or API
changes; three as-filed errata recorded in the review):
- **C-1** — the template's plain-bundle install-failure arm is pinned
(un-compilable `input_schema` → the install task ends before the
dispatch loop, channel 0 never dispatches); ADR-051 §5 carries the
loud-install coverage note (relay-openable + plain-bundle arms
pinned; generic-ops + bootstrap-discovery arms documented as
best-effort-loud).
- **C-2** — the `HubLegImports` filter: the `filtered` closure path
and the `only` marked/plain partition unit-tested; the empty-stash
e2e gate (serves only the generic ops + discovery, dropped ops
resolve `NOT_FOUND`).
- **C-3** — the batch-form reserved-reply-key rejection
(`with_reply_fields` smuggling `channel_id`) pinned: reason
`handler_error`, channel torn down, ledger decremented, pump never
spawns.
- **C-4** — `open_channel_with_reply`'s failure path pinned e2e: the
typed error carries the `channel:open_failed` code and the full
`details` shape (`reason` + `message`) through the new API.
- **C-5** — both "byte-identical" claims are golden-pinned: the
no-fields reply against the exact `{"channel_id": 2}` literal, and
the standard-shape wire payload against the full 9-key literal.
- **C-6** — the derivation's edge shapes pinned: `channels//sub` /
`channels//direct` / `channels` → `None`; the 4-segment strict
superset (`channels/x/sub/extra` → `Some("x/sub")`) annotated as the
pre-amendment behavior change; the verbatim `channels/alk/tty/sub`
case.
- **C-7** — the builder's overwrite semantics pinned last-win (single
and batch forms) with the doc sentence on `with_reply_field`.
### Verified
- 682 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 `jsonschema` → `idna`, 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_token` →
`ServingConfig.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` 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.8.1]: https://git.alk.dev/alkdev/alkcall/releases/tag/v0.8.1
[0.8.0]: https://git.alk.dev/alkdev/alkcall/releases/tag/v0.8.0
[0.7.1]: https://git.alk.dev/alkdev/alkcall/releases/tag/v0.7.1
[0.7.0]: https://git.alk.dev/alkdev/alkcall/releases/tag/v0.7.0
[0.6.0]: https://git.alk.dev/alkdev/alkcall/releases/tag/v0.6.0
[0.5.0]: https://git.alk.dev/alkdev/alkcall/releases/tag/v0.5.0
[0.4.1]: https://git.alk.dev/alkdev/alkcall/releases/tag/v0.4.1
[0.4.0]: https://git.alk.dev/alkdev/alkcall/releases/tag/v0.4.0
[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