Files
alknet/docs/architecture/decisions/035-streaminterface-messageinterface-split.md
glm-5.1 cfc44008d3 Sync architecture specs with Phase 2 research findings
- 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)
2026-06-09 08:09:45 +00:00

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:

  1. StreamInterface — consumes a TransportStream, produces a long-lived Session that yields InterfaceEvent frames. Existing SshInterface and RawFramingInterface become StreamInterface implementations.

  2. MessageInterface — handles individual InterfaceRequestInterfaceResponse pairs. Manages its own transport (HTTP server, DNS server). HttpInterface and DnsInterface are MessageInterface implementations.

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 InterfaceRequestInterfaceResponse 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