## Summary
- Factor `load_config_as_toml` into `core::config_loader` so config
loading is reusable across callers.
- Layer `~/.codex/config.toml`, optional `~/.codex/managed_config.toml`,
and macOS managed preferences (base64) with recursive table merging and
scoped threads per source.
## Config Flow
```
Managed prefs (macOS profile: com.openai.codex/config_toml_base64)
▲
│
~/.codex/managed_config.toml │ (optional file-based override)
▲
│
~/.codex/config.toml (user-defined settings)
```
- The loader searches under the resolved `CODEX_HOME` directory
(defaults to `~/.codex`).
- Managed configs let administrators ship fleet-wide overrides via
device profiles which is useful for enforcing certain settings like
sandbox or approval defaults.
- For nested hash tables: overlays merge recursively. Child tables are
merged key-by-key, while scalar or array values replace the prior layer
entirely. This lets admins add or tweak individual fields without
clobbering unrelated user settings.
337 lines
12 KiB
Rust
337 lines
12 KiB
Rust
//! Configuration object accepted by the `codex` MCP tool-call.
|
|
|
|
use codex_core::protocol::AskForApproval;
|
|
use codex_protocol::config_types::SandboxMode;
|
|
use codex_utils_json_to_toml::json_to_toml;
|
|
use mcp_types::Tool;
|
|
use mcp_types::ToolInputSchema;
|
|
use schemars::JsonSchema;
|
|
use schemars::r#gen::SchemaSettings;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
|
|
/// Client-supplied configuration for a `codex` tool-call.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub struct CodexToolCallParam {
|
|
/// The *initial user prompt* to start the Codex conversation.
|
|
pub prompt: String,
|
|
|
|
/// Optional override for the model name (e.g. "o3", "o4-mini").
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub model: Option<String>,
|
|
|
|
/// Configuration profile from config.toml to specify default options.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub profile: Option<String>,
|
|
|
|
/// Working directory for the session. If relative, it is resolved against
|
|
/// the server process's current working directory.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub cwd: Option<String>,
|
|
|
|
/// Approval policy for shell commands generated by the model:
|
|
/// `untrusted`, `on-failure`, `on-request`, `never`.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub approval_policy: Option<CodexToolCallApprovalPolicy>,
|
|
|
|
/// Sandbox mode: `read-only`, `workspace-write`, or `danger-full-access`.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub sandbox: Option<CodexToolCallSandboxMode>,
|
|
|
|
/// Individual config settings that will override what is in
|
|
/// CODEX_HOME/config.toml.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub config: Option<HashMap<String, serde_json::Value>>,
|
|
|
|
/// The set of instructions to use instead of the default ones.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub base_instructions: Option<String>,
|
|
|
|
/// Whether to include the plan tool in the conversation.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub include_plan_tool: Option<bool>,
|
|
}
|
|
|
|
/// Custom enum mirroring [`AskForApproval`], but has an extra dependency on
|
|
/// [`JsonSchema`].
|
|
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub enum CodexToolCallApprovalPolicy {
|
|
Untrusted,
|
|
OnFailure,
|
|
OnRequest,
|
|
Never,
|
|
}
|
|
|
|
impl From<CodexToolCallApprovalPolicy> for AskForApproval {
|
|
fn from(value: CodexToolCallApprovalPolicy) -> Self {
|
|
match value {
|
|
CodexToolCallApprovalPolicy::Untrusted => AskForApproval::UnlessTrusted,
|
|
CodexToolCallApprovalPolicy::OnFailure => AskForApproval::OnFailure,
|
|
CodexToolCallApprovalPolicy::OnRequest => AskForApproval::OnRequest,
|
|
CodexToolCallApprovalPolicy::Never => AskForApproval::Never,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Custom enum mirroring [`SandboxMode`] from config_types.rs, but with
|
|
/// `JsonSchema` support.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub enum CodexToolCallSandboxMode {
|
|
ReadOnly,
|
|
WorkspaceWrite,
|
|
DangerFullAccess,
|
|
}
|
|
|
|
impl From<CodexToolCallSandboxMode> for SandboxMode {
|
|
fn from(value: CodexToolCallSandboxMode) -> Self {
|
|
match value {
|
|
CodexToolCallSandboxMode::ReadOnly => SandboxMode::ReadOnly,
|
|
CodexToolCallSandboxMode::WorkspaceWrite => SandboxMode::WorkspaceWrite,
|
|
CodexToolCallSandboxMode::DangerFullAccess => SandboxMode::DangerFullAccess,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Builds a `Tool` definition (JSON schema etc.) for the Codex tool-call.
|
|
pub(crate) fn create_tool_for_codex_tool_call_param() -> Tool {
|
|
let schema = SchemaSettings::draft2019_09()
|
|
.with(|s| {
|
|
s.inline_subschemas = true;
|
|
s.option_add_null_type = false;
|
|
})
|
|
.into_generator()
|
|
.into_root_schema_for::<CodexToolCallParam>();
|
|
|
|
#[expect(clippy::expect_used)]
|
|
let schema_value =
|
|
serde_json::to_value(&schema).expect("Codex tool schema should serialise to JSON");
|
|
|
|
let tool_input_schema =
|
|
serde_json::from_value::<ToolInputSchema>(schema_value).unwrap_or_else(|e| {
|
|
panic!("failed to create Tool from schema: {e}");
|
|
});
|
|
|
|
Tool {
|
|
name: "codex".to_string(),
|
|
title: Some("Codex".to_string()),
|
|
input_schema: tool_input_schema,
|
|
// TODO(mbolin): This should be defined.
|
|
output_schema: None,
|
|
description: Some(
|
|
"Run a Codex session. Accepts configuration parameters matching the Codex Config struct.".to_string(),
|
|
),
|
|
annotations: None,
|
|
}
|
|
}
|
|
|
|
impl CodexToolCallParam {
|
|
/// Returns the initial user prompt to start the Codex conversation and the
|
|
/// effective Config object generated from the supplied parameters.
|
|
pub async fn into_config(
|
|
self,
|
|
codex_linux_sandbox_exe: Option<PathBuf>,
|
|
) -> std::io::Result<(String, codex_core::config::Config)> {
|
|
let Self {
|
|
prompt,
|
|
model,
|
|
profile,
|
|
cwd,
|
|
approval_policy,
|
|
sandbox,
|
|
config: cli_overrides,
|
|
base_instructions,
|
|
include_plan_tool,
|
|
} = self;
|
|
|
|
// Build the `ConfigOverrides` recognized by codex-core.
|
|
let overrides = codex_core::config::ConfigOverrides {
|
|
model,
|
|
review_model: None,
|
|
config_profile: profile,
|
|
cwd: cwd.map(PathBuf::from),
|
|
approval_policy: approval_policy.map(Into::into),
|
|
sandbox_mode: sandbox.map(Into::into),
|
|
model_provider: None,
|
|
codex_linux_sandbox_exe,
|
|
base_instructions,
|
|
include_plan_tool,
|
|
include_apply_patch_tool: None,
|
|
include_view_image_tool: None,
|
|
show_raw_agent_reasoning: None,
|
|
tools_web_search_request: None,
|
|
};
|
|
|
|
let cli_overrides = cli_overrides
|
|
.unwrap_or_default()
|
|
.into_iter()
|
|
.map(|(k, v)| (k, json_to_toml(v)))
|
|
.collect();
|
|
|
|
let cfg =
|
|
codex_core::config::Config::load_with_cli_overrides(cli_overrides, overrides).await?;
|
|
|
|
Ok((prompt, cfg))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CodexToolCallReplyParam {
|
|
/// The conversation id for this Codex session.
|
|
pub conversation_id: String,
|
|
|
|
/// The *next user prompt* to continue the Codex conversation.
|
|
pub prompt: String,
|
|
}
|
|
|
|
/// Builds a `Tool` definition for the `codex-reply` tool-call.
|
|
pub(crate) fn create_tool_for_codex_tool_call_reply_param() -> Tool {
|
|
let schema = SchemaSettings::draft2019_09()
|
|
.with(|s| {
|
|
s.inline_subschemas = true;
|
|
s.option_add_null_type = false;
|
|
})
|
|
.into_generator()
|
|
.into_root_schema_for::<CodexToolCallReplyParam>();
|
|
|
|
#[expect(clippy::expect_used)]
|
|
let schema_value =
|
|
serde_json::to_value(&schema).expect("Codex reply tool schema should serialise to JSON");
|
|
|
|
let tool_input_schema =
|
|
serde_json::from_value::<ToolInputSchema>(schema_value).unwrap_or_else(|e| {
|
|
panic!("failed to create Tool from schema: {e}");
|
|
});
|
|
|
|
Tool {
|
|
name: "codex-reply".to_string(),
|
|
title: Some("Codex Reply".to_string()),
|
|
input_schema: tool_input_schema,
|
|
output_schema: None,
|
|
description: Some(
|
|
"Continue a Codex conversation by providing the conversation id and prompt."
|
|
.to_string(),
|
|
),
|
|
annotations: None,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
/// We include a test to verify the exact JSON schema as "executable
|
|
/// documentation" for the schema. When can track changes to this test as a
|
|
/// way to audit changes to the generated schema.
|
|
///
|
|
/// Seeing the fully expanded schema makes it easier to casually verify that
|
|
/// the generated JSON for enum types such as "approval-policy" is compact.
|
|
/// Ideally, modelcontextprotocol/inspector would provide a simpler UI for
|
|
/// enum fields versus open string fields to take advantage of this.
|
|
///
|
|
/// As of 2025-05-04, there is an open PR for this:
|
|
/// https://github.com/modelcontextprotocol/inspector/pull/196
|
|
#[test]
|
|
fn verify_codex_tool_json_schema() {
|
|
let tool = create_tool_for_codex_tool_call_param();
|
|
let tool_json = serde_json::to_value(&tool).expect("tool serializes");
|
|
let expected_tool_json = serde_json::json!({
|
|
"name": "codex",
|
|
"title": "Codex",
|
|
"description": "Run a Codex session. Accepts configuration parameters matching the Codex Config struct.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"approval-policy": {
|
|
"description": "Approval policy for shell commands generated by the model: `untrusted`, `on-failure`, `on-request`, `never`.",
|
|
"enum": [
|
|
"untrusted",
|
|
"on-failure",
|
|
"on-request",
|
|
"never"
|
|
],
|
|
"type": "string"
|
|
},
|
|
"sandbox": {
|
|
"description": "Sandbox mode: `read-only`, `workspace-write`, or `danger-full-access`.",
|
|
"enum": [
|
|
"read-only",
|
|
"workspace-write",
|
|
"danger-full-access"
|
|
],
|
|
"type": "string"
|
|
},
|
|
"config": {
|
|
"description": "Individual config settings that will override what is in CODEX_HOME/config.toml.",
|
|
"additionalProperties": true,
|
|
"type": "object"
|
|
},
|
|
"cwd": {
|
|
"description": "Working directory for the session. If relative, it is resolved against the server process's current working directory.",
|
|
"type": "string"
|
|
},
|
|
"include-plan-tool": {
|
|
"description": "Whether to include the plan tool in the conversation.",
|
|
"type": "boolean"
|
|
},
|
|
"model": {
|
|
"description": "Optional override for the model name (e.g. \"o3\", \"o4-mini\").",
|
|
"type": "string"
|
|
},
|
|
"profile": {
|
|
"description": "Configuration profile from config.toml to specify default options.",
|
|
"type": "string"
|
|
},
|
|
"prompt": {
|
|
"description": "The *initial user prompt* to start the Codex conversation.",
|
|
"type": "string"
|
|
},
|
|
"base-instructions": {
|
|
"description": "The set of instructions to use instead of the default ones.",
|
|
"type": "string"
|
|
},
|
|
},
|
|
"required": [
|
|
"prompt"
|
|
]
|
|
}
|
|
});
|
|
assert_eq!(expected_tool_json, tool_json);
|
|
}
|
|
|
|
#[test]
|
|
fn verify_codex_tool_reply_json_schema() {
|
|
let tool = create_tool_for_codex_tool_call_reply_param();
|
|
let tool_json = serde_json::to_value(&tool).expect("tool serializes");
|
|
let expected_tool_json = serde_json::json!({
|
|
"description": "Continue a Codex conversation by providing the conversation id and prompt.",
|
|
"inputSchema": {
|
|
"properties": {
|
|
"conversationId": {
|
|
"description": "The conversation id for this Codex session.",
|
|
"type": "string"
|
|
},
|
|
"prompt": {
|
|
"description": "The *next user prompt* to continue the Codex conversation.",
|
|
"type": "string"
|
|
},
|
|
},
|
|
"required": [
|
|
"conversationId",
|
|
"prompt",
|
|
],
|
|
"type": "object",
|
|
},
|
|
"name": "codex-reply",
|
|
"title": "Codex Reply",
|
|
});
|
|
assert_eq!(expected_tool_json, tool_json);
|
|
}
|
|
}
|