- 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
753 lines
39 KiB
Markdown
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
|