From 71d69c8cd548070a69b2719310b2cf1bc01c5c4a Mon Sep 17 00:00:00 2001
From: cai <cai@nbcai.cc>
Date: Tue, 11 Aug 2026 21:17:01 +0800
Subject: [PATCH] feat(helper): add pre-audio fixture attribute probe ack

---
 src/main.rs |  370 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 370 insertions(+), 0 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 9950573..8a1e10d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -59,6 +59,119 @@
 const STREAM_TIMING_FIRST_CHUNK: u64 = 1;
 const STREAM_TIMING_MAX_ELAPSED_MS: u64 = 5_000;
 const STREAM_TIMING_SOURCE: &str = "stream_anchor_monotonic";
+const CONTROLLED_FIXTURE_PROBE_TOPIC: &str = "controlled_fixture_attribute_probe";
+const CONTROLLED_FIXTURE_ACK_TOPIC: &str = "controlled_fixture_attribute_ack";
+const CONTROLLED_FIXTURE_PROTOCOL_VERSION: u64 = 1;
+const CONTROLLED_FIXTURE_GENERATION: u64 = 1;
+const CONTROLLED_FIXTURE_PROBE_RECHECKS: usize = 3;
+const CONTROLLED_FIXTURE_PROBE_RECHECK_DELAY: Duration = Duration::from_millis(25);
+const CONTROLLED_FIXTURE_PROBE_TTL: Duration = Duration::from_millis(250);
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct ControlledFixtureAttributeProbe {
+    #[serde(rename = "type")]
+    message_type: String,
+    protocol_version: u64,
+    call_id_hash: String,
+    call_trace_id_hash: String,
+    generation: u64,
+    client_fixture_sequence: String,
+}
+
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct ControlledFixtureAttributeAck {
+    #[serde(rename = "type")]
+    message_type: &'static str,
+    protocol_version: u64,
+    call_id_hash: String,
+    call_trace_id_hash: String,
+    generation: u64,
+    client_fixture_sequence: String,
+    result: &'static str,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    input_source_category: Option<&'static str>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    reject_reason: Option<&'static str>,
+}
+
+#[derive(Debug)]
+struct PendingControlledFixtureProbe {
+    sender: ParticipantIdentity,
+    sequence: String,
+    expires_at: Instant,
+}
+
+fn sha256_hex(value: &str) -> String {
+    let mut digest = Sha256::new();
+    digest.update(value.as_bytes());
+    digest
+        .finalize()
+        .iter()
+        .map(|byte| format!("{byte:02x}"))
+        .collect()
+}
+
+fn controlled_fixture_probe(
+    payload: &[u8],
+    call_id: &str,
+    trace_id: &str,
+    sender: &ParticipantIdentity,
+    expected_participant: Option<&str>,
+) -> Option<PendingControlledFixtureProbe> {
+    if !is_bound_user_participant(sender.as_str(), expected_participant) {
+        return None;
+    }
+    let probe: ControlledFixtureAttributeProbe = serde_json::from_slice(payload).ok()?;
+    if probe.message_type != CONTROLLED_FIXTURE_PROBE_TOPIC
+        || probe.protocol_version != CONTROLLED_FIXTURE_PROTOCOL_VERSION
+        || probe.generation != CONTROLLED_FIXTURE_GENERATION
+        || probe.call_id_hash != sha256_hex(call_id)
+        || probe.call_trace_id_hash != sha256_hex(trace_id)
+        || probe.client_fixture_sequence.trim().is_empty()
+    {
+        return None;
+    }
+    Some(PendingControlledFixtureProbe {
+        sender: sender.clone(),
+        sequence: probe.client_fixture_sequence,
+        expires_at: Instant::now() + CONTROLLED_FIXTURE_PROBE_TTL,
+    })
+}
+
+fn classify_controlled_fixture_attributes(
+    actual_participant: &str,
+    expected_participant: Option<&str>,
+    attributes: &std::collections::HashMap<String, String>,
+    requested_sequence: &str,
+) -> Result<(), &'static str> {
+    if !is_bound_user_participant(actual_participant, expected_participant) {
+        return Err("wrong_participant");
+    }
+    let source = attributes
+        .get("inputSourceCategory")
+        .map(String::as_str)
+        .unwrap_or("");
+    if source != "controlled_fixture" {
+        return Err(if source.is_empty() {
+            "missing_attributes"
+        } else {
+            "wrong_source"
+        });
+    }
+    let sequence = attributes
+        .get("clientFixtureSequence")
+        .map(String::as_str)
+        .unwrap_or("");
+    if sequence.trim().is_empty() {
+        return Err("missing_sequence");
+    }
+    if sequence != requested_sequence {
+        return Err("wrong_sequence");
+    }
+    Ok(())
+}
 
 #[tokio::main(flavor = "multi_thread")]
 async fn main() -> Result<()> {
@@ -768,6 +881,10 @@
         "runtime helper user_track_subscribe_requested"
     );
 
+    let mut current_user_participant: Option<RemoteParticipant> = None;
+    let mut pending_probe: Option<PendingControlledFixtureProbe> = None;
+    let mut acknowledged_probe_sequences = HashSet::new();
+
     while let Some(event) = events.recv().await {
         match event {
             RoomEvent::TrackSubscribed {
@@ -797,6 +914,7 @@
                     track_source = %track_source,
                     "runtime helper user_track_subscribed"
                 );
+                let participant_for_probe = participant.clone();
                 spawn_user_audio_frame_observer(
                     track,
                     call_id.clone(),
@@ -812,6 +930,48 @@
                     user_participant_identity.clone(),
                     participant,
                 );
+                current_user_participant = Some(participant_for_probe);
+                process_controlled_fixture_probe(
+                    &mut pending_probe,
+                    &mut acknowledged_probe_sequences,
+                    current_user_participant.as_ref(),
+                    &sink,
+                    &call_id,
+                    &trace_id,
+                    user_participant_identity.as_deref(),
+                )
+                .await;
+            }
+            RoomEvent::DataReceived {
+                payload,
+                topic: Some(topic),
+                participant: Some(sender),
+                ..
+            } if topic == CONTROLLED_FIXTURE_PROBE_TOPIC => {
+                if let Ok(probe) =
+                    serde_json::from_slice::<ControlledFixtureAttributeProbe>(&payload)
+                {
+                    if acknowledged_probe_sequences.contains(&probe.client_fixture_sequence) {
+                        continue;
+                    }
+                }
+                pending_probe = controlled_fixture_probe(
+                    &payload,
+                    &call_id,
+                    &trace_id,
+                    &sender.identity(),
+                    user_participant_identity.as_deref(),
+                );
+                process_controlled_fixture_probe(
+                    &mut pending_probe,
+                    &mut acknowledged_probe_sequences,
+                    current_user_participant.as_ref(),
+                    &sink,
+                    &call_id,
+                    &trace_id,
+                    user_participant_identity.as_deref(),
+                )
+                .await;
             }
             RoomEvent::TrackSubscribed {
                 track: RemoteTrack::Video(track),
@@ -851,6 +1011,78 @@
             }
             _ => {}
         }
+    }
+}
+
+async fn process_controlled_fixture_probe(
+    pending_probe: &mut Option<PendingControlledFixtureProbe>,
+    acknowledged_probe_sequences: &mut HashSet<String>,
+    participant: Option<&RemoteParticipant>,
+    sink: &BotAudioOutputSink,
+    call_id: &str,
+    trace_id: &str,
+    expected_participant: Option<&str>,
+) {
+    let Some(probe) = pending_probe.take() else {
+        return;
+    };
+    if acknowledged_probe_sequences.contains(&probe.sequence) || Instant::now() > probe.expires_at {
+        return;
+    }
+    let Some(participant) = participant else {
+        *pending_probe = Some(probe);
+        return;
+    };
+    if participant.identity() != probe.sender {
+        return;
+    }
+    let mut decision = Err("timeout");
+    for attempt in 0..CONTROLLED_FIXTURE_PROBE_RECHECKS {
+        if Instant::now() > probe.expires_at {
+            break;
+        }
+        let attributes = participant.attributes();
+        decision = classify_controlled_fixture_attributes(
+            &participant.identity().to_string(),
+            expected_participant,
+            &attributes,
+            &probe.sequence,
+        );
+        if decision.is_ok() || !matches!(decision, Err("missing_attributes")) {
+            break;
+        }
+        if attempt + 1 < CONTROLLED_FIXTURE_PROBE_RECHECKS {
+            sleep(CONTROLLED_FIXTURE_PROBE_RECHECK_DELAY).await;
+        }
+    }
+    let (result, input_source_category, reject_reason) = match decision {
+        Ok(()) => ("observed", Some("controlled_fixture"), None),
+        Err(reason) => ("rejected", None, Some(reason)),
+    };
+    let ack = ControlledFixtureAttributeAck {
+        message_type: CONTROLLED_FIXTURE_ACK_TOPIC,
+        protocol_version: CONTROLLED_FIXTURE_PROTOCOL_VERSION,
+        call_id_hash: sha256_hex(call_id),
+        call_trace_id_hash: sha256_hex(trace_id),
+        generation: CONTROLLED_FIXTURE_GENERATION,
+        client_fixture_sequence: probe.sequence.clone(),
+        result,
+        input_source_category,
+        reject_reason,
+    };
+    let payload = match serde_json::to_vec(&ack) {
+        Ok(payload) => payload,
+        Err(_) => return,
+    };
+    let local_participant = sink.room.local_participant();
+    let publish = local_participant.publish_data(DataPacket {
+        payload,
+        topic: Some(CONTROLLED_FIXTURE_ACK_TOPIC.to_string()),
+        reliable: true,
+        destination_identities: vec![probe.sender],
+    });
+    if publish.await.is_ok() {
+        acknowledged_probe_sequences.insert(probe.sequence);
     }
 }
 
@@ -4628,4 +4860,142 @@
             assert!(published.is_empty());
         }
     }
+
+    #[test]
+    fn controlled_fixture_probe_requires_bound_hashes_and_protocol() {
+        let call_id = "call-ack-1";
+        let trace_id = "trace-ack-1";
+        let payload = serde_json::to_vec(&json!({
+            "type": CONTROLLED_FIXTURE_PROBE_TOPIC,
+            "protocolVersion": CONTROLLED_FIXTURE_PROTOCOL_VERSION,
+            "callIdHash": sha256_hex(call_id),
+            "callTraceIdHash": sha256_hex(trace_id),
+            "generation": CONTROLLED_FIXTURE_GENERATION,
+            "clientFixtureSequence": "fixture-01"
+        }))
+        .unwrap();
+        let sender = ParticipantIdentity("user-1".to_string());
+        let pending =
+            controlled_fixture_probe(&payload, call_id, trace_id, &sender, Some("user-1"))
+                .expect("valid probe");
+        assert_eq!(pending.sequence, "fixture-01");
+        assert!(
+            controlled_fixture_probe(&payload, call_id, "other-trace", &sender, Some("user-1"),)
+                .is_none()
+        );
+        assert!(
+            controlled_fixture_probe(
+                &payload,
+                call_id,
+                trace_id,
+                &ParticipantIdentity("other-user".to_string()),
+                Some("user-1"),
+            )
+            .is_none()
+        );
+    }
+
+    #[test]
+    fn controlled_fixture_attributes_ack_only_on_exact_current_sequence() {
+        let mut attributes = std::collections::HashMap::from([
+            (
+                "inputSourceCategory".to_string(),
+                "controlled_fixture".to_string(),
+            ),
+            (
+                "clientFixtureSequence".to_string(),
+                "fixture-01".to_string(),
+            ),
+        ]);
+        assert!(
+            classify_controlled_fixture_attributes(
+                "user-1",
+                Some("user-1"),
+                &attributes,
+                "fixture-01"
+            )
+            .is_ok()
+        );
+        attributes.insert(
+            "clientFixtureSequence".to_string(),
+            "fixture-02".to_string(),
+        );
+        assert_eq!(
+            classify_controlled_fixture_attributes(
+                "user-1",
+                Some("user-1"),
+                &attributes,
+                "fixture-01"
+            ),
+            Err("wrong_sequence")
+        );
+        attributes.remove("clientFixtureSequence");
+        assert_eq!(
+            classify_controlled_fixture_attributes(
+                "user-1",
+                Some("user-1"),
+                &attributes,
+                "fixture-01"
+            ),
+            Err("missing_sequence")
+        );
+        assert_eq!(
+            classify_controlled_fixture_attributes(
+                "other-user",
+                Some("user-1"),
+                &attributes,
+                "fixture-01"
+            ),
+            Err("wrong_participant")
+        );
+    }
+
+    #[test]
+    fn controlled_fixture_ack_payload_is_reliable_and_redacted() {
+        let ack = ControlledFixtureAttributeAck {
+            message_type: CONTROLLED_FIXTURE_ACK_TOPIC,
+            protocol_version: CONTROLLED_FIXTURE_PROTOCOL_VERSION,
+            call_id_hash: sha256_hex("call-1"),
+            call_trace_id_hash: sha256_hex("trace-1"),
+            generation: CONTROLLED_FIXTURE_GENERATION,
+            client_fixture_sequence: "fixture-01".to_string(),
+            result: "observed",
+            input_source_category: Some("controlled_fixture"),
+            reject_reason: None,
+        };
+        let encoded = serde_json::to_vec(&ack).unwrap();
+        let decoded: serde_json::Value = serde_json::from_slice(&encoded).unwrap();
+        assert_eq!(decoded["type"], CONTROLLED_FIXTURE_ACK_TOPIC);
+        assert_eq!(
+            decoded["protocolVersion"],
+            CONTROLLED_FIXTURE_PROTOCOL_VERSION
+        );
+        assert_eq!(decoded["result"], "observed");
+        assert!(decoded.get("callId").is_none());
+        assert!(decoded.get("traceId").is_none());
+        assert!(decoded.get("participantIdentity").is_none());
+        assert!(decoded.get("audio").is_none());
+    }
+
+    #[test]
+    fn controlled_fixture_probe_does_not_create_session_or_audio_side_effects() {
+        let attributes = std::collections::HashMap::new();
+        assert_eq!(
+            classify_controlled_fixture_attributes(
+                "user-1",
+                Some("user-1"),
+                &attributes,
+                "fixture-01"
+            ),
+            Err("missing_attributes")
+        );
+        assert_eq!(
+            CONTROLLED_FIXTURE_PROBE_TOPIC,
+            "controlled_fixture_attribute_probe"
+        );
+        assert_eq!(
+            CONTROLLED_FIXTURE_ACK_TOPIC,
+            "controlled_fixture_attribute_ack"
+        );
+    }
 }

--
Gitblit v1.9.3