Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion codex-rs/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ use codex_tui::Cli as TuiCli;
use codex_tui::ExitReason;
use codex_tui::UpdateAction;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::canonicalize_existing_preserving_symlinks;
use codex_utils_cli::CliConfigOverrides;
use codex_utils_cli::ProfileV2Name;
use codex_utils_cli::SharedCliOptions;
Expand Down Expand Up @@ -83,6 +84,7 @@ use codex_login::read_codex_access_token_from_env;
use codex_memories_write::clear_memory_roots_contents;
use codex_models_manager::bundled_models_response;
use codex_models_manager::manager::RefreshStrategy;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::user_input::UserInput;
use codex_terminal_detection::TerminalName;
Expand Down Expand Up @@ -1854,6 +1856,9 @@ async fn run_debug_prompt_input_command(
) -> anyhow::Result<()> {
let loader_overrides = loader_overrides_for_profile(interactive.config_profile_v2.as_ref())?;
let shared = interactive.shared.into_inner();
shared
.validate_workspace_root_mode()
.map_err(anyhow::Error::msg)?;
let mut cli_kv_overrides = root_config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?;
Expand All @@ -1874,10 +1879,34 @@ async fn run_debug_prompt_input_command(
} else {
shared.sandbox_mode.map(Into::into)
};
let workspace_base = match shared.cwd.as_deref() {
Some(path) => {
AbsolutePathBuf::from_absolute_path(canonicalize_existing_preserving_symlinks(path)?)?
}
None => AbsolutePathBuf::current_dir()?,
};
let workspace_roots = (!shared.workspace_root.is_empty()).then(|| {
shared
.workspace_root
.iter()
.cloned()
.map(|path| AbsolutePathBuf::resolve_path_against_base(path, workspace_base.as_path()))
.collect()
});
let exact_workspace_profile = workspace_roots.as_ref().is_some_and(|_| {
sandbox_mode == Some(codex_protocol::config_types::SandboxMode::WorkspaceWrite)
});
let sandbox_mode_override = if exact_workspace_profile {
None
} else {
sandbox_mode
};
let overrides = ConfigOverrides {
model: shared.model,
approval_policy,
sandbox_mode,
sandbox_mode: sandbox_mode_override,
default_permissions: exact_workspace_profile
.then(|| BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string()),
cwd: shared.cwd,
codex_self_exe: arg0_paths.codex_self_exe,
codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe,
Expand All @@ -1886,6 +1915,7 @@ async fn run_debug_prompt_input_command(
ephemeral: Some(true),
bypass_hook_trust: shared.bypass_hook_trust.then_some(true),
additional_writable_roots: shared.add_dir,
workspace_roots,
..Default::default()
};
let config = ConfigBuilder::default()
Expand Down
41 changes: 41 additions & 0 deletions codex-rs/core/src/config/config_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4682,6 +4682,47 @@ async fn add_dir_override_extends_workspace_writable_roots() -> std::io::Result<
Ok(())
}

#[tokio::test]
async fn explicit_workspace_roots_replace_cwd_for_workspace_write() -> std::io::Result<()> {
let temp_dir = tempfile::tempdir_in(std::env::current_dir()?)?;
let workspace = temp_dir.path().join("workspace");
let tenant = temp_dir.path().join("tenant");
let devkit = temp_dir.path().join("devkit");
std::fs::create_dir_all(&workspace)?;
std::fs::create_dir_all(&tenant)?;
std::fs::create_dir_all(&devkit)?;

let tenant_abs = tenant.abs();
let devkit_abs = devkit.abs();
let overrides = ConfigOverrides {
cwd: Some(workspace.clone()),
default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string()),
workspace_roots: Some(vec![tenant_abs.clone(), devkit_abs.clone()]),
..Default::default()
};

let config = Config::load_from_base_config_with_overrides(
ConfigToml::default(),
overrides,
temp_dir.path().abs(),
)
.await?;

assert_eq!(
config.workspace_roots,
vec![tenant_abs.clone(), devkit_abs.clone()]
);
let policy = config.permissions.file_system_sandbox_policy();
assert!(policy.can_write_path_with_cwd(tenant_abs.as_path(), &workspace));
assert!(policy.can_write_path_with_cwd(devkit_abs.as_path(), &workspace));
assert!(
!policy.can_write_path_with_cwd(&workspace, &workspace),
"workspace cwd should remain read-only: {policy:#?}"
);

Ok(())
}

#[tokio::test]
async fn default_zsh_path_sets_runtime_zsh_path() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
Expand Down
5 changes: 3 additions & 2 deletions codex-rs/core/src/session/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2831,7 +2831,7 @@ async fn session_configured_reports_permission_profile_for_external_sandbox() ->
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn session_permission_profile_rebinds_runtime_workspace_roots() -> anyhow::Result<()> {
async fn session_permission_profile_rebinds_exact_runtime_workspace_roots() -> anyhow::Result<()> {
let codex_home = tempfile::TempDir::new()?;
let cwd = tempfile::TempDir::new()?;
let old_root = test_path_buf("/workspace/old").abs();
Expand All @@ -2841,11 +2841,12 @@ async fn session_permission_profile_rebinds_runtime_workspace_roots() -> anyhow:
.harness_overrides(crate::config::ConfigOverrides {
cwd: Some(cwd.path().to_path_buf()),
default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string()),
additional_writable_roots: vec![old_root.to_path_buf()],
workspace_roots: Some(vec![old_root.clone()]),
..Default::default()
})
.build()
.await?;
assert_eq!(config.workspace_roots, vec![old_root.clone()]);

let session_permission_profile_state = session_permission_profile_state_from_config(&config)?;
let stored_file_system_policy = session_permission_profile_state
Expand Down
26 changes: 23 additions & 3 deletions codex-rs/exec/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ use codex_protocol::ThreadId;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::ReviewRequest;
Expand Down Expand Up @@ -268,6 +269,9 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
config_overrides,
} = cli;
let shared = shared.into_inner();
shared
.validate_workspace_root_mode()
.map_err(anyhow::Error::msg)?;
let SharedCliOptions {
images,
model: model_cli_arg,
Expand All @@ -280,6 +284,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
bypass_hook_trust,
cwd,
add_dir,
workspace_root,
} = shared;

let (_stdout_with_ansi, stderr_with_ansi) = match color {
Expand Down Expand Up @@ -320,6 +325,20 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
}
None => AbsolutePathBuf::current_dir()?,
};
let workspace_roots = (!workspace_root.is_empty()).then(|| {
workspace_root
.into_iter()
.map(|path| AbsolutePathBuf::resolve_path_against_base(path, config_cwd.as_path()))
.collect()
});
let exact_workspace_profile = workspace_roots
.as_ref()
.is_some_and(|_| sandbox_mode == Some(SandboxMode::WorkspaceWrite));
let sandbox_mode_override = if exact_workspace_profile {
None
} else {
sandbox_mode
};

// we load config.toml here to determine project state.
#[allow(clippy::print_stderr)]
Expand Down Expand Up @@ -426,11 +445,12 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
// the fully resolved reviewer is AutoReview.
approval_policy: Some(AskForApproval::Never),
approvals_reviewer: None,
sandbox_mode,
sandbox_mode: sandbox_mode_override,
permission_profile: None,
default_permissions: None,
default_permissions: exact_workspace_profile
.then(|| BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string()),
cwd: resolved_cwd,
workspace_roots: None,
workspace_roots,
model_provider: model_provider.clone(),
service_tier: None,
codex_self_exe: arg0_paths.codex_self_exe.clone(),
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/prompts/src/permissions_instructions_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use std::path::PathBuf;
fn renders_sandbox_mode_text() {
assert_eq!(
sandbox_text(SandboxMode::WorkspaceWrite, NetworkAccess::Restricted),
"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."
"Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `workspace-write`: The sandbox permits reading files and editing files only within the writable roots listed below. Editing files in other directories requires approval. Network access is restricted."
);

assert_eq!(
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
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 {{network_access}}.
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `workspace-write`: The sandbox permits reading files and editing files only within the writable roots listed below. Editing files in other directories requires approval. Network access is {{network_access}}.
32 changes: 31 additions & 1 deletion codex-rs/tui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ use codex_protocol::config_types::AltScreenMode;
use codex_protocol::config_types::SandboxMode;
#[cfg(target_os = "windows")]
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
use codex_rollout::StateDbHandle;
use codex_rollout::state_db;
use codex_state::log_db;
Expand Down Expand Up @@ -915,6 +916,8 @@ pub async fn run_main(
loader_overrides: LoaderOverrides,
explicit_remote_endpoint: Option<RemoteAppServerEndpoint>,
) -> std::io::Result<AppExitInfo> {
cli.validate_workspace_root_mode()
.map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?;
let strict_config = cli.strict_config;
let (sandbox_mode, approval_policy) = if cli.dangerously_bypass_approvals_and_sandbox {
(
Expand Down Expand Up @@ -1107,11 +1110,37 @@ pub async fn run_main(
};

let additional_dirs = cli.add_dir.clone();
let workspace_roots = if cli.workspace_root.is_empty() {
None
} else {
let workspace_base = config_cwd.as_ref().ok_or_else(|| {
std::io::Error::other("--workspace-root is unavailable for remote workspaces")
})?;
Some(
cli.workspace_root
.iter()
.cloned()
.map(|path| {
AbsolutePathBuf::resolve_path_against_base(path, workspace_base.as_path())
})
.collect(),
)
};
let exact_workspace_profile = workspace_roots
.as_ref()
.is_some_and(|_| sandbox_mode == Some(SandboxMode::WorkspaceWrite));
let sandbox_mode_override = if exact_workspace_profile {
None
} else {
sandbox_mode
};

let overrides = ConfigOverrides {
model,
approval_policy,
sandbox_mode,
sandbox_mode: sandbox_mode_override,
default_permissions: exact_workspace_profile
.then(|| BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string()),
cwd: cwd_override,
model_provider: model_provider_override.clone(),
codex_self_exe: arg0_paths.codex_self_exe.clone(),
Expand All @@ -1120,6 +1149,7 @@ pub async fn run_main(
show_raw_agent_reasoning: cli.oss.then_some(true),
bypass_hook_trust: cli.bypass_hook_trust.then_some(true),
additional_writable_roots: additional_dirs,
workspace_roots,
..Default::default()
};

Expand Down
Loading
Loading