Files
codex/codex-rs/exec/tests/suite/mcp_required_exit.rs
T
Eric Traut 11ca3f7531 Require explicit project trust in non-interactive runs (#60041)
## What changed

Keep untrusted project configuration and MCP servers disabled in exec runs, even with `--sandbox workspace-write`, and warn when project configuration is disabled.

Add `--trust` to `codex exec` and `codex review` to save project trust for the current and future chats, including MCP commands outside the sandbox. The grant persists with `--ephemeral`, refuses explicitly untrusted projects, and cannot be combined with `--ignore-user-config`.

Pass the working directory as a configuration default so embedded server startup does not implicitly grant trust. Preserve the selected `--oss` provider across configuration reloads.

## Testing

Extend CLI parsing and MCP integration coverage for explicit trust, persistence across ephemeral runs, disabled project configuration with write access, and rejection of conflicting or explicitly untrusted settings.

GitOrigin-RevId: b57f4f8829f98d77c5487fa8405c7f29b5d70d0c
2026-11-07 02:39:03 +00:00

70 lines
2.9 KiB
Rust

use codex_core::config::set_project_trust_level;
use codex_protocol::config_types::TrustLevel;
use core_test_support::responses;
use core_test_support::test_codex_exec::test_codex_exec;
use predicates::str::contains;
use pretty_assertions::assert_eq;
use std::fs;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn exits_non_zero_when_required_mcp_server_fails_to_initialize() -> anyhow::Result<()> {
let test = test_codex_exec();
let config_toml = r#"
[mcp_servers.required_broken]
command = "codex-definitely-not-a-real-binary"
required = true
"#;
let user_config = test.home_path().join("config.toml");
fs::write(&user_config, config_toml)?;
let project = test.cwd_path().join("repo");
let cwd = project.join("subdir");
fs::create_dir_all(&cwd)?;
fs::create_dir(project.join(".codex"))?;
fs::create_dir(project.join(".git"))?;
fs::write(project.join(".git/HEAD"), "ref: refs/heads/main\n")?;
let server = responses::start_mock_server().await;
responses::mount_sse_once(&server, responses::sse_completed("resp_1")).await;
let run = |args: &[&str]| {
test.cmd_with_server(&server)
.arg("--cd")
.arg(&cwd)
.args(["--ephemeral", "--experimental-json"])
.args(args)
.arg("hello")
.assert()
};
let expect_mcp_failure = |args: &[&str]| {
run(args).code(1).stderr(contains(
"required MCP servers failed to initialize: required_broken",
));
};
expect_mcp_failure(&[]);
// The same server in project config requires explicit consent, even with write access.
fs::rename(&user_config, project.join(".codex/config.toml"))?;
let first = run(&["--sandbox", "workspace-write"]).success();
let stderr = std::str::from_utf8(&first.get_output().stderr)?;
let warning = stderr
.lines()
.find(|line| line.contains("codex exec --trust"))
.expect("project trust warning");
insta::assert_snapshot!(warning, @"warning: Untrusted project-local configuration is disabled. Run `codex exec --trust` to allow project configuration and MCP servers for this and future chats in the project, including MCP commands outside the sandbox. Explicitly untrusted projects must have their trust setting updated first.");
expect_mcp_failure(&["--sandbox", "read-only", "--trust"]);
expect_mcp_failure(&[]);
set_project_trust_level(test.home_path(), &project, TrustLevel::Untrusted)?;
let refusal = fs::read_to_string(&user_config)?;
run(&["--trust"])
.failure()
.stderr(contains("is marked as untrusted"));
run(&["--ignore-user-config", "resume", "--last", "--trust"])
.failure()
.stderr(contains(
"--trust cannot be combined with --ignore-user-config",
));
assert_eq!(fs::read_to_string(user_config)?, refusal);
Ok(())
}