Files
alknet/tasks/integration/phase2/ssh-session-call-protocol-bridge.md

5.5 KiB

id, name, status, depends_on, scope, risk, impact, level
id name status depends_on scope risk impact level
ssh-session-call-protocol-bridge Bridge SshSession recv/send to call protocol via alknet-control:0 channel completed
stream-interface-message-interface-split
moderate medium component implementation

Description

Implement SshSession::recv() and SshSession::send() to bridge SSH channel data to and from the call protocol's InterfaceEvent/EventEnvelope frames. Currently both methods are stubs: recv() always returns None and send() silently discards.

Per the integration plan section 2.1 and interface.md (OQ-IF-01, resolved):

The bridge works as follows:

  • When SshHandler::channel_open_direct_tcpip detects a destination starting with alknet-, it currently accepts the channel but doesn't bridge the data. The ControlChannelRouter exists in control_channel.rs but has no handler wired.
  • SshSession::recv() should read EventEnvelope frames from the alknet-control:0 channel stream (using the 4-byte length prefix + JSON wire format from call::frame::{encode, decode}), wrap them in InterfaceEvent with the session's Identity (obtained during SSH auth).
  • SshSession::send() should write EventEnvelope frames to the alknet-control:0 channel stream using the same framing format.
  • The ControlChannelRouter should be wired to bridge incoming channel data to the call protocol handler.

Current state of the code:

  • SshSession::recv() returns None (stub)
  • SshSession::send() discards silently (stub)
  • ControlChannelRouter in control_channel.rs has route() and has_handler() but no handler is registered
  • call::frame::{encode, decode} functions exist and are well-tested (4-byte BE length prefix + JSON)
  • SshHandler detects alknet-* destinations in channel_open_direct_tcpip but doesn't bridge data
  • SshHandler stores authenticated_identity: Option<Identity> from SSH auth
  • InterfaceEvent struct carries EventEnvelope + Option<Identity> — already defined

Key design considerations:

  • The SshSession needs access to the SSH channel's data stream to read/write EventEnvelope frames. This requires getting the russh::Channel data stream and framing it.
  • The ControlChannelRouter currently uses Box<dyn DuplexStream> — it can be wired as a ControlChannelHandler that reads frames from the stream and produces InterfaceEvents.
  • The alknet-control:0 channel is the first SSH direct-tcpip channel with the alknet-control destination. Additional alknet-* channels may follow.
  • The session's Identity (from SSH auth) must be attached to every InterfaceEvent produced by recv().

Acceptance Criteria

  • SshSession::recv() reads EventEnvelope frames from the SSH channel data stream and produces InterfaceEvent with the session's Identity
  • SshSession::send() writes EventEnvelope frames to the SSH channel data stream using call::frame::encode
  • ControlChannelRouter is wired as the handler for alknet-control:0 channels, bridging SSH channel data to the call protocol
  • Frame encoding matches call::frame::{encode, decode} — 4-byte big-endian length prefix + UTF-8 JSON body
  • The session's Identity (from SshHandler::authenticated_identity) is attached to every InterfaceEvent produced by recv()
  • SshHandler::channel_open_direct_tcpip correctly routes alknet-control:0 channels to the ControlChannelRouter handler
  • Unit test: SshSession can round-trip an EventEnvelope through send() and recv() (using a mock channel stream)
  • Unit test: ControlChannelRouter.with_handler() successfully routes channel data
  • All existing server/auth/transport tests continue to pass
  • No behavioral changes for non-alknet-* channel forwarding (port proxy logic unchanged)

References

  • docs/research/integration-plan.md — Phase 2.1
  • docs/architecture/interface.md — OQ-IF-01 resolution, InterfaceEvent model
  • docs/architecture/call-protocol.md — EventEnvelope, frame encoding
  • crates/alknet-core/src/interface/ssh.rs — SshSession stubs (recv/send)
  • crates/alknet-core/src/server/control_channel.rs — ControlChannelRouter
  • crates/alknet-core/src/call/frame.rs — frame encode/decode
  • crates/alknet-core/src/interface/session.rs — InterfaceEvent, InterfaceSession traits

Notes

This is the highest-risk task in Phase 2. The russh channel data stream API needs careful handling — getting a Channel's data stream for async reading/writing is non-trivial and may require understanding russh's data() callback pattern vs. the Channel::into_stream() method.

Consider implementing incrementally: first wire the ControlChannelRouter handler to produce InterfaceEvents from raw channel data, then connect that to SshSession::recv()/send(). Each step should have passing tests before proceeding.

The SshSession struct currently holds a server::Handle and a JoinHandle. It may need additional fields to track the control channel stream and the authenticated identity for producing InterfaceEvents with identity attached.

Summary

Implemented SshSession recv/send bridge to call protocol via alknet-control:0 channel. Added FrameFramedReader/FrameFramedWriter for async length-prefixed EventEnvelope I/O. SshSession::recv() reads InterfaceEvents from mpsc channel bridged from SSH channel. SshSession::send() writes EventEnvelopes to mpsc channel bridged to SSH channel. ControlChannelBridge implements ControlChannelHandler. SshHandler routes alknet-control:0 channels to bridge task using tokio::select!. Session Identity attached to every InterfaceEvent.