mirror of
https://github.com/openai/codex.git
synced 2026-05-02 10:26:45 +00:00
Merge branch 'main' into var-expansion
This commit is contained in:
@@ -35,9 +35,10 @@ use crate::features::FeatureOverrides;
|
||||
use crate::features::Features;
|
||||
use crate::features::FeaturesToml;
|
||||
use crate::git_info::resolve_root_git_project_for_trust;
|
||||
use crate::model_provider_info::LEGACY_OLLAMA_CHAT_PROVIDER_ID;
|
||||
use crate::model_provider_info::LMSTUDIO_OSS_PROVIDER_ID;
|
||||
use crate::model_provider_info::ModelProviderInfo;
|
||||
use crate::model_provider_info::OLLAMA_CHAT_PROVIDER_ID;
|
||||
use crate::model_provider_info::OLLAMA_CHAT_PROVIDER_REMOVED_ERROR;
|
||||
use crate::model_provider_info::OLLAMA_OSS_PROVIDER_ID;
|
||||
use crate::model_provider_info::built_in_model_providers;
|
||||
use crate::project_doc::DEFAULT_PROJECT_DOC_FILENAME;
|
||||
@@ -214,7 +215,7 @@ pub struct Config {
|
||||
/// Show startup tooltips in the TUI welcome screen.
|
||||
pub show_tooltips: bool,
|
||||
|
||||
/// Start the TUI in the specified collaboration mode (plan/execute/etc.).
|
||||
/// Start the TUI in the specified collaboration mode (plan/default).
|
||||
pub experimental_mode: Option<ModeKind>,
|
||||
|
||||
/// Controls whether the TUI uses the terminal's alternate screen buffer.
|
||||
@@ -875,14 +876,20 @@ pub fn set_project_trust_level(
|
||||
pub fn set_default_oss_provider(codex_home: &Path, provider: &str) -> std::io::Result<()> {
|
||||
// Validate that the provider is one of the known OSS providers
|
||||
match provider {
|
||||
LMSTUDIO_OSS_PROVIDER_ID | OLLAMA_OSS_PROVIDER_ID | OLLAMA_CHAT_PROVIDER_ID => {
|
||||
LMSTUDIO_OSS_PROVIDER_ID | OLLAMA_OSS_PROVIDER_ID => {
|
||||
// Valid provider, continue
|
||||
}
|
||||
LEGACY_OLLAMA_CHAT_PROVIDER_ID => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
OLLAMA_CHAT_PROVIDER_REMOVED_ERROR,
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"Invalid OSS provider '{provider}'. Must be one of: {LMSTUDIO_OSS_PROVIDER_ID}, {OLLAMA_OSS_PROVIDER_ID}, {OLLAMA_CHAT_PROVIDER_ID}"
|
||||
"Invalid OSS provider '{provider}'. Must be one of: {LMSTUDIO_OSS_PROVIDER_ID}, {OLLAMA_OSS_PROVIDER_ID}"
|
||||
),
|
||||
));
|
||||
}
|
||||
@@ -1103,7 +1110,7 @@ pub struct ConfigToml {
|
||||
pub experimental_compact_prompt_file: Option<AbsolutePathBuf>,
|
||||
pub experimental_use_unified_exec_tool: Option<bool>,
|
||||
pub experimental_use_freeform_apply_patch: Option<bool>,
|
||||
/// Preferred OSS provider for local models, e.g. "lmstudio", "ollama", or "ollama-chat".
|
||||
/// Preferred OSS provider for local models, e.g. "lmstudio" or "ollama".
|
||||
pub oss_provider: Option<String>,
|
||||
}
|
||||
|
||||
@@ -1205,7 +1212,11 @@ impl ConfigToml {
|
||||
profile_sandbox_mode: Option<SandboxMode>,
|
||||
windows_sandbox_level: WindowsSandboxLevel,
|
||||
resolved_cwd: &Path,
|
||||
sandbox_policy_constraint: Option<&Constrained<SandboxPolicy>>,
|
||||
) -> SandboxPolicyResolution {
|
||||
let sandbox_mode_was_explicit = sandbox_mode_override.is_some()
|
||||
|| profile_sandbox_mode.is_some()
|
||||
|| self.sandbox_mode.is_some();
|
||||
let resolved_sandbox_mode = sandbox_mode_override
|
||||
.or(profile_sandbox_mode)
|
||||
.or(self.sandbox_mode)
|
||||
@@ -1239,13 +1250,30 @@ impl ConfigToml {
|
||||
SandboxMode::DangerFullAccess => SandboxPolicy::DangerFullAccess,
|
||||
};
|
||||
let mut forced_auto_mode_downgraded_on_windows = false;
|
||||
if cfg!(target_os = "windows")
|
||||
&& matches!(resolved_sandbox_mode, SandboxMode::WorkspaceWrite)
|
||||
// If the experimental Windows sandbox is enabled, do not force a downgrade.
|
||||
&& windows_sandbox_level == codex_protocol::config_types::WindowsSandboxLevel::Disabled
|
||||
let mut downgrade_workspace_write_if_unsupported = |policy: &mut SandboxPolicy| {
|
||||
if cfg!(target_os = "windows")
|
||||
// If the experimental Windows sandbox is enabled, do not force a downgrade.
|
||||
&& windows_sandbox_level
|
||||
== codex_protocol::config_types::WindowsSandboxLevel::Disabled
|
||||
&& matches!(&*policy, SandboxPolicy::WorkspaceWrite { .. })
|
||||
{
|
||||
*policy = SandboxPolicy::new_read_only_policy();
|
||||
forced_auto_mode_downgraded_on_windows = true;
|
||||
}
|
||||
};
|
||||
if matches!(resolved_sandbox_mode, SandboxMode::WorkspaceWrite) {
|
||||
downgrade_workspace_write_if_unsupported(&mut sandbox_policy);
|
||||
}
|
||||
if !sandbox_mode_was_explicit
|
||||
&& let Some(constraint) = sandbox_policy_constraint
|
||||
&& let Err(err) = constraint.can_set(&sandbox_policy)
|
||||
{
|
||||
sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
forced_auto_mode_downgraded_on_windows = true;
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"default sandbox policy is disallowed by requirements; falling back to required default"
|
||||
);
|
||||
sandbox_policy = constraint.get().clone();
|
||||
downgrade_workspace_write_if_unsupported(&mut sandbox_policy);
|
||||
}
|
||||
SandboxPolicyResolution {
|
||||
policy: sandbox_policy,
|
||||
@@ -1493,6 +1521,9 @@ impl Config {
|
||||
let active_project = cfg
|
||||
.get_active_project(&resolved_cwd)
|
||||
.unwrap_or(ProjectConfig { trust_level: None });
|
||||
let sandbox_mode_was_explicit = sandbox_mode.is_some()
|
||||
|| config_profile.sandbox_mode.is_some()
|
||||
|| cfg.sandbox_mode.is_some();
|
||||
|
||||
let windows_sandbox_level = WindowsSandboxLevel::from_features(&features);
|
||||
let SandboxPolicyResolution {
|
||||
@@ -1503,6 +1534,7 @@ impl Config {
|
||||
config_profile.sandbox_mode,
|
||||
windows_sandbox_level,
|
||||
&resolved_cwd,
|
||||
Some(&requirements.sandbox_policy),
|
||||
);
|
||||
if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = &mut sandbox_policy {
|
||||
for path in additional_writable_roots {
|
||||
@@ -1511,7 +1543,10 @@ impl Config {
|
||||
}
|
||||
}
|
||||
}
|
||||
let approval_policy = approval_policy_override
|
||||
let approval_policy_was_explicit = approval_policy_override.is_some()
|
||||
|| config_profile.approval_policy.is_some()
|
||||
|| cfg.approval_policy.is_some();
|
||||
let mut approval_policy = approval_policy_override
|
||||
.or(config_profile.approval_policy)
|
||||
.or(cfg.approval_policy)
|
||||
.unwrap_or_else(|| {
|
||||
@@ -1523,16 +1558,20 @@ impl Config {
|
||||
AskForApproval::default()
|
||||
}
|
||||
});
|
||||
if !approval_policy_was_explicit
|
||||
&& let Err(err) = requirements.approval_policy.can_set(&approval_policy)
|
||||
{
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"default approval policy is disallowed by requirements; falling back to required default"
|
||||
);
|
||||
approval_policy = requirements.approval_policy.value();
|
||||
}
|
||||
let web_search_mode = resolve_web_search_mode(&cfg, &config_profile, &features);
|
||||
// TODO(dylan): We should be able to leverage ConfigLayerStack so that
|
||||
// we can reliably check this at every config level.
|
||||
let did_user_set_custom_approval_policy_or_sandbox_mode = approval_policy_override
|
||||
.is_some()
|
||||
|| config_profile.approval_policy.is_some()
|
||||
|| cfg.approval_policy.is_some()
|
||||
|| sandbox_mode.is_some()
|
||||
|| config_profile.sandbox_mode.is_some()
|
||||
|| cfg.sandbox_mode.is_some();
|
||||
let did_user_set_custom_approval_policy_or_sandbox_mode =
|
||||
approval_policy_was_explicit || sandbox_mode_was_explicit;
|
||||
|
||||
let mut model_providers = built_in_model_providers();
|
||||
// Merge user-defined providers into the built-in list.
|
||||
@@ -1547,10 +1586,12 @@ impl Config {
|
||||
let model_provider = model_providers
|
||||
.get(&model_provider_id)
|
||||
.ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("Model provider `{model_provider_id}` not found"),
|
||||
)
|
||||
let message = if model_provider_id == LEGACY_OLLAMA_CHAT_PROVIDER_ID {
|
||||
OLLAMA_CHAT_PROVIDER_REMOVED_ERROR.to_string()
|
||||
} else {
|
||||
format!("Model provider `{model_provider_id}` not found")
|
||||
};
|
||||
std::io::Error::new(std::io::ErrorKind::NotFound, message)
|
||||
})?
|
||||
.clone();
|
||||
|
||||
@@ -1683,9 +1724,9 @@ impl Config {
|
||||
model_provider_id,
|
||||
model_provider,
|
||||
cwd: resolved_cwd,
|
||||
approval_policy: constrained_approval_policy,
|
||||
sandbox_policy: constrained_sandbox_policy,
|
||||
enforce_residency,
|
||||
approval_policy: constrained_approval_policy.value,
|
||||
sandbox_policy: constrained_sandbox_policy.value,
|
||||
enforce_residency: enforce_residency.value,
|
||||
did_user_set_custom_approval_policy_or_sandbox_mode,
|
||||
forced_auto_mode_downgraded_on_windows,
|
||||
shell_environment_policy,
|
||||
@@ -2047,6 +2088,7 @@ network_access = false # This should be ignored.
|
||||
None,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
&PathBuf::from("/tmp/test"),
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
resolution,
|
||||
@@ -2071,6 +2113,7 @@ network_access = true # This should be ignored.
|
||||
None,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
&PathBuf::from("/tmp/test"),
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
resolution,
|
||||
@@ -2103,6 +2146,7 @@ exclude_slash_tmp = true
|
||||
None,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
&PathBuf::from("/tmp/test"),
|
||||
None,
|
||||
);
|
||||
if cfg!(target_os = "windows") {
|
||||
assert_eq!(
|
||||
@@ -2152,6 +2196,7 @@ trust_level = "trusted"
|
||||
None,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
&PathBuf::from("/tmp/test"),
|
||||
None,
|
||||
);
|
||||
if cfg!(target_os = "windows") {
|
||||
assert_eq!(
|
||||
@@ -3713,7 +3758,7 @@ model = "gpt-5.1-codex"
|
||||
cfg: ConfigToml,
|
||||
model_provider_map: HashMap<String, ModelProviderInfo>,
|
||||
openai_provider: ModelProviderInfo,
|
||||
openai_chat_completions_provider: ModelProviderInfo,
|
||||
openai_custom_provider: ModelProviderInfo,
|
||||
}
|
||||
|
||||
impl PrecedenceTestFixture {
|
||||
@@ -3795,11 +3840,11 @@ profile = "gpt3"
|
||||
[analytics]
|
||||
enabled = true
|
||||
|
||||
[model_providers.openai-chat-completions]
|
||||
name = "OpenAI using Chat Completions"
|
||||
[model_providers.openai-custom]
|
||||
name = "OpenAI custom"
|
||||
base_url = "https://api.openai.com/v1"
|
||||
env_key = "OPENAI_API_KEY"
|
||||
wire_api = "chat"
|
||||
wire_api = "responses"
|
||||
request_max_retries = 4 # retry failed HTTP requests
|
||||
stream_max_retries = 10 # retry dropped SSE streams
|
||||
stream_idle_timeout_ms = 300000 # 5m idle timeout
|
||||
@@ -3813,7 +3858,7 @@ model_reasoning_summary = "detailed"
|
||||
|
||||
[profiles.gpt3]
|
||||
model = "gpt-3.5-turbo"
|
||||
model_provider = "openai-chat-completions"
|
||||
model_provider = "openai-custom"
|
||||
|
||||
[profiles.zdr]
|
||||
model = "o3"
|
||||
@@ -3844,11 +3889,11 @@ model_verbosity = "high"
|
||||
|
||||
let codex_home_temp_dir = TempDir::new().unwrap();
|
||||
|
||||
let openai_chat_completions_provider = ModelProviderInfo {
|
||||
name: "OpenAI using Chat Completions".to_string(),
|
||||
let openai_custom_provider = ModelProviderInfo {
|
||||
name: "OpenAI custom".to_string(),
|
||||
base_url: Some("https://api.openai.com/v1".to_string()),
|
||||
env_key: Some("OPENAI_API_KEY".to_string()),
|
||||
wire_api: crate::WireApi::Chat,
|
||||
wire_api: crate::WireApi::Responses,
|
||||
env_key_instructions: None,
|
||||
experimental_bearer_token: None,
|
||||
query_params: None,
|
||||
@@ -3862,10 +3907,7 @@ model_verbosity = "high"
|
||||
};
|
||||
let model_provider_map = {
|
||||
let mut model_provider_map = built_in_model_providers();
|
||||
model_provider_map.insert(
|
||||
"openai-chat-completions".to_string(),
|
||||
openai_chat_completions_provider.clone(),
|
||||
);
|
||||
model_provider_map.insert("openai-custom".to_string(), openai_custom_provider.clone());
|
||||
model_provider_map
|
||||
};
|
||||
|
||||
@@ -3880,7 +3922,7 @@ model_verbosity = "high"
|
||||
cfg,
|
||||
model_provider_map,
|
||||
openai_provider,
|
||||
openai_chat_completions_provider,
|
||||
openai_custom_provider,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3957,7 +3999,7 @@ model_verbosity = "high"
|
||||
forced_login_method: None,
|
||||
include_apply_patch_tool: false,
|
||||
web_search_mode: None,
|
||||
use_experimental_unified_exec_tool: false,
|
||||
use_experimental_unified_exec_tool: !cfg!(windows),
|
||||
ghost_snapshot: GhostSnapshotConfig::default(),
|
||||
features: Features::with_defaults(),
|
||||
suppress_unstable_features_warning: false,
|
||||
@@ -4001,8 +4043,8 @@ model_verbosity = "high"
|
||||
review_model: None,
|
||||
model_context_window: None,
|
||||
model_auto_compact_token_limit: None,
|
||||
model_provider_id: "openai-chat-completions".to_string(),
|
||||
model_provider: fixture.openai_chat_completions_provider.clone(),
|
||||
model_provider_id: "openai-custom".to_string(),
|
||||
model_provider: fixture.openai_custom_provider.clone(),
|
||||
approval_policy: Constrained::allow_any(AskForApproval::UnlessTrusted),
|
||||
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
|
||||
enforce_residency: Constrained::allow_any(None),
|
||||
@@ -4042,7 +4084,7 @@ model_verbosity = "high"
|
||||
forced_login_method: None,
|
||||
include_apply_patch_tool: false,
|
||||
web_search_mode: None,
|
||||
use_experimental_unified_exec_tool: false,
|
||||
use_experimental_unified_exec_tool: !cfg!(windows),
|
||||
ghost_snapshot: GhostSnapshotConfig::default(),
|
||||
features: Features::with_defaults(),
|
||||
suppress_unstable_features_warning: false,
|
||||
@@ -4142,7 +4184,7 @@ model_verbosity = "high"
|
||||
forced_login_method: None,
|
||||
include_apply_patch_tool: false,
|
||||
web_search_mode: None,
|
||||
use_experimental_unified_exec_tool: false,
|
||||
use_experimental_unified_exec_tool: !cfg!(windows),
|
||||
ghost_snapshot: GhostSnapshotConfig::default(),
|
||||
features: Features::with_defaults(),
|
||||
suppress_unstable_features_warning: false,
|
||||
@@ -4228,7 +4270,7 @@ model_verbosity = "high"
|
||||
forced_login_method: None,
|
||||
include_apply_patch_tool: false,
|
||||
web_search_mode: None,
|
||||
use_experimental_unified_exec_tool: false,
|
||||
use_experimental_unified_exec_tool: !cfg!(windows),
|
||||
ghost_snapshot: GhostSnapshotConfig::default(),
|
||||
features: Features::with_defaults(),
|
||||
suppress_unstable_features_warning: false,
|
||||
@@ -4588,6 +4630,50 @@ trust_level = "trusted"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_default_oss_provider_rejects_legacy_ollama_chat_provider() -> std::io::Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let codex_home = temp_dir.path();
|
||||
|
||||
let result = set_default_oss_provider(codex_home, LEGACY_OLLAMA_CHAT_PROVIDER_ID);
|
||||
assert!(result.is_err());
|
||||
let error = result.unwrap_err();
|
||||
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains(OLLAMA_CHAT_PROVIDER_REMOVED_ERROR)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_config_rejects_legacy_ollama_chat_provider_with_helpful_error()
|
||||
-> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cfg = ConfigToml {
|
||||
model_provider: Some(LEGACY_OLLAMA_CHAT_PROVIDER_ID.to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = Config::load_from_base_config_with_overrides(
|
||||
cfg,
|
||||
ConfigOverrides::default(),
|
||||
codex_home.path().to_path_buf(),
|
||||
);
|
||||
assert!(result.is_err());
|
||||
let error = result.unwrap_err();
|
||||
assert_eq!(error.kind(), std::io::ErrorKind::NotFound);
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains(OLLAMA_CHAT_PROVIDER_REMOVED_ERROR)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_untrusted_project_gets_workspace_write_sandbox() -> anyhow::Result<()> {
|
||||
let config_with_untrusted = r#"
|
||||
@@ -4603,6 +4689,7 @@ trust_level = "untrusted"
|
||||
None,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
&PathBuf::from("/tmp/test"),
|
||||
None,
|
||||
);
|
||||
|
||||
// Verify that untrusted projects get WorkspaceWrite (or ReadOnly on Windows due to downgrade)
|
||||
@@ -4623,6 +4710,103 @@ trust_level = "untrusted"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_sandbox_policy_falls_back_to_constraint_value_for_implicit_defaults()
|
||||
-> anyhow::Result<()> {
|
||||
let project_dir = TempDir::new()?;
|
||||
let project_path = project_dir.path().to_path_buf();
|
||||
let project_key = project_path.to_string_lossy().to_string();
|
||||
let cfg = ConfigToml {
|
||||
projects: Some(HashMap::from([(
|
||||
project_key,
|
||||
ProjectConfig {
|
||||
trust_level: Some(TrustLevel::Trusted),
|
||||
},
|
||||
)])),
|
||||
..Default::default()
|
||||
};
|
||||
let constrained = Constrained::new(SandboxPolicy::DangerFullAccess, |candidate| {
|
||||
if matches!(candidate, SandboxPolicy::DangerFullAccess) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "sandbox_mode",
|
||||
candidate: format!("{candidate:?}"),
|
||||
allowed: "[DangerFullAccess]".to_string(),
|
||||
requirement_source: RequirementSource::Unknown,
|
||||
})
|
||||
}
|
||||
})?;
|
||||
|
||||
let resolution = cfg.derive_sandbox_policy(
|
||||
None,
|
||||
None,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
&project_path,
|
||||
Some(&constrained),
|
||||
);
|
||||
|
||||
assert_eq!(resolution.policy, SandboxPolicy::DangerFullAccess);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_sandbox_policy_preserves_windows_downgrade_for_unsupported_fallback()
|
||||
-> anyhow::Result<()> {
|
||||
let project_dir = TempDir::new()?;
|
||||
let project_path = project_dir.path().to_path_buf();
|
||||
let project_key = project_path.to_string_lossy().to_string();
|
||||
let cfg = ConfigToml {
|
||||
projects: Some(HashMap::from([(
|
||||
project_key,
|
||||
ProjectConfig {
|
||||
trust_level: Some(TrustLevel::Trusted),
|
||||
},
|
||||
)])),
|
||||
..Default::default()
|
||||
};
|
||||
let constrained =
|
||||
Constrained::new(SandboxPolicy::new_workspace_write_policy(), |candidate| {
|
||||
if matches!(candidate, SandboxPolicy::WorkspaceWrite { .. }) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "sandbox_mode",
|
||||
candidate: format!("{candidate:?}"),
|
||||
allowed: "[WorkspaceWrite]".to_string(),
|
||||
requirement_source: RequirementSource::Unknown,
|
||||
})
|
||||
}
|
||||
})?;
|
||||
|
||||
let resolution = cfg.derive_sandbox_policy(
|
||||
None,
|
||||
None,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
&project_path,
|
||||
Some(&constrained),
|
||||
);
|
||||
|
||||
if cfg!(target_os = "windows") {
|
||||
assert_eq!(
|
||||
resolution,
|
||||
SandboxPolicyResolution {
|
||||
policy: SandboxPolicy::ReadOnly,
|
||||
forced_auto_mode_downgraded_on_windows: true,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
resolution,
|
||||
SandboxPolicyResolution {
|
||||
policy: SandboxPolicy::new_workspace_write_policy(),
|
||||
forced_auto_mode_downgraded_on_windows: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_oss_provider_explicit_override() {
|
||||
let config_toml = ConfigToml::default();
|
||||
@@ -4778,6 +4962,125 @@ mcp_oauth_callback_port = 5678
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn requirements_disallowing_default_sandbox_falls_back_to_required_default()
|
||||
-> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async {
|
||||
Some(crate::config_loader::ConfigRequirementsToml {
|
||||
allowed_sandbox_modes: Some(vec![
|
||||
crate::config_loader::SandboxModeRequirement::ReadOnly,
|
||||
]),
|
||||
..Default::default()
|
||||
})
|
||||
}))
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert_eq!(*config.sandbox_policy.get(), SandboxPolicy::ReadOnly);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn explicit_sandbox_mode_still_errors_when_disallowed_by_requirements()
|
||||
-> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"sandbox_mode = "danger-full-access"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let requirements = crate::config_loader::ConfigRequirementsToml {
|
||||
allowed_approval_policies: None,
|
||||
allowed_sandbox_modes: Some(vec![
|
||||
crate::config_loader::SandboxModeRequirement::ReadOnly,
|
||||
]),
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
};
|
||||
|
||||
let err = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.cloud_requirements(CloudRequirementsLoader::new(
|
||||
async move { Some(requirements) },
|
||||
))
|
||||
.build()
|
||||
.await
|
||||
.expect_err("explicit disallowed mode should still fail");
|
||||
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
|
||||
let message = err.to_string();
|
||||
assert!(message.contains("invalid value for `sandbox_mode`"));
|
||||
assert!(message.contains("set by cloud requirements"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn requirements_disallowing_default_approval_falls_back_to_required_default()
|
||||
-> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let workspace = TempDir::new()?;
|
||||
let workspace_key = workspace.path().to_string_lossy().replace('\\', "\\\\");
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
format!(
|
||||
r#"
|
||||
[projects."{workspace_key}"]
|
||||
trust_level = "untrusted"
|
||||
"#
|
||||
),
|
||||
)?;
|
||||
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(workspace.path().to_path_buf()))
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async {
|
||||
Some(crate::config_loader::ConfigRequirementsToml {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
|
||||
..Default::default()
|
||||
})
|
||||
}))
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert_eq!(config.approval_policy.value(), AskForApproval::OnRequest);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn explicit_approval_policy_still_errors_when_disallowed_by_requirements()
|
||||
-> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"approval_policy = "untrusted"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let err = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async {
|
||||
Some(crate::config_loader::ConfigRequirementsToml {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
|
||||
..Default::default()
|
||||
})
|
||||
}))
|
||||
.build()
|
||||
.await
|
||||
.expect_err("explicit disallowed approval policy should fail");
|
||||
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
|
||||
let message = err.to_string();
|
||||
assert!(message.contains("invalid value for `approval_policy`"));
|
||||
assert!(message.contains("set by cloud requirements"));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user