diff --git a/AGENTS.md b/AGENTS.md index 1926d69..092f2f4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,8 +32,8 @@ Exceptions — **do not** commit or push without asking: - The user is actively reviewing the diff and may ask for changes - The change touches published wire formats or semver-relevant public API (this crate will be on crates.io; the `EventEnvelope` shape and the - channels 8-byte chunk header are wire-format-stable — see ADR-064 and - ADR-071 in the alknet source docs, to be renumbered as alkcall ADRs) + channels 8-byte chunk header are wire-format-stable — see ADR-014 and + ADR-034) - You'd be force-pushing, amending a published commit, creating an empty commit, or skipping hooks @@ -84,8 +84,7 @@ session, not just spawned implementation agents. `OperationContext.metadata`. Outbound credentials flow through `Capabilities` injected at the assembly layer → `HandlerRegistration.capabilities` → `OperationContext.capabilities` - → handler. See the no-env-vars invariant below and ADR-014 (alknet - source). + → handler. See the no-env-vars invariant below and ADR-010. 5. **No-env-vars invariant** — no handler reads outbound credentials from any source other than `OperationContext.capabilities`. The @@ -100,8 +99,7 @@ session, not just spawned implementation agents. peer-keyed composition). Making `OperationEnv` concrete or hardcoding the global registry into the dispatch path would close the session-overlay and connection-overlay patterns. This is the same - integration-point pattern as `IdentityProvider`. See ADR-024, ADR-029 - (alknet source). + integration-point pattern as `IdentityProvider`. See ADR-019, ADR-024 7. **Wire formats are stable** — two wire formats live in this crate: - **`EventEnvelope`** (`{ type, id, payload }` with length-prefixed @@ -110,13 +108,12 @@ session, not just spawned implementation agents. and the five event types (`call.requested`, `call.responded`, `call.completed`, `call.aborted`, `call.error`) are stable. New event types may be added; existing ones must not change shape. See - ADR-064 (alknet source). + ADR-014. - **Channels 8-byte chunk header** (`[channel_id:u32 BE][length:u32 BE][payload]`) — the channels wire format. This is a one-way door: changing the header format breaks all peers. The channels layer has no `stream_type` concept — the handler owns its sub-stream - multiplexing on the `BiStream` it receives. See ADR-071, ADR-093 - (alknet source). + multiplexing on the `BiStream` it receives. See ADR-034, ADR-035 8. **Producer/consumer, not server/client** — both sides of a call or channels connection can initiate. A producer exposes operations @@ -125,8 +122,8 @@ session, not just spawned implementation agents. direction (who opened it) is independent of call/channel direction (who calls/opens). Avoid "server" and "client" framing in docs and API names; use "producer" and "consumer," or "accept side" / "connect - side" for the connection-establishment half specifically. See ADR-017, - ADR-073 §direction semantics (alknet source). + side" for the connection-establishment half specifically. See ADR-022, + ADR-037 §direction semantics. 9. **Vendored core types** — the types formerly in `alknet-core` (`Connection`, `ProtocolHandler`, `BiStream`, `BidiStreamSource`, @@ -137,7 +134,7 @@ session, not just spawned implementation agents. is reworked, it will consume alkcall's versions. Keep these types lean (no TLS, no transport coupling, no endpoint/accept-loop); the dial and the TLS config are concerns of the consumer, not of this - crate. See ADR-065, ADR-070, ADR-092 (alknet source). + crate. See ADR-007, ADR-008, ADR-009. 10. **`alktype` dependency** — use `alktype` for binary layout (the channels chunk header, future binary payload schemas) and JSON @@ -173,7 +170,7 @@ session, not just spawned implementation agents. opt-in for long-running work. The abort policy is set on `OperationContext` and propagated through `OperationEnv::invoke()` — the composing handler decides the child's policy, not the wire - caller. See ADR-016 (alknet source). + caller. See ADR-020. 15. **Peer authorization via `AccessControl`** — a remote peer's call is authorized by `AccessControl::check(peer_identity)` against the op's @@ -182,7 +179,7 @@ session, not just spawned implementation agents. `AccessControl::default()` is callable by any peer; an op with `required_scopes` is callable only by peers whose `Identity.scopes` satisfy them; an op with `Visibility::Internal` is never callable - from the wire. See ADR-029 (alknet source). + from the wire. See ADR-024. ## Verification Commands @@ -202,79 +199,76 @@ If feature flags are added, also run `cargo test --all-features` and ## Architecture Context - `docs/architecture/` — the authoritative spec. Read it before - non-trivial changes. ADRs are numbered; OQs (open questions) track - resolved/deferred decisions. + non-trivial changes. ADRs are numbered 001..047; OQs (open questions) + track resolved/deferred decisions. - This crate unifies `alknet-call` and `alknet-channels` from the alknet mono-repo (`/workspace/@alkdev/alknet`). The source - architecture docs are at - `/workspace/@alkdev/alknet/docs/architecture/crates/call/` and - `/workspace/@alkdev/alknet/docs/architecture/crates/channels/`. They - will be ported into `docs/architecture/` here and renumbered as - alkcall ADRs (starting at ADR-001). -- The source ADRs are in - `/workspace/@alkdev/alknet/docs/architecture/decisions/`. Key ADRs - that inform this crate's design: + architecture docs were ported from + `/workspace/@alkdev/alknet/docs/architecture/` and renumbered as + alkcall ADRs (001..047). The ALPN strings (`alknet/call`, + `alknet/channels`) are wire-stable and unchanged. +- Key ADRs that inform this crate's design: **Call protocol:** - - ADR-064 — hand-rolled `EventEnvelope` framing (irpc never - integrated; supersedes ADR-005) - - ADR-012 — call protocol stream model (bidirectional streams, + - ADR-014 — hand-rolled `EventEnvelope` framing (irpc never + integrated; supersedes ADR-013) + - ADR-015 — call protocol stream model (bidirectional streams, ID-based correlation) - - ADR-017 — call protocol client and adapter contract (`CallClient` + - ADR-022 — call protocol client and adapter contract (`CallClient` `spawn_dispatch` transport-agnostic; `from_call` imports remote ops; connection direction independent of call direction) - - ADR-024 — operation registry layering (curated + session + + - ADR-019 — operation registry layering (curated + session + connection overlays; `OperationEnv` as trait-object integration point) - - ADR-029 — peer-graph routing model (peer-keyed overlays + + - ADR-024 — peer-graph routing model (peer-keyed overlays + `PeerRef` routing; `AccessControl`-based peer authorization) - - ADR-014 — secret material flow and capability injection (no secret + - ADR-010 — secret material flow and capability injection (no secret material on the wire; capabilities injected at assembly layer) - - ADR-015 — privilege model and authority context (`internal` = + - ADR-017 — privilege model and authority context (`internal` = authority switch not ACL skip; External/Internal visibility) - - ADR-016 — abort cascade for nested calls (default + - ADR-020 — abort cascade for nested calls (default `abort-dependents`, `continue-running` opt-in) - - ADR-022 — handler registration, provenance, and composition + - ADR-018 — handler registration, provenance, and composition authority - - ADR-023 — operation error schemas (typed `details` in `call.error`) - - ADR-049 — streaming handler for subscriptions + - ADR-016 — operation error schemas (typed `details` in `call.error`) + - ADR-021 — streaming handler for subscriptions (`StreamingHandler` type, `invoke_streaming()` dispatch path) - - ADR-032 — forwarded-for identity (metadata only, never used by + - ADR-026 — forwarded-for identity (metadata only, never used by `AccessControl::check`) **Channels:** - - ADR-071 — channels wire format (8-byte chunk header; one-way door) - - ADR-093 — channels pure channel multiplexing (no `stream_type`, + - ADR-034 — channels wire format (8-byte chunk header; one-way door) + - ADR-035 — channels pure channel multiplexing (no `stream_type`, `BiStream`-only, handler owns sub-stream multiplexing) - - ADR-072 — channel 0 pre-negotiated as `alknet/call` - - ADR-073 — channel lifecycle operations (`channel/open`/`close`/ + - ADR-036 — channel 0 pre-negotiated as `alknet/call` + - ADR-037 — channel lifecycle operations (`channel/open`/`close`/ `control`/`resources/subscribe` on channel 0's call registry) - - ADR-075 — `ChannelsAdapter` and `ChannelManager` (substrate-agnostic + - ADR-039 — `ChannelsAdapter` and `ChannelManager` (substrate-agnostic demux loop; `ChannelManager` is ALPN-blind, auth-blind, transport-blind) - - ADR-076 — backpressure, channel limits, ID reuse (bounded-buffer, + - ADR-040 — backpressure, channel limits, ID reuse (bounded-buffer, 256-channel per-connection memory bound, monotonic IDs) - - ADR-079 — hub relay (translate channel 0, byte-forward data + - ADR-042 — hub relay (translate channel 0, byte-forward data channels with ID rewrite) - - ADR-080 — `ChannelClient` (transport-agnostic + - ADR-043 — `ChannelClient` (transport-agnostic `from_connection` primary; dial lives in the consumer) - - ADR-094 — per-identity channel cap (256 per `PeerId` via + - ADR-041 — per-identity channel cap (256 per `PeerId` via `ChannelLifecyclePolicy`) **Shared (vendored core types):** - ADR-001 — ALPN-based protocol dispatch - ADR-002 — `ProtocolHandler` trait - - ADR-004 — auth as shared core (`IdentityProvider` in core, + - ADR-003 — auth as shared core (`IdentityProvider` in core, handlers extract credentials) - - ADR-006 — ALPN string convention (`alknet/` prefix, one ALPN per + - ADR-004 — ALPN string convention (`alknet/` prefix, one ALPN per connection) - - ADR-007 — `BiStream` type definition (handlers receive + - ADR-005 — `BiStream` type definition (handlers receive `Connection`, not `BiStream`) - - ADR-065 — `Connection::from_stream` (generic single-stream + - ADR-007 — `Connection::from_stream` (generic single-stream connections — unblocks TCP+TLS, SSH, WebTransport, wasm) - - ADR-070 — `BidiStreamSource` trait (the `Connection` extension + - ADR-008 — `BidiStreamSource` trait (the `Connection` extension point `ChannelBidiStreamSource` implements) - - ADR-092 — `BiStream` as the handler leaf (`accept_bi` returns + - ADR-009 — `BiStream` as the handler leaf (`accept_bi` returns `BiStream`) - If a TODO references a "Phase B" or a design direction that an ADR diff --git a/Cargo.toml b/Cargo.toml index 86e5629..be66ffb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" rust-version = "1.85" license = "MIT OR Apache-2.0" description = "Call + channels RPC: structured JSON operations, streaming subscriptions, service discovery, and N-channel multiplexing over one transport stream" +readme = "README.md" repository = "https://git.alk.dev/alkdev/alkcall" keywords = ["rpc", "json-rpc", "multiplexing", "wire-format", "alpn"] categories = ["network-programming", "asynchronous", "encoding"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..e6d45d2 --- /dev/null +++ b/README.md @@ -0,0 +1,128 @@ +# 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 mut 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"alknet/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"alknet/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; +``` + +## 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 | 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 `alknet/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