Governance (Tier 2):
- Advance ADR-022 and ADR-023 from Proposed to Accepted (specs already
depend on their types as source of truth)
- Amend ADR-015: mark Decision 3 and Assumption 6 as superseded by ADR-022;
update handler_identity type to CompositionAuthority
- Amend ADR-002: note handle() signature revised by ADR-007 (BiStream → Connection)
- Amend ADR-004: note 'enrich/replace' AuthContext language superseded by
ADR-011's immutability model; update to describe set_identity on Connection
- Update main README ADR table to show ADR-022/023 as Accepted
Spec-ADR consistency (Tier 3):
- Add abort_policy: AbortPolicy field to OperationContext struct (ADR-016
Decision 6 mandated this but the spec omitted it)
- Define AbortPolicy enum (AbortDependents | ContinueRunning) with Default impl
- Add abort_policy to build_root_context and LocalOperationEnv::invoke()
- Define the OperationEnv trait explicitly with invoke() and
invoke_with_policy() methods (was referenced as 'must remain a trait'
but never defined)
- Specify From<StreamError> for HandlerError impl with exact variant mapping
- Add Connection::from_quinn() / from_iroh() constructors (was referenced
as Connection::new() but never defined)
- Remove undefined CertAuthorityEntry placeholder from AuthPolicy v1 (will
be added additively when alknet-ssh lands)
- Fix config.md key-differences table: rate limits are in DynamicConfig,
not StaticConfig
Mechanical fixes (Tier 1):
- overview.md: 'closes the QUIC stream' → 'closes the connection' (stale
from pre-ADR-007 model)
- overview.md: OQ-04 entry updated from stale 'defer to implementation'
to 'resolved: static at startup'
- mnemonic-derivation.md: remove duplicate helper functions block (incomplete
first copy, complete second copy)
- ADR-003: add iroh (feature-gated) to alknet-core dependency list, added
by ADR-010
- ADR-021: fix ambiguous 'W1 drift issue from the vault review' cross-reference
- ADR-022: rephrase FromCall 'leaf locally' to 'leaf in the local registry'
- ADR-017: add error_schemas to from_call mirror list and services/schema
step (inconsistency with ADR-023)
- ADR-016: fix self-referential citation ('ADR-016 Assumption 5' → 'Assumption 5')
- Add ScopedOperationEnv::empty(), allows(), new() and
CompositionAuthority::none(), new() impl blocks (referenced but undefined)
- Add call.completed clarification for non-subscription calls
- Add services/schema leading-slash normalization note
- Crate README ADR tables: add missing ADR-013 (call), ADR-015 (core),
ADR-006 + ADR-010 (vault)
- Vault README: add consolidated 'Known Source Drift' table tracking all
four drift items (OsRng, unwrap, CURRENT_KEY_VERSION, spawn bug) in one
place, including the two previously missing from README
66 lines
4.2 KiB
Markdown
66 lines
4.2 KiB
Markdown
# ADR-002: ProtocolHandler Trait
|
|
|
|
## Status
|
|
|
|
Accepted
|
|
|
|
## Context
|
|
|
|
The previous architecture had two separate interface traits: `StreamInterface` (for byte-stream protocols like SSH, raw TCP) and `MessageInterface` (for message-based protocols like DNS, HTTP). This split created complexity — each interface type needed its own listener configuration, its own dispatch path, and its own framing assumptions. The `ListenerConfig` enum had three variants. The server accept loop handled three different listener types.
|
|
|
|
In practice, the distinction between "stream" and "message" protocols is artificial at the handler level. SSH starts as a byte stream but internally multiplexes channels and messages. DNS over QUIC is message-based but arrives as a stream of frames. HTTP/2 is both — bidirectional streams with message semantics. Every protocol can be modeled as "receive a byte stream, manage your own wire format."
|
|
|
|
iroh's `ProtocolHandler` trait demonstrates this: it takes a bidirectional QUIC stream and the handler is responsible for its own protocol. One trait, one dispatch point.
|
|
|
|
## Decision
|
|
|
|
A single `ProtocolHandler` trait replaces both `StreamInterface` and `MessageInterface`:
|
|
|
|
> **Note**: The signature below was revised by ADR-007. The `handle()` method
|
|
> now receives a `Connection` (not a `BiStream`) — see ADR-007 for the
|
|
> current authoritative signature. The original signature is retained here
|
|
> for historical context.
|
|
|
|
```rust
|
|
#[async_trait]
|
|
pub trait ProtocolHandler: Send + Sync + 'static {
|
|
/// The ALPN string this handler claims (e.g. b"alknet/ssh")
|
|
fn alpn(&self) -> &'static [u8];
|
|
|
|
/// Handle an incoming connection (revised by ADR-007 to receive
|
|
/// `Connection` instead of `BiStream`)
|
|
async fn handle(&self, connection: Connection, auth: &AuthContext) -> Result<(), HandlerError>;
|
|
}
|
|
```
|
|
|
|
- `alpn()` returns a static byte string — the handler's ALPN identifier
|
|
- `handle()` receives a `Connection` (revised by ADR-007 from the original
|
|
`BiStream`) and an `AuthContext` carrying the authenticated identity, and
|
|
returns `HandlerError` on failure
|
|
- Every handler manages its own wire format — no shared framing, no StreamInterface/MessageInterface split
|
|
- The `ListenerConfig` enum is eliminated — ALPN advertisement configuration replaces it
|
|
|
|
**AuthContext resolution is hybrid** (see ADR-004, OQ-02 resolution): the endpoint resolves what it can before calling `handle()` (e.g., TLS client certificate fingerprint), and the handler resolves what it must inside `handle()` (e.g., AuthToken in the first frame of a call stream). The `AuthContext` passed to `handle()` may contain partial identity information — the handler is responsible for completing authentication if the endpoint didn't have enough information.
|
|
|
|
## Consequences
|
|
|
|
**Positive:**
|
|
- One trait, one dispatch point — eliminates the StreamInterface/MessageInterface split and ListenerConfig enum
|
|
- Each handler owns its wire format — no shared framing assumptions that constrain protocol design
|
|
- Adding a new protocol is implementing one trait with two methods
|
|
- Testable in isolation — give a handler a mock BiStream and AuthContext
|
|
- WASM-compatible in principle — handlers that don't need tokio runtime features compile to WASM
|
|
|
|
**Negative:**
|
|
- Every handler must implement its own framing — no shared "read a length-prefixed message" utility (mitigated: common utilities can live in alknet-core without mandating their use)
|
|
- Handlers that want message semantics must build them (mitigated: alknet-call provides this as a handler, not a mandatory layer)
|
|
- AuthContext resolution is hybrid — the endpoint resolves what it can (TLS-level auth), but handlers that need protocol-level credential extraction must do so inside handle(). This means AuthContext may be partial when handle() is called. Handlers must not assume AuthContext is fully resolved.
|
|
|
|
## References
|
|
|
|
- Pivot proposal: `docs/research/pivot/alpn-service-architecture.md`
|
|
- ADR-001: ALPN-based protocol dispatch
|
|
- ADR-004: Auth as shared core (IdentityProvider)
|
|
- ADR-007: BiStream type definition (revised this ADR's signature from BiStream to Connection)
|
|
- iroh ProtocolHandler pattern: `docs/research/references/iroh/`
|
|
- Replaces StreamInterface, MessageInterface, and ListenerConfig |