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:
parent
82c981cafc
commit
a118494323
17 changed files with 618 additions and 40 deletions
|
|
@ -11001,6 +11001,15 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"allowedWebSearchModes": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/WebSearchMode"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"enforceResidency": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -30,6 +30,15 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"allowedWebSearchModes": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/WebSearchMode"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"enforceResidency": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
|
@ -56,6 +65,14 @@
|
|||
"danger-full-access"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"WebSearchMode": {
|
||||
"enum": [
|
||||
"disabled",
|
||||
"cached",
|
||||
"live"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
// 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.
|
||||
import type { WebSearchMode } from "../WebSearchMode";
|
||||
import type { AskForApproval } from "./AskForApproval";
|
||||
import type { ResidencyRequirement } from "./ResidencyRequirement";
|
||||
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, };
|
||||
|
|
|
|||
|
|
@ -533,6 +533,7 @@ pub struct ConfigReadResponse {
|
|||
pub struct ConfigRequirements {
|
||||
pub allowed_approval_policies: Option<Vec<AskForApproval>>,
|
||||
pub allowed_sandbox_modes: Option<Vec<SandboxMode>>,
|
||||
pub allowed_web_search_modes: Option<Vec<WebSearchMode>>,
|
||||
pub enforce_residency: Option<ResidencyRequirement>,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ Example (from OpenAI's official VSCode extension):
|
|||
- `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/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
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ use codex_core::config_loader::ConfigRequirementsToml;
|
|||
use codex_core::config_loader::LoaderOverrides;
|
||||
use codex_core::config_loader::ResidencyRequirement as CoreResidencyRequirement;
|
||||
use codex_core::config_loader::SandboxModeRequirement as CoreSandboxModeRequirement;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -115,6 +116,16 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR
|
|||
.filter_map(map_sandbox_mode_requirement_to_api)
|
||||
.collect()
|
||||
}),
|
||||
allowed_web_search_modes: requirements.allowed_web_search_modes.map(|modes| {
|
||||
let mut normalized = modes
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<WebSearchMode>>();
|
||||
if !normalized.contains(&WebSearchMode::Disabled) {
|
||||
normalized.push(WebSearchMode::Disabled);
|
||||
}
|
||||
normalized
|
||||
}),
|
||||
enforce_residency: requirements
|
||||
.enforce_residency
|
||||
.map(map_residency_requirement_to_api),
|
||||
|
|
@ -177,6 +188,9 @@ mod tests {
|
|||
CoreSandboxModeRequirement::ReadOnly,
|
||||
CoreSandboxModeRequirement::ExternalSandbox,
|
||||
]),
|
||||
allowed_web_search_modes: Some(vec![
|
||||
codex_core::config_loader::WebSearchModeRequirement::Cached,
|
||||
]),
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: Some(CoreResidencyRequirement::Us),
|
||||
|
|
@ -195,9 +209,32 @@ mod tests {
|
|||
mapped.allowed_sandbox_modes,
|
||||
Some(vec![SandboxMode::ReadOnly]),
|
||||
);
|
||||
assert_eq!(
|
||||
mapped.allowed_web_search_modes,
|
||||
Some(vec![WebSearchMode::Cached, WebSearchMode::Disabled]),
|
||||
);
|
||||
assert_eq!(
|
||||
mapped.enforce_residency,
|
||||
Some(codex_app_server_protocol::ResidencyRequirement::Us),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_requirements_toml_to_api_normalizes_allowed_web_search_modes() {
|
||||
let requirements = ConfigRequirementsToml {
|
||||
allowed_approval_policies: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: Some(Vec::new()),
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
};
|
||||
|
||||
let mapped = map_requirements_toml_to_api(requirements);
|
||||
|
||||
assert_eq!(
|
||||
mapped.allowed_web_search_modes,
|
||||
Some(vec![WebSearchMode::Disabled])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -381,6 +381,7 @@ mod tests {
|
|||
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,
|
||||
|
|
@ -421,6 +422,7 @@ mod tests {
|
|||
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,
|
||||
|
|
@ -464,6 +466,7 @@ mod tests {
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -733,10 +733,22 @@ impl Session {
|
|||
session_configuration.collaboration_mode.reasoning_effort();
|
||||
per_turn_config.model_reasoning_summary = session_configuration.model_reasoning_summary;
|
||||
per_turn_config.personality = session_configuration.personality;
|
||||
per_turn_config.web_search_mode = Some(resolve_web_search_mode_for_turn(
|
||||
per_turn_config.web_search_mode,
|
||||
let resolved_web_search_mode = resolve_web_search_mode_for_turn(
|
||||
&per_turn_config.web_search_mode,
|
||||
session_configuration.sandbox_policy.get(),
|
||||
));
|
||||
);
|
||||
if let Err(err) = per_turn_config
|
||||
.web_search_mode
|
||||
.set(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
|
||||
}
|
||||
|
|
@ -794,7 +806,7 @@ impl Session {
|
|||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
features: &per_turn_config.features,
|
||||
web_search_mode: per_turn_config.web_search_mode,
|
||||
web_search_mode: Some(per_turn_config.web_search_mode.value()),
|
||||
});
|
||||
|
||||
let cwd = session_configuration.cwd.clone();
|
||||
|
|
@ -3524,7 +3536,15 @@ async fn spawn_review_thread(
|
|||
let mut per_turn_config = (*config).clone();
|
||||
per_turn_config.model = Some(model.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(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
|
||||
.otel_manager
|
||||
|
|
|
|||
|
|
@ -329,7 +329,7 @@ pub struct Config {
|
|||
pub include_apply_patch_tool: bool,
|
||||
|
||||
/// Explicit or feature-derived web search mode.
|
||||
pub web_search_mode: Option<WebSearchMode>,
|
||||
pub web_search_mode: Constrained<WebSearchMode>,
|
||||
|
||||
/// If set to `true`, used only the experimental unified exec tool.
|
||||
pub use_experimental_unified_exec_tool: bool,
|
||||
|
|
@ -1331,17 +1331,39 @@ fn resolve_web_search_mode(
|
|||
}
|
||||
|
||||
pub(crate) fn resolve_web_search_mode_for_turn(
|
||||
explicit_mode: Option<WebSearchMode>,
|
||||
web_search_mode: &Constrained<WebSearchMode>,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
) -> WebSearchMode {
|
||||
if let Some(mode) = explicit_mode {
|
||||
return mode;
|
||||
}
|
||||
if matches!(sandbox_policy, SandboxPolicy::DangerFullAccess) {
|
||||
WebSearchMode::Live
|
||||
let preferred = web_search_mode.value();
|
||||
|
||||
if matches!(sandbox_policy, SandboxPolicy::DangerFullAccess)
|
||||
&& preferred != WebSearchMode::Disabled
|
||||
{
|
||||
for mode in [
|
||||
WebSearchMode::Live,
|
||||
WebSearchMode::Cached,
|
||||
WebSearchMode::Disabled,
|
||||
] {
|
||||
if web_search_mode.can_set(&mode).is_ok() {
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
WebSearchMode::Cached
|
||||
if web_search_mode.can_set(&preferred).is_ok() {
|
||||
return preferred;
|
||||
}
|
||||
for mode in [
|
||||
WebSearchMode::Cached,
|
||||
WebSearchMode::Live,
|
||||
WebSearchMode::Disabled,
|
||||
] {
|
||||
if web_search_mode.can_set(&mode).is_ok() {
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WebSearchMode::Disabled
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
|
@ -1482,7 +1504,8 @@ impl Config {
|
|||
);
|
||||
approval_policy = requirements.approval_policy.value();
|
||||
}
|
||||
let web_search_mode = resolve_web_search_mode(&cfg, &config_profile, &features);
|
||||
let web_search_mode = resolve_web_search_mode(&cfg, &config_profile, &features)
|
||||
.unwrap_or(WebSearchMode::Cached);
|
||||
// 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 =
|
||||
|
|
@ -1626,6 +1649,7 @@ impl Config {
|
|||
let ConfigRequirements {
|
||||
approval_policy: mut constrained_approval_policy,
|
||||
sandbox_policy: mut constrained_sandbox_policy,
|
||||
web_search_mode: mut constrained_web_search_mode,
|
||||
mcp_servers,
|
||||
exec_policy: _,
|
||||
enforce_residency,
|
||||
|
|
@ -1643,6 +1667,12 @@ impl Config {
|
|||
&mut constrained_sandbox_policy,
|
||||
&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())
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{e}")))?;
|
||||
|
|
@ -1722,7 +1752,7 @@ impl Config {
|
|||
forced_chatgpt_workspace_id,
|
||||
forced_login_method,
|
||||
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,
|
||||
ghost_snapshot,
|
||||
features,
|
||||
|
|
@ -2462,27 +2492,51 @@ trust_level = "trusted"
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn web_search_mode_for_turn_defaults_to_cached_when_unset() {
|
||||
let mode = resolve_web_search_mode_for_turn(None, &SandboxPolicy::ReadOnly);
|
||||
fn web_search_mode_for_turn_uses_preference_for_read_only() {
|
||||
let web_search_mode = Constrained::allow_any(WebSearchMode::Cached);
|
||||
let mode = resolve_web_search_mode_for_turn(&web_search_mode, &SandboxPolicy::ReadOnly);
|
||||
|
||||
assert_eq!(mode, WebSearchMode::Cached);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_search_mode_for_turn_defaults_to_live_for_danger_full_access() {
|
||||
let mode = resolve_web_search_mode_for_turn(None, &SandboxPolicy::DangerFullAccess);
|
||||
fn web_search_mode_for_turn_prefers_live_for_danger_full_access() {
|
||||
let web_search_mode = Constrained::allow_any(WebSearchMode::Cached);
|
||||
let mode =
|
||||
resolve_web_search_mode_for_turn(&web_search_mode, &SandboxPolicy::DangerFullAccess);
|
||||
|
||||
assert_eq!(mode, WebSearchMode::Live);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_search_mode_for_turn_prefers_explicit_value() {
|
||||
let mode = resolve_web_search_mode_for_turn(
|
||||
Some(WebSearchMode::Cached),
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
);
|
||||
fn web_search_mode_for_turn_respects_disabled_for_danger_full_access() {
|
||||
let web_search_mode = Constrained::allow_any(WebSearchMode::Disabled);
|
||||
let mode =
|
||||
resolve_web_search_mode_for_turn(&web_search_mode, &SandboxPolicy::DangerFullAccess);
|
||||
|
||||
assert_eq!(mode, WebSearchMode::Disabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_search_mode_for_turn_falls_back_when_live_is_disallowed() -> anyhow::Result<()> {
|
||||
let allowed = [WebSearchMode::Disabled, WebSearchMode::Cached];
|
||||
let web_search_mode = Constrained::new(WebSearchMode::Cached, move |candidate| {
|
||||
if allowed.contains(candidate) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "web_search_mode",
|
||||
candidate: format!("{candidate:?}"),
|
||||
allowed: format!("{allowed:?}"),
|
||||
requirement_source: RequirementSource::Unknown,
|
||||
})
|
||||
}
|
||||
})?;
|
||||
let mode =
|
||||
resolve_web_search_mode_for_turn(&web_search_mode, &SandboxPolicy::DangerFullAccess);
|
||||
|
||||
assert_eq!(mode, WebSearchMode::Cached);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -3985,7 +4039,7 @@ model_verbosity = "high"
|
|||
forced_chatgpt_workspace_id: None,
|
||||
forced_login_method: None,
|
||||
include_apply_patch_tool: false,
|
||||
web_search_mode: None,
|
||||
web_search_mode: Constrained::allow_any(WebSearchMode::Cached),
|
||||
use_experimental_unified_exec_tool: !cfg!(windows),
|
||||
ghost_snapshot: GhostSnapshotConfig::default(),
|
||||
features: Features::with_defaults(),
|
||||
|
|
@ -4073,7 +4127,7 @@ model_verbosity = "high"
|
|||
forced_chatgpt_workspace_id: None,
|
||||
forced_login_method: None,
|
||||
include_apply_patch_tool: false,
|
||||
web_search_mode: None,
|
||||
web_search_mode: Constrained::allow_any(WebSearchMode::Cached),
|
||||
use_experimental_unified_exec_tool: !cfg!(windows),
|
||||
ghost_snapshot: GhostSnapshotConfig::default(),
|
||||
features: Features::with_defaults(),
|
||||
|
|
@ -4176,7 +4230,7 @@ model_verbosity = "high"
|
|||
forced_chatgpt_workspace_id: None,
|
||||
forced_login_method: None,
|
||||
include_apply_patch_tool: false,
|
||||
web_search_mode: None,
|
||||
web_search_mode: Constrained::allow_any(WebSearchMode::Cached),
|
||||
use_experimental_unified_exec_tool: !cfg!(windows),
|
||||
ghost_snapshot: GhostSnapshotConfig::default(),
|
||||
features: Features::with_defaults(),
|
||||
|
|
@ -4265,7 +4319,7 @@ model_verbosity = "high"
|
|||
forced_chatgpt_workspace_id: None,
|
||||
forced_login_method: None,
|
||||
include_apply_patch_tool: false,
|
||||
web_search_mode: None,
|
||||
web_search_mode: Constrained::allow_any(WebSearchMode::Cached),
|
||||
use_experimental_unified_exec_tool: !cfg!(windows),
|
||||
ghost_snapshot: GhostSnapshotConfig::default(),
|
||||
features: Features::with_defaults(),
|
||||
|
|
@ -4311,6 +4365,72 @@ model_verbosity = "high"
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset() -> anyhow::Result<()>
|
||||
{
|
||||
let fixture = create_test_fixture()?;
|
||||
|
||||
let requirements_toml = crate::config_loader::ConfigRequirementsToml {
|
||||
allowed_approval_policies: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: Some(vec![
|
||||
crate::config_loader::WebSearchModeRequirement::Cached,
|
||||
]),
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
};
|
||||
let requirement_source = crate::config_loader::RequirementSource::Unknown;
|
||||
let requirement_source_for_error = requirement_source.clone();
|
||||
let allowed = vec![WebSearchMode::Disabled, WebSearchMode::Cached];
|
||||
let constrained = Constrained::new(WebSearchMode::Cached, move |candidate| {
|
||||
if matches!(candidate, WebSearchMode::Cached | WebSearchMode::Disabled) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "web_search_mode",
|
||||
candidate: format!("{candidate:?}"),
|
||||
allowed: format!("{allowed:?}"),
|
||||
requirement_source: requirement_source_for_error.clone(),
|
||||
})
|
||||
}
|
||||
})?;
|
||||
let requirements = crate::config_loader::ConfigRequirements {
|
||||
web_search_mode: crate::config_loader::ConstrainedWithSource::new(
|
||||
constrained,
|
||||
Some(requirement_source),
|
||||
),
|
||||
..Default::default()
|
||||
};
|
||||
let config_layer_stack = crate::config_loader::ConfigLayerStack::new(
|
||||
Vec::new(),
|
||||
requirements,
|
||||
requirements_toml,
|
||||
)
|
||||
.expect("config layer stack");
|
||||
|
||||
let config = Config::load_config_with_layer_stack(
|
||||
fixture.cfg.clone(),
|
||||
ConfigOverrides {
|
||||
cwd: Some(fixture.cwd()),
|
||||
..Default::default()
|
||||
},
|
||||
fixture.codex_home(),
|
||||
config_layer_stack,
|
||||
)?;
|
||||
|
||||
assert!(
|
||||
!config
|
||||
.startup_warnings
|
||||
.iter()
|
||||
.any(|warning| warning.contains("Configured value for `web_search_mode`")),
|
||||
"{:?}",
|
||||
config.startup_warnings
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_project_trusted_writes_explicit_tables() -> anyhow::Result<()> {
|
||||
let project_dir = Path::new("/some/path");
|
||||
|
|
@ -4812,6 +4932,7 @@ mcp_oauth_callback_port = 5678
|
|||
allowed_sandbox_modes: Some(vec![
|
||||
crate::config_loader::SandboxModeRequirement::ReadOnly,
|
||||
]),
|
||||
allowed_web_search_modes: None,
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
|
|
@ -4829,6 +4950,38 @@ mcp_oauth_callback_port = 5678
|
|||
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(), WebSearchMode::Cached);
|
||||
assert_eq!(
|
||||
resolve_web_search_mode_for_turn(&config.web_search_mode, config.sandbox_policy.get()),
|
||||
WebSearchMode::Cached,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn requirements_disallowing_default_approval_falls_back_to_required_default()
|
||||
-> std::io::Result<()> {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use codex_protocol::config_types::SandboxMode;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
|
@ -76,6 +77,7 @@ impl<T> std::ops::DerefMut for ConstrainedWithSource<T> {
|
|||
pub struct ConfigRequirements {
|
||||
pub approval_policy: ConstrainedWithSource<AskForApproval>,
|
||||
pub sandbox_policy: ConstrainedWithSource<SandboxPolicy>,
|
||||
pub web_search_mode: ConstrainedWithSource<WebSearchMode>,
|
||||
pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>,
|
||||
pub(crate) exec_policy: Option<Sourced<RequirementsExecPolicy>>,
|
||||
pub enforce_residency: ConstrainedWithSource<Option<ResidencyRequirement>>,
|
||||
|
|
@ -92,6 +94,10 @@ impl Default for ConfigRequirements {
|
|||
Constrained::allow_any(SandboxPolicy::ReadOnly),
|
||||
None,
|
||||
),
|
||||
web_search_mode: ConstrainedWithSource::new(
|
||||
Constrained::allow_any(WebSearchMode::Cached),
|
||||
None,
|
||||
),
|
||||
mcp_servers: None,
|
||||
exec_policy: None,
|
||||
enforce_residency: ConstrainedWithSource::new(Constrained::allow_any(None), None),
|
||||
|
|
@ -117,11 +123,50 @@ pub struct McpServerRequirement {
|
|||
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.
|
||||
#[derive(Deserialize, Debug, Clone, Default, PartialEq)]
|
||||
pub struct ConfigRequirementsToml {
|
||||
pub allowed_approval_policies: Option<Vec<AskForApproval>>,
|
||||
pub allowed_sandbox_modes: Option<Vec<SandboxModeRequirement>>,
|
||||
pub allowed_web_search_modes: Option<Vec<WebSearchModeRequirement>>,
|
||||
pub mcp_servers: Option<BTreeMap<String, McpServerRequirement>>,
|
||||
pub rules: Option<RequirementsExecPolicyToml>,
|
||||
pub enforce_residency: Option<ResidencyRequirement>,
|
||||
|
|
@ -153,6 +198,7 @@ impl<T> std::ops::Deref for Sourced<T> {
|
|||
pub struct ConfigRequirementsWithSources {
|
||||
pub allowed_approval_policies: Option<Sourced<Vec<AskForApproval>>>,
|
||||
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 rules: Option<Sourced<RequirementsExecPolicyToml>>,
|
||||
pub enforce_residency: Option<Sourced<ResidencyRequirement>>,
|
||||
|
|
@ -186,6 +232,7 @@ impl ConfigRequirementsWithSources {
|
|||
{
|
||||
allowed_approval_policies,
|
||||
allowed_sandbox_modes,
|
||||
allowed_web_search_modes,
|
||||
mcp_servers,
|
||||
rules,
|
||||
enforce_residency,
|
||||
|
|
@ -197,6 +244,7 @@ impl ConfigRequirementsWithSources {
|
|||
let ConfigRequirementsWithSources {
|
||||
allowed_approval_policies,
|
||||
allowed_sandbox_modes,
|
||||
allowed_web_search_modes,
|
||||
mcp_servers,
|
||||
rules,
|
||||
enforce_residency,
|
||||
|
|
@ -204,6 +252,7 @@ impl ConfigRequirementsWithSources {
|
|||
ConfigRequirementsToml {
|
||||
allowed_approval_policies: allowed_approval_policies.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),
|
||||
rules: rules.map(|sourced| sourced.value),
|
||||
enforce_residency: enforce_residency.map(|sourced| sourced.value),
|
||||
|
|
@ -248,6 +297,7 @@ impl ConfigRequirementsToml {
|
|||
pub fn is_empty(&self) -> bool {
|
||||
self.allowed_approval_policies.is_none()
|
||||
&& self.allowed_sandbox_modes.is_none()
|
||||
&& self.allowed_web_search_modes.is_none()
|
||||
&& self.mcp_servers.is_none()
|
||||
&& self.rules.is_none()
|
||||
&& self.enforce_residency.is_none()
|
||||
|
|
@ -261,6 +311,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
|||
let ConfigRequirementsWithSources {
|
||||
allowed_approval_policies,
|
||||
allowed_sandbox_modes,
|
||||
allowed_web_search_modes,
|
||||
mcp_servers,
|
||||
rules,
|
||||
enforce_residency,
|
||||
|
|
@ -356,6 +407,46 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
|||
}
|
||||
None => None,
|
||||
};
|
||||
let web_search_mode = match allowed_web_search_modes {
|
||||
Some(Sourced {
|
||||
value: modes,
|
||||
source: requirement_source,
|
||||
}) => {
|
||||
let mut accepted = modes.into_iter().collect::<std::collections::BTreeSet<_>>();
|
||||
accepted.insert(WebSearchModeRequirement::Disabled);
|
||||
let allowed_for_error = format!(
|
||||
"{:?}",
|
||||
accepted
|
||||
.iter()
|
||||
.copied()
|
||||
.map(WebSearchMode::from)
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
let initial_value = if accepted.contains(&WebSearchModeRequirement::Cached) {
|
||||
WebSearchMode::Cached
|
||||
} else if accepted.contains(&WebSearchModeRequirement::Live) {
|
||||
WebSearchMode::Live
|
||||
} else {
|
||||
WebSearchMode::Disabled
|
||||
};
|
||||
let requirement_source_for_error = requirement_source.clone();
|
||||
let constrained = Constrained::new(initial_value, move |candidate| {
|
||||
if accepted.contains(&(*candidate).into()) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "web_search_mode",
|
||||
candidate: format!("{candidate:?}"),
|
||||
allowed: allowed_for_error.clone(),
|
||||
requirement_source: requirement_source_for_error.clone(),
|
||||
})
|
||||
}
|
||||
})?;
|
||||
ConstrainedWithSource::new(constrained, Some(requirement_source))
|
||||
}
|
||||
None => ConstrainedWithSource::new(Constrained::allow_any(WebSearchMode::Cached), None),
|
||||
};
|
||||
|
||||
let enforce_residency = match enforce_residency {
|
||||
Some(Sourced {
|
||||
|
|
@ -383,6 +474,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
|||
Ok(ConfigRequirements {
|
||||
approval_policy,
|
||||
sandbox_policy,
|
||||
web_search_mode,
|
||||
mcp_servers,
|
||||
exec_policy,
|
||||
enforce_residency,
|
||||
|
|
@ -410,6 +502,7 @@ mod tests {
|
|||
let ConfigRequirementsToml {
|
||||
allowed_approval_policies,
|
||||
allowed_sandbox_modes,
|
||||
allowed_web_search_modes,
|
||||
mcp_servers,
|
||||
rules,
|
||||
enforce_residency,
|
||||
|
|
@ -419,6 +512,8 @@ mod tests {
|
|||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
allowed_sandbox_modes: allowed_sandbox_modes
|
||||
.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)),
|
||||
rules: rules.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
enforce_residency: enforce_residency
|
||||
|
|
@ -436,6 +531,10 @@ mod tests {
|
|||
SandboxModeRequirement::WorkspaceWrite,
|
||||
SandboxModeRequirement::DangerFullAccess,
|
||||
];
|
||||
let allowed_web_search_modes = vec![
|
||||
WebSearchModeRequirement::Cached,
|
||||
WebSearchModeRequirement::Live,
|
||||
];
|
||||
let enforce_residency = ResidencyRequirement::Us;
|
||||
let enforce_source = source.clone();
|
||||
|
||||
|
|
@ -444,6 +543,7 @@ mod tests {
|
|||
let other = ConfigRequirementsToml {
|
||||
allowed_approval_policies: Some(allowed_approval_policies.clone()),
|
||||
allowed_sandbox_modes: Some(allowed_sandbox_modes.clone()),
|
||||
allowed_web_search_modes: Some(allowed_web_search_modes.clone()),
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: Some(enforce_residency),
|
||||
|
|
@ -459,6 +559,10 @@ mod tests {
|
|||
source.clone()
|
||||
)),
|
||||
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,
|
||||
rules: None,
|
||||
enforce_residency: Some(Sourced::new(enforce_residency, enforce_source)),
|
||||
|
|
@ -489,6 +593,7 @@ mod tests {
|
|||
source_location,
|
||||
)),
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: None,
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
|
|
@ -527,6 +632,7 @@ mod tests {
|
|||
existing_source,
|
||||
)),
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: None,
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
|
|
@ -615,6 +721,7 @@ mod tests {
|
|||
r#"
|
||||
allowed_approval_policies = ["on-request"]
|
||||
allowed_sandbox_modes = ["read-only"]
|
||||
allowed_web_search_modes = ["cached"]
|
||||
enforce_residency = "us"
|
||||
"#,
|
||||
)?;
|
||||
|
|
@ -632,6 +739,10 @@ mod tests {
|
|||
requirements.sandbox_policy.source,
|
||||
Some(source_location.clone())
|
||||
);
|
||||
assert_eq!(
|
||||
requirements.web_search_mode.source,
|
||||
Some(source_location.clone())
|
||||
);
|
||||
assert_eq!(requirements.enforce_residency.source, Some(source_location));
|
||||
|
||||
Ok(())
|
||||
|
|
@ -746,6 +857,100 @@ mod tests {
|
|||
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(), WebSearchMode::Cached);
|
||||
assert!(
|
||||
requirements
|
||||
.web_search_mode
|
||||
.can_set(&WebSearchMode::Disabled)
|
||||
.is_ok()
|
||||
);
|
||||
assert_eq!(
|
||||
requirements.web_search_mode.can_set(&WebSearchMode::Live),
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "web_search_mode",
|
||||
candidate: "Live".into(),
|
||||
allowed: "[Disabled, Cached]".into(),
|
||||
requirement_source: RequirementSource::Unknown,
|
||||
})
|
||||
);
|
||||
assert!(
|
||||
requirements
|
||||
.web_search_mode
|
||||
.can_set(&WebSearchMode::Cached)
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allowed_web_search_modes_allows_disabled() -> 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(),
|
||||
WebSearchMode::Disabled
|
||||
);
|
||||
assert!(
|
||||
requirements
|
||||
.web_search_mode
|
||||
.can_set(&WebSearchMode::Disabled)
|
||||
.is_ok()
|
||||
);
|
||||
assert_eq!(
|
||||
requirements.web_search_mode.can_set(&WebSearchMode::Cached),
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "web_search_mode",
|
||||
candidate: "Cached".into(),
|
||||
allowed: "[Disabled]".into(),
|
||||
requirement_source: RequirementSource::Unknown,
|
||||
})
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allowed_web_search_modes_empty_restricts_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(),
|
||||
WebSearchMode::Disabled
|
||||
);
|
||||
assert!(
|
||||
requirements
|
||||
.web_search_mode
|
||||
.can_set(&WebSearchMode::Disabled)
|
||||
.is_ok()
|
||||
);
|
||||
assert_eq!(
|
||||
requirements.web_search_mode.can_set(&WebSearchMode::Cached),
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "web_search_mode",
|
||||
candidate: "Cached".into(),
|
||||
allowed: "[Disabled]".into(),
|
||||
requirement_source: RequirementSource::Unknown,
|
||||
})
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_mcp_server_requirements() -> Result<()> {
|
||||
let toml_str = r#"
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ pub use config_requirements::RequirementSource;
|
|||
pub use config_requirements::ResidencyRequirement;
|
||||
pub use config_requirements::SandboxModeRequirement;
|
||||
pub use config_requirements::Sourced;
|
||||
pub use config_requirements::WebSearchModeRequirement;
|
||||
pub use diagnostics::ConfigError;
|
||||
pub use diagnostics::ConfigLoadError;
|
||||
pub use diagnostics::TextPosition;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ use tokio_util::sync::CancellationToken;
|
|||
use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::codex_delegate::run_codex_thread_one_shot;
|
||||
use crate::config::Constrained;
|
||||
use crate::review_format::format_review_findings_block;
|
||||
use crate::review_format::render_review_output_text;
|
||||
use crate::state::TaskKind;
|
||||
|
|
@ -86,7 +87,20 @@ async fn start_review_conversation(
|
|||
let mut sub_agent_config = config.as_ref().clone();
|
||||
// Carry over review-only feature restrictions so the delegate cannot
|
||||
// re-enable blocked tools (web search, view image).
|
||||
sub_agent_config.web_search_mode = Some(WebSearchMode::Disabled);
|
||||
if let Err(err) = sub_agent_config
|
||||
.web_search_mode
|
||||
.set(WebSearchMode::Disabled)
|
||||
{
|
||||
tracing::warn!(
|
||||
"failed to force review web_search_mode=disabled; falling back to a normalizer: {err}"
|
||||
);
|
||||
sub_agent_config.web_search_mode =
|
||||
Constrained::normalized(WebSearchMode::Disabled, |_| WebSearchMode::Disabled)
|
||||
.unwrap_or_else(|err| {
|
||||
tracing::warn!("failed to build normalizer for review web_search_mode: {err}");
|
||||
Constrained::allow_any(WebSearchMode::Disabled)
|
||||
});
|
||||
}
|
||||
|
||||
// Set explicit review rubric for the sub-agent
|
||||
sub_agent_config.base_instructions = Some(crate::REVIEW_PROMPT.to_string());
|
||||
|
|
|
|||
|
|
@ -36,7 +36,10 @@ async fn collect_tool_identifiers_for_model(model: &str) -> Vec<String> {
|
|||
.with_model(model)
|
||||
// Keep tool expectations stable when the default web_search mode changes.
|
||||
.with_config(|config| {
|
||||
config.web_search_mode = Some(WebSearchMode::Cached);
|
||||
config
|
||||
.web_search_mode
|
||||
.set(WebSearchMode::Cached)
|
||||
.expect("test web_search_mode should satisfy constraints");
|
||||
config.features.enable(Feature::CollaborationModes);
|
||||
});
|
||||
let test = builder
|
||||
|
|
|
|||
|
|
@ -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.model = Some("gpt-5.1-codex-max".to_string());
|
||||
// Keep tool expectations stable when the default web_search mode changes.
|
||||
config.web_search_mode = Some(WebSearchMode::Cached);
|
||||
config
|
||||
.web_search_mode
|
||||
.set(WebSearchMode::Cached)
|
||||
.expect("test web_search_mode should satisfy constraints");
|
||||
config.features.enable(Feature::CollaborationModes);
|
||||
})
|
||||
.build(&server)
|
||||
|
|
|
|||
|
|
@ -34,14 +34,17 @@ async fn web_search_mode_cached_sets_external_web_access_false() {
|
|||
let mut builder = test_codex()
|
||||
.with_model("gpt-5-codex")
|
||||
.with_config(|config| {
|
||||
config.web_search_mode = Some(WebSearchMode::Cached);
|
||||
config
|
||||
.web_search_mode
|
||||
.set(WebSearchMode::Cached)
|
||||
.expect("test web_search_mode should satisfy constraints");
|
||||
});
|
||||
let test = builder
|
||||
.build(&server)
|
||||
.await
|
||||
.expect("create test Codex conversation");
|
||||
|
||||
test.submit_turn("hello cached web search")
|
||||
test.submit_turn_with_policy("hello cached web search", SandboxPolicy::ReadOnly)
|
||||
.await
|
||||
.expect("submit turn");
|
||||
|
||||
|
|
@ -69,14 +72,17 @@ async fn web_search_mode_takes_precedence_over_legacy_flags() {
|
|||
.with_model("gpt-5-codex")
|
||||
.with_config(|config| {
|
||||
config.features.enable(Feature::WebSearchRequest);
|
||||
config.web_search_mode = Some(WebSearchMode::Cached);
|
||||
config
|
||||
.web_search_mode
|
||||
.set(WebSearchMode::Cached)
|
||||
.expect("test web_search_mode should satisfy constraints");
|
||||
});
|
||||
let test = builder
|
||||
.build(&server)
|
||||
.await
|
||||
.expect("create test Codex conversation");
|
||||
|
||||
test.submit_turn("hello cached+live flags")
|
||||
test.submit_turn_with_policy("hello cached+live flags", SandboxPolicy::ReadOnly)
|
||||
.await
|
||||
.expect("submit turn");
|
||||
|
||||
|
|
@ -90,7 +96,7 @@ async fn web_search_mode_takes_precedence_over_legacy_flags() {
|
|||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn web_search_mode_defaults_to_cached_when_unset() {
|
||||
async fn web_search_mode_defaults_to_cached_when_features_disabled() {
|
||||
skip_if_no_network!();
|
||||
|
||||
let server = start_mock_server().await;
|
||||
|
|
@ -103,7 +109,10 @@ async fn web_search_mode_defaults_to_cached_when_unset() {
|
|||
let mut builder = test_codex()
|
||||
.with_model("gpt-5-codex")
|
||||
.with_config(|config| {
|
||||
config.web_search_mode = None;
|
||||
config
|
||||
.web_search_mode
|
||||
.set(WebSearchMode::Cached)
|
||||
.expect("test web_search_mode should satisfy constraints");
|
||||
config.features.disable(Feature::WebSearchCached);
|
||||
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()
|
||||
.with_model("gpt-5-codex")
|
||||
.with_config(|config| {
|
||||
config.web_search_mode = None;
|
||||
config
|
||||
.web_search_mode
|
||||
.set(WebSearchMode::Cached)
|
||||
.expect("test web_search_mode should satisfy constraints");
|
||||
config.features.disable(Feature::WebSearchCached);
|
||||
config.features.disable(Feature::WebSearchRequest);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use codex_core::config_loader::ConfigLayerStackOrdering;
|
|||
use codex_core::config_loader::RequirementSource;
|
||||
use codex_core::config_loader::ResidencyRequirement;
|
||||
use codex_core::config_loader::SandboxModeRequirement;
|
||||
use codex_core::config_loader::WebSearchModeRequirement;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
|
||||
|
|
@ -70,6 +71,21 @@ fn render_debug_config_lines(stack: &ConfigLayerStack) -> Vec<Line<'static>> {
|
|||
));
|
||||
}
|
||||
|
||||
if let Some(modes) = requirements_toml.allowed_web_search_modes.as_ref() {
|
||||
let normalized = normalize_allowed_web_search_modes(modes);
|
||||
let value = join_or_empty(
|
||||
normalized
|
||||
.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() {
|
||||
let value = join_or_empty(servers.keys().cloned().collect::<Vec<_>>());
|
||||
requirement_lines.push(requirement_line(
|
||||
|
|
@ -127,6 +143,20 @@ fn join_or_empty(values: Vec<String>) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
fn normalize_allowed_web_search_modes(
|
||||
modes: &[WebSearchModeRequirement],
|
||||
) -> Vec<WebSearchModeRequirement> {
|
||||
if modes.is_empty() {
|
||||
return vec![WebSearchModeRequirement::Disabled];
|
||||
}
|
||||
|
||||
let mut normalized = modes.to_vec();
|
||||
if !normalized.contains(&WebSearchModeRequirement::Disabled) {
|
||||
normalized.push(WebSearchModeRequirement::Disabled);
|
||||
}
|
||||
normalized
|
||||
}
|
||||
|
||||
fn format_config_layer_source(source: &ConfigLayerSource) -> String {
|
||||
match source {
|
||||
ConfigLayerSource::Mdm { domain, key } => {
|
||||
|
|
@ -185,8 +215,10 @@ mod tests {
|
|||
use codex_core::config_loader::ResidencyRequirement;
|
||||
use codex_core::config_loader::SandboxModeRequirement;
|
||||
use codex_core::config_loader::Sourced;
|
||||
use codex_core::config_loader::WebSearchModeRequirement;
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use ratatui::text::Line;
|
||||
use std::collections::BTreeMap;
|
||||
|
|
@ -287,10 +319,15 @@ mod tests {
|
|||
Constrained::allow_any(Some(ResidencyRequirement::Us)),
|
||||
Some(RequirementSource::CloudRequirements),
|
||||
);
|
||||
requirements.web_search_mode = ConstrainedWithSource::new(
|
||||
Constrained::allow_any(WebSearchMode::Cached),
|
||||
Some(RequirementSource::CloudRequirements),
|
||||
);
|
||||
|
||||
let requirements_toml = ConfigRequirementsToml {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
|
||||
allowed_sandbox_modes: Some(vec![SandboxModeRequirement::ReadOnly]),
|
||||
allowed_web_search_modes: Some(vec![WebSearchModeRequirement::Cached]),
|
||||
mcp_servers: Some(BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
McpServerRequirement {
|
||||
|
|
@ -331,8 +368,39 @@ mod tests {
|
|||
.as_str(),
|
||||
)
|
||||
);
|
||||
assert!(
|
||||
rendered.contains(
|
||||
"allowed_web_search_modes: cached, disabled (source: cloud requirements)"
|
||||
)
|
||||
);
|
||||
assert!(rendered.contains("mcp_servers: docs (source: MDM managed_config.toml (legacy))"));
|
||||
assert!(rendered.contains("enforce_residency: us (source: cloud requirements)"));
|
||||
assert!(!rendered.contains(" - rules:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_config_output_normalizes_empty_web_search_mode_list() {
|
||||
let mut requirements = ConfigRequirements::default();
|
||||
requirements.web_search_mode = ConstrainedWithSource::new(
|
||||
Constrained::allow_any(WebSearchMode::Disabled),
|
||||
Some(RequirementSource::CloudRequirements),
|
||||
);
|
||||
|
||||
let requirements_toml = ConfigRequirementsToml {
|
||||
allowed_approval_policies: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: Some(Vec::new()),
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
};
|
||||
|
||||
let stack = ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)
|
||||
.expect("config layer stack");
|
||||
|
||||
let rendered = render_to_text(&render_debug_config_lines(&stack));
|
||||
assert!(
|
||||
rendered.contains("allowed_web_search_modes: disabled (source: cloud requirements)")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue