pub(crate) mod apply_patch; pub(crate) mod apply_patch_spec; mod current_time; mod dynamic; pub(crate) mod extension_tools; mod get_context_remaining; pub(crate) mod get_context_remaining_spec; mod list_available_plugins_to_install; pub(crate) mod list_available_plugins_to_install_spec; mod mcp; mod mcp_resource; pub(crate) mod mcp_resource_spec; pub(crate) mod multi_agents; pub(crate) mod multi_agents_common; pub(crate) mod multi_agents_spec; pub(crate) mod multi_agents_v2; mod new_context_window; pub(crate) mod new_context_window_spec; mod plan; pub(crate) mod plan_spec; mod request_permissions; mod request_plugin_install; pub(crate) mod request_plugin_install_spec; mod request_user_input; mod request_user_input_async; pub(crate) mod request_user_input_spec; mod send_message_to_user_async; pub(crate) mod shell_spec; mod sleep; mod test_sync; pub(crate) mod test_sync_spec; mod tool_search; pub(crate) mod tool_search_spec; pub(crate) mod unified_exec; mod view_image; pub(crate) mod view_image_spec; mod wait_for_environment; use codex_file_system::FileSystemSandboxContext; use codex_sandboxing::policy_transforms::materialize_additional_permissions_with_context; use codex_sandboxing::policy_transforms::merge_permission_profiles; use codex_sandboxing::policy_transforms::normalize_additional_permissions_with_context; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::AbsolutePathBufGuard; use codex_utils_path_uri::PathUri; use serde::Deserialize; use serde_json::Map; use serde_json::Value; use crate::environment_selection::TurnEnvironmentSnapshot; use crate::function_tool::FunctionCallError; use crate::sandboxing::SandboxPermissions; use crate::session::session::Session; use crate::session::turn_context::TurnEnvironment; pub(crate) use crate::tools::code_mode::CodeModeExecuteHandler; pub(crate) use crate::tools::code_mode::CodeModeWaitHandler; pub use apply_patch::ApplyPatchHandler; use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::protocol::AskForApproval; pub use current_time::CurrentTimeHandler; pub use dynamic::DynamicToolHandler; pub use get_context_remaining::GetContextRemainingHandler; pub use list_available_plugins_to_install::ListAvailablePluginsToInstallHandler; pub use mcp::McpHandler; pub use mcp_resource::ListMcpResourceTemplatesHandler; pub use mcp_resource::ListMcpResourcesHandler; pub use mcp_resource::ReadMcpResourceHandler; pub use new_context_window::NewContextWindowHandler; pub use plan::PlanHandler; pub use request_permissions::RequestPermissionsHandler; pub use request_plugin_install::RequestPluginInstallHandler; pub use request_user_input::RequestUserInputHandler; pub use request_user_input_async::RequestUserInputAsyncHandler; pub use send_message_to_user_async::SendMessageToUserAsyncHandler; pub use sleep::SleepHandler; pub use test_sync::TestSyncHandler; pub(crate) use tool_search::ToolSearchHandlerCache; pub use unified_exec::ExecCommandHandler; pub(crate) use unified_exec::ExecCommandHandlerOptions; pub use unified_exec::WriteStdinHandler; pub use view_image::ViewImageHandler; pub(crate) use wait_for_environment::WaitForEnvironmentHandler; pub use wait_for_environment::WaitForEnvironmentToolConfig; pub(crate) fn parse_arguments(arguments: &str) -> Result where T: for<'de> Deserialize<'de>, { serde_json::from_str(arguments).map_err(|err| { FunctionCallError::RespondToModel(format!("failed to parse function arguments: {err}")) }) } fn resolve_sandbox_permissions( sandbox_permissions: Option, justification: Option<&str>, ) -> Result { if justification.is_some() && sandbox_permissions.is_none() { return Err(FunctionCallError::RespondToModel( "`justification` requires an explicit `sandbox_permissions`; use `sandbox_permissions: \"require_escalated\"` for unsandboxed execution, or omit `justification`.".to_string(), )); } Ok(sandbox_permissions.unwrap_or_default()) } fn updated_hook_command(updated_input: &Value) -> Result<&str, FunctionCallError> { updated_input .get("command") .and_then(Value::as_str) .ok_or_else(|| { FunctionCallError::RespondToModel( "hook returned updatedInput without string field `command`".to_string(), ) }) } fn rewrite_function_arguments( arguments: &str, tool_name: &str, rewrite: impl FnOnce(&mut Map), ) -> Result { let mut arguments: Value = parse_arguments(arguments)?; let Value::Object(arguments) = &mut arguments else { return Err(FunctionCallError::RespondToModel(format!( "{tool_name} arguments must be an object" ))); }; rewrite(arguments); serde_json::to_string(&arguments).map_err(|err| { FunctionCallError::RespondToModel(format!( "failed to serialize rewritten {tool_name} arguments: {err}" )) }) } fn rewrite_function_string_argument( arguments: &str, tool_name: &str, field_name: &str, value: &str, ) -> Result { rewrite_function_arguments(arguments, tool_name, |arguments| { arguments.insert(field_name.to_string(), Value::String(value.to_string())); }) } fn parse_arguments_with_base_path( arguments: &str, base_path: &AbsolutePathBuf, ) -> Result where T: for<'de> Deserialize<'de>, { let _guard = AbsolutePathBufGuard::new(base_path); parse_arguments(arguments) } fn resolve_tool_environment<'a>( environments: &'a TurnEnvironmentSnapshot, environment_id: Option<&str>, ) -> Result, FunctionCallError> { environment_id.map_or_else( || Ok(environments.primary()), |environment_id| { environments .turn_environments() .find(|environment| environment.selection.environment_id == environment_id) .map(Some) .ok_or_else(|| { FunctionCallError::RespondToModel(format!( "unknown turn environment id `{environment_id}`" )) }) }, ) } /// Validates feature/policy constraints for `with_additional_permissions` and /// normalizes any path-based permissions. Errors if the request is invalid. pub(crate) fn normalize_and_validate_additional_permissions( additional_permissions_allowed: bool, approval_policy: AskForApproval, sandbox_permissions: SandboxPermissions, additional_permissions: Option, permissions_preapproved: bool, context: &codex_protocol::permissions::FileSystemSandboxPolicyContext<'_>, ) -> Result, String> { let uses_additional_permissions = matches!( sandbox_permissions, SandboxPermissions::WithAdditionalPermissions ); if !permissions_preapproved && !additional_permissions_allowed && (uses_additional_permissions || additional_permissions.is_some()) { return Err( "additional permissions are disabled; enable `features.exec_permission_approvals` before using `with_additional_permissions`" .to_string(), ); } if uses_additional_permissions { if !permissions_preapproved && !matches!(approval_policy, AskForApproval::OnRequest) { return Err(format!( "approval policy is {approval_policy:?}; reject command — you cannot request additional permissions unless the approval policy is OnRequest" )); } let Some(additional_permissions) = additional_permissions else { return Err( "missing `additional_permissions`; provide at least one of `network` or `file_system` when using `with_additional_permissions`" .to_string(), ); }; let normalized = normalize_additional_permissions_with_context(additional_permissions, context)?; if normalized.is_empty() { return Err( "`additional_permissions` must include at least one requested permission in `network` or `file_system`" .to_string(), ); } return Ok(Some(normalized)); } if additional_permissions.is_some() { Err( "`additional_permissions` requires `sandbox_permissions` set to `with_additional_permissions`" .to_string(), ) } else { Ok(None) } } pub(super) struct EffectiveAdditionalPermissions { pub sandbox_permissions: SandboxPermissions, pub additional_permissions: Option, pub permissions_preapproved: bool, } pub(super) fn file_system_sandbox_policy_context_for_cwd<'a>( sandbox_context: &'a FileSystemSandboxContext, cwd: &'a PathUri, ) -> Option> { let mut context = sandbox_context.policy_context()?; context.cwd = cwd; Some(context) } pub(super) fn implicit_granted_permissions( sandbox_permissions: SandboxPermissions, additional_permissions: Option<&AdditionalPermissionProfile>, effective_additional_permissions: &EffectiveAdditionalPermissions, ) -> Option { if !sandbox_permissions.uses_additional_permissions() && !matches!(sandbox_permissions, SandboxPermissions::RequireEscalated) && additional_permissions.is_none() { effective_additional_permissions .additional_permissions .clone() } else { None } } pub(super) async fn apply_granted_turn_permissions( session: &Session, environment: &TurnEnvironment, cwd: &PathUri, sandbox_permissions: SandboxPermissions, additional_permissions: Option, ) -> EffectiveAdditionalPermissions { if matches!(sandbox_permissions, SandboxPermissions::RequireEscalated) { return EffectiveAdditionalPermissions { sandbox_permissions, additional_permissions, permissions_preapproved: false, }; } let environment_id = &environment.selection.environment_id; 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(), ); let effective_permissions = merge_permission_profiles( additional_permissions.as_ref(), granted_permissions.as_ref(), ); let sandbox_context = environment.sandbox_context(/*additional_permissions*/ None); let context = file_system_sandbox_policy_context_for_cwd(&sandbox_context, cwd); let preapproved_permissions = granted_permissions.as_ref().and_then(|granted| { if additional_permissions.is_none() { Some(granted.clone()) } else { effective_permissions.as_ref().and_then(|effective| { preapproved_permission_profile(effective, granted, context.as_ref()?) }) } }); let permissions_preapproved = preapproved_permissions.is_some(); // A preapproved command must execute with the stored authority, never an // unchecked merge that could reopen one of the grant's denied paths. let effective_permissions = preapproved_permissions.or(effective_permissions); let sandbox_permissions = if effective_permissions.is_some() && !sandbox_permissions.uses_additional_permissions() { SandboxPermissions::WithAdditionalPermissions } else { sandbox_permissions }; EffectiveAdditionalPermissions { sandbox_permissions, additional_permissions: effective_permissions, permissions_preapproved, } } fn preapproved_permission_profile( effective_permissions: &AdditionalPermissionProfile, granted_permissions: &AdditionalPermissionProfile, context: &codex_protocol::permissions::FileSystemSandboxPolicyContext<'_>, ) -> Option { if effective_permissions .file_system .as_ref() .is_some_and(|permissions| { permissions.entries.iter().any(|entry| { (matches!( &entry.path, codex_protocol::permissions::FileSystemPath::Special { value: codex_protocol::permissions::FileSystemSpecialPath::Tmpdir, } ) && context .temporary_directories .is_none_or(<[PathUri]>::is_empty)) || matches!( &entry.path, codex_protocol::permissions::FileSystemPath::Special { value: codex_protocol::permissions::FileSystemSpecialPath::ProjectRoots { .. }, } if context.workspace_roots.is_empty() ) }) }) { return None; } let (Ok(effective), Ok(granted)) = ( materialize_additional_permissions_with_context(effective_permissions.clone(), context), materialize_additional_permissions_with_context(granted_permissions.clone(), context), ) else { return None; }; if effective.network != granted.network { return None; } let unchanged = match (effective.file_system, granted.file_system) { (Some(effective), Some(granted)) => { effective.glob_scan_max_depth == granted.glob_scan_max_depth && effective.entries.len() == granted.entries.len() && effective .entries .iter() .all(|entry| granted.entries.contains(entry)) } (None, None) => true, (Some(_), None) | (None, Some(_)) => false, }; unchanged.then(|| granted_permissions.clone()) } #[cfg(test)] #[path = "permission_preapproval_tests.rs"] mod permission_preapproval_tests; #[cfg(test)] mod tests { use super::EffectiveAdditionalPermissions; use super::implicit_granted_permissions; use super::normalize_and_validate_additional_permissions; use super::preapproved_permission_profile; use crate::sandboxing::SandboxPermissions; use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::NetworkPermissions; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicyContext; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::GranularApprovalConfig; use codex_sandboxing::policy_transforms::intersect_permission_profiles_with_context; use codex_sandboxing::policy_transforms::merge_permission_profiles; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::PathUri; use pretty_assertions::assert_eq; use tempfile::tempdir; fn network_permissions() -> AdditionalPermissionProfile { AdditionalPermissionProfile { network: Some(NetworkPermissions { enabled: Some(true), }), ..Default::default() } } fn file_system_permissions(path: &std::path::Path) -> AdditionalPermissionProfile { AdditionalPermissionProfile { file_system: Some(FileSystemPermissions::from_read_write_roots( /*read*/ None, Some(vec![ AbsolutePathBuf::from_absolute_path(path).expect("absolute path"), ]), )), ..Default::default() } } fn local_context(cwd: &PathUri) -> FileSystemSandboxPolicyContext<'_> { FileSystemSandboxPolicyContext { cwd, workspace_roots: std::slice::from_ref(cwd), temporary_directories: None, user_home_dir: None, } } #[test] fn preapproved_permissions_work_when_request_permissions_tool_is_enabled_without_exec_permission_approvals_feature() { let cwd = tempdir().expect("tempdir"); let cwd = PathUri::from_host_native_path(cwd.path()).expect("cwd URI"); let normalized = normalize_and_validate_additional_permissions( /*additional_permissions_allowed*/ false, AskForApproval::Granular(GranularApprovalConfig { sandbox_approval: true, rules: true, skill_approval: true, request_permissions: false, mcp_elicitations: true, }), SandboxPermissions::WithAdditionalPermissions, Some(network_permissions()), /*permissions_preapproved*/ true, &local_context(&cwd), ) .expect("preapproved permissions should be allowed"); assert_eq!(normalized, Some(network_permissions())); } #[test] fn fresh_additional_permissions_still_require_exec_permission_approvals_feature() { let cwd = tempdir().expect("tempdir"); let cwd = PathUri::from_host_native_path(cwd.path()).expect("cwd URI"); let err = normalize_and_validate_additional_permissions( /*additional_permissions_allowed*/ false, AskForApproval::OnRequest, SandboxPermissions::WithAdditionalPermissions, Some(network_permissions()), /*permissions_preapproved*/ false, &local_context(&cwd), ) .expect_err("fresh inline permission requests should remain disabled"); assert_eq!( err, "additional permissions are disabled; enable `features.exec_permission_approvals` before using `with_additional_permissions`" ); } #[test] fn implicit_sticky_grants_bypass_inline_permission_validation() { let cwd = tempdir().expect("tempdir"); let granted_permissions = file_system_permissions(cwd.path()); let implicit_permissions = implicit_granted_permissions( SandboxPermissions::UseDefault, /*additional_permissions*/ None, &EffectiveAdditionalPermissions { sandbox_permissions: SandboxPermissions::WithAdditionalPermissions, additional_permissions: Some(granted_permissions.clone()), permissions_preapproved: false, }, ); assert_eq!(implicit_permissions, Some(granted_permissions)); } #[test] fn explicit_inline_permissions_do_not_use_implicit_sticky_grant_path() { let cwd = tempdir().expect("tempdir"); let requested_permissions = file_system_permissions(cwd.path()); let implicit_permissions = implicit_granted_permissions( SandboxPermissions::WithAdditionalPermissions, Some(&requested_permissions), &EffectiveAdditionalPermissions { sandbox_permissions: SandboxPermissions::WithAdditionalPermissions, additional_permissions: Some(requested_permissions.clone()), permissions_preapproved: false, }, ); assert_eq!(implicit_permissions, None); } #[test] fn relative_deny_glob_grants_remain_preapproved_after_materialization() { let cwd = tempdir().expect("tempdir"); let requested_permissions = AdditionalPermissionProfile { file_system: Some(FileSystemPermissions { entries: vec![ FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern: "**/*.env".to_string(), }, access: FileSystemAccessMode::Deny, missing_path_behavior: None, }, ], glob_scan_max_depth: None, }), ..Default::default() }; let cwd_uri = PathUri::from_host_native_path(cwd.path()).expect("cwd URI"); let stored_grant = intersect_permission_profiles_with_context( requested_permissions.clone(), requested_permissions.clone(), &local_context(&cwd_uri), ); let effective_permissions = merge_permission_profiles(Some(&requested_permissions), Some(&stored_grant)) .expect("merged permissions"); assert_eq!( preapproved_permission_profile( &effective_permissions, &stored_grant, &local_context(&cwd_uri), ), Some(stored_grant) ); } #[test] fn symbolic_tmpdir_preapproval_requires_executor_metadata() { let cwd = PathUri::parse("file:///C:/workspace").expect("Windows cwd"); let temporary_directory = PathUri::parse("file:///C:/Temp").expect("Windows temp dir"); let concrete = FileSystemSandboxEntry::new( temporary_directory.clone().into(), FileSystemAccessMode::Write, ); let symbolic = FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::Tmpdir, }, access: FileSystemAccessMode::Write, missing_path_behavior: None, }; let effective = AdditionalPermissionProfile { file_system: Some(FileSystemPermissions { entries: vec![concrete.clone(), symbolic], glob_scan_max_depth: None, }), ..Default::default() }; let granted = AdditionalPermissionProfile { file_system: Some(FileSystemPermissions { entries: vec![concrete], glob_scan_max_depth: None, }), ..Default::default() }; assert_eq!( preapproved_permission_profile(&effective, &granted, &local_context(&cwd)), None ); let empty_temporary_directories = []; let empty_tmpdir_context = FileSystemSandboxPolicyContext { temporary_directories: Some(&empty_temporary_directories), ..local_context(&cwd) }; assert_eq!( preapproved_permission_profile(&effective, &granted, &empty_tmpdir_context), None ); let temporary_directories = [temporary_directory]; let tmpdir_context = FileSystemSandboxPolicyContext { temporary_directories: Some(&temporary_directories), ..local_context(&cwd) }; assert_eq!( preapproved_permission_profile(&effective, &granted, &tmpdir_context), Some(granted) ); } }