use codex_execpolicy::Decision;use codex_execpolicy::Policy;use codex_execpolicy::RuleRef;use codex_execpolicy::rule::PatternToken;use codex_execpolicy::rule::PrefixPattern;use codex_execpolicy::rule::PrefixRule;use multimap::MultiMap;use serde::Deserialize;use std::sync::Arc;use thiserror::Error;#[derive(Debug, Clone)]pub struct RequirementsExecPolicy { policy: Policy,}impl RequirementsExecPolicy { pub fn new(policy: Policy) -> Self { Self { policy } }}impl PartialEq for RequirementsExecPolicy { fn eq(&self, other: &Self) -> bool { policy_fingerprint(&self.policy) == policy_fingerprint(&other.policy) }}impl Eq for RequirementsExecPolicy {}impl AsRef<Policy> for RequirementsExecPolicy { fn as_ref(&self) -> &Policy { &self.policy }}fn policy_fingerprint(policy: &Policy) -> Vec<String> { let mut entries = Vec::new(); for (program, rules) in policy.rules().iter_all() { for rule in rules { entries.push(format!("{program}:{rule:?}")); } } entries.sort(); entries}/// TOML representation of `[rules]` within `requirements.toml`.#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]pub struct RequirementsExecPolicyToml { pub prefix_rules: Vec<RequirementsExecPolicyPrefixRuleToml>,}/// A TOML representation of the `prefix_rule(...)` Starlark builtin.////// This mirrors the builtin defined in `execpolicy/src/parser.rs`.#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]pub struct RequirementsExecPolicyPrefixRuleToml { pub pattern: Vec<RequirementsExecPolicyPatternTokenToml>, pub decision: Option<RequirementsExecPolicyDecisionToml>, pub justification: Option<String>,}/// TOML-friendly representation of a pattern token.////// Starlark supports either a string token or a list of alternative tokens at/// each position, but TOML arrays cannot mix strings and arrays. Using an/// array of tables sidesteps that restriction.#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]pub struct RequirementsExecPolicyPatternTokenToml { pub token: Option<String>, pub any_of: Option<Vec<String>>,}#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]#[serde(rename_all = "kebab-case")]pub enum RequirementsExecPolicyDecisionToml { Allow, Prompt, Forbidden,}impl RequirementsExecPolicyDecisionToml { fn as_decision(self) -> Decision { match self { Self::Allow => Decision::Allow, Self::Prompt => Decision::Prompt, Self::Forbidden => Decision::Forbidden, } }}#[derive(Debug, Error)]pub enum RequirementsExecPolicyParseError { #[error("rules prefix_rules cannot be empty")] EmptyPrefixRules, #[error("rules prefix_rule at index {rule_index} has an empty pattern")] EmptyPattern { rule_index: usize }, #[error( "rules prefix_rule at index {rule_index} has an invalid pattern token at index {token_index}: {reason}" )] InvalidPatternToken { rule_index: usize, token_index: usize, reason: String, }, #[error("rules prefix_rule at index {rule_index} has an empty justification")] EmptyJustification { rule_index: usize }, #[error("rules prefix_rule at index {rule_index} is missing a decision")] MissingDecision { rule_index: usize }, #[error( "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')" )] AllowDecisionNotAllowed { rule_index: usize },}impl RequirementsExecPolicyToml { /// Convert requirements TOML rules into the internal `.rules` /// representation used by `codex-execpolicy`. pub fn to_policy(&self) -> Result<Policy, RequirementsExecPolicyParseError> { if self.prefix_rules.is_empty() { return Err(RequirementsExecPolicyParseError::EmptyPrefixRules); } let mut rules_by_program: MultiMap<String, RuleRef> = MultiMap::new(); for (rule_index, rule) in self.prefix_rules.iter().enumerate() { if let Some(justification) = &rule.justification && justification.trim().is_empty() { return Err(RequirementsExecPolicyParseError::EmptyJustification { rule_index }); } if rule.pattern.is_empty() { return Err(RequirementsExecPolicyParseError::EmptyPattern { rule_index }); } let pattern_tokens = rule .pattern .iter() .enumerate() .map(|(token_index, token)| parse_pattern_token(token, rule_index, token_index)) .collect::<Result<Vec<_>, _>>()?; let decision = match rule.decision { Some(RequirementsExecPolicyDecisionToml::Allow) => { return Err(RequirementsExecPolicyParseError::AllowDecisionNotAllowed { rule_index, }); } Some(decision) => decision.as_decision(), None => { return Err(RequirementsExecPolicyParseError::MissingDecision { rule_index }); } }; let justification = rule.justification.clone(); let (first_token, remaining_tokens) = pattern_tokens .split_first() .ok_or(RequirementsExecPolicyParseError::EmptyPattern { rule_index })?; let rest: Arc<[PatternToken]> = remaining_tokens.to_vec().into(); for head in first_token.alternatives() { let rule: RuleRef = Arc::new(PrefixRule { pattern: PrefixPattern { first: Arc::from(head.as_str()), rest: rest.clone(), }, decision, justification: justification.clone(), }); rules_by_program.insert(head.clone(), rule); } } Ok(Policy::new(rules_by_program)) } pub(crate) fn to_requirements_policy( &self, ) -> Result<RequirementsExecPolicy, RequirementsExecPolicyParseError> { self.to_policy().map(RequirementsExecPolicy::new) }}fn parse_pattern_token( token: &RequirementsExecPolicyPatternTokenToml, rule_index: usize, token_index: usize,) -> Result<PatternToken, RequirementsExecPolicyParseError> { match (&token.token, &token.any_of) { (Some(single), None) => { if single.trim().is_empty() { return Err(RequirementsExecPolicyParseError::InvalidPatternToken { rule_index, token_index, reason: "token cannot be empty".to_string(), }); } Ok(PatternToken::Single(single.clone())) } (None, Some(alternatives)) => { if alternatives.is_empty() { return Err(RequirementsExecPolicyParseError::InvalidPatternToken { rule_index, token_index, reason: "any_of cannot be empty".to_string(), }); } if alternatives.iter().any(|alt| alt.trim().is_empty()) { return Err(RequirementsExecPolicyParseError::InvalidPatternToken { rule_index, token_index, reason: "any_of cannot include empty tokens".to_string(), }); } Ok(PatternToken::Alts(alternatives.clone())) } (Some(_), Some(_)) => Err(RequirementsExecPolicyParseError::InvalidPatternToken { rule_index, token_index, reason: "set either token or any_of, not both".to_string(), }), (None, None) => Err(RequirementsExecPolicyParseError::InvalidPatternToken { rule_index, token_index, reason: "set either token or any_of".to_string(), }), }}