From 356a3e2d6cff8860f58bdd9f5ebae2dd42968fc6 Mon Sep 17 00:00:00 2001 From: Eva Wong Date: Tue, 5 May 2026 11:59:54 -0700 Subject: [PATCH] fix(exec): store project roots in config --- codex-rs/core/src/config/config_tests.rs | 14 +++++++++- codex-rs/core/src/config/mod.rs | 9 +++++++ codex-rs/exec/src/lib.rs | 34 +++++------------------- codex-rs/exec/src/lib_tests.rs | 11 ++++---- 4 files changed, 35 insertions(+), 33 deletions(-) diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index b48cf020f2..9ac562a282 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -2854,7 +2854,7 @@ async fn add_dir_override_extends_workspace_writable_roots() -> std::io::Result< std::fs::create_dir_all(&backend)?; let overrides = ConfigOverrides { - cwd: Some(frontend), + cwd: Some(frontend.clone()), sandbox_mode: Some(SandboxMode::WorkspaceWrite), additional_writable_roots: vec![PathBuf::from("../backend"), backend.clone()], ..Default::default() @@ -2868,6 +2868,14 @@ async fn add_dir_override_extends_workspace_writable_roots() -> std::io::Result< .await?; let expected_backend = backend.abs(); + assert_eq!( + config.project_roots, + vec![ + frontend.abs(), + expected_backend.clone(), + expected_backend.clone() + ] + ); if cfg!(target_os = "windows") { match &config.legacy_sandbox_policy() { SandboxPolicy::ReadOnly { .. } => {} @@ -6366,6 +6374,7 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> { user_instructions: None, notify: None, cwd: fixture.cwd(), + project_roots: vec![fixture.cwd()], cli_auth_credentials_store_mode: Default::default(), mcp_servers: Constrained::allow_any(HashMap::new()), mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode( @@ -6568,6 +6577,7 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> { user_instructions: None, notify: None, cwd: fixture.cwd(), + project_roots: vec![fixture.cwd()], cli_auth_credentials_store_mode: Default::default(), mcp_servers: Constrained::allow_any(HashMap::new()), mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode( @@ -6724,6 +6734,7 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> { user_instructions: None, notify: None, cwd: fixture.cwd(), + project_roots: vec![fixture.cwd()], cli_auth_credentials_store_mode: Default::default(), mcp_servers: Constrained::allow_any(HashMap::new()), mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode( @@ -6865,6 +6876,7 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> { user_instructions: None, notify: None, cwd: fixture.cwd(), + project_roots: vec![fixture.cwd()], cli_auth_credentials_store_mode: Default::default(), mcp_servers: Constrained::allow_any(HashMap::new()), mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode( diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 2154bb0170..3fa2355aee 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -561,6 +561,11 @@ pub struct Config { /// layer are resolved against this path. pub cwd: AbsolutePathBuf, + /// Absolute project roots for the session. This is the cwd plus any + /// user-requested additional roots, before implicit writable roots are + /// added to the effective permission profile. + pub project_roots: Vec, + /// Preferred store for CLI auth credentials. /// file (default): Use a file in the Codex home directory. /// keyring: Use an OS-specific keyring service. @@ -2950,6 +2955,9 @@ impl Config { .value .set(effective_permission_profile) .map_err(std::io::Error::from)?; + let project_roots = std::iter::once(resolved_cwd.clone()) + .chain(requested_additional_writable_roots.iter().cloned()) + .collect::>(); let config = Self { model, service_tier, @@ -2959,6 +2967,7 @@ impl Config { model_provider_id, model_provider, cwd: resolved_cwd, + project_roots, startup_warnings, permissions: Permissions { approval_policy: constrained_approval_policy.value, diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 4f086323ba..c73e7b5a44 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -207,7 +207,6 @@ struct ExecRunArgs { oss: bool, output_schema_path: Option, prompt: Option, - project_roots: Option>, skip_git_repo_check: bool, stderr_with_ansi: bool, } @@ -305,15 +304,6 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result } None => AbsolutePathBuf::current_dir()?, }; - let project_roots = - (!add_dir.is_empty()).then(|| { - std::iter::once(config_cwd.clone()) - .chain(add_dir.iter().cloned().map(|path| { - AbsolutePathBuf::resolve_path_against_base(path, config_cwd.as_path()) - })) - .collect() - }); - // we load config.toml here to determine project state. #[allow(clippy::print_stderr)] let codex_home = match find_codex_home() { @@ -550,7 +540,6 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result oss, output_schema_path, prompt, - project_roots, skip_git_repo_check, stderr_with_ansi, }) @@ -573,11 +562,9 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> { oss, output_schema_path, prompt, - project_roots, skip_git_repo_check, stderr_with_ansi, } = args; - let project_roots = project_roots.as_deref(); let mut event_processor: Box = match json_mode { true => Box::new(EventProcessorWithJsonOutput::new(last_message_file.clone())), @@ -697,7 +684,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> { &client, ClientRequest::ThreadResume { request_id: request_ids.next(), - params: thread_resume_params_from_config(&config, thread_id, project_roots), + params: thread_resume_params_from_config(&config, thread_id), }, "thread/resume", ) @@ -712,7 +699,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> { &client, ClientRequest::ThreadStart { request_id: request_ids.next(), - params: thread_start_params_from_config(&config, project_roots), + params: thread_start_params_from_config(&config), }, "thread/start", ) @@ -728,7 +715,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> { &client, ClientRequest::ThreadStart { request_id: request_ids.next(), - params: thread_start_params_from_config(&config, project_roots), + params: thread_start_params_from_config(&config), }, "thread/start", ) @@ -941,10 +928,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> { Ok(()) } -fn thread_start_params_from_config( - config: &Config, - project_roots: Option<&[AbsolutePathBuf]>, -) -> ThreadStartParams { +fn thread_start_params_from_config(config: &Config) -> ThreadStartParams { let permissions = permissions_selection_from_config(config); let sandbox = permissions.is_none().then(|| { sandbox_mode_from_permission_profile( @@ -960,18 +944,14 @@ fn thread_start_params_from_config( approvals_reviewer: approvals_reviewer_override_from_config(config), sandbox: sandbox.flatten(), permissions, - project_roots: project_roots.map(<[AbsolutePathBuf]>::to_vec), + project_roots: Some(config.project_roots.clone()), config: config_request_overrides_from_config(config), ephemeral: Some(config.ephemeral), ..ThreadStartParams::default() } } -fn thread_resume_params_from_config( - config: &Config, - thread_id: String, - project_roots: Option<&[AbsolutePathBuf]>, -) -> ThreadResumeParams { +fn thread_resume_params_from_config(config: &Config, thread_id: String) -> ThreadResumeParams { let permissions = permissions_selection_from_config(config); let sandbox = permissions.is_none().then(|| { sandbox_mode_from_permission_profile( @@ -988,7 +968,7 @@ fn thread_resume_params_from_config( approvals_reviewer: approvals_reviewer_override_from_config(config), sandbox: sandbox.flatten(), permissions, - project_roots: project_roots.map(<[AbsolutePathBuf]>::to_vec), + project_roots: Some(config.project_roots.clone()), config: config_request_overrides_from_config(config), ..ThreadResumeParams::default() } diff --git a/codex-rs/exec/src/lib_tests.rs b/codex-rs/exec/src/lib_tests.rs index 82314772d0..dda246b823 100644 --- a/codex-rs/exec/src/lib_tests.rs +++ b/codex-rs/exec/src/lib_tests.rs @@ -355,7 +355,7 @@ async fn thread_start_params_include_review_policy_when_review_policy_is_manual_ .await .expect("build config with manual-only review policy"); - let params = thread_start_params_from_config(&config, /*project_roots*/ None); + let params = thread_start_params_from_config(&config); assert_eq!( params.approvals_reviewer, @@ -383,7 +383,7 @@ async fn thread_start_params_include_review_policy_when_auto_review_is_enabled() .await .expect("build config with guardian review policy"); - let params = thread_start_params_from_config(&config, /*project_roots*/ None); + let params = thread_start_params_from_config(&config); assert_eq!( params.approvals_reviewer, @@ -400,6 +400,7 @@ async fn thread_lifecycle_params_include_legacy_sandbox_when_no_active_profile() .codex_home(codex_home.path().to_path_buf()) .harness_overrides(ConfigOverrides { sandbox_mode: Some(SandboxMode::WorkspaceWrite), + additional_writable_roots: vec![extra.clone().into_path_buf()], ..Default::default() }) .fallback_cwd(Some(cwd.path().to_path_buf())) @@ -412,11 +413,11 @@ async fn thread_lifecycle_params_include_legacy_sandbox_when_no_active_profile() config.cwd.as_path(), ); - let start_params = thread_start_params_from_config(&config, Some(&project_roots)); - let resume_params = - thread_resume_params_from_config(&config, "thread-id".to_string(), Some(&project_roots)); + let start_params = thread_start_params_from_config(&config); + let resume_params = thread_resume_params_from_config(&config, "thread-id".to_string()); assert_eq!(config.permissions.active_permission_profile(), None); + assert_eq!(config.project_roots, project_roots); assert_eq!(start_params.sandbox, expected_sandbox); assert_eq!(start_params.permissions, None); assert_eq!(start_params.project_roots, Some(project_roots.clone()));