docs(architecture): untangle TLS identity use cases, remove phase framing, add ADR-013 Rust canonical + agent crate
- Rewrite OQ-12: separate two distinct TLS identity use cases (RFC 7250
raw keys as default for P2P, X.509 for domain-hosted/browsers) instead
of conflating them as 'file paths now, ACME later'. ACME is a proven
pattern from the reverse-proxy project, not speculative future work.
- Resolve OQ-13 and OQ-14: remove 'Phase 1' framing from core crate
specs. /{service}/{op} is the correct design for alknet-call, not a
simplification. Batch as correlated call.requested events is the correct
protocol design. Core crates need to be done right from the start.
- Add ADR-013: Rust as canonical implementation language. TypeScript
@alkdev/operations is a reference that informed the design, not a
parallel implementation. The only JS use case is browser SDK adaptation.
Five reasons: memory safety, LLM competence, supply chain attacks,
performance, browser-only JS.
- Add alknet-agent crate to the crate graph (depends on alknet-call, not
alknet-core). Agent service uses call protocol client for tool dispatch
and vault/derive for provider keys — no env vars for secrets. ALPN
alknet/agent added to the registry.
- Add OQ-15: call protocol client and adapter contract. alknet-call needs
both server (CallAdapter) and client (remote invocation over QUIC), plus
the adapter traits (from_*, to_*) that enable composition.
- Clarify alknet-napi as thin NAPI projection layer, not business logic.
- Fix bugs: ProtocolController → ProtocolHandler typo, OperationEnv
invoke() path format inconsistency, RateLimitConfig comment confusion.
- Update endpoint.md TLS section: comprehensive identity model comparison
table, RFC 7250 as default mode, ACME as proven pattern.
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-06-16
|
||||
last_updated: 2026-06-17
|
||||
---
|
||||
|
||||
# Alknet Architecture
|
||||
|
||||
## Current State
|
||||
|
||||
**Pre-implementation.** The project has completed a pivot from a three-layer model to an ALPN-as-service model. The greenfield workspace contains only `alknet-vault` (stable) and research/reference material. Foundational ADRs (001–012) are in place, including the BiStream type definition (ADR-007), vault integration (ADR-008), ALPN router/endpoint (ADR-010), AuthContext structure (ADR-011), and call protocol stream model (ADR-012). The alknet-core and alknet-call crate specs are in draft.
|
||||
**Pre-implementation.** The project has completed a pivot from a three-layer model to an ALPN-as-service model. The greenfield workspace contains only `alknet-vault` (stable) and research/reference material. Foundational ADRs (001–013) are in place, including the BiStream type definition (ADR-007), vault integration (ADR-008), ALPN router/endpoint (ADR-010), AuthContext structure (ADR-011), call protocol stream model (ADR-012), and Rust as canonical implementation language (ADR-013). The alknet-core and alknet-call crate specs are in draft.
|
||||
|
||||
**Next step**: Review alknet-call spec documents, then begin implementation. Two-way-door questions (OQ-11, OQ-13, OQ-14) will be resolved during implementation.
|
||||
**Next step**: Review alknet-call spec documents, then begin implementation. OQ-11 (handler-level auth resolution observability) will be resolved during implementation.
|
||||
|
||||
## Architecture Documents
|
||||
|
||||
@@ -42,6 +42,7 @@ last_updated: 2026-06-16
|
||||
| [010](decisions/010-alpn-router-and-endpoint.md) | ALPN Router and Endpoint | Accepted |
|
||||
| [011](decisions/011-authcontext-structure.md) | AuthContext Structure and Resolution Flow | Accepted |
|
||||
| [012](decisions/012-call-protocol-stream-model.md) | Call Protocol Stream Model | Accepted |
|
||||
| [013](decisions/013-rust-canonical-implementation.md) | Rust as Canonical Implementation Language | Accepted |
|
||||
|
||||
## Open Questions
|
||||
|
||||
@@ -58,12 +59,15 @@ See [open-questions.md](open-questions.md) for the full tracker.
|
||||
**Resolved two-way doors:**
|
||||
- **OQ-04**: Dynamic handler registration — static at startup (ADR-010)
|
||||
- **OQ-07**: Call protocol scope — bidirectional streams, EventEnvelope, ID-based correlation (ADR-012)
|
||||
- **OQ-12**: TLS certificate provisioning — file paths in StaticConfig, ACME later
|
||||
- **OQ-12**: TLS identity provisioning — two use cases: RFC 7250 raw keys (default, P2P) and X.509 certs (domain-hosted, browsers). ACME is a proven pattern.
|
||||
- **OQ-13**: Operation path format — `/{service}/{op}` is the correct design for alknet-call, not a simplification
|
||||
- **OQ-14**: Batch operation semantics — multiple correlated `call.requested` events is the correct protocol design, not a simplification
|
||||
|
||||
**Open two-way doors (resolved during implementation):**
|
||||
- **OQ-11**: Handler-level auth resolution observability — decide during implementation
|
||||
- **OQ-13**: Operation path format — `/{service}/{op}` for Phase 1, `/{node}/{service}/{op}` later
|
||||
- **OQ-14**: Batch operation semantics — client-side pattern for Phase 1, batch event types later
|
||||
|
||||
**Open one-way doors (need ADR before implementation):**
|
||||
- **OQ-15**: Call protocol client and adapter contract — alknet-call needs both the server (CallAdapter) and client (call invocation over QUIC), plus the adapter contract traits (from_*, to_*) that enable composition
|
||||
|
||||
**Deferred (not active):**
|
||||
- **OQ-09**: WASM target boundaries — design constraint, not deliverable
|
||||
|
||||
@@ -33,8 +33,8 @@ Structured RPC over QUIC: operations, request/response, streaming subscriptions,
|
||||
| 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 | open | Namespace paths: `/{service}/{op}` vs `/{node}/{service}/{op}` |
|
||||
| OQ-14 | Batch operation semantics | open | Whether batch is a protocol primitive or client-side pattern |
|
||||
| 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 |
|
||||
|
||||
## Key Design Principles
|
||||
|
||||
@@ -43,4 +43,4 @@ Structured RPC over QUIC: operations, request/response, streaming subscriptions,
|
||||
3. **Stream-agnostic correlation**: PendingRequestMap correlates by request ID, not by stream. The protocol works with any stream arrangement.
|
||||
4. **Operation registry is dynamic**: Operations are registered at startup by the CLI binary. The registry supports JSON Schema discovery.
|
||||
5. **irpc is one dispatch backend**: Local operations dispatch directly. irpc service calls (vault, auth) are internal. The call protocol is the external interface.
|
||||
6. **Phase 1 is local dispatch only**: The operation registry dispatches to local handlers. Remote dispatch (head/worker routing) and irpc service dispatch are contracted but not built yet.
|
||||
6. **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.
|
||||
@@ -85,7 +85,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 the same framing used by irpc and by the `@alkdev/pubsub` TypeScript adapters. The wire format is identical — an `EventEnvelope` flowing from a Rust handler through core, out over a QUIC stream, can be consumed by a JavaScript `@alkdev/operations` call handler with zero translation at the wire level.
|
||||
This is the same framing used by irpc. The Rust implementation in alknet-call is canonical — the `@alkdev/pubsub` TypeScript adapters serve as a reference and browser adaptation, not a parallel implementation (see ADR-013).
|
||||
|
||||
### Event Types
|
||||
|
||||
@@ -115,7 +115,7 @@ The `id` field carries the `requestId` for correlation.
|
||||
}
|
||||
```
|
||||
|
||||
Error codes use an extensible string enum. Phase 1 defines the following codes:
|
||||
Error codes use an extensible string enum. The protocol defines the following codes:
|
||||
- `NOT_FOUND` — operation not in registry
|
||||
- `FORBIDDEN` — access denied (insufficient scopes or unauthenticated)
|
||||
- `INVALID_INPUT` — input doesn't match the operation's JSON Schema
|
||||
@@ -254,7 +254,7 @@ Local dispatch produces `ResponseEnvelope` with no serialization overhead. The `
|
||||
- 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 QUIC-specific implementation.
|
||||
- Phase 1 is local dispatch only. The operation registry dispatches to handlers in the same process. Remote dispatch (head/worker routing) and irpc service dispatch are contracted but not built. See ADR-005 and OQ-13.
|
||||
- `OperationEnv::invoke()` dispatches through the local registry. Remote dispatch (federation, head/worker routing) would be a separate mechanism at a different layer. See ADR-005 and OQ-13.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
@@ -267,8 +267,10 @@ Local dispatch produces `ResponseEnvelope` with no serialization overhead. The `
|
||||
|
||||
## Open Questions
|
||||
|
||||
- **OQ-13**: What is the operation path format for the alknet-call crate? The reference implementation used `/{node}/{service}/{op}` for head/worker routing. Phase 1 is single-node, so `/{service}/{op}` may be sufficient. The node prefix can be added later when remote dispatch is implemented.
|
||||
- **OQ-14**: Should batch be a distinct protocol primitive with its own event types, or is the "multiple call.requested with correlated IDs" pattern sufficient? The reference implementation treats batch as a client-side pattern. This is a two-way door — batch-specific event types can be added later without breaking existing clients.
|
||||
See [open-questions.md](../../open-questions.md) for full details.
|
||||
|
||||
- **OQ-13** (resolved): Operation path format is `/{service}/{op}`. Remote dispatch is a separate mechanism, not a path prefix.
|
||||
- **OQ-14** (resolved): Batch is a client-side pattern of correlated `call.requested` events, not a protocol primitive.
|
||||
|
||||
## References
|
||||
|
||||
|
||||
@@ -19,9 +19,9 @@ The operation registry provides:
|
||||
- **Discoverability**: Clients can query `/services/list` and `/services/schema` to learn what operations exist before calling them
|
||||
- **Access control**: Each operation declares its required scopes and resources; the registry enforces ACL before invoking the handler
|
||||
- **Type safety**: JSON Schema for input and output enables validation and client code generation
|
||||
- **Composability**: Handlers can invoke other operations through `OperationEnv` (local dispatch in Phase 1)
|
||||
- **Composability**: Handlers can invoke other operations through `OperationEnv` (local dispatch — remote dispatch is a separate architectural concern, see Constraints)
|
||||
|
||||
The registry design is derived from the `@alkdev/operations` TypeScript package, which provides the same capabilities in JavaScript runtimes. The Rust implementation preserves the behavioral contract: namespace + operation name → invoke with input, return output.
|
||||
The registry design is informed by the `@alkdev/operations` TypeScript package, which demonstrated the same capabilities in JavaScript runtimes. The Rust implementation in alknet-call is canonical — it preserves the behavioral contract (namespace + operation name → invoke with input, return output) while defining the adapter contract (from_*, to_*) in Rust (see ADR-013).
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -146,7 +146,7 @@ pub trait OperationEnv: Send + Sync {
|
||||
|
||||
The `parent` parameter propagates the calling context: the nested call gets `parent_request_id: Some(parent.request_id)`, inherits `parent.identity`, and is marked `trusted: true`.
|
||||
|
||||
**Phase 1: Local dispatch only.** The initial `OperationEnv` implementation dispatches directly through the local `OperationRegistry`:
|
||||
**Local dispatch only.** The initial `OperationEnv` implementation dispatches directly through the local `OperationRegistry`:
|
||||
|
||||
```rust
|
||||
pub struct LocalOperationEnv {
|
||||
@@ -156,7 +156,7 @@ pub struct LocalOperationEnv {
|
||||
#[async_trait]
|
||||
impl OperationEnv for LocalOperationEnv {
|
||||
async fn invoke(&self, namespace: &str, operation: &str, input: Value, parent: &OperationContext) -> ResponseEnvelope {
|
||||
let name = format!("/{namespace}/{operation}");
|
||||
let name = format!("{namespace}/{operation}");
|
||||
let context = OperationContext {
|
||||
request_id: format!("env-{name}"),
|
||||
parent_request_id: Some(parent.request_id.clone()),
|
||||
@@ -170,7 +170,7 @@ impl OperationEnv for LocalOperationEnv {
|
||||
}
|
||||
```
|
||||
|
||||
Future phases add irpc service dispatch and remote call protocol dispatch as additional backends. The handler-facing API stays the same.
|
||||
Future work may add irpc service dispatch and remote call protocol dispatch as additional backends. The handler-facing API stays the same.
|
||||
|
||||
### Service Discovery
|
||||
|
||||
@@ -239,7 +239,7 @@ The registry is immutable after construction. Adding operations requires restart
|
||||
|
||||
- The registry is immutable after construction. No runtime registration or deregistration. Two-way door — `ArcSwap<OperationRegistry>` can be added later.
|
||||
- Operation specs use JSON Schema. The call protocol's external interface is always JSON. irpc's postcard serialization is internal only.
|
||||
- Phase 1 is local dispatch only. `OperationEnv::invoke()` goes through the local registry. irpc service dispatch and remote call protocol dispatch are contracted but not built.
|
||||
- `OperationEnv::invoke()` dispatches through the local registry. Remote dispatch (federation, head/worker routing) would be a separate mechanism at a different layer — not a prefix added to operation paths. irpc service dispatch is contracted but not built.
|
||||
- The call protocol does not depend on any database. Operation specs are in-memory, populated at startup.
|
||||
- `OperationContext.trusted` is set by `OperationEnv`, not by callers. A handler cannot mark its own call as trusted.
|
||||
|
||||
@@ -254,8 +254,10 @@ The registry is immutable after construction. Adding operations requires restart
|
||||
|
||||
## Open Questions
|
||||
|
||||
- **OQ-13**: Operation path format — `/{service}/{op}` for Phase 1 (single-node), with the node prefix `/{node}/{service}/{op}` added when remote dispatch is implemented. Two-way door — the prefix can be added later without breaking existing operations.
|
||||
- **OQ-14**: Batch operation semantics — whether to add batch-specific event types or rely on the "multiple call.requested with correlated IDs" pattern. Two-way door — can be added later.
|
||||
See [open-questions.md](../../open-questions.md) for full details.
|
||||
|
||||
- **OQ-13** (resolved): Operation path format is `/{service}/{op}`. Remote dispatch is a separate mechanism, not a path prefix.
|
||||
- **OQ-14** (resolved): Batch is a client-side pattern of correlated `call.requested` events, not a protocol primitive.
|
||||
|
||||
## References
|
||||
|
||||
|
||||
@@ -51,7 +51,9 @@ pub enum TlsIdentity {
|
||||
|
||||
### Why `TlsIdentity` instead of `tls_cert`/`tls_key` options
|
||||
|
||||
The original `tls_cert: Option<PathBuf>` / `tls_key: Option<PathBuf>` assumed X.509 was the only TLS identity model. RFC 7250 raw public keys (used by iroh, supported by rustls) provide an alternative: Ed25519 key as identity, no X.509, no CA, no domain. This is a separate mode, not just "no cert."
|
||||
TLS identity in alknet has two distinct use cases, not one. The original `tls_cert: Option<PathBuf>` / `tls_key: Option<PathBuf>` assumed X.509 was the only TLS identity model. RFC 7250 raw public keys (used by iroh, supported by rustls) provide a fundamentally different mode: Ed25519 key as identity, no X.509, no CA, no domain. This is the default for most alknet nodes — it works natively with SSH auth and git. X.509 certs are for domain-hosted services and browser/WebTransport clients, which don't support RFC 7250.
|
||||
|
||||
The `TlsIdentity` enum captures both use cases plus a development mode. See OQ-12 for the full rationale.
|
||||
|
||||
### Key differences from reference implementation
|
||||
|
||||
@@ -70,10 +72,23 @@ The reference `StaticConfig` (in `alknet-main/crates/alknet-core/src/config/stat
|
||||
// The CLI binary constructs StaticConfig from its own options/config.
|
||||
// StartupOptions is NOT a core type — it belongs to the alknet CLI binary.
|
||||
// alknet-core receives a fully populated StaticConfig.
|
||||
let static_config = StaticConfig {
|
||||
listen_addr: "0.0.0.0:4433".parse()?,
|
||||
tls_cert: Some("/path/to/cert.pem".into()),
|
||||
tls_key: Some("/path/to/key.pem".into()),
|
||||
|
||||
// P2P / key-based identity (default for most nodes)
|
||||
let p2p_config = StaticConfig {
|
||||
listen_addr: Some("0.0.0.0:4433".parse()?),
|
||||
tls_identity: Some(TlsIdentity::RawKey(SecretKey::generate())),
|
||||
iroh_relay: None,
|
||||
drain_timeout: Duration::from_secs(2),
|
||||
};
|
||||
|
||||
// Domain-hosted service (relays, public services, browsers)
|
||||
let domain_config = StaticConfig {
|
||||
listen_addr: Some("0.0.0.0:4433".parse()?),
|
||||
tls_identity: Some(TlsIdentity::X509 {
|
||||
cert: "/path/to/cert.pem".into(),
|
||||
key: "/path/to/key.pem".into(),
|
||||
}),
|
||||
iroh_relay: None,
|
||||
drain_timeout: Duration::from_secs(2),
|
||||
};
|
||||
```
|
||||
@@ -148,12 +163,7 @@ pub struct RateLimitConfig {
|
||||
}
|
||||
```
|
||||
|
||||
Carries forward from the reference implementation. Note: `max_connections_per_ip` and `max_auth_attempts` appear in both `StaticConfig` and `RateLimitConfig`. The relationship is:
|
||||
|
||||
- `StaticConfig` does NOT contain rate limit fields. Rate limits are entirely dynamic.
|
||||
- `RateLimitConfig` in `DynamicConfig` is the authoritative source at runtime.
|
||||
- The CLI binary sets initial `RateLimitConfig` values when creating the initial `DynamicConfig`.
|
||||
- Hot-reloading `DynamicConfig` via `ConfigReloadHandle` replaces rate limits immediately — no restart needed.
|
||||
Carries forward from the reference implementation. Rate limits are entirely dynamic — `StaticConfig` does not contain rate limit fields. The CLI binary sets initial `RateLimitConfig` values when constructing the initial `DynamicConfig`. Hot-reloading via `ConfigReloadHandle` replaces rate limits immediately without restart.
|
||||
|
||||
## ArcSwap Pattern
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ pub struct HandlerRegistry {
|
||||
|
||||
impl HandlerRegistry {
|
||||
pub fn new() -> Self;
|
||||
pub fn register(&mut self, handler: Arc<dyn ProtocolController>);
|
||||
pub fn register(&mut self, handler: Arc<dyn ProtocolHandler>);
|
||||
pub fn get(&self, alpn: &[u8]) -> Option<&Arc<dyn ProtocolHandler>>;
|
||||
pub fn alpn_strings(&self) -> Vec<Vec<u8>>;
|
||||
}
|
||||
@@ -170,34 +170,58 @@ This matches the reference implementation: the TLS cert encrypts and camouflages
|
||||
|
||||
## RFC 7250: Raw Public Keys in TLS
|
||||
|
||||
iroh uses RFC 7250 raw public keys instead of X.509 certificates for TLS. The implementation is ~100 lines (see `iroh/iroh/src/tls/resolver.rs`): take an Ed25519 key, wrap its SPKI public key as a `CertificateDer`, tell rustls `only_raw_public_keys() -> true`. No X.509, no CAs, no domain names, no cert renewal.
|
||||
RFC 7250 raw public keys are the **default TLS identity mode** for most alknet nodes. They eliminate the need for domain names, CAs, and certificate renewal — the Ed25519 public key IS the node's identity.
|
||||
|
||||
rustls already supports RFC 7250. This means the quinn endpoint can also use raw Ed25519 public keys instead of X.509 certs:
|
||||
iroh uses this model with its `NodeId`. The implementation is ~100 lines (see `iroh/iroh/src/tls/resolver.rs`): take an Ed25519 key, wrap its SPKI public key as a `CertificateDer`, tell rustls `only_raw_public_keys() -> true`. No X.509, no CAs, no domain names, no cert renewal.
|
||||
|
||||
- **No domain required**: A node without a domain name can use raw public keys for the quinn path — key-based identity, but with direct QUIC over UDP instead of relay-assisted connections.
|
||||
Key implications:
|
||||
|
||||
- **Default for alknet-native clients**: SSH, git, and alknet-native clients all work with raw Ed25519 keys out of the box. The same key type used for SSH auth can serve as the TLS identity. This is the most common deployment mode.
|
||||
- **No domain required**: A node without a domain name uses raw public keys for the quinn path — key-based identity with direct QUIC over UDP.
|
||||
- **Key = identity**: The Ed25519 public key IS the node's identity. No CA trust chain, no cert expiry. The key can be derived from alknet-vault.
|
||||
- **X.509 is optional**: Domain-facing identity (replicators, public services) uses X.509 certs. Key-based identity (personal nodes, P2P) uses raw public keys. Both work with the same quinn endpoint.
|
||||
- **Browser limitation**: Browsers don't support RFC 7250. For browser/WebTransport clients, X.509 certs are needed. For alknet-native clients, raw public keys work fine.
|
||||
- **X.509 is for domain-hosted services**: Domain-facing identity (replicators, public services, browsers) uses X.509 certs. This is a separate use case, not the default.
|
||||
- **Browser limitation**: Browsers don't support RFC 7250. For browser/WebTransport clients, X.509 certs are needed. For all other clients, raw public keys work fine.
|
||||
|
||||
This reframes the connectivity model. The quinn and iroh paths share the same key-based identity model via RFC 7250. They're distinguished by **connection establishment** (direct UDP vs relay-assisted), not by identity:
|
||||
The quinn and iroh paths share the same key-based identity model via RFC 7250. They're distinguished by **connection establishment** (direct UDP vs relay-assisted), not by identity:
|
||||
|
||||
| Path | Connection establishment | Identity (domain-facing) | Identity (key-facing) |
|
||||
|------|------------------------|------------------------|---------------------|
|
||||
| quinn | Direct UDP, public IP | X.509 cert (domain name) | RFC 7250 raw key |
|
||||
| iroh | Relay-assisted P2P | N/A | RFC 7250 raw key (NodeId) |
|
||||
| Path | Connection establishment | Default identity | Alternative identity |
|
||||
|------|------------------------|-----------------|---------------------|
|
||||
| quinn | Direct UDP, public IP | RFC 7250 raw key (most nodes) | X.509 cert (domain-hosted, browsers) |
|
||||
| iroh | Relay-assisted P2P | RFC 7250 raw key (NodeId) | N/A |
|
||||
|
||||
## TLS Certificate Provisioning
|
||||
## TLS Identity
|
||||
|
||||
For the quinn endpoint, `StaticConfig` provides TLS configuration via file paths:
|
||||
TLS identity in alknet has two distinct use cases, each with a different trust model and provisioning mechanism. See OQ-12 for the full rationale.
|
||||
|
||||
- **Manual**: `tls_cert` and `tls_key` file paths. Required for production use.
|
||||
- **Self-signed**: For development. The endpoint can generate a self-signed cert on startup.
|
||||
### Use case 1: P2P / Key-based identity (default)
|
||||
|
||||
The `rustls::ServerConfig` is built from cert + key + ALPN list at startup.
|
||||
Most alknet nodes use RFC 7250 raw Ed25519 public keys for TLS identity. No domain name, no CA, no certificate renewal. The Ed25519 public key IS the node's identity — the same key model as iroh's `NodeId`, but for direct QUIC connections.
|
||||
|
||||
ACME auto-provisioning (Let's Encrypt) is not in scope for v1. It will be added as a feature later (see OQ-12).
|
||||
`TlsIdentity::RawKey` in `StaticConfig` configures this mode. The endpoint builds a `rustls::ServerConfig` with `only_raw_public_keys() -> true` and a `ResolvesServerCert` that generates the certificate on-the-fly from the key, exactly as iroh does (see `iroh/iroh/src/tls/resolver.rs`).
|
||||
|
||||
The iroh endpoint does not need TLS certs — it uses `NodeId` for identity.
|
||||
This mode works natively with SSH auth (same key type) and git (SSH key-based auth). It is the default for alknet-native clients. **Browser/WebTransport clients do not support RFC 7250** — they require X.509 certificates.
|
||||
|
||||
### Use case 2: Domain-hosted services
|
||||
|
||||
Nodes that serve browser/WebTransport clients, or nodes with public domain names, use X.509 certificates. This has two sub-cases:
|
||||
|
||||
- **Manual**: Provide cert/key file paths via `TlsIdentity::X509`. The endpoint loads them at startup and builds a standard `rustls::ServerConfig`.
|
||||
- **ACME auto-provisioning**: Let's Encrypt via `rustls-acme`. The reverse-proxy project (`/workspace/@alkdev/reverse-proxy`) demonstrates the complete pattern: per-listener ACME state machine, `ResolvesServerCertAcme` rustls integration, TLS-ALPN-01 challenge handling, automatic renewal. This is a proven, solved implementation pattern. It will be adapted to alknet's `AlknetEndpoint` context as an additional `TlsIdentity` variant or `ResolvesServerCert` implementation.
|
||||
|
||||
`TlsIdentity::SelfSigned` is for development only — the endpoint generates a self-signed cert on startup. External clients will not trust it.
|
||||
|
||||
### iroh endpoint identity
|
||||
|
||||
The iroh endpoint does not need TLS certificate configuration — it uses `NodeId` (Ed25519) for identity, which is RFC 7250 raw key identity built into the iroh endpoint.
|
||||
|
||||
### Identity model comparison
|
||||
|
||||
| Path | Identity model | Client compatibility | Use case |
|
||||
|------|---------------|---------------------|----------|
|
||||
| quinn + `TlsIdentity::RawKey` | RFC 7250 Ed25519 raw key | alknet-native, SSH, git | Personal nodes, P2P, most deployments |
|
||||
| quinn + `TlsIdentity::X509` | X.509 domain certificate | All clients including browsers | Relays, public services, WebTransport |
|
||||
| quinn + `TlsIdentity::SelfSigned` | X.509 self-signed cert | None (dev only) | Local development |
|
||||
| iroh | NodeId (Ed25519, RFC 7250 built-in) | alknet-native, iroh clients | NAT traversal, home servers |
|
||||
|
||||
## Graceful Shutdown
|
||||
|
||||
@@ -270,4 +294,4 @@ See [open-questions.md](../../open-questions.md) for full details.
|
||||
|
||||
- **OQ-04**: Resolved — HandlerRegistry is static at startup.
|
||||
- **OQ-05**: Resolved — multi-connectivity endpoint with quinn + iroh, both feature-gated.
|
||||
- **OQ-12**: Resolved — start with file paths in StaticConfig, add ACME later.
|
||||
- **OQ-12**: Resolved — two distinct TLS identity use cases: RFC 7250 raw keys (default, P2P) and X.509 certs (domain-hosted, browsers). ACME is a proven pattern from the reverse-proxy project, not speculative future work.
|
||||
@@ -14,8 +14,9 @@ Key constraints:
|
||||
- Protocol crates must depend on alknet-core for auth/identity/config — but not on each other
|
||||
- alknet-vault is already standalone (no alknet-core dependency) and must remain so (see ADR-008)
|
||||
- The CLI binary assembles everything — it's the only crate that depends on all handler crates
|
||||
- Some handlers (SFTP, call protocol) need to compile to WASM for browser/client use
|
||||
- irpc is the foundation for the call protocol — it provides the operation registry, framing, and pub/sub patterns
|
||||
- Handlers with protocol-agnostic cores (SFTP, call protocol) preserve the WASM door — browser clients can implement the wire format over WebTransport (see ADR-009, ADR-013)
|
||||
- alknet-call includes the call protocol client and adapter traits, not just the server side — this enables alknet-agent and alknet-napi to use it for remote invocation
|
||||
- Rust is the canonical implementation language. TypeScript is a reference/browser adaptation, not a parallel implementation (see ADR-013)
|
||||
|
||||
## Decision
|
||||
|
||||
@@ -26,24 +27,30 @@ The workspace decomposes into the following crates:
|
||||
| `alknet-core` | ProtocolHandler trait, ALPN router, endpoint, BiStream, AuthContext, IdentityProvider, config, ArcSwap dynamic config | tokio, quinn, rustls, irpc |
|
||||
| `alknet-vault` | Local key vault: BIP39/SLIP-0010/AES-GCM key derivation, encryption, VaultProtocol dispatch | (standalone, no alknet-core) |
|
||||
| `alknet-ssh` | SshAdapter (russh, SOCKS5, port forwarding) | alknet-core, russh |
|
||||
| `alknet-call` | CallAdapter (JSON-RPC via irpc, operation registry, pub/sub, access control) | alknet-core, irpc |
|
||||
| `alknet-call` | CallAdapter (JSON-RPC via irpc, operation registry, pub/sub, access control, call protocol client, adapter traits) | alknet-core, irpc |
|
||||
| `alknet-agent` | Agent service: LLM execution loop (forked aisdk), tool dispatch via call protocol, provider key retrieval via vault | alknet-call |
|
||||
| `alknet-git` | GitAdapter (gix, pkt-line protocol) | alknet-core, gix |
|
||||
| `alknet-sftp` | SftpAdapter (russh-sftp protocol core) | alknet-core, russh-sftp |
|
||||
| `alknet-msg` | MessageAdapter (E2E encryption, mixnet) | alknet-core |
|
||||
| `alknet-http` | HttpAdapter (axum, REST API, MCP endpoint) | alknet-core, axum |
|
||||
| `alknet-dns` | DnsAdapter (hickory-proto, pkarr, service discovery) | alknet-core, hickory-proto |
|
||||
| `alknet-napi` | Node.js native addon (call protocol client) | alknet-call, napi-rs |
|
||||
| `alknet` | CLI binary — registers handlers, starts endpoint | all handler crates |
|
||||
| `alknet-napi` | Node.js native addon — thin NAPI projection of the call protocol client | alknet-call, napi-rs |
|
||||
| `alknet` | CLI binary — registers handlers, starts endpoint | all handler crates, alknet-vault |
|
||||
|
||||
Dependency flow:
|
||||
```
|
||||
alknet-vault (standalone)
|
||||
alknet-core ← all handler crates ← alknet (CLI)
|
||||
alknet-call ← alknet-agent
|
||||
alknet-call ← alknet-napi
|
||||
```
|
||||
|
||||
No handler crate depends on another handler crate. Cross-handler communication goes through the call protocol (alknet-call) or through alknet-core's endpoint.
|
||||
|
||||
alknet-agent depends on alknet-call (not alknet-core directly) because it uses the call protocol client for tool dispatch and the operation registry for tool registration. It retrieves LLM provider keys through alknet-call → alknet-vault (via the call protocol), never from environment variables.
|
||||
|
||||
alknet-napi is a thin projection layer — it exposes the Rust call protocol client to Node.js via NAPI. It does not contain business logic or adapter implementations. See ADR-013.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
|
||||
@@ -14,7 +14,7 @@ The previous implementation used `irpc` for the call protocol's operation regist
|
||||
- Request/response and streaming patterns
|
||||
- Type-safe operation definitions via derive macros
|
||||
|
||||
The call protocol is derived from a TypeScript implementation of "operations" and "pub/sub" that can wholesale import OpenAPI schemas, wrap MCP servers, and go the other direction — exposing operations as HTTP endpoints, MCP tools, etc. This bidirectional capability is strategically important.
|
||||
The call protocol is derived from a TypeScript implementation (`@alkdev/operations`, `@alkdev/pubsub`) that informed the design of the operation registry, EventEnvelope framing, and adapter patterns (from_openapi, from_mcp, from_call). This bidirectional composition capability is strategically important. The TypeScript code is a reference that informed the Rust design — it is not a parallel implementation (see ADR-013).
|
||||
|
||||
## Decision
|
||||
|
||||
@@ -27,8 +27,8 @@ irpc is not replaced or wrapped in an abstraction layer — it IS the call proto
|
||||
This means:
|
||||
- The wire format is irpc's EventEnvelope framing — length-prefixed JSON
|
||||
- Operation schemas follow irpc's schema model — JSON Schema compatible
|
||||
- The TypeScript "operations" and "pub/sub" patterns that can import OpenAPI schemas and expose MCP tools are supported at the protocol level
|
||||
- Future NAPI and WASM clients speak the same wire format
|
||||
- The TypeScript operation and pub/sub patterns that can import OpenAPI schemas, wrap MCP servers, and expose operations as endpoints are supported at the protocol level — the adapter contract (from_*, to_*) is defined in Rust (see ADR-013)
|
||||
- Future NAPI and WASM clients speak the same wire format — alknet-napi projects the Rust call protocol client to Node.js; a browser SDK can be adapted from the existing TypeScript code
|
||||
|
||||
The `VaultProtocol` in alknet-vault also uses irpc as its service protocol. This is consistent — alknet-vault's irpc service is an independent service that happens to use the same framing, not a dependency on alknet-call.
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ pub enum HandlerError {
|
||||
**Negative:**
|
||||
- alknet-core depends on both quinn and iroh (mitigated: both are feature-gated; a node that only needs one doesn't compile the other)
|
||||
- The endpoint is more complex than a single quinn listener — it manages multiple accept loops
|
||||
- TLS cert provisioning is manual (file paths) for v1 — ACME auto-provisioning is a future feature (OQ-12)
|
||||
- TLS identity provisioning has two distinct use cases: RFC 7250 raw keys (default for P2P/key-based identity) and X.509 certs (for domain-hosted services and browsers). ACME auto-provisioning for X.509 is a proven pattern from the reverse-proxy project, not speculative future work. See OQ-12.
|
||||
- No runtime handler registration without regenerating the TLS config (mitigated: two-way door, start static, add ArcSwap later if needed)
|
||||
|
||||
## References
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# ADR-013: Rust as Canonical Implementation Language
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
alknet's core crates (alknet-core, alknet-call, alknet-vault) and all handler crates are implemented in Rust. A previous TypeScript implementation (`@alkdev/operations`, `@alkdev/pubsub`) informed the design of the call protocol — its operation registry, EventEnvelope framing, adapter patterns (from_openapi, from_mcp, from_call), and bidirectional composition.
|
||||
|
||||
The question is: what is the relationship between the TypeScript implementation and the Rust implementation? Is TypeScript a parallel implementation that must be maintained in lockstep, or is Rust the canonical implementation with TypeScript serving a specific role?
|
||||
|
||||
Five factors make Rust the canonical choice:
|
||||
|
||||
1. **Memory safety eliminates an entire vulnerability class.** Rust's ownership model prevents buffer overflows, use-after-free, and other memory corruption bugs that are endemic in C/C++ and impossible to audit away in JavaScript runtimes.
|
||||
|
||||
2. **LLM code generation quality is comparable across Rust and TypeScript.** Agents "grok" both languages roughly equally, so there is no productivity argument for TypeScript.
|
||||
|
||||
3. **NPM supply chain attacks are growing rapidly.** The JavaScript ecosystem's dependency density makes supply chain attacks a persistent and increasing risk. NPM is dropping features like post-install scripts in response. This trend makes JavaScript an unreliable foundation for security-critical infrastructure.
|
||||
|
||||
4. **Rust is significantly faster.** For networking, encryption, and protocol handling, the performance difference is material — not marginal.
|
||||
|
||||
5. **The only legitimate JavaScript use case is the browser.** WASM/WebTransport clients need a JavaScript SDK, and the existing `@alkdev/operations` TypeScript code can be adapted for browser use cases where users want to expose operations to web applications. This is a consumer SDK, not a parallel implementation.
|
||||
|
||||
## Decision
|
||||
|
||||
**Rust is the canonical implementation language.** All alknet crates are implemented in Rust. The TypeScript `@alkdev/operations` and `@alkdev/pubsub` libraries are reference implementations that informed the design; they are not maintained as parallel implementations.
|
||||
|
||||
The relationship between the TypeScript and Rust implementations:
|
||||
|
||||
| Aspect | Rust (canonical) | TypeScript (reference/browser) |
|
||||
|--------|-----------------|-------------------------------|
|
||||
| OperationSpec, OperationRegistry | alknet-call owns canonical types | `@alkdev/operations` projects canonical types into TS |
|
||||
| Wire protocol (EventEnvelope) | alknet-call owns canonical framing | `@alkdev/pubsub` implements the same wire format for browser |
|
||||
| Adapter patterns (from_*, to_*) | alknet-call defines adapter traits and Rust implementations | Browser-adapted implementations where needed |
|
||||
| Call protocol client | alknet-call (QUIC) | alknet-napi (QUIC via NAPI) or browser SDK (WebTransport) |
|
||||
| LLM provider integration | alknet-agent (forked aisdk, simplified) | Not applicable |
|
||||
| Provider key management | alknet-vault via call protocol (no env vars) | Not applicable |
|
||||
|
||||
**The adapter contract (from_openapi, from_mcp, from_call, to_openapi, to_mcp) lives in Rust.** These patterns convert external specifications or protocols into `OperationSpec + Handler` pairs that register in the local `OperationRegistry`. The TypeScript implementations serve as reference for browser adaptations, not as the source of truth.
|
||||
|
||||
**alknet-napi is a thin projection layer.** It exposes the Rust call protocol client to Node.js via NAPI. It does not contain business logic or adapter implementations. TypeScript consumers who want to use alknet from Node.js use alknet-napi to access the Rust implementation.
|
||||
|
||||
**The browser SDK is a future adaptation.** When WASM/WebTransport support is needed, the existing TypeScript code can be adapted to run in browsers, speaking the same EventEnvelope wire format over WebTransport streams. This preserves the WASM door (ADR-009) without requiring Rust-to-WASM compilation of the full stack.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
- Single implementation to maintain, test, and secure
|
||||
- Memory safety eliminates a whole class of vulnerabilities
|
||||
- Provider key management through alknet-vault (call protocol) instead of env vars
|
||||
- No NPM dependency chain for security-critical infrastructure
|
||||
- The existing TypeScript code informs the Rust design — its patterns are preserved, not its implementation
|
||||
- Browser clients get a thin, adapted SDK rather than the full operations library
|
||||
|
||||
**Negative:**
|
||||
- Browser support requires a separate JavaScript SDK (adapted from existing TS code) rather than a shared implementation
|
||||
- Contributors who only know JavaScript cannot contribute to core alknet crates
|
||||
- The `@alkdev/operations` TypeScript library may drift from the canonical Rust types if not kept in sync during the transition period
|
||||
|
||||
**Risks mitigated:**
|
||||
- WASM door preserved: The `@alkdev/operations` TypeScript code can be adapted for browser use without recompiling Rust to WASM. The wire format is JSON, which any runtime can produce and consume.
|
||||
- NAPI consumers: alknet-napi provides the call protocol client to Node.js without reimplementing in JavaScript.
|
||||
|
||||
## References
|
||||
|
||||
- ADR-003: Crate decomposition
|
||||
- ADR-005: irpc as call protocol foundation
|
||||
- ADR-009: One-way door decision framework (WASM door)
|
||||
- Reference TypeScript implementation: `/workspace/@alkdev/operations`
|
||||
- Reference TypeScript pubsub: `/workspace/@alkdev/pubsub`
|
||||
- aisdk (Rust port to be forked): `/workspace/aisdk`
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-06-16
|
||||
last_updated: 2026-06-17
|
||||
---
|
||||
|
||||
# Open Questions
|
||||
@@ -95,7 +95,7 @@ Door type classifications follow ADR-009:
|
||||
|
||||
## Deferred Questions
|
||||
|
||||
These questions are acknowledged but not active. They will be promoted to open when their crate becomes a Phase 1 implementation target.
|
||||
These questions are acknowledged but not active. They will be promoted to open when their crate is being specified.
|
||||
|
||||
### OQ-09: WASM Target Boundaries
|
||||
|
||||
@@ -126,29 +126,50 @@ These questions are acknowledged but not active. They will be promoted to open w
|
||||
- **Resolution**: When a handler resolves identity inside `handle()`, should the resolved `Identity` be stored somewhere for observability (e.g., connection logging), or is the handler's local variable sufficient? Options: (A) handlers return the resolved identity from `handle()`, (B) handlers call a method on Connection to set identity, (C) handlers log locally and the resolved identity stays local. Two-way door — can be decided during implementation.
|
||||
- **Cross-references**: ADR-004, ADR-011
|
||||
|
||||
### OQ-12: TLS Certificate Provisioning in AlknetEndpoint
|
||||
### OQ-12: TLS Identity Provisioning in AlknetEndpoint
|
||||
|
||||
- **Origin**: [endpoint.md](crates/core/endpoint.md), [config.md](crates/core/config.md)
|
||||
- **Status**: resolved
|
||||
- **Door type**: Two-way
|
||||
- **Priority**: medium
|
||||
- **Resolution**: Start with file paths in StaticConfig (option a). The CLI binary provides `tls_cert` and `tls_key` paths at startup. ACME auto-provisioning (option b) and external cert managers (option c) are additive — they can be added as features without changing the core StaticConfig or endpoint lifecycle. `StaticConfig` does NOT include `acme_domain` in v1; ACME will be a separate feature when implemented.
|
||||
- **Cross-references**: ADR-010, [config.md](crates/core/config.md)
|
||||
- **Door type**: One-way
|
||||
- **Priority**: high
|
||||
- **Resolution**: TLS identity in alknet has two distinct use cases, not one:
|
||||
|
||||
**Use case 1 — P2P / key-based identity (default for most alknet nodes):** RFC 7250 raw Ed25519 public keys. No domain, no CA, no cert renewal. The Ed25519 public key IS the node's identity. This is the same model iroh uses with its `NodeId`. It works natively with SSH auth (same key type) and git (SSH key-based auth). `TlsIdentity::RawKey` in `StaticConfig` covers this. This is the primary identity mode for alknet-native clients — most nodes will use this.
|
||||
|
||||
**Use case 2 — Domain-hosted services (relays, public-facing nodes):** X.509 certificates with domain names. Required for browser/WebTransport clients, which don't support RFC 7250. This has two sub-cases:
|
||||
- **Manual**: Provide cert/key file paths via `TlsIdentity::X509`. Already specified in `StaticConfig`.
|
||||
- **ACME auto-provisioning**: Let's Encrypt via rustls-acme. The reverse-proxy project (`/workspace/@alkdev/reverse-proxy`) demonstrates the complete pattern: per-listener ACME state machine, `ResolvesServerCertAcme` rustls integration, TLS-ALPN-01 challenge handling, automatic renewal. This is a proven, solved implementation pattern — not speculative future work. It will be adapted to alknet's `AlknetEndpoint` context when domain-hosted nodes need it.
|
||||
|
||||
**Browser constraint**: Browsers require X.509 and don't support RFC 7250. For browser/WebTransport clients, domain-hosted nodes with X.509 certs are mandatory. All other clients (SSH, git, alknet-native) work with raw keys by default.
|
||||
|
||||
The `TlsIdentity` enum in `StaticConfig` already captures all three modes (`X509`, `RawKey`, `SelfSigned`). ACME auto-provisioning is additive — it produces an X.509 cert at runtime rather than from files, and fits naturally as an additional `TlsIdentity` variant or as a `rustls::ResolvesServerCert` implementation behind the existing `X509` path.
|
||||
- **Cross-references**: ADR-010, [config.md](crates/core/config.md), [endpoint.md](crates/core/endpoint.md)
|
||||
|
||||
### OQ-13: Operation Path Format and Routing Scope
|
||||
|
||||
- **Origin**: [operation-registry.md](crates/call/operation-registry.md)
|
||||
- **Status**: open
|
||||
- **Status**: resolved
|
||||
- **Door type**: Two-way
|
||||
- **Priority**: medium
|
||||
- **Resolution**: Phase 1 uses `/{service}/{op}` (e.g., `/vault/derive`, `/services/list`). The head/worker `/{node}/{service}/{op}` routing from the reference implementation is a Phase 2+ concern that can be added when remote dispatch is implemented — the node prefix is additive and doesn't break existing operations. Two-way door.
|
||||
- **Resolution**: alknet-call uses `/{service}/{op}` (e.g., `/vault/derive`, `/services/list`). This is the correct format for the alknet-call crate — it is not a "Phase 1 simplification" but the right design for this architecture. The `/{node}/{service}/{op}` pattern from the reference implementation served a head/worker routing model that is a separate architectural concern. Remote dispatch (federation / node-level routing) would be a different mechanism at a different layer, not a prefix added to alknet-call's operation paths. If remote dispatch is ever needed, it would be addressed by a separate crate or a routing layer above the operation registry, not by changing alknet-call's path format. Two-way door — the path format can be extended later if needed, but `/{service}/{op}` is the correct design now.
|
||||
- **Cross-references**: ADR-005, ADR-012
|
||||
|
||||
### OQ-14: Batch Operation Semantics
|
||||
|
||||
- **Origin**: [call-protocol.md](crates/call/call-protocol.md)
|
||||
- **Status**: open
|
||||
- **Status**: resolved
|
||||
- **Door type**: Two-way
|
||||
- **Priority**: low
|
||||
- **Resolution**: Phase 1 treats batch as a client-side pattern — multiple `call.requested` events with correlated IDs, responses arrive independently. Batch-specific event types can be added later if needed (e.g., `batch.requested`, `batch.responded`) without breaking the existing protocol. Two-way door.
|
||||
- **Cross-references**: ADR-012
|
||||
- **Resolution**: Batch is a client-side pattern — multiple `call.requested` events with correlated IDs, responses arrive independently. This is the correct protocol design, not a simplification to be "upgraded" later. QUIC's stream multiplexing already provides the concurrency and ordering guarantees that batch would need. Batch-specific event types (e.g., `batch.requested`, `batch.responded`) would add protocol complexity without clear benefit over sending multiple `call.requested` events. If a compelling use case for atomic batch semantics emerges, it can be added as a new event type without breaking existing clients. Two-way door.
|
||||
- **Cross-references**: ADR-012
|
||||
|
||||
## Theme: alknet-call
|
||||
|
||||
### OQ-15: Call Protocol Client and Adapter Contract
|
||||
|
||||
- **Origin**: [call-protocol.md](crates/call/call-protocol.md), [operation-registry.md](crates/call/operation-registry.md), ADR-013
|
||||
- **Status**: open
|
||||
- **Door type**: One-way
|
||||
- **Priority**: high
|
||||
- **Resolution**: alknet-call currently specifies only the server side (CallAdapter receives connections and dispatches to the operation registry). A call protocol client is needed for: (1) alknet-napi to expose remote invocation to Node.js, (2) alknet-agent to dispatch tool calls (call, batch, search, schema) to remote nodes, (3) the `from_call` adapter pattern that creates operations whose handlers invoke remote services. The adapter contract (from_openapi, from_mcp, from_call, to_openapi, to_mcp) determines how external specifications and protocols compose with the operation registry. These traits belong in alknet-call because they define how operations are produced and consumed — the same contract that enables an agent to register call/batch/search/schema as tools also enables from_openapi to register HTTP-backed operations. The TypeScript `@alkdev/operations` library demonstrated these patterns; the Rust implementation defines the canonical traits (ADR-013). Two-way door for the specific trait signatures, one-way door for the architectural commitment that the adapter contract lives in alknet-call.
|
||||
- **Cross-references**: ADR-005, ADR-013, [call-protocol.md](crates/call/call-protocol.md), [operation-registry.md](crates/call/operation-registry.md)
|
||||
@@ -36,6 +36,16 @@ alknet-core
|
||||
│
|
||||
├── alknet-ssh (depends on alknet-core, russh)
|
||||
├── alknet-call (depends on alknet-core, irpc)
|
||||
│ ├── CallAdapter (server: ProtocolHandler for alknet/call)
|
||||
│ ├── Call client (send/receive over QUIC)
|
||||
│ ├── OperationSpec, OperationRegistry, AccessControl
|
||||
│ └── Adapter traits (from_*, to_*)
|
||||
│
|
||||
├── alknet-agent (depends on alknet-call)
|
||||
│ ├── LLM execution loop (forked aisdk, simplified)
|
||||
│ ├── Tool dispatch via call protocol
|
||||
│ └── Provider key retrieval via vault (no env vars)
|
||||
│
|
||||
├── alknet-git (depends on alknet-core, gix)
|
||||
├── alknet-sftp (depends on alknet-core, russh-sftp)
|
||||
├── alknet-msg (depends on alknet-core)
|
||||
@@ -43,6 +53,7 @@ alknet-core
|
||||
├── alknet-dns (depends on alknet-core, hickory-proto)
|
||||
│
|
||||
├── alknet-napi (depends on alknet-call, napi-rs)
|
||||
│ └── Thin NAPI projection of call protocol client to Node.js
|
||||
│
|
||||
└── alknet (CLI binary, depends on all handler crates + alknet-vault)
|
||||
```
|
||||
@@ -51,8 +62,10 @@ Dependency rules:
|
||||
- No handler crate depends on another handler crate
|
||||
- All handler crates depend on alknet-core
|
||||
- alknet-vault has zero alknet crate dependencies
|
||||
- alknet-napi depends only on alknet-call (call protocol client)
|
||||
- alknet-agent depends on alknet-call (not alknet-core) — it uses the call protocol client for tool dispatch
|
||||
- alknet-napi depends only on alknet-call — thin NAPI projection, no business logic
|
||||
- alknet (CLI) is the only crate that depends on all handler crates and alknet-vault
|
||||
- Rust is the canonical implementation language — TypeScript is a reference/browser adaptation, not a parallel implementation (see ADR-013)
|
||||
|
||||
See [ADR-003](decisions/003-crate-decomposition.md) for the full decomposition rationale.
|
||||
|
||||
@@ -83,6 +96,7 @@ See [ADR-002](decisions/002-protocol-handler-trait.md) and [ADR-007](decisions/0
|
||||
|------|---------|-------------|
|
||||
| `alknet/ssh` | SshAdapter | SSH-2 handshake, channel multiplexing, SOCKS5, port forwarding |
|
||||
| `alknet/call` | CallAdapter | JSON-RPC via irpc: operations, streaming, pub/sub |
|
||||
| `alknet/agent` | AgentAdapter | LLM agent service: tool dispatch via call protocol, provider key retrieval via vault |
|
||||
| `alknet/git` | GitAdapter | Git smart protocol over QUIC (gix, pkt-line) |
|
||||
| `alknet/sftp` | SftpAdapter | SFTP protocol (russh-sftp core) |
|
||||
| `alknet/msg` | MessageAdapter | E2E encrypted messaging, mixnet |
|
||||
@@ -112,7 +126,7 @@ See [ADR-004](decisions/004-auth-as-shared-core.md) for the full rationale.
|
||||
|
||||
alknet-call uses irpc as its foundation. The wire format is length-prefixed JSON (EventEnvelope framing). Operations are registered in an irpc registry with JSON Schema discovery. The call protocol supports request/response, streaming subscriptions, and pub/sub.
|
||||
|
||||
The call protocol's TypeScript predecessor can import OpenAPI schemas and expose operations as HTTP endpoints or MCP tools. This bidirectional capability carries forward.
|
||||
The call protocol's adapter contract (from_openapi, from_mcp, from_call, to_openapi, to_mcp) enables bidirectional composition — operations can be imported from external sources and exported to external protocols. These adapter traits are defined in Rust in alknet-call. The existing TypeScript `@alkdev/operations` library informed the design and may be adapted for browser use (see ADR-013).
|
||||
|
||||
See [ADR-005](decisions/005-irpc-as-call-protocol-foundation.md) for the full rationale.
|
||||
|
||||
@@ -124,15 +138,16 @@ This means:
|
||||
- Core types (BiStream, Connection, ProtocolHandler, AuthContext) must not assume tokio or quinn
|
||||
- Protocol parsers that are pure data transformations remain transport-agnostic
|
||||
- The cost of keeping the WASM door open is low (trait vs concrete type, abstracted I/O) and the cost of closing it is high
|
||||
- The call protocol's wire format (length-prefixed JSON EventEnvelope) is inherently cross-language and WASM-friendly
|
||||
|
||||
Handlers with transport-agnostic cores are particularly WASM-friendly:
|
||||
The browser path is through a JavaScript SDK adapted from the existing TypeScript `@alkdev/operations` library, speaking the EventEnvelope wire format over WebTransport streams — not through Rust-to-WASM compilation of the full stack (see ADR-013). A browser gets a WebTransport stream and speaks the call protocol directly.
|
||||
|
||||
Handlers with protocol-agnostic cores are particularly WASM-friendly:
|
||||
- russh-sftp's protocol core is already transport-agnostic
|
||||
- hickory-proto is `#![no_std]` with `wasm-bindgen` feature
|
||||
- The call protocol's JSON framing is inherently cross-language
|
||||
- Git's pkt-line is simple enough to implement anywhere
|
||||
|
||||
A browser gets a WebTransport stream and speaks SFTP, Git, or call protocol directly — but only if we haven't closed that door with concrete type choices.
|
||||
|
||||
## Shared Types
|
||||
|
||||
The following types live in alknet-core and are used across handler crates:
|
||||
|
||||
Reference in New Issue
Block a user