From 1a493e9748a1186a29888e005f333aef84c4fba6 Mon Sep 17 00:00:00 2001 From: "glm-5.3-flash" Date: Wed, 14 Oct 2026 04:50:55 +0000 Subject: [PATCH] =?UTF-8?q?chore(release):=200.9.0=20=E2=80=94=20the=20stu?= =?UTF-8?q?b=20sweep?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - version 0.8.1 -> 0.9.0 (minor bump: 5 breaking lints vs 0.8.1 — ManagerError::InvalidAdopt variant, RelayError::PubTypedOpen removal, register_imported* Result return, Establishment Debug derive dropped; all pre-1.0 permitted) - CHANGELOG 0.9.0: S-01..S-04 from reviews 010/011 (the marked-pub open contract, channel/control, channel/resources/subscribe, op/register forward-through), the stub guard, and the verification summary - open_pub_channel: a non-object input tears down the freshly allocated id before the early return (the slot is not parked against the ADR-040 bound; regression test added) - register_imported_all: atomic — a refusal leaves the overlay untouched; atomicity documented Verified: cargo test (697 + stub guard), clippy --all-targets --all-features -- -D warnings, fmt --check, cargo doc, wasm32-unknown-unknown check, fuzz corpus replay 5/5, cargo publish --dry-run --allow-dirty clean. --- CHANGELOG.md | 98 ++++++++++++++++++++++++++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- src/channels/client.rs | 1 + src/channels/pub_open_tests.rs | 30 +++++++++++ src/protocol/connection.rs | 8 ++- 6 files changed, 138 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cf2533..bdbb1a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,103 @@ 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.9.0] - 2026-10-14 + +The stub sweep: four registered-but-always-failing stubs shipped across +nine published versions (reviews 010/011) are now real implementations +— the marked-pub open contract (S-01), `channel/control` (S-02), +`channel/resources/subscribe` (S-03), and `op/register` +forward-through (S-04). Every handler that registers on a discoverable +surface can now succeed; the four stubs are replaced and the class is +release-blocking in both directions (a loud-failure stub is still a +stub — the stub guard plus registration-time invariants hold the gate). + +**Breaking (semver-relevant, hence the minor bump).** New variant +`ManagerError::InvalidAdopt`; `RelayError::PubTypedOpen` removed (the +relay put-translation now allocates and opens on the producer leg); +`register_imported`/`register_imported_all` return `Result<(), String>` +(beyond the boolean); `Establishment` no longer derives `Debug` (the +establisher-contributed `ChannelControl` inner is not `Debug`). + +### Added + +- **The marked-pub open contract — S-01 (ADR-047 amendment 4, review + 011 §3).** The put path end-to-end: + - `ChannelCore::register_pub_openable` + `PubOpenHandler` — the + put's handler returns its result for the merged single response; + the wrapper awaits it inline, teardown is EOF-driven, no + `handler_task` is ever spawned. `register_openable*` refuses + `Pub`-typed specs; the wrapper entry points refuse unmarked / + non-`Pub` shapes. + - The reserved input key `channel_id` (the initiator-allocated id) + is stripped pre-validation by `resolve_sink_handler` (P-08, the + single source of truth), handed to the wrapper via + `OperationContext.metadata`; a missing key rejects + `INVALID_INPUT`; adopt parity is enforced + (`ManagerError::InvalidAdopt`) and adopt failures answer + `channel:open_failed` / reason `adopt_failed`. + - Zero `call.published` on marked ops: the first publish chunk on a + marked op is rejected loudly (`channel:publish_chunks_rejected`) + in both dispatch loops; the handler is never fed. + - The relay's put-translation (ADR-042): the hub allocates odd on + the producer leg, opens the spoke op in the background + (put-acks-in-result), pumps one direction only, and mirrors the + response with the producer's reserved key stripped. + - `ChannelClient::open_pub_channel` + `PubOpenReply` — the + initiator-allocated put open capability (odd ids on the connect + side, ADR-047 §5). A failed open tears the local adopt entry down, + including the pre-flight INVALID_INPUT path (the allocated id is + released, not parked against the ADR-040 bound). +- **`channel/control` implemented — S-02 (ADR-052).** The + establisher-contributed control handle (`Establishment::with_control`) + is registered manager-side at establishment, removed on teardown, and + reachable through the `channel/control` op; an establisher without a + control handle answers `channel:no_control_handle`. +- **`channel/resources/subscribe` implemented — S-03 (ADR-053).** The + per-peer-connection `ResourceEnumerators` registry with + snapshot-then-changes enumerators, scan-once aggregation over the + channel operations, and the empty seed; the always-failing one-shot + error stream is gone. +- **`op/register` forward-through — S-04 (ADR-022 amendment).** + Announced unmarked `Sub`/`Pub` operations register the real + streaming/sink forwarding shapes; marked announcements are refused + with `MARKED_SPEC_UNSUPPORTED`; marked specs refuse both the generic + registration path (`OperationRegistry::register`, `op/register`) and + the connection-overlay path (`CallConnection::register_imported` + — now returning `Result`; `register_imported_all` is atomic: a + refusal leaves the overlay untouched). + +### Changed + +- `HandlerRegistration` gained the private `channel_wrapper` field + (the channel wrapper family's plumbing); externally constructed + registrations stay impossible (`HandlerRegistration::new` unchanged). + +### Fixed + +- **`open_pub_channel` no longer parks a channel slot on the + pre-flight rejection.** The early-return on a non-object input now + tears down the freshly allocated id (regression: + `open_pub_channel_invalid_input_releases_the_allocated_id`). +- `Capabilities::zeroize`: an unused map-key iteration tripped + `clippy::for_kv_map` under `-D warnings`. No behavior change. + +### Guard + +- `tests/stub_guard.rs` — `cargo test` fails if `not_implemented` + appears anywhere under `src/`: the four former pinned-stub tests are + deleted and the marked-spec registration refusals are pinned by + tests instead. + +### Verified + +- 698 tests pass (including the stub guard); `clippy --all-targets + --all-features -- -D warnings`, `fmt --check`, `cargo doc`, + `wasm32-unknown-unknown` check, fuzz corpus replay + (`cargo test --manifest-path fuzz/shared/Cargo.toml`, 5/5), and + `cargo semver-checks` (5 breaking lints vs 0.8.1 — the minor-bump + classification above). + ## [0.8.1] - 2026-09-28 Bug-fix release: a duplicate `adopt_channel`/`open_channel` no longer @@ -737,6 +834,7 @@ Vendored core types (`Connection`, `ProtocolHandler`, `BiStream`, (ADR-046), the channels protocol with openable-ALPNs-as-operations (ADR-047), and the `ChannelClient` transport-agnostic client. +[0.9.0]: https://git.alk.dev/alkdev/alkcall/releases/tag/v0.9.0 [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 diff --git a/Cargo.lock b/Cargo.lock index fa231a9..959adb7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,7 +27,7 @@ dependencies = [ [[package]] name = "alkcall" -version = "0.8.1" +version = "0.9.0" dependencies = [ "async-trait", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 25c6288..4a8206f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "alkcall" -version = "0.8.1" +version = "0.9.0" edition = "2021" rust-version = "1.88" license = "MIT OR Apache-2.0" diff --git a/src/channels/client.rs b/src/channels/client.rs index c09d849..9391c3c 100644 --- a/src/channels/client.rs +++ b/src/channels/client.rs @@ -471,6 +471,7 @@ impl ChannelClient { ); } None => { + let _ = self.manager.teardown_channel(channel_id); return Err(ChannelOpenError::CallFailed { error: crate::protocol::wire::CallError::invalid_input( "marked pub open op input must be an object (ADR-047 amendment 4 §4.3)", diff --git a/src/channels/pub_open_tests.rs b/src/channels/pub_open_tests.rs index 5f631ad..b5c572d 100644 --- a/src/channels/pub_open_tests.rs +++ b/src/channels/pub_open_tests.rs @@ -445,3 +445,33 @@ async fn put_handler_error_becomes_the_response() { Some("blobs:read_only".to_string()) ); } + +/// A non-object input to `open_pub_channel` is rejected INVALID_INPUT +/// before any wire traffic — and the locally allocated channel id is +/// released (teardown) so the slot is not parked until connection +/// close (the 256-per-connection bound, ADR-040). +#[tokio::test] +async fn open_pub_channel_invalid_input_releases_the_allocated_id() { + let (client_end, server_end) = tokio::io::duplex(64 * 1024); + let client_conn = Connection::from_bidi(client_end, b"alk/channels".to_vec(), Some(TEST_ADDR)); + let _captured = serve_put_side(server_end, token_establisher(), digest_handler()).await; + + let client = ChannelClient::from_connection(client_conn) + .await + .expect("client"); + let before = client.manager().open_count(); + assert!(before >= 1, "channel 0 is installed at client init"); + let error = match client.open_pub_channel(PUT_OP, json!(7), "alk/blobs").await { + Ok(_) => panic!("a non-object input is rejected"), + Err(error) => error, + }; + assert_eq!( + error.call_error().map(|e| e.code.as_str()), + Some("INVALID_INPUT") + ); + assert_eq!( + client.manager().open_count(), + before, + "the allocated id is not parked on the early-return path" + ); +} diff --git a/src/protocol/connection.rs b/src/protocol/connection.rs index d3bcedc..edc9fa6 100644 --- a/src/protocol/connection.rs +++ b/src/protocol/connection.rs @@ -194,12 +194,16 @@ impl CallConnection { self.imported_operations.read().get(name).cloned() } + /// Install a bundle of remote announcements on the connection + /// overlay. Atomic: if any registration carries the `channel_open` + /// marker, the overlay is left untouched (nothing partially + /// installed). pub fn register_imported_all( &self, registrations: Vec, ) -> Result<(), String> { let mut overlay = self.imported_operations.write(); - for reg in registrations { + for reg in ®istrations { if reg.spec.channel_open.is_some() { return Err(format!( "register_imported_all: spec `{}` carries the `channel_open` marker — \ @@ -208,6 +212,8 @@ impl CallConnection { reg.spec.name )); } + } + for reg in registrations { let name = reg.spec.name.clone(); overlay.insert(name, reg); }