feat: add support for allowed_web_search_modes in requirements.toml

Example:

```toml
# This means that "live" is not allowed; "disabled" is allowed even though not listed explicitly.
allowed_web_search_modes = ["cached"]
```

### Why
- Support admin/MDM/requirements.toml constraints over web-search behavior, independent of user config or sandbox defaults.
- Ensure per-turn config resolution and review-mode overrides can never crash when constraints are present.
- Note that `allowed_web_search_modes = ["cached"]` denies `"live"`, even if `--yolo` is used. See `resolve_web_search_mode_for_turn()`.

### What
- Add `allowed_web_search_modes` to requirements parsing and app-server v2 `ConfigRequirements` (`allowedWebSearchModes`), with schema/TS fixture updates.
- Introduce TOML-only `WebSearchModeRequirement` for requirements allowlists; accept `disabled|cached|live`, and treat an empty list as `[disabled]`.
- Convert the allowlist to a `ConstrainedWithSource<Option<WebSearchMode>>`, always permitting `Disabled` while enforcing membership for other values.
- Make `Config.web_search_mode` a constrained field, update call sites/tests, and surface constraint violations via warnings/fallback.
- Extend TUI `/debug-config` output to display `allowed_web_search_modes`.

### Safety
- Avoid `expect()` on constrained `web_search_mode` mutation in session per-turn config and review-thread setup; warn and keep the constrained value instead.
This commit is contained in:
Michael Bolin
2026-02-06 18:02:36 -08:00
parent ba8b5d9018
commit 2f8beb9f4e
17 changed files with 404 additions and 31 deletions
@@ -10988,6 +10988,15 @@
"null" "null"
] ]
}, },
"allowedWebSearchModes": {
"items": {
"$ref": "#/definitions/v2/WebSearchMode"
},
"type": [
"array",
"null"
]
},
"enforceResidency": { "enforceResidency": {
"anyOf": [ "anyOf": [
{ {
@@ -30,6 +30,15 @@
"null" "null"
] ]
}, },
"allowedWebSearchModes": {
"items": {
"$ref": "#/definitions/WebSearchMode"
},
"type": [
"array",
"null"
]
},
"enforceResidency": { "enforceResidency": {
"anyOf": [ "anyOf": [
{ {
@@ -56,6 +65,14 @@
"danger-full-access" "danger-full-access"
], ],
"type": "string" "type": "string"
},
"WebSearchMode": {
"enum": [
"disabled",
"cached",
"live"
],
"type": "string"
} }
}, },
"properties": { "properties": {
@@ -1,8 +1,9 @@
// GENERATED CODE! DO NOT MODIFY BY HAND! // GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { WebSearchMode } from "../WebSearchMode";
import type { AskForApproval } from "./AskForApproval"; import type { AskForApproval } from "./AskForApproval";
import type { ResidencyRequirement } from "./ResidencyRequirement"; import type { ResidencyRequirement } from "./ResidencyRequirement";
import type { SandboxMode } from "./SandboxMode"; import type { SandboxMode } from "./SandboxMode";
export type ConfigRequirements = { allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, enforceResidency: ResidencyRequirement | null, }; export type ConfigRequirements = { allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWebSearchModes: Array<WebSearchMode> | null, enforceResidency: ResidencyRequirement | null, };
@@ -532,6 +532,7 @@ pub struct ConfigReadResponse {
pub struct ConfigRequirements { pub struct ConfigRequirements {
pub allowed_approval_policies: Option<Vec<AskForApproval>>, pub allowed_approval_policies: Option<Vec<AskForApproval>>,
pub allowed_sandbox_modes: Option<Vec<SandboxMode>>, pub allowed_sandbox_modes: Option<Vec<SandboxMode>>,
pub allowed_web_search_modes: Option<Vec<WebSearchMode>>,
pub enforce_residency: Option<ResidencyRequirement>, pub enforce_residency: Option<ResidencyRequirement>,
} }
+1 -1
View File
@@ -116,7 +116,7 @@ Example (from OpenAI's official VSCode extension):
- `config/read` — fetch the effective config on disk after resolving config layering. - `config/read` — fetch the effective config on disk after resolving config layering.
- `config/value/write` — write a single config key/value to the user's config.toml on disk. - `config/value/write` — write a single config key/value to the user's config.toml on disk.
- `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk. - `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk.
- `configRequirements/read` — fetch the loaded requirements allow-lists and `enforceResidency` from `requirements.toml` and/or MDM (or `null` if none are configured). - `configRequirements/read` — fetch the loaded requirements allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`) and `enforceResidency` from `requirements.toml` and/or MDM (or `null` if none are configured).
### Example: Start or resume a thread ### Example: Start or resume a thread
+10
View File
@@ -115,6 +115,9 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR
.filter_map(map_sandbox_mode_requirement_to_api) .filter_map(map_sandbox_mode_requirement_to_api)
.collect() .collect()
}), }),
allowed_web_search_modes: requirements
.allowed_web_search_modes
.map(|modes| modes.into_iter().map(Into::into).collect()),
enforce_residency: requirements enforce_residency: requirements
.enforce_residency .enforce_residency
.map(map_residency_requirement_to_api), .map(map_residency_requirement_to_api),
@@ -177,6 +180,9 @@ mod tests {
CoreSandboxModeRequirement::ReadOnly, CoreSandboxModeRequirement::ReadOnly,
CoreSandboxModeRequirement::ExternalSandbox, CoreSandboxModeRequirement::ExternalSandbox,
]), ]),
allowed_web_search_modes: Some(vec![
codex_core::config_loader::WebSearchModeRequirement::Cached,
]),
mcp_servers: None, mcp_servers: None,
rules: None, rules: None,
enforce_residency: Some(CoreResidencyRequirement::Us), enforce_residency: Some(CoreResidencyRequirement::Us),
@@ -195,6 +201,10 @@ mod tests {
mapped.allowed_sandbox_modes, mapped.allowed_sandbox_modes,
Some(vec![SandboxMode::ReadOnly]), Some(vec![SandboxMode::ReadOnly]),
); );
assert_eq!(
mapped.allowed_web_search_modes,
Some(vec![codex_protocol::config_types::WebSearchMode::Cached]),
);
assert_eq!( assert_eq!(
mapped.enforce_residency, mapped.enforce_residency,
Some(codex_app_server_protocol::ResidencyRequirement::Us), Some(codex_app_server_protocol::ResidencyRequirement::Us),
+3
View File
@@ -381,6 +381,7 @@ mod tests {
Some(ConfigRequirementsToml { Some(ConfigRequirementsToml {
allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_sandbox_modes: None, allowed_sandbox_modes: None,
allowed_web_search_modes: None,
mcp_servers: None, mcp_servers: None,
rules: None, rules: None,
enforce_residency: None, enforce_residency: None,
@@ -421,6 +422,7 @@ mod tests {
Some(ConfigRequirementsToml { Some(ConfigRequirementsToml {
allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_sandbox_modes: None, allowed_sandbox_modes: None,
allowed_web_search_modes: None,
mcp_servers: None, mcp_servers: None,
rules: None, rules: None,
enforce_residency: None, enforce_residency: None,
@@ -464,6 +466,7 @@ mod tests {
Some(ConfigRequirementsToml { Some(ConfigRequirementsToml {
allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_sandbox_modes: None, allowed_sandbox_modes: None,
allowed_web_search_modes: None,
mcp_servers: None, mcp_servers: None,
rules: None, rules: None,
enforce_residency: None, enforce_residency: None,
+28 -5
View File
@@ -733,10 +733,22 @@ impl Session {
session_configuration.collaboration_mode.reasoning_effort(); session_configuration.collaboration_mode.reasoning_effort();
per_turn_config.model_reasoning_summary = session_configuration.model_reasoning_summary; per_turn_config.model_reasoning_summary = session_configuration.model_reasoning_summary;
per_turn_config.personality = session_configuration.personality; per_turn_config.personality = session_configuration.personality;
per_turn_config.web_search_mode = Some(resolve_web_search_mode_for_turn( let resolved_web_search_mode = resolve_web_search_mode_for_turn(
per_turn_config.web_search_mode, &per_turn_config.web_search_mode,
session_configuration.sandbox_policy.get(), session_configuration.sandbox_policy.get(),
)); );
if let Err(err) = per_turn_config
.web_search_mode
.set(Some(resolved_web_search_mode))
{
let fallback_value = per_turn_config.web_search_mode.value();
tracing::warn!(
error = %err,
?resolved_web_search_mode,
?fallback_value,
"resolved web_search_mode is disallowed by requirements; keeping constrained value"
);
}
per_turn_config.features = config.features.clone(); per_turn_config.features = config.features.clone();
per_turn_config per_turn_config
} }
@@ -794,7 +806,7 @@ impl Session {
let tools_config = ToolsConfig::new(&ToolsConfigParams { let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info, model_info: &model_info,
features: &per_turn_config.features, features: &per_turn_config.features,
web_search_mode: per_turn_config.web_search_mode, web_search_mode: per_turn_config.web_search_mode.value(),
}); });
let cwd = session_configuration.cwd.clone(); let cwd = session_configuration.cwd.clone();
@@ -3521,7 +3533,18 @@ async fn spawn_review_thread(
let mut per_turn_config = (*config).clone(); let mut per_turn_config = (*config).clone();
per_turn_config.model = Some(model.clone()); per_turn_config.model = Some(model.clone());
per_turn_config.features = review_features.clone(); per_turn_config.features = review_features.clone();
per_turn_config.web_search_mode = Some(review_web_search_mode); if let Err(err) = per_turn_config
.web_search_mode
.set(Some(review_web_search_mode))
{
let fallback_value = per_turn_config.web_search_mode.value();
tracing::warn!(
error = %err,
?review_web_search_mode,
?fallback_value,
"review web_search_mode is disallowed by requirements; keeping constrained value"
);
}
let otel_manager = parent_turn_context let otel_manager = parent_turn_context
.otel_manager .otel_manager
+67 -17
View File
@@ -329,7 +329,7 @@ pub struct Config {
pub include_apply_patch_tool: bool, pub include_apply_patch_tool: bool,
/// Explicit or feature-derived web search mode. /// Explicit or feature-derived web search mode.
pub web_search_mode: Option<WebSearchMode>, pub web_search_mode: Constrained<Option<WebSearchMode>>,
/// If set to `true`, used only the experimental unified exec tool. /// If set to `true`, used only the experimental unified exec tool.
pub use_experimental_unified_exec_tool: bool, pub use_experimental_unified_exec_tool: bool,
@@ -1331,17 +1331,25 @@ fn resolve_web_search_mode(
} }
pub(crate) fn resolve_web_search_mode_for_turn( pub(crate) fn resolve_web_search_mode_for_turn(
explicit_mode: Option<WebSearchMode>, web_search_mode: &Constrained<Option<WebSearchMode>>,
sandbox_policy: &SandboxPolicy, sandbox_policy: &SandboxPolicy,
) -> WebSearchMode { ) -> WebSearchMode {
if let Some(mode) = explicit_mode { if let Some(mode) = web_search_mode.value() {
return mode; return mode;
} }
if matches!(sandbox_policy, SandboxPolicy::DangerFullAccess) {
WebSearchMode::Live let (preferred, alternate) = if matches!(sandbox_policy, SandboxPolicy::DangerFullAccess) {
(WebSearchMode::Live, WebSearchMode::Cached)
} else { } else {
WebSearchMode::Cached (WebSearchMode::Cached, WebSearchMode::Live)
};
for mode in [preferred, alternate, WebSearchMode::Disabled] {
let candidate = Some(mode);
if web_search_mode.can_set(&candidate).is_ok() {
return mode;
}
} }
WebSearchMode::Disabled
} }
impl Config { impl Config {
@@ -1626,6 +1634,7 @@ impl Config {
let ConfigRequirements { let ConfigRequirements {
approval_policy: mut constrained_approval_policy, approval_policy: mut constrained_approval_policy,
sandbox_policy: mut constrained_sandbox_policy, sandbox_policy: mut constrained_sandbox_policy,
web_search_mode: mut constrained_web_search_mode,
mcp_servers, mcp_servers,
exec_policy: _, exec_policy: _,
enforce_residency, enforce_residency,
@@ -1643,6 +1652,12 @@ impl Config {
&mut constrained_sandbox_policy, &mut constrained_sandbox_policy,
&mut startup_warnings, &mut startup_warnings,
)?; )?;
apply_requirement_constrained_value(
"web_search_mode",
web_search_mode,
&mut constrained_web_search_mode,
&mut startup_warnings,
)?;
let mcp_servers = constrain_mcp_servers(cfg.mcp_servers.clone(), mcp_servers.as_ref()) let mcp_servers = constrain_mcp_servers(cfg.mcp_servers.clone(), mcp_servers.as_ref())
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{e}")))?; .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{e}")))?;
@@ -1722,7 +1737,7 @@ impl Config {
forced_chatgpt_workspace_id, forced_chatgpt_workspace_id,
forced_login_method, forced_login_method,
include_apply_patch_tool: include_apply_patch_tool_flag, include_apply_patch_tool: include_apply_patch_tool_flag,
web_search_mode, web_search_mode: constrained_web_search_mode.value,
use_experimental_unified_exec_tool, use_experimental_unified_exec_tool,
ghost_snapshot, ghost_snapshot,
features, features,
@@ -2463,24 +2478,26 @@ trust_level = "trusted"
#[test] #[test]
fn web_search_mode_for_turn_defaults_to_cached_when_unset() { fn web_search_mode_for_turn_defaults_to_cached_when_unset() {
let mode = resolve_web_search_mode_for_turn(None, &SandboxPolicy::ReadOnly); let web_search_mode = Constrained::allow_any(None);
let mode = resolve_web_search_mode_for_turn(&web_search_mode, &SandboxPolicy::ReadOnly);
assert_eq!(mode, WebSearchMode::Cached); assert_eq!(mode, WebSearchMode::Cached);
} }
#[test] #[test]
fn web_search_mode_for_turn_defaults_to_live_for_danger_full_access() { fn web_search_mode_for_turn_defaults_to_live_for_danger_full_access() {
let mode = resolve_web_search_mode_for_turn(None, &SandboxPolicy::DangerFullAccess); let web_search_mode = Constrained::allow_any(None);
let mode =
resolve_web_search_mode_for_turn(&web_search_mode, &SandboxPolicy::DangerFullAccess);
assert_eq!(mode, WebSearchMode::Live); assert_eq!(mode, WebSearchMode::Live);
} }
#[test] #[test]
fn web_search_mode_for_turn_prefers_explicit_value() { fn web_search_mode_for_turn_prefers_explicit_value() {
let mode = resolve_web_search_mode_for_turn( let web_search_mode = Constrained::allow_any(Some(WebSearchMode::Cached));
Some(WebSearchMode::Cached), let mode =
&SandboxPolicy::DangerFullAccess, resolve_web_search_mode_for_turn(&web_search_mode, &SandboxPolicy::DangerFullAccess);
);
assert_eq!(mode, WebSearchMode::Cached); assert_eq!(mode, WebSearchMode::Cached);
} }
@@ -3983,7 +4000,7 @@ model_verbosity = "high"
forced_chatgpt_workspace_id: None, forced_chatgpt_workspace_id: None,
forced_login_method: None, forced_login_method: None,
include_apply_patch_tool: false, include_apply_patch_tool: false,
web_search_mode: None, web_search_mode: Constrained::allow_any(None),
use_experimental_unified_exec_tool: !cfg!(windows), use_experimental_unified_exec_tool: !cfg!(windows),
ghost_snapshot: GhostSnapshotConfig::default(), ghost_snapshot: GhostSnapshotConfig::default(),
features: Features::with_defaults(), features: Features::with_defaults(),
@@ -4071,7 +4088,7 @@ model_verbosity = "high"
forced_chatgpt_workspace_id: None, forced_chatgpt_workspace_id: None,
forced_login_method: None, forced_login_method: None,
include_apply_patch_tool: false, include_apply_patch_tool: false,
web_search_mode: None, web_search_mode: Constrained::allow_any(None),
use_experimental_unified_exec_tool: !cfg!(windows), use_experimental_unified_exec_tool: !cfg!(windows),
ghost_snapshot: GhostSnapshotConfig::default(), ghost_snapshot: GhostSnapshotConfig::default(),
features: Features::with_defaults(), features: Features::with_defaults(),
@@ -4174,7 +4191,7 @@ model_verbosity = "high"
forced_chatgpt_workspace_id: None, forced_chatgpt_workspace_id: None,
forced_login_method: None, forced_login_method: None,
include_apply_patch_tool: false, include_apply_patch_tool: false,
web_search_mode: None, web_search_mode: Constrained::allow_any(None),
use_experimental_unified_exec_tool: !cfg!(windows), use_experimental_unified_exec_tool: !cfg!(windows),
ghost_snapshot: GhostSnapshotConfig::default(), ghost_snapshot: GhostSnapshotConfig::default(),
features: Features::with_defaults(), features: Features::with_defaults(),
@@ -4263,7 +4280,7 @@ model_verbosity = "high"
forced_chatgpt_workspace_id: None, forced_chatgpt_workspace_id: None,
forced_login_method: None, forced_login_method: None,
include_apply_patch_tool: false, include_apply_patch_tool: false,
web_search_mode: None, web_search_mode: Constrained::allow_any(None),
use_experimental_unified_exec_tool: !cfg!(windows), use_experimental_unified_exec_tool: !cfg!(windows),
ghost_snapshot: GhostSnapshotConfig::default(), ghost_snapshot: GhostSnapshotConfig::default(),
features: Features::with_defaults(), features: Features::with_defaults(),
@@ -4810,6 +4827,7 @@ mcp_oauth_callback_port = 5678
allowed_sandbox_modes: Some(vec![ allowed_sandbox_modes: Some(vec![
crate::config_loader::SandboxModeRequirement::ReadOnly, crate::config_loader::SandboxModeRequirement::ReadOnly,
]), ]),
allowed_web_search_modes: None,
mcp_servers: None, mcp_servers: None,
rules: None, rules: None,
enforce_residency: None, enforce_residency: None,
@@ -4827,6 +4845,38 @@ mcp_oauth_callback_port = 5678
Ok(()) Ok(())
} }
#[tokio::test]
async fn requirements_web_search_mode_overrides_danger_full_access_default()
-> 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 config = 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_web_search_modes: Some(vec![
crate::config_loader::WebSearchModeRequirement::Cached,
]),
..Default::default()
})
}))
.build()
.await?;
assert_eq!(config.web_search_mode.value(), Some(WebSearchMode::Cached));
assert_eq!(
resolve_web_search_mode_for_turn(&config.web_search_mode, config.sandbox_policy.get()),
WebSearchMode::Cached,
);
Ok(())
}
#[tokio::test] #[tokio::test]
async fn requirements_disallowing_default_approval_falls_back_to_required_default() async fn requirements_disallowing_default_approval_falls_back_to_required_default()
-> std::io::Result<()> { -> std::io::Result<()> {
@@ -1,4 +1,5 @@
use codex_protocol::config_types::SandboxMode; use codex_protocol::config_types::SandboxMode;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::AbsolutePathBuf;
@@ -76,6 +77,7 @@ impl<T> std::ops::DerefMut for ConstrainedWithSource<T> {
pub struct ConfigRequirements { pub struct ConfigRequirements {
pub approval_policy: ConstrainedWithSource<AskForApproval>, pub approval_policy: ConstrainedWithSource<AskForApproval>,
pub sandbox_policy: ConstrainedWithSource<SandboxPolicy>, pub sandbox_policy: ConstrainedWithSource<SandboxPolicy>,
pub web_search_mode: ConstrainedWithSource<Option<WebSearchMode>>,
pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>, pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>,
pub(crate) exec_policy: Option<Sourced<RequirementsExecPolicy>>, pub(crate) exec_policy: Option<Sourced<RequirementsExecPolicy>>,
pub enforce_residency: ConstrainedWithSource<Option<ResidencyRequirement>>, pub enforce_residency: ConstrainedWithSource<Option<ResidencyRequirement>>,
@@ -92,6 +94,7 @@ impl Default for ConfigRequirements {
Constrained::allow_any(SandboxPolicy::ReadOnly), Constrained::allow_any(SandboxPolicy::ReadOnly),
None, None,
), ),
web_search_mode: ConstrainedWithSource::new(Constrained::allow_any(None), None),
mcp_servers: None, mcp_servers: None,
exec_policy: None, exec_policy: None,
enforce_residency: ConstrainedWithSource::new(Constrained::allow_any(None), None), enforce_residency: ConstrainedWithSource::new(Constrained::allow_any(None), None),
@@ -117,11 +120,50 @@ pub struct McpServerRequirement {
pub identity: McpServerIdentity, pub identity: McpServerIdentity,
} }
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(rename_all = "lowercase")]
pub enum WebSearchModeRequirement {
Disabled,
Cached,
Live,
}
impl From<WebSearchMode> for WebSearchModeRequirement {
fn from(mode: WebSearchMode) -> Self {
match mode {
WebSearchMode::Disabled => WebSearchModeRequirement::Disabled,
WebSearchMode::Cached => WebSearchModeRequirement::Cached,
WebSearchMode::Live => WebSearchModeRequirement::Live,
}
}
}
impl From<WebSearchModeRequirement> for WebSearchMode {
fn from(mode: WebSearchModeRequirement) -> Self {
match mode {
WebSearchModeRequirement::Disabled => WebSearchMode::Disabled,
WebSearchModeRequirement::Cached => WebSearchMode::Cached,
WebSearchModeRequirement::Live => WebSearchMode::Live,
}
}
}
impl fmt::Display for WebSearchModeRequirement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
WebSearchModeRequirement::Disabled => write!(f, "disabled"),
WebSearchModeRequirement::Cached => write!(f, "cached"),
WebSearchModeRequirement::Live => write!(f, "live"),
}
}
}
/// Base config deserialized from /etc/codex/requirements.toml or MDM. /// Base config deserialized from /etc/codex/requirements.toml or MDM.
#[derive(Deserialize, Debug, Clone, Default, PartialEq)] #[derive(Deserialize, Debug, Clone, Default, PartialEq)]
pub struct ConfigRequirementsToml { pub struct ConfigRequirementsToml {
pub allowed_approval_policies: Option<Vec<AskForApproval>>, pub allowed_approval_policies: Option<Vec<AskForApproval>>,
pub allowed_sandbox_modes: Option<Vec<SandboxModeRequirement>>, pub allowed_sandbox_modes: Option<Vec<SandboxModeRequirement>>,
pub allowed_web_search_modes: Option<Vec<WebSearchModeRequirement>>,
pub mcp_servers: Option<BTreeMap<String, McpServerRequirement>>, pub mcp_servers: Option<BTreeMap<String, McpServerRequirement>>,
pub rules: Option<RequirementsExecPolicyToml>, pub rules: Option<RequirementsExecPolicyToml>,
pub enforce_residency: Option<ResidencyRequirement>, pub enforce_residency: Option<ResidencyRequirement>,
@@ -153,6 +195,7 @@ impl<T> std::ops::Deref for Sourced<T> {
pub struct ConfigRequirementsWithSources { pub struct ConfigRequirementsWithSources {
pub allowed_approval_policies: Option<Sourced<Vec<AskForApproval>>>, pub allowed_approval_policies: Option<Sourced<Vec<AskForApproval>>>,
pub allowed_sandbox_modes: Option<Sourced<Vec<SandboxModeRequirement>>>, pub allowed_sandbox_modes: Option<Sourced<Vec<SandboxModeRequirement>>>,
pub allowed_web_search_modes: Option<Sourced<Vec<WebSearchModeRequirement>>>,
pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>, pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>,
pub rules: Option<Sourced<RequirementsExecPolicyToml>>, pub rules: Option<Sourced<RequirementsExecPolicyToml>>,
pub enforce_residency: Option<Sourced<ResidencyRequirement>>, pub enforce_residency: Option<Sourced<ResidencyRequirement>>,
@@ -186,6 +229,7 @@ impl ConfigRequirementsWithSources {
{ {
allowed_approval_policies, allowed_approval_policies,
allowed_sandbox_modes, allowed_sandbox_modes,
allowed_web_search_modes,
mcp_servers, mcp_servers,
rules, rules,
enforce_residency, enforce_residency,
@@ -197,6 +241,7 @@ impl ConfigRequirementsWithSources {
let ConfigRequirementsWithSources { let ConfigRequirementsWithSources {
allowed_approval_policies, allowed_approval_policies,
allowed_sandbox_modes, allowed_sandbox_modes,
allowed_web_search_modes,
mcp_servers, mcp_servers,
rules, rules,
enforce_residency, enforce_residency,
@@ -204,6 +249,7 @@ impl ConfigRequirementsWithSources {
ConfigRequirementsToml { ConfigRequirementsToml {
allowed_approval_policies: allowed_approval_policies.map(|sourced| sourced.value), allowed_approval_policies: allowed_approval_policies.map(|sourced| sourced.value),
allowed_sandbox_modes: allowed_sandbox_modes.map(|sourced| sourced.value), allowed_sandbox_modes: allowed_sandbox_modes.map(|sourced| sourced.value),
allowed_web_search_modes: allowed_web_search_modes.map(|sourced| sourced.value),
mcp_servers: mcp_servers.map(|sourced| sourced.value), mcp_servers: mcp_servers.map(|sourced| sourced.value),
rules: rules.map(|sourced| sourced.value), rules: rules.map(|sourced| sourced.value),
enforce_residency: enforce_residency.map(|sourced| sourced.value), enforce_residency: enforce_residency.map(|sourced| sourced.value),
@@ -248,6 +294,7 @@ impl ConfigRequirementsToml {
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.allowed_approval_policies.is_none() self.allowed_approval_policies.is_none()
&& self.allowed_sandbox_modes.is_none() && self.allowed_sandbox_modes.is_none()
&& self.allowed_web_search_modes.is_none()
&& self.mcp_servers.is_none() && self.mcp_servers.is_none()
&& self.rules.is_none() && self.rules.is_none()
&& self.enforce_residency.is_none() && self.enforce_residency.is_none()
@@ -261,6 +308,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
let ConfigRequirementsWithSources { let ConfigRequirementsWithSources {
allowed_approval_policies, allowed_approval_policies,
allowed_sandbox_modes, allowed_sandbox_modes,
allowed_web_search_modes,
mcp_servers, mcp_servers,
rules, rules,
enforce_residency, enforce_residency,
@@ -356,6 +404,48 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
} }
None => None, None => None,
}; };
let web_search_mode = match allowed_web_search_modes {
Some(Sourced {
value: mut modes,
source: requirement_source,
}) => {
if modes.is_empty() {
modes.push(WebSearchModeRequirement::Disabled);
}
let initial_mode = match modes.first().copied() {
Some(mode) => mode,
None => WebSearchModeRequirement::Disabled,
};
let mut accepted = modes.into_iter().collect::<std::collections::BTreeSet<_>>();
accepted.insert(WebSearchModeRequirement::Disabled);
let accepted_for_error: Vec<WebSearchMode> =
accepted.iter().copied().map(Into::into).collect();
let initial_value = Some(WebSearchMode::from(initial_mode));
let requirement_source_for_error = requirement_source.clone();
let constrained = Constrained::new(initial_value, move |candidate| {
let Some(mode) = candidate else {
return Err(ConstraintError::InvalidValue {
field_name: "web_search_mode",
candidate: format!("{candidate:?}"),
allowed: format!("{accepted_for_error:?}"),
requirement_source: requirement_source_for_error.clone(),
});
};
if accepted.contains(&(*mode).into()) {
Ok(())
} else {
Err(ConstraintError::InvalidValue {
field_name: "web_search_mode",
candidate: format!("{candidate:?}"),
allowed: format!("{accepted_for_error:?}"),
requirement_source: requirement_source_for_error.clone(),
})
}
})?;
ConstrainedWithSource::new(constrained, Some(requirement_source))
}
None => ConstrainedWithSource::new(Constrained::allow_any(None), None),
};
let enforce_residency = match enforce_residency { let enforce_residency = match enforce_residency {
Some(Sourced { Some(Sourced {
@@ -383,6 +473,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
Ok(ConfigRequirements { Ok(ConfigRequirements {
approval_policy, approval_policy,
sandbox_policy, sandbox_policy,
web_search_mode,
mcp_servers, mcp_servers,
exec_policy, exec_policy,
enforce_residency, enforce_residency,
@@ -410,6 +501,7 @@ mod tests {
let ConfigRequirementsToml { let ConfigRequirementsToml {
allowed_approval_policies, allowed_approval_policies,
allowed_sandbox_modes, allowed_sandbox_modes,
allowed_web_search_modes,
mcp_servers, mcp_servers,
rules, rules,
enforce_residency, enforce_residency,
@@ -419,6 +511,8 @@ mod tests {
.map(|value| Sourced::new(value, RequirementSource::Unknown)), .map(|value| Sourced::new(value, RequirementSource::Unknown)),
allowed_sandbox_modes: allowed_sandbox_modes allowed_sandbox_modes: allowed_sandbox_modes
.map(|value| Sourced::new(value, RequirementSource::Unknown)), .map(|value| Sourced::new(value, RequirementSource::Unknown)),
allowed_web_search_modes: allowed_web_search_modes
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
mcp_servers: mcp_servers.map(|value| Sourced::new(value, RequirementSource::Unknown)), mcp_servers: mcp_servers.map(|value| Sourced::new(value, RequirementSource::Unknown)),
rules: rules.map(|value| Sourced::new(value, RequirementSource::Unknown)), rules: rules.map(|value| Sourced::new(value, RequirementSource::Unknown)),
enforce_residency: enforce_residency enforce_residency: enforce_residency
@@ -436,6 +530,10 @@ mod tests {
SandboxModeRequirement::WorkspaceWrite, SandboxModeRequirement::WorkspaceWrite,
SandboxModeRequirement::DangerFullAccess, SandboxModeRequirement::DangerFullAccess,
]; ];
let allowed_web_search_modes = vec![
WebSearchModeRequirement::Cached,
WebSearchModeRequirement::Live,
];
let enforce_residency = ResidencyRequirement::Us; let enforce_residency = ResidencyRequirement::Us;
let enforce_source = source.clone(); let enforce_source = source.clone();
@@ -444,6 +542,7 @@ mod tests {
let other = ConfigRequirementsToml { let other = ConfigRequirementsToml {
allowed_approval_policies: Some(allowed_approval_policies.clone()), allowed_approval_policies: Some(allowed_approval_policies.clone()),
allowed_sandbox_modes: Some(allowed_sandbox_modes.clone()), allowed_sandbox_modes: Some(allowed_sandbox_modes.clone()),
allowed_web_search_modes: Some(allowed_web_search_modes.clone()),
mcp_servers: None, mcp_servers: None,
rules: None, rules: None,
enforce_residency: Some(enforce_residency), enforce_residency: Some(enforce_residency),
@@ -459,6 +558,10 @@ mod tests {
source.clone() source.clone()
)), )),
allowed_sandbox_modes: Some(Sourced::new(allowed_sandbox_modes, source)), allowed_sandbox_modes: Some(Sourced::new(allowed_sandbox_modes, source)),
allowed_web_search_modes: Some(Sourced::new(
allowed_web_search_modes,
enforce_source.clone(),
)),
mcp_servers: None, mcp_servers: None,
rules: None, rules: None,
enforce_residency: Some(Sourced::new(enforce_residency, enforce_source)), enforce_residency: Some(Sourced::new(enforce_residency, enforce_source)),
@@ -489,6 +592,7 @@ mod tests {
source_location, source_location,
)), )),
allowed_sandbox_modes: None, allowed_sandbox_modes: None,
allowed_web_search_modes: None,
mcp_servers: None, mcp_servers: None,
rules: None, rules: None,
enforce_residency: None, enforce_residency: None,
@@ -527,6 +631,7 @@ mod tests {
existing_source, existing_source,
)), )),
allowed_sandbox_modes: None, allowed_sandbox_modes: None,
allowed_web_search_modes: None,
mcp_servers: None, mcp_servers: None,
rules: None, rules: None,
enforce_residency: None, enforce_residency: None,
@@ -615,6 +720,7 @@ mod tests {
r#" r#"
allowed_approval_policies = ["on-request"] allowed_approval_policies = ["on-request"]
allowed_sandbox_modes = ["read-only"] allowed_sandbox_modes = ["read-only"]
allowed_web_search_modes = ["cached"]
enforce_residency = "us" enforce_residency = "us"
"#, "#,
)?; )?;
@@ -632,6 +738,10 @@ mod tests {
requirements.sandbox_policy.source, requirements.sandbox_policy.source,
Some(source_location.clone()) Some(source_location.clone())
); );
assert_eq!(
requirements.web_search_mode.source,
Some(source_location.clone())
);
assert_eq!(requirements.enforce_residency.source, Some(source_location)); assert_eq!(requirements.enforce_residency.source, Some(source_location));
Ok(()) Ok(())
@@ -746,6 +856,100 @@ mod tests {
Ok(()) Ok(())
} }
#[test]
fn deserialize_allowed_web_search_modes() -> Result<()> {
let toml_str = r#"
allowed_web_search_modes = ["cached"]
"#;
let config: ConfigRequirementsToml = from_str(toml_str)?;
let requirements: ConfigRequirements = with_unknown_source(config).try_into()?;
assert_eq!(
requirements.web_search_mode.value(),
Some(WebSearchMode::Cached)
);
assert!(
requirements
.web_search_mode
.can_set(&Some(WebSearchMode::Disabled))
.is_ok()
);
assert_eq!(
requirements
.web_search_mode
.can_set(&Some(WebSearchMode::Live)),
Err(ConstraintError::InvalidValue {
field_name: "web_search_mode",
candidate: "Some(Live)".into(),
allowed: "[Disabled, Cached]".into(),
requirement_source: RequirementSource::Unknown,
})
);
assert_eq!(
requirements.web_search_mode.can_set(&None),
Err(ConstraintError::InvalidValue {
field_name: "web_search_mode",
candidate: "None".into(),
allowed: "[Disabled, Cached]".into(),
requirement_source: RequirementSource::Unknown,
})
);
Ok(())
}
#[test]
fn allowed_web_search_modes_allows_disabled_and_sets_default() -> Result<()> {
let toml_str = r#"
allowed_web_search_modes = ["disabled"]
"#;
let config: ConfigRequirementsToml = from_str(toml_str)?;
let requirements: ConfigRequirements = with_unknown_source(config).try_into()?;
assert_eq!(
requirements.web_search_mode.value(),
Some(WebSearchMode::Disabled)
);
assert_eq!(
requirements
.web_search_mode
.can_set(&Some(WebSearchMode::Cached)),
Err(ConstraintError::InvalidValue {
field_name: "web_search_mode",
candidate: "Some(Cached)".into(),
allowed: "[Disabled]".into(),
requirement_source: RequirementSource::Unknown,
})
);
Ok(())
}
#[test]
fn allowed_web_search_modes_empty_defaults_to_disabled() -> Result<()> {
let toml_str = r#"
allowed_web_search_modes = []
"#;
let config: ConfigRequirementsToml = from_str(toml_str)?;
let requirements: ConfigRequirements = with_unknown_source(config).try_into()?;
assert_eq!(
requirements.web_search_mode.value(),
Some(WebSearchMode::Disabled)
);
assert_eq!(
requirements
.web_search_mode
.can_set(&Some(WebSearchMode::Cached)),
Err(ConstraintError::InvalidValue {
field_name: "web_search_mode",
candidate: "Some(Cached)".into(),
allowed: "[Disabled]".into(),
requirement_source: RequirementSource::Unknown,
})
);
Ok(())
}
#[test] #[test]
fn deserialize_mcp_server_requirements() -> Result<()> { fn deserialize_mcp_server_requirements() -> Result<()> {
let toml_str = r#" let toml_str = r#"
+1
View File
@@ -41,6 +41,7 @@ pub use config_requirements::RequirementSource;
pub use config_requirements::ResidencyRequirement; pub use config_requirements::ResidencyRequirement;
pub use config_requirements::SandboxModeRequirement; pub use config_requirements::SandboxModeRequirement;
pub use config_requirements::Sourced; pub use config_requirements::Sourced;
pub use config_requirements::WebSearchModeRequirement;
pub use diagnostics::ConfigError; pub use diagnostics::ConfigError;
pub use diagnostics::ConfigLoadError; pub use diagnostics::ConfigLoadError;
pub use diagnostics::TextPosition; pub use diagnostics::TextPosition;
+16
View File
@@ -16,6 +16,7 @@ use crate::config_loader::config_requirements::RequirementSource;
use crate::config_loader::fingerprint::version_for_toml; use crate::config_loader::fingerprint::version_for_toml;
use crate::config_loader::load_requirements_toml; use crate::config_loader::load_requirements_toml;
use codex_protocol::config_types::TrustLevel; use codex_protocol::config_types::TrustLevel;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::AskForApproval;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SandboxPolicy;
@@ -475,6 +476,7 @@ async fn load_requirements_toml_produces_expected_constraints() -> anyhow::Resul
&requirements_file, &requirements_file,
r#" r#"
allowed_approval_policies = ["never", "on-request"] allowed_approval_policies = ["never", "on-request"]
allowed_web_search_modes = ["cached"]
enforce_residency = "us" enforce_residency = "us"
"#, "#,
) )
@@ -490,6 +492,13 @@ enforce_residency = "us"
.cloned(), .cloned(),
Some(vec![AskForApproval::Never, AskForApproval::OnRequest]) Some(vec![AskForApproval::Never, AskForApproval::OnRequest])
); );
assert_eq!(
config_requirements_toml
.allowed_web_search_modes
.as_deref()
.cloned(),
Some(vec![crate::config_loader::WebSearchModeRequirement::Cached])
);
let config_requirements: ConfigRequirements = config_requirements_toml.try_into()?; let config_requirements: ConfigRequirements = config_requirements_toml.try_into()?;
assert_eq!( assert_eq!(
config_requirements.approval_policy.value(), config_requirements.approval_policy.value(),
@@ -504,6 +513,10 @@ enforce_residency = "us"
.can_set(&AskForApproval::OnFailure) .can_set(&AskForApproval::OnFailure)
.is_err() .is_err()
); );
assert_eq!(
config_requirements.web_search_mode.value(),
Some(WebSearchMode::Cached)
);
assert_eq!( assert_eq!(
config_requirements.enforce_residency.value(), config_requirements.enforce_residency.value(),
Some(crate::config_loader::ResidencyRequirement::Us) Some(crate::config_loader::ResidencyRequirement::Us)
@@ -536,6 +549,7 @@ allowed_approval_policies = ["on-request"]
Some(ConfigRequirementsToml { Some(ConfigRequirementsToml {
allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_sandbox_modes: None, allowed_sandbox_modes: None,
allowed_web_search_modes: None,
mcp_servers: None, mcp_servers: None,
rules: None, rules: None,
enforce_residency: None, enforce_residency: None,
@@ -582,6 +596,7 @@ allowed_approval_policies = ["on-request"]
ConfigRequirementsToml { ConfigRequirementsToml {
allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_sandbox_modes: None, allowed_sandbox_modes: None,
allowed_web_search_modes: None,
mcp_servers: None, mcp_servers: None,
rules: None, rules: None,
enforce_residency: None, enforce_residency: None,
@@ -617,6 +632,7 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()>
let requirements = ConfigRequirementsToml { let requirements = ConfigRequirementsToml {
allowed_approval_policies: Some(vec![AskForApproval::Never]), allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_sandbox_modes: None, allowed_sandbox_modes: None,
allowed_web_search_modes: None,
mcp_servers: None, mcp_servers: None,
rules: None, rules: None,
enforce_residency: None, enforce_residency: None,
+4 -1
View File
@@ -86,7 +86,10 @@ async fn start_review_conversation(
let mut sub_agent_config = config.as_ref().clone(); let mut sub_agent_config = config.as_ref().clone();
// Carry over review-only feature restrictions so the delegate cannot // Carry over review-only feature restrictions so the delegate cannot
// re-enable blocked tools (web search, view image). // re-enable blocked tools (web search, view image).
sub_agent_config.web_search_mode = Some(WebSearchMode::Disabled); sub_agent_config
.web_search_mode
.set(Some(WebSearchMode::Disabled))
.expect("review web_search_mode should satisfy constraints");
// Set explicit review rubric for the sub-agent // Set explicit review rubric for the sub-agent
sub_agent_config.base_instructions = Some(crate::REVIEW_PROMPT.to_string()); sub_agent_config.base_instructions = Some(crate::REVIEW_PROMPT.to_string());
+4 -1
View File
@@ -36,7 +36,10 @@ async fn collect_tool_identifiers_for_model(model: &str) -> Vec<String> {
.with_model(model) .with_model(model)
// Keep tool expectations stable when the default web_search mode changes. // Keep tool expectations stable when the default web_search mode changes.
.with_config(|config| { .with_config(|config| {
config.web_search_mode = Some(WebSearchMode::Cached); config
.web_search_mode
.set(Some(WebSearchMode::Cached))
.expect("test web_search_mode should satisfy constraints");
config.features.enable(Feature::CollaborationModes); config.features.enable(Feature::CollaborationModes);
}); });
let test = builder let test = builder
+4 -1
View File
@@ -97,7 +97,10 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> {
config.user_instructions = Some("be consistent and helpful".to_string()); config.user_instructions = Some("be consistent and helpful".to_string());
config.model = Some("gpt-5.1-codex-max".to_string()); config.model = Some("gpt-5.1-codex-max".to_string());
// Keep tool expectations stable when the default web_search mode changes. // Keep tool expectations stable when the default web_search mode changes.
config.web_search_mode = Some(WebSearchMode::Cached); config
.web_search_mode
.set(Some(WebSearchMode::Cached))
.expect("test web_search_mode should satisfy constraints");
config.features.enable(Feature::CollaborationModes); config.features.enable(Feature::CollaborationModes);
}) })
.build(&server) .build(&server)
+16 -4
View File
@@ -34,7 +34,10 @@ async fn web_search_mode_cached_sets_external_web_access_false() {
let mut builder = test_codex() let mut builder = test_codex()
.with_model("gpt-5-codex") .with_model("gpt-5-codex")
.with_config(|config| { .with_config(|config| {
config.web_search_mode = Some(WebSearchMode::Cached); config
.web_search_mode
.set(Some(WebSearchMode::Cached))
.expect("test web_search_mode should satisfy constraints");
}); });
let test = builder let test = builder
.build(&server) .build(&server)
@@ -69,7 +72,10 @@ async fn web_search_mode_takes_precedence_over_legacy_flags() {
.with_model("gpt-5-codex") .with_model("gpt-5-codex")
.with_config(|config| { .with_config(|config| {
config.features.enable(Feature::WebSearchRequest); config.features.enable(Feature::WebSearchRequest);
config.web_search_mode = Some(WebSearchMode::Cached); config
.web_search_mode
.set(Some(WebSearchMode::Cached))
.expect("test web_search_mode should satisfy constraints");
}); });
let test = builder let test = builder
.build(&server) .build(&server)
@@ -103,7 +109,10 @@ async fn web_search_mode_defaults_to_cached_when_unset() {
let mut builder = test_codex() let mut builder = test_codex()
.with_model("gpt-5-codex") .with_model("gpt-5-codex")
.with_config(|config| { .with_config(|config| {
config.web_search_mode = None; config
.web_search_mode
.set(None)
.expect("test web_search_mode should satisfy constraints");
config.features.disable(Feature::WebSearchCached); config.features.disable(Feature::WebSearchCached);
config.features.disable(Feature::WebSearchRequest); config.features.disable(Feature::WebSearchRequest);
}); });
@@ -148,7 +157,10 @@ async fn web_search_mode_updates_between_turns_with_sandbox_policy() {
let mut builder = test_codex() let mut builder = test_codex()
.with_model("gpt-5-codex") .with_model("gpt-5-codex")
.with_config(|config| { .with_config(|config| {
config.web_search_mode = None; config
.web_search_mode
.set(None)
.expect("test web_search_mode should satisfy constraints");
config.features.disable(Feature::WebSearchCached); config.features.disable(Feature::WebSearchCached);
config.features.disable(Feature::WebSearchRequest); config.features.disable(Feature::WebSearchRequest);
}); });
+17
View File
@@ -70,6 +70,15 @@ fn render_debug_config_lines(stack: &ConfigLayerStack) -> Vec<Line<'static>> {
)); ));
} }
if let Some(modes) = requirements_toml.allowed_web_search_modes.as_ref() {
let value = join_or_empty(modes.iter().map(ToString::to_string).collect::<Vec<_>>());
requirement_lines.push(requirement_line(
"allowed_web_search_modes",
value,
requirements.web_search_mode.source.as_ref(),
));
}
if let Some(servers) = requirements_toml.mcp_servers.as_ref() { if let Some(servers) = requirements_toml.mcp_servers.as_ref() {
let value = join_or_empty(servers.keys().cloned().collect::<Vec<_>>()); let value = join_or_empty(servers.keys().cloned().collect::<Vec<_>>());
requirement_lines.push(requirement_line( requirement_lines.push(requirement_line(
@@ -185,8 +194,10 @@ mod tests {
use codex_core::config_loader::ResidencyRequirement; use codex_core::config_loader::ResidencyRequirement;
use codex_core::config_loader::SandboxModeRequirement; use codex_core::config_loader::SandboxModeRequirement;
use codex_core::config_loader::Sourced; use codex_core::config_loader::Sourced;
use codex_core::config_loader::WebSearchModeRequirement;
use codex_core::protocol::AskForApproval; use codex_core::protocol::AskForApproval;
use codex_core::protocol::SandboxPolicy; use codex_core::protocol::SandboxPolicy;
use codex_protocol::config_types::WebSearchMode;
use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::AbsolutePathBuf;
use ratatui::text::Line; use ratatui::text::Line;
use std::collections::BTreeMap; use std::collections::BTreeMap;
@@ -287,10 +298,15 @@ mod tests {
Constrained::allow_any(Some(ResidencyRequirement::Us)), Constrained::allow_any(Some(ResidencyRequirement::Us)),
Some(RequirementSource::CloudRequirements), Some(RequirementSource::CloudRequirements),
); );
requirements.web_search_mode = ConstrainedWithSource::new(
Constrained::allow_any(Some(WebSearchMode::Cached)),
Some(RequirementSource::CloudRequirements),
);
let requirements_toml = ConfigRequirementsToml { let requirements_toml = ConfigRequirementsToml {
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]), allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
allowed_sandbox_modes: Some(vec![SandboxModeRequirement::ReadOnly]), allowed_sandbox_modes: Some(vec![SandboxModeRequirement::ReadOnly]),
allowed_web_search_modes: Some(vec![WebSearchModeRequirement::Cached]),
mcp_servers: Some(BTreeMap::from([( mcp_servers: Some(BTreeMap::from([(
"docs".to_string(), "docs".to_string(),
McpServerRequirement { McpServerRequirement {
@@ -331,6 +347,7 @@ mod tests {
.as_str(), .as_str(),
) )
); );
assert!(rendered.contains("allowed_web_search_modes: cached (source: cloud requirements)"));
assert!(rendered.contains("mcp_servers: docs (source: MDM managed_config.toml (legacy))")); assert!(rendered.contains("mcp_servers: docs (source: MDM managed_config.toml (legacy))"));
assert!(rendered.contains("enforce_residency: us (source: cloud requirements)")); assert!(rendered.contains("enforce_residency: us (source: cloud requirements)"));
assert!(!rendered.contains(" - rules:")); assert!(!rendered.contains(" - rules:"));