返回 DeepSeek-TUI-2026
rule.rs
根目录 / crates / tui / src / execpolicy / rule.rs
1 use super::decision::Decision;
2 use super::error::Error;
3 use super::error::Result;
4 use serde::Deserialize;
5 use serde::Serialize;
6 use shlex::try_join;
7 use std::any::Any;
8 use std::fmt::Debug;
9 use std::sync::Arc;
10
11 /// Matches a single command token, either a fixed string or one of several allowed alternatives.
12 #[derive(Clone, Debug, Eq, PartialEq)]
13 pub enum PatternToken {
14 Single(String),
15 Alts(Vec<String>),
16 }
17
18 impl PatternToken {
19 fn matches(&self, token: &str) -> bool {
20 match self {
21 Self::Single(expected) => expected == token,
22 Self::Alts(alternatives) => alternatives.iter().any(|alt| alt == token),
23 }
24 }
25
26 pub fn alternatives(&self) -> &[String] {
27 match self {
28 Self::Single(expected) => std::slice::from_ref(expected),
29 Self::Alts(alternatives) => alternatives,
30 }
31 }
32 }
33
34 /// Prefix matcher for commands with support for alternative match tokens.
35 /// First token is fixed since we key by the first token in policy.
36 #[derive(Clone, Debug, Eq, PartialEq)]
37 pub struct PrefixPattern {
38 pub first: Arc<str>,
39 pub rest: Arc<[PatternToken]>,
40 }
41
42 impl PrefixPattern {
43 pub fn matches_prefix(&self, cmd: &[String]) -> Option<Vec<String>> {
44 let pattern_length = self.rest.len() + 1;
45 if cmd.len() < pattern_length || cmd[0] != self.first.as_ref() {
46 return None;
47 }
48
49 for (pattern_token, cmd_token) in self.rest.iter().zip(&cmd[1..pattern_length]) {
50 if !pattern_token.matches(cmd_token) {
51 return None;
52 }
53 }
54
55 Some(cmd[..pattern_length].to_vec())
56 }
57 }
58
59 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
60 #[serde(rename_all = "camelCase")]
61 pub enum RuleMatch {
62 PrefixRuleMatch {
63 #[serde(rename = "matchedPrefix")]
64 matched_prefix: Vec<String>,
65 decision: Decision,
66 /// Optional rationale for why this rule exists.
67 ///
68 /// This can be supplied for any decision and may be surfaced in different contexts
69 /// (e.g., prompt reasons or rejection messages).
70 #[serde(skip_serializing_if = "Option::is_none")]
71 justification: Option<String>,
72 },
73 HeuristicsRuleMatch {
74 command: Vec<String>,
75 decision: Decision,
76 },
77 }
78
79 impl RuleMatch {
80 pub fn decision(&self) -> Decision {
81 match self {
82 Self::PrefixRuleMatch { decision, .. } => *decision,
83 Self::HeuristicsRuleMatch { decision, .. } => *decision,
84 }
85 }
86 }
87
88 #[derive(Clone, Debug, Eq, PartialEq)]
89 pub struct PrefixRule {
90 pub pattern: PrefixPattern,
91 pub decision: Decision,
92 pub justification: Option<String>,
93 }
94
95 pub trait Rule: Any + Debug + Send + Sync {
96 fn program(&self) -> &str;
97
98 fn matches(&self, cmd: &[String]) -> Option<RuleMatch>;
99 }
100
101 pub type RuleRef = Arc<dyn Rule>;
102
103 impl Rule for PrefixRule {
104 fn program(&self) -> &str {
105 self.pattern.first.as_ref()
106 }
107
108 fn matches(&self, cmd: &[String]) -> Option<RuleMatch> {
109 self.pattern
110 .matches_prefix(cmd)
111 .map(|matched_prefix| RuleMatch::PrefixRuleMatch {
112 matched_prefix,
113 decision: self.decision,
114 justification: self.justification.clone(),
115 })
116 }
117 }
118
119 /// Count how many rules match each provided example and error if any example is unmatched.
120 pub(crate) fn validate_match_examples(rules: &[RuleRef], matches: &[Vec<String>]) -> Result<()> {
121 let mut unmatched_examples = Vec::new();
122
123 for example in matches {
124 if rules.iter().any(|rule| rule.matches(example).is_some()) {
125 continue;
126 }
127
128 unmatched_examples.push(
129 try_join(example.iter().map(String::as_str))
130 .unwrap_or_else(|_| "unable to render example".to_string()),
131 );
132 }
133
134 if unmatched_examples.is_empty() {
135 Ok(())
136 } else {
137 Err(Error::ExampleDidNotMatch {
138 rules: rules.iter().map(|rule| format!("{rule:?}")).collect(),
139 examples: unmatched_examples,
140 })
141 }
142 }
143
144 /// Ensure that no rule matches any provided negative example.
145 pub(crate) fn validate_not_match_examples(
146 rules: &[RuleRef],
147 not_matches: &[Vec<String>],
148 ) -> Result<()> {
149 for example in not_matches {
150 if let Some(rule) = rules.iter().find(|rule| rule.matches(example).is_some()) {
151 return Err(Error::ExampleDidMatch {
152 rule: format!("{rule:?}"),
153 example: try_join(example.iter().map(String::as_str))
154 .unwrap_or_else(|_| "unable to render example".to_string()),
155 });
156 }
157 }
158
159 Ok(())
160 }
161
161 lines RUST