@@ -167,6 +167,7 @@ pub enum ExecPolicyUpdateError {
167167
168168pub ( crate ) struct ExecPolicyManager {
169169 policy : ArcSwap < Policy > ,
170+ update_lock : tokio:: sync:: Mutex < ( ) > ,
170171}
171172
172173pub ( crate ) struct ExecApprovalRequest < ' a > {
@@ -181,6 +182,7 @@ impl ExecPolicyManager {
181182 pub ( crate ) fn new ( policy : Arc < Policy > ) -> Self {
182183 Self {
183184 policy : ArcSwap :: from ( policy) ,
185+ update_lock : tokio:: sync:: Mutex :: new ( ( ) ) ,
184186 }
185187 }
186188
@@ -285,6 +287,7 @@ impl ExecPolicyManager {
285287 codex_home : & Path ,
286288 amendment : & ExecPolicyAmendment ,
287289 ) -> Result < ( ) , ExecPolicyUpdateError > {
290+ let _update_guard = self . update_lock . lock ( ) . await ;
288291 let policy_path = default_policy_path ( codex_home) ;
289292 spawn_blocking ( {
290293 let policy_path = policy_path. clone ( ) ;
@@ -298,14 +301,6 @@ impl ExecPolicyManager {
298301 source,
299302 } ) ?;
300303
301- self . apply_amendment_in_memory ( amendment) ?;
302- Ok ( ( ) )
303- }
304-
305- pub ( crate ) fn apply_amendment_in_memory (
306- & self ,
307- amendment : & ExecPolicyAmendment ,
308- ) -> Result < ( ) , ExecPolicyUpdateError > {
309304 let current_policy = self . current ( ) ;
310305 let match_options = MatchOptions {
311306 resolve_host_executables : true ,
@@ -323,7 +318,7 @@ impl ExecPolicyManager {
323318 return Ok ( ( ) ) ;
324319 }
325320
326- let mut updated_policy = self . current ( ) . as_ref ( ) . clone ( ) ;
321+ let mut updated_policy = current_policy . as_ref ( ) . clone ( ) ;
327322 updated_policy. add_prefix_rule ( & amendment. command , Decision :: Allow ) ?;
328323 self . policy . store ( Arc :: new ( updated_policy) ) ;
329324 Ok ( ( ) )
@@ -337,6 +332,7 @@ impl ExecPolicyManager {
337332 decision : Decision ,
338333 justification : Option < String > ,
339334 ) -> Result < ( ) , ExecPolicyUpdateError > {
335+ let _update_guard = self . update_lock . lock ( ) . await ;
340336 let policy_path = default_policy_path ( codex_home) ;
341337 let host = host. to_string ( ) ;
342338 spawn_blocking ( {
@@ -1920,6 +1916,42 @@ prefix_rule(pattern=["git"], decision="prompt")
19201916 ) ) ;
19211917 }
19221918
1919+ #[ tokio:: test]
1920+ async fn concurrent_execpolicy_amendments_preserve_both_rules ( ) {
1921+ let codex_home = tempdir ( ) . expect ( "create temp dir" ) ;
1922+ let manager = Arc :: new ( ExecPolicyManager :: default ( ) ) ;
1923+ let echo = ExecPolicyAmendment :: new ( vec ! [ "echo" . to_string( ) ] ) ;
1924+ let cargo = ExecPolicyAmendment :: new ( vec ! [ "cargo" . to_string( ) , "test" . to_string( ) ] ) ;
1925+
1926+ let ( echo_result, cargo_result) = tokio:: join!(
1927+ manager. append_amendment_and_update( codex_home. path( ) , & echo) ,
1928+ manager. append_amendment_and_update( codex_home. path( ) , & cargo) ,
1929+ ) ;
1930+ echo_result. expect ( "update echo policy" ) ;
1931+ cargo_result. expect ( "update cargo policy" ) ;
1932+
1933+ let updated_policy = manager. current ( ) ;
1934+ let echo_evaluation = updated_policy. check ( & [ "echo" . to_string ( ) ] , & |_| Decision :: Forbidden ) ;
1935+ assert_eq ! ( echo_evaluation. decision, Decision :: Allow ) ;
1936+ assert ! ( echo_evaluation. matched_rules. iter( ) . any( |rule_match| {
1937+ is_policy_match( rule_match) && rule_match. decision( ) == Decision :: Allow
1938+ } ) ) ;
1939+
1940+ let cargo_evaluation = updated_policy
1941+ . check ( & [ "cargo" . to_string ( ) , "test" . to_string ( ) ] , & |_| {
1942+ Decision :: Forbidden
1943+ } ) ;
1944+ assert_eq ! ( cargo_evaluation. decision, Decision :: Allow ) ;
1945+ assert ! ( cargo_evaluation. matched_rules. iter( ) . any( |rule_match| {
1946+ is_policy_match( rule_match) && rule_match. decision( ) == Decision :: Allow
1947+ } ) ) ;
1948+
1949+ let contents = fs:: read_to_string ( default_policy_path ( codex_home. path ( ) ) )
1950+ . expect ( "policy file should have been created" ) ;
1951+ assert ! ( contents. contains( r#"prefix_rule(pattern=["echo"], decision="allow")"# ) ) ;
1952+ assert ! ( contents. contains( r#"prefix_rule(pattern=["cargo", "test"], decision="allow")"# ) ) ;
1953+ }
1954+
19231955 #[ tokio:: test]
19241956 async fn proposed_execpolicy_amendment_is_present_for_single_command_without_policy_match ( ) {
19251957 let command = vec ! [ "cargo" . to_string( ) , "build" . to_string( ) ] ;
0 commit comments