chore: prepublish review — bump to 0.3.0, changelog, doc alignment

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.
This commit is contained in:
glm-5.3-flash committed 2026-09-04 14:14:22 +00:00
1 parent d5b2661b38
commit 1e20bb77d0
6 files changed
+133 -7

No files matched your search

+3 -2
View File
@@ -215,13 +215,14 @@ cargo semver-checks check-release # before a release
## Architecture Context ## Architecture Context
- `docs/architecture/` — the authoritative spec. Read it before - `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. track resolved/deferred decisions.
- This crate unifies `alknet-call` and `alknet-channels` from the - This crate unifies `alknet-call` and `alknet-channels` from the
alknet mono-repo (`/workspace/@alkdev/alknet`). The source alknet mono-repo (`/workspace/@alkdev/alknet`). The source
architecture docs were ported from architecture docs were ported from
`/workspace/@alkdev/alknet/docs/architecture/` and renumbered as `/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 `alk/channels`) are wire-stable going forward (renamed from
`alknet/` to `alk/` in v0.1.1, before the first published consumer). `alknet/` to `alk/` in v0.1.1, before the first published consumer).
- Key ADRs that inform this crate's design: - Key ADRs that inform this crate's design:
+97
View File
@@ -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 based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
this crate adheres to [Semantic Versioning](https://semver.org/). 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<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 ## [0.2.0] - 2026-08-31
Consumer-findings remediation (CF-001..004 from 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-046), the channels protocol with openable-ALPNs-as-operations
(ADR-047), and the `ChannelClient` transport-agnostic client. (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.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.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 [0.1.0]: https://git.alk.dev/alkdev/alkcall/releases/tag/v0.1.0
Generated
+1 -1
View File
@@ -27,7 +27,7 @@ dependencies = [
[[package]] [[package]]
name = "alkcall" name = "alkcall"
version = "0.2.0" version = "0.3.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"bytes", "bytes",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "alkcall" name = "alkcall"
version = "0.2.0" version = "0.3.0"
edition = "2021" edition = "2021"
rust-version = "1.85" rust-version = "1.85"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
+30 -2
View File
@@ -21,7 +21,7 @@ use alkcall::registry::{
spec::{OperationSpec, OperationType, Visibility, AccessControl}, spec::{OperationSpec, OperationType, Visibility, AccessControl},
}; };
let mut registry = OperationRegistry::new(); let registry = OperationRegistry::new();
registry.register(HandlerRegistration::new( registry.register(HandlerRegistration::new(
OperationSpec::new( OperationSpec::new(
"echo/run", "echo/run",
@@ -98,6 +98,34 @@ for reg in registrations {
let response = conn.call("remote/status", serde_json::json!({})).await; 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(&registry)?;
let client = ChannelClient::from_connection_with_serving(
connection,
Some(ServingConfig {
registry: Arc::clone(&registry),
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 ## Architecture
alkcall is a pure protocol crate — no networking, no transport 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 | | Role | Call protocol | Channels protocol |
|------|---------------|-------------------| |------|---------------|-------------------|
| **Producer** | Registers ops on an `OperationRegistry`, runs a `Dispatcher` | Runs a `ChannelsAdapter`, registers openable ALPNs via `ChannelCore::register_openable` | | **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 | | **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 | | **Spoke / Worker** | Both: produces ops (its own services), consumes hub ops | Both: produces channels (TTY, tunnel), may consume hub channels |
+1 -1
View File
@@ -115,7 +115,7 @@ impl std::fmt::Debug for DispatchResult {
} }
/// The started half of a dispatched `call.requested` event, from /// 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 /// for the caller to spawn (review 005 G-01); `Sink` was started
/// inline (its `chunk_tx` must be registered before the next /// inline (its `chunk_tx` must be registered before the next
/// `call.published` frame is read) and carries the started handler /// `call.published` frame is read) and carries the started handler