use crate::FeatureConfig; use crate::FeatureToml; use codex_network_proxy::CredentialProviderConfig; use codex_protocol::openai_models::ReasoningEffort; use codex_utils_redacted_string::RedactedString; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use std::collections::BTreeMap; use std::num::NonZeroUsize; /// Connection to a board provisioned by the research host. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] #[serde(deny_unknown_fields)] pub struct RemoteMessageBoardConfigToml { pub url: String, /// Board credential supplied directly by a runtime config override. #[serde(skip_serializing_if = "Option::is_none")] pub bearer_token: Option, /// Read the credential from this environment variable instead of bearer_token. #[serde(skip_serializing_if = "Option::is_none")] pub bearer_token_env_var: Option, } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[serde(deny_unknown_fields)] pub struct ToolRegistryConfigToml { /// Fail the turn when multiple tools share the same effective name. #[serde(skip_serializing_if = "Option::is_none")] pub error_on_tool_collisions: Option, /// Include authoritative tool information in per-turn request metadata. #[serde(skip_serializing_if = "Option::is_none")] pub turn_metadata_includes_tool_info: Option, } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[serde(deny_unknown_fields)] pub struct CodeModeConfigToml { #[serde(skip_serializing_if = "Option::is_none")] pub enabled: Option, /// Default yield timeout for code-mode exec calls, in milliseconds. #[serde(skip_serializing_if = "Option::is_none")] pub default_exec_yield_time_ms: Option, /// Show handler duration, code-mode host duration, and harness overhead /// in each code-mode cell response. /// Experimental: this option and the response format may change or be removed. #[serde(skip_serializing_if = "Option::is_none")] pub experimental_show_cell_overhead: Option, /// Maximum UTF-8 bytes per rendered tool input type, with a 16,000-byte minimum and default. /// For ordinary MCP tools, this is also at least their server's explicitly configured input limit. #[serde(skip_serializing_if = "Option::is_none")] pub tool_input_schema_max_bytes: Option, /// Exact tool namespaces to omit from the code-mode nested tool surface. #[serde(skip_serializing_if = "Option::is_none")] pub excluded_tool_namespaces: Option>, /// Exact tool namespaces to expose only as direct model tools. /// These tools bypass deferral, remain top-level in code-mode-only sessions, and are omitted /// from the nested code-mode tool surface. #[serde(skip_serializing_if = "Option::is_none")] pub direct_only_tool_namespaces: Option>, } impl FeatureConfig for CodeModeConfigToml { fn enabled(&self) -> Option { self.enabled } } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[serde(deny_unknown_fields)] pub struct CodeModeHostConfigToml { #[serde(skip_serializing_if = "Option::is_none")] pub enabled: Option, /// Keep code mode fail-closed when the standalone host is unavailable. #[serde(skip_serializing_if = "Option::is_none")] pub disable_in_process_fallback: Option, } impl FeatureConfig for CodeModeHostConfigToml { fn enabled(&self) -> Option { self.enabled } } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[serde(deny_unknown_fields)] pub struct NonPrefixedMcpToolNamesConfigToml { #[serde(skip_serializing_if = "Option::is_none")] pub enabled: Option, /// MCP servers whose tools should omit the legacy `mcp__` namespace prefix. #[serde(skip_serializing_if = "Option::is_none")] pub server_names: Option>, } impl FeatureConfig for NonPrefixedMcpToolNamesConfigToml { fn enabled(&self) -> Option { self.enabled } } /// Optional conversation sources available to the Guardian v2 classifier. #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum GuardianV2TranscriptSource { ToolCalls, ToolOutputs, Reasoning, } /// Bounds and optional sources for the Guardian v2 conversation transcript. #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[serde(deny_unknown_fields)] pub struct GuardianV2TranscriptConfigToml { #[serde(skip_serializing_if = "Option::is_none")] pub sources: Option>, /// Include recent screenshots from messages and configured tool outputs. #[serde(skip_serializing_if = "Option::is_none")] pub include_images: Option, #[serde(skip_serializing_if = "Option::is_none")] #[schemars(range(min = 100, max = 100000))] pub max_message_entry_tokens: Option, #[serde(skip_serializing_if = "Option::is_none")] #[schemars(range(min = 100, max = 100000))] pub max_tool_entry_tokens: Option, #[serde(skip_serializing_if = "Option::is_none")] #[schemars(range(min = 100, max = 100000))] pub max_message_transcript_tokens: Option, #[serde(skip_serializing_if = "Option::is_none")] #[schemars(range(min = 100, max = 100000))] pub max_tool_transcript_tokens: Option, #[serde(skip_serializing_if = "Option::is_none")] #[schemars(range(min = 1))] pub max_recent_non_user_entries: Option, } /// Optional tool-call categories available to the Guardian v2 classifier. #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[serde(deny_unknown_fields)] pub struct GuardianV2ReviewScopeConfigToml { /// Restrict asynchronous classification and fast approvals to browser and computer-use tools. #[serde(skip_serializing_if = "Option::is_none")] pub computer_use_only: Option, /// Include sandboxed shell command calls in Guardian v2 classification. #[serde(skip_serializing_if = "Option::is_none")] pub sandboxed_exec_commands: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] #[serde(deny_unknown_fields)] pub struct GuardianElasticSectionConfigToml { pub min_total_section_tokens: usize, pub max_section_entry_tokens: usize, /// Upper bound for the section's allocation. /// Omit to use the global request budget as the upper bound. #[serde(skip_serializing_if = "Option::is_none")] pub max_total_section_tokens: Option, } /// User-configurable prompt, approval, and context settings for Guardian v2. #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)] #[serde(deny_unknown_fields)] pub struct GuardianV2ConfigToml { /// Enable elastic allocation for synchronous Guardian requests. Defaults to false. #[serde(skip_serializing_if = "Option::is_none")] pub sync_elastic_budgeting: Option, /// Named section limits, decoded and validated by Guardian when the switch is enabled. #[serde(skip_serializing_if = "Option::is_none")] pub sync_elastic_sections: Option>, /// Transcript encoding for synchronous Guardian review and asynchronous scoring. Defaults to line. #[serde(skip_serializing_if = "Option::is_none")] pub transcript_mode: Option, /// Deprecated and ignored; asynchronous classification always uses conversations. #[serde(skip_serializing_if = "Option::is_none")] pub async_classifier_mode: Option, /// Reset retained async history when the next request exceeds this token estimate. /// Defaults to 100,000. Fresh requests remain subject to the model's input limit. #[serde(skip_serializing_if = "Option::is_none")] #[schemars(range(min = 1))] pub async_classifier_conversation_token_limit: Option, #[serde(skip_serializing_if = "Option::is_none")] pub enabled: Option, /// Legacy setting retained for config compatibility; the backend now controls Guardian billing. #[serde(skip_serializing_if = "Option::is_none")] pub free_guardian: Option, /// Deprecated and ignored; thread-owned Guardian context is always enabled. #[serde(skip_serializing_if = "Option::is_none")] pub thread_context: Option, /// Persist reviewed actions and risk scores to rollout files for debugging. #[serde(skip_serializing_if = "Option::is_none")] pub persist_scores: Option, #[serde(skip_serializing_if = "Option::is_none")] pub classifier_instructions: Option, #[serde(skip_serializing_if = "Option::is_none")] #[schemars(range(min = 0.0, max = 1.0))] pub review_threshold: Option, #[serde(skip_serializing_if = "Option::is_none")] pub max_tool_call_lag: Option, #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, #[serde(skip_serializing_if = "Option::is_none")] #[schemars(range(min = 100, max = 100000))] pub max_action_tokens: Option, #[serde(skip_serializing_if = "Option::is_none")] #[schemars(range(min = 100, max = 100000))] pub max_classifier_instruction_tokens: Option, #[serde(skip_serializing_if = "Option::is_none")] pub reuse_parent_compaction: Option, #[serde(skip_serializing_if = "Option::is_none")] #[schemars(range(min = 100, max = 100000))] pub max_parent_compaction_tokens: Option, #[serde(skip_serializing_if = "Option::is_none")] pub review_scope: Option, #[serde(skip_serializing_if = "Option::is_none")] pub transcript: Option, } impl FeatureConfig for GuardianV2ConfigToml { fn enabled(&self) -> Option { self.enabled } } pub(crate) fn deserialize_guardian_v2_feature<'de, D>( deserializer: D, ) -> Result>, D::Error> where D: serde::Deserializer<'de>, { const MIN_TOKENS: usize = 100; const MAX_TOKENS: usize = 100_000; let feature = Option::>::deserialize(deserializer)?; let Some(FeatureToml::Config(config)) = feature.as_ref() else { return Ok(feature); }; if let Some(threshold) = config.review_threshold && (!threshold.is_finite() || !(0.0..=1.0).contains(&threshold)) { return Err(serde::de::Error::custom( "Guardian v2 review_threshold must be between 0.0 and 1.0", )); } let transcript = config.transcript.as_ref(); if config.async_classifier_conversation_token_limit == Some(0) { return Err(serde::de::Error::custom( "Guardian v2 async_classifier_conversation_token_limit must be positive", )); } let message_entry = transcript.and_then(|value| value.max_message_entry_tokens); let tool_entry = transcript.and_then(|value| value.max_tool_entry_tokens); let message_total = transcript.and_then(|value| value.max_message_transcript_tokens); let tool_total = transcript.and_then(|value| value.max_tool_transcript_tokens); for (field, tokens) in [ ("max_action_tokens", config.max_action_tokens), ( "max_classifier_instruction_tokens", config.max_classifier_instruction_tokens, ), ( "max_parent_compaction_tokens", config.max_parent_compaction_tokens, ), ("transcript max_message_entry_tokens", message_entry), ("transcript max_tool_entry_tokens", tool_entry), ("transcript max_message_transcript_tokens", message_total), ("transcript max_tool_transcript_tokens", tool_total), ] { if let Some(tokens) = tokens && !(MIN_TOKENS..=MAX_TOKENS).contains(&tokens) { return Err(serde::de::Error::custom(format!( "Guardian v2 {field} must be between {MIN_TOKENS} and {MAX_TOKENS} tokens" ))); } } if transcript .and_then(|value| value.max_recent_non_user_entries) .is_some_and(|count| count == 0) { return Err(serde::de::Error::custom( "Guardian v2 transcript max_recent_non_user_entries must be positive", )); } for (kind, entry, total) in [ ("message", message_entry, message_total), ("tool", tool_entry, tool_total), ] { if let (Some(entry), Some(total)) = (entry, total) && entry > total { return Err(serde::de::Error::custom(format!( "Guardian v2 transcript max_{kind}_entry_tokens must not exceed max_{kind}_transcript_tokens" ))); } } Ok(feature) } /// User-configurable settings for Guardian V3. #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[serde(deny_unknown_fields)] pub struct GuardianV3ConfigToml { #[serde(skip_serializing_if = "Option::is_none")] pub enabled: Option, } impl FeatureConfig for GuardianV3ConfigToml { fn enabled(&self) -> Option { self.enabled } } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[serde(deny_unknown_fields)] pub struct MultiAgentV2ConfigToml { #[serde(skip_serializing_if = "Option::is_none")] pub enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] #[schemars(range(min = 1))] pub max_concurrent_threads_per_session: Option, #[serde(skip_serializing_if = "Option::is_none")] #[schemars(range(min = 0, max = 3600000))] pub min_wait_timeout_ms: Option, #[serde(skip_serializing_if = "Option::is_none")] #[schemars(range(min = 0, max = 3600000))] pub max_wait_timeout_ms: Option, #[serde(skip_serializing_if = "Option::is_none")] #[schemars(range(min = 0, max = 3600000))] pub default_wait_timeout_ms: Option, /// Deprecated compatibility field. Its value is ignored. #[serde(skip_serializing_if = "Option::is_none")] pub usage_hint_enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub usage_hint_text: Option, #[serde(skip_serializing_if = "Option::is_none")] pub root_agent_usage_hint_text: Option, #[serde(skip_serializing_if = "Option::is_none")] pub subagent_usage_hint_text: Option, /// Overrides inherited developer instructions for subagents without role-specific instructions. #[serde(skip_serializing_if = "Option::is_none")] pub subagent_developer_instructions: Option, /// Preserve the parent's model-visible history on full forks and append child instructions. /// Defaults to true. Legacy compaction without a reusable context baseline still rebuilds. #[serde(skip_serializing_if = "Option::is_none")] pub preserve_fork_prefix: Option, #[serde(skip_serializing_if = "Option::is_none")] pub multi_agent_mode_hint_text: Option, #[serde(skip_serializing_if = "Option::is_none")] #[schemars(length(min = 1, max = 64), regex(pattern = r"^[a-zA-Z0-9_-]+$"))] pub tool_namespace: Option, #[serde(skip_serializing_if = "Option::is_none")] pub hide_spawn_agent_metadata: Option, /// Exposes `model` and `reasoning_effort` on the multi-agent v2 spawn tool and adds /// corresponding guidance to root and subagent usage hints. #[serde(skip_serializing_if = "Option::is_none")] pub expose_spawn_agent_model_overrides: Option, /// Expose the multi-agent v2 `wait_agent` tool. #[serde(skip_serializing_if = "Option::is_none")] pub wait_agent_enabled: Option, /// Hide native multi-agent tools while retaining the V2 runtime for host-provided tools. #[serde(skip_serializing_if = "Option::is_none")] pub disable_native_tools: Option, /// Disable the model's direct-message tools; spawning and automatic child results remain available. #[serde(skip_serializing_if = "Option::is_none")] pub disable_direct_message: Option, /// Keep the message board in memory for a training session, including ephemeral sessions. #[serde(skip_serializing_if = "Option::is_none")] pub message_board_in_memory: Option, /// Use a session-scoped remote board instead of local storage. #[serde(skip_serializing_if = "Option::is_none")] pub message_board_remote: Option, #[serde(skip_serializing_if = "Option::is_none")] pub non_code_mode_only: Option, } impl FeatureConfig for MultiAgentV2ConfigToml { fn enabled(&self) -> Option { self.enabled } } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[serde(deny_unknown_fields)] pub struct ContextManagementConfigToml { /// Enables experimental context management. #[serde(skip_serializing_if = "Option::is_none")] pub experimental_mode: Option, } impl FeatureConfig for ContextManagementConfigToml { fn enabled(&self) -> Option { self.experimental_mode } } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[serde(deny_unknown_fields)] pub struct TokenBudgetConfigToml { #[serde(skip_serializing_if = "Option::is_none")] pub enabled: Option, /// Whether to expose the built-in history and notes extension. #[serde(skip_serializing_if = "Option::is_none")] pub use_history_notes_extension: Option, /// Number of tokens remaining before auto-compaction when the wrap-up reminder is emitted. #[serde(skip_serializing_if = "Option::is_none")] #[schemars(range(min = 1))] pub reminder_threshold_tokens: Option, /// Reminder template. `{n_remaining}` is replaced with the tokens remaining before /// auto-compaction. #[serde(skip_serializing_if = "Option::is_none")] #[schemars(length(min = 1, max = 2000))] pub reminder_message_template: Option, /// Guidance appended to the context-window metadata in a developer message. #[serde(skip_serializing_if = "Option::is_none")] #[schemars(length(max = 2000))] pub guidance_message: Option, /// Developer message sampled before an automatic context-window rollover. #[serde(skip_serializing_if = "Option::is_none")] #[schemars(length(max = 2000))] pub auto_compact_fallback_prompt: Option, /// Additional tokens available after the compaction threshold for fallback note-taking. #[serde(skip_serializing_if = "Option::is_none")] #[schemars(range(min = 1))] pub auto_compact_fallback_buffer_tokens: Option, } impl FeatureConfig for TokenBudgetConfigToml { fn enabled(&self) -> Option { self.enabled } } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)] #[serde(deny_unknown_fields)] pub struct RolloutBudgetConfigToml { /// Stop when the shared budget is exhausted. Defaults to true. #[serde(skip_serializing_if = "Option::is_none")] pub enforce_limit: Option, #[serde(skip_serializing_if = "Option::is_none")] pub enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] #[schemars(range(min = 1))] pub limit_tokens: Option, #[serde(skip_serializing_if = "Option::is_none")] /// Remaining weighted-token values that trigger reminders when crossed. pub reminder_at_remaining_tokens: Option>, #[serde(skip_serializing_if = "Option::is_none")] #[schemars(range(min = 0.0))] pub sampling_token_weight: Option, #[serde(skip_serializing_if = "Option::is_none")] #[schemars(range(min = 0.0))] pub prefill_token_weight: Option, } impl FeatureConfig for RolloutBudgetConfigToml { fn enabled(&self) -> Option { self.enabled } } #[derive(Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum CurrentTimeSource { #[default] System, External, } /// Which inference boundaries may receive current-time reminders. #[derive(Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum CurrentTimeReminderDeliveryMode { /// Allow a reminder before any inference request once the interval is due. #[default] AnyInference, /// Allow reminders after user input or tool output; new context windows still force one. AfterUserOrToolOutput, } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[serde(deny_unknown_fields)] pub struct CurrentTimeReminderConfigToml { #[serde(skip_serializing_if = "Option::is_none")] pub enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub reminder_interval_seconds: Option, #[serde(skip_serializing_if = "Option::is_none")] pub clock_source: Option, #[serde(skip_serializing_if = "Option::is_none")] pub delivery_mode: Option, /// Expose the input-interruptible `clock.sleep` tool. #[serde(skip_serializing_if = "Option::is_none")] pub sleep_tool: Option, } impl FeatureConfig for CurrentTimeReminderConfigToml { fn enabled(&self) -> Option { self.enabled } } /// How the sleep tool is selected when its feature gate is enabled. #[derive(Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum SleepToolMode { /// Preserve the existing model and legacy clock configuration defaults. #[default] ModelDriven, /// Register sleep regardless of the model or legacy clock configuration. AlwaysOn, } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[serde(deny_unknown_fields)] pub struct SleepToolConfigToml { #[serde(skip_serializing_if = "Option::is_none")] pub enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub mode: Option, } impl FeatureConfig for SleepToolConfigToml { fn enabled(&self) -> Option { self.enabled } } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[serde(deny_unknown_fields)] pub(crate) struct RemovedAppsMcpPathOverrideConfigToml { #[serde(skip_serializing_if = "Option::is_none")] enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] path: Option, } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[serde(deny_unknown_fields)] pub struct NetworkProxyConfigToml { #[serde(skip_serializing_if = "Option::is_none")] pub enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub proxy_url: Option, #[serde(skip_serializing_if = "Option::is_none")] pub enable_socks5: Option, #[serde(skip_serializing_if = "Option::is_none")] pub socks_url: Option, #[serde(skip_serializing_if = "Option::is_none")] pub enable_socks5_udp: Option, #[serde(skip_serializing_if = "Option::is_none")] pub allow_upstream_proxy: Option, #[serde(skip_serializing_if = "Option::is_none")] pub dangerously_allow_non_loopback_proxy: Option, #[serde(skip_serializing_if = "Option::is_none")] pub dangerously_allow_all_unix_sockets: Option, #[serde(skip_serializing_if = "Option::is_none")] pub mode: Option, #[serde(skip_serializing_if = "Option::is_none")] pub domains: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub unix_sockets: Option>, /// Permits local servers and direct host-loopback connections and skips the proxy's /// additional private-network destination checks. Proxy domain rules still apply. /// Defaults to true for MXC, which cannot enforce false; otherwise defaults to false. #[serde(skip_serializing_if = "Option::is_none")] pub allow_local_binding: Option, #[serde(skip_serializing_if = "Option::is_none")] pub credential_broker: Option, #[serde(skip_serializing_if = "Option::is_none")] pub credentials: Option>, } impl FeatureConfig for NetworkProxyConfigToml { fn enabled(&self) -> Option { self.enabled } } #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "lowercase")] pub enum NetworkProxyModeToml { Limited, Full, } #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "lowercase")] pub enum NetworkProxyDomainPermissionToml { Allow, Deny, } #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "lowercase")] pub enum NetworkProxyUnixSocketPermissionToml { Allow, Deny, } /// Background compaction settings. A threshold alone does not enable the feature. #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[serde(deny_unknown_fields)] pub struct AsyncCompactionConfigToml { #[serde(skip_serializing_if = "Option::is_none")] pub enabled: Option, /// Percentage of the model's full context window that starts background compaction. /// Defaults to 65; valid values are 1–100, including when disabled. #[serde(skip_serializing_if = "Option::is_none")] #[schemars(range(min = 1, max = 100))] pub model_async_compact_threshold_percent: Option, } impl FeatureConfig for AsyncCompactionConfigToml { fn enabled(&self) -> Option { self.enabled } }