diff --git a/codex-rs/core/src/context/world_state/permissions.rs b/codex-rs/core/src/context/world_state/permissions.rs index 635a9fea99..90c13d94f1 100644 --- a/codex-rs/core/src/context/world_state/permissions.rs +++ b/codex-rs/core/src/context/world_state/permissions.rs @@ -10,6 +10,7 @@ use codex_execpolicy::Policy; use codex_prompts::ApprovalPromptContext; use codex_protocol::models::PermissionProfile; use codex_protocol::models::format_allow_prefixes; +use codex_protocol::permissions::FileSystemSandboxPolicyContext; use codex_protocol::protocol::AskForApproval; use serde::Deserialize; use serde::Serialize; @@ -34,12 +35,14 @@ pub(crate) enum PermissionsSnapshot { } impl PermissionsState { + #[allow(clippy::too_many_arguments)] pub(crate) fn new( permission_profile: &PermissionProfile, approval_policy: AskForApproval, approval_context: ApprovalPromptContext<'_>, exec_policy: &Policy, cwd: &Path, + paths: Option<&FileSystemSandboxPolicyContext<'_>>, exec_permission_approvals_enabled: bool, request_permissions_tool_enabled: bool, ) -> Self { @@ -50,6 +53,7 @@ impl PermissionsState { approval_context, exec_policy, cwd, + paths, exec_permission_approvals_enabled, request_permissions_tool_enabled, ) diff --git a/codex-rs/core/src/context/world_state/permissions_tests.rs b/codex-rs/core/src/context/world_state/permissions_tests.rs index 978a5da3a7..5d994a6ac6 100644 --- a/codex-rs/core/src/context/world_state/permissions_tests.rs +++ b/codex-rs/core/src/context/world_state/permissions_tests.rs @@ -237,6 +237,7 @@ fn permissions_state( ApprovalPromptContext::new(ApprovalsReviewer::User, model_messages), &Policy::empty(), Path::new("/workspace"), + /*paths*/ None, /*exec_permission_approvals_enabled*/ false, /*request_permissions_tool_enabled*/ false, ) @@ -250,6 +251,7 @@ fn permissions_state_with_default_messages(exec_policy: &Policy) -> PermissionsS ApprovalPromptContext::new(ApprovalsReviewer::User, model_messages), exec_policy, Path::new("/workspace"), + /*paths*/ None, /*exec_permission_approvals_enabled*/ false, /*request_permissions_tool_enabled*/ false, ) diff --git a/codex-rs/core/src/session/world_state.rs b/codex-rs/core/src/session/world_state.rs index 249aca8bd4..d72080027b 100644 --- a/codex-rs/core/src/session/world_state.rs +++ b/codex-rs/core/src/session/world_state.rs @@ -24,6 +24,7 @@ use crate::context::world_state::WorldState; use codex_connectors::AppToolPolicyEvaluator; use codex_extension_api::WorldStateContributionInput; use codex_features::Feature; +use codex_file_system::FileSystemSandboxContext; use codex_prompts::ApprovalPromptContext; use codex_prompts::ResolvedModelMessages; use codex_prompts::render_model_instructions; @@ -166,6 +167,14 @@ impl Session { .current_for_prefix_rules(turn_context.allow_prefix_rules()); if turn_context.config.include_permissions_instructions { let environment = step_context.environments.primary(); + let sandbox = environment + .filter(|environment| environment.environment.is_remote()) + .map(|environment| { + environment.sandbox_context(/*additional_permissions*/ None) + }); + let paths = sandbox + .as_ref() + .map(FileSystemSandboxContext::policy_context); let permission_profile = turn_context.permission_profile_for_environments(&step_context.environments); #[allow(deprecated)] @@ -178,6 +187,7 @@ impl Session { ApprovalPromptContext::new(settings.approvals_reviewer(), model_messages), exec_policy.as_ref(), &cwd, + paths.as_ref(), turn_context .config .features diff --git a/codex-rs/core/tests/suite/unified_exec_process_events.rs b/codex-rs/core/tests/suite/unified_exec_process_events.rs index 4e2210e851..ff85b4fba4 100644 --- a/codex-rs/core/tests/suite/unified_exec_process_events.rs +++ b/codex-rs/core/tests/suite/unified_exec_process_events.rs @@ -729,6 +729,28 @@ timeout = 900 )]), NetworkSandboxPolicy::Enabled, ) + } else if foreign_cwd + && matches!( + scenario, + PushedExecScenario::Complete | PushedExecScenario::ElevatedPowerShell + ) + { + let mut file_system = PermissionProfile::workspace_write().file_system_sandbox_policy(); + file_system.entries.extend([ + FileSystemSandboxEntry::new( + FileSystemPath::Path { + path: "file:///C:/workspace/blocked".parse()?, + }, + FileSystemAccessMode::Deny, + ), + FileSystemSandboxEntry::new( + FileSystemPath::GlobPattern { + pattern: r"C:\workspace\private\**".into(), + }, + FileSystemAccessMode::Deny, + ), + ]); + PermissionProfile::from_runtime_permissions(&file_system, NetworkSandboxPolicy::Restricted) } else if foreign_cwd && !matches!(scenario, PushedExecScenario::UnsandboxedInterceptedPatch) { PermissionProfile::workspace_write() } else { @@ -976,6 +998,28 @@ timeout = 900 "toolCallId": CALL_ID, }), ); + if foreign_cwd && !managed_network_configured { + let permissions = response_mock.requests()[0] + .message_input_texts("developer") + .join("\n") + .lines() + .filter(|line| { + line.contains("`sandbox_mode`") + || line.starts_with(" The writable root") + || line.starts_with("- path `") + || line.starts_with("- glob `") + }) + .collect::>() + .join("\n"); + insta::allow_duplicates! { + insta::assert_snapshot!(permissions, @r#" + Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `workspace-write`: The sandbox permits reading files, and editing files in `cwd` and `writable_roots`. Editing files in other directories requires approval. Network access is restricted. + The writable roots are `C:\workspace`, `D:\other-workspace`. + - path `C:\workspace\blocked` + - glob `C:\workspace\private\**` + "#); + } + } if foreign_cwd { let params = &exec_server_result.process_start["params"]; assert_eq!(params["cwd"], "file:///C:/workspace"); diff --git a/codex-rs/prompts/src/permissions_instructions.rs b/codex-rs/prompts/src/permissions_instructions.rs index 08de58d20d..a11ee20cc6 100644 --- a/codex-rs/prompts/src/permissions_instructions.rs +++ b/codex-rs/prompts/src/permissions_instructions.rs @@ -1,5 +1,6 @@ //! Composes selected permission text and prepared runtime facts into a stored fragment body. -//! Only the profile adapter inspects the host filesystem; composition uses the supplied facts. +//! Remote paths use executor context; the local adapter retains host filesystem resolution. +//! Displayed paths share a byte budget and are omitted only at whole-entry boundaries. //! Catalog text uses literal substitution; bundled sandbox templates are parsed and cached here. use crate::ResolvedMessage; @@ -16,10 +17,12 @@ use codex_protocol::config_types::SandboxMode; use codex_protocol::models::ContentItemKind; use codex_protocol::models::PermissionProfile; use codex_protocol::models::format_allow_prefixes; +use codex_protocol::permissions::FileSystemSandboxPolicyContext; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::GranularApprovalConfig; use codex_protocol::protocol::NetworkAccess; use codex_utils_template::Template; +use std::collections::HashSet; use std::path::Path; use std::sync::LazyLock; @@ -34,6 +37,8 @@ const GRANULAR_PROMPTED_CATEGORIES: &str = "These approval categories may still prompt the user when needed:"; const GRANULAR_REJECTED_CATEGORIES: &str = "These approval categories are automatically rejected instead of prompting the user:"; +const MAX_PERMISSION_PATH_BYTES: usize = 32 * 1024; +const OMITTED_PERMISSION_PATHS: &str = "Additional permission paths/globs are omitted. All restrictions still apply; do not use escalation or additional permissions to bypass omitted read denials."; static DANGER_FULL_ACCESS: LazyLock