Skip to main content

codex_config/
requirements_exec_policy.rs

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/// TOML representation of `[rules]` within `requirements.toml`.
49#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
50pub struct RequirementsExecPolicyToml {
51    pub prefix_rules: Vec<RequirementsExecPolicyPrefixRuleToml>,
52}
53
54/// A TOML representation of the `prefix_rule(...)` Starlark builtin.
55///
56/// This mirrors the builtin defined in `execpolicy/src/parser.rs`.
57#[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/// TOML-friendly representation of a pattern token.
65///
66/// Starlark supports either a string token or a list of alternative tokens at
67/// each position, but TOML arrays cannot mix strings and arrays. Using an
68/// array of tables sidesteps that restriction.
69#[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    /// Convert requirements TOML rules into the internal `.rules`
124    /// representation used by `codex-execpolicy`.
125    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}