docs: re-key architecture docs off the alknet mono-repo — speak as alkcall

The ported docs still framed alknet siblings (alknet-core/-call/-channels/
-http/-tls/-client) as live load-bearing context of this crate. With the
extraction complete and alknet headed for glue-level replacement, the docs
now speak as alkcall and cite the real sibling crates (alkhttp, alktls,
alktty, alktunnels, alktrader).

- ADR-054 (new): re-own the verifier-selection rule in-crate — three
  remote roles (public X.509 endpoint / transport relay / hub),
  ServerCertVerifier by PeerEntry presence, RemoteIdentity None
  load-bearing. Provenance: alknet ADR-034 (never ported), demoted to
  footnote. Resolves OQ-21's cross-repo authority gap.
- OQ-43 (new, deferred(scope)): composition-layer ownership and shape
  after the alknet replacement — the named receiver for the
  downstream-side deferrals (OQ-33 endpoint layer, OQ-41 QUIC-native
  substrate, OQ-42 hub broker, OQ-19 persistence adapters, ADR-045 dial
  seam). README layering diagram redrawn with the composition layer as
  TBD-ownership instead of "alknet / alknode" in concrete.
- Spec docs reframed: titles, self-references, dial/TLS/from_jsonschema
  homes, malformed legacy cite rows removed; dead relative research
  paths qualified as source-repo paths.
- ADR amendments (append-only): 027 (alknet-http -> alkhttp), 031 (post-
  extraction state), 044 (split landed as modules in alkcall), 045
  (name normalization; citation-space note).
- Citation hygiene: unprefixed/colliding source-repo cites prefixed
  ("alknet ADR-NNN (provenance)") across 009/011/022/034/035/038/040/
  043/044/045; stale OQ cites corrected (session registries OQ-04, batch
  semantics OQ-03, verifier rule ADR-054); stale anchors fixed.
- README provenance note consolidated; citation conventions documented.

Verification: markdown link check (all local links resolve, all in-crate
ADR cites point at existing decisions/ files); table integrity check
(uniform row widths across open-questions.md and edited tables); residual
alknet-mention sweep (only provenance/historical/intentional framing
remains); architecture review subagent pass with zero remaining criticals.
Docs-only change — no code paths touched.
This commit is contained in:
glm-5.3-flash committed 2026-10-13 10:53:47 +00:00
1 parent 7b8a3e7558
commit 5f019c02f1
26 files changed
+670 -383

No files matched your search

+30 -26
View File
@@ -1,11 +1,11 @@
---
status: draft
last_updated: 2026-07-09
last_updated: 2026-10-13 (alkcall reframing)
---
# Call Protocol
The wire protocol, stream model, framing, and adapter that alknet-call implements on ALPN `alk/call`.
The wire protocol, stream model, framing, and adapter that alkcall implements on ALPN `alk/call`.
## What
@@ -15,7 +15,7 @@ The `CallAdapter` implements `ProtocolHandler` for ALPN `alk/call`. It receives
## Why
The call protocol is the primary programmatic interface to an alknet node. While SSH provides interactive shell access and HTTP provides REST APIs, the call protocol provides structured, discoverable RPC — the same interface that NAPI clients, MCP tools, and other automation consumers use.
The call protocol is the primary programmatic interface to an alk node. While SSH provides interactive shell access and HTTP provides REST APIs, the call protocol provides structured, discoverable RPC — the same interface that NAPI clients, MCP tools, and other automation consumers use.
The protocol must be:
- **Cross-language**: JSON wire format consumable from TypeScript, Python, any language
@@ -40,7 +40,7 @@ pub struct CallAdapter {
registry: Arc<OperationRegistry>,
identity_provider: Arc<dyn IdentityProvider>,
/// Layer 1 — optional session-overlay source (agent crate supplies this;
/// None for non-agent deployments). See ADR-019, OQ-19.
/// None for non-agent deployments). See ADR-019, OQ-04.
session_source: Option<Arc<dyn SessionOverlaySource + Send + Sync>>,
/// Default timeout for wire calls (30s). Composed calls inherit the
/// parent's remaining deadline via `OperationContext.deadline`.
@@ -57,8 +57,9 @@ impl CallAdapter {
default_timeout: Duration::from_secs(30) }
}
/// Agent deployment: supply a session-overlay source. The agent crate
/// implements `SessionOverlaySource`; alknet-call defines the trait.
/// Agent deployment: supply a session-overlay source. A downstream
/// consumer (e.g. the agent crate) implements `SessionOverlaySource`;
/// alkcall defines the trait.
pub fn with_session_source(mut self, source: Arc<dyn SessionOverlaySource + Send + Sync>) -> Self {
self.session_source = Some(source);
self
@@ -71,10 +72,11 @@ impl CallAdapter {
}
}
/// Session overlay integration point (ADR-019). Defined in alknet-call
/// because `CallAdapter` must name the type — alknet-call cannot depend on
/// alknet-agent (agent depends on call, not reverse). The agent crate
/// implements this trait; alknet-call defines it. This is the same pattern
/// Session overlay integration point (ADR-019). Defined in alkcall
/// because `CallAdapter` must name the type — alkcall cannot depend on
/// its downstream consumers (an agent depends on call, not reverse). The
/// consumer (e.g. the agent crate) implements this trait; alkcall
/// defines it. This is the same pattern
/// as `IdentityProvider` (ADR-003: core defines the trait, handlers impl it).
///
/// The session overlay is an `OperationEnv` impl that wraps the curated base
@@ -234,7 +236,7 @@ The `Value` type is `serde_json::Value`. The envelope is JSON because it must be
Binary payloads (postcard, protobuf) are base64-encoded as a JSON string within the `payload` field. The convention is: if an operation's output schema specifies a binary field, the handler encodes it as a base64 string and the client decodes it. The `EventEnvelope` structure is not aware of this convention — it carries a `serde_json::Value` and does not interpret the payload. This is a handler-level concern, not a protocol-level concern.
This is hand-rolled length-prefixed JSON framing (ADR-014), coincidentally
the same shape irpc uses. The Rust implementation in alknet-call is
the same shape irpc uses. The Rust implementation in alkcall is
canonical — the `@alkdev/pubsub` TypeScript adapters serve as a reference
and browser adaptation, not a parallel implementation (see ADR-033).
@@ -352,7 +354,7 @@ The call protocol defines five top-level operations, expressed through event typ
| **batch** | multiple `call.requested` (different IDs) → multiple `call.responded` | Multiple operations in one round |
| **schema** | `call.requested` name `services/list` or `services/schema` → `call.responded` | Discover available operations |
Batch is not a separate event type — it's multiple `call.requested` events with different request IDs. The client sends them (on one or many streams) and correlates the responses by ID. See OQ-14.
Batch is not a separate event type — it's multiple `call.requested` events with different request IDs. The client sends them (on one or many streams) and correlates the responses by ID. See OQ-03.
### Bidirectional Calls
@@ -573,8 +575,8 @@ Handlers clean up resources when their call is cancelled (in Rust, the future is
- The call protocol does not depend on any database. `PendingRequestMap` is in-memory. Durable session storage is a consumer concern.
- Operation specs use JSON Schema. The envelope is always JSON. Binary payloads may be base64-encoded in the `payload` field.
- Batch is not a protocol primitive — multiple `call.requested` events with correlated IDs provide equivalent semantics. See OQ-14.
- The call protocol is transport-agnostic at the envelope level. The `EventEnvelope` framing can run over QUIC streams, WebSocket frames, or Worker `postMessage`. The `CallAdapter` is the `ProtocolHandler` implementation that receives a `Connection` (any transport — ADR-007) and dispatches `EventEnvelope` frames. **The `EventEnvelope` shape (`{ type, id, payload }`) was derived from the `@alkdev/pubsub` `EventEnvelope` (`/workspace/@alkdev/pubsub/src/types.ts`), which already has a working WebSocket client/server implementation (`event-target-websocket-client.ts` / `event-target-websocket-server.ts`) and a generalized "event target" abstraction. The call protocol refined the envelope with typed event names (`call.requested`, `call.responded`, etc.) and structured payloads; the delta is small and well-defined, making a browser (and Node) WebSocket client straightforward to derive from the pubsub prior art. See alknet ADR-044, [ADR-048](/workspace/@alkdev/alknet/docs/architecture/decisions/048-websocket-native-session-not-gateway.md), and [websocket.md](/workspace/@alkdev/alknet/docs/architecture/crates/http/websocket.md).
- Batch is not a protocol primitive — multiple `call.requested` events with correlated IDs provide equivalent semantics. See OQ-03.
- The call protocol is transport-agnostic at the envelope level. The `EventEnvelope` framing can run over QUIC streams, WebSocket frames, or Worker `postMessage`. The `CallAdapter` is the `ProtocolHandler` implementation that receives a `Connection` (any transport — ADR-007) and dispatches `EventEnvelope` frames. **The `EventEnvelope` shape (`{ type, id, payload }`) was derived from the `@alkdev/pubsub` `EventEnvelope` (`/workspace/@alkdev/pubsub/src/types.ts`), which already has a working WebSocket client/server implementation (`event-target-websocket-client.ts` / `event-target-websocket-server.ts`) and a generalized "event target" abstraction. The call protocol refined the envelope with typed event names (`call.requested`, `call.responded`, etc.) and structured payloads; the delta is small and well-defined, making a browser (and Node) WebSocket client straightforward to derive from the pubsub prior art. Provenance: alknet ADR-044 and the alknet websocket spec (not ported) recorded the WebSocket-native session decision; the WebSocket carrier for the alk protocols is implemented in the sibling host crate `alkhttp` (its `websocket` feature), not in alkcall — alkcall stays transport- and host-blind.
- `OperationEnv::invoke()` dispatches through the local registry. Remote dispatch (federation, head/worker routing) would be a separate mechanism at a different layer. See ADR-014 and OQ-13.
- **The call protocol carries no secret material.** Secret material (private keys, API keys, mnemonics, decrypted credentials, raw tokens) must not appear in `call.requested` payloads, `call.responded` payloads, or `OperationContext.metadata`. The wire format carries `serde_json::Value` and cannot enforce this at the type level — the constraint is architectural, enforced by the operation registry and by convention. Operations that need to share public key material use a dedicated operation that returns only the public component. See ADR-010.
- **Abort cascades to descendants.** `call.aborted` for a parent request cascades to all non-terminal descendants in the call tree. Default policy is `abort-dependents`; `continue-running` is an opt-in. See ADR-020.
@@ -587,7 +589,7 @@ Handlers clean up resources when their call is cancelled (in Rust, the future is
| Call protocol stream model | [ADR-015](decisions/015-call-protocol-stream-model.md) | Bidirectional streams, EventEnvelope, ID-based correlation |
| ALPN per connection | [ADR-004](decisions/004-alpn-convention-and-connection-model.md) | `alk/call` is a distinct ALPN, one connection per ALPN |
| ProtocolHandler receives Connection | [ADR-005](decisions/005-bistream-type-definition.md) | CallAdapter gets Connection, can accept/open multiple streams |
| Vault integration point | [ADR-008](/workspace/@alkdev/alknet/docs/architecture/decisions/008-secret-service-integration.md) | Vault is a capability source, accessed at assembly time |
| Vault integration point | [ADR-010](decisions/010-secret-material-flow-and-capability-injection.md) | Vault is a capability source, accessed at assembly time (provenance: alknet ADR-008, superseded in-crate by ADR-010) |
| Secret material flow | [ADR-010](decisions/010-secret-material-flow-and-capability-injection.md) | Call protocol carries no secret material; capabilities injected at assembly layer |
| Privilege model and authority context | [ADR-017](decisions/017-privilege-model-and-authority-context.md) | `internal` = authority switch not ACL skip; External/Internal visibility; handler identity + scoped env |
| Abort cascade for nested calls | [ADR-020](decisions/020-abort-cascade-for-nested-calls.md) | `call.aborted` cascades to descendants; default `abort-dependents`, `continue-running` opt-in |
@@ -615,8 +617,8 @@ See [open-questions.md](open-questions.md) for full details.
`SamePeerCollision`); `#[non_exhaustive]`. See
[client-and-adapters.md](client-and-adapters.md).
- **OQ-12** (resolved): `from_call` re-import trigger — `from_call` is a
manual free function; the assembly layer calls it after the dial (in
`AlknetClient`). See
manual free function; after the dial (in the composition layer's dial helper
`AlknetClient` — today the alknet glue; OQ-43). See
[ADR-028](decisions/028-from-call-manual-free-function.md).
- **OQ-13** (resolved): `from_call` namespace collision — same-peer collision
= error; cross-peer dissolved by ADR-024 (separate sub-overlays). See
@@ -637,14 +639,16 @@ See [open-questions.md](open-questions.md) for full details.
rotation), not a connection-assigned UUID.
- **OQ-19** (resolved by ADR-025): Persistent peer registry —
the storage boundary is `core trait + in-memory default`; persistence
adapters are separate crates (the concrete adapter shapes were named in
alknet research, not re-specced here).
- **OQ-21** (resolved): X.509 outgoing-only case — three remote
adapters are separate crates (composition-layer work — OQ-43; the
concrete adapter shapes were named in the source repo's research
(alknet, provenance), not re-specced here).
- **OQ-21** (resolved by ADR-054): X.509 outgoing-only case — three remote
roles named (public X.509 endpoint, transport relay, hub); pure-client
X.509 connections are not in the peer graph on the client side. The
deciding ADR (alknet ADR-034, "Outgoing-Only X.509 and Three Peer
Roles") was not ported into alkcall; the surviving alkcall surface is
ADR-025's `PeerEntry` asymmetry. See [client-and-adapters.md](client-and-adapters.md).
X.509 connections are not in the peer graph on the client side.
Re-owned in-crate by ADR-054 (verifier selection by `PeerEntry`
presence; provenance: alknet ADR-034, not ported); ADR-025's
`PeerEntry` asymmetry is the surviving registry surface. See
[client-and-adapters.md](client-and-adapters.md).
## References
@@ -655,6 +659,6 @@ See [open-questions.md](open-questions.md) for full details.
- ADR-024: Peer-graph routing model (peer-keyed overlays + `PeerRef` routing)
- ADR-025: PeerEntry and Identity.id decoupling (`PeerId` source)
- ADR-026: Forwarded-for identity (`forwarded_for` on `call.requested` and `OperationContext`)
- ADR-034: Outgoing-only X.509 and the three peer roles
- ADR-054: Outgoing-only X.509 and the three remote roles (re-owned in-crate; verifier selection by `PeerEntry` presence)
- ADR-021: Streaming handler for subscriptions (server-side streaming dispatch path)
- Reference implementation: `/workspace/@alkdev/alknet-main/crates/alknet-core/src/call/`
- Reference implementation: this crate (`src/`); historical origin: the alknet / alknet-main call-core sources (provenance)