Files
alknet/tasks/integration/phase2/raw-framing-interface-implementation.md

75 lines
5.5 KiB
Markdown

---
id: raw-framing-interface-implementation
name: Implement RawFramingInterface accept/recv/send with first-frame auth
status: completed
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
> Implemented RawFramingInterface::accept() and RawFramingSession with first-frame auth. RawFramingConfig now has auth: Arc<dyn IdentityProvider>. RawFramingSession splits stream via tokio::io::split, recv() implements first-frame auth with AuthToken resolution via IdentityProvider::resolve_from_token(), send() encodes via call::frame::encode with buffered writes, read_frame() uses decode_with_remainder for partial reassembly. 7 new tests.