1use codex_execpolicy::Decision;
2use codex_execpolicy::Policy;
3use codex_execpolicy::RuleRef;
4use codex_execpolicy::rule::PatternToken;
5use codex_execpolicy::rule::PrefixPattern;
6use codex_execpolicy::rule::PrefixRule;
7use multimap::MultiMap;
8use serde::Deserialize;
9use std::sync::Arc;
10use thiserror::Error;
11
12#[derive(Debug, Clone)]
13pub struct RequirementsExecPolicy {
14 policy: Policy,
15}
16
17impl RequirementsExecPolicy {
18 pub fn new(policy: Policy) -> Self {
19 Self { policy }
20 }
21}
22
23impl PartialEq for RequirementsExecPolicy {
24 fn eq(&self, other: &Self) -> bool {
25 policy_fingerprint(&self.policy) == policy_fingerprint(&other.policy)
26 }
27}
28
29impl Eq for RequirementsExecPolicy {}
30
31impl AsRef<Policy> for RequirementsExecPolicy {
32 fn as_ref(&self) -> &Policy {
33 &self.policy
34 }
35}
36
37fn policy_fingerprint(policy: &Policy) -> Vec<String> {
38 let mut entries = Vec::new();
39 for (program, rules) in policy.rules().iter_all() {
40 for rule in rules {
41 entries.push(format!("{program}:{rule:?}"));
42 }
43 }
44 entries.sort();
45 entries
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
50pub struct RequirementsExecPolicyToml {
51 pub prefix_rules: Vec<RequirementsExecPolicyPrefixRuleToml>,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
58pub struct RequirementsExecPolicyPrefixRuleToml {
59 pub pattern: Vec<RequirementsExecPolicyPatternTokenToml>,
60 pub decision: Option<RequirementsExecPolicyDecisionToml>,
61 pub justification: Option<String>,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
70pub struct RequirementsExecPolicyPatternTokenToml {
71 pub token: Option<String>,
72 pub any_of: Option<Vec<String>>,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
76#[serde(rename_all = "kebab-case")]
77pub enum RequirementsExecPolicyDecisionToml {
78 Allow,
79 Prompt,
80 Forbidden,
81}
82
83impl RequirementsExecPolicyDecisionToml {
84 fn as_decision(self) -> Decision {
85 match self {
86 Self::Allow => Decision::Allow,
87 Self::Prompt => Decision::Prompt,
88 Self::Forbidden => Decision::Forbidden,
89 }
90 }
91}
92
93#[derive(Debug, Error)]
94pub enum RequirementsExecPolicyParseError {
95 #[error("rules prefix_rules cannot be empty")]
96 EmptyPrefixRules,
97
98 #[error("rules prefix_rule at index {rule_index} has an empty pattern")]
99 EmptyPattern { rule_index: usize },
100
101 #[error(
102 "rules prefix_rule at index {rule_index} has an invalid pattern token at index {token_index}: {reason}"
103 )]
104 InvalidPatternToken {
105 rule_index: usize,
106 token_index: usize,
107 reason: String,
108 },
109
110 #[error("rules prefix_rule at index {rule_index} has an empty justification")]
111 EmptyJustification { rule_index: usize },
112
113 #[error("rules prefix_rule at index {rule_index} is missing a decision")]
114 MissingDecision { rule_index: usize },
115
116 #[error(
117 "rules prefix_rule at index {rule_index} has decision 'allow', which is not permitted in requirements.toml: Codex merges these rules with other config and uses the most restrictive result (use 'prompt' or 'forbidden')"
118 )]
119 AllowDecisionNotAllowed { rule_index: usize },
120}
121
122impl RequirementsExecPolicyToml {
123 pub fn to_policy(&self) -> Result<Policy, RequirementsExecPolicyParseError> {
126 if self.prefix_rules.is_empty() {
127 return Err(RequirementsExecPolicyParseError::EmptyPrefixRules);
128 }
129
130 let mut rules_by_program: MultiMap<String, RuleRef> = MultiMap::new();
131
132 for (rule_index, rule) in self.prefix_rules.iter().enumerate() {
133 if let Some(justification) = &rule.justification
134 && justification.trim().is_empty()
135 {
136 return Err(RequirementsExecPolicyParseError::EmptyJustification { rule_index });
137 }
138
139 if rule.pattern.is_empty() {
140 return Err(RequirementsExecPolicyParseError::EmptyPattern { rule_index });
141 }
142
143 let pattern_tokens = rule
144 .pattern
145 .iter()
146 .enumerate()
147 .map(|(token_index, token)| parse_pattern_token(token, rule_index, token_index))
148 .collect::<Result<Vec<_>, _>>()?;
149
150 let decision = match rule.decision {
151 Some(RequirementsExecPolicyDecisionToml::Allow) => {
152 return Err(RequirementsExecPolicyParseError::AllowDecisionNotAllowed {
153 rule_index,
154 });
155 }
156 Some(decision) => decision.as_decision(),
157 None => {
158 return Err(RequirementsExecPolicyParseError::MissingDecision { rule_index });
159 }
160 };
161 let justification = rule.justification.clone();
162
163 let (first_token, remaining_tokens) = pattern_tokens
164 .split_first()
165 .ok_or(RequirementsExecPolicyParseError::EmptyPattern { rule_index })?;
166
167 let rest: Arc<[PatternToken]> = remaining_tokens.to_vec().into();
168
169 for head in first_token.alternatives() {
170 let rule: RuleRef = Arc::new(PrefixRule {
171 pattern: PrefixPattern {
172 first: Arc::from(head.as_str()),
173 rest: rest.clone(),
174 },
175 decision,
176 justification: justification.clone(),
177 });
178 rules_by_program.insert(head.clone(), rule);
179 }
180 }
181
182 Ok(Policy::new(rules_by_program))
183 }
184
185 pub(crate) fn to_requirements_policy(
186 &self,
187 ) -> Result<RequirementsExecPolicy, RequirementsExecPolicyParseError> {
188 self.to_policy().map(RequirementsExecPolicy::new)
189 }
190}
191
192fn parse_pattern_token(
193 token: &RequirementsExecPolicyPatternTokenToml,
194 rule_index: usize,
195 token_index: usize,
196) -> Result<PatternToken, RequirementsExecPolicyParseError> {
197 match (&token.token, &token.any_of) {
198 (Some(single), None) => {
199 if single.trim().is_empty() {
200 return Err(RequirementsExecPolicyParseError::InvalidPatternToken {
201 rule_index,
202 token_index,
203 reason: "token cannot be empty".to_string(),
204 });
205 }
206 Ok(PatternToken::Single(single.clone()))
207 }
208 (None, Some(alternatives)) => {
209 if alternatives.is_empty() {
210 return Err(RequirementsExecPolicyParseError::InvalidPatternToken {
211 rule_index,
212 token_index,
213 reason: "any_of cannot be empty".to_string(),
214 });
215 }
216 if alternatives.iter().any(|alt| alt.trim().is_empty()) {
217 return Err(RequirementsExecPolicyParseError::InvalidPatternToken {
218 rule_index,
219 token_index,
220 reason: "any_of cannot include empty tokens".to_string(),
221 });
222 }
223 Ok(PatternToken::Alts(alternatives.clone()))
224 }
225 (Some(_), Some(_)) => Err(RequirementsExecPolicyParseError::InvalidPatternToken {
226 rule_index,
227 token_index,
228 reason: "set either token or any_of, not both".to_string(),
229 }),
230 (None, None) => Err(RequirementsExecPolicyParseError::InvalidPatternToken {
231 rule_index,
232 token_index,
233 reason: "set either token or any_of".to_string(),
234 }),
235 }
236}