Skip to main content

codex_config/
config_toml.rs

1//! Schema-heavy configuration TOML types used by Codex.
2
3use std::collections::BTreeMap;
4use std::collections::HashMap;
5use std::path::Path;
6
7use crate::HooksToml;
8use crate::permissions_toml::PermissionsToml;
9use crate::profile_toml::ConfigProfile;
10use crate::types::AnalyticsConfigToml;
11use crate::types::ApprovalsReviewer;
12use crate::types::AppsConfigToml;
13use crate::types::AuthCredentialsStoreMode;
14use crate::types::FeedbackConfigToml;
15use crate::types::History;
16use crate::types::MarketplaceConfig;
17use crate::types::McpServerConfig;
18use crate::types::MemoriesToml;
19use crate::types::Notice;
20use crate::types::OAuthCredentialsStoreMode;
21use crate::types::OtelConfigToml;
22use crate::types::PluginConfig;
23use crate::types::SandboxWorkspaceWrite;
24use crate::types::ShellEnvironmentPolicyToml;
25use crate::types::SkillsConfig;
26use crate::types::ToolSuggestConfig;
27use crate::types::Tui;
28use crate::types::UriBasedFileOpener;
29use crate::types::WindowsToml;
30use codex_features::FeaturesToml;
31use codex_model_provider_info::AMAZON_BEDROCK_PROVIDER_ID;
32use codex_model_provider_info::LEGACY_OLLAMA_CHAT_PROVIDER_ID;
33use codex_model_provider_info::LMSTUDIO_OSS_PROVIDER_ID;
34use codex_model_provider_info::ModelProviderInfo;
35use codex_model_provider_info::OLLAMA_CHAT_PROVIDER_REMOVED_ERROR;
36use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID;
37use codex_model_provider_info::OPENAI_PROVIDER_ID;
38use codex_protocol::config_types::AutoCompactTokenLimitScope;
39use codex_protocol::config_types::ForcedLoginMethod;
40use codex_protocol::config_types::Personality;
41use codex_protocol::config_types::ReasoningSummary;
42use codex_protocol::config_types::SandboxMode;
43use codex_protocol::config_types::TrustLevel;
44use codex_protocol::config_types::Verbosity;
45use codex_protocol::config_types::WebSearchMode;
46use codex_protocol::config_types::WebSearchToolConfig;
47use codex_protocol::config_types::WindowsSandboxLevel;
48use codex_protocol::models::PermissionProfile;
49use codex_protocol::openai_models::ReasoningEffort;
50use codex_protocol::permissions::NetworkSandboxPolicy;
51use codex_protocol::protocol::AskForApproval;
52use codex_utils_absolute_path::AbsolutePathBuf;
53use codex_utils_path::normalize_for_path_comparison;
54use schemars::JsonSchema;
55use serde::Deserialize;
56use serde::Deserializer;
57use serde::Serialize;
58use serde::de::Error as SerdeError;
59use serde_json::Value as JsonValue;
60
61const RESERVED_MODEL_PROVIDER_IDS: [&str; 4] = [
62    AMAZON_BEDROCK_PROVIDER_ID,
63    OPENAI_PROVIDER_ID,
64    OLLAMA_OSS_PROVIDER_ID,
65    LMSTUDIO_OSS_PROVIDER_ID,
66];
67
68pub const DEFAULT_PROJECT_DOC_MAX_BYTES: usize = 32 * 1024;
69
70fn default_history() -> Option<History> {
71    Some(History::default())
72}
73
74const fn default_project_doc_max_bytes() -> Option<usize> {
75    Some(DEFAULT_PROJECT_DOC_MAX_BYTES)
76}
77
78fn default_project_doc_fallback_filenames() -> Option<Vec<String>> {
79    Some(Vec::new())
80}
81
82const fn default_hide_agent_reasoning() -> Option<bool> {
83    Some(false)
84}
85
86const fn default_true() -> bool {
87    true
88}
89
90/// Backward-compatible shape for ChatGPT workspace login restrictions in config.toml.
91#[derive(Serialize, Debug, Clone, PartialEq, JsonSchema)]
92#[serde(untagged)]
93pub enum ForcedChatgptWorkspaceIds {
94    Single(String),
95    Multiple(Vec<String>),
96}
97
98impl ForcedChatgptWorkspaceIds {
99    pub fn into_vec(self) -> Vec<String> {
100        match self {
101            Self::Single(value) => vec![value],
102            Self::Multiple(values) => values,
103        }
104    }
105}
106
107impl<'de> Deserialize<'de> for ForcedChatgptWorkspaceIds {
108    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
109    where
110        D: Deserializer<'de>,
111    {
112        #[derive(Deserialize)]
113        #[serde(untagged)]
114        enum Repr {
115            Single(String),
116            Multiple(Vec<String>),
117        }
118
119        match Repr::deserialize(deserializer)? {
120            Repr::Single(value) if value.contains(',') => Err(D::Error::custom(
121                "forced_chatgpt_workspace_id must be a single workspace ID string or a TOML list \
122of strings; comma-separated strings are not supported. Use \
123`forced_chatgpt_workspace_id = [\"123e4567-e89b-42d3-a456-426614174000\", \
124\"123e4567-e89b-42d3-a456-426614174001\"]` instead.",
125            )),
126            Repr::Single(value) => Ok(Self::Single(value)),
127            Repr::Multiple(values) => Ok(Self::Multiple(values)),
128        }
129    }
130}
131
132/// Orchestrator-owned feature settings.
133#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
134#[schemars(deny_unknown_fields)]
135pub struct OrchestratorToml {
136    pub skills: Option<OrchestratorFeatureToml>,
137    pub mcp: Option<OrchestratorFeatureToml>,
138}
139
140/// Settings for a feature owned by the orchestrator.
141#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
142#[schemars(deny_unknown_fields)]
143pub struct OrchestratorFeatureToml {
144    pub enabled: Option<bool>,
145}
146
147/// Base config deserialized from ~/.codex/config.toml.
148#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)]
149#[schemars(deny_unknown_fields)]
150pub struct ConfigToml {
151    /// Optional override of model selection.
152    pub model: Option<String>,
153    /// Review model override used by the `/review` feature.
154    pub review_model: Option<String>,
155
156    /// Provider to use from the model_providers map.
157    pub model_provider: Option<String>,
158
159    /// Size of the context window for the model, in tokens.
160    pub model_context_window: Option<i64>,
161
162    /// Token usage threshold triggering auto-compaction of conversation history.
163    pub model_auto_compact_token_limit: Option<i64>,
164
165    /// Controls whether the auto-compaction limit applies to the full context or
166    /// only to tokens after the carried prefix in the current compaction window.
167    pub model_auto_compact_token_limit_scope: Option<AutoCompactTokenLimitScope>,
168
169    /// Default approval policy for executing commands.
170    pub approval_policy: Option<AskForApproval>,
171
172    /// Configures who approval requests are routed to for review once they have
173    /// been escalated. This does not disable separate safety checks such as
174    /// ARC.
175    pub approvals_reviewer: Option<ApprovalsReviewer>,
176
177    /// Optional policy instructions for the guardian auto-reviewer.
178    #[serde(default)]
179    pub auto_review: Option<AutoReviewToml>,
180
181    #[serde(default)]
182    pub shell_environment_policy: ShellEnvironmentPolicyToml,
183
184    /// Whether the model may request a login shell for shell-based tools.
185    /// Default to `true`
186    ///
187    /// If `true`, the model may request a login shell (`login = true`), and
188    /// omitting `login` defaults to using a login shell.
189    /// If `false`, the model can never use a login shell: `login = true`
190    /// requests are rejected, and omitting `login` defaults to a non-login
191    /// shell.
192    pub allow_login_shell: Option<bool>,
193
194    /// Sandbox mode to use.
195    pub sandbox_mode: Option<SandboxMode>,
196
197    /// Sandbox configuration to apply if `sandbox` is `WorkspaceWrite`.
198    pub sandbox_workspace_write: Option<SandboxWorkspaceWrite>,
199
200    /// Default permissions profile to apply. Names starting with `:` refer to
201    /// built-in profiles; other names are resolved from the `[permissions]`
202    /// table.
203    pub default_permissions: Option<String>,
204
205    /// Named permissions profiles.
206    #[serde(default)]
207    pub permissions: Option<PermissionsToml>,
208
209    /// Optional external command to spawn for end-user notifications.
210    #[serde(default)]
211    pub notify: Option<Vec<String>>,
212
213    /// System instructions.
214    pub instructions: Option<String>,
215
216    /// Developer instructions inserted as a `developer` role message.
217    #[serde(default)]
218    pub developer_instructions: Option<String>,
219
220    /// Whether to inject the `<permissions instructions>` developer block.
221    pub include_permissions_instructions: Option<bool>,
222
223    /// Whether to inject the `<apps_instructions>` developer block.
224    pub include_apps_instructions: Option<bool>,
225
226    /// Whether to inject the `<collaboration_mode>` developer block.
227    pub include_collaboration_mode_instructions: Option<bool>,
228
229    /// Whether to inject the `<environment_context>` user block.
230    pub include_environment_context: Option<bool>,
231
232    /// Optional path to a file containing model instructions that will override
233    /// the built-in instructions for the selected model. Users are STRONGLY
234    /// DISCOURAGED from using this field, as deviating from the instructions
235    /// sanctioned by Codex will likely degrade model performance.
236    pub model_instructions_file: Option<AbsolutePathBuf>,
237
238    /// Compact prompt used for history compaction.
239    pub compact_prompt: Option<String>,
240
241    /// When set, restricts ChatGPT login to one or more workspace identifiers.
242    #[serde(default)]
243    pub forced_chatgpt_workspace_id: Option<ForcedChatgptWorkspaceIds>,
244
245    /// When set, restricts the login mechanism users may use.
246    #[serde(default)]
247    pub forced_login_method: Option<ForcedLoginMethod>,
248
249    /// Preferred backend for storing CLI auth credentials.
250    /// file (default): Use a file in the Codex home directory.
251    /// keyring: Use an OS-specific keyring service.
252    /// auto: Use the keyring if available, otherwise use a file.
253    #[serde(default)]
254    pub cli_auth_credentials_store: Option<AuthCredentialsStoreMode>,
255
256    /// Definition for MCP servers that Codex can reach out to for tool calls.
257    #[serde(default)]
258    // Uses the raw MCP input shape (custom deserialization) rather than `McpServerConfig`.
259    #[schemars(schema_with = "crate::schema::mcp_servers_schema")]
260    pub mcp_servers: HashMap<String, McpServerConfig>,
261
262    /// Preferred backend for storing MCP OAuth credentials.
263    /// keyring: Use an OS-specific keyring service.
264    ///          https://github.com/openai/codex/blob/main/codex-rs/rmcp-client/src/oauth.rs#L2
265    /// file: Use a file in the Codex home directory.
266    /// auto (default): Use the OS-specific keyring service if available, otherwise use a file.
267    #[serde(default)]
268    pub mcp_oauth_credentials_store: Option<OAuthCredentialsStoreMode>,
269
270    /// Optional fixed port for the local HTTP callback server used during MCP OAuth login.
271    /// When unset, Codex will bind to an ephemeral port chosen by the OS.
272    pub mcp_oauth_callback_port: Option<u16>,
273
274    /// Optional redirect URI to use during MCP OAuth login.
275    /// When set, this URI is used in the OAuth authorization request instead
276    /// of the local listener address. The local callback listener still binds
277    /// to 127.0.0.1 (using `mcp_oauth_callback_port` when provided).
278    pub mcp_oauth_callback_url: Option<String>,
279
280    /// User-defined provider entries that extend the built-in list. Built-in
281    /// IDs cannot be overridden.
282    #[serde(default, deserialize_with = "deserialize_model_providers")]
283    pub model_providers: HashMap<String, ModelProviderInfo>,
284
285    /// Maximum number of bytes to include from an AGENTS.md project doc file.
286    #[serde(default = "default_project_doc_max_bytes")]
287    pub project_doc_max_bytes: Option<usize>,
288
289    /// Ordered list of fallback filenames to look for when AGENTS.md is missing.
290    #[serde(default = "default_project_doc_fallback_filenames")]
291    pub project_doc_fallback_filenames: Option<Vec<String>>,
292
293    /// Token budget applied when storing tool/function outputs in the context manager.
294    pub tool_output_token_limit: Option<usize>,
295
296    /// Maximum poll window for background terminal output (`write_stdin`), in milliseconds.
297    /// Default: `300000` (5 minutes).
298    pub background_terminal_max_timeout: Option<u64>,
299
300    /// Deprecated: ignored.
301    #[schemars(skip)]
302    pub js_repl_node_path: Option<AbsolutePathBuf>,
303
304    /// Deprecated: ignored.
305    #[schemars(skip)]
306    pub js_repl_node_module_dirs: Option<Vec<AbsolutePathBuf>>,
307
308    /// Profile to use from the `profiles` map.
309    pub profile: Option<String>,
310
311    /// Named profiles to facilitate switching between different configurations.
312    #[serde(default)]
313    pub profiles: HashMap<String, ConfigProfile>,
314
315    /// Settings that govern if and what will be written to `~/.codex/history.jsonl`.
316    #[serde(default = "default_history")]
317    pub history: Option<History>,
318
319    /// Directory where Codex stores the SQLite state DB.
320    /// Defaults to `$CODEX_SQLITE_HOME` when set. Otherwise uses `$CODEX_HOME`.
321    pub sqlite_home: Option<AbsolutePathBuf>,
322
323    /// Directory where Codex writes log files. Setting this value explicitly
324    /// also enables the TUI text log in this directory.
325    /// Defaults to `$CODEX_HOME/log`.
326    pub log_dir: Option<AbsolutePathBuf>,
327
328    /// Debugging and reproducibility settings.
329    pub debug: Option<DebugToml>,
330
331    /// Optional URI-based file opener. If set, citations to files in the model
332    /// output will be hyperlinked using the specified URI scheme.
333    pub file_opener: Option<UriBasedFileOpener>,
334
335    /// Collection of settings that are specific to the TUI.
336    pub tui: Option<Tui>,
337
338    /// When set to `true`, `AgentReasoning` events will be hidden from the
339    /// UI/output. Defaults to `false`.
340    #[serde(default = "default_hide_agent_reasoning")]
341    pub hide_agent_reasoning: Option<bool>,
342
343    /// When set to `true`, `AgentReasoningRawContentEvent` events will be shown in the UI/output.
344    /// Defaults to `false`.
345    pub show_raw_agent_reasoning: Option<bool>,
346
347    pub model_reasoning_effort: Option<ReasoningEffort>,
348    pub plan_mode_reasoning_effort: Option<ReasoningEffort>,
349    pub model_reasoning_summary: Option<ReasoningSummary>,
350    /// Optional verbosity control for GPT-5 models (Responses API `text.verbosity`).
351    pub model_verbosity: Option<Verbosity>,
352
353    /// Optional path to a JSON model catalog (applied on startup only).
354    /// Per-thread `config` overrides are accepted but do not reapply this (no-ops).
355    pub model_catalog_json: Option<AbsolutePathBuf>,
356
357    /// Optionally specify a personality for the model
358    pub personality: Option<Personality>,
359
360    /// Optional explicit service tier request id for new turns (for example
361    /// `default`, `priority`, or `flex`; legacy `fast` also works).
362    pub service_tier: Option<String>,
363
364    /// Base URL for requests to ChatGPT (as opposed to the OpenAI API).
365    pub chatgpt_base_url: Option<String>,
366
367    /// Optional product SKU forwarded on host-owned Codex Apps MCP requests.
368    pub apps_mcp_product_sku: Option<String>,
369
370    /// Orchestrator-owned feature settings.
371    pub orchestrator: Option<OrchestratorToml>,
372
373    /// Base URL override for the built-in `openai` model provider.
374    pub openai_base_url: Option<String>,
375
376    /// Machine-local realtime audio device preferences used by realtime voice.
377    #[serde(default)]
378    pub audio: Option<RealtimeAudioToml>,
379
380    /// Experimental / do not use. Overrides only the realtime conversation
381    /// websocket transport base URL (the `Op::RealtimeConversation`
382    /// `/v1/realtime`
383    /// connection) without changing normal provider HTTP requests.
384    pub experimental_realtime_ws_base_url: Option<String>,
385    /// Experimental / do not use. Overrides only the WebRTC realtime call
386    /// creation base URL. This is separate from `experimental_realtime_ws_base_url`
387    /// because WebRTC call creation is HTTP, while sideband control is websocket.
388    pub experimental_realtime_webrtc_call_base_url: Option<String>,
389    /// Experimental / do not use. Selects the realtime websocket model/snapshot
390    /// used for the `Op::RealtimeConversation` connection.
391    pub experimental_realtime_ws_model: Option<String>,
392    /// Experimental / do not use. Realtime websocket session selection.
393    /// `version` controls v1/v2 and `type` controls conversational/transcription.
394    #[serde(default)]
395    pub realtime: Option<RealtimeToml>,
396    /// Experimental / do not use. Overrides only the realtime conversation
397    /// websocket transport instructions (the `Op::RealtimeConversation`
398    /// `/ws` session.update instructions) without changing normal prompts.
399    pub experimental_realtime_ws_backend_prompt: Option<String>,
400    /// Experimental / do not use. Replaces the synthesized realtime startup
401    /// context appended to websocket session instructions. An empty string
402    /// disables startup context injection entirely.
403    pub experimental_realtime_ws_startup_context: Option<String>,
404    /// Experimental / do not use. Replaces the built-in realtime start
405    /// instructions inserted into developer messages when realtime becomes
406    /// active.
407    pub experimental_realtime_start_instructions: Option<String>,
408
409    /// Experimental / do not use. When set, app-server fetches thread-scoped
410    /// config from a remote service at this endpoint.
411    pub experimental_thread_config_endpoint: Option<String>,
412
413    /// Removed. Former remote thread-store endpoint setting kept only so we can
414    /// fail fast instead of silently falling back to local persistence.
415    #[schemars(skip)]
416    pub experimental_thread_store_endpoint: Option<String>,
417
418    /// Experimental / do not use. Selects the thread store implementation.
419    pub experimental_thread_store: Option<ThreadStoreToml>,
420    pub projects: Option<HashMap<String, ProjectConfig>>,
421
422    /// Controls the web search tool mode: disabled, cached, indexed, or live.
423    pub web_search: Option<WebSearchMode>,
424
425    /// Nested tools section for feature toggles
426    pub tools: Option<ToolsToml>,
427
428    /// Additional discoverable tools that can be suggested for installation.
429    pub tool_suggest: Option<ToolSuggestConfig>,
430
431    /// Agent-related settings (thread limits, etc.).
432    pub agents: Option<AgentsToml>,
433
434    /// Memories subsystem settings.
435    pub memories: Option<MemoriesToml>,
436
437    /// User-level skill config entries keyed by SKILL.md path.
438    pub skills: Option<SkillsConfig>,
439
440    /// Lifecycle hooks configured inline in TOML plus user-level overrides.
441    pub hooks: Option<HooksToml>,
442
443    /// User-level plugin config entries keyed by plugin name.
444    #[serde(default)]
445    pub plugins: HashMap<String, PluginConfig>,
446
447    /// User-level marketplace entries keyed by marketplace name.
448    #[serde(default)]
449    pub marketplaces: HashMap<String, MarketplaceConfig>,
450
451    /// Centralized feature flags (new). Prefer this over individual toggles.
452    #[serde(default)]
453    // Injects known feature keys into the schema and forbids unknown keys.
454    #[schemars(schema_with = "crate::schema::features_schema")]
455    pub features: Option<FeaturesToml>,
456
457    /// Suppress warnings about unstable (under development) features.
458    pub suppress_unstable_features_warning: Option<bool>,
459
460    /// Compatibility-only settings retained so legacy `ghost_snapshot`
461    /// config still loads.
462    #[serde(default)]
463    pub ghost_snapshot: Option<GhostSnapshotToml>,
464
465    /// Markers used to detect the project root when searching parent
466    /// directories for `.codex` folders. Defaults to [".git"] when unset.
467    #[serde(default)]
468    pub project_root_markers: Option<Vec<String>>,
469
470    /// When `true`, checks for Codex updates on startup and surfaces update prompts.
471    /// Set to `false` only if your Codex updates are centrally managed.
472    /// Defaults to `true`.
473    pub check_for_update_on_startup: Option<bool>,
474
475    /// When true, disables burst-paste detection for typed input entirely.
476    /// All characters are inserted as they are received, and no buffering
477    /// or placeholder replacement will occur for fast keypress bursts.
478    pub disable_paste_burst: Option<bool>,
479
480    /// When `false`, disables analytics across Codex product surfaces in this machine.
481    /// Defaults to `true`.
482    pub analytics: Option<AnalyticsConfigToml>,
483
484    /// When `false`, disables feedback collection across Codex product surfaces.
485    /// Defaults to `true`.
486    pub feedback: Option<FeedbackConfigToml>,
487
488    /// Settings for app-specific controls.
489    #[serde(default)]
490    pub apps: Option<AppsConfigToml>,
491
492    /// Opaque desktop settings stored alongside the rest of config.toml.
493    #[serde(default)]
494    pub desktop: Option<HashMap<String, JsonValue>>,
495
496    /// OTEL configuration.
497    pub otel: Option<OtelConfigToml>,
498
499    /// Windows-specific configuration.
500    #[serde(default)]
501    pub windows: Option<WindowsToml>,
502
503    /// Collection of in-product notices (different from notifications)
504    /// See [`crate::types::Notice`] for more details
505    pub notice: Option<Notice>,
506
507    pub experimental_compact_prompt_file: Option<AbsolutePathBuf>,
508    pub experimental_use_unified_exec_tool: Option<bool>,
509    /// Preferred OSS provider for local models, e.g. "lmstudio" or "ollama".
510    pub oss_provider: Option<String>,
511}
512
513#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)]
514#[schemars(deny_unknown_fields)]
515pub struct ConfigLockfileToml {
516    pub version: u32,
517    pub codex_version: String,
518
519    /// Replayable effective config captured in the lockfile.
520    pub config: ConfigToml,
521}
522
523#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
524#[schemars(deny_unknown_fields)]
525pub struct DebugToml {
526    pub config_lockfile: Option<DebugConfigLockToml>,
527}
528
529#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
530#[schemars(deny_unknown_fields)]
531pub struct DebugConfigLockToml {
532    /// Directory where Codex writes effective session config lock files.
533    pub export_dir: Option<AbsolutePathBuf>,
534
535    /// Lockfile to replay as the authoritative effective config.
536    pub load_path: Option<AbsolutePathBuf>,
537
538    /// Allow replaying a lock generated by a different Codex version.
539    pub allow_codex_version_mismatch: Option<bool>,
540
541    /// Save fields resolved from the model catalog/session configuration.
542    pub save_fields_resolved_from_model_catalog: Option<bool>,
543}
544
545#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
546#[serde(tag = "type", rename_all = "snake_case")]
547pub enum ThreadStoreToml {
548    Local {},
549    #[schemars(skip)]
550    InMemory {
551        id: String,
552    },
553}
554
555#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
556pub struct AutoReviewToml {
557    /// Additional policy instructions inserted into the guardian prompt.
558    pub policy: Option<String>,
559}
560
561#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
562#[schemars(deny_unknown_fields)]
563pub struct ProjectConfig {
564    pub trust_level: Option<TrustLevel>,
565}
566
567impl ProjectConfig {
568    pub fn is_trusted(&self) -> bool {
569        matches!(self.trust_level, Some(TrustLevel::Trusted))
570    }
571
572    pub fn is_untrusted(&self) -> bool {
573        matches!(self.trust_level, Some(TrustLevel::Untrusted))
574    }
575}
576
577#[derive(Debug, Clone, Default, PartialEq, Eq)]
578pub struct RealtimeAudioConfig {
579    pub microphone: Option<String>,
580    pub speaker: Option<String>,
581}
582
583#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq, JsonSchema)]
584#[serde(rename_all = "snake_case")]
585pub enum RealtimeWsMode {
586    #[default]
587    Conversational,
588    Transcription,
589}
590
591#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq, JsonSchema)]
592#[serde(rename_all = "snake_case")]
593pub enum RealtimeTransport {
594    #[default]
595    #[serde(rename = "webrtc")]
596    WebRtc,
597    Websocket,
598}
599
600pub use codex_protocol::protocol::RealtimeConversationVersion as RealtimeWsVersion;
601pub use codex_protocol::protocol::RealtimeVoice;
602
603#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
604#[schemars(deny_unknown_fields)]
605pub struct RealtimeConfig {
606    pub version: RealtimeWsVersion,
607    #[serde(rename = "type")]
608    pub session_type: RealtimeWsMode,
609    pub transport: RealtimeTransport,
610    pub voice: Option<RealtimeVoice>,
611}
612
613#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
614#[schemars(deny_unknown_fields)]
615pub struct RealtimeToml {
616    pub version: Option<RealtimeWsVersion>,
617    #[serde(rename = "type")]
618    pub session_type: Option<RealtimeWsMode>,
619    pub transport: Option<RealtimeTransport>,
620    pub voice: Option<RealtimeVoice>,
621}
622
623#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
624#[schemars(deny_unknown_fields)]
625pub struct RealtimeAudioToml {
626    pub microphone: Option<String>,
627    pub speaker: Option<String>,
628}
629
630#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)]
631#[schemars(deny_unknown_fields)]
632pub struct ToolsToml {
633    #[serde(
634        default,
635        deserialize_with = "deserialize_optional_web_search_tool_config"
636    )]
637    pub web_search: Option<WebSearchToolConfig>,
638    pub experimental_request_user_input: Option<ExperimentalRequestUserInput>,
639}
640
641#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
642#[schemars(deny_unknown_fields)]
643pub struct ExperimentalRequestUserInput {
644    #[serde(default = "default_true")]
645    pub enabled: bool,
646}
647
648#[derive(Deserialize)]
649#[serde(untagged)]
650enum WebSearchToolConfigInput {
651    Enabled(bool),
652    Config(WebSearchToolConfig),
653}
654
655fn deserialize_optional_web_search_tool_config<'de, D>(
656    deserializer: D,
657) -> Result<Option<WebSearchToolConfig>, D::Error>
658where
659    D: Deserializer<'de>,
660{
661    let value = Option::<WebSearchToolConfigInput>::deserialize(deserializer)?;
662
663    Ok(match value {
664        None => None,
665        Some(WebSearchToolConfigInput::Enabled(enabled)) => {
666            let _ = enabled;
667            None
668        }
669        Some(WebSearchToolConfigInput::Config(config)) => Some(config),
670    })
671}
672
673#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
674#[schemars(deny_unknown_fields)]
675pub struct AgentsToml {
676    /// Whether multi-agent tools are enabled. Defaults to true.
677    /// An enabled `features.multi_agent_v2` setting takes precedence.
678    pub enabled: Option<bool>,
679    /// Maximum number of spawned agent threads that can be open concurrently per session.
680    /// When unset, the selected multi-agent backend uses its default.
681    #[serde(alias = "max_threads")]
682    #[schemars(range(min = 1))]
683    pub max_concurrent_threads_per_session: Option<usize>,
684    /// Maximum nesting depth for V1 agent threads. Ignored by V2.
685    pub max_depth: Option<i32>,
686    /// Default model for spawned subagents when the spawn call does not select one.
687    pub default_subagent_model: Option<String>,
688    /// Default reasoning effort for spawned subagents when the spawn call does not select one.
689    pub default_subagent_reasoning_effort: Option<ReasoningEffort>,
690    /// Removed agent-job setting retained as a no-op for compatibility.
691    #[schemars(skip)]
692    pub job_max_runtime_seconds: Option<u64>,
693    /// Whether to record a model-visible message when an agent turn is interrupted.
694    /// Defaults to true.
695    pub interrupt_message: Option<bool>,
696
697    /// User-defined role declarations keyed by role name.
698    ///
699    /// Example:
700    /// ```toml
701    /// [agents.researcher]
702    /// description = "Research-focused role."
703    /// config_file = "./agents/researcher.toml"
704    /// nickname_candidates = ["Herodotus", "Ibn Battuta"]
705    /// ```
706    #[serde(default, flatten)]
707    pub roles: BTreeMap<String, AgentRoleToml>,
708}
709
710#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
711#[schemars(deny_unknown_fields)]
712pub struct AgentRoleToml {
713    /// Human-facing role documentation used in spawn tool guidance.
714    /// Required unless supplied by the referenced agent role file.
715    pub description: Option<String>,
716
717    /// Path to a role-specific config layer.
718    /// Relative paths are resolved relative to the `config.toml` that defines them.
719    pub config_file: Option<AbsolutePathBuf>,
720
721    /// Candidate nicknames for agents spawned with this role.
722    pub nickname_candidates: Option<Vec<String>>,
723}
724
725#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
726#[schemars(deny_unknown_fields)]
727pub struct GhostSnapshotToml {
728    /// Legacy no-op setting retained for compatibility.
729    #[serde(alias = "ignore_untracked_files_over_bytes")]
730    pub ignore_large_untracked_files: Option<i64>,
731    /// Legacy no-op setting retained for compatibility.
732    #[serde(alias = "large_untracked_dir_warning_threshold")]
733    pub ignore_large_untracked_dirs: Option<i64>,
734    /// Legacy no-op setting retained for compatibility.
735    pub disable_warnings: Option<bool>,
736}
737
738impl ConfigToml {
739    /// Derive the effective permission profile from legacy sandbox config.
740    ///
741    /// Call this only after ruling out `default_permissions`: named
742    /// `[permissions]` profiles must be compiled through the permissions
743    /// profile pipeline, not reconstructed from `sandbox_mode`.
744    pub async fn derive_permission_profile(
745        &self,
746        sandbox_mode_override: Option<SandboxMode>,
747        windows_sandbox_level: WindowsSandboxLevel,
748        active_project: Option<&ProjectConfig>,
749        permission_profile_constraint: Option<&crate::Constrained<PermissionProfile>>,
750    ) -> PermissionProfile {
751        let configured_sandbox_mode = sandbox_mode_override.or(self.sandbox_mode);
752        let resolved_sandbox_mode = configured_sandbox_mode
753            .or_else(|| {
754                // If no sandbox_mode is set but this directory has a trust decision,
755                // default to workspace-write except on unsandboxed Windows where we
756                // default to read-only.
757                active_project
758                    .filter(|project| project.is_trusted() || project.is_untrusted())
759                    .map(|_| {
760                        if cfg!(target_os = "windows")
761                            && windows_sandbox_level == WindowsSandboxLevel::Disabled
762                        {
763                            SandboxMode::ReadOnly
764                        } else {
765                            SandboxMode::WorkspaceWrite
766                        }
767                    })
768            })
769            .unwrap_or_default();
770        let effective_sandbox_mode = if cfg!(target_os = "windows")
771            // If the experimental Windows sandbox is enabled, do not force a downgrade.
772            && windows_sandbox_level == WindowsSandboxLevel::Disabled
773            && matches!(resolved_sandbox_mode, SandboxMode::WorkspaceWrite)
774        {
775            SandboxMode::ReadOnly
776        } else {
777            resolved_sandbox_mode
778        };
779
780        let permission_profile = match effective_sandbox_mode {
781            SandboxMode::ReadOnly => PermissionProfile::read_only(),
782            SandboxMode::WorkspaceWrite => match self.sandbox_workspace_write.as_ref() {
783                Some(SandboxWorkspaceWrite {
784                    writable_roots,
785                    network_access,
786                    exclude_tmpdir_env_var,
787                    exclude_slash_tmp,
788                }) => {
789                    let network_policy = if *network_access {
790                        NetworkSandboxPolicy::Enabled
791                    } else {
792                        NetworkSandboxPolicy::Restricted
793                    };
794                    PermissionProfile::workspace_write_with(
795                        writable_roots,
796                        network_policy,
797                        *exclude_tmpdir_env_var,
798                        *exclude_slash_tmp,
799                    )
800                }
801                None => PermissionProfile::workspace_write(),
802            },
803            SandboxMode::DangerFullAccess => PermissionProfile::Disabled,
804        };
805        if configured_sandbox_mode.is_none()
806            && let Some(constraint) = permission_profile_constraint
807            && let Err(err) = constraint.can_set(&permission_profile)
808        {
809            tracing::warn!(
810                error = %err,
811                "default sandbox policy is disallowed by requirements; falling back to required default"
812            );
813            PermissionProfile::read_only()
814        } else {
815            permission_profile
816        }
817    }
818
819    /// Resolves the cwd to an existing project, or returns None if ConfigToml
820    /// does not contain a project corresponding to cwd or the resolved git repo
821    /// root for cwd.
822    pub fn get_active_project(
823        &self,
824        resolved_cwd: &Path,
825        repo_root: Option<&Path>,
826    ) -> Option<ProjectConfig> {
827        let projects = self.projects.as_ref()?;
828
829        for normalized_cwd in normalized_project_lookup_keys(resolved_cwd) {
830            if let Some(project_config) = project_config_for_lookup_key(projects, &normalized_cwd) {
831                return Some(project_config);
832            }
833        }
834
835        if let Some(repo_root) = repo_root {
836            for normalized_repo_root in normalized_project_lookup_keys(repo_root) {
837                if let Some(project_config_for_root) =
838                    project_config_for_lookup_key(projects, &normalized_repo_root)
839                {
840                    return Some(project_config_for_root);
841                }
842            }
843        }
844
845        None
846    }
847}
848
849/// Canonicalize the path and convert it to a string to be used as a key in the
850/// projects trust map. On Windows, strips UNC, when possible, to try to ensure
851/// that different paths that point to the same location have the same key.
852fn normalized_project_lookup_keys(path: &Path) -> Vec<String> {
853    let normalized_path = normalize_project_lookup_key(path.to_string_lossy().to_string());
854    let normalized_canonical_path = normalize_project_lookup_key(
855        normalize_for_path_comparison(path)
856            .unwrap_or_else(|_| path.to_path_buf())
857            .to_string_lossy()
858            .to_string(),
859    );
860    if normalized_path == normalized_canonical_path {
861        vec![normalized_canonical_path]
862    } else {
863        vec![normalized_canonical_path, normalized_path]
864    }
865}
866
867fn normalize_project_lookup_key(key: String) -> String {
868    if cfg!(windows) {
869        key.to_ascii_lowercase()
870    } else {
871        key
872    }
873}
874
875fn project_config_for_lookup_key(
876    projects: &HashMap<String, ProjectConfig>,
877    lookup_key: &str,
878) -> Option<ProjectConfig> {
879    if let Some(project_config) = projects.get(lookup_key) {
880        return Some(project_config.clone());
881    }
882
883    let mut normalized_matches: Vec<_> = projects
884        .iter()
885        .filter(|(key, _)| normalize_project_lookup_key((*key).clone()) == lookup_key)
886        .collect();
887    normalized_matches.sort_by_key(|(key, _)| *key);
888    normalized_matches
889        .first()
890        .map(|(_, project_config)| (**project_config).clone())
891}
892
893pub fn validate_reserved_model_provider_ids(
894    model_providers: &HashMap<String, ModelProviderInfo>,
895) -> Result<(), String> {
896    let mut conflicts = model_providers
897        .keys()
898        .filter(|key| {
899            key.as_str() != AMAZON_BEDROCK_PROVIDER_ID
900                && RESERVED_MODEL_PROVIDER_IDS.contains(&key.as_str())
901        })
902        .map(|key| format!("`{key}`"))
903        .collect::<Vec<_>>();
904    conflicts.sort_unstable();
905    if conflicts.is_empty() {
906        Ok(())
907    } else {
908        Err(format!(
909            "model_providers contains reserved built-in provider IDs: {}. \
910Built-in providers cannot be overridden. Rename your custom provider (for example, `openai-custom`).",
911            conflicts.join(", ")
912        ))
913    }
914}
915
916pub fn validate_model_providers(
917    model_providers: &HashMap<String, ModelProviderInfo>,
918) -> Result<(), String> {
919    validate_reserved_model_provider_ids(model_providers)?;
920    for (key, provider) in model_providers {
921        if key != AMAZON_BEDROCK_PROVIDER_ID {
922            if provider.aws.is_some() {
923                return Err(format!(
924                    "model_providers.{key}: provider aws is only supported for `{AMAZON_BEDROCK_PROVIDER_ID}`"
925                ));
926            }
927            if provider.name.trim().is_empty() {
928                return Err(format!(
929                    "model_providers.{key}: provider name must not be empty"
930                ));
931            }
932        }
933        provider
934            .validate()
935            .map_err(|message| format!("model_providers.{key}: {message}"))?;
936    }
937    Ok(())
938}
939
940fn deserialize_model_providers<'de, D>(
941    deserializer: D,
942) -> Result<HashMap<String, ModelProviderInfo>, D::Error>
943where
944    D: serde::Deserializer<'de>,
945{
946    let model_providers = HashMap::<String, ModelProviderInfo>::deserialize(deserializer)?;
947    validate_model_providers(&model_providers).map_err(serde::de::Error::custom)?;
948    Ok(model_providers)
949}
950
951pub fn validate_oss_provider(provider: &str) -> std::io::Result<()> {
952    match provider {
953        LMSTUDIO_OSS_PROVIDER_ID | OLLAMA_OSS_PROVIDER_ID => Ok(()),
954        LEGACY_OLLAMA_CHAT_PROVIDER_ID => Err(std::io::Error::new(
955            std::io::ErrorKind::InvalidInput,
956            OLLAMA_CHAT_PROVIDER_REMOVED_ERROR,
957        )),
958        _ => Err(std::io::Error::new(
959            std::io::ErrorKind::InvalidInput,
960            format!(
961                "Invalid OSS provider '{provider}'. Must be one of: {LMSTUDIO_OSS_PROVIDER_ID}, {OLLAMA_OSS_PROVIDER_ID}"
962            ),
963        )),
964    }
965}
966
967#[cfg(test)]
968mod tests {
969    use super::*;
970    use pretty_assertions::assert_eq;
971
972    const WORKSPACE_ID_A: &str = "123e4567-e89b-42d3-a456-426614174000";
973    const WORKSPACE_ID_B: &str = "123e4567-e89b-42d3-a456-426614174001";
974
975    #[test]
976    fn forced_chatgpt_workspace_id_accepts_single_string() {
977        let config: ConfigToml = toml::from_str(&format!(
978            r#"forced_chatgpt_workspace_id = "{WORKSPACE_ID_A}""#
979        ))
980        .expect("single workspace id should deserialize");
981
982        assert_eq!(
983            config
984                .forced_chatgpt_workspace_id
985                .expect("workspace id should be set")
986                .into_vec(),
987            vec![WORKSPACE_ID_A.to_string()]
988        );
989    }
990
991    #[test]
992    fn forced_chatgpt_workspace_id_accepts_string_list() {
993        let config: ConfigToml = toml::from_str(&format!(
994            r#"forced_chatgpt_workspace_id = ["{WORKSPACE_ID_A}", "{WORKSPACE_ID_B}"]"#
995        ))
996        .expect("workspace id list should deserialize");
997
998        assert_eq!(
999            config
1000                .forced_chatgpt_workspace_id
1001                .expect("workspace ids should be set")
1002                .into_vec(),
1003            vec![WORKSPACE_ID_A.to_string(), WORKSPACE_ID_B.to_string()]
1004        );
1005    }
1006
1007    #[test]
1008    fn forced_chatgpt_workspace_id_rejects_comma_separated_string() {
1009        let err = toml::from_str::<ConfigToml>(&format!(
1010            r#"forced_chatgpt_workspace_id = "{WORKSPACE_ID_A},{WORKSPACE_ID_B}""#
1011        ))
1012        .expect_err("comma-separated string should be rejected");
1013
1014        let message = err.to_string();
1015        assert!(message.contains("TOML list of strings"));
1016        assert!(message.contains("comma-separated strings are not supported"));
1017    }
1018
1019    #[test]
1020    fn amazon_bedrock_auth_command_must_not_be_empty() {
1021        let err = toml::from_str::<ConfigToml>(
1022            r#"
1023[model_providers.amazon-bedrock.auth]
1024command = "   "
1025"#,
1026        )
1027        .expect_err("empty Amazon Bedrock auth command should be rejected");
1028
1029        assert!(
1030            err.to_string().contains(
1031                "model_providers.amazon-bedrock: provider auth.command must not be empty"
1032            )
1033        );
1034    }
1035}