mirror of
https://github.com/openai/codex.git
synced 2026-09-11 20:36:49 +00:00
## What changed - Load doctor configuration through the cloud-aware CLI configuration path while preserving invocation overrides such as `--cd`. - Report configuration load time and clarify that diagnostics cover invocation configuration, including cloud-managed policy, but not active thread overrides. - Add redacted filesystem sandbox diagnostics for denied-read rule counts, glob rule counts, glob scan depth, and the managed policy source. ## Testing Add an integration test that verifies valid cloud-managed filesystem requirements are reported without exposing denied paths, and invalid requirements fail the configuration check. GitOrigin-RevId: 65c3c9ad3fd42c1579ca0c9d8e209066f98e5407
235 lines
8.6 KiB
Rust
235 lines
8.6 KiB
Rust
#[cfg(target_os = "macos")]
|
|
use std::path::Path;
|
|
use std::process::Command;
|
|
use std::process::Stdio;
|
|
|
|
use anyhow::Context as _;
|
|
use anyhow::Result;
|
|
use app_test_support::ChatGptAuthFixture;
|
|
use app_test_support::write_chatgpt_auth;
|
|
use codex_config::types::AuthCredentialsStoreMode;
|
|
use pretty_assertions::assert_eq;
|
|
use serde_json::Value;
|
|
use serde_json::json;
|
|
use tempfile::TempDir;
|
|
use wiremock::Mock;
|
|
use wiremock::MockServer;
|
|
use wiremock::ResponseTemplate;
|
|
use wiremock::matchers::method;
|
|
use wiremock::matchers::path;
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn doctor_reports_cloud_filesystem_policy_and_rejects_invalid_requirements() -> Result<()> {
|
|
for valid_requirements in [true, false] {
|
|
let server = MockServer::start().await;
|
|
let codex_home = TempDir::new()?;
|
|
let workspace = TempDir::new()?;
|
|
let workspace_key = serde_json::to_string(workspace.path())?;
|
|
let private_path = workspace.path().join("private-doctor-control");
|
|
let glob = format!("{}/**/*.doctor-secret", workspace.path().display());
|
|
let requirements = if valid_requirements {
|
|
format!("[permissions.filesystem]\ndeny_read = [{private_path:?}, {glob:?}]\n")
|
|
} else {
|
|
"[permissions.filesystem]\ndeny_read = false\n".to_string()
|
|
};
|
|
std::fs::write(
|
|
codex_home.path().join("config.toml"),
|
|
format!(
|
|
r#"
|
|
cli_auth_credentials_store = "ephemeral"
|
|
chatgpt_base_url = "{}/backend-api"
|
|
model_provider = "local"
|
|
[model_providers.local]
|
|
name = "local"
|
|
base_url = "{}/v1"
|
|
wire_api = "responses"
|
|
[windows]
|
|
sandbox = "elevated"
|
|
[projects.{workspace_key}]
|
|
trust_level = "trusted"
|
|
"#,
|
|
server.uri(),
|
|
server.uri(),
|
|
),
|
|
)?;
|
|
// Cloud authentication must use the project selected by --cd.
|
|
std::fs::create_dir(workspace.path().join(".codex"))?;
|
|
std::fs::write(
|
|
workspace.path().join(".codex/config.toml"),
|
|
"cli_auth_credentials_store = \"file\"\n",
|
|
)?;
|
|
write_chatgpt_auth(
|
|
codex_home.path(),
|
|
ChatGptAuthFixture::new("doctor-test-token")
|
|
.account_id("doctor-workspace")
|
|
.chatgpt_account_id("doctor-workspace")
|
|
.chatgpt_user_id("doctor-user")
|
|
.plan_type("enterprise"),
|
|
AuthCredentialsStoreMode::File,
|
|
)?;
|
|
Mock::given(method("GET"))
|
|
.and(path("/backend-api/wham/config/bundle"))
|
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
|
"requirements_toml": {
|
|
"enterprise_managed": [{
|
|
"id": "doctor-policy",
|
|
"name": "Doctor policy fixture",
|
|
"contents": requirements,
|
|
}],
|
|
},
|
|
})))
|
|
.expect(1)
|
|
.mount(&server)
|
|
.await;
|
|
|
|
let output = Command::new(codex_utils_cargo_bin::cargo_bin("codex")?)
|
|
.current_dir(codex_home.path())
|
|
.env("CODEX_HOME", codex_home.path())
|
|
.env("NO_PROXY", "127.0.0.1,localhost")
|
|
.env("no_proxy", "127.0.0.1,localhost")
|
|
.env_remove("CODEX_ACCESS_TOKEN")
|
|
.env_remove("CODEX_API_KEY")
|
|
.env_remove("OPENAI_API_KEY")
|
|
.arg("--cd")
|
|
.arg(workspace.path())
|
|
.args(["doctor", "--json"])
|
|
.stdin(Stdio::null())
|
|
.output()?;
|
|
let report: Value = serde_json::from_slice(&output.stdout)?;
|
|
if valid_requirements {
|
|
let config = &report["checks"]["config.load"];
|
|
let sandbox = &report["checks"]["sandbox.helpers"]["details"];
|
|
assert_eq!(config["status"], "ok", "{config:#}");
|
|
assert_eq!(
|
|
config["details"]["cwd"],
|
|
workspace.path().display().to_string()
|
|
);
|
|
insta::assert_snapshot!(
|
|
serde_json::to_string_pretty(&json!({
|
|
"scope": config["details"]["configuration scope"],
|
|
"activeThreadOverrides": config["details"]["active thread overrides"],
|
|
"denyRules": sandbox["denied-read rules"],
|
|
"denyGlobs": sandbox["denied-read glob rules"],
|
|
"scanDepth": sandbox["glob scan max depth"],
|
|
"managedFilesystemSource": sandbox["managed filesystem source"],
|
|
}))?,
|
|
@r#"
|
|
{
|
|
"scope": "invocation config, including cloud-managed policy",
|
|
"activeThreadOverrides": "not inspected",
|
|
"denyRules": "2",
|
|
"denyGlobs": "1",
|
|
"scanDepth": "unbounded",
|
|
"managedFilesystemSource": "cloud"
|
|
}
|
|
"#
|
|
);
|
|
let stdout = String::from_utf8(output.stdout)?;
|
|
assert!(!stdout.contains("doctor-secret"));
|
|
assert!(!stdout.contains("private-doctor-control"));
|
|
} else {
|
|
assert_eq!(report["checks"]["config.load"]["status"], "fail");
|
|
assert!(report["checks"].get("sandbox.helpers").is_none());
|
|
}
|
|
server.verify().await;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn invalid_custom_ca_falls_back_to_system_roots() -> Result<()> {
|
|
let server = MockServer::start().await;
|
|
for (request_method, request_path) in [("HEAD", "/v1/responses"), ("GET", "/v1/models")] {
|
|
Mock::given(method(request_method))
|
|
.and(path(request_path))
|
|
.respond_with(ResponseTemplate::new(200))
|
|
.expect(2)
|
|
.mount(&server)
|
|
.await;
|
|
}
|
|
|
|
let codex_home = TempDir::new()?;
|
|
let certificate = codex_home.path().join("invalid-ca.pem");
|
|
std::fs::write(&certificate, "not a certificate")?;
|
|
std::fs::write(
|
|
codex_home.path().join("config.toml"),
|
|
format!(
|
|
"model_provider = \"local\"\n[model_providers.local]\nname = \"local\"\nbase_url = \"{}/v1\"\nwire_api = \"responses\"\n",
|
|
server.uri()
|
|
),
|
|
)?;
|
|
for sandbox in [None, Some("seatbelt")] {
|
|
let mut command = Command::new(codex_utils_cargo_bin::cargo_bin("codex")?);
|
|
command
|
|
.args(["doctor", "--json"])
|
|
.env("CODEX_HOME", codex_home.path())
|
|
.env("CODEX_CA_CERTIFICATE", &certificate)
|
|
.stdin(Stdio::null());
|
|
if let Some(sandbox) = sandbox {
|
|
command
|
|
.env("CODEX_SANDBOX", sandbox)
|
|
.env("HTTP_PROXY", "http://127.0.0.1:1")
|
|
.env("http_proxy", "http://127.0.0.1:1")
|
|
.env("HTTPS_PROXY", "http://127.0.0.1:1")
|
|
.env("https_proxy", "http://127.0.0.1:1")
|
|
.env("NO_PROXY", "")
|
|
.env("no_proxy", "");
|
|
} else {
|
|
command
|
|
.env("NO_PROXY", "127.0.0.1,localhost")
|
|
.env("no_proxy", "127.0.0.1,localhost");
|
|
}
|
|
let output = command
|
|
.output()
|
|
.context("failed to run the doctor with an invalid custom CA")?;
|
|
let report: Value = serde_json::from_slice(&output.stdout)?;
|
|
|
|
assert!(
|
|
report["checks"]["network.provider_reachability"]["details"]["local API inference URL"]
|
|
.as_str()
|
|
.is_some_and(|detail| detail.ends_with("reachable (HTTP 200)"))
|
|
);
|
|
}
|
|
server.verify().await;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
#[test]
|
|
fn doctor_reports_macos_system_proxy_configuration_and_policy() -> Result<()> {
|
|
let codex_home = TempDir::new()?;
|
|
let report = doctor_report(codex_home.path())?;
|
|
let details = &report["checks"]["network.env"]["details"];
|
|
|
|
assert_eq!(details["respect system proxy"], json!("disabled"));
|
|
assert!(matches!(
|
|
details["system proxy"].as_str(),
|
|
Some("automatic (PAC)" | "manual" | "direct" | "unavailable")
|
|
));
|
|
|
|
std::fs::write(
|
|
codex_home.path().join("config.toml"),
|
|
"[features]\nrespect_system_proxy = true\n",
|
|
)?;
|
|
let report = doctor_report(codex_home.path())?;
|
|
assert_eq!(
|
|
report["checks"]["network.env"]["details"]["respect system proxy"],
|
|
json!("enabled")
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
fn doctor_report(codex_home: &Path) -> Result<Value> {
|
|
let output = Command::new(codex_utils_cargo_bin::cargo_bin("codex")?)
|
|
.args(["doctor", "--json"])
|
|
.env("CODEX_HOME", codex_home)
|
|
.stdin(Stdio::null())
|
|
.output()
|
|
.context("failed to run the doctor")?;
|
|
|
|
serde_json::from_slice(&output.stdout).context("doctor did not emit a valid json report")
|
|
}
|