feat(host): build desktop audio exclusion foundation

This commit is contained in:
2026-08-21 15:39:07 -04:00
parent 5d3da8b006
commit 781defcd84
16 changed files with 2586 additions and 564 deletions
+83 -1
View File
@@ -16,7 +16,7 @@ use std::collections::BTreeSet;
use super::fixture::{Graph, NodeRef, PULSE_PID, app};
use super::owner::{OwnerCtx, OwnerKey, strongest_shared_key};
use super::snapshot::{MediaRole, NodeProps, PortDirection, Serial};
use super::snapshot::{GlobalId, MediaRole, NodeProps, PortDirection, Serial};
use super::{Decisions, Eligibility, ExclusionCtx, ObjectRef, Reason, StickyState, evaluate};
fn ctx() -> ExclusionCtx {
@@ -176,6 +176,88 @@ fn peerspeak_tagged_nodes_are_excluded_and_plain_apps_are_not() {
assert_tainted(&decisions, sink, "tainted-upstream");
}
// ──────────────────────────────────────────────────────────────────────
// Hidden hardware playback-to-capture paths — same Device only
// ─────────────────────────────────────────────────────────────────────
#[test]
fn same_hardware_device_closes_an_unpublished_playback_to_capture_hop() {
let mut graph = Graph::new();
let card = GlobalId(700);
let sink = graph.device_node_on("card-playback", MediaRole::Sink, card);
let source = graph.device_node_on("card-capture", MediaRole::Source, card);
let call = graph.peerspeak_node("peerspeak-call", 7);
let music = graph.app_node("music", MediaRole::StreamOutput, 8);
let recorder_in = graph.app_node("recorder-in", MediaRole::StreamInput, 9);
let recorder_out = graph.app_node("recorder-out", MediaRole::StreamOutput, 9);
graph.link(call, sink);
graph.link(music, sink);
// There is deliberately no sink → source Link: the hardware bridge is
// the route being modeled.
graph.link(source, recorder_in);
let decisions = run(&graph, &ctx());
assert_partition(
&decisions,
&[("music", music)],
&[
("call", call, "peerspeak-owned"),
("recorder-out", recorder_out, "tainted-owner-bridge"),
],
);
assert_tainted(&decisions, sink, "tainted-upstream");
assert_tainted(&decisions, source, "tainted-upstream");
assert_tainted(&decisions, recorder_in, "tainted-upstream");
}
#[test]
fn different_hardware_devices_do_not_invent_a_capture_path() {
let mut graph = Graph::new();
let sink = graph.device_node_on("speaker", MediaRole::Sink, GlobalId(700));
let source = graph.device_node_on("usb-mic", MediaRole::Source, GlobalId(701));
let call = graph.peerspeak_node("peerspeak-call", 7);
let recorder_in = graph.app_node("recorder-in", MediaRole::StreamInput, 9);
let recorder_out = graph.app_node("recorder-out", MediaRole::StreamOutput, 9);
graph.link(call, sink);
graph.link(source, recorder_in);
let decisions = run(&graph, &ctx());
assert_partition(
&decisions,
&[("recorder-out", recorder_out)],
&[("call", call, "peerspeak-owned")],
);
assert_untainted(&decisions, source);
assert_untainted(&decisions, recorder_in);
}
#[test]
fn same_device_microphone_use_is_intentionally_over_excluded() {
// The hardware's private mixer/firmware path is not observable in the
// PipeWire graph. If an app captures the same device receiving the call,
// v1 cannot prove that its capture is clean, so its playback is excluded.
let mut graph = Graph::new();
let card = GlobalId(700);
let sink = graph.device_node_on("headset-output", MediaRole::Sink, card);
let mic = graph.device_node_on("headset-mic", MediaRole::Source, card);
let call = graph.peerspeak_node("peerspeak-call", 7);
let firefox_in = graph.app_node("firefox-mic", MediaRole::StreamInput, 11_114);
let firefox_out = graph.app_node("firefox-audio", MediaRole::StreamOutput, 11_114);
graph.link(call, sink);
graph.link(mic, firefox_in);
assert_partition(
&run(&graph, &ctx()),
&[],
&[
("call", call, "peerspeak-owned"),
("firefox-out", firefox_out, "tainted-owner-bridge"),
],
);
}
/// Each ownership carrier must work **alone** (v3.5 §5.1).
///
/// ⚠️ The phase-3r lesson, applied deliberately: a gate that asserts a value