feat(agent,server): v2 secure-session-core Task 7 - HW H.264 + negotiated raw fallback
All checks were successful
Build and Test / Build Agent (Windows) (push) Successful in 6m57s
Build and Test / Build Server (Linux) (push) Successful in 10m23s
Build and Test / Security Audit (push) Successful in 4m15s
Build and Test / Build Summary (push) Successful in 9s

SPEC-002 Phase 1 Task 7 (the last), code-reviewed APPROVED, locally verified
(cargo fmt + clippy -D warnings exit 0 + cargo test --workspace 89 pass + build).

- Encoder trait + factory: RawEncoder (salvaged, UNCHANGED) and H264Encoder,
  selected by negotiation; factory falls back to raw on H.264 init failure.
- Negotiation: agent advertises supports_h264 (MFTEnumEx HW probe, cached) in
  AgentStatus; server picks the codec via select_video_codec(supports, prefer)
  and stamps StartStream.video_codec; agent re-guards on local HW. Policy
  constant DEFAULT_PREFER_H264 = false, so RAW is negotiated for every session
  today - H.264 stays dormant until live hardware validation (Task 8).
- MF H.264 encoder (h264.rs, FIRST-CUT / compile-verified-only): HW encoder MFT,
  BGRA->NV12 (color.rs, unit-tested), sync drain, fall-back-to-raw on any failure.
- Viewer H.264 decoder (decoder.rs, FIRST-CUT): MF decoder on a dedicated COM
  thread; drops+logs on failure, raw render path untouched.
- proto additive: VideoCodec enum, StartStream.video_codec=3,
  SessionResponse.video_codec=5, AgentStatus.supports_h264=11.
- Raw+Zstd path byte-for-byte unchanged; remains the guaranteed default/fallback.

Review confirmed unsafe impl Send for H264Encoder is sound (single-owned &mut on
the block_on thread; session future never spawned) and every MF failure degrades
to raw. H.264 is NOT claimed functional - compile/clippy/build-verified only;
live validation + force-IDR + the no-spawn-invariant doc are Task 8 go-live gates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 10:35:04 -07:00
parent bb73ba667f
commit f9bdecbfdb
12 changed files with 1885 additions and 23 deletions

View File

@@ -805,6 +805,7 @@ async fn handle_agent_connection(
organization.clone(),
site.clone(),
status.tags.clone(),
status.supports_h264,
)
.await;

View File

@@ -93,6 +93,36 @@ pub struct Session {
pub organization: Option<String>, // Company/organization name
pub site: Option<String>, // Site/location name
pub tags: Vec<String>, // Tags for categorization
/// Whether the agent advertised a hardware H.264 encoder (Task 7). Set from
/// `AgentStatus.supports_h264`; drives codec negotiation in `select_video_codec`.
pub supports_h264: bool,
}
/// Default codec-negotiation policy (Task 7).
///
/// `false` means: even when an agent advertises hardware H.264 support, the
/// server still negotiates RAW. H.264 is compile-verified only and not yet
/// validated on real hardware (plan Task 8), so we deliberately do NOT ship it
/// as the default — raw+Zstd stays the guaranteed working path. Flip this to
/// `true` once H.264 is live-validated, or make it per-tenant policy later.
pub const DEFAULT_PREFER_H264: bool = false;
/// Negotiate the video codec for a stream (Task 7).
///
/// Pure decision function (unit-tested): given whether the agent advertised
/// hardware H.264 and whether policy prefers H.264, pick the codec. H.264 is
/// chosen ONLY when both the agent supports it AND policy allows it; otherwise
/// raw — the safe default/fallback. HEVC is intentionally never selected here
/// (future opt-in; TODO).
pub fn select_video_codec(
agent_supports_h264: bool,
prefer_h264: bool,
) -> crate::proto::VideoCodec {
if agent_supports_h264 && prefer_h264 {
crate::proto::VideoCodec::H264
} else {
crate::proto::VideoCodec::Raw
}
}
/// Channel for sending frames from agent to viewers
@@ -206,6 +236,7 @@ impl SessionManager {
organization: None,
site: None,
tags: Vec::new(),
supports_h264: false,
};
let session_data = SessionData {
@@ -240,12 +271,14 @@ impl SessionManager {
organization: Option<String>,
site: Option<String>,
tags: Vec<String>,
supports_h264: bool,
) {
let mut sessions = self.sessions.write().await;
if let Some(session_data) = sessions.get_mut(&session_id) {
session_data.info.last_heartbeat = chrono::Utc::now();
session_data.last_heartbeat_instant = Instant::now();
session_data.info.is_streaming = is_streaming;
session_data.info.supports_h264 = supports_h264;
if let Some(os) = os_version {
session_data.info.os_version = Some(os);
}
@@ -409,10 +442,23 @@ impl SessionManager {
use crate::proto;
use prost::Message;
// Negotiate the video codec for this stream (Task 7): H.264 only when the
// agent advertised hardware support AND policy prefers it. With
// DEFAULT_PREFER_H264 = false this always resolves to RAW today (H.264 is
// compile-verified only, validated on hardware in Task 8).
let codec = select_video_codec(session_data.info.supports_h264, DEFAULT_PREFER_H264);
tracing::info!(
"StartStream codec negotiation: agent_supports_h264={}, prefer_h264={} -> {:?}",
session_data.info.supports_h264,
DEFAULT_PREFER_H264,
codec
);
let start_stream = proto::Message {
payload: Some(proto::message::Payload::StartStream(proto::StartStream {
viewer_id: viewer_id.to_string(),
display_id: 0, // Primary display
video_codec: codec as i32,
})),
};
@@ -618,6 +664,7 @@ impl SessionManager {
organization: None,
site: None,
tags: Vec::new(),
supports_h264: false,
};
// Create placeholder channels (will be replaced on reconnect)
@@ -717,6 +764,69 @@ mod tests {
.is_some());
}
#[test]
fn codec_negotiation_picks_h264_only_when_supported_and_preferred() {
use crate::proto::VideoCodec;
// Agent supports H.264 AND policy prefers it -> H.264.
assert_eq!(select_video_codec(true, true), VideoCodec::H264);
// Agent supports it but policy does not prefer it -> raw (the safe default).
assert_eq!(select_video_codec(true, false), VideoCodec::Raw);
// Policy prefers H.264 but the agent has no HW encoder -> raw.
assert_eq!(select_video_codec(false, true), VideoCodec::Raw);
// Neither -> raw.
assert_eq!(select_video_codec(false, false), VideoCodec::Raw);
}
#[test]
fn default_policy_does_not_prefer_h264() {
// Guardrail: until H.264 is hardware-validated (Task 8) the default policy
// MUST keep raw as the negotiated codec even for capable agents. We assert
// the OBSERVABLE behavior (codec selection under the default policy) rather
// than the constant directly, which keeps the test meaningful if the policy
// later becomes dynamic.
let chosen = select_video_codec(true, DEFAULT_PREFER_H264);
assert_eq!(
chosen,
crate::proto::VideoCodec::Raw,
"default policy must negotiate raw until H.264 is hardware-validated"
);
}
#[tokio::test]
async fn agent_status_updates_h264_capability() {
let mgr = SessionManager::new();
let (session_id, _frame_tx, _input_rx) = mgr
.register_agent("agent-cap".to_string(), "Cap PC".to_string(), true)
.await;
// Default is false until a status reports capability.
assert_eq!(
mgr.get_session(session_id).await.map(|s| s.supports_h264),
Some(false)
);
mgr.update_agent_status(
session_id,
Some("Windows".to_string()),
true,
10,
1,
false,
Some("0.2.0".to_string()),
None,
None,
Vec::new(),
true, // supports_h264
)
.await;
assert_eq!(
mgr.get_session(session_id).await.map(|s| s.supports_h264),
Some(true)
);
}
#[tokio::test]
async fn denied_attended_session_keeps_viewer_blocked() {
let mgr = SessionManager::new();