- Add definitions.md: normative terminology disambiguation (Interface, Service, Transport, Token, Identity, Domain, Scope, CredentialProvider, etc.) - Add credentials.md: CredentialProvider trait and CredentialSet enum for outbound auth, mirroring IdentityProvider pattern for inbound auth - Rewrite interface.md: StreamInterface/MessageInterface split (ADR-035), InterfaceRequest/InterfaceResponse, HttpInterface/DnsInterface stubs, ListenerConfig with Stream/Http/Dns variants, credential presentation table - Update auth.md: API keys in DynamicConfig (ADR-037), credential presentation per (Transport, Interface) pair, ApiKeyEntry struct in AuthPolicy - Update configuration.md: API keys, ListenerConfig with Http/Dns variants, expanded TOML config examples - Update call-protocol.md: resolve OQ-IF-01 (InterfaceEvent carries EventEnvelope + Identity), add MessageInterface awareness to protocol adapter layer - Update overview.md: three-layer model now includes StreamInterface/ MessageInterface, CredentialProvider/CredentialSet exports, definitions.md reference, ADRs 035-037 - Update open-questions.md: resolve OQ-IF-01, OQ-IF-02, add OQ-P2-01 through OQ-P2-04, add OQ-CP-01 through OQ-CP-04, add OQ-DEF-01, OQ-DEF-03, OQ-DEF-08 - Update README.md: add definitions.md, credentials.md, ADRs 035-037, phase2 research docs, current state description Key architectural decisions: - ADR-035: StreamInterface/MessageInterface split (two Layer 2 traits) - ADR-036: CredentialProvider as core type (outbound auth, alknet_core::credentials) - ADR-037: API keys as DynamicConfig auth (hash-verified bearer tokens)
4.0 KiB
ADR-035: StreamInterface and MessageInterface Split
Status
Accepted
Context
The Interface trait (ADR-026) assumes a persistent byte stream from a Transport. It produces a Session that yields InterfaceEvent frames. This works for SSH and raw framing — both run over duplex streams.
However, HTTP and DNS do not fit this model. They handle individual request/response pairs, not persistent sessions. HTTP runs over a TLS connection after byte-peek protocol detection (extending the existing stealth mode pattern). DNS runs its own server on port 53. Both are stateless per-request, not session-oriented.
The three-layer model (Transport, Interface, Protocol) remains correct. The issue is that Layer 2 has two distinct patterns: stream-based (SSH, raw framing) where the transport provides a continuous byte stream, and message-based (HTTP, DNS) where the interface manages its own transport and handles discrete requests.
Decision
Split the Interface trait into two independent traits:
-
StreamInterface— consumes aTransportStream, produces a long-livedSessionthat yieldsInterfaceEventframes. ExistingSshInterfaceandRawFramingInterfacebecomeStreamInterfaceimplementations. -
MessageInterface— handles individualInterfaceRequest→InterfaceResponsepairs. Manages its own transport (HTTP server, DNS server).HttpInterfaceandDnsInterfaceareMessageInterfaceimplementations.
The traits are independent. They have different signatures (accept(stream) vs handle_request(req)), different lifecycles (long-lived session vs stateless per-request), and different transport ownership (provided by caller vs self-managed).
ListenerConfig gains variants for both:
pub enum ListenerConfig {
Stream {
transport: TransportKind,
interface: StreamInterfaceKind,
},
Http {
bind_addr: SocketAddr,
tls: bool,
stealth: bool,
},
Dns {
bind_addr: SocketAddr,
tls: bool,
},
}
TransportKind::Dns is removed. DNS is a MessageInterface that manages its own transport (UDP/TCP port 53), not a transport variant.
The call protocol handler (Layer 3) is interface-agnostic: it processes InterfaceEvent frames from StreamInterface sessions and InterfaceRequest → InterfaceResponse from MessageInterface handlers. The dispatch logic is the same — only the framing differs.
Consequences
Positive: HTTP and DNS are first-class interfaces with proper type signatures. No forcing stateless protocols into a session model. The existing stealth mode byte-peek pattern naturally extends to HttpInterface. The InterfaceRequest / InterfaceResponse types normalize calls across message-based interfaces.
Positive: Removing TransportKind::Dns prevents a breaking change later — code should never depend on DNS as a transport variant.
Positive: ListenerConfig correctly models the server's accept loop: stream listeners spawn one accept loop per (transport, interface) pair, while HTTP and DNS listeners each manage their own server.
Negative: Two traits where there was one. But they serve fundamentally different purposes. A common super-trait would add complexity (accept_stream + handle_request + transport_kind) without practical benefit — implementations satisfy one trait or the other, never both.
Negative: The accept() method on the current Interface trait needs to be renamed. This is a rename of an existing method signature, not a semantic change — SshInterface and RawFramingInterface implementations become StreamInterface implementations with the same accept() logic.
References
- ADR-026 (transport/interface separation — updated by this ADR)
- interface.md — Interface layer spec
- research/phase2/interface-model.md — Full analysis
- research/phase2/tls-transport.md — HTTP interface, ListenerConfig