docs: roles, composition, and dependency layering for downstream crates

- docs/architecture/README.md: add Roles and Composition section with
  the four roles (producer, consumer, hub, spoke), dependency layering
  diagram, protocol crate pattern, and the two-path adapter model
- docs/architecture/channels-overview.md: fix stale alknet ADR numbers
  (071/072/073/074/075/076/077/078/079/080/081 -> 034-044), update
  relationship section for post-extraction world, update crate
  dependencies to reflect single-crate alkcall
- src/lib.rs: add Downstream composition section with role table and
  protocol crate pattern, linking to the architecture README

Verification: cargo test (483 passed), cargo clippy (clean),
cargo fmt (clean), cargo doc (no warnings)
This commit is contained in:
deepseek-v4-pro committed 2026-08-14 09:25:18 +00:00
1 parent 6c5aa0c8e1
commit 7cd8a57bc7
3 files changed
+250 -104

No files matched your search

+127
View File
@@ -140,6 +140,133 @@ questions affecting this crate:
10. **Wire formats are stable**: EventEnvelope shape and the 8-byte chunk
header are one-way doors. See ADR-014, ADR-034.
## Roles and Composition
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.
### The four roles
| Role | Definition | Call protocol | Channels protocol |
|------|-----------|---------------|-------------------|
| **Producer** | Provides a resource consumers can use (often called "server") | Registers ops on an `OperationRegistry`, runs a `Dispatcher` to handle incoming `call.requested` | Runs a `ChannelsAdapter`, registers openable ALPNs via `ChannelCore::register_openable` |
| **Consumer** | Consumes a resource from a producer (often called "client") | 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** | Central location spokes connect to; relays and routes between them | Both: runs a `Dispatcher` for ops it produces, holds `CallConnection`s to spokes for ops it consumes. Relays calls via `OperationEnv` peer routing | Both: runs a `ChannelsAdapter` for inbound connections, holds `ChannelClient`s to spokes. Relays data channels with `channel_id` rewrite (ADR-042) |
| **Spoke / Worker** | Connects to a hub; provides and consumes resources | Both: produces ops (its own services), consumes hub ops (e.g. `services/list` to discover peers) | 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. The types don't encode the system
role; they just don't prevent any combination.
Within a single connection, direction is independent of role:
- **Call initiator** / **call responder** — who sent `call.requested` vs
who handles it. Either side can initiate on any connection.
- **Channel opener** / **channel acceptor** — who called the per-ALPN
open op vs who allocated the `channel_id`. The connection owner
allocates (ADR-047 §5); either side can open.
### Dependency layering
```
┌──────────────────────────────────────────────┐
│ alknet / alknode │
│ (networking + composition) │
│ │
│ • QUIC / TCP+TLS dial and accept │
│ • Hub: ChannelsAdapter + Dispatcher + │
│ peer routing (PeerCompositeEnv) │
│ • Spoke: ChannelClient + CallConnection + │
│ from_call │
│ • Wires protocol crates' producers into │
│ registries, consumers into clients │
└──────────────────┬───────────────────────────┘
│ depends on
┌──────────────┼──────────────┐
│ │ │
┌───┴───────┐ ┌────┴─────┐ ┌─────┴──────────┐
│ alktty │ │alktunnels│ │ alktrader │
│ (protocol)│ │(protocol)│ │ (protocol) │
│ │ │ │ │ │
│ • Session │ │ • Tunnel │ │ • Backend trait │
│ • Backend │ │ • Backend│ │ • register_ops()│
│ • 5-byte │ │ • bytes │ │ • TypedClient │
│ wire │ │ • OpenH. │ │ │
│ • OpenH. │ │ • reg_*()│ │ │
│ • reg_*() │ │ │ │ │
└───┬───────┘ └────┬─────┘ └─────┬───────────┘
│ │ │
└──────────────┼──────────────┘
│ depends on
┌──────────────────┴───────────────────────────┐
│ alkcall │
│ (call + channels protocols, no networking) │
│ │
│ • Connection, BiStream, ProtocolHandler │
│ • OperationRegistry, OperationSpec, Handler │
│ • CallConnection, Dispatcher, from_call │
│ • ChannelClient, ChannelsAdapter, │
│ ChannelManager, ChannelCore │
└───────────────────────────────────────────────┘
```
**Protocol crates** (alktty, alktunnels, alktrader) depend only on
alkcall. They provide two halves:
1. **Producer half** — a `register_*()` function that takes an
`&mut OperationRegistry` and registers ops with their handlers. For
channels-based protocols, an `OpenHandler` factory. The crate doesn't
know whether it's running on a hub, a spoke, or a standalone process.
2. **Consumer half** — a typed client wrapper around `CallConnection`
(or `ChannelClient`) that exposes the crate's ops as async methods
(e.g. `trader_client.status().await` instead of
`call_connection.call("trader/status", ...).await`).
**alknet/alknode** depends on alkcall + whichever protocol crates are
needed. It's the composition layer — the only place that knows about
network topology, peer routing, and which protocol crates are wired in.
Protocol crates never import a QUIC or TLS dependency.
### Pattern for protocol crates
A protocol crate that uses channels (e.g. alktty) follows this pattern:
```
┌─────────────────────────────────────────┐
│ alktty (protocol crate, no network) │
│ │
│ TtySession { │
│ drive(send, recv, backend) -> ExitCode │ ← pure protocol, takes
│ } │ AsyncRead + AsyncWrite
│ │
│ TtyBackend trait │
│ NegotiateRequest / ControlMessage │
│ ChunkReader / ChunkWriter (5-byte) │
└─────────────────────────────────────────┘
▲ ▲
│ │
┌────────┴────────┐ ┌───────┴──────────────┐
│ Direct ALPN │ │ Through channels │
│ (alknet/tty) │ │ (alknet/channels) │
│ │ │ │
│ TtyAdapter │ │ OpenHandler │
│ impl Protocol │ │ (registered via │
│ Handler │ │ ChannelCore:: │
│ │ │ register_openable) │
│ Gets Connection │ │ │
│ loops accept_bi │ │ Gets BiStream per │
│ │ │ session │
└─────────────────┘ └───────────────────────┘
```
The protocol crate doesn't know which path it's on. It takes a
`BiStream` (or `AsyncRead + AsyncWrite`) and drives the session. The
two adapters are thin and live either in the protocol crate (behind
feature flags) or in the downstream alknet crate.
## References
- `@alkdev/alknet: docs/architecture/` — the source architecture docs