@@ -147,41 +147,54 @@ pub(crate) async fn append_execpolicy_amendment_and_update(
147147 Ok ( ( ) )
148148}
149149
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:
158- /// - execpolicy: empty. Command: `["python"]`. Heuristics prompt -> `Some(vec!["python"])`.
159- /// - execpolicy: empty. Command: `["bash", "-c", "cd /some/folder && prog1 --option1 arg1 && prog2 --option2 arg2"]`.
160- /// Parsed commands include `cd /some/folder`, `prog1 --option1 arg1`, and `prog2 --option2 arg2`. If heuristics allow `cd` but prompt
161- /// on `prog1`, we return `Some(vec!["prog1", "--option1", "arg1"])`.
162- /// - execpolicy: contains a `prompt for prefix ["prog2"]` rule. For the same command as above,
163- /// 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 {
150+ // - If any execpolicy rule prompts, return None, because an amendment would not skip that policy requirement.
151+ // - Otherwise return the first heuristics Prompt.
152+ // Examples:
153+ // - execpolicy: empty. Command: `["python"]`. Heuristics prompt -> `Some(vec!["python"])`.
154+ // - execpolicy: empty. Command: `["bash", "-c", "cd /some/folder && prog1 --option1 arg1 && prog2 --option2 arg2"]`.
155+ // Parsed commands include `cd /some/folder`, `prog1 --option1 arg1`, and `prog2 --option2 arg2`. If heuristics allow `cd` but prompt
156+ // on `prog1`, we return `Some(vec!["prog1", "--option1", "arg1"])`.
157+ // - execpolicy: contains a `prompt for prefix ["prog2"]` rule. For the same command as above,
158+ // we return `None` because an execpolicy prompt still applies even if we amend execpolicy to allow ["prog1", "--option1", "arg1"].
159+ fn prompt_execpolicy_amendment ( matched_rules : & [ RuleMatch ] ) -> Option < ExecPolicyAmendment > {
160+ if matched_rules. iter ( ) . any ( |rule_match| {
161+ !matches ! ( rule_match, RuleMatch :: HeuristicsRuleMatch { .. } )
162+ && rule_match. decision ( ) == Decision :: Prompt
163+ } ) {
166164 return None ;
167165 }
168166
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- }
167+ matched_rules
168+ . iter ( )
169+ . find_map ( |rule_match| match rule_match {
170+ RuleMatch :: HeuristicsRuleMatch {
171+ command,
172+ decision : Decision :: Prompt ,
173+ } => Some ( ExecPolicyAmendment :: from ( command. clone ( ) ) ) ,
174+ _ => None ,
175+ } )
176+ }
177+
178+ // - Note: we only use this amendment when the command fails to run in sandbox and codex prompts the user to run outside the sandbox
179+ // - The purpose of this amendment is to bypass sandbox for similar commands in the future
180+ // - If any execpolicy rule matches, return None, because we would already be running command outside the sandbox
181+ fn allow_execpolicy_amendment ( matched_rules : & [ RuleMatch ] ) -> Option < ExecPolicyAmendment > {
182+ if matched_rules
183+ . iter ( )
184+ . any ( |rule_match| !matches ! ( rule_match, RuleMatch :: HeuristicsRuleMatch { .. } ) )
185+ {
186+ return None ;
182187 }
183188
184- first_prompt_from_heuristics. map ( ExecPolicyAmendment :: from)
189+ matched_rules
190+ . iter ( )
191+ . find_map ( |rule_match| match rule_match {
192+ RuleMatch :: HeuristicsRuleMatch {
193+ command,
194+ decision : Decision :: Allow ,
195+ } => Some ( ExecPolicyAmendment :: from ( command. clone ( ) ) ) ,
196+ _ => None ,
197+ } )
185198}
186199
187200/// Only return PROMPT_REASON when an execpolicy rule drove the prompt decision.
@@ -233,7 +246,7 @@ pub(crate) async fn create_exec_approval_requirement_for_command(
233246 ExecApprovalRequirement :: NeedsApproval {
234247 reason : derive_prompt_reason ( & evaluation) ,
235248 proposed_execpolicy_amendment : if features. enabled ( Feature :: ExecPolicy ) {
236- proposed_execpolicy_amendment ( & evaluation)
249+ prompt_execpolicy_amendment ( & evaluation. matched_rules )
237250 } else {
238251 None
239252 } ,
@@ -242,6 +255,11 @@ pub(crate) async fn create_exec_approval_requirement_for_command(
242255 }
243256 Decision :: Allow => ExecApprovalRequirement :: Skip {
244257 bypass_sandbox : has_policy_allow,
258+ proposed_execpolicy_amendment : if features. enabled ( Feature :: ExecPolicy ) {
259+ allow_execpolicy_amendment ( & evaluation. matched_rules )
260+ } else {
261+ None
262+ } ,
245263 } ,
246264 }
247265}
@@ -730,4 +748,56 @@ prefix_rule(pattern=["rm"], decision="forbidden")
730748 }
731749 ) ;
732750 }
751+
752+ #[ tokio:: test]
753+ async fn proposed_execpolicy_amendment_is_present_when_heuristics_allow ( ) {
754+ let command = vec ! [ "echo" . to_string( ) , "safe" . to_string( ) ] ;
755+
756+ let requirement = create_exec_approval_requirement_for_command (
757+ & Arc :: new ( RwLock :: new ( Policy :: empty ( ) ) ) ,
758+ & Features :: with_defaults ( ) ,
759+ & command,
760+ AskForApproval :: OnRequest ,
761+ & SandboxPolicy :: ReadOnly ,
762+ SandboxPermissions :: UseDefault ,
763+ )
764+ . await ;
765+
766+ assert_eq ! (
767+ requirement,
768+ ExecApprovalRequirement :: Skip {
769+ bypass_sandbox: false ,
770+ proposed_execpolicy_amendment: Some ( ExecPolicyAmendment :: new( command) ) ,
771+ }
772+ ) ;
773+ }
774+
775+ #[ tokio:: test]
776+ async fn proposed_execpolicy_amendment_is_suppressed_when_policy_matches_allow ( ) {
777+ let policy_src = r#"prefix_rule(pattern=["echo"], decision="allow")"# ;
778+ let mut parser = PolicyParser :: new ( ) ;
779+ parser
780+ . parse ( "test.codexpolicy" , policy_src)
781+ . expect ( "parse policy" ) ;
782+ let policy = Arc :: new ( RwLock :: new ( parser. build ( ) ) ) ;
783+ let command = vec ! [ "echo" . to_string( ) , "safe" . to_string( ) ] ;
784+
785+ let requirement = create_exec_approval_requirement_for_command (
786+ & policy,
787+ & Features :: with_defaults ( ) ,
788+ & command,
789+ AskForApproval :: OnRequest ,
790+ & SandboxPolicy :: ReadOnly ,
791+ SandboxPermissions :: UseDefault ,
792+ )
793+ . await ;
794+
795+ assert_eq ! (
796+ requirement,
797+ ExecApprovalRequirement :: Skip {
798+ bypass_sandbox: true ,
799+ proposed_execpolicy_amendment: None ,
800+ }
801+ ) ;
802+ }
733803}
0 commit comments