# 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 = /* 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, identity: None, // peer identity: transport `Connection::set_identity` propagates }), ).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