Port the call + channels architecture documentation from the alknet mono-repo into docs/architecture/, renumbered as alkcall ADR-001..045. Renumbering map (alknet -> alkcall): Core: 001,002,004,006,007,011,065,070,092,014,050,091 -> 001-012 Call: 005,064,012,023,015,022,024,016,049,017,028,029,030,032,066,069,067,068 -> 013-030 Shared: 003,009,013 -> 031-033 Channels: 071,093,072,073,074,075,076,094,079,080,081,089 -> 034-045 3 superseded/reversed ADRs kept for historical trail: - ADR-013 (irpc foundation, superseded by ADR-014) - ADR-023 (peer-scoped filtering, superseded by ADR-024) - ADR-077 (TTY inside channels, reversed by ADR-035 — not ported, TTY-only) Ported docs (11 spec files + README + open-questions): - call-README.md, call-protocol.md, operation-registry.md, client-and-adapters.md - channels-README.md, channels-overview.md, channels-wire.md, channels-connection.md, channels-adapter.md, channel-operations.md, channel-client.md - README.md (index with doc table, ADR table grouped by category, key principles) - open-questions.md (lean — 30 OQs, renumbered OQ-01..030; includes new OQ-22 for the pub/sub gap) Cross-reference rewriting: - All ADR-NNN references rewritten single-pass (no chaining bug) - Markdown link paths fixed - Title lines aligned with filenames - Non-ported ADR refs (052, 082, 086, etc.) left as-is with README note The open-questions.md includes OQ-22 (new): the call protocol pub/sub gap — subscribe exists but pub does not, needed for channels channel/resources/subscribe fan-out. This is the next ADR to write (alkcall ADR-046).
14 KiB
14 KiB
status: draft
last_updated: 2026-07-17
review: call/review-call passed 2026-06-23 — registry, protocol, ADR (005/012/014/015/016/017/022/023/024), security, and pattern-consistency checks all conformant; 159 unit/integration tests green;
cargo build, cargo clippy -- -D warnings, cargo fmt --check, cargo test clean. Call-completion gap (ADR-022 client/adapter surface) addressed 2026-06-26; ADR-024 migration landed. Transport generalization sweep (ADR-014 supersedes ADR-013; ADR-007 from_stream) synced 2026-07-09. Crate-extraction sweep (phases 0–5) landed 2026-07-17: ConnectionCredentials/RemoteIdentity in alknet-core (ADR-012); TLS helpers in alknet-tls (ADR-045 §5); dial in alknet-client (ADR-045); alknet-call is a pure protocol crate with no TLS/transport deps.
alknet-call
Structured RPC: operations, request/response, streaming subscriptions, and service discovery. Implements ProtocolHandler on ALPN alknet/call. Runs over QUIC (quinn/iroh) and, via Connection::from_stream (ADR-007), over any AsyncRead + AsyncWrite transport. A pure protocol crate — no TLS or transport deps (the dial is in alknet-client, the TLS config is in alknet-tls).
Documents
| Document | Status | Description |
|---|---|---|
| call-protocol.md | draft | CallAdapter, hand-rolled EventEnvelope framing (no irpc — ADR-014), stream model, PendingRequestMap, bidirectional calls |
| operation-registry.md | draft | OperationSpec, Handler, OperationRegistry, AccessControl, service discovery, hand-rolled framing (no irpc — ADR-014) |
| client-and-adapters.md | draft | CallClient (transport-agnostic spawn_dispatch primary; dial lives in AlknetClient per ADR-045), from_call, OperationAdapter trait, adapter location map, no-env-vars invariant, exchange-of-operations pattern (from_jsonschema in alknet-http per ADR-027) |
Applicable ADRs
| ADR | Title | Relevance |
|---|---|---|
| 001 | ALPN-Based Protocol Dispatch | CallAdapter registers on ALPN alknet/call |
| 002 | ProtocolHandler Trait | CallAdapter implements ProtocolHandler |
| 003 | Crate Decomposition | alknet-call depends on alknet-core (no irpc — ADR-014) |
| 013 | Rust as Canonical Implementation Language | Adapter traits defined in Rust; TS is reference/browser adaptation |
| 004 | Auth as Shared Core | AuthContext passed to call handlers |
| 005 | ||
| 064 | Hand-Rolled EventEnvelope Framing | Wire format, registry, dispatch are hand-rolled in alknet-call; supersedes ADR-013 |
| 065 | Connection::from_stream |
Generic single-stream connections; unblocks TCP+TLS/SSH/WT/wasm dispatch |
| 006 | ALPN String Convention | alknet/call ALPN, one ALPN per connection |
| 007 | BiStream Type Definition | CallAdapter receives Connection, not BiStream |
| 008 | Vault Integration Point | Vault accessed at assembly layer, not on the wire |
| 010 | ALPN Router and Endpoint | Static handler registration |
| 012 | Call Protocol Stream Model | Bidirectional streams, EventEnvelope, ID-based correlation |
| 014 | Secret Material Flow and Capability Injection | Call protocol carries no secret material; capabilities injected at assembly layer |
| 015 | Privilege Model and Authority Context | internal = authority switch not ACL skip; External/Internal visibility; handler identity + scoped env |
| 016 | Abort Cascade for Nested Calls | call.aborted cascades to descendants; default abort-dependents, continue-running opt-in |
| 017 | Call Protocol Client and Adapter Contract | CallClient opens connections; from_call imports remote ops; connection direction independent of call direction |
| 066 | from_jsonschema as HTTP-Backed Single-Endpoint Adapter in alknet-http |
Moved from_jsonschema from alknet-call (broken schema-only placeholder) to alknet-http as a real reqwest-backed single-endpoint adapter; FromJsonSchema provenance stays in alknet-call as a leaf |
| 022 | Handler Registration, Provenance, and Composition Authority | Registration bundle carries provenance, composition authority, scoped env, capabilities |
| 023 | Operation Error Schemas | Operations declare domain errors; call.error carries typed details; adapter fidelity |
| 024 | Operation Registry Layering | Curated (static) + session/connection overlays (dynamic); OperationEnv as trait-object integration point; OperationContext.env split into scoped_env (data) and env (dispatch trait) |
| 028 | AccessControl) |
|
| 029 | Peer-Graph Routing Model | Peer-keyed overlays + PeerRef routing; AccessControl-based peer authorization; retires remote_safe/trusted_peer |
| 030 | PeerEntry and Identity.id Decoupling | PeerId source = Identity.id = PeerEntry.peer_id (stable); supersedes ADR-024's UUID source |
| 032 | Forwarded-For Identity | forwarded_for on OperationContext and call.requested; metadata only, never used by AccessControl::check |
| 033 | Storage Boundary and Repo/Adapter Pattern | Core defines repo traits + in-memory defaults; persistence adapters are separate crates |
| 089 | AlknetClient — Native Client Dial Seam | The dial is in alknet-client; CallClient is spawn_dispatch only; alknet-call is a pure protocol crate with no TLS/transport deps |
| 091 | ConnectionCredentials — Decouple Dial from Call Protocol |
ConnectionCredentials/RemoteIdentity in alknet-core (not alknet-call); auth_token is a per-request payload field |
Relevant Open Questions
| OQ | Title | Status | Relevance |
|---|---|---|---|
| OQ-07 | Call protocol scope within a connection | resolved (ADR-015) | Stream model, multiplexing, scope |
| OQ-13 | Operation path format and routing scope | resolved | /{service}/{op} is the correct design; remote dispatch is a separate layer |
| OQ-14 | Batch operation semantics | resolved | Correlated call.requested events is the correct protocol design |
| OQ-16 | Safe vault operations for call protocol exposure | resolved (ADR-010) | None exposed for now |
| OQ-19 | Session-scoped operation registries | resolved | Agent-written operations overlaid on curated registry via OperationEnv trait layering. Protocol doesn't need changes; OperationEnv must remain a trait. Generalized by ADR-019 to cover connection-scoped overlays. |
| OQ-25 | dissolved (ADR-024) | remote_safe/trusted_peer retired; peer authorization is AccessControl::check(peer_identity) |
|
| OQ-26 | OperationAdapter error type (AdapterError variants) | resolved | DiscoveryFailed, SchemaParse, Transport, Unauthorized, SamePeerCollision; #[non_exhaustive] |
| OQ-27 | from_call re-import trigger | resolved | from_call is a manual free function; the assembly layer calls it after the dial (in AlknetClient). refresh() is a genuine feature addition. See ADR-028. |
| OQ-28 | from_call namespace collision | resolved | Same-peer collision = error; cross-peer dissolved by ADR-024 (separate sub-overlays) |
| OQ-29 | CallClient TLS client-auth | resolved | Wire quinn client-auth; key-type-aware server cert verification; fingerprint normalization |
| OQ-30 | PeerRef::Any routing policy |
resolved | Insertion-order first-match; richer routing is a feature extension |
| OQ-31 | services/list-peers re-export semantics |
resolved | Opt-in services/list-peers; services/list is "own ops only" |
| OQ-32 | Multi-hop federation | open (feature extension) | One-hop model is the commitment; multi-hop is a feature extension, not a deferral |
| OQ-33 | PeerId — crypto identity vs stable logical id | resolved (ADR-025) | PeerId = Identity.id = PeerEntry.peer_id (stable across key rotation) |
| OQ-34 | Persistent peer registry | resolved (ADR-025+033) | Core trait + in-memory default; persistence adapters are separate crates |
| OQ-35 | dissolved | PeerEntry supports multiple credential paths; ApiKeyEntry is for tokens that ARE the identity |
|
| OQ-37 | X.509 outgoing-only case | resolved (ADR-034) | Three remote roles (public X.509 endpoint, transport relay, hub); PeerEntry asymmetry correct; verifier by PeerEntry presence |
Key Design Principles
- One connection, full access: An
alknet/callconnection gives access to the entire operation registry — calls, subscriptions, batch, schema. - Protocol is symmetric: Both sides can initiate calls. The server calling a client uses the same EventEnvelope format and correlation.
- Stream-agnostic correlation: PendingRequestMap correlates by request ID, not by stream. The protocol works with any stream arrangement.
- Operation registry is layered: The curated layer (
Localprovenance) is static — registered at startup by the CLI binary, immutable for the process lifetime. Session (Session) and imported (FromCalletc.) ops are dynamic overlays at their respective scopes (per-session, per-connection). The registry supports JSON Schema discovery. See ADR-019. - Hand-rolled dispatch (no irpc): Operations dispatch through the hand-rolled
OperationRegistry(ADR-014). The call protocol is the external interface; internal handler dispatch usesHandler/StreamingHandlertrait objects (ADR-021), not an irpc service. - Local dispatch only: The operation registry dispatches to local handlers. Remote dispatch (federation, head/worker routing) would be a separate mechanism at a different layer, not a modification to alknet-call's path format.
- No secret material on the wire: The call protocol carries no private keys, API keys, mnemonics, or decrypted credentials. Handlers receive outbound credentials through
OperationContext.capabilities, injected at the assembly layer. See ADR-010. - Abort cascades to descendants:
call.abortedfor a parent request cascades to all non-terminal descendants. Defaultabort-dependents;continue-runningopt-in. See ADR-020. - Internal calls switch authority context, not skip ACL: The
internalflag marks composition-originated calls. ACL runs against the handler's composition authority, not the caller's and not as a blanket skip. Operations have External/Internal visibility. Scoped composition env bounds reachability. See ADR-017, ADR-018. - Provenance determines composition capability: Only
LocalandSessionops can compose. Leaves (FromOpenAPI,FromMCP,FromCall,FromJsonSchema) are forwarding stubs — they don't get composition authority or a scoped env. The assembly layer is the sole grantor of composition authority. See ADR-018. (FromJsonSchemais now a real HTTP-forwarding leaf per ADR-027, not a schema-only placeholder.) - Connection direction is independent of call direction: Who opens the connection is a connection-layer concern, not a protocol-layer concern. Both sides can call each other once connected. The
CallAdapteraccepts connections; theCallClienttakes them over (spawn_dispatchprimary; dial inAlknetClientper ADR-045); both produce the sameCallConnectionand dispatch through the same loop. See ADR-022, client-and-adapters.md. - Peer authorization via
AccessControl: A remote peer's call is authorized byAccessControl::check(peer_identity)against the op'sAccessControl— the same mechanism that gates every other call. Noremote_safeflag, notrusted_peerbypass. An op withAccessControl::default()is callable by any peer; an op withrequired_scopesis callable only by peers whoseIdentity.scopessatisfy them; an op withVisibility::Internalis never callable from the wire. See ADR-024. - Adapter trait lives with the types; implementations live with their transport:
OperationAdapteris inalknet-call;from_callis inalknet-call(QUIC);from_jsonschema/from_openapi/from_mcp/to_openapi/to_mcpare inalknet-http(reqwest / axum).alknet-callstays lean — no HTTP client, no HTTP server. (from_jsonschemawas originally inalknet-callas a schema-only placeholder; ADR-027 moved it toalknet-httpas a real HTTP-backed adapter.) See client-and-adapters.md. - No handler reads outbound credentials from any source other than
OperationContext.capabilities(no-env-vars invariant): the credential injection path is vault → assembly layer →Capabilities→HandlerRegistration.capabilities→OperationContext.capabilities→ handler. Downstream consumers'std::env::varreads are unreachable because the assembly layer never callsDefault::default(). See ADR-010, client-and-adapters.md.