diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index b3dcf504e..d8f660e2a 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -2107,12 +2107,18 @@ impl Session { args: RequestPermissionsArgs, cancellation_token: CancellationToken, ) -> Option { - self.request_permissions_for_cwd( + let Some(turn_environment) = turn_context.environments.primary() else { + return Some(RequestPermissionsResponse { + permissions: RequestPermissionProfile::default(), + scope: PermissionGrantScope::Turn, + strict_auto_review: false, + }); + }; + self.request_permissions_for_environment( turn_context, call_id, args, - #[allow(deprecated)] - turn_context.cwd.clone(), + turn_environment.selection(), cancellation_token, ) .await @@ -2122,12 +2128,12 @@ impl Session { clippy::await_holding_invalid_type, reason = "active turn checks and turn state updates must remain atomic" )] - pub(crate) async fn request_permissions_for_cwd( + pub(crate) async fn request_permissions_for_environment( self: &Arc, turn_context: &Arc, call_id: String, args: RequestPermissionsArgs, - cwd: AbsolutePathBuf, + environment: TurnEnvironmentSelection, cancellation_token: CancellationToken, ) -> Option { match turn_context.as_ref().approval_policy.value() { @@ -2221,10 +2227,11 @@ impl Session { let response = Self::normalize_request_permissions_response( requested_permissions, response, - cwd.as_path(), + environment.cwd.as_path(), ); self.record_granted_request_permissions_for_turn( &response, + &environment.environment_id, originating_turn_state.as_ref(), ) .await; @@ -2242,7 +2249,7 @@ impl Session { PendingRequestPermissions { tx_response, requested_permissions: requested_permissions.clone(), - cwd: cwd.clone(), + environment: environment.clone(), }, ) } @@ -2259,7 +2266,7 @@ impl Session { started_at_ms: now_unix_timestamp_ms(), reason: args.reason, permissions: requested_permissions, - cwd: Some(cwd), + cwd: Some(environment.cwd), }); self.send_event(turn_context.as_ref(), event).await; tokio::select! { @@ -2276,6 +2283,33 @@ impl Session { } } + pub(crate) async fn request_permissions_for_cwd( + self: &Arc, + turn_context: &Arc, + call_id: String, + args: RequestPermissionsArgs, + cwd: AbsolutePathBuf, + cancellation_token: CancellationToken, + ) -> Option { + let Some(primary_environment) = turn_context.environments.primary() else { + return Some(RequestPermissionsResponse { + permissions: RequestPermissionProfile::default(), + scope: PermissionGrantScope::Turn, + strict_auto_review: false, + }); + }; + let mut environment = primary_environment.selection(); + environment.cwd = cwd; + self.request_permissions_for_environment( + turn_context, + call_id, + args, + environment, + cancellation_token, + ) + .await + } + #[expect( clippy::await_holding_invalid_type, reason = "active turn checks and turn state updates must remain atomic" @@ -2370,10 +2404,11 @@ impl Session { let response = Self::normalize_request_permissions_response( entry.requested_permissions, response, - entry.cwd.as_path(), + entry.environment.cwd.as_path(), ); self.record_granted_request_permissions_for_turn( &response, + &entry.environment.environment_id, originating_turn_state.as_ref(), ) .await; @@ -2417,6 +2452,7 @@ impl Session { async fn record_granted_request_permissions_for_turn( &self, response: &RequestPermissionsResponse, + environment_id: &str, originating_turn_state: Option<&Arc>>, ) { if response.permissions.is_empty() { @@ -2428,7 +2464,7 @@ impl Session { let mut ts = turn_state.lock().await; let permissions: AdditionalPermissionProfile = response.permissions.clone().into(); - ts.record_granted_permissions(permissions); + ts.record_granted_permissions(environment_id, permissions); if response.strict_auto_review { ts.enable_strict_auto_review(); } @@ -2436,7 +2472,10 @@ impl Session { } PermissionGrantScope::Session => { let mut state = self.state.lock().await; - state.record_granted_permissions(response.permissions.clone().into()); + state.record_granted_permissions( + environment_id, + response.permissions.clone().into(), + ); } } } @@ -2445,11 +2484,14 @@ impl Session { clippy::await_holding_invalid_type, reason = "active turn reads must stay consistent with the matching turn state" )] - pub(crate) async fn granted_turn_permissions(&self) -> Option { + pub(crate) async fn granted_turn_permissions( + &self, + environment_id: &str, + ) -> Option { let active = self.active_turn.lock().await; let active = active.as_ref()?; let ts = active.turn_state.lock().await; - ts.granted_permissions() + ts.granted_permissions(environment_id) } #[expect( @@ -2465,9 +2507,12 @@ impl Session { ts.strict_auto_review_enabled() } - pub(crate) async fn granted_session_permissions(&self) -> Option { + pub(crate) async fn granted_session_permissions( + &self, + environment_id: &str, + ) -> Option { let state = self.state.lock().await; - state.granted_permissions() + state.granted_permissions(environment_id) } #[expect( diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index d4b4942a8..260e91c09 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -73,6 +73,7 @@ use crate::tools::context::ToolInvocation; use crate::tools::context::ToolPayload; use crate::tools::handlers::CreateGoalHandler; use crate::tools::handlers::ExecCommandHandler; +use crate::tools::handlers::RequestPermissionsHandler; use crate::tools::handlers::ShellCommandHandler; use crate::tools::handlers::UpdateGoalHandler; use crate::tools::registry::ToolExecutor; @@ -5155,7 +5156,12 @@ async fn notify_request_permissions_response_ignores_unmatched_call_id() { ) .await; - assert_eq!(session.granted_turn_permissions().await, None); + assert_eq!( + session + .granted_turn_permissions(codex_exec_server::LOCAL_ENVIRONMENT_ID) + .await, + None + ); } #[tokio::test] @@ -5182,16 +5188,84 @@ async fn record_granted_request_permissions_for_turn_uses_originating_turn() { scope: PermissionGrantScope::Turn, strict_auto_review: false, }, + codex_exec_server::LOCAL_ENVIRONMENT_ID, Some(&originating_turn_state), ) .await; assert_eq!( - originating_turn_state.lock().await.granted_permissions(), + originating_turn_state + .lock() + .await + .granted_permissions(codex_exec_server::LOCAL_ENVIRONMENT_ID), Some(requested_permissions.into()) ); - assert_eq!(current_turn_state.lock().await.granted_permissions(), None); - assert_eq!(session.granted_turn_permissions().await, None); + assert_eq!( + current_turn_state + .lock() + .await + .granted_permissions(codex_exec_server::LOCAL_ENVIRONMENT_ID), + None + ); + assert_eq!( + session + .granted_turn_permissions(codex_exec_server::LOCAL_ENVIRONMENT_ID) + .await, + None + ); +} + +#[tokio::test] +async fn request_permission_grants_are_environment_keyed() { + let (session, _turn_context) = make_session_and_context().await; + let originating_active_turn = ActiveTurn::default(); + let originating_turn_state = Arc::clone(&originating_active_turn.turn_state); + *session.active_turn.lock().await = Some(originating_active_turn); + + let requested_permissions = RequestPermissionProfile { + network: Some(codex_protocol::models::NetworkPermissions { + enabled: Some(true), + }), + ..RequestPermissionProfile::default() + }; + session + .record_granted_request_permissions_for_turn( + &codex_protocol::request_permissions::RequestPermissionsResponse { + permissions: requested_permissions.clone(), + scope: PermissionGrantScope::Turn, + strict_auto_review: false, + }, + "remote", + Some(&originating_turn_state), + ) + .await; + + { + let turn_state = originating_turn_state.lock().await; + assert_eq!( + turn_state.granted_permissions("remote"), + Some(requested_permissions.clone().into()) + ); + assert_eq!(turn_state.granted_permissions("local"), None); + } + + session + .record_granted_request_permissions_for_turn( + &codex_protocol::request_permissions::RequestPermissionsResponse { + permissions: requested_permissions.clone(), + scope: PermissionGrantScope::Session, + strict_auto_review: false, + }, + "remote", + /*originating_turn_state*/ None, + ) + .await; + + assert_eq!( + session.granted_session_permissions("remote").await, + Some(requested_permissions.into()) + ); + assert_eq!(session.granted_session_permissions("local").await, None); } #[tokio::test] @@ -5214,6 +5288,7 @@ async fn enable_strict_auto_review_for_turn_uses_originating_turn() { scope: PermissionGrantScope::Turn, strict_auto_review: true, }, + codex_exec_server::LOCAL_ENVIRONMENT_ID, Some(&originating_turn_state), ) .await; @@ -5333,6 +5408,107 @@ async fn request_permissions_emits_event_when_granular_policy_allows_requests() assert_eq!(response, Some(expected_response)); } +#[tokio::test] +async fn request_permissions_tool_resolves_relative_paths_against_primary_environment() { + let (session, mut turn_context, rx) = make_session_and_context_with_rx().await; + *session.active_turn.lock().await = Some(ActiveTurn::default()); + let environment_cwd = { + #[allow(deprecated)] + let legacy_cwd = turn_context.cwd.clone(); + legacy_cwd.join("request-permissions-environment") + }; + std::fs::create_dir_all(environment_cwd.as_path()).expect("create environment cwd"); + let turn_context_mut = Arc::get_mut(&mut turn_context).expect("single thread settings ref"); + turn_context_mut + .approval_policy + .set(AskForApproval::Granular(GranularApprovalConfig { + sandbox_approval: true, + rules: true, + skill_approval: true, + request_permissions: true, + mcp_elicitations: true, + })) + .expect("test setup should allow updating approval policy"); + turn_context_mut.environments.turn_environments[0].cwd = environment_cwd.clone(); + + let call_id = "call-1".to_string(); + let handler = RequestPermissionsHandler; + let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new())); + let handle = tokio::spawn({ + let session = Arc::clone(&session); + let turn_context = Arc::clone(&turn_context); + let tracker = Arc::clone(&tracker); + let call_id = call_id.clone(); + async move { + handler + .handle(ToolInvocation { + session, + turn: turn_context, + cancellation_token: CancellationToken::new(), + tracker, + call_id, + tool_name: codex_tools::ToolName::plain("request_permissions"), + source: ToolCallSource::Direct, + payload: ToolPayload::Function { + arguments: json!({ + "reason": "need write", + "permissions": { + "file_system": { + "entries": [{ + "path": { + "type": "path", + "path": "relative.txt", + }, + "access": "write", + }], + }, + }, + }) + .to_string(), + }, + }) + .await + } + }); + + let request_event = tokio::time::timeout(StdDuration::from_secs(1), rx.recv()) + .await + .expect("request_permissions event timed out") + .expect("request_permissions event missing"); + let EventMsg::RequestPermissions(request) = request_event.msg else { + panic!("expected request_permissions event"); + }; + let expected_permissions = RequestPermissionProfile { + file_system: Some(FileSystemPermissions { + entries: vec![FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: environment_cwd.join("relative.txt"), + }, + access: FileSystemAccessMode::Write, + }], + glob_scan_max_depth: None, + }), + ..Default::default() + }; + assert_eq!(request.permissions, expected_permissions); + + session + .notify_request_permissions_response( + &request.call_id, + codex_protocol::request_permissions::RequestPermissionsResponse { + permissions: request.permissions, + scope: PermissionGrantScope::Turn, + strict_auto_review: false, + }, + ) + .await; + tokio::time::timeout(StdDuration::from_secs(1), handle) + .await + .expect("request_permissions handler timed out") + .expect("request_permissions handler join error") + .expect("request_permissions handler should succeed"); +} + #[tokio::test] async fn request_permissions_response_materializes_session_cwd_grants_before_recording() { let (session, mut turn_context, rx) = make_session_and_context_with_rx().await; @@ -5425,7 +5601,9 @@ async fn request_permissions_response_materializes_session_cwd_grants_before_rec assert_eq!(response, Some(expected_response)); assert_eq!( - session.granted_session_permissions().await, + session + .granted_session_permissions(codex_exec_server::LOCAL_ENVIRONMENT_ID) + .await, Some(expected_permissions.into()) ); } @@ -10450,7 +10628,12 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() { ); pretty_assertions::assert_eq!(output, expected); - pretty_assertions::assert_eq!(session.granted_turn_permissions().await, None); + pretty_assertions::assert_eq!( + session + .granted_turn_permissions(codex_exec_server::LOCAL_ENVIRONMENT_ID) + .await, + None + ); // The rejection should not poison the non-escalated path for the same // command. Force DangerFullAccess so this check stays focused on approval diff --git a/codex-rs/core/src/session/tests/guardian_tests.rs b/codex-rs/core/src/session/tests/guardian_tests.rs index 2977e6bdf..e8fccd7d7 100644 --- a/codex-rs/core/src/session/tests/guardian_tests.rs +++ b/codex-rs/core/src/session/tests/guardian_tests.rs @@ -148,7 +148,9 @@ async fn request_permissions_routes_to_guardian_when_reviewer_is_enabled() { }) ); assert_eq!( - session.granted_turn_permissions().await, + session + .granted_turn_permissions(codex_exec_server::LOCAL_ENVIRONMENT_ID) + .await, Some(requested_permissions.into()) ); @@ -246,7 +248,12 @@ async fn request_permissions_guardian_review_stops_when_cancelled() { .expect("request_permissions should stop when cancelled") .expect("request_permissions task should not panic"); assert_eq!(response, None); - assert_eq!(session.granted_turn_permissions().await, None); + assert_eq!( + session + .granted_turn_permissions(codex_exec_server::LOCAL_ENVIRONMENT_ID) + .await, + None + ); } #[tokio::test] @@ -380,6 +387,7 @@ async fn strict_auto_review_turn_grant_forces_guardian_for_shell_command_policy_ scope: PermissionGrantScope::Turn, strict_auto_review: true, }, + codex_exec_server::LOCAL_ENVIRONMENT_ID, Some(&originating_turn_state), ) .await; @@ -564,12 +572,15 @@ async fn shell_command_allows_sticky_turn_permissions_without_inline_request_per let mut active_turn = session.active_turn.lock().await; let active_turn = active_turn.as_mut().expect("active turn"); let mut turn_state = active_turn.turn_state.lock().await; - turn_state.record_granted_permissions(PermissionProfile { - network: Some(NetworkPermissions { - enabled: Some(true), - }), - ..Default::default() - }); + turn_state.record_granted_permissions( + codex_exec_server::LOCAL_ENVIRONMENT_ID, + PermissionProfile { + network: Some(NetworkPermissions { + enabled: Some(true), + }), + ..Default::default() + }, + ); } let session = Arc::new(session); diff --git a/codex-rs/core/src/state/session.rs b/codex-rs/core/src/state/session.rs index 2663943f8..024f1904c 100644 --- a/codex-rs/core/src/state/session.rs +++ b/codex-rs/core/src/state/session.rs @@ -3,6 +3,7 @@ use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::ResponseItem; use codex_sandboxing::policy_transforms::merge_permission_profiles; +use std::collections::HashMap; use std::collections::HashSet; use std::collections::VecDeque; @@ -37,7 +38,7 @@ pub(crate) struct SessionState { pub(crate) startup_prewarm: Option, pub(crate) active_connector_selection: HashSet, pub(crate) pending_session_start_sources: VecDeque, - granted_permissions: Option, + granted_permissions_by_environment_id: HashMap, next_turn_is_first: bool, } @@ -57,7 +58,7 @@ impl SessionState { startup_prewarm: None, active_connector_selection: HashSet::new(), pending_session_start_sources: VecDeque::new(), - granted_permissions: None, + granted_permissions_by_environment_id: HashMap::new(), next_turn_is_first: true, } } @@ -235,13 +236,29 @@ impl SessionState { self.pending_session_start_sources.pop_front() } - pub(crate) fn record_granted_permissions(&mut self, permissions: AdditionalPermissionProfile) { - self.granted_permissions = - merge_permission_profiles(self.granted_permissions.as_ref(), Some(&permissions)); + pub(crate) fn record_granted_permissions( + &mut self, + environment_id: &str, + permissions: AdditionalPermissionProfile, + ) { + let granted_permissions = merge_permission_profiles( + self.granted_permissions_by_environment_id + .get(environment_id), + Some(&permissions), + ); + if let Some(granted_permissions) = granted_permissions { + self.granted_permissions_by_environment_id + .insert(environment_id.to_string(), granted_permissions); + } } - pub(crate) fn granted_permissions(&self) -> Option { - self.granted_permissions.clone() + pub(crate) fn granted_permissions( + &self, + environment_id: &str, + ) -> Option { + self.granted_permissions_by_environment_id + .get(environment_id) + .cloned() } } diff --git a/codex-rs/core/src/state/turn.rs b/codex-rs/core/src/state/turn.rs index fe0923519..eaf26ba85 100644 --- a/codex-rs/core/src/state/turn.rs +++ b/codex-rs/core/src/state/turn.rs @@ -1,6 +1,5 @@ //! Turn-scoped state and active turn metadata scaffolding. -use codex_sandboxing::policy_transforms::merge_permission_profiles; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::Mutex; @@ -10,11 +9,12 @@ use tokio_util::task::AbortOnDropHandle; use codex_extension_api::ExtensionData; use codex_protocol::dynamic_tools::DynamicToolResponse; +use codex_protocol::protocol::TurnEnvironmentSelection; use codex_protocol::request_permissions::RequestPermissionProfile; use codex_protocol::request_permissions::RequestPermissionsResponse; use codex_protocol::request_user_input::RequestUserInputResponse; use codex_rmcp_client::ElicitationResponse; -use codex_utils_absolute_path::AbsolutePathBuf; +use codex_sandboxing::policy_transforms::merge_permission_profiles; use rmcp::model::RequestId; use tokio::sync::oneshot; @@ -90,7 +90,7 @@ pub(crate) struct TurnState { pending_dynamic_tools: HashMap>, pub(crate) pending_input: TurnInputQueue, mailbox_delivery_phase: MailboxDeliveryPhase, - granted_permissions: Option, + granted_permissions_by_environment_id: HashMap, strict_auto_review_enabled: bool, pub(crate) tool_calls: u64, pub(crate) has_memory_citation: bool, @@ -100,7 +100,7 @@ pub(crate) struct TurnState { pub(crate) struct PendingRequestPermissions { pub(crate) tx_response: oneshot::Sender, pub(crate) requested_permissions: RequestPermissionProfile, - pub(crate) cwd: AbsolutePathBuf, + pub(crate) environment: TurnEnvironmentSelection, } impl TurnState { @@ -204,13 +204,29 @@ impl TurnState { self.mailbox_delivery_phase = phase; } - pub(crate) fn record_granted_permissions(&mut self, permissions: AdditionalPermissionProfile) { - self.granted_permissions = - merge_permission_profiles(self.granted_permissions.as_ref(), Some(&permissions)); + pub(crate) fn record_granted_permissions( + &mut self, + environment_id: &str, + permissions: AdditionalPermissionProfile, + ) { + let granted_permissions = merge_permission_profiles( + self.granted_permissions_by_environment_id + .get(environment_id), + Some(&permissions), + ); + if let Some(granted_permissions) = granted_permissions { + self.granted_permissions_by_environment_id + .insert(environment_id.to_string(), granted_permissions); + } } - pub(crate) fn granted_permissions(&self) -> Option { - self.granted_permissions.clone() + pub(crate) fn granted_permissions( + &self, + environment_id: &str, + ) -> Option { + self.granted_permissions_by_environment_id + .get(environment_id) + .cloned() } pub(crate) fn enable_strict_auto_review(&mut self) { diff --git a/codex-rs/core/src/tools/handlers/apply_patch.rs b/codex-rs/core/src/tools/handlers/apply_patch.rs index bfc3ab94b..c12ba0517 100644 --- a/codex-rs/core/src/tools/handlers/apply_patch.rs +++ b/codex-rs/core/src/tools/handlers/apply_patch.rs @@ -265,6 +265,7 @@ fn apply_patch_payload_command(payload: &ToolPayload) -> Option { async fn effective_patch_permissions( session: &Session, turn: &TurnContext, + environment_id: &str, action: &ApplyPatchAction, cwd: &AbsolutePathBuf, ) -> ( @@ -274,8 +275,14 @@ async fn effective_patch_permissions( ) { let file_paths = file_paths_for_action(action); let granted_permissions = merge_permission_profiles( - session.granted_session_permissions().await.as_ref(), - session.granted_turn_permissions().await.as_ref(), + session + .granted_session_permissions(environment_id) + .await + .as_ref(), + session + .granted_turn_permissions(environment_id) + .await + .as_ref(), ); let base_file_system_sandbox_policy = turn.file_system_sandbox_policy(); let file_system_sandbox_policy = effective_file_system_sandbox_policy( @@ -284,6 +291,7 @@ async fn effective_patch_permissions( ); let effective_additional_permissions = apply_granted_turn_permissions( session, + environment_id, cwd.as_path(), crate::sandboxing::SandboxPermissions::UseDefault, write_permissions_for_paths(&file_paths, &file_system_sandbox_policy, cwd), @@ -353,8 +361,14 @@ impl ToolExecutor for ApplyPatchHandler { { codex_apply_patch::MaybeApplyPatchVerified::Body(changes) => { let (file_paths, effective_additional_permissions, file_system_sandbox_policy) = - effective_patch_permissions(session.as_ref(), turn.as_ref(), &changes, &cwd) - .await; + effective_patch_permissions( + session.as_ref(), + turn.as_ref(), + &turn_environment.environment_id, + &changes, + &cwd, + ) + .await; match apply_patch::apply_patch(turn.as_ref(), &file_system_sandbox_policy, changes) .await { @@ -506,7 +520,14 @@ pub(crate) async fn intercept_apply_patch( { codex_apply_patch::MaybeApplyPatchVerified::Body(changes) => { let (approval_keys, effective_additional_permissions, file_system_sandbox_policy) = - effective_patch_permissions(session.as_ref(), turn.as_ref(), &changes, cwd).await; + effective_patch_permissions( + session.as_ref(), + turn.as_ref(), + &turn_environment.environment_id, + &changes, + cwd, + ) + .await; match apply_patch::apply_patch(turn.as_ref(), &file_system_sandbox_policy, changes) .await { diff --git a/codex-rs/core/src/tools/handlers/mod.rs b/codex-rs/core/src/tools/handlers/mod.rs index b2cbe6290..d6963f076 100644 --- a/codex-rs/core/src/tools/handlers/mod.rs +++ b/codex-rs/core/src/tools/handlers/mod.rs @@ -250,7 +250,8 @@ pub(super) fn implicit_granted_permissions( pub(super) async fn apply_granted_turn_permissions( session: &Session, - cwd: &std::path::Path, + environment_id: &str, + cwd: &Path, sandbox_permissions: SandboxPermissions, additional_permissions: Option, ) -> EffectiveAdditionalPermissions { @@ -262,8 +263,8 @@ pub(super) async fn apply_granted_turn_permissions( }; } - let granted_session_permissions = session.granted_session_permissions().await; - let granted_turn_permissions = session.granted_turn_permissions().await; + let granted_session_permissions = session.granted_session_permissions(environment_id).await; + let granted_turn_permissions = session.granted_turn_permissions(environment_id).await; let granted_permissions = merge_permission_profiles( granted_session_permissions.as_ref(), granted_turn_permissions.as_ref(), diff --git a/codex-rs/core/src/tools/handlers/request_permissions.rs b/codex-rs/core/src/tools/handlers/request_permissions.rs index 70a31fe0e..f4a89fe0c 100644 --- a/codex-rs/core/src/tools/handlers/request_permissions.rs +++ b/codex-rs/core/src/tools/handlers/request_permissions.rs @@ -48,9 +48,13 @@ impl ToolExecutor for RequestPermissionsHandler { } }; - #[allow(deprecated)] + let Some(turn_environment) = turn.environments.primary() else { + return Err(FunctionCallError::RespondToModel( + "request_permissions requires a primary environment".to_string(), + )); + }; let mut args: RequestPermissionsArgs = - parse_arguments_with_base_path(&arguments, &turn.cwd)?; + parse_arguments_with_base_path(&arguments, &turn_environment.cwd)?; args.permissions = normalize_additional_permissions(args.permissions.into()) .map(codex_protocol::request_permissions::RequestPermissionProfile::from) .map_err(FunctionCallError::RespondToModel)?; diff --git a/codex-rs/core/src/tools/handlers/shell.rs b/codex-rs/core/src/tools/handlers/shell.rs index c890e0d8d..504653cf7 100644 --- a/codex-rs/core/src/tools/handlers/shell.rs +++ b/codex-rs/core/src/tools/handlers/shell.rs @@ -84,10 +84,10 @@ async fn run_exec_like(args: RunExecLikeArgs) -> Result for ExecCommandHandler { let requested_additional_permissions = additional_permissions.clone(); let effective_additional_permissions = apply_granted_turn_permissions( context.session.as_ref(), + &turn_environment.environment_id, cwd.as_path(), sandbox_permissions, additional_permissions,