From 1e20bb77d0aac5774b34a7f8a5eac2419a281587 Mon Sep 17 00:00:00 2001 From: "glm-5.3-flash" Date: Fri, 4 Sep 2026 14:14:22 +0000 Subject: [PATCH] =?UTF-8?q?chore:=20prepublish=20review=20=E2=80=94=20bump?= =?UTF-8?q?=20to=200.3.0,=20changelog,=20doc=20alignment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0.2.0 is already on crates.io (2026-08-31, c16b069); the review 004/005 remediation work is unreleased on top of it and lands as 0.3.0. - Bump version 0.2.0 -> 0.3.0. Two OperationRegistry methods changed borrowed returns to owned (registration, list_operations) — source-breaking for annotated call sites, minor bump per 0.x semver rules. cargo semver-checks passes (196 checks) against the published baseline; the return-type changes were caught by manual diff review. - CHANGELOG 0.3.0: connect-side serving (from_connection_with_serving + ServingConfig), OperationRegistry::fork + builder from_registry, registry::op_register (bootstrap op, collision policy, ALREADY_EXISTS), install_bootstrap_discovery, spec_to_json_pub + resource_id_path round-trip, overlay accessors, concurrent serving loops, &self registration. - README: serving-as-consumer section, consumer role table update, drop the stale `mut` on the registry example. - AGENTS.md: ADR range 001..047 -> 001..048. - Fix rustdoc private-intra-doc-link warning on StartedDispatch. Verification: 589 default / 606 all-features tests, clippy (all-targets, all-features, wasm32) clean, fmt clean, doc clean, publish dry-run OK. --- AGENTS.md | 5 ++- CHANGELOG.md | 97 ++++++++++++++++++++++++++++++++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 32 ++++++++++++- src/protocol/dispatch.rs | 2 +- 6 files changed, 133 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 17084e9..3c70e07 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -215,13 +215,14 @@ cargo semver-checks check-release # before a release ## Architecture Context - `docs/architecture/` — the authoritative spec. Read it before - non-trivial changes. ADRs are numbered 001..047; OQs (open questions) + non-trivial changes. ADRs are numbered 001..048; OQs (open questions) track resolved/deferred decisions. - This crate unifies `alknet-call` and `alknet-channels` from the alknet mono-repo (`/workspace/@alkdev/alknet`). The source architecture docs were ported from `/workspace/@alkdev/alknet/docs/architecture/` and renumbered as - alkcall ADRs (001..047). The ALPN strings (`alk/call`, + alkcall ADRs (001..048; ADR-048 was authored in this crate). The ALPN + strings (`alk/call`, `alk/channels`) are wire-stable going forward (renamed from `alknet/` to `alk/` in v0.1.1, before the first published consumer). - Key ADRs that inform this crate's design: diff --git a/CHANGELOG.md b/CHANGELOG.md index bdf31bf..7f6d586 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,102 @@ 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.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` (`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`** (was `Option<&HandlerRegistration>`) + and **`list_operations` returns `Vec`** (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 @@ -137,6 +233,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.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 diff --git a/Cargo.lock b/Cargo.lock index 0a9307a..2da4446 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,7 +27,7 @@ dependencies = [ [[package]] name = "alkcall" -version = "0.2.0" +version = "0.3.0" dependencies = [ "async-trait", "bytes", diff --git a/Cargo.toml b/Cargo.toml index f97450a..25ff382 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "alkcall" -version = "0.2.0" +version = "0.3.0" edition = "2021" rust-version = "1.85" license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index c7173c8..d4dfca7 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ use alkcall::registry::{ spec::{OperationSpec, OperationType, Visibility, AccessControl}, }; -let mut registry = OperationRegistry::new(); +let registry = OperationRegistry::new(); registry.register(HandlerRegistration::new( OperationSpec::new( "echo/run", @@ -98,6 +98,34 @@ for reg in registrations { let response = conn.call("remote/status", serde_json::json!({})).await; ``` +### Serving your own ops as a connected consumer + +The call protocol is symmetric — both sides of a connection can serve +ops. A `ChannelClient` built with `from_connection` is a pure consumer +(inbound `call.requested` frames are dropped); pass a `ServingConfig` +to also serve your registry to the peer, and use `op/register` to +announce which ops you serve: + +```rust +use std::sync::Arc; +use alkcall::channels::client::{ChannelClient, ServingConfig}; +use alkcall::registry::discovery::install_bootstrap_discovery; + +let registry = Arc::new(OperationRegistry::new()); +// ... register your ops on the registry, then: +install_bootstrap_discovery(®istry)?; + +let client = ChannelClient::from_connection_with_serving( + connection, + Some(ServingConfig { + registry: Arc::clone(®istry), + identity_provider: provider, + }), +).await?; +// peer-callable ops resolve against `registry` on channel 0; +// `client.call_open_op` still works — both directions share the pump +``` + ## Architecture alkcall is a pure protocol crate — no networking, no transport @@ -107,7 +135,7 @@ Downstream crates compose on top of it in a layered dependency chain. | Role | Call protocol | Channels protocol | |------|---------------|-------------------| | **Producer** | Registers ops on an `OperationRegistry`, runs a `Dispatcher` | Runs a `ChannelsAdapter`, registers openable ALPNs via `ChannelCore::register_openable` | -| **Consumer** | Uses `CallConnection` to call ops, uses `from_call` to discover/import remote ops | Uses `ChannelClient` to open channels via `call_open_op` + `open_channel` | +| **Consumer** | Uses `CallConnection` to call ops, uses `from_call` to discover/import remote ops; may also serve its own ops (`from_connection_with_serving`) | Uses `ChannelClient` to open channels via `call_open_op` + `open_channel` | | **Hub** | Both: runs a `Dispatcher` for ops it produces, holds `CallConnection`s to spokes for ops it consumes | Both: runs a `ChannelsAdapter` for inbound connections, holds `ChannelClient`s to spokes | | **Spoke / Worker** | Both: produces ops (its own services), consumes hub ops | Both: produces channels (TTY, tunnel), may consume hub channels | diff --git a/src/protocol/dispatch.rs b/src/protocol/dispatch.rs index a0312f9..eaf7868 100644 --- a/src/protocol/dispatch.rs +++ b/src/protocol/dispatch.rs @@ -115,7 +115,7 @@ impl std::fmt::Debug for DispatchResult { } /// The started half of a dispatched `call.requested` event, from -/// [`Dispatcher::dispatch_start`]. `Once`/`Stream` are returned ready +/// `Dispatcher::dispatch_start`. `Once`/`Stream` are returned ready /// for the caller to spawn (review 005 G-01); `Sink` was started /// inline (its `chunk_tx` must be registered before the next /// `call.published` frame is read) and carries the started handler