Files
WhispAssist/src-tauri/src/mcp/scope.rs
T
2026-07-07 00:29:50 -05:00

101 lines
4.1 KiB
Rust

//! Pure scope-control logic (FR-MCP-3), split out from `mcp/mod.rs` so it's
//! unit-testable without a DB, a running server, or the `mcp` cargo feature.
//!
//! Design note (documented here because the schema doesn't (yet) carry a
//! per-meeting "expose this meeting" flag -- only `feature_briefs.exposed`
//! does, per `docs/03-data-model.md`): with `ExposeScope::Selected`, meetings/
//! transcripts/action-items have no selection mechanism to key off in this
//! milestone, so they are treated the same as `None` (deny) rather than the
//! same as `All` (allow) -- a privacy-conservative default consistent with
//! every other WA default (recording/sync/hosted-AI/MCP itself all default
//! OFF). Only `get_feature_brief` has real per-item selection today, via the
//! brief's own `exposed` flag (M1). A future "select meetings" UI/schema
//! addition should upgrade `Selected` for the other three tools without
//! changing this function's callers.
use crate::mcp::ExposeScope;
/// Whether `list_recent_meetings`/`get_transcript`/`get_action_items` may see
/// meetings at all under the current scope. `Selected` has no per-meeting
/// selection mechanism yet (see module docs) so it is conservatively treated
/// like `None`.
pub fn meetings_visible(scope: ExposeScope) -> bool {
matches!(scope, ExposeScope::All)
}
/// Whether a specific feature brief may be served. `exposed` is the brief's
/// own per-item flag (`feature_briefs.exposed`, set via `set_brief_exposed`).
pub fn brief_visible(scope: ExposeScope, exposed: bool) -> bool {
match scope {
ExposeScope::None => false,
ExposeScope::Selected => exposed,
ExposeScope::All => true,
}
}
/// Recordings (`.wav`) are never exposed unless explicitly allowed (FR-MCP-3),
/// independent of `ExposeScope`. None of the four MCP tools serve raw audio
/// bytes today, but a meeting that retained its recording (ADR-0009) is
/// treated as more sensitive-by-association: its transcript/action items are
/// also withheld unless the user opted into `expose_recordings`. Every tool
/// handler must call this for each candidate meeting -- there is no central
/// choke point (FR-MCP-3 "enforce in every tool handler").
pub fn recording_gate_ok(expose_recordings: bool, meeting_recorded: bool) -> bool {
expose_recordings || !meeting_recorded
}
/// Combined check a tool handler runs before including one meeting's data.
pub fn meeting_allowed(
scope: ExposeScope,
expose_recordings: bool,
meeting_recorded: bool,
) -> bool {
meetings_visible(scope) && recording_gate_ok(expose_recordings, meeting_recorded)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn none_hides_all_meetings() {
assert!(!meetings_visible(ExposeScope::None));
}
#[test]
fn selected_hides_meetings_pending_a_selection_mechanism() {
// Documented conservative choice -- see module docs.
assert!(!meetings_visible(ExposeScope::Selected));
}
#[test]
fn all_shows_meetings() {
assert!(meetings_visible(ExposeScope::All));
}
#[test]
fn brief_visibility_follows_the_exposed_flag_only_under_selected() {
assert!(!brief_visible(ExposeScope::None, true));
assert!(!brief_visible(ExposeScope::Selected, false));
assert!(brief_visible(ExposeScope::Selected, true));
assert!(brief_visible(ExposeScope::All, false));
assert!(brief_visible(ExposeScope::All, true));
}
#[test]
fn recordings_never_served_unless_explicitly_allowed() {
assert!(!recording_gate_ok(false, true));
assert!(recording_gate_ok(false, false));
assert!(recording_gate_ok(true, true));
assert!(recording_gate_ok(true, false));
}
#[test]
fn meeting_allowed_requires_both_scope_and_recording_gate() {
assert!(!meeting_allowed(ExposeScope::All, false, true)); // recorded, not opted-in
assert!(meeting_allowed(ExposeScope::All, false, false)); // not recorded
assert!(meeting_allowed(ExposeScope::All, true, true)); // opted-in
assert!(!meeting_allowed(ExposeScope::None, true, false)); // scope still wins
}
}