feat: add support for allowed_web_search_modes in requirements.toml (#10964)

This PR makes it possible to disable live web search via an enterprise
config even if the user is running in `--yolo` mode (though cached web
search will still be available). To do this, create
`/etc/codex/requirements.toml` as follows:

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

Or set `requirements_toml_base64` MDM as explained on
https://developers.openai.com/codex/security/#locations.

### Why
- Enforce admin/MDM/`requirements.toml` constraints on web-search
behavior, independent of user config and per-turn sandbox defaults.
- Ensure per-turn config resolution and review-mode overrides never
crash when constraints are present.

### What
- Add `allowed_web_search_modes` to requirements parsing and surface it
in app-server v2 `ConfigRequirements` (`allowedWebSearchModes`), with
fixtures updated.
- Define a requirements allowlist type (`WebSearchModeRequirement`) and
normalize semantics:
  - `disabled` is always implicitly allowed (even if not listed).
  - An empty list is treated as `["disabled"]`.
- Make `Config.web_search_mode` a `Constrained<WebSearchMode>` and apply
requirements via `ConstrainedWithSource<WebSearchMode>`.
- Update per-turn resolution (`resolve_web_search_mode_for_turn`) to:
- Prefer `Live → Cached → Disabled` when
`SandboxPolicy::DangerFullAccess` is active (subject to requirements),
unless the user preference is explicitly `Disabled`.
- Otherwise, honor the user’s preferred mode, falling back to an allowed
mode when necessary.
- Update TUI `/debug-config` and app-server mapping to display
normalized `allowed_web_search_modes` (including implicit `disabled`).
- Fix web-search integration tests to assert cached behavior under
`SandboxPolicy::ReadOnly` (since `DangerFullAccess` legitimately prefers
`live` when allowed).
This commit is contained in:
Michael Bolin
2026-02-06 21:55:15 -08:00
committed by GitHub
parent 82c981cafc
commit a118494323
17 changed files with 618 additions and 40 deletions

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::load_requirements_toml;
use codex_protocol::config_types::TrustLevel;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::protocol::AskForApproval;
#[cfg(target_os = "macos")]
use codex_protocol::protocol::SandboxPolicy;
@@ -475,6 +476,7 @@ async fn load_requirements_toml_produces_expected_constraints() -> anyhow::Resul
&requirements_file,
r#"
allowed_approval_policies = ["never", "on-request"]
allowed_web_search_modes = ["cached"]
enforce_residency = "us"
"#,
)
@@ -490,6 +492,13 @@ enforce_residency = "us"
.cloned(),
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()?;
assert_eq!(
config_requirements.approval_policy.value(),
@@ -504,6 +513,25 @@ enforce_residency = "us"
.can_set(&AskForApproval::OnFailure)
.is_err()
);
assert_eq!(
config_requirements.web_search_mode.value(),
WebSearchMode::Cached
);
config_requirements
.web_search_mode
.can_set(&WebSearchMode::Cached)?;
config_requirements
.web_search_mode
.can_set(&WebSearchMode::Cached)?;
config_requirements
.web_search_mode
.can_set(&WebSearchMode::Disabled)?;
assert!(
config_requirements
.web_search_mode
.can_set(&WebSearchMode::Live)
.is_err()
);
assert_eq!(
config_requirements.enforce_residency.value(),
Some(crate::config_loader::ResidencyRequirement::Us)
@@ -536,6 +564,7 @@ allowed_approval_policies = ["on-request"]
Some(ConfigRequirementsToml {
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_sandbox_modes: None,
allowed_web_search_modes: None,
mcp_servers: None,
rules: None,
enforce_residency: None,
@@ -582,6 +611,7 @@ allowed_approval_policies = ["on-request"]
ConfigRequirementsToml {
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_sandbox_modes: None,
allowed_web_search_modes: None,
mcp_servers: None,
rules: None,
enforce_residency: None,
@@ -617,6 +647,7 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()>
let requirements = ConfigRequirementsToml {
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_sandbox_modes: None,
allowed_web_search_modes: None,
mcp_servers: None,
rules: None,
enforce_residency: None,