Decompose Phase 2 (Core Bridge) into 8 dependency-ordered tasks

Phase 2 completes the interface-to-protocol bridge and adds core types
that external crates depend on. The 8 tasks are organized into 5
generations with clear dependencies:

- Gen 1: StreamInterface/MessageInterface trait split (must go first)
- Gen 2: SshSession bridge, RawFraming impl, CredentialProvider (parallel)
- Gen 3: API keys in DynamicConfig (depends on CredentialProvider)
- Gen 4: ListenerConfig HTTP/DNS stubs + axum scaffold
- Gen 5: Review gate before Phase 3

Key design decisions:
- 2.4a/2.4b split: SecretStoreCredentialProvider deferred to Phase 3
- API keys (2.6) must land before axum scaffold (2.7)
- ListenerConfig (2.5) must land before axum scaffold (2.7)
- Gen 2 tasks are parallelizable (separate modules)
This commit is contained in:
2026-06-09 09:33:22 +00:00
parent 8f24fa6c09
commit aafee72f4c
8 changed files with 565 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
---
id: raw-framing-interface-implementation
name: Implement RawFramingInterface accept/recv/send with first-frame auth
status: pending
depends_on: [stream-interface-message-interface-split]
scope: narrow
risk: low
impact: component
level: implementation
---
## Description
Implement `RawFramingInterface` and `RawFramingSession` to handle length-prefixed `EventEnvelope` frames over a byte stream, with first-frame authentication. Currently `RawFramingInterface::accept()` returns an error and `RawFramingSession` stubs exist.
Per the integration plan section 2.2 and interface.md:
**RawFramingInterface**: Reads 4-byte length-prefixed JSON `EventEnvelope` frames from a transport stream (TCP, TLS, iroh, etc.). No SSH wrapping — the raw framing interface carries call protocol events directly.
**First-frame auth**: The first `InterfaceEvent` on a `RawFramingSession` carries an auth token in the `InterfaceEvent.identity` field or a dedicated auth event type. After `IdentityProvider::resolve_from_token()` verifies the token and produces an `Identity`, the session is authenticated. Subsequent frames are call protocol `EventEnvelope` data. If auth fails, the session is terminated immediately.
**Current state of the code**:
- `RawFramingInterface` accepts any `TransportStream` but returns an error
- `RawFramingSession` is an empty struct with stub `recv()` (returns `None`) and `send()` (returns error)
- `call::frame::{encode, decode, decode_with_remainder}` already implement the wire format
- `IdentityProvider::resolve_from_token()` exists but is not yet wired to `AuthToken` verification (that's coming in the API keys task)
**Implementation approach**:
1. `RawFramingInterface::accept()` takes a `TransportStream`, wraps it in a `BufReader` for buffered reading, stores it in `RawFramingSession`. The `RawFramingSession` is created in an "unauthenticated" state.
2. `RawFramingSession::recv()` reads frames from the stream:
- If unauthenticated: read the first frame, extract the auth token, call `IdentityProvider::resolve_from_token()`. On success, transition to "authenticated" with the resolved `Identity`. On failure, return an error (session terminated).
- If authenticated: read `EventEnvelope` frames, wrap in `InterfaceEvent::with_identity(envelope, identity)`.
3. `RawFramingSession::send()` writes `EventEnvelope` frames to the stream using `call::frame::encode`.
The `RawFramingSession` needs:
- A `BufReader<Box<dyn TransportStream>>` for reading framed data
- A `Box<dyn TransportStream>` (or WriteHalf) for writing framed data
- An `Option<Identity>` tracking auth state
- A reference to `IdentityProvider` for token resolution
- A buffer for partial frame reads (`decode_with_remainder` pattern)
## Acceptance Criteria
- [ ] `RawFramingInterface::accept()` takes a `TransportStream` and `StreamInterfaceConfig::RawFraming` config, creates a `RawFramingSession` wrapping the stream
- [ ] `RawFramingSession` holds a buffered reader and writer over the transport stream, an auth state (`Option<Identity>`), and a reference to `IdentityProvider`
- [ ] `RawFramingSession::recv()` reads length-prefixed `EventEnvelope` frames from the stream using `call::frame::decode_with_remainder`
- [ ] First-frame auth: the first `recv()` call resolves the auth token via `IdentityProvider::resolve_from_token()` and stores the resulting `Identity`
- [ ] Subsequent `recv()` calls produce `InterfaceEvent::with_identity(envelope, identity)` using the authenticated identity
- [ ] Auth failure terminates the session: `recv()` returns an error result on bad tokens
- [ ] `RawFramingSession::send()` writes `EventEnvelope` frames to the stream using `call::frame::encode`
- [ ] Unit test: `RawFramingInterface::accept()` succeeds with a valid stream
- [ ] Unit test: `RawFramingSession` round-trips an `EventEnvelope` through `send()` and `recv()` (after mock auth)
- [ ] Unit test: First-frame auth with a valid token transitions to authenticated state
- [ ] Unit test: First-frame auth with an invalid token returns an error
- [ ] Integration test: `RawFramingSession` over a `tokio::io::duplex` stream (simulated TCP) sends and receives multiple frames
## References
- docs/research/integration-plan.md — Phase 2.2
- docs/architecture/interface.md — RawFramingInterface, first-frame auth model
- crates/alknet-core/src/interface/raw_framing.rs — Current stubs
- crates/alknet-core/src/call/frame.rs — Frame encode/decode
- crates/alknet-core/src/auth/identity.rs — IdentityProvider, resolve_from_token
## Notes
> The frame format is already implemented and tested in `call::frame`. This task is primarily about wiring the frame reader/writer to the `InterfaceSession` trait and adding first-frame auth logic.
> Consider using `tokio::io::BufReader` for buffered reading and `tokio::io::BufWriter` for buffered writing. The `decode_with_remainder` function handles partial reads by returning how many bytes were consumed — the session needs to maintain a read buffer for reassembly.
> The `RawFramingInterface` config should include an `Arc<dyn IdentityProvider>` for first-frame auth. This follows the same pattern as `SshInterfaceConfig`.
## Summary
> To be filled on completion