glm-5.3-flash a1f257757b fix(channels): duplicate adopt/open must not destroy the live channel; fuzz targets 3-5
Found by the manager_routing fuzz target (docs/research/fuzzing.md \u00a77.8):

- ChannelManager::open_channel / adopt_channel used
  HashMap::insert(...).is_some() as a collision check \u2014 insert REPLACES
  the existing entry, so a duplicate adopt/open installed the new state,
  dropped the live channel's demux_sender (spurious EOF to its readers,
  subsequently routed chunks lost) and still returned
  Err(ChannelExists). Fixed with contains_key pre-check; map untouched
  on collision. Regression tests: adopt_channel_duplicate_id_leaves_
  live_channel_intact, open_channel_duplicate_id_leaves_live_channel_
  intact.

New fuzz targets (\u00a77.4 step 3):
- manager_routing: Arbitrary op sequences over ChannelManager; exact
  counter models (parked/dropped must equal the manager's monotonic
  counters), parked-bytes bound per \u00a76.2-1, clear_all ledger-vs-map
  semantics, drainer-byte reconciliation (lossless routing)
- envelope_semantic: constructors -> serde -> write_frame/read_frame
  structural round-trip; event-type constants; call.error parse-back
- spec_parse: OpRegisterRequest::from_json -> rebuild -> registry
  registration (attacker schemas compile at register, CF-003)
- fuzz/shared/src/arbitrary_value.rs: bounded Arbitrary for
  serde_json::Value; 20 spec_parse + 4 manager_routing seeds
- corpus replay for the new targets in fuzz/shared tests

Verification: cargo test 684 passed (682 + 2 regression); clippy
-D warnings clean (main + fuzz/shared); fmt clean (main + fuzz);
cargo fuzz build clean; 20 s smoke on all three new targets clean
(manager_routing 79k, envelope_semantic 141k, spec_parse 517k runs);
crash input replays clean post-fix
2026-09-27 23:19:18 +00:00
2026-08-11 08:48:35 +00:00
2026-08-11 08:48:35 +00:00

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(&registry)?;

let client = ChannelClient::from_connection_with_serving(
    connection,
    Some(ServingConfig {
        registry: Arc::clone(&registry),
        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

S
Description
No description provided
Readme
1.5 MiB
0 Stars 6 Watchers 0 Forks
Languages
Rust 99.3%
Python 0.6%