@@ -34,6 +34,13 @@ const POLICY_DIR_NAME: &str = "policy";
3434const POLICY_EXTENSION : & str = "codexpolicy" ;
3535const DEFAULT_POLICY_FILE : & str = "default.codexpolicy" ;
3636
37+ fn is_policy_match ( rule_match : & RuleMatch ) -> bool {
38+ match rule_match {
39+ RuleMatch :: PrefixRuleMatch { .. } => true ,
40+ RuleMatch :: HeuristicsRuleMatch { .. } => false ,
41+ }
42+ }
43+
3744#[ derive( Debug , Error ) ]
3845pub enum ExecPolicyError {
3946 #[ error( "failed to read execpolicy files from {dir}: {source}" ) ]
@@ -147,49 +154,62 @@ pub(crate) async fn append_execpolicy_amendment_and_update(
147154 Ok ( ( ) )
148155}
149156
150- /// Returns a proposed execpolicy amendment only when heuristics caused
151- /// the prompt decision, so we can offer to apply that amendment for future runs.
152- ///
153- /// The amendment uses the first command heuristics marked as `Prompt`. If any explicit
154- /// execpolicy rule also prompts, we return `None` because applying the amendment would not
155- /// skip that policy requirement.
156- ///
157- /// Examples:
157+ /// Derive a proposed execpolicy amendment when a command requires user approval
158+ /// - If any execpolicy rule prompts, return None, because an amendment would not skip that policy requirement.
159+ /// - Otherwise return the first heuristics Prompt.
160+ /// - Examples:
158161/// - execpolicy: empty. Command: `["python"]`. Heuristics prompt -> `Some(vec!["python"])`.
159162/// - execpolicy: empty. Command: `["bash", "-c", "cd /some/folder && prog1 --option1 arg1 && prog2 --option2 arg2"]`.
160163/// Parsed commands include `cd /some/folder`, `prog1 --option1 arg1`, and `prog2 --option2 arg2`. If heuristics allow `cd` but prompt
161164/// on `prog1`, we return `Some(vec!["prog1", "--option1", "arg1"])`.
162165/// - execpolicy: contains a `prompt for prefix ["prog2"]` rule. For the same command as above,
163166/// we return `None` because an execpolicy prompt still applies even if we amend execpolicy to allow ["prog1", "--option1", "arg1"].
164- fn proposed_execpolicy_amendment ( evaluation : & Evaluation ) -> Option < ExecPolicyAmendment > {
165- if evaluation. decision != Decision :: Prompt {
167+ fn try_derive_execpolicy_amendment_for_prompt_rules (
168+ matched_rules : & [ RuleMatch ] ,
169+ ) -> Option < ExecPolicyAmendment > {
170+ if matched_rules
171+ . iter ( )
172+ . any ( |rule_match| is_policy_match ( rule_match) && rule_match. decision ( ) == Decision :: Prompt )
173+ {
166174 return None ;
167175 }
168176
169- let mut first_prompt_from_heuristics: Option < Vec < String > > = None ;
170- for rule_match in & evaluation. matched_rules {
171- match rule_match {
172- RuleMatch :: HeuristicsRuleMatch { command, decision } => {
173- if * decision == Decision :: Prompt && first_prompt_from_heuristics. is_none ( ) {
174- first_prompt_from_heuristics = Some ( command. clone ( ) ) ;
175- }
176- }
177- _ if rule_match. decision ( ) == Decision :: Prompt => {
178- return None ;
179- }
180- _ => { }
181- }
177+ matched_rules
178+ . iter ( )
179+ . find_map ( |rule_match| match rule_match {
180+ RuleMatch :: HeuristicsRuleMatch {
181+ command,
182+ decision : Decision :: Prompt ,
183+ } => Some ( ExecPolicyAmendment :: from ( command. clone ( ) ) ) ,
184+ _ => None ,
185+ } )
186+ }
187+
188+ /// - Note: we only use this amendment when the command fails to run in sandbox and codex prompts the user to run outside the sandbox
189+ /// - The purpose of this amendment is to bypass sandbox for similar commands in the future
190+ /// - If any execpolicy rule matches, return None, because we would already be running command outside the sandbox
191+ fn try_derive_execpolicy_amendment_for_allow_rules (
192+ matched_rules : & [ RuleMatch ] ,
193+ ) -> Option < ExecPolicyAmendment > {
194+ if matched_rules. iter ( ) . any ( is_policy_match) {
195+ return None ;
182196 }
183197
184- first_prompt_from_heuristics. map ( ExecPolicyAmendment :: from)
198+ matched_rules
199+ . iter ( )
200+ . find_map ( |rule_match| match rule_match {
201+ RuleMatch :: HeuristicsRuleMatch {
202+ command,
203+ decision : Decision :: Allow ,
204+ } => Some ( ExecPolicyAmendment :: from ( command. clone ( ) ) ) ,
205+ _ => None ,
206+ } )
185207}
186208
187209/// Only return PROMPT_REASON when an execpolicy rule drove the prompt decision.
188210fn derive_prompt_reason ( evaluation : & Evaluation ) -> Option < String > {
189211 evaluation. matched_rules . iter ( ) . find_map ( |rule_match| {
190- if !matches ! ( rule_match, RuleMatch :: HeuristicsRuleMatch { .. } )
191- && rule_match. decision ( ) == Decision :: Prompt
192- {
212+ if is_policy_match ( rule_match) && rule_match. decision ( ) == Decision :: Prompt {
193213 Some ( PROMPT_REASON . to_string ( ) )
194214 } else {
195215 None
@@ -215,10 +235,6 @@ pub(crate) async fn create_exec_approval_requirement_for_command(
215235 } ;
216236 let policy = exec_policy. read ( ) . await ;
217237 let evaluation = policy. check_multiple ( commands. iter ( ) , & heuristics_fallback) ;
218- let has_policy_allow = evaluation. matched_rules . iter ( ) . any ( |rule_match| {
219- !matches ! ( rule_match, RuleMatch :: HeuristicsRuleMatch { .. } )
220- && rule_match. decision ( ) == Decision :: Allow
221- } ) ;
222238
223239 match evaluation. decision {
224240 Decision :: Forbidden => ExecApprovalRequirement :: Forbidden {
@@ -233,15 +249,23 @@ pub(crate) async fn create_exec_approval_requirement_for_command(
233249 ExecApprovalRequirement :: NeedsApproval {
234250 reason : derive_prompt_reason ( & evaluation) ,
235251 proposed_execpolicy_amendment : if features. enabled ( Feature :: ExecPolicy ) {
236- proposed_execpolicy_amendment ( & evaluation)
252+ try_derive_execpolicy_amendment_for_prompt_rules ( & evaluation. matched_rules )
237253 } else {
238254 None
239255 } ,
240256 }
241257 }
242258 }
243259 Decision :: Allow => ExecApprovalRequirement :: Skip {
244- bypass_sandbox : has_policy_allow,
260+ // Bypass sandbox if execpolicy allows the command
261+ bypass_sandbox : evaluation. matched_rules . iter ( ) . any ( |rule_match| {
262+ is_policy_match ( rule_match) && rule_match. decision ( ) == Decision :: Allow
263+ } ) ,
264+ proposed_execpolicy_amendment : if features. enabled ( Feature :: ExecPolicy ) {
265+ try_derive_execpolicy_amendment_for_allow_rules ( & evaluation. matched_rules )
266+ } else {
267+ None
268+ } ,
245269 } ,
246270 }
247271}
@@ -730,4 +754,56 @@ prefix_rule(pattern=["rm"], decision="forbidden")
730754 }
731755 ) ;
732756 }
757+
758+ #[ tokio:: test]
759+ async fn proposed_execpolicy_amendment_is_present_when_heuristics_allow ( ) {
760+ let command = vec ! [ "echo" . to_string( ) , "safe" . to_string( ) ] ;
761+
762+ let requirement = create_exec_approval_requirement_for_command (
763+ & Arc :: new ( RwLock :: new ( Policy :: empty ( ) ) ) ,
764+ & Features :: with_defaults ( ) ,
765+ & command,
766+ AskForApproval :: OnRequest ,
767+ & SandboxPolicy :: ReadOnly ,
768+ SandboxPermissions :: UseDefault ,
769+ )
770+ . await ;
771+
772+ assert_eq ! (
773+ requirement,
774+ ExecApprovalRequirement :: Skip {
775+ bypass_sandbox: false ,
776+ proposed_execpolicy_amendment: Some ( ExecPolicyAmendment :: new( command) ) ,
777+ }
778+ ) ;
779+ }
780+
781+ #[ tokio:: test]
782+ async fn proposed_execpolicy_amendment_is_suppressed_when_policy_matches_allow ( ) {
783+ let policy_src = r#"prefix_rule(pattern=["echo"], decision="allow")"# ;
784+ let mut parser = PolicyParser :: new ( ) ;
785+ parser
786+ . parse ( "test.codexpolicy" , policy_src)
787+ . expect ( "parse policy" ) ;
788+ let policy = Arc :: new ( RwLock :: new ( parser. build ( ) ) ) ;
789+ let command = vec ! [ "echo" . to_string( ) , "safe" . to_string( ) ] ;
790+
791+ let requirement = create_exec_approval_requirement_for_command (
792+ & policy,
793+ & Features :: with_defaults ( ) ,
794+ & command,
795+ AskForApproval :: OnRequest ,
796+ & SandboxPolicy :: ReadOnly ,
797+ SandboxPermissions :: UseDefault ,
798+ )
799+ . await ;
800+
801+ assert_eq ! (
802+ requirement,
803+ ExecApprovalRequirement :: Skip {
804+ bypass_sandbox: true ,
805+ proposed_execpolicy_amendment: None ,
806+ }
807+ ) ;
808+ }
733809}
0 commit comments