OQ-26 (resolved): AdapterError variants decided — DiscoveryFailed, SchemaParse, Transport, Unauthorized, SamePeerCollision (replaces flat Conflict per ADR-029 §5). #[non_exhaustive] for downstream extension. Two-way door; the initial set is the code's return type. OQ-33 (resolved): PeerId is a logical identifier, NOT Identity.id. The research's v1 default (PeerId = fingerprint) is overridden: coupling PeerId to crypto material breaks every in-flight PeerRef::Specific and every ACL entry on key rotation. v1 source is a connection-assigned UUID — a no-storage workaround that works for the immediate use case (head→workers, reconnect produces fresh PeerRef, in-flight gets NOT_FOUND which is correct). The one-way door: PeerId is logical, not crypto — this determines PeerCompositeEnv key type and PeerRef::Specific payload. The id source (UUID vs configured name vs peer registry) is the two-way-door remainder. OQ-34 (new): the storage dimension OQ-33 surfaced. The core crates are deliberately DB-free (smaller, fewer deps, simpler testing) — this served local-only state (vault, registry) well, but peer identity is the first cross-node state that wants persistence. The real solution (a persistent peer registry mapping stable logical name → current crypto material, surviving key rotation) is not a v1 blocker (UUID works), but tracked so the no-DB posture's limit is deliberate, not accidental. The storage boundary (core gets a PeerRegistry trait vs stays storage-free) is the one-way door; the backend choice is two-way. Key-rotation/ACL note: decoupling PeerId from crypto keeps the door open for ACL entries that persist across key rotation — when the peer registry is built, ACLs key on the logical name and key rotation becomes vault-only with no remote-side ACL update.
status, last_updated, review
| status | last_updated | review |
|---|---|---|
| draft | 2026-06-26 | 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-017 client/adapter surface) addressed 2026-06-26 — ADR-028 + client-and-adapters.md added; implementation pending. |
alknet-call
Structured RPC over QUIC: operations, request/response, streaming subscriptions, and service discovery. Implements ProtocolHandler on ALPN alknet/call.
Documents
| Document | Status | Description |
|---|---|---|
| call-protocol.md | draft | CallAdapter, EventEnvelope framing, stream model, PendingRequestMap, bidirectional calls |
| operation-registry.md | draft | OperationSpec, Handler, OperationRegistry, AccessControl, service discovery, irpc integration |
| client-and-adapters.md | draft | CallClient (outbound connection opener), from_call / from_jsonschema, OperationAdapter trait, adapter location map, no-env-vars invariant, exchange-of-operations pattern |
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 and irpc |
| 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 | irpc as Call Protocol Foundation | irpc provides framing and service 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 |
| 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 |
Relevant Open Questions
| OQ | Title | Status | Relevance |
|---|---|---|---|
| OQ-07 | Call protocol scope within a connection | resolved (ADR-012) | 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-014) | 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-024 to cover connection-scoped overlays. |
| OQ-25 | dissolved (ADR-029) | 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 | open (two-way) | v1 default: auto-on-reconnect; explicit refresh() additive |
| OQ-28 | from_call namespace collision | cross-peer dissolved (ADR-029) / same-peer stays | Cross-peer: separate sub-overlays, no collision. Same-peer: error. namespace_prefix is local-naming sugar |
| OQ-29 | CallClient TLS client-auth and remote-identity verification | open (two-way) | v1 with_no_client_auth() + AcceptAnyServerCertVerifier; wiring RawKey client-auth is additive (orthogonal to ADR-029) |
| OQ-30 | PeerRef::Any routing policy |
open (two-way) | v1 insertion-order first-match; round-robin/least-loaded is future (ADR-029) |
| OQ-31 | services/list-peers re-export semantics |
open (two-way) | v1 "own ops only"; services/list-peers is opt-in (ADR-029) |
| OQ-32 | Multi-hop federation | open | v1 one-hop; peer-keyed model extends without redesign; petgraph candidate (ADR-029) |
| OQ-33 | PeerId — crypto identity vs stable logical id | resolved | Logical id (UUID v1), not Identity.id; decoupled from crypto for key-rotation-safe ACLs |
| OQ-34 | Persistent peer registry (cross-node state storage) | open | Not a v1 blocker (UUID works); the no-DB posture's limit, tracked for deliberate future decision |
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-024. - irpc is one dispatch backend: Local operations dispatch directly. irpc service calls (in-process, type-safe) are internal. The call protocol is the external interface.
- 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-014. - Abort cascades to descendants:
call.abortedfor a parent request cascades to all non-terminal descendants. Defaultabort-dependents;continue-runningopt-in. See ADR-016. - 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-015, ADR-022. - Provenance determines composition capability: Only
LocalandSessionops can compose. Leaves (FromOpenAPI,FromMCP,FromCall) 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-022. - Connection direction is independent of call direction: Who opens the QUIC connection is a connection-layer concern, not a protocol-layer concern. Both sides can call each other once connected. The
CallAdapteraccepts connections; theCallClientopens them; both produce the sameCallConnectionand dispatch through the same loop. See ADR-017, client-and-adapters.md. - CallClient registry is default-deny: A
CallClientexposes no operations to the remote peer unless explicitly marked remote-safe. Sharing the global registry is an explicit trusted-peer opt-in, never the default. This prevents a remote peer's call from triggering dispatch that populatesOperationContext.capabilitiesfrom the local node's registration bundle. See ADR-028. - Adapter trait lives with the types; implementations live with their transport:
OperationAdapteris inalknet-call;from_call/from_jsonschemaare inalknet-call(QUIC / pure parse);from_openapi/from_mcp/to_openapi/to_mcpare inalknet-http(reqwest / axum).alknet-callstays lean — no HTTP client, no HTTP server. 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-014, client-and-adapters.md.