chore(release): 0.9.0 — the stub sweep

- 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.
This commit is contained in:
glm-5.3-flash committed 2026-10-14 04:50:55 +00:00
1 parent 4278c13b2d
commit 1a493e9748
6 files changed
+138 -3

No files matched your search

+98
View File
@@ -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
Generated
+1 -1
View File
@@ -27,7 +27,7 @@ dependencies = [
[[package]]
name = "alkcall"
version = "0.8.1"
version = "0.9.0"
dependencies = [
"async-trait",
"bytes",
+1 -1
View File
@@ -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"
+1
View File
@@ -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)",
+30
View File
@@ -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"
);
}
+7 -1
View File
@@ -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<HandlerRegistration>,
) -> Result<(), String> {
let mut overlay = self.imported_operations.write();
for reg in registrations {
for reg in &registrations {
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);
}