Verifies and fixes CF-005 (alktunnels reverse-flow POC W1): the connect-side serving path built channel 0 internally and never set an identity, so a scope-gated serving op could only be satisfied via the payload auth_token. Token is now the fallback (hub-forwarding / browser path); transport/key-based identity is the primary path. - ServingConfig gains identity: Option<Identity> — the explicit override (remediation a). Semver-relevant struct-literal change → 0.7.0 (minor bump at 0.x, wire surface unchanged). - from_connection_with_serving propagates the transport Connection::identity() to the channel-0 connection via set_identity before the serving loop starts (remediation b) — mirrors the accept side's install-hook set_identity; process-local, nothing new on the wire. - Dispatch identity precedence on the serving loop: payload auth_token → identity_provider, then ServingConfig.identity, then transport identity; identity-less dispatch still fails closed (FORBIDDEN). - Public core::auth::NoopIdentityProvider (resolves nothing; the ServingConfig::default() provider — three private test copies existed). - Regression gates: four cf005_* e2e tests (transport propagation, override precedence, identity-less denial, token fallback + precedence). - Ledger CF-005 → resolved; ADR-022 §connect-side-serving amended; README example updated; changelog 0.7.0. Verification: cargo test (629) + --all-features (646), clippy (all-targets, all-features, -D warnings), fmt --check, doc --no-deps, wasm32 check, semver-checks (no update required at 0.7.0), publish --dry-run.
158 lines
5.5 KiB
Markdown
158 lines
5.5 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,
|
|
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
|