On a high-level, we try to design `config.toml` so that you don't have to "comment out a lot of stuff" when testing different options. Previously, defining a sandbox policy was somewhat at odds with this principle because you would define the policy as attributes of `[sandbox]` like so: ```toml [sandbox] mode = "workspace-write" writable_roots = [ "/tmp" ] ``` but if you wanted to temporarily change to a read-only sandbox, you might feel compelled to modify your file to be: ```toml [sandbox] mode = "read-only" # mode = "workspace-write" # writable_roots = [ "/tmp" ] ``` Technically, commenting out `writable_roots` would not be strictly necessary, as `mode = "read-only"` would ignore `writable_roots`, but it's still a reasonable thing to do to keep things tidy. Currently, the various values for `mode` do not support that many attributes, so this is not that hard to maintain, but one could imagine this becoming more complex in the future. In this PR, we change Codex CLI so that it no longer recognizes `[sandbox]`. Instead, it introduces a top-level option, `sandbox_mode`, and `[sandbox_workspace_write]` is used to further configure the sandbox when when `sandbox_mode = "workspace-write"` is used: ```toml sandbox_mode = "workspace-write" [sandbox_workspace_write] writable_roots = [ "/tmp" ] ``` This feels a bit more future-proof in that it is less tedious to configure different sandboxes: ```toml sandbox_mode = "workspace-write" [sandbox_read_only] # read-only options here... [sandbox_workspace_write] writable_roots = [ "/tmp" ] [sandbox_danger_full_access] # danger-full-access options here... ``` In this scheme, you never need to comment out the configuration for an individual sandbox type: you only need to redefine `sandbox_mode`. Relatedly, previous to this change, a user had to do `-c sandbox.mode=read-only` to change the mode on the command line. With this change, things are arguably a bit cleaner because the equivalent option is `-c sandbox_mode=read-only` (and now `-c sandbox_workspace_write=...` can be set separately). Though more importantly, we introduce the `-s/--sandbox` option to the CLI, which maps directly to `sandbox_mode` in `config.toml`, making config override behavior easier to reason about. Moreover, as you can see in the updates to the various Markdown files, it is much easier to explain how to configure sandboxing when things like `--sandbox read-only` can be used as an example. Relatedly, this cleanup also made it straightforward to add support for a `sandbox` option for Codex when used as an MCP server (see the changes to `mcp-server/src/codex_tool_config.rs`). Fixes https://github.com/openai/codex/issues/1248.
234 lines
8.4 KiB
Rust
234 lines
8.4 KiB
Rust
//! Configuration object accepted by the `codex` MCP tool-call.
|
|
|
|
use codex_core::config_types::SandboxMode;
|
|
use codex_core::protocol::AskForApproval;
|
|
use mcp_types::Tool;
|
|
use mcp_types::ToolInputSchema;
|
|
use schemars::JsonSchema;
|
|
use schemars::r#gen::SchemaSettings;
|
|
use serde::Deserialize;
|
|
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
|
|
use crate::json_to_toml::json_to_toml;
|
|
|
|
/// Client-supplied configuration for a `codex` tool-call.
|
|
#[derive(Debug, Clone, Deserialize, JsonSchema)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub(crate) 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`, `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>>,
|
|
}
|
|
|
|
/// Custom enum mirroring [`AskForApproval`], but has an extra dependency on
|
|
/// [`JsonSchema`].
|
|
#[derive(Debug, Clone, Deserialize, JsonSchema)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub(crate) enum CodexToolCallApprovalPolicy {
|
|
Untrusted,
|
|
OnFailure,
|
|
Never,
|
|
}
|
|
|
|
impl From<CodexToolCallApprovalPolicy> for AskForApproval {
|
|
fn from(value: CodexToolCallApprovalPolicy) -> Self {
|
|
match value {
|
|
CodexToolCallApprovalPolicy::Untrusted => AskForApproval::UnlessTrusted,
|
|
CodexToolCallApprovalPolicy::OnFailure => AskForApproval::OnFailure,
|
|
CodexToolCallApprovalPolicy::Never => AskForApproval::Never,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Custom enum mirroring [`SandboxMode`] from config_types.rs, but with
|
|
/// `JsonSchema` support.
|
|
#[derive(Debug, Clone, Deserialize, JsonSchema)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub(crate) 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(),
|
|
input_schema: tool_input_schema,
|
|
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 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,
|
|
} = self;
|
|
|
|
// Build the `ConfigOverrides` recognised by codex-core.
|
|
let overrides = codex_core::config::ConfigOverrides {
|
|
model,
|
|
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,
|
|
};
|
|
|
|
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)?;
|
|
|
|
Ok((prompt, cfg))
|
|
}
|
|
}
|
|
|
|
#[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();
|
|
#[expect(clippy::expect_used)]
|
|
let tool_json = serde_json::to_value(&tool).expect("tool serializes");
|
|
let expected_tool_json = serde_json::json!({
|
|
"name": "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`, `never`.",
|
|
"enum": [
|
|
"untrusted",
|
|
"on-failure",
|
|
"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"
|
|
},
|
|
"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"
|
|
},
|
|
},
|
|
"required": [
|
|
"prompt"
|
|
]
|
|
}
|
|
});
|
|
assert_eq!(expected_tool_json, tool_json);
|
|
}
|
|
}
|