//! Schema-heavy configuration TOML types used by Codex.use std::collections::BTreeMap;use std::collections::HashMap;use std::path::Path;use crate::HooksToml;use crate::permissions_toml::PermissionsToml;use crate::profile_toml::ConfigProfile;use crate::types::AnalyticsConfigToml;use crate::types::ApprovalsReviewer;use crate::types::AppsConfigToml;use crate::types::AuthCredentialsStoreMode;use crate::types::FeedbackConfigToml;use crate::types::History;use crate::types::MarketplaceConfig;use crate::types::McpServerConfig;use crate::types::MemoriesToml;use crate::types::Notice;use crate::types::OAuthCredentialsStoreMode;use crate::types::OtelConfigToml;use crate::types::PluginConfig;use crate::types::SandboxWorkspaceWrite;use crate::types::ShellEnvironmentPolicyToml;use crate::types::SkillsConfig;use crate::types::ToolSuggestConfig;use crate::types::Tui;use crate::types::UriBasedFileOpener;use crate::types::WindowsToml;use codex_features::FeaturesToml;use codex_model_provider_info::AMAZON_BEDROCK_PROVIDER_ID;use codex_model_provider_info::LEGACY_OLLAMA_CHAT_PROVIDER_ID;use codex_model_provider_info::LMSTUDIO_OSS_PROVIDER_ID;use codex_model_provider_info::ModelProviderInfo;use codex_model_provider_info::OLLAMA_CHAT_PROVIDER_REMOVED_ERROR;use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID;use codex_model_provider_info::OPENAI_PROVIDER_ID;use codex_protocol::config_types::AutoCompactTokenLimitScope;use codex_protocol::config_types::ForcedLoginMethod;use codex_protocol::config_types::Personality;use codex_protocol::config_types::ReasoningSummary;use codex_protocol::config_types::SandboxMode;use codex_protocol::config_types::TrustLevel;use codex_protocol::config_types::Verbosity;use codex_protocol::config_types::WebSearchMode;use codex_protocol::config_types::WebSearchToolConfig;use codex_protocol::config_types::WindowsSandboxLevel;use codex_protocol::models::PermissionProfile;use codex_protocol::openai_models::ReasoningEffort;use codex_protocol::permissions::NetworkSandboxPolicy;use codex_protocol::protocol::AskForApproval;use codex_utils_absolute_path::AbsolutePathBuf;use codex_utils_path::normalize_for_path_comparison;use schemars::JsonSchema;use serde::Deserialize;use serde::Deserializer;use serde::Serialize;use serde::de::Error as SerdeError;use serde_json::Value as JsonValue;const RESERVED_MODEL_PROVIDER_IDS: [&str; 4] = [ AMAZON_BEDROCK_PROVIDER_ID, OPENAI_PROVIDER_ID, OLLAMA_OSS_PROVIDER_ID, LMSTUDIO_OSS_PROVIDER_ID,];pub const DEFAULT_PROJECT_DOC_MAX_BYTES: usize = 32 * 1024;const fn default_allow_login_shell() -> Option<bool> { Some(true)}fn default_history() -> Option<History> { Some(History::default())}const fn default_project_doc_max_bytes() -> Option<usize> { Some(DEFAULT_PROJECT_DOC_MAX_BYTES)}fn default_project_doc_fallback_filenames() -> Option<Vec<String>> { Some(Vec::new())}const fn default_hide_agent_reasoning() -> Option<bool> { Some(false)}const fn default_true() -> bool { true}/// Backward-compatible shape for ChatGPT workspace login restrictions in config.toml.#[derive(Serialize, Debug, Clone, PartialEq, JsonSchema)]#[serde(untagged)]pub enum ForcedChatgptWorkspaceIds { Single(String), Multiple(Vec<String>),}impl ForcedChatgptWorkspaceIds { pub fn into_vec(self) -> Vec<String> { match self { Self::Single(value) => vec![value], Self::Multiple(values) => values, } }}impl<'de> Deserialize<'de> for ForcedChatgptWorkspaceIds { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { #[derive(Deserialize)] #[serde(untagged)] enum Repr { Single(String), Multiple(Vec<String>), } match Repr::deserialize(deserializer)? { Repr::Single(value) if value.contains(',') => Err(D::Error::custom( "forced_chatgpt_workspace_id must be a single workspace ID string or a TOML list \of strings; comma-separated strings are not supported. Use \`forced_chatgpt_workspace_id = [\"123e4567-e89b-42d3-a456-426614174000\", \\"123e4567-e89b-42d3-a456-426614174001\"]` instead.", )), Repr::Single(value) => Ok(Self::Single(value)), Repr::Multiple(values) => Ok(Self::Multiple(values)), } }}/// Base config deserialized from ~/.codex/config.toml.#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)]#[schemars(deny_unknown_fields)]pub struct ConfigToml { /// Optional override of model selection. pub model: Option<String>, /// Review model override used by the `/review` feature. pub review_model: Option<String>, /// Provider to use from the model_providers map. pub model_provider: Option<String>, /// Size of the context window for the model, in tokens. pub model_context_window: Option<i64>, /// Token usage threshold triggering auto-compaction of conversation history. pub model_auto_compact_token_limit: Option<i64>, /// Controls whether the auto-compaction limit applies to the full context or /// only to tokens after the carried prefix in the current compaction window. pub model_auto_compact_token_limit_scope: Option<AutoCompactTokenLimitScope>, /// Default approval policy for executing commands. pub approval_policy: Option<AskForApproval>, /// Configures who approval requests are routed to for review once they have /// been escalated. This does not disable separate safety checks such as /// ARC. pub approvals_reviewer: Option<ApprovalsReviewer>, /// Optional policy instructions for the guardian auto-reviewer. #[serde(default)] pub auto_review: Option<AutoReviewToml>, #[serde(default)] pub shell_environment_policy: ShellEnvironmentPolicyToml, /// Whether the model may request a login shell for shell-based tools. /// Default to `true` /// /// If `true`, the model may request a login shell (`login = true`), and /// omitting `login` defaults to using a login shell. /// If `false`, the model can never use a login shell: `login = true` /// requests are rejected, and omitting `login` defaults to a non-login /// shell. #[serde(default = "default_allow_login_shell")] pub allow_login_shell: Option<bool>, /// Sandbox mode to use. pub sandbox_mode: Option<SandboxMode>, /// Sandbox configuration to apply if `sandbox` is `WorkspaceWrite`. pub sandbox_workspace_write: Option<SandboxWorkspaceWrite>, /// Default permissions profile to apply. Names starting with `:` refer to /// built-in profiles; other names are resolved from the `[permissions]` /// table. pub default_permissions: Option<String>, /// Named permissions profiles. #[serde(default)] pub permissions: Option<PermissionsToml>, /// Optional external command to spawn for end-user notifications. #[serde(default)] pub notify: Option<Vec<String>>, /// System instructions. pub instructions: Option<String>, /// Developer instructions inserted as a `developer` role message. #[serde(default)] pub developer_instructions: Option<String>, /// Whether to inject the `<permissions instructions>` developer block. pub include_permissions_instructions: Option<bool>, /// Whether to inject the `<apps_instructions>` developer block. pub include_apps_instructions: Option<bool>, /// Whether to inject the `<collaboration_mode>` developer block. pub include_collaboration_mode_instructions: Option<bool>, /// Whether to inject the `<environment_context>` user block. pub include_environment_context: Option<bool>, /// Optional path to a file containing model instructions that will override /// the built-in instructions for the selected model. Users are STRONGLY /// DISCOURAGED from using this field, as deviating from the instructions /// sanctioned by Codex will likely degrade model performance. pub model_instructions_file: Option<AbsolutePathBuf>, /// Compact prompt used for history compaction. pub compact_prompt: Option<String>, /// When set, restricts ChatGPT login to one or more workspace identifiers. #[serde(default)] pub forced_chatgpt_workspace_id: Option<ForcedChatgptWorkspaceIds>, /// When set, restricts the login mechanism users may use. #[serde(default)] pub forced_login_method: Option<ForcedLoginMethod>, /// Preferred backend for storing CLI auth credentials. /// file (default): Use a file in the Codex home directory. /// keyring: Use an OS-specific keyring service. /// auto: Use the keyring if available, otherwise use a file. #[serde(default)] pub cli_auth_credentials_store: Option<AuthCredentialsStoreMode>, /// Definition for MCP servers that Codex can reach out to for tool calls. #[serde(default)] // Uses the raw MCP input shape (custom deserialization) rather than `McpServerConfig`. #[schemars(schema_with = "crate::schema::mcp_servers_schema")] pub mcp_servers: HashMap<String, McpServerConfig>, /// Preferred backend for storing MCP OAuth credentials. /// keyring: Use an OS-specific keyring service. /// https://github.com/openai/codex/blob/main/codex-rs/rmcp-client/src/oauth.rs#L2 /// file: Use a file in the Codex home directory. /// auto (default): Use the OS-specific keyring service if available, otherwise use a file. #[serde(default)] pub mcp_oauth_credentials_store: Option<OAuthCredentialsStoreMode>, /// Optional fixed port for the local HTTP callback server used during MCP OAuth login. /// When unset, Codex will bind to an ephemeral port chosen by the OS. pub mcp_oauth_callback_port: Option<u16>, /// Optional redirect URI to use during MCP OAuth login. /// When set, this URI is used in the OAuth authorization request instead /// of the local listener address. The local callback listener still binds /// to 127.0.0.1 (using `mcp_oauth_callback_port` when provided). pub mcp_oauth_callback_url: Option<String>, /// User-defined provider entries that extend the built-in list. Built-in /// IDs cannot be overridden. #[serde(default, deserialize_with = "deserialize_model_providers")] pub model_providers: HashMap<String, ModelProviderInfo>, /// Maximum number of bytes to include from an AGENTS.md project doc file. #[serde(default = "default_project_doc_max_bytes")] pub project_doc_max_bytes: Option<usize>, /// Ordered list of fallback filenames to look for when AGENTS.md is missing. #[serde(default = "default_project_doc_fallback_filenames")] pub project_doc_fallback_filenames: Option<Vec<String>>, /// Token budget applied when storing tool/function outputs in the context manager. pub tool_output_token_limit: Option<usize>, /// Maximum poll window for background terminal output (`write_stdin`), in milliseconds. /// Default: `300000` (5 minutes). pub background_terminal_max_timeout: Option<u64>, /// Deprecated: ignored. #[schemars(skip)] pub js_repl_node_path: Option<AbsolutePathBuf>, /// Deprecated: ignored. #[schemars(skip)] pub js_repl_node_module_dirs: Option<Vec<AbsolutePathBuf>>, /// Profile to use from the `profiles` map. pub profile: Option<String>, /// Named profiles to facilitate switching between different configurations. #[serde(default)] pub profiles: HashMap<String, ConfigProfile>, /// Settings that govern if and what will be written to `~/.codex/history.jsonl`. #[serde(default = "default_history")] pub history: Option<History>, /// Directory where Codex stores the SQLite state DB. /// Defaults to `$CODEX_SQLITE_HOME` when set. Otherwise uses `$CODEX_HOME`. pub sqlite_home: Option<AbsolutePathBuf>, /// Directory where Codex writes log files. Setting this value explicitly /// also enables the TUI text log in this directory. /// Defaults to `$CODEX_HOME/log`. pub log_dir: Option<AbsolutePathBuf>, /// Debugging and reproducibility settings. pub debug: Option<DebugToml>, /// Optional URI-based file opener. If set, citations to files in the model /// output will be hyperlinked using the specified URI scheme. pub file_opener: Option<UriBasedFileOpener>, /// Collection of settings that are specific to the TUI. pub tui: Option<Tui>, /// When set to `true`, `AgentReasoning` events will be hidden from the /// UI/output. Defaults to `false`. #[serde(default = "default_hide_agent_reasoning")] pub hide_agent_reasoning: Option<bool>, /// When set to `true`, `AgentReasoningRawContentEvent` events will be shown in the UI/output. /// Defaults to `false`. pub show_raw_agent_reasoning: Option<bool>, pub model_reasoning_effort: Option<ReasoningEffort>, pub plan_mode_reasoning_effort: Option<ReasoningEffort>, pub model_reasoning_summary: Option<ReasoningSummary>, /// Optional verbosity control for GPT-5 models (Responses API `text.verbosity`). pub model_verbosity: Option<Verbosity>, /// Override to force-enable reasoning summaries for the configured model. pub model_supports_reasoning_summaries: Option<bool>, /// Optional path to a JSON model catalog (applied on startup only). /// Per-thread `config` overrides are accepted but do not reapply this (no-ops). pub model_catalog_json: Option<AbsolutePathBuf>, /// Optionally specify a personality for the model pub personality: Option<Personality>, /// Optional explicit service tier request id for new turns (for example /// `default`, `priority`, or `flex`; legacy `fast` also works). pub service_tier: Option<String>, /// Base URL for requests to ChatGPT (as opposed to the OpenAI API). pub chatgpt_base_url: Option<String>, /// Optional product SKU forwarded on host-owned Codex Apps MCP requests. pub apps_mcp_product_sku: Option<String>, /// Base URL override for the built-in `openai` model provider. pub openai_base_url: Option<String>, /// Machine-local realtime audio device preferences used by realtime voice. #[serde(default)] pub audio: Option<RealtimeAudioToml>, /// Experimental / do not use. Overrides only the realtime conversation /// websocket transport base URL (the `Op::RealtimeConversation` /// `/v1/realtime` /// connection) without changing normal provider HTTP requests. pub experimental_realtime_ws_base_url: Option<String>, /// Experimental / do not use. Overrides only the WebRTC realtime call /// creation base URL. This is separate from `experimental_realtime_ws_base_url` /// because WebRTC call creation is HTTP, while sideband control is websocket. pub experimental_realtime_webrtc_call_base_url: Option<String>, /// Experimental / do not use. Selects the realtime websocket model/snapshot /// used for the `Op::RealtimeConversation` connection. pub experimental_realtime_ws_model: Option<String>, /// Experimental / do not use. Realtime websocket session selection. /// `version` controls v1/v2 and `type` controls conversational/transcription. #[serde(default)] pub realtime: Option<RealtimeToml>, /// Experimental / do not use. Overrides only the realtime conversation /// websocket transport instructions (the `Op::RealtimeConversation` /// `/ws` session.update instructions) without changing normal prompts. pub experimental_realtime_ws_backend_prompt: Option<String>, /// Experimental / do not use. Replaces the synthesized realtime startup /// context appended to websocket session instructions. An empty string /// disables startup context injection entirely. pub experimental_realtime_ws_startup_context: Option<String>, /// Experimental / do not use. Replaces the built-in realtime start /// instructions inserted into developer messages when realtime becomes /// active. pub experimental_realtime_start_instructions: Option<String>, /// Experimental / do not use. When set, app-server fetches thread-scoped /// config from a remote service at this endpoint. pub experimental_thread_config_endpoint: Option<String>, /// Removed. Former remote thread-store endpoint setting kept only so we can /// fail fast instead of silently falling back to local persistence. #[schemars(skip)] pub experimental_thread_store_endpoint: Option<String>, /// Experimental / do not use. Selects the thread store implementation. pub experimental_thread_store: Option<ThreadStoreToml>, pub projects: Option<HashMap<String, ProjectConfig>>, /// Controls the web search tool mode: disabled, cached, or live. pub web_search: Option<WebSearchMode>, /// Nested tools section for feature toggles pub tools: Option<ToolsToml>, /// Additional discoverable tools that can be suggested for installation. pub tool_suggest: Option<ToolSuggestConfig>, /// Agent-related settings (thread limits, etc.). pub agents: Option<AgentsToml>, /// Memories subsystem settings. pub memories: Option<MemoriesToml>, /// User-level skill config entries keyed by SKILL.md path. pub skills: Option<SkillsConfig>, /// Lifecycle hooks configured inline in TOML plus user-level overrides. pub hooks: Option<HooksToml>, /// User-level plugin config entries keyed by plugin name. #[serde(default)] pub plugins: HashMap<String, PluginConfig>, /// User-level marketplace entries keyed by marketplace name. #[serde(default)] pub marketplaces: HashMap<String, MarketplaceConfig>, /// Centralized feature flags (new). Prefer this over individual toggles. #[serde(default)] // Injects known feature keys into the schema and forbids unknown keys. #[schemars(schema_with = "crate::schema::features_schema")] pub features: Option<FeaturesToml>, /// Suppress warnings about unstable (under development) features. pub suppress_unstable_features_warning: Option<bool>, /// Compatibility-only settings retained so legacy `ghost_snapshot` /// config still loads. #[serde(default)] pub ghost_snapshot: Option<GhostSnapshotToml>, /// Markers used to detect the project root when searching parent /// directories for `.codex` folders. Defaults to [".git"] when unset. #[serde(default)] pub project_root_markers: Option<Vec<String>>, /// When `true`, checks for Codex updates on startup and surfaces update prompts. /// Set to `false` only if your Codex updates are centrally managed. /// Defaults to `true`. pub check_for_update_on_startup: Option<bool>, /// When true, disables burst-paste detection for typed input entirely. /// All characters are inserted as they are received, and no buffering /// or placeholder replacement will occur for fast keypress bursts. pub disable_paste_burst: Option<bool>, /// When `false`, disables analytics across Codex product surfaces in this machine. /// Defaults to `true`. pub analytics: Option<AnalyticsConfigToml>, /// When `false`, disables feedback collection across Codex product surfaces. /// Defaults to `true`. pub feedback: Option<FeedbackConfigToml>, /// Settings for app-specific controls. #[serde(default)] pub apps: Option<AppsConfigToml>, /// Opaque desktop settings stored alongside the rest of config.toml. #[serde(default)] pub desktop: Option<HashMap<String, JsonValue>>, /// OTEL configuration. pub otel: Option<OtelConfigToml>, /// Windows-specific configuration. #[serde(default)] pub windows: Option<WindowsToml>, /// Collection of in-product notices (different from notifications) /// See [`crate::types::Notice`] for more details pub notice: Option<Notice>, pub experimental_compact_prompt_file: Option<AbsolutePathBuf>, pub experimental_use_unified_exec_tool: Option<bool>, /// Preferred OSS provider for local models, e.g. "lmstudio" or "ollama". pub oss_provider: Option<String>,}#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)]#[schemars(deny_unknown_fields)]pub struct ConfigLockfileToml { pub version: u32, pub codex_version: String, /// Replayable effective config captured in the lockfile. pub config: ConfigToml,}#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]#[schemars(deny_unknown_fields)]pub struct DebugToml { pub config_lockfile: Option<DebugConfigLockToml>,}#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]#[schemars(deny_unknown_fields)]pub struct DebugConfigLockToml { /// Directory where Codex writes effective session config lock files. pub export_dir: Option<AbsolutePathBuf>, /// Lockfile to replay as the authoritative effective config. pub load_path: Option<AbsolutePathBuf>, /// Allow replaying a lock generated by a different Codex version. pub allow_codex_version_mismatch: Option<bool>, /// Save fields resolved from the model catalog/session configuration. pub save_fields_resolved_from_model_catalog: Option<bool>,}#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]#[serde(tag = "type", rename_all = "snake_case")]pub enum ThreadStoreToml { Local {}, #[schemars(skip)] InMemory { id: String, },}#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]pub struct AutoReviewToml { /// Additional policy instructions inserted into the guardian prompt. pub policy: Option<String>,}#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]#[schemars(deny_unknown_fields)]pub struct ProjectConfig { pub trust_level: Option<TrustLevel>,}impl ProjectConfig { pub fn is_trusted(&self) -> bool { matches!(self.trust_level, Some(TrustLevel::Trusted)) } pub fn is_untrusted(&self) -> bool { matches!(self.trust_level, Some(TrustLevel::Untrusted)) }}#[derive(Debug, Clone, Default, PartialEq, Eq)]pub struct RealtimeAudioConfig { pub microphone: Option<String>, pub speaker: Option<String>,}#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq, JsonSchema)]#[serde(rename_all = "snake_case")]pub enum RealtimeWsMode { #[default] Conversational, Transcription,}#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq, JsonSchema)]#[serde(rename_all = "snake_case")]pub enum RealtimeTransport { #[default] #[serde(rename = "webrtc")] WebRtc, Websocket,}pub use codex_protocol::protocol::RealtimeConversationArchitecture as RealtimeArchitecture;pub use codex_protocol::protocol::RealtimeConversationVersion as RealtimeWsVersion;pub use codex_protocol::protocol::RealtimeVoice;#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]#[schemars(deny_unknown_fields)]pub struct RealtimeConfig { pub architecture: RealtimeArchitecture, pub version: RealtimeWsVersion, #[serde(rename = "type")] pub session_type: RealtimeWsMode, pub transport: RealtimeTransport, pub voice: Option<RealtimeVoice>,}#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]#[schemars(deny_unknown_fields)]pub struct RealtimeToml { pub architecture: Option<RealtimeArchitecture>, pub version: Option<RealtimeWsVersion>, #[serde(rename = "type")] pub session_type: Option<RealtimeWsMode>, pub transport: Option<RealtimeTransport>, pub voice: Option<RealtimeVoice>,}#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]#[schemars(deny_unknown_fields)]pub struct RealtimeAudioToml { pub microphone: Option<String>, pub speaker: Option<String>,}#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)]#[schemars(deny_unknown_fields)]pub struct ToolsToml { #[serde( default, deserialize_with = "deserialize_optional_web_search_tool_config" )] pub web_search: Option<WebSearchToolConfig>, pub experimental_request_user_input: Option<ExperimentalRequestUserInput>,}#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]#[schemars(deny_unknown_fields)]pub struct ExperimentalRequestUserInput { #[serde(default = "default_true")] pub enabled: bool,}#[derive(Deserialize)]#[serde(untagged)]enum WebSearchToolConfigInput { Enabled(bool), Config(WebSearchToolConfig),}fn deserialize_optional_web_search_tool_config<'de, D>( deserializer: D,) -> Result<Option<WebSearchToolConfig>, D::Error>where D: Deserializer<'de>,{ let value = Option::<WebSearchToolConfigInput>::deserialize(deserializer)?; Ok(match value { None => None, Some(WebSearchToolConfigInput::Enabled(enabled)) => { let _ = enabled; None } Some(WebSearchToolConfigInput::Config(config)) => Some(config), })}#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]#[schemars(deny_unknown_fields)]pub struct AgentsToml { /// Maximum number of agent threads that can be open concurrently. /// When unset, no limit is enforced. #[schemars(range(min = 1))] pub max_threads: Option<usize>, /// Maximum nesting depth allowed for spawned agent threads. /// Root sessions start at depth 0. #[schemars(range(min = 1))] pub max_depth: Option<i32>, /// Default maximum runtime in seconds for agent job workers. #[schemars(range(min = 1))] pub job_max_runtime_seconds: Option<u64>, /// Whether to record a model-visible message when an agent turn is interrupted. /// Defaults to true. pub interrupt_message: Option<bool>, /// User-defined role declarations keyed by role name. /// /// Example: /// ```toml /// [agents.researcher] /// description = "Research-focused role." /// config_file = "./agents/researcher.toml" /// nickname_candidates = ["Herodotus", "Ibn Battuta"] /// ``` #[serde(default, flatten)] pub roles: BTreeMap<String, AgentRoleToml>,}#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]#[schemars(deny_unknown_fields)]pub struct AgentRoleToml { /// Human-facing role documentation used in spawn tool guidance. /// Required unless supplied by the referenced agent role file. pub description: Option<String>, /// Path to a role-specific config layer. /// Relative paths are resolved relative to the `config.toml` that defines them. pub config_file: Option<AbsolutePathBuf>, /// Candidate nicknames for agents spawned with this role. pub nickname_candidates: Option<Vec<String>>,}#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]#[schemars(deny_unknown_fields)]pub struct GhostSnapshotToml { /// Legacy no-op setting retained for compatibility. #[serde(alias = "ignore_untracked_files_over_bytes")] pub ignore_large_untracked_files: Option<i64>, /// Legacy no-op setting retained for compatibility. #[serde(alias = "large_untracked_dir_warning_threshold")] pub ignore_large_untracked_dirs: Option<i64>, /// Legacy no-op setting retained for compatibility. pub disable_warnings: Option<bool>,}impl ConfigToml { /// Derive the effective permission profile from legacy sandbox config. /// /// Call this only after ruling out `default_permissions`: named /// `[permissions]` profiles must be compiled through the permissions /// profile pipeline, not reconstructed from `sandbox_mode`. pub async fn derive_permission_profile( &self, sandbox_mode_override: Option<SandboxMode>, windows_sandbox_level: WindowsSandboxLevel, active_project: Option<&ProjectConfig>, permission_profile_constraint: Option<&crate::Constrained<PermissionProfile>>, ) -> PermissionProfile { let configured_sandbox_mode = sandbox_mode_override.or(self.sandbox_mode); let resolved_sandbox_mode = configured_sandbox_mode .or_else(|| { // If no sandbox_mode is set but this directory has a trust decision, // default to workspace-write except on unsandboxed Windows where we // default to read-only. active_project .filter(|project| project.is_trusted() || project.is_untrusted()) .map(|_| { if cfg!(target_os = "windows") && windows_sandbox_level == WindowsSandboxLevel::Disabled { SandboxMode::ReadOnly } else { SandboxMode::WorkspaceWrite } }) }) .unwrap_or_default(); let effective_sandbox_mode = if cfg!(target_os = "windows") // If the experimental Windows sandbox is enabled, do not force a downgrade. && windows_sandbox_level == WindowsSandboxLevel::Disabled && matches!(resolved_sandbox_mode, SandboxMode::WorkspaceWrite) { SandboxMode::ReadOnly } else { resolved_sandbox_mode }; let permission_profile = match effective_sandbox_mode { SandboxMode::ReadOnly => PermissionProfile::read_only(), SandboxMode::WorkspaceWrite => match self.sandbox_workspace_write.as_ref() { Some(SandboxWorkspaceWrite { writable_roots, network_access, exclude_tmpdir_env_var, exclude_slash_tmp, }) => { let network_policy = if *network_access { NetworkSandboxPolicy::Enabled } else { NetworkSandboxPolicy::Restricted }; PermissionProfile::workspace_write_with( writable_roots, network_policy, *exclude_tmpdir_env_var, *exclude_slash_tmp, ) } None => PermissionProfile::workspace_write(), }, SandboxMode::DangerFullAccess => PermissionProfile::Disabled, }; if configured_sandbox_mode.is_none() && let Some(constraint) = permission_profile_constraint && let Err(err) = constraint.can_set(&permission_profile) { tracing::warn!( error = %err, "default sandbox policy is disallowed by requirements; falling back to required default" ); PermissionProfile::read_only() } else { permission_profile } } /// Resolves the cwd to an existing project, or returns None if ConfigToml /// does not contain a project corresponding to cwd or the resolved git repo /// root for cwd. pub fn get_active_project( &self, resolved_cwd: &Path, repo_root: Option<&Path>, ) -> Option<ProjectConfig> { let projects = self.projects.as_ref()?; for normalized_cwd in normalized_project_lookup_keys(resolved_cwd) { if let Some(project_config) = project_config_for_lookup_key(projects, &normalized_cwd) { return Some(project_config); } } if let Some(repo_root) = repo_root { for normalized_repo_root in normalized_project_lookup_keys(repo_root) { if let Some(project_config_for_root) = project_config_for_lookup_key(projects, &normalized_repo_root) { return Some(project_config_for_root); } } } None }}/// Canonicalize the path and convert it to a string to be used as a key in the/// projects trust map. On Windows, strips UNC, when possible, to try to ensure/// that different paths that point to the same location have the same key.fn normalized_project_lookup_keys(path: &Path) -> Vec<String> { let normalized_path = normalize_project_lookup_key(path.to_string_lossy().to_string()); let normalized_canonical_path = normalize_project_lookup_key( normalize_for_path_comparison(path) .unwrap_or_else(|_| path.to_path_buf()) .to_string_lossy() .to_string(), ); if normalized_path == normalized_canonical_path { vec![normalized_canonical_path] } else { vec![normalized_canonical_path, normalized_path] }}fn normalize_project_lookup_key(key: String) -> String { if cfg!(windows) { key.to_ascii_lowercase() } else { key }}fn project_config_for_lookup_key( projects: &HashMap<String, ProjectConfig>, lookup_key: &str,) -> Option<ProjectConfig> { if let Some(project_config) = projects.get(lookup_key) { return Some(project_config.clone()); } let mut normalized_matches: Vec<_> = projects .iter() .filter(|(key, _)| normalize_project_lookup_key((*key).clone()) == lookup_key) .collect(); normalized_matches.sort_by_key(|(key, _)| *key); normalized_matches .first() .map(|(_, project_config)| (**project_config).clone())}pub fn validate_reserved_model_provider_ids( model_providers: &HashMap<String, ModelProviderInfo>,) -> Result<(), String> { let mut conflicts = model_providers .keys() .filter(|key| { key.as_str() != AMAZON_BEDROCK_PROVIDER_ID && RESERVED_MODEL_PROVIDER_IDS.contains(&key.as_str()) }) .map(|key| format!("`{key}`")) .collect::<Vec<_>>(); conflicts.sort_unstable(); if conflicts.is_empty() { Ok(()) } else { Err(format!( "model_providers contains reserved built-in provider IDs: {}. \Built-in providers cannot be overridden. Rename your custom provider (for example, `openai-custom`).", conflicts.join(", ") )) }}pub fn validate_model_providers( model_providers: &HashMap<String, ModelProviderInfo>,) -> Result<(), String> { validate_reserved_model_provider_ids(model_providers)?; for (key, provider) in model_providers { if key == AMAZON_BEDROCK_PROVIDER_ID { continue; } if provider.aws.is_some() { return Err(format!( "model_providers.{key}: provider aws is only supported for `{AMAZON_BEDROCK_PROVIDER_ID}`" )); } if provider.name.trim().is_empty() { return Err(format!( "model_providers.{key}: provider name must not be empty" )); } provider .validate() .map_err(|message| format!("model_providers.{key}: {message}"))?; } Ok(())}fn deserialize_model_providers<'de, D>( deserializer: D,) -> Result<HashMap<String, ModelProviderInfo>, D::Error>where D: serde::Deserializer<'de>,{ let model_providers = HashMap::<String, ModelProviderInfo>::deserialize(deserializer)?; validate_model_providers(&model_providers).map_err(serde::de::Error::custom)?; Ok(model_providers)}pub fn validate_oss_provider(provider: &str) -> std::io::Result<()> { match provider { LMSTUDIO_OSS_PROVIDER_ID | OLLAMA_OSS_PROVIDER_ID => Ok(()), LEGACY_OLLAMA_CHAT_PROVIDER_ID => Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, OLLAMA_CHAT_PROVIDER_REMOVED_ERROR, )), _ => Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, format!( "Invalid OSS provider '{provider}'. Must be one of: {LMSTUDIO_OSS_PROVIDER_ID}, {OLLAMA_OSS_PROVIDER_ID}" ), )), }}#[cfg(test)]mod tests { use super::*; use pretty_assertions::assert_eq; const WORKSPACE_ID_A: &str = "123e4567-e89b-42d3-a456-426614174000"; const WORKSPACE_ID_B: &str = "123e4567-e89b-42d3-a456-426614174001"; #[test] fn forced_chatgpt_workspace_id_accepts_single_string() { let config: ConfigToml = toml::from_str(&format!( r#"forced_chatgpt_workspace_id = "{WORKSPACE_ID_A}""# )) .expect("single workspace id should deserialize"); assert_eq!( config .forced_chatgpt_workspace_id .expect("workspace id should be set") .into_vec(), vec![WORKSPACE_ID_A.to_string()] ); } #[test] fn forced_chatgpt_workspace_id_accepts_string_list() { let config: ConfigToml = toml::from_str(&format!( r#"forced_chatgpt_workspace_id = ["{WORKSPACE_ID_A}", "{WORKSPACE_ID_B}"]"# )) .expect("workspace id list should deserialize"); assert_eq!( config .forced_chatgpt_workspace_id .expect("workspace ids should be set") .into_vec(), vec![WORKSPACE_ID_A.to_string(), WORKSPACE_ID_B.to_string()] ); } #[test] fn forced_chatgpt_workspace_id_rejects_comma_separated_string() { let err = toml::from_str::<ConfigToml>(&format!( r#"forced_chatgpt_workspace_id = "{WORKSPACE_ID_A},{WORKSPACE_ID_B}""# )) .expect_err("comma-separated string should be rejected"); let message = err.to_string(); assert!(message.contains("TOML list of strings")); assert!(message.contains("comma-separated strings are not supported")); }}