From c2423f42d148f83aff8e119a218885673a1c0e4f Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Thu, 23 Apr 2026 10:51:17 -0700 Subject: [PATCH] Respect explicit untrusted project config (#18626) ## Why Fixes #18475. A `-c` override such as `projects..trust_level = "untrusted"` is meant to be a runtime config override, but app-server thread startup treated any non-trusted project as eligible for automatic trust persistence when a permissive sandbox/cwd was requested. That meant an explicit `untrusted` session override could still cause `config.toml` to be updated with `trusted`. ## What changed The app-server auto-trust path now runs only when the active project trust level is unknown. Explicit `trusted` and explicit `untrusted` values are both respected, regardless of whether they came from persisted config or session flags. A focused `thread/start` test now covers the explicit `untrusted` case with a permissive sandbox request. ## Verification - `cargo test -p codex-app-server` - `just fix -p codex-app-server` --- .../app-server/src/codex_message_processor.rs | 2 +- .../app-server/tests/suite/v2/thread_start.rs | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 967329b0b..197552782 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -2597,7 +2597,7 @@ impl CodexMessageProcessor { requested_permissions_trust_project(&typesafe_overrides, config.cwd.as_path()); if requested_cwd.is_some() - && !config.active_project.is_trusted() + && config.active_project.trust_level.is_none() && (requested_permissions_trust_project || matches!( config.permissions.sandbox_policy.get(), diff --git a/codex-rs/app-server/tests/suite/v2/thread_start.rs b/codex-rs/app-server/tests/suite/v2/thread_start.rs index 5180df7c9..2aae14e20 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_start.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_start.rs @@ -802,6 +802,44 @@ async fn thread_start_with_read_only_sandbox_does_not_persist_project_trust() -> Ok(()) } +#[tokio::test] +async fn thread_start_preserves_untrusted_project_trust() -> Result<()> { + let server = create_mock_responses_server_repeating_assistant("Done").await; + + let codex_home = TempDir::new()?; + create_config_toml_without_approval_policy(codex_home.path(), &server.uri())?; + + let workspace = TempDir::new()?; + let config_path = codex_home.path().join("config.toml"); + let workspace_key = workspace.path().display().to_string(); + let mut config_toml = + std::fs::read_to_string(&config_path)?.parse::()?; + config_toml["projects"][workspace_key.as_str()]["trust_level"] = toml_edit::value("untrusted"); + std::fs::write(&config_path, config_toml.to_string())?; + let config_before = std::fs::read_to_string(&config_path)?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_thread_start_request(ThreadStartParams { + cwd: Some(workspace.path().display().to_string()), + sandbox: Some(SandboxMode::WorkspaceWrite), + ..Default::default() + }) + .await?; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + + let config_after = std::fs::read_to_string(&config_path)?; + assert_eq!(config_after, config_before); + + Ok(()) +} + #[tokio::test] async fn thread_start_skips_trust_write_when_project_is_already_trusted() -> Result<()> { let server = create_mock_responses_server_repeating_assistant("Done").await;