返回 DeepSeek-TUI-2026
policy.rs
根目录 / crates / tui / src / execpolicy / policy.rs
1 use super::decision::Decision;
2 use super::error::Error;
3 use super::error::Result;
4 use super::rule::PatternToken;
5 use super::rule::PrefixPattern;
6 use super::rule::PrefixRule;
7 use super::rule::RuleMatch;
8 use super::rule::RuleRef;
9 use multimap::MultiMap;
10 use serde::Deserialize;
11 use serde::Serialize;
12 use std::sync::Arc;
13
14 type HeuristicsFallback<'a> = Option<&'a dyn Fn(&[String]) -> Decision>;
15
16 #[derive(Clone, Debug)]
17 pub struct Policy {
18 rules_by_program: MultiMap<String, RuleRef>,
19 }
20
21 impl Policy {
22 pub fn new(rules_by_program: MultiMap<String, RuleRef>) -> Self {
23 Self { rules_by_program }
24 }
25
26 pub fn empty() -> Self {
27 Self::new(MultiMap::new())
28 }
29
30 pub fn rules(&self) -> &MultiMap<String, RuleRef> {
31 &self.rules_by_program
32 }
33
34 pub fn add_prefix_rule(&mut self, prefix: &[String], decision: Decision) -> Result<()> {
35 let (first_token, rest) = prefix
36 .split_first()
37 .ok_or_else(|| Error::InvalidPattern("prefix cannot be empty".to_string()))?;
38
39 let rule: RuleRef = Arc::new(PrefixRule {
40 pattern: PrefixPattern {
41 first: Arc::from(first_token.as_str()),
42 rest: rest
43 .iter()
44 .map(|token| PatternToken::Single(token.clone()))
45 .collect::<Vec<_>>()
46 .into(),
47 },
48 decision,
49 justification: None,
50 });
51
52 self.rules_by_program.insert(first_token.clone(), rule);
53 Ok(())
54 }
55
56 pub fn check<F>(&self, cmd: &[String], heuristics_fallback: &F) -> Evaluation
57 where
58 F: Fn(&[String]) -> Decision,
59 {
60 let matched_rules = self.matches_for_command(cmd, Some(heuristics_fallback));
61 Evaluation::from_matches(matched_rules)
62 }
63
64 /// Checks multiple commands and aggregates the results.
65 pub fn check_multiple<Commands, F>(
66 &self,
67 commands: Commands,
68 heuristics_fallback: &F,
69 ) -> Evaluation
70 where
71 Commands: IntoIterator,
72 Commands::Item: AsRef<[String]>,
73 F: Fn(&[String]) -> Decision,
74 {
75 let matched_rules: Vec<RuleMatch> = commands
76 .into_iter()
77 .flat_map(|command| {
78 self.matches_for_command(command.as_ref(), Some(heuristics_fallback))
79 })
80 .collect();
81
82 Evaluation::from_matches(matched_rules)
83 }
84
85 /// Returns matching rules for the given command. If no rules match and
86 /// `heuristics_fallback` is provided, returns a single
87 /// `HeuristicsRuleMatch` with the decision rendered by
88 /// `heuristics_fallback`.
89 ///
90 /// If `heuristics_fallback.is_some()`, then the returned vector is
91 /// guaranteed to be non-empty.
92 pub fn matches_for_command(
93 &self,
94 cmd: &[String],
95 heuristics_fallback: HeuristicsFallback<'_>,
96 ) -> Vec<RuleMatch> {
97 let matched_rules: Vec<RuleMatch> = match cmd.first() {
98 Some(first) => self
99 .rules_by_program
100 .get_vec(first)
101 .map(|rules| rules.iter().filter_map(|rule| rule.matches(cmd)).collect())
102 .unwrap_or_default(),
103 None => Vec::new(),
104 };
105
106 if matched_rules.is_empty()
107 && let Some(heuristics_fallback) = heuristics_fallback
108 {
109 vec![RuleMatch::HeuristicsRuleMatch {
110 command: cmd.to_vec(),
111 decision: heuristics_fallback(cmd),
112 }]
113 } else {
114 matched_rules
115 }
116 }
117 }
118
119 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
120 #[serde(rename_all = "camelCase")]
121 pub struct Evaluation {
122 pub decision: Decision,
123 #[serde(rename = "matchedRules")]
124 pub matched_rules: Vec<RuleMatch>,
125 }
126
127 impl Evaluation {
128 pub fn is_match(&self) -> bool {
129 self.matched_rules
130 .iter()
131 .any(|rule_match| !matches!(rule_match, RuleMatch::HeuristicsRuleMatch { .. }))
132 }
133
134 /// Caller is responsible for ensuring that `matched_rules` is non-empty.
135 fn from_matches(matched_rules: Vec<RuleMatch>) -> Self {
136 let decision = matched_rules.iter().map(RuleMatch::decision).max();
137 #[expect(clippy::expect_used)]
138 let decision = decision.expect("invariant failed: matched_rules must be non-empty");
139
140 Self {
141 decision,
142 matched_rules,
143 }
144 }
145 }
146
146 lines RUST