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.
157 lines
5.4 KiB
Markdown
157 lines
5.4 KiB
Markdown
# alkcall
|
|
|
|
Call + channels RPC: structured JSON operations, streaming subscriptions,
|
|
service discovery, and N-channel multiplexing over one transport stream.
|
|
|
|
This crate unifies the call protocol and the channels protocol, plus the
|
|
vendored core types formerly in `alknet-core`. It is a pure protocol crate
|
|
— no networking, no transport dependencies. Downstream crates (alktty,
|
|
alktunnels, alktrader) compose on top of it.
|
|
|
|
## Quick start
|
|
|
|
### Producer — register an operation and run a dispatcher
|
|
|
|
```rust
|
|
use std::sync::Arc;
|
|
use alkcall::core::{Capabilities, IdentityProvider};
|
|
use alkcall::protocol::{CallAdapter, connection::CallConnection};
|
|
use alkcall::registry::{
|
|
registration::{HandlerRegistration, HandlerKind, OperationRegistry, make_handler},
|
|
spec::{OperationSpec, OperationType, Visibility, AccessControl},
|
|
};
|
|
|
|
let registry = OperationRegistry::new();
|
|
registry.register(HandlerRegistration::new(
|
|
OperationSpec::new(
|
|
"echo/run",
|
|
OperationType::Query,
|
|
Visibility::External,
|
|
serde_json::json!({}),
|
|
serde_json::json!({}),
|
|
vec![],
|
|
AccessControl::default(),
|
|
None,
|
|
),
|
|
HandlerKind::Once(make_handler(|input, ctx| async move {
|
|
alkcall::protocol::wire::ResponseEnvelope::ok(ctx.request_id, input)
|
|
})),
|
|
alkcall::registry::registration::OperationProvenance::Local,
|
|
None,
|
|
None,
|
|
Capabilities::new(),
|
|
)).unwrap();
|
|
|
|
let registry = Arc::new(registry);
|
|
let provider: Arc<dyn IdentityProvider> = /* your identity provider */;
|
|
|
|
let adapter = CallAdapter::new(registry, provider);
|
|
// adapter implements ProtocolHandler — call adapter.handle(connection, &auth).await
|
|
```
|
|
|
|
### Consumer — call an operation
|
|
|
|
```rust
|
|
use alkcall::core::Connection;
|
|
use alkcall::protocol::connection::CallConnection;
|
|
|
|
let connection = Connection::from_bidi(
|
|
transport_stream,
|
|
b"alk/call".to_vec(),
|
|
Some(remote_addr),
|
|
);
|
|
let conn = CallConnection::new(connection);
|
|
|
|
let response = conn.call("echo/run", serde_json::json!({"msg": "hello"})).await;
|
|
assert!(response.result.is_ok());
|
|
```
|
|
|
|
### Channels — open a channel and call through channel 0
|
|
|
|
```rust
|
|
use alkcall::channels::client::ChannelClient;
|
|
use alkcall::core::Connection;
|
|
|
|
let connection = Connection::from_bidi(
|
|
transport_stream,
|
|
b"alk/channels".to_vec(),
|
|
Some(remote_addr),
|
|
);
|
|
let client = ChannelClient::from_connection(connection).await?;
|
|
|
|
let response = client.call_open_op(
|
|
"echo/run",
|
|
serde_json::json!({"msg": "hello"}),
|
|
).await;
|
|
```
|
|
|
|
### from_call — discover and import remote operations
|
|
|
|
```rust
|
|
use alkcall::client::{from_call, FromCallConfig};
|
|
|
|
let registrations = from_call(&conn, FromCallConfig::new()).await?;
|
|
for reg in registrations {
|
|
conn.register_imported(reg);
|
|
}
|
|
// now call remote ops as if they were local
|
|
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
|
|
dependencies. It provides the call and channels protocols as a library.
|
|
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; 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 |
|
|
|
|
A single process can be a producer of some ops, a consumer of others, a
|
|
channel opener for TTY, and a channel acceptor for tunnels — all on the
|
|
same `alk/channels` connection.
|
|
|
|
## Documentation
|
|
|
|
- [Architecture docs](docs/architecture/README.md) — the authoritative
|
|
spec: ADRs, wire formats, protocol contracts, and composition patterns.
|
|
- [API docs](https://docs.rs/alkcall) — full crate documentation on docs.rs.
|
|
- [Open questions](docs/architecture/open-questions.md) — tracked
|
|
deferred decisions and feature gaps.
|
|
|
|
## License
|
|
|
|
MIT OR Apache-2.0
|