The ported docs still framed alknet siblings (alknet-core/-call/-channels/
-http/-tls/-client) as live load-bearing context of this crate. With the
extraction complete and alknet headed for glue-level replacement, the docs
now speak as alkcall and cite the real sibling crates (alkhttp, alktls,
alktty, alktunnels, alktrader).
- ADR-054 (new): re-own the verifier-selection rule in-crate — three
remote roles (public X.509 endpoint / transport relay / hub),
ServerCertVerifier by PeerEntry presence, RemoteIdentity None
load-bearing. Provenance: alknet ADR-034 (never ported), demoted to
footnote. Resolves OQ-21's cross-repo authority gap.
- OQ-43 (new, deferred(scope)): composition-layer ownership and shape
after the alknet replacement — the named receiver for the
downstream-side deferrals (OQ-33 endpoint layer, OQ-41 QUIC-native
substrate, OQ-42 hub broker, OQ-19 persistence adapters, ADR-045 dial
seam). README layering diagram redrawn with the composition layer as
TBD-ownership instead of "alknet / alknode" in concrete.
- Spec docs reframed: titles, self-references, dial/TLS/from_jsonschema
homes, malformed legacy cite rows removed; dead relative research
paths qualified as source-repo paths.
- ADR amendments (append-only): 027 (alknet-http -> alkhttp), 031 (post-
extraction state), 044 (split landed as modules in alkcall), 045
(name normalization; citation-space note).
- Citation hygiene: unprefixed/colliding source-repo cites prefixed
("alknet ADR-NNN (provenance)") across 009/011/022/034/035/038/040/
043/044/045; stale OQ cites corrected (session registries OQ-04, batch
semantics OQ-03, verifier rule ADR-054); stale anchors fixed.
- README provenance note consolidated; citation conventions documented.
Verification: markdown link check (all local links resolve, all in-crate
ADR cites point at existing decisions/ files); table integrity check
(uniform row widths across open-questions.md and edited tables); residual
alknet-mention sweep (only provenance/historical/intentional framing
remains); architecture review subagent pass with zero remaining criticals.
Docs-only change — no code paths touched.
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
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
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
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
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:
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 CallConnections to spokes for ops it consumes |
Both: runs a ChannelsAdapter for inbound connections, holds ChannelClients 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 — the authoritative spec: ADRs, wire formats, protocol contracts, and composition patterns.
- API docs — full crate documentation on docs.rs.
- Open questions — tracked deferred decisions and feature gaps.
License
MIT OR Apache-2.0