diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index b56bcf0f9c..b69894e725 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1407,6 +1407,7 @@ dependencies = [ "codex-backend-client", "codex-chatgpt", "codex-cloud-requirements", + "codex-config", "codex-core", "codex-exec-server", "codex-features", @@ -1803,11 +1804,13 @@ dependencies = [ "serde_json", "serde_path_to_error", "sha2", + "tempfile", "thiserror 2.0.18", "tokio", "toml 0.9.11+spec-1.1.0", "toml_edit 0.24.0+spec-1.1.0", "tracing", + "wildmatch", ] [[package]] @@ -1951,7 +1954,6 @@ dependencies = [ "uuid", "walkdir", "which 8.0.0", - "wildmatch", "windows-sys 0.52.0", "wiremock", "zip", @@ -2215,6 +2217,7 @@ version = "0.0.0" dependencies = [ "cc", "clap", + "codex-config", "codex-core", "codex-protocol", "codex-sandboxing", @@ -2318,6 +2321,7 @@ version = "0.0.0" dependencies = [ "anyhow", "codex-arg0", + "codex-config", "codex-core", "codex-exec-server", "codex-features", @@ -2708,6 +2712,7 @@ dependencies = [ "codex-chatgpt", "codex-cli", "codex-cloud-requirements", + "codex-config", "codex-core", "codex-features", "codex-feedback", diff --git a/codex-rs/app-server/Cargo.toml b/codex-rs/app-server/Cargo.toml index 3d58bbe146..b0525685cd 100644 --- a/codex-rs/app-server/Cargo.toml +++ b/codex-rs/app-server/Cargo.toml @@ -32,6 +32,7 @@ axum = { workspace = true, default-features = false, features = [ codex-analytics = { workspace = true } codex-arg0 = { workspace = true } codex-cloud-requirements = { workspace = true } +codex-config = { workspace = true } codex-core = { workspace = true } codex-exec-server = { workspace = true } codex-features = { workspace = true } diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index f833e5234f..2b9aa12ab4 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -181,6 +181,7 @@ use codex_arg0::Arg0DispatchPaths; use codex_backend_client::Client as BackendClient; use codex_chatgpt::connectors; use codex_cloud_requirements::cloud_requirements_loader; +use codex_config::types::McpServerTransportConfig; use codex_core::CodexThread; use codex_core::Cursor as RolloutCursor; use codex_core::ForkSnapshot; @@ -196,7 +197,6 @@ use codex_core::config::ConfigOverrides; use codex_core::config::NetworkProxyAuditMetadata; use codex_core::config::edit::ConfigEdit; use codex_core::config::edit::ConfigEditsBuilder; -use codex_core::config::types::McpServerTransportConfig; use codex_core::config_loader::CloudRequirementsLoadError; use codex_core::config_loader::CloudRequirementsLoadErrorCode; use codex_core::config_loader::CloudRequirementsLoader; diff --git a/codex-rs/app-server/src/codex_message_processor/plugin_mcp_oauth.rs b/codex-rs/app-server/src/codex_message_processor/plugin_mcp_oauth.rs index eec359835f..58f4a5e8dd 100644 --- a/codex-rs/app-server/src/codex_message_processor/plugin_mcp_oauth.rs +++ b/codex-rs/app-server/src/codex_message_processor/plugin_mcp_oauth.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use codex_app_server_protocol::McpServerOauthLoginCompletedNotification; use codex_app_server_protocol::ServerNotification; +use codex_config::types::McpServerConfig; use codex_core::config::Config; -use codex_core::config::types::McpServerConfig; use codex_mcp::mcp::auth::McpOAuthLoginSupport; use codex_mcp::mcp::auth::oauth_login_support; use codex_mcp::mcp::auth::resolve_oauth_scopes; diff --git a/codex-rs/app-server/tests/suite/v2/analytics.rs b/codex-rs/app-server/tests/suite/v2/analytics.rs index d4884d1337..8e8e328a84 100644 --- a/codex-rs/app-server/tests/suite/v2/analytics.rs +++ b/codex-rs/app-server/tests/suite/v2/analytics.rs @@ -2,9 +2,9 @@ use anyhow::Result; use app_test_support::ChatGptAuthFixture; use app_test_support::DEFAULT_CLIENT_NAME; use app_test_support::write_chatgpt_auth; +use codex_config::types::OtelExporterKind; +use codex_config::types::OtelHttpProtocol; use codex_core::config::ConfigBuilder; -use codex_core::config::types::OtelExporterKind; -use codex_core::config::types::OtelHttpProtocol; use codex_login::AuthCredentialsStoreMode; use pretty_assertions::assert_eq; use serde_json::Value; diff --git a/codex-rs/cli/src/mcp_cmd.rs b/codex-rs/cli/src/mcp_cmd.rs index ca89984337..144316b50e 100644 --- a/codex-rs/cli/src/mcp_cmd.rs +++ b/codex-rs/cli/src/mcp_cmd.rs @@ -6,12 +6,12 @@ use anyhow::Result; use anyhow::anyhow; use anyhow::bail; use clap::ArgGroup; +use codex_config::types::McpServerConfig; +use codex_config::types::McpServerTransportConfig; use codex_core::config::Config; use codex_core::config::edit::ConfigEditsBuilder; use codex_core::config::find_codex_home; use codex_core::config::load_global_mcp_servers; -use codex_core::config::types::McpServerConfig; -use codex_core::config::types::McpServerTransportConfig; use codex_core::mcp::McpManager; use codex_core::plugins::PluginsManager; use codex_mcp::mcp::auth::McpOAuthLoginSupport; diff --git a/codex-rs/cli/tests/mcp_add_remove.rs b/codex-rs/cli/tests/mcp_add_remove.rs index bc3fedc2a7..15afaf0828 100644 --- a/codex-rs/cli/tests/mcp_add_remove.rs +++ b/codex-rs/cli/tests/mcp_add_remove.rs @@ -1,8 +1,8 @@ use std::path::Path; use anyhow::Result; +use codex_config::types::McpServerTransportConfig; use codex_core::config::load_global_mcp_servers; -use codex_core::config::types::McpServerTransportConfig; use predicates::str::contains; use pretty_assertions::assert_eq; use tempfile::TempDir; diff --git a/codex-rs/cli/tests/mcp_list.rs b/codex-rs/cli/tests/mcp_list.rs index cb78644b1b..d41a3cc62a 100644 --- a/codex-rs/cli/tests/mcp_list.rs +++ b/codex-rs/cli/tests/mcp_list.rs @@ -1,9 +1,9 @@ use std::path::Path; use anyhow::Result; +use codex_config::types::McpServerTransportConfig; use codex_core::config::edit::ConfigEditsBuilder; use codex_core::config::load_global_mcp_servers; -use codex_core::config::types::McpServerTransportConfig; use predicates::prelude::PredicateBooleanExt; use predicates::str::contains; use pretty_assertions::assert_eq; diff --git a/codex-rs/config-schema/BUILD.bazel b/codex-rs/config-schema/BUILD.bazel index 2cb45d457a..b9e5b8a835 100644 --- a/codex-rs/config-schema/BUILD.bazel +++ b/codex-rs/config-schema/BUILD.bazel @@ -3,7 +3,7 @@ load("//:defs.bzl", "codex_rust_crate") codex_rust_crate( name = "config-schema", crate_name = "codex_config_schema", - compile_data = ["src/schema_presentation_template.json"], + compile_data = ["//codex-rs/core:config.schema.json"], rustc_env = { "CARGO_MANIFEST_DIR": "codex-rs/config-schema", }, diff --git a/codex-rs/config-schema/src/lib.rs b/codex-rs/config-schema/src/lib.rs index dbb851b426..ad149f60ac 100644 --- a/codex-rs/config-schema/src/lib.rs +++ b/codex-rs/config-schema/src/lib.rs @@ -10,8 +10,6 @@ use serde_json::Map; use serde_json::Value; use std::path::Path; -const SCHEMA_PRESENTATION_TEMPLATE_JSON: &str = include_str!("schema_presentation_template.json"); - pub use config_toml::mcp_servers_schema; pub use features::features_schema; @@ -61,7 +59,9 @@ pub fn write_config_schema(out_path: &Path) -> anyhow::Result<()> { } fn preserve_schema_presentation(value: Value) -> anyhow::Result { - let template = serde_json::from_str(SCHEMA_PRESENTATION_TEMPLATE_JSON)?; + let template_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../core/config.schema.json"); + let template = std::fs::read_to_string(template_path)?; + let template = serde_json::from_str(&template)?; Ok(preserve_schema_presentation_from_template(value, &template)) } diff --git a/codex-rs/config-schema/src/schema_presentation_template.json b/codex-rs/config-schema/src/schema_presentation_template.json deleted file mode 100644 index 7e6a59690a..0000000000 --- a/codex-rs/config-schema/src/schema_presentation_template.json +++ /dev/null @@ -1,2666 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "definitions": { - "AbsolutePathBuf": { - "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - "type": "string" - }, - "AgentRoleToml": { - "additionalProperties": false, - "properties": { - "config_file": { - "allOf": [ - { - "$ref": "#/definitions/AbsolutePathBuf" - } - ], - "description": "Path to a role-specific config layer. Relative paths are resolved relative to the `config.toml` that defines them." - }, - "description": { - "description": "Human-facing role documentation used in spawn tool guidance. Required unless supplied by the referenced agent role file.", - "type": "string" - }, - "nickname_candidates": { - "description": "Candidate nicknames for agents spawned with this role.", - "items": { - "type": "string" - }, - "type": "array" - } - }, - "type": "object" - }, - "AgentsToml": { - "additionalProperties": { - "$ref": "#/definitions/AgentRoleToml" - }, - "properties": { - "job_max_runtime_seconds": { - "description": "Default maximum runtime in seconds for agent job workers.", - "format": "uint64", - "minimum": 1.0, - "type": "integer" - }, - "max_depth": { - "description": "Maximum nesting depth allowed for spawned agent threads. Root sessions start at depth 0.", - "format": "int32", - "minimum": 1.0, - "type": "integer" - }, - "max_threads": { - "description": "Maximum number of agent threads that can be open concurrently. When unset, no limit is enforced.", - "format": "uint", - "minimum": 1.0, - "type": "integer" - } - }, - "type": "object" - }, - "AltScreenMode": { - "description": "Controls whether the TUI uses the terminal's alternate screen buffer.\n\n**Background:** The alternate screen buffer provides a cleaner fullscreen experience without polluting the terminal's scrollback history. However, it conflicts with terminal multiplexers like Zellij that strictly follow the xterm specification, which defines that alternate screen buffers should not have scrollback.\n\n**Zellij's behavior:** Zellij intentionally disables scrollback in alternate screen mode (see https://github.com/zellij-org/zellij/pull/1032) to comply with the xterm spec. This is by design and not configurable in Zellij—there is no option to enable scrollback in alternate screen mode.\n\n**Solution:** This setting provides a pragmatic workaround: - `auto` (default): Automatically detect the terminal multiplexer. If running in Zellij, disable alternate screen to preserve scrollback. Enable it everywhere else. - `always`: Always use alternate screen mode (original behavior before this fix). - `never`: Never use alternate screen mode. Runs in inline mode, preserving scrollback in all multiplexers.\n\nThe CLI flag `--no-alt-screen` can override this setting at runtime.", - "oneOf": [ - { - "description": "Auto-detect: disable alternate screen in Zellij, enable elsewhere.", - "enum": [ - "auto" - ], - "type": "string" - }, - { - "description": "Always use alternate screen (original behavior).", - "enum": [ - "always" - ], - "type": "string" - }, - { - "description": "Never use alternate screen (inline mode only).", - "enum": [ - "never" - ], - "type": "string" - } - ] - }, - "AnalyticsConfigToml": { - "additionalProperties": false, - "description": "Analytics settings loaded from config.toml. Fields are optional so we can apply defaults.", - "properties": { - "enabled": { - "description": "When `false`, disables analytics across Codex product surfaces in this profile.", - "type": "boolean" - } - }, - "type": "object" - }, - "AppConfig": { - "additionalProperties": false, - "description": "Config values for a single app/connector.", - "properties": { - "default_tools_approval_mode": { - "allOf": [ - { - "$ref": "#/definitions/AppToolApproval" - } - ], - "description": "Approval mode for tools in this app unless a tool override exists." - }, - "default_tools_enabled": { - "description": "Whether tools are enabled by default for this app.", - "type": "boolean" - }, - "destructive_enabled": { - "description": "Whether tools with `destructive_hint = true` are allowed for this app.", - "type": "boolean" - }, - "enabled": { - "default": true, - "description": "When `false`, Codex does not surface this app.", - "type": "boolean" - }, - "open_world_enabled": { - "description": "Whether tools with `open_world_hint = true` are allowed for this app.", - "type": "boolean" - }, - "tools": { - "allOf": [ - { - "$ref": "#/definitions/AppToolsConfig" - } - ], - "description": "Per-tool settings for this app." - } - }, - "type": "object" - }, - "AppToolApproval": { - "enum": [ - "auto", - "prompt", - "approve" - ], - "type": "string" - }, - "AppToolConfig": { - "additionalProperties": false, - "description": "Per-tool settings for a single app tool.", - "properties": { - "approval_mode": { - "allOf": [ - { - "$ref": "#/definitions/AppToolApproval" - } - ], - "description": "Approval mode for this tool." - }, - "enabled": { - "description": "Whether this tool is enabled. `Some(true)` explicitly allows this tool.", - "type": "boolean" - } - }, - "type": "object" - }, - "AppToolsConfig": { - "additionalProperties": { - "$ref": "#/definitions/AppToolConfig" - }, - "description": "Tool settings for a single app.", - "type": "object" - }, - "ApprovalsReviewer": { - "description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `guardian_subagent` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request.", - "enum": [ - "user", - "guardian_subagent" - ], - "type": "string" - }, - "AppsConfigToml": { - "additionalProperties": { - "$ref": "#/definitions/AppConfig" - }, - "description": "App/connector settings loaded from `config.toml`.", - "properties": { - "_default": { - "allOf": [ - { - "$ref": "#/definitions/AppsDefaultConfig" - } - ], - "description": "Default settings for all apps." - } - }, - "type": "object" - }, - "AppsDefaultConfig": { - "additionalProperties": false, - "description": "Default settings that apply to all apps.", - "properties": { - "destructive_enabled": { - "description": "Whether tools with `destructive_hint = true` are allowed by default.", - "type": "boolean" - }, - "enabled": { - "default": true, - "description": "When `false`, apps are disabled unless overridden by per-app settings.", - "type": "boolean" - }, - "open_world_enabled": { - "description": "Whether tools with `open_world_hint = true` are allowed by default.", - "type": "boolean" - } - }, - "type": "object" - }, - "AskForApproval": { - "description": "Determines the conditions under which the user is consulted to approve running the command proposed by Codex.", - "oneOf": [ - { - "description": "Under this policy, only \"known safe\" commands—as determined by `is_safe_command()`—that **only read files** are auto‑approved. Everything else will ask the user to approve.", - "enum": [ - "untrusted" - ], - "type": "string" - }, - { - "description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.", - "enum": [ - "on-failure" - ], - "type": "string" - }, - { - "description": "The model decides when to ask the user for approval.", - "enum": [ - "on-request" - ], - "type": "string" - }, - { - "additionalProperties": false, - "description": "Fine-grained controls for individual approval flows.\n\nWhen a field is `true`, commands in that category are allowed. When it is `false`, those requests are automatically rejected instead of shown to the user.", - "properties": { - "granular": { - "$ref": "#/definitions/GranularApprovalConfig" - } - }, - "required": [ - "granular" - ], - "type": "object" - }, - { - "description": "Never ask the user to approve commands. Failures are immediately returned to the model, and never escalated to the user for approval.", - "enum": [ - "never" - ], - "type": "string" - } - ] - }, - "AuthCredentialsStoreMode": { - "description": "Determine where Codex should store CLI auth credentials.", - "oneOf": [ - { - "description": "Persist credentials in CODEX_HOME/auth.json.", - "enum": [ - "file" - ], - "type": "string" - }, - { - "description": "Persist credentials in the keyring. Fail if unavailable.", - "enum": [ - "keyring" - ], - "type": "string" - }, - { - "description": "Use keyring when available; otherwise, fall back to a file in CODEX_HOME.", - "enum": [ - "auto" - ], - "type": "string" - }, - { - "description": "Store credentials in memory only for the current process.", - "enum": [ - "ephemeral" - ], - "type": "string" - } - ] - }, - "BundledSkillsConfig": { - "additionalProperties": false, - "properties": { - "enabled": { - "default": true, - "type": "boolean" - } - }, - "type": "object" - }, - "ConfigProfile": { - "additionalProperties": false, - "description": "Collection of common configuration options that a user can define as a unit in `config.toml`.", - "properties": { - "analytics": { - "$ref": "#/definitions/AnalyticsConfigToml" - }, - "approval_policy": { - "$ref": "#/definitions/AskForApproval" - }, - "approvals_reviewer": { - "$ref": "#/definitions/ApprovalsReviewer" - }, - "chatgpt_base_url": { - "type": "string" - }, - "experimental_compact_prompt_file": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "experimental_use_freeform_apply_patch": { - "type": "boolean" - }, - "experimental_use_unified_exec_tool": { - "type": "boolean" - }, - "features": { - "additionalProperties": false, - "default": null, - "description": "Optional feature toggles scoped to this profile.", - "properties": { - "apply_patch_freeform": { - "type": "boolean" - }, - "apps": { - "type": "boolean" - }, - "child_agents_md": { - "type": "boolean" - }, - "code_mode": { - "type": "boolean" - }, - "code_mode_only": { - "type": "boolean" - }, - "codex_git_commit": { - "type": "boolean" - }, - "codex_hooks": { - "type": "boolean" - }, - "collab": { - "type": "boolean" - }, - "collaboration_modes": { - "type": "boolean" - }, - "connectors": { - "type": "boolean" - }, - "default_mode_request_user_input": { - "type": "boolean" - }, - "elevated_windows_sandbox": { - "type": "boolean" - }, - "enable_experimental_windows_sandbox": { - "type": "boolean" - }, - "enable_fanout": { - "type": "boolean" - }, - "enable_request_compression": { - "type": "boolean" - }, - "exec_permission_approvals": { - "type": "boolean" - }, - "experimental_use_freeform_apply_patch": { - "type": "boolean" - }, - "experimental_use_unified_exec_tool": { - "type": "boolean" - }, - "experimental_windows_sandbox": { - "type": "boolean" - }, - "fast_mode": { - "type": "boolean" - }, - "general_analytics": { - "type": "boolean" - }, - "guardian_approval": { - "type": "boolean" - }, - "image_detail_original": { - "type": "boolean" - }, - "image_generation": { - "type": "boolean" - }, - "include_apply_patch_tool": { - "type": "boolean" - }, - "js_repl": { - "type": "boolean" - }, - "js_repl_tools_only": { - "type": "boolean" - }, - "memories": { - "type": "boolean" - }, - "memory_tool": { - "type": "boolean" - }, - "multi_agent": { - "type": "boolean" - }, - "multi_agent_v2": { - "type": "boolean" - }, - "personality": { - "type": "boolean" - }, - "plugins": { - "type": "boolean" - }, - "prevent_idle_sleep": { - "type": "boolean" - }, - "realtime_conversation": { - "type": "boolean" - }, - "remote_models": { - "type": "boolean" - }, - "request_permissions": { - "type": "boolean" - }, - "request_permissions_tool": { - "type": "boolean" - }, - "request_rule": { - "type": "boolean" - }, - "responses_websockets": { - "type": "boolean" - }, - "responses_websockets_v2": { - "type": "boolean" - }, - "runtime_metrics": { - "type": "boolean" - }, - "search_tool": { - "type": "boolean" - }, - "shell_snapshot": { - "type": "boolean" - }, - "shell_tool": { - "type": "boolean" - }, - "shell_zsh_fork": { - "type": "boolean" - }, - "skill_env_var_dependency_prompt": { - "type": "boolean" - }, - "skill_mcp_dependency_install": { - "type": "boolean" - }, - "sqlite": { - "type": "boolean" - }, - "steer": { - "type": "boolean" - }, - "tool_call_mcp_elicitation": { - "type": "boolean" - }, - "tool_search": { - "type": "boolean" - }, - "tool_suggest": { - "type": "boolean" - }, - "tui_app_server": { - "type": "boolean" - }, - "undo": { - "type": "boolean" - }, - "unified_exec": { - "type": "boolean" - }, - "use_legacy_landlock": { - "type": "boolean" - }, - "use_linux_sandbox_bwrap": { - "type": "boolean" - }, - "web_search": { - "type": "boolean" - }, - "web_search_cached": { - "type": "boolean" - }, - "web_search_request": { - "type": "boolean" - } - }, - "type": "object" - }, - "include_apply_patch_tool": { - "type": "boolean" - }, - "js_repl_node_module_dirs": { - "description": "Ordered list of directories to search for Node modules in `js_repl`.", - "items": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": "array" - }, - "js_repl_node_path": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "model": { - "type": "string" - }, - "model_catalog_json": { - "allOf": [ - { - "$ref": "#/definitions/AbsolutePathBuf" - } - ], - "description": "Optional path to a JSON model catalog (applied on startup only)." - }, - "model_instructions_file": { - "allOf": [ - { - "$ref": "#/definitions/AbsolutePathBuf" - } - ], - "description": "Optional path to a file containing model instructions." - }, - "model_provider": { - "description": "The key in the `model_providers` map identifying the [`ModelProviderInfo`] to use.", - "type": "string" - }, - "model_reasoning_effort": { - "$ref": "#/definitions/ReasoningEffort" - }, - "model_reasoning_summary": { - "$ref": "#/definitions/ReasoningSummary" - }, - "model_verbosity": { - "$ref": "#/definitions/Verbosity" - }, - "oss_provider": { - "type": "string" - }, - "personality": { - "$ref": "#/definitions/Personality" - }, - "plan_mode_reasoning_effort": { - "$ref": "#/definitions/ReasoningEffort" - }, - "sandbox_mode": { - "$ref": "#/definitions/SandboxMode" - }, - "service_tier": { - "$ref": "#/definitions/ServiceTier" - }, - "tools": { - "$ref": "#/definitions/ToolsToml" - }, - "tools_view_image": { - "type": "boolean" - }, - "web_search": { - "$ref": "#/definitions/WebSearchMode" - }, - "windows": { - "allOf": [ - { - "$ref": "#/definitions/WindowsToml" - } - ], - "default": null - }, - "zsh_path": { - "allOf": [ - { - "$ref": "#/definitions/AbsolutePathBuf" - } - ], - "description": "Optional absolute path to patched zsh used by zsh-exec-bridge-backed shell execution." - } - }, - "type": "object" - }, - "CustomModelToml": { - "additionalProperties": false, - "properties": { - "model": { - "description": "Provider-facing model slug used on API requests.", - "type": "string" - }, - "model_auto_compact_token_limit": { - "description": "Optional auto-compaction token limit override applied when this alias is selected.", - "format": "int64", - "type": "integer" - }, - "model_context_window": { - "description": "Optional context window override applied when this alias is selected.", - "format": "int64", - "type": "integer" - }, - "name": { - "description": "User-facing alias shown in the model picker.", - "type": "string" - } - }, - "required": [ - "model", - "name" - ], - "type": "object" - }, - "FeedbackConfigToml": { - "additionalProperties": false, - "properties": { - "enabled": { - "description": "When `false`, disables the feedback flow across Codex product surfaces.", - "type": "boolean" - } - }, - "type": "object" - }, - "FileSystemAccessMode": { - "description": "Access mode for a filesystem entry.\n\nWhen two equally specific entries target the same path, we compare these by conflict precedence rather than by capability breadth: `none` beats `write`, and `write` beats `read`.", - "enum": [ - "read", - "write", - "none" - ], - "type": "string" - }, - "FilesystemPermissionToml": { - "anyOf": [ - { - "$ref": "#/definitions/FileSystemAccessMode" - }, - { - "additionalProperties": { - "$ref": "#/definitions/FileSystemAccessMode" - }, - "type": "object" - } - ] - }, - "FilesystemPermissionsToml": { - "type": "object" - }, - "ForcedLoginMethod": { - "enum": [ - "chatgpt", - "api" - ], - "type": "string" - }, - "GhostSnapshotToml": { - "additionalProperties": false, - "properties": { - "disable_warnings": { - "description": "Disable all ghost snapshot warning events.", - "type": "boolean" - }, - "ignore_large_untracked_dirs": { - "description": "Ignore untracked directories that contain this many files or more. (Still emits a warning unless warnings are disabled.)", - "format": "int64", - "type": "integer" - }, - "ignore_large_untracked_files": { - "description": "Exclude untracked files larger than this many bytes from ghost snapshots.", - "format": "int64", - "type": "integer" - } - }, - "type": "object" - }, - "GranularApprovalConfig": { - "properties": { - "mcp_elicitations": { - "description": "Whether to allow MCP elicitation prompts.", - "type": "boolean" - }, - "request_permissions": { - "default": false, - "description": "Whether to allow prompts triggered by the `request_permissions` tool.", - "type": "boolean" - }, - "rules": { - "description": "Whether to allow prompts triggered by execpolicy `prompt` rules.", - "type": "boolean" - }, - "sandbox_approval": { - "description": "Whether to allow shell command approval requests, including inline `with_additional_permissions` and `require_escalated` requests.", - "type": "boolean" - }, - "skill_approval": { - "default": false, - "description": "Whether to allow approval prompts triggered by skill script execution.", - "type": "boolean" - } - }, - "required": [ - "mcp_elicitations", - "rules", - "sandbox_approval" - ], - "type": "object" - }, - "History": { - "additionalProperties": false, - "description": "Settings that govern if and what will be written to `~/.codex/history.jsonl`.", - "properties": { - "max_bytes": { - "description": "If set, the maximum size of the history file in bytes. The oldest entries are dropped once the file exceeds this limit.", - "format": "uint", - "minimum": 0.0, - "type": "integer" - }, - "persistence": { - "allOf": [ - { - "$ref": "#/definitions/HistoryPersistence" - } - ], - "description": "If true, history entries will not be written to disk." - } - }, - "required": [ - "persistence" - ], - "type": "object" - }, - "HistoryPersistence": { - "oneOf": [ - { - "description": "Save all history entries to disk.", - "enum": [ - "save-all" - ], - "type": "string" - }, - { - "description": "Do not write history to disk.", - "enum": [ - "none" - ], - "type": "string" - } - ] - }, - "McpServerToolConfig": { - "additionalProperties": false, - "description": "Per-tool approval settings for a single MCP server tool.", - "properties": { - "approval_mode": { - "allOf": [ - { - "$ref": "#/definitions/AppToolApproval" - } - ], - "description": "Approval mode for this tool." - } - }, - "type": "object" - }, - "MemoriesToml": { - "additionalProperties": false, - "description": "Memories settings loaded from config.toml.", - "properties": { - "consolidation_model": { - "description": "Model used for memory consolidation.", - "type": "string" - }, - "extract_model": { - "description": "Model used for thread summarisation.", - "type": "string" - }, - "generate_memories": { - "description": "When `false`, newly created threads are stored with `memory_mode = \"disabled\"` in the state DB.", - "type": "boolean" - }, - "max_raw_memories_for_consolidation": { - "description": "Maximum number of recent raw memories retained for global consolidation.", - "format": "uint", - "minimum": 0.0, - "type": "integer" - }, - "max_rollout_age_days": { - "description": "Maximum age of the threads used for memories.", - "format": "int64", - "type": "integer" - }, - "max_rollouts_per_startup": { - "description": "Maximum number of rollout candidates processed per pass.", - "format": "uint", - "minimum": 0.0, - "type": "integer" - }, - "max_unused_days": { - "description": "Maximum number of days since a memory was last used before it becomes ineligible for phase 2 selection.", - "format": "int64", - "type": "integer" - }, - "min_rollout_idle_hours": { - "description": "Minimum idle time between last thread activity and memory creation (hours). > 12h recommended.", - "format": "int64", - "type": "integer" - }, - "no_memories_if_mcp_or_web_search": { - "description": "When `true`, web searches and MCP tool calls mark the thread `memory_mode` as `\"polluted\"`.", - "type": "boolean" - }, - "use_memories": { - "description": "When `false`, skip injecting memory usage instructions into developer prompts.", - "type": "boolean" - } - }, - "type": "object" - }, - "ModelAvailabilityNuxConfig": { - "additionalProperties": { - "format": "uint32", - "minimum": 0.0, - "type": "integer" - }, - "type": "object" - }, - "ModelProviderAuthInfo": { - "additionalProperties": false, - "description": "Configuration for obtaining a provider bearer token from a command.", - "properties": { - "args": { - "default": [], - "description": "Command arguments.", - "items": { - "type": "string" - }, - "type": "array" - }, - "command": { - "description": "Command to execute. Bare names are resolved via `PATH`; paths are resolved against `cwd`.", - "type": "string" - }, - "cwd": { - "allOf": [ - { - "$ref": "#/definitions/AbsolutePathBuf" - } - ], - "description": "Working directory used when running the token command." - }, - "refresh_interval_ms": { - "default": 300000, - "description": "Maximum age for the cached token before rerunning the command. Set to `0` to disable proactive refresh and only rerun after a 401 retry path.", - "format": "uint64", - "minimum": 0.0, - "type": "integer" - }, - "timeout_ms": { - "default": 5000, - "description": "Maximum time to wait for the token command to exit successfully.", - "format": "uint64", - "minimum": 1.0, - "type": "integer" - } - }, - "required": [ - "command" - ], - "type": "object" - }, - "ModelProviderInfo": { - "additionalProperties": false, - "description": "Serializable representation of a provider definition.", - "properties": { - "auth": { - "allOf": [ - { - "$ref": "#/definitions/ModelProviderAuthInfo" - } - ], - "description": "Command-backed bearer-token configuration for this provider." - }, - "base_url": { - "description": "Base URL for the provider's OpenAI-compatible API.", - "type": "string" - }, - "env_http_headers": { - "additionalProperties": { - "type": "string" - }, - "description": "Optional HTTP headers to include in requests to this provider where the (key, value) pairs are the header name and _environment variable_ whose value should be used. If the environment variable is not set, or the value is empty, the header will not be included in the request.", - "type": "object" - }, - "env_key": { - "description": "Environment variable that stores the user's API key for this provider.", - "type": "string" - }, - "env_key_instructions": { - "description": "Optional instructions to help the user get a valid value for the variable and set it.", - "type": "string" - }, - "experimental_bearer_token": { - "description": "Value to use with `Authorization: Bearer ` header. Use of this config is discouraged in favor of `env_key` for security reasons, but this may be necessary when using this programmatically.", - "type": "string" - }, - "http_headers": { - "additionalProperties": { - "type": "string" - }, - "description": "Additional HTTP headers to include in requests to this provider where the (key, value) pairs are the header name and value.", - "type": "object" - }, - "name": { - "description": "Friendly display name.", - "type": "string" - }, - "query_params": { - "additionalProperties": { - "type": "string" - }, - "description": "Optional query parameters to append to the base URL.", - "type": "object" - }, - "request_max_retries": { - "description": "Maximum number of times to retry a failed HTTP request to this provider.", - "format": "uint64", - "minimum": 0.0, - "type": "integer" - }, - "requires_openai_auth": { - "default": false, - "description": "Does this provider require an OpenAI API Key or ChatGPT login token? If true, user is presented with login screen on first run, and login preference and token/key are stored in auth.json. If false (which is the default), login screen is skipped, and API key (if needed) comes from the \"env_key\" environment variable.", - "type": "boolean" - }, - "stream_idle_timeout_ms": { - "description": "Idle timeout (in milliseconds) to wait for activity on a streaming response before treating the connection as lost.", - "format": "uint64", - "minimum": 0.0, - "type": "integer" - }, - "stream_max_retries": { - "description": "Number of times to retry reconnecting a dropped streaming response before failing.", - "format": "uint64", - "minimum": 0.0, - "type": "integer" - }, - "supports_websockets": { - "default": false, - "description": "Whether this provider supports the Responses API WebSocket transport.", - "type": "boolean" - }, - "websocket_connect_timeout_ms": { - "description": "Maximum time (in milliseconds) to wait for a websocket connection attempt before treating it as failed.", - "format": "uint64", - "minimum": 0.0, - "type": "integer" - }, - "wire_api": { - "allOf": [ - { - "$ref": "#/definitions/WireApi" - } - ], - "default": "responses", - "description": "Which wire protocol this provider expects." - } - }, - "required": [ - "name" - ], - "type": "object" - }, - "NetworkDomainPermissionToml": { - "enum": [ - "allow", - "deny" - ], - "type": "string" - }, - "NetworkDomainPermissionsToml": { - "type": "object" - }, - "NetworkModeSchema": { - "enum": [ - "limited", - "full" - ], - "type": "string" - }, - "NetworkToml": { - "additionalProperties": false, - "properties": { - "allow_local_binding": { - "type": "boolean" - }, - "allow_upstream_proxy": { - "type": "boolean" - }, - "dangerously_allow_all_unix_sockets": { - "type": "boolean" - }, - "dangerously_allow_non_loopback_proxy": { - "type": "boolean" - }, - "domains": { - "$ref": "#/definitions/NetworkDomainPermissionsToml" - }, - "enable_socks5": { - "type": "boolean" - }, - "enable_socks5_udp": { - "type": "boolean" - }, - "enabled": { - "type": "boolean" - }, - "mode": { - "$ref": "#/definitions/NetworkModeSchema" - }, - "proxy_url": { - "type": "string" - }, - "socks_url": { - "type": "string" - }, - "unix_sockets": { - "$ref": "#/definitions/NetworkUnixSocketPermissionsToml" - } - }, - "type": "object" - }, - "NetworkUnixSocketPermissionToml": { - "enum": [ - "allow", - "none" - ], - "type": "string" - }, - "NetworkUnixSocketPermissionsToml": { - "type": "object" - }, - "Notice": { - "description": "Settings for notices we display to users via the tui and app-server clients (primarily the Codex IDE extension). NOTE: these are different from notifications - notices are warnings, NUX screens, acknowledgements, etc.", - "properties": { - "hide_full_access_warning": { - "description": "Tracks whether the user has acknowledged the full access warning prompt.", - "type": "boolean" - }, - "hide_gpt-5.1-codex-max_migration_prompt": { - "description": "Tracks whether the user has seen the gpt-5.1-codex-max migration prompt", - "type": "boolean" - }, - "hide_gpt5_1_migration_prompt": { - "description": "Tracks whether the user has seen the model migration prompt", - "type": "boolean" - }, - "hide_rate_limit_model_nudge": { - "description": "Tracks whether the user opted out of the rate limit model switch reminder.", - "type": "boolean" - }, - "hide_world_writable_warning": { - "description": "Tracks whether the user has acknowledged the Windows world-writable directories warning.", - "type": "boolean" - }, - "model_migrations": { - "additionalProperties": { - "type": "string" - }, - "default": {}, - "description": "Tracks acknowledged model migrations as old->new model slug mappings.", - "type": "object" - } - }, - "type": "object" - }, - "NotificationMethod": { - "enum": [ - "auto", - "osc9", - "bel" - ], - "type": "string" - }, - "Notifications": { - "anyOf": [ - { - "type": "boolean" - }, - { - "items": { - "type": "string" - }, - "type": "array" - } - ] - }, - "OAuthCredentialsStoreMode": { - "description": "Determine where Codex should store and read MCP credentials.", - "oneOf": [ - { - "description": "`Keyring` when available; otherwise, `File`. Credentials stored in the keyring will only be readable by Codex unless the user explicitly grants access via OS-level keyring access.", - "enum": [ - "auto" - ], - "type": "string" - }, - { - "description": "CODEX_HOME/.credentials.json This file will be readable to Codex and other applications running as the same user.", - "enum": [ - "file" - ], - "type": "string" - }, - { - "description": "Keyring when available, otherwise fail.", - "enum": [ - "keyring" - ], - "type": "string" - } - ] - }, - "OtelConfigToml": { - "additionalProperties": false, - "description": "OTEL settings loaded from config.toml. Fields are optional so we can apply defaults.", - "properties": { - "environment": { - "description": "Mark traces with environment (dev, staging, prod, test). Defaults to dev.", - "type": "string" - }, - "exporter": { - "allOf": [ - { - "$ref": "#/definitions/OtelExporterKind" - } - ], - "description": "Optional log exporter" - }, - "log_user_prompt": { - "description": "Log user prompt in traces", - "type": "boolean" - }, - "metrics_exporter": { - "allOf": [ - { - "$ref": "#/definitions/OtelExporterKind" - } - ], - "description": "Optional metrics exporter" - }, - "trace_exporter": { - "allOf": [ - { - "$ref": "#/definitions/OtelExporterKind" - } - ], - "description": "Optional trace exporter" - } - }, - "type": "object" - }, - "OtelExporterKind": { - "description": "Which OTEL exporter to use.", - "oneOf": [ - { - "enum": [ - "none", - "statsig" - ], - "type": "string" - }, - { - "additionalProperties": false, - "properties": { - "otlp-http": { - "additionalProperties": false, - "properties": { - "endpoint": { - "type": "string" - }, - "headers": { - "additionalProperties": { - "type": "string" - }, - "default": {}, - "type": "object" - }, - "protocol": { - "$ref": "#/definitions/OtelHttpProtocol" - }, - "tls": { - "allOf": [ - { - "$ref": "#/definitions/OtelTlsConfig" - } - ], - "default": null - } - }, - "required": [ - "endpoint", - "protocol" - ], - "type": "object" - } - }, - "required": [ - "otlp-http" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "otlp-grpc": { - "additionalProperties": false, - "properties": { - "endpoint": { - "type": "string" - }, - "headers": { - "additionalProperties": { - "type": "string" - }, - "default": {}, - "type": "object" - }, - "tls": { - "allOf": [ - { - "$ref": "#/definitions/OtelTlsConfig" - } - ], - "default": null - } - }, - "required": [ - "endpoint" - ], - "type": "object" - } - }, - "required": [ - "otlp-grpc" - ], - "type": "object" - } - ] - }, - "OtelHttpProtocol": { - "oneOf": [ - { - "description": "Binary payload", - "enum": [ - "binary" - ], - "type": "string" - }, - { - "description": "JSON payload", - "enum": [ - "json" - ], - "type": "string" - } - ] - }, - "OtelTlsConfig": { - "additionalProperties": false, - "properties": { - "ca-certificate": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "client-certificate": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "client-private-key": { - "$ref": "#/definitions/AbsolutePathBuf" - } - }, - "type": "object" - }, - "PermissionProfileToml": { - "additionalProperties": false, - "properties": { - "filesystem": { - "$ref": "#/definitions/FilesystemPermissionsToml" - }, - "network": { - "$ref": "#/definitions/NetworkToml" - } - }, - "type": "object" - }, - "PermissionsToml": { - "type": "object" - }, - "Personality": { - "enum": [ - "none", - "friendly", - "pragmatic" - ], - "type": "string" - }, - "PluginConfig": { - "additionalProperties": false, - "properties": { - "enabled": { - "default": true, - "type": "boolean" - } - }, - "type": "object" - }, - "ProjectConfig": { - "additionalProperties": false, - "properties": { - "trust_level": { - "$ref": "#/definitions/TrustLevel" - } - }, - "type": "object" - }, - "RawMcpServerConfig": { - "additionalProperties": false, - "description": "Raw MCP config shape used for deserialization and JSON Schema generation.\n\nKeep `TryFrom for McpServerConfig` exhaustively destructuring this struct so new TOML fields cannot be added here without updating the validation/mapping logic that produces [`McpServerConfig`].", - "properties": { - "args": { - "default": null, - "items": { - "type": "string" - }, - "type": "array" - }, - "bearer_token": { - "type": "string" - }, - "bearer_token_env_var": { - "type": "string" - }, - "command": { - "type": "string" - }, - "cwd": { - "default": null, - "type": "string" - }, - "disabled_tools": { - "default": null, - "items": { - "type": "string" - }, - "type": "array" - }, - "enabled": { - "default": null, - "type": "boolean" - }, - "enabled_tools": { - "default": null, - "items": { - "type": "string" - }, - "type": "array" - }, - "env": { - "additionalProperties": { - "type": "string" - }, - "default": null, - "type": "object" - }, - "env_http_headers": { - "additionalProperties": { - "type": "string" - }, - "default": null, - "type": "object" - }, - "env_vars": { - "default": null, - "items": { - "type": "string" - }, - "type": "array" - }, - "http_headers": { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - "name": { - "default": null, - "description": "Legacy display-name field accepted for backward compatibility.", - "type": "string" - }, - "oauth_resource": { - "default": null, - "type": "string" - }, - "required": { - "default": null, - "type": "boolean" - }, - "scopes": { - "default": null, - "items": { - "type": "string" - }, - "type": "array" - }, - "startup_timeout_ms": { - "default": null, - "format": "uint64", - "minimum": 0.0, - "type": "integer" - }, - "startup_timeout_sec": { - "default": null, - "format": "double", - "type": "number" - }, - "tool_timeout_sec": { - "default": null, - "format": "double", - "type": "number" - }, - "tools": { - "additionalProperties": { - "$ref": "#/definitions/McpServerToolConfig" - }, - "default": null, - "type": "object" - }, - "url": { - "type": "string" - } - }, - "type": "object" - }, - "RealtimeAudioToml": { - "additionalProperties": false, - "properties": { - "microphone": { - "type": "string" - }, - "speaker": { - "type": "string" - } - }, - "type": "object" - }, - "RealtimeConversationVersion": { - "enum": [ - "v1", - "v2" - ], - "type": "string" - }, - "RealtimeToml": { - "additionalProperties": false, - "properties": { - "type": { - "$ref": "#/definitions/RealtimeWsMode" - }, - "version": { - "$ref": "#/definitions/RealtimeConversationVersion" - } - }, - "type": "object" - }, - "RealtimeWsMode": { - "enum": [ - "conversational", - "transcription" - ], - "type": "string" - }, - "ReasoningEffort": { - "description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", - "enum": [ - "none", - "minimal", - "low", - "medium", - "high", - "xhigh" - ], - "type": "string" - }, - "ReasoningSummary": { - "description": "A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries", - "oneOf": [ - { - "enum": [ - "auto", - "concise", - "detailed" - ], - "type": "string" - }, - { - "description": "Option to disable reasoning summaries.", - "enum": [ - "none" - ], - "type": "string" - } - ] - }, - "SandboxMode": { - "enum": [ - "read-only", - "workspace-write", - "danger-full-access" - ], - "type": "string" - }, - "SandboxWorkspaceWrite": { - "additionalProperties": false, - "properties": { - "exclude_slash_tmp": { - "default": false, - "type": "boolean" - }, - "exclude_tmpdir_env_var": { - "default": false, - "type": "boolean" - }, - "network_access": { - "default": false, - "type": "boolean" - }, - "writable_roots": { - "default": [], - "items": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": "array" - } - }, - "type": "object" - }, - "ServiceTier": { - "enum": [ - "fast", - "flex" - ], - "type": "string" - }, - "ShellEnvironmentPolicyInherit": { - "oneOf": [ - { - "description": "\"Core\" environment variables for the platform. On UNIX, this would include HOME, LOGNAME, PATH, SHELL, and USER, among others.", - "enum": [ - "core" - ], - "type": "string" - }, - { - "description": "Inherits the full environment from the parent process.", - "enum": [ - "all" - ], - "type": "string" - }, - { - "description": "Do not inherit any environment variables from the parent process.", - "enum": [ - "none" - ], - "type": "string" - } - ] - }, - "ShellEnvironmentPolicyToml": { - "additionalProperties": false, - "description": "Policy for building the `env` when spawning a process via either the `shell` or `local_shell` tool.", - "properties": { - "exclude": { - "description": "List of regular expressions.", - "items": { - "type": "string" - }, - "type": "array" - }, - "experimental_use_profile": { - "type": "boolean" - }, - "ignore_default_excludes": { - "type": "boolean" - }, - "include_only": { - "description": "List of regular expressions.", - "items": { - "type": "string" - }, - "type": "array" - }, - "inherit": { - "$ref": "#/definitions/ShellEnvironmentPolicyInherit" - }, - "set": { - "additionalProperties": { - "type": "string" - }, - "type": "object" - } - }, - "type": "object" - }, - "SkillConfig": { - "additionalProperties": false, - "properties": { - "enabled": { - "type": "boolean" - }, - "name": { - "description": "Name-based selector.", - "type": "string" - }, - "path": { - "allOf": [ - { - "$ref": "#/definitions/AbsolutePathBuf" - } - ], - "description": "Path-based selector." - } - }, - "required": [ - "enabled" - ], - "type": "object" - }, - "SkillsConfig": { - "additionalProperties": false, - "properties": { - "bundled": { - "$ref": "#/definitions/BundledSkillsConfig" - }, - "config": { - "items": { - "$ref": "#/definitions/SkillConfig" - }, - "type": "array" - } - }, - "type": "object" - }, - "ToolSuggestConfig": { - "additionalProperties": false, - "properties": { - "discoverables": { - "default": [], - "items": { - "$ref": "#/definitions/ToolSuggestDiscoverable" - }, - "type": "array" - } - }, - "type": "object" - }, - "ToolSuggestDiscoverable": { - "additionalProperties": false, - "properties": { - "id": { - "type": "string" - }, - "type": { - "$ref": "#/definitions/ToolSuggestDiscoverableType" - } - }, - "required": [ - "id", - "type" - ], - "type": "object" - }, - "ToolSuggestDiscoverableType": { - "enum": [ - "connector", - "plugin" - ], - "type": "string" - }, - "ToolsToml": { - "additionalProperties": false, - "properties": { - "view_image": { - "default": null, - "description": "Enable the `view_image` tool that lets the agent attach local images.", - "type": "boolean" - }, - "web_search": { - "allOf": [ - { - "$ref": "#/definitions/WebSearchToolConfig" - } - ], - "default": null - } - }, - "type": "object" - }, - "TrustLevel": { - "description": "Represents the trust level for a project directory. This determines the approval policy and sandbox mode applied.", - "enum": [ - "trusted", - "untrusted" - ], - "type": "string" - }, - "Tui": { - "additionalProperties": false, - "description": "Collection of settings that are specific to the TUI.", - "properties": { - "alternate_screen": { - "allOf": [ - { - "$ref": "#/definitions/AltScreenMode" - } - ], - "default": "auto", - "description": "Controls whether the TUI uses the terminal's alternate screen buffer.\n\n- `auto` (default): Disable alternate screen in Zellij, enable elsewhere. - `always`: Always use alternate screen (original behavior). - `never`: Never use alternate screen (inline mode only, preserves scrollback).\n\nUsing alternate screen provides a cleaner fullscreen experience but prevents scrollback in terminal multiplexers like Zellij that follow the xterm spec." - }, - "animations": { - "default": true, - "description": "Enable animations (welcome screen, shimmer effects, spinners). Defaults to `true`.", - "type": "boolean" - }, - "model_availability_nux": { - "allOf": [ - { - "$ref": "#/definitions/ModelAvailabilityNuxConfig" - } - ], - "default": {}, - "description": "Startup tooltip availability NUX state persisted by the TUI." - }, - "notification_method": { - "allOf": [ - { - "$ref": "#/definitions/NotificationMethod" - } - ], - "default": "auto", - "description": "Notification method to use for unfocused terminal notifications. Defaults to `auto`." - }, - "notifications": { - "allOf": [ - { - "$ref": "#/definitions/Notifications" - } - ], - "default": true, - "description": "Enable desktop notifications from the TUI when the terminal is unfocused. Defaults to `true`." - }, - "show_tooltips": { - "default": true, - "description": "Show startup tooltips in the TUI welcome screen. Defaults to `true`.", - "type": "boolean" - }, - "status_line": { - "default": null, - "description": "Ordered list of status line item identifiers.\n\nWhen set, the TUI renders the selected items as the status line. When unset, the TUI defaults to: `model-with-reasoning`, `context-remaining`, and `current-dir`.", - "items": { - "type": "string" - }, - "type": "array" - }, - "terminal_title": { - "default": null, - "description": "Ordered list of terminal title item identifiers.\n\nWhen set, the TUI renders the selected items into the terminal window/tab title. When unset, the TUI defaults to: `spinner` and `project`.", - "items": { - "type": "string" - }, - "type": "array" - }, - "theme": { - "default": null, - "description": "Syntax highlighting theme name (kebab-case).\n\nWhen set, overrides automatic light/dark theme detection. Use `/theme` in the TUI or see `$CODEX_HOME/themes` for custom themes.", - "type": "string" - } - }, - "type": "object" - }, - "UriBasedFileOpener": { - "oneOf": [ - { - "enum": [ - "vscode", - "vscode-insiders", - "windsurf", - "cursor" - ], - "type": "string" - }, - { - "description": "Option to disable the URI-based file opener.", - "enum": [ - "none" - ], - "type": "string" - } - ] - }, - "Verbosity": { - "description": "Controls output length/detail on GPT-5 models via the Responses API. Serialized with lowercase values to match the OpenAI API.", - "enum": [ - "low", - "medium", - "high" - ], - "type": "string" - }, - "WebSearchContextSize": { - "enum": [ - "low", - "medium", - "high" - ], - "type": "string" - }, - "WebSearchLocation": { - "additionalProperties": false, - "properties": { - "city": { - "type": "string" - }, - "country": { - "type": "string" - }, - "region": { - "type": "string" - }, - "timezone": { - "type": "string" - } - }, - "type": "object" - }, - "WebSearchMode": { - "enum": [ - "disabled", - "cached", - "live" - ], - "type": "string" - }, - "WebSearchToolConfig": { - "additionalProperties": false, - "properties": { - "allowed_domains": { - "items": { - "type": "string" - }, - "type": "array" - }, - "context_size": { - "$ref": "#/definitions/WebSearchContextSize" - }, - "location": { - "$ref": "#/definitions/WebSearchLocation" - } - }, - "type": "object" - }, - "WindowsSandboxModeToml": { - "enum": [ - "elevated", - "unelevated" - ], - "type": "string" - }, - "WindowsToml": { - "additionalProperties": false, - "properties": { - "sandbox": { - "$ref": "#/definitions/WindowsSandboxModeToml" - }, - "sandbox_private_desktop": { - "description": "Defaults to `true`. Set to `false` to launch the final sandboxed child process on `Winsta0\\\\Default` instead of a private desktop.", - "type": "boolean" - } - }, - "type": "object" - }, - "WireApi": { - "description": "Wire protocol that the provider speaks.", - "oneOf": [ - { - "description": "The Responses API exposed by OpenAI at `/v1/responses`.", - "enum": [ - "responses" - ], - "type": "string" - } - ] - } - }, - "description": "Base config deserialized from ~/.codex/config.toml.", - "properties": { - "agents": { - "allOf": [ - { - "$ref": "#/definitions/AgentsToml" - } - ], - "description": "Agent-related settings (thread limits, etc.)." - }, - "allow_login_shell": { - "description": "Whether the model may request a login shell for shell-based tools. Default to `true`\n\nIf `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.", - "type": "boolean" - }, - "analytics": { - "allOf": [ - { - "$ref": "#/definitions/AnalyticsConfigToml" - } - ], - "description": "When `false`, disables analytics across Codex product surfaces in this machine. Defaults to `true`." - }, - "approval_policy": { - "allOf": [ - { - "$ref": "#/definitions/AskForApproval" - } - ], - "description": "Default approval policy for executing commands." - }, - "approvals_reviewer": { - "allOf": [ - { - "$ref": "#/definitions/ApprovalsReviewer" - } - ], - "description": "Configures who approval requests are routed to for review once they have been escalated. This does not disable separate safety checks such as ARC." - }, - "apps": { - "allOf": [ - { - "$ref": "#/definitions/AppsConfigToml" - } - ], - "default": null, - "description": "Settings for app-specific controls." - }, - "audio": { - "allOf": [ - { - "$ref": "#/definitions/RealtimeAudioToml" - } - ], - "default": null, - "description": "Machine-local realtime audio device preferences used by realtime voice." - }, - "background_terminal_max_timeout": { - "description": "Maximum poll window for background terminal output (`write_stdin`), in milliseconds. Default: `300000` (5 minutes).", - "format": "uint64", - "minimum": 0.0, - "type": "integer" - }, - "chatgpt_base_url": { - "description": "Base URL for requests to ChatGPT (as opposed to the OpenAI API).", - "type": "string" - }, - "check_for_update_on_startup": { - "description": "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`.", - "type": "boolean" - }, - "cli_auth_credentials_store": { - "allOf": [ - { - "$ref": "#/definitions/AuthCredentialsStoreMode" - } - ], - "default": null, - "description": "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." - }, - "commit_attribution": { - "description": "Optional commit attribution text for commit message co-author trailers.\n\nSet to an empty string to disable automatic commit attribution.", - "type": "string" - }, - "compact_prompt": { - "description": "Compact prompt used for history compaction.", - "type": "string" - }, - "custom_models": { - "default": [], - "description": "User-defined model aliases that can override model context settings.", - "items": { - "$ref": "#/definitions/CustomModelToml" - }, - "type": "array" - }, - "default_permissions": { - "description": "Default named permissions profile to apply from the `[permissions]` table.", - "type": "string" - }, - "developer_instructions": { - "default": null, - "description": "Developer instructions inserted as a `developer` role message.", - "type": "string" - }, - "disable_paste_burst": { - "description": "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.", - "type": "boolean" - }, - "experimental_compact_prompt_file": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "experimental_realtime_start_instructions": { - "description": "Experimental / do not use. Replaces the built-in realtime start instructions inserted into developer messages when realtime becomes active.", - "type": "string" - }, - "experimental_realtime_ws_backend_prompt": { - "description": "Experimental / do not use. Overrides only the realtime conversation websocket transport instructions (the `Op::RealtimeConversation` `/ws` session.update instructions) without changing normal prompts.", - "type": "string" - }, - "experimental_realtime_ws_base_url": { - "description": "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.", - "type": "string" - }, - "experimental_realtime_ws_model": { - "description": "Experimental / do not use. Selects the realtime websocket model/snapshot used for the `Op::RealtimeConversation` connection.", - "type": "string" - }, - "experimental_realtime_ws_startup_context": { - "description": "Experimental / do not use. Replaces the synthesized realtime startup context appended to websocket session instructions. An empty string disables startup context injection entirely.", - "type": "string" - }, - "experimental_use_freeform_apply_patch": { - "type": "boolean" - }, - "experimental_use_unified_exec_tool": { - "type": "boolean" - }, - "features": { - "additionalProperties": false, - "default": null, - "description": "Centralized feature flags (new). Prefer this over individual toggles.", - "properties": { - "apply_patch_freeform": { - "type": "boolean" - }, - "apps": { - "type": "boolean" - }, - "child_agents_md": { - "type": "boolean" - }, - "code_mode": { - "type": "boolean" - }, - "code_mode_only": { - "type": "boolean" - }, - "codex_git_commit": { - "type": "boolean" - }, - "codex_hooks": { - "type": "boolean" - }, - "collab": { - "type": "boolean" - }, - "collaboration_modes": { - "type": "boolean" - }, - "connectors": { - "type": "boolean" - }, - "default_mode_request_user_input": { - "type": "boolean" - }, - "elevated_windows_sandbox": { - "type": "boolean" - }, - "enable_experimental_windows_sandbox": { - "type": "boolean" - }, - "enable_fanout": { - "type": "boolean" - }, - "enable_request_compression": { - "type": "boolean" - }, - "exec_permission_approvals": { - "type": "boolean" - }, - "experimental_use_freeform_apply_patch": { - "type": "boolean" - }, - "experimental_use_unified_exec_tool": { - "type": "boolean" - }, - "experimental_windows_sandbox": { - "type": "boolean" - }, - "fast_mode": { - "type": "boolean" - }, - "general_analytics": { - "type": "boolean" - }, - "guardian_approval": { - "type": "boolean" - }, - "image_detail_original": { - "type": "boolean" - }, - "image_generation": { - "type": "boolean" - }, - "include_apply_patch_tool": { - "type": "boolean" - }, - "js_repl": { - "type": "boolean" - }, - "js_repl_tools_only": { - "type": "boolean" - }, - "memories": { - "type": "boolean" - }, - "memory_tool": { - "type": "boolean" - }, - "multi_agent": { - "type": "boolean" - }, - "multi_agent_v2": { - "type": "boolean" - }, - "personality": { - "type": "boolean" - }, - "plugins": { - "type": "boolean" - }, - "prevent_idle_sleep": { - "type": "boolean" - }, - "realtime_conversation": { - "type": "boolean" - }, - "remote_models": { - "type": "boolean" - }, - "request_permissions": { - "type": "boolean" - }, - "request_permissions_tool": { - "type": "boolean" - }, - "request_rule": { - "type": "boolean" - }, - "responses_websockets": { - "type": "boolean" - }, - "responses_websockets_v2": { - "type": "boolean" - }, - "runtime_metrics": { - "type": "boolean" - }, - "search_tool": { - "type": "boolean" - }, - "shell_snapshot": { - "type": "boolean" - }, - "shell_tool": { - "type": "boolean" - }, - "shell_zsh_fork": { - "type": "boolean" - }, - "skill_env_var_dependency_prompt": { - "type": "boolean" - }, - "skill_mcp_dependency_install": { - "type": "boolean" - }, - "sqlite": { - "type": "boolean" - }, - "steer": { - "type": "boolean" - }, - "tool_call_mcp_elicitation": { - "type": "boolean" - }, - "tool_search": { - "type": "boolean" - }, - "tool_suggest": { - "type": "boolean" - }, - "tui_app_server": { - "type": "boolean" - }, - "undo": { - "type": "boolean" - }, - "unified_exec": { - "type": "boolean" - }, - "use_legacy_landlock": { - "type": "boolean" - }, - "use_linux_sandbox_bwrap": { - "type": "boolean" - }, - "web_search": { - "type": "boolean" - }, - "web_search_cached": { - "type": "boolean" - }, - "web_search_request": { - "type": "boolean" - } - }, - "type": "object" - }, - "feedback": { - "allOf": [ - { - "$ref": "#/definitions/FeedbackConfigToml" - } - ], - "description": "When `false`, disables feedback collection across Codex product surfaces. Defaults to `true`." - }, - "file_opener": { - "allOf": [ - { - "$ref": "#/definitions/UriBasedFileOpener" - } - ], - "description": "Optional URI-based file opener. If set, citations to files in the model output will be hyperlinked using the specified URI scheme." - }, - "forced_chatgpt_workspace_id": { - "default": null, - "description": "When set, restricts ChatGPT login to a specific workspace identifier.", - "type": "string" - }, - "forced_login_method": { - "allOf": [ - { - "$ref": "#/definitions/ForcedLoginMethod" - } - ], - "default": null, - "description": "When set, restricts the login mechanism users may use." - }, - "ghost_snapshot": { - "allOf": [ - { - "$ref": "#/definitions/GhostSnapshotToml" - } - ], - "default": null, - "description": "Settings for ghost snapshots (used for undo)." - }, - "hide_agent_reasoning": { - "description": "When set to `true`, `AgentReasoning` events will be hidden from the UI/output. Defaults to `false`.", - "type": "boolean" - }, - "history": { - "allOf": [ - { - "$ref": "#/definitions/History" - } - ], - "default": null, - "description": "Settings that govern if and what will be written to `~/.codex/history.jsonl`." - }, - "instructions": { - "description": "System instructions.", - "type": "string" - }, - "js_repl_node_module_dirs": { - "description": "Ordered list of directories to search for Node modules in `js_repl`.", - "items": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": "array" - }, - "js_repl_node_path": { - "allOf": [ - { - "$ref": "#/definitions/AbsolutePathBuf" - } - ], - "description": "Optional absolute path to the Node runtime used by `js_repl`." - }, - "log_dir": { - "allOf": [ - { - "$ref": "#/definitions/AbsolutePathBuf" - } - ], - "description": "Directory where Codex writes log files, for example `codex-tui.log`. Defaults to `$CODEX_HOME/log`." - }, - "mcp_oauth_callback_port": { - "description": "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.", - "format": "uint16", - "minimum": 0.0, - "type": "integer" - }, - "mcp_oauth_callback_url": { - "description": "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).", - "type": "string" - }, - "mcp_oauth_credentials_store": { - "allOf": [ - { - "$ref": "#/definitions/OAuthCredentialsStoreMode" - } - ], - "default": null, - "description": "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." - }, - "mcp_servers": { - "additionalProperties": { - "$ref": "#/definitions/RawMcpServerConfig" - }, - "default": {}, - "description": "Definition for MCP servers that Codex can reach out to for tool calls.", - "type": "object" - }, - "memories": { - "allOf": [ - { - "$ref": "#/definitions/MemoriesToml" - } - ], - "description": "Memories subsystem settings." - }, - "model": { - "description": "Optional override of model selection.", - "type": "string" - }, - "model_auto_compact_token_limit": { - "description": "Token usage threshold triggering auto-compaction of conversation history.", - "format": "int64", - "type": "integer" - }, - "model_catalog_json": { - "allOf": [ - { - "$ref": "#/definitions/AbsolutePathBuf" - } - ], - "description": "Optional path to a JSON model catalog (applied on startup only). Per-thread `config` overrides are accepted but do not reapply this (no-ops)." - }, - "model_context_window": { - "description": "Size of the context window for the model, in tokens.", - "format": "int64", - "type": "integer" - }, - "model_instructions_file": { - "allOf": [ - { - "$ref": "#/definitions/AbsolutePathBuf" - } - ], - "description": "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." - }, - "model_provider": { - "description": "Provider to use from the model_providers map.", - "type": "string" - }, - "model_providers": { - "additionalProperties": { - "$ref": "#/definitions/ModelProviderInfo" - }, - "default": {}, - "description": "User-defined provider entries that extend the built-in list. Built-in IDs cannot be overridden.", - "type": "object" - }, - "model_reasoning_effort": { - "$ref": "#/definitions/ReasoningEffort" - }, - "model_reasoning_summary": { - "$ref": "#/definitions/ReasoningSummary" - }, - "model_supports_reasoning_summaries": { - "description": "Override to force-enable reasoning summaries for the configured model.", - "type": "boolean" - }, - "model_verbosity": { - "allOf": [ - { - "$ref": "#/definitions/Verbosity" - } - ], - "description": "Optional verbosity control for GPT-5 models (Responses API `text.verbosity`)." - }, - "notice": { - "allOf": [ - { - "$ref": "#/definitions/Notice" - } - ], - "description": "Collection of in-product notices (different from notifications) See [`crate::config::types::Notices`] for more details" - }, - "notify": { - "default": null, - "description": "Optional external command to spawn for end-user notifications.", - "items": { - "type": "string" - }, - "type": "array" - }, - "openai_base_url": { - "description": "Base URL override for the built-in `openai` model provider.", - "type": "string" - }, - "oss_provider": { - "description": "Preferred OSS provider for local models, e.g. \"lmstudio\" or \"ollama\".", - "type": "string" - }, - "otel": { - "allOf": [ - { - "$ref": "#/definitions/OtelConfigToml" - } - ], - "description": "OTEL configuration." - }, - "permissions": { - "allOf": [ - { - "$ref": "#/definitions/PermissionsToml" - } - ], - "default": null, - "description": "Named permissions profiles." - }, - "personality": { - "allOf": [ - { - "$ref": "#/definitions/Personality" - } - ], - "description": "Optionally specify a personality for the model" - }, - "plan_mode_reasoning_effort": { - "$ref": "#/definitions/ReasoningEffort" - }, - "plugins": { - "additionalProperties": { - "$ref": "#/definitions/PluginConfig" - }, - "default": {}, - "description": "User-level plugin config entries keyed by plugin name.", - "type": "object" - }, - "profile": { - "description": "Profile to use from the `profiles` map.", - "type": "string" - }, - "profiles": { - "additionalProperties": { - "$ref": "#/definitions/ConfigProfile" - }, - "default": {}, - "description": "Named profiles to facilitate switching between different configurations.", - "type": "object" - }, - "project_doc_fallback_filenames": { - "description": "Ordered list of fallback filenames to look for when AGENTS.md is missing.", - "items": { - "type": "string" - }, - "type": "array" - }, - "project_doc_max_bytes": { - "description": "Maximum number of bytes to include from an AGENTS.md project doc file.", - "format": "uint", - "minimum": 0.0, - "type": "integer" - }, - "project_root_markers": { - "default": null, - "description": "Markers used to detect the project root when searching parent directories for `.codex` folders. Defaults to [\".git\"] when unset.", - "items": { - "type": "string" - }, - "type": "array" - }, - "projects": { - "additionalProperties": { - "$ref": "#/definitions/ProjectConfig" - }, - "type": "object" - }, - "realtime": { - "allOf": [ - { - "$ref": "#/definitions/RealtimeToml" - } - ], - "default": null, - "description": "Experimental / do not use. Realtime websocket session selection. `version` controls v1/v2 and `type` controls conversational/transcription." - }, - "review_model": { - "description": "Review model override used by the `/review` feature.", - "type": "string" - }, - "sandbox_mode": { - "allOf": [ - { - "$ref": "#/definitions/SandboxMode" - } - ], - "description": "Sandbox mode to use." - }, - "sandbox_workspace_write": { - "allOf": [ - { - "$ref": "#/definitions/SandboxWorkspaceWrite" - } - ], - "description": "Sandbox configuration to apply if `sandbox` is `WorkspaceWrite`." - }, - "service_tier": { - "allOf": [ - { - "$ref": "#/definitions/ServiceTier" - } - ], - "description": "Optional explicit service tier preference for new turns (`fast` or `flex`)." - }, - "shell_environment_policy": { - "allOf": [ - { - "$ref": "#/definitions/ShellEnvironmentPolicyToml" - } - ], - "default": { - "exclude": null, - "experimental_use_profile": null, - "ignore_default_excludes": null, - "include_only": null, - "inherit": null, - "set": null - } - }, - "show_raw_agent_reasoning": { - "description": "When set to `true`, `AgentReasoningRawContentEvent` events will be shown in the UI/output. Defaults to `false`.", - "type": "boolean" - }, - "skills": { - "allOf": [ - { - "$ref": "#/definitions/SkillsConfig" - } - ], - "description": "User-level skill config entries keyed by SKILL.md path." - }, - "sqlite_home": { - "allOf": [ - { - "$ref": "#/definitions/AbsolutePathBuf" - } - ], - "description": "Directory where Codex stores the SQLite state DB. Defaults to `$CODEX_SQLITE_HOME` when set. Otherwise uses `$CODEX_HOME`." - }, - "suppress_unstable_features_warning": { - "description": "Suppress warnings about unstable (under development) features.", - "type": "boolean" - }, - "tool_output_token_limit": { - "description": "Token budget applied when storing tool/function outputs in the context manager.", - "format": "uint", - "minimum": 0.0, - "type": "integer" - }, - "tool_suggest": { - "allOf": [ - { - "$ref": "#/definitions/ToolSuggestConfig" - } - ], - "description": "Additional discoverable tools that can be suggested for installation." - }, - "tools": { - "allOf": [ - { - "$ref": "#/definitions/ToolsToml" - } - ], - "description": "Nested tools section for feature toggles" - }, - "tui": { - "allOf": [ - { - "$ref": "#/definitions/Tui" - } - ], - "description": "Collection of settings that are specific to the TUI." - }, - "web_search": { - "allOf": [ - { - "$ref": "#/definitions/WebSearchMode" - } - ], - "description": "Controls the web search tool mode: disabled, cached, or live." - }, - "windows": { - "allOf": [ - { - "$ref": "#/definitions/WindowsToml" - } - ], - "default": null, - "description": "Windows-specific configuration." - }, - "windows_wsl_setup_acknowledged": { - "description": "Tracks whether the Windows onboarding screen has been acknowledged.", - "type": "boolean" - }, - "zsh_path": { - "allOf": [ - { - "$ref": "#/definitions/AbsolutePathBuf" - } - ], - "description": "Optional absolute path to patched zsh used by zsh-exec-bridge-backed shell execution." - } - }, - "title": "ConfigToml", - "type": "object" -} diff --git a/codex-rs/config/Cargo.toml b/codex-rs/config/Cargo.toml index f1bd2dba16..0b440263eb 100644 --- a/codex-rs/config/Cargo.toml +++ b/codex-rs/config/Cargo.toml @@ -24,8 +24,10 @@ tokio = { workspace = true, features = ["fs"] } toml = { workspace = true } toml_edit = { workspace = true } tracing = { workspace = true } +wildmatch = { workspace = true } [dev-dependencies] anyhow = { workspace = true } pretty_assertions = { workspace = true } +tempfile = { workspace = true } tokio = { workspace = true, features = ["full"] } diff --git a/codex-rs/config/src/lib.rs b/codex-rs/config/src/lib.rs index f769f2496c..2196011323 100644 --- a/codex-rs/config/src/lib.rs +++ b/codex-rs/config/src/lib.rs @@ -11,6 +11,7 @@ mod project_root_markers; mod requirements_exec_policy; mod skills_config; mod state; +pub mod types; pub const CONFIG_TOML_FILE: &str = "config.toml"; diff --git a/codex-rs/core/src/config/types.rs b/codex-rs/config/src/types.rs similarity index 97% rename from codex-rs/core/src/config/types.rs rename to codex-rs/config/src/types.rs index b83be53e03..c523833525 100644 --- a/codex-rs/core/src/config/types.rs +++ b/codex-rs/config/src/types.rs @@ -1,14 +1,14 @@ -//! Types used to define the fields of [`crate::config::Config`]. +//! Types used to define loaded and effective Codex configuration values. // Note this file should generally be restricted to simple struct/enum // definitions that do not contain business logic. -pub use codex_config::AppToolApproval; -pub use codex_config::McpServerConfig; -pub use codex_config::McpServerDisabledReason; -pub use codex_config::McpServerToolConfig; -pub use codex_config::McpServerTransportConfig; -pub use codex_config::RawMcpServerConfig; +pub use crate::mcp_types::AppToolApproval; +pub use crate::mcp_types::McpServerConfig; +pub use crate::mcp_types::McpServerDisabledReason; +pub use crate::mcp_types::McpServerToolConfig; +pub use crate::mcp_types::McpServerTransportConfig; +pub use crate::mcp_types::RawMcpServerConfig; pub use codex_protocol::config_types::AltScreenMode; pub use codex_protocol::config_types::ApprovalsReviewer; pub use codex_protocol::config_types::ModeKind; @@ -537,14 +537,9 @@ pub struct Notice { pub model_migrations: BTreeMap, } -impl Notice { - /// referenced by config_edit helpers when writing notice flags - pub(crate) const TABLE_KEY: &'static str = "notice"; -} - -pub use codex_config::BundledSkillsConfig; -pub use codex_config::SkillConfig; -pub use codex_config::SkillsConfig; +pub use crate::skills_config::BundledSkillsConfig; +pub use crate::skills_config::SkillConfig; +pub use crate::skills_config::SkillsConfig; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] #[schemars(deny_unknown_fields)] diff --git a/codex-rs/core/src/config/types_tests.rs b/codex-rs/config/src/types_tests.rs similarity index 100% rename from codex-rs/core/src/config/types_tests.rs rename to codex-rs/config/src/types_tests.rs diff --git a/codex-rs/core/BUILD.bazel b/codex-rs/core/BUILD.bazel index 591eb1f28a..e895e9ff47 100644 --- a/codex-rs/core/BUILD.bazel +++ b/codex-rs/core/BUILD.bazel @@ -2,6 +2,7 @@ load("//:defs.bzl", "codex_rust_crate") exports_files( [ + "config.schema.json", "templates/collaboration_mode/default.md", "templates/collaboration_mode/plan.md", ], diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index b4aff687d2..98a9338e20 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -116,7 +116,6 @@ tracing = { workspace = true, features = ["log"] } url = { workspace = true } uuid = { workspace = true, features = ["serde", "v4", "v5"] } which = { workspace = true } -wildmatch = { workspace = true } zip = { workspace = true } [target.'cfg(target_os = "linux")'.dependencies] diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 7e6a59690a..d0b8b3eb49 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -2432,7 +2432,7 @@ "$ref": "#/definitions/Notice" } ], - "description": "Collection of in-product notices (different from notifications) See [`crate::config::types::Notices`] for more details" + "description": "Collection of in-product notices (different from notifications) See [`codex_config::types::Notice`] for more details" }, "notify": { "default": null, diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index ccc2f62975..8a6c42bb96 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -1,6 +1,6 @@ -use crate::config::types::Personality; use crate::error::Result; pub use codex_api::common::ResponseEvent; +use codex_config::types::Personality; use codex_protocol::models::BaseInstructions; use codex_protocol::models::FunctionCallOutputBody; use codex_protocol::models::ResponseItem; diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 6c1d3216bb..c2f09c8803 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -181,8 +181,6 @@ use crate::config::ConstraintResult; use crate::config::GhostSnapshotConfig; use crate::config::StartedNetworkProxy; use crate::config::resolve_web_search_mode_for_turn; -use crate::config::types::McpServerConfig; -use crate::config::types::ShellEnvironmentPolicy; use crate::context_manager::ContextManager; use crate::context_manager::TotalTokenUsageBreakdown; use crate::environment_context::EnvironmentContext; @@ -191,6 +189,8 @@ use crate::error::Result as CodexResult; #[cfg(test)] use crate::exec::StreamOutput; use codex_config::CONFIG_TOML_FILE; +use codex_config::types::McpServerConfig; +use codex_config::types::ShellEnvironmentPolicy; mod rollout_reconstruction; #[cfg(test)] diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index 4eaf1c93b7..fd10a80af5 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -605,7 +605,7 @@ async fn reload_user_config_layer_updates_effective_apps_config() { .and_then(|table| table.get("apps")) .cloned() .expect("apps table"); - let apps = crate::config::types::AppsConfigToml::deserialize(apps_toml) + let apps = codex_config::types::AppsConfigToml::deserialize(apps_toml) .expect("deserialize apps config"); let app = apps .apps @@ -3094,7 +3094,7 @@ async fn user_turn_updates_approvals_reviewer() { }], cwd: config.cwd.to_path_buf(), approval_policy: config.permissions.approval_policy.value(), - approvals_reviewer: Some(crate::config::types::ApprovalsReviewer::GuardianSubagent), + approvals_reviewer: Some(codex_config::types::ApprovalsReviewer::GuardianSubagent), sandbox_policy: config.permissions.sandbox_policy.get().clone(), model: turn_context.model_info.slug.clone(), effort: config.model_reasoning_effort, @@ -3110,7 +3110,7 @@ async fn user_turn_updates_approvals_reviewer() { let state = session.state.lock().await; assert_eq!( state.session_configuration.approvals_reviewer, - crate::config::types::ApprovalsReviewer::GuardianSubagent + codex_config::types::ApprovalsReviewer::GuardianSubagent ); } diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 5b4b9492fc..6f8655ba57 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -1,23 +1,23 @@ use crate::config::edit::ConfigEdit; use crate::config::edit::ConfigEditsBuilder; use crate::config::edit::apply_blocking; -use crate::config::types::AppToolApproval; -use crate::config::types::ApprovalsReviewer; -use crate::config::types::BundledSkillsConfig; -use crate::config::types::FeedbackConfigToml; -use crate::config::types::HistoryPersistence; -use crate::config::types::McpServerToolConfig; -use crate::config::types::McpServerTransportConfig; -use crate::config::types::MemoriesConfig; -use crate::config::types::MemoriesToml; -use crate::config::types::ModelAvailabilityNuxConfig; -use crate::config::types::NotificationMethod; -use crate::config::types::Notifications; -use crate::config::types::ToolSuggestDiscoverableType; use crate::config_loader::RequirementSource; use crate::plugins::PluginsManager; use assert_matches::assert_matches; use codex_config::CONFIG_TOML_FILE; +use codex_config::types::AppToolApproval; +use codex_config::types::ApprovalsReviewer; +use codex_config::types::BundledSkillsConfig; +use codex_config::types::FeedbackConfigToml; +use codex_config::types::HistoryPersistence; +use codex_config::types::McpServerToolConfig; +use codex_config::types::McpServerTransportConfig; +use codex_config::types::MemoriesConfig; +use codex_config::types::MemoriesToml; +use codex_config::types::ModelAvailabilityNuxConfig; +use codex_config::types::NotificationMethod; +use codex_config::types::Notifications; +use codex_config::types::ToolSuggestDiscoverableType; use codex_features::Feature; use codex_features::FeaturesToml; use codex_protocol::permissions::FileSystemAccessMode; diff --git a/codex-rs/core/src/config/edit.rs b/codex-rs/core/src/config/edit.rs index 370c46ce4c..1b3d700ab7 100644 --- a/codex-rs/core/src/config/edit.rs +++ b/codex-rs/core/src/config/edit.rs @@ -1,9 +1,8 @@ -use crate::config::types::McpServerConfig; -use crate::config::types::Notice; use crate::path_utils::resolve_symlink_write_paths; use crate::path_utils::write_atomically; use anyhow::Context; use codex_config::CONFIG_TOML_FILE; +use codex_config::types::McpServerConfig; use codex_features::FEATURES; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ServiceTier; @@ -20,6 +19,8 @@ use toml_edit::Item as TomlItem; use toml_edit::Table as TomlTable; use toml_edit::value; +const NOTICE_TABLE_KEY: &str = "notice"; + /// Discrete config mutations supported by the persistence engine. #[derive(Clone, Debug)] pub enum ConfigEdit { @@ -125,10 +126,10 @@ pub fn model_availability_nux_count_edits(shown_count: &HashMap) -> // TODO(jif) move to a dedicated file mod document_helpers { - use crate::config::types::AppToolApproval; - use crate::config::types::McpServerConfig; - use crate::config::types::McpServerToolConfig; - use crate::config::types::McpServerTransportConfig; + use codex_config::types::AppToolApproval; + use codex_config::types::McpServerConfig; + use codex_config::types::McpServerToolConfig; + use codex_config::types::McpServerTransportConfig; use toml_edit::Array as TomlArray; use toml_edit::InlineTable; use toml_edit::Item as TomlItem; @@ -387,29 +388,29 @@ impl ConfigDocument { )), ConfigEdit::SetNoticeHideFullAccessWarning(acknowledged) => Ok(self.write_value( Scope::Global, - &[Notice::TABLE_KEY, "hide_full_access_warning"], + &[NOTICE_TABLE_KEY, "hide_full_access_warning"], value(*acknowledged), )), ConfigEdit::SetNoticeHideWorldWritableWarning(acknowledged) => Ok(self.write_value( Scope::Global, - &[Notice::TABLE_KEY, "hide_world_writable_warning"], + &[NOTICE_TABLE_KEY, "hide_world_writable_warning"], value(*acknowledged), )), ConfigEdit::SetNoticeHideRateLimitModelNudge(acknowledged) => Ok(self.write_value( Scope::Global, - &[Notice::TABLE_KEY, "hide_rate_limit_model_nudge"], + &[NOTICE_TABLE_KEY, "hide_rate_limit_model_nudge"], value(*acknowledged), )), ConfigEdit::SetNoticeHideModelMigrationPrompt(migration_config, acknowledged) => { Ok(self.write_value( Scope::Global, - &[Notice::TABLE_KEY, migration_config.as_str()], + &[NOTICE_TABLE_KEY, migration_config.as_str()], value(*acknowledged), )) } ConfigEdit::RecordModelMigrationSeen { from, to } => Ok(self.write_value( Scope::Global, - &[Notice::TABLE_KEY, "model_migrations", from.as_str()], + &[NOTICE_TABLE_KEY, "model_migrations", from.as_str()], value(to.clone()), )), ConfigEdit::SetWindowsWslSetupAcknowledged(acknowledged) => Ok(self.write_value( diff --git a/codex-rs/core/src/config/edit_tests.rs b/codex-rs/core/src/config/edit_tests.rs index e27add0054..314d8badb3 100644 --- a/codex-rs/core/src/config/edit_tests.rs +++ b/codex-rs/core/src/config/edit_tests.rs @@ -1,7 +1,7 @@ use super::*; -use crate::config::types::AppToolApproval; -use crate::config::types::McpServerToolConfig; -use crate::config::types::McpServerTransportConfig; +use codex_config::types::AppToolApproval; +use codex_config::types::McpServerToolConfig; +use codex_config::types::McpServerTransportConfig; use codex_protocol::openai_models::ReasoningEffort; use pretty_assertions::assert_eq; #[cfg(unix)] diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 48c3d70023..828b4c1484 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -1,31 +1,5 @@ use crate::config::edit::ConfigEdit; use crate::config::edit::ConfigEditsBuilder; -use crate::config::types::AppsConfigToml; -use crate::config::types::DEFAULT_OTEL_ENVIRONMENT; -use crate::config::types::History; -use crate::config::types::McpServerConfig; -use crate::config::types::McpServerDisabledReason; -use crate::config::types::McpServerTransportConfig; -use crate::config::types::MemoriesConfig; -use crate::config::types::MemoriesToml; -use crate::config::types::ModelAvailabilityNuxConfig; -use crate::config::types::Notice; -use crate::config::types::NotificationMethod; -use crate::config::types::Notifications; -use crate::config::types::OtelConfig; -use crate::config::types::OtelConfigToml; -use crate::config::types::OtelExporterKind; -use crate::config::types::PluginConfig; -use crate::config::types::SandboxWorkspaceWrite; -use crate::config::types::ShellEnvironmentPolicy; -use crate::config::types::ShellEnvironmentPolicyToml; -use crate::config::types::SkillsConfig; -use crate::config::types::ToolSuggestConfig; -use crate::config::types::ToolSuggestDiscoverable; -use crate::config::types::Tui; -use crate::config::types::UriBasedFileOpener; -use crate::config::types::WindowsSandboxModeToml; -use crate::config::types::WindowsToml; use crate::config_loader::CloudRequirementsLoader; use crate::config_loader::ConfigLayerStack; use crate::config_loader::ConfigLayerStackOrdering; @@ -56,6 +30,33 @@ use crate::windows_sandbox::resolve_windows_sandbox_mode; use crate::windows_sandbox::resolve_windows_sandbox_private_desktop; use codex_app_server_protocol::Tools; use codex_app_server_protocol::UserSavedConfig; +use codex_config::types::ApprovalsReviewer; +use codex_config::types::AppsConfigToml; +use codex_config::types::DEFAULT_OTEL_ENVIRONMENT; +use codex_config::types::History; +use codex_config::types::McpServerConfig; +use codex_config::types::McpServerDisabledReason; +use codex_config::types::McpServerTransportConfig; +use codex_config::types::MemoriesConfig; +use codex_config::types::MemoriesToml; +use codex_config::types::ModelAvailabilityNuxConfig; +use codex_config::types::Notice; +use codex_config::types::NotificationMethod; +use codex_config::types::Notifications; +use codex_config::types::OtelConfig; +use codex_config::types::OtelConfigToml; +use codex_config::types::OtelExporterKind; +use codex_config::types::PluginConfig; +use codex_config::types::SandboxWorkspaceWrite; +use codex_config::types::ShellEnvironmentPolicy; +use codex_config::types::ShellEnvironmentPolicyToml; +use codex_config::types::SkillsConfig; +use codex_config::types::ToolSuggestConfig; +use codex_config::types::ToolSuggestDiscoverable; +use codex_config::types::Tui; +use codex_config::types::UriBasedFileOpener; +use codex_config::types::WindowsSandboxModeToml; +use codex_config::types::WindowsToml; use codex_features::Feature; use codex_features::FeatureConfigSource; use codex_features::FeatureOverrides; @@ -113,7 +114,6 @@ mod permissions; pub mod profile; pub mod schema; pub mod service; -pub mod types; pub use codex_config::Constrained; pub use codex_config::ConstraintError; pub use codex_config::ConstraintResult; @@ -135,7 +135,6 @@ pub(crate) use permissions::overlay_network_domain_permissions; pub(crate) use permissions::resolve_permission_profile; pub use service::ConfigService; pub use service::ConfigServiceError; -pub use types::ApprovalsReviewer; pub use codex_git_utils::GhostSnapshotConfig; @@ -591,7 +590,7 @@ pub struct Config { pub tool_suggest: ToolSuggestConfig, /// OTEL configuration (exporter type, endpoint, headers, etc.). - pub otel: crate::config::types::OtelConfig, + pub otel: codex_config::types::OtelConfig, } #[derive(Debug, Clone, Default)] @@ -1406,18 +1405,18 @@ pub struct ConfigToml { /// When `false`, disables analytics across Codex product surfaces in this machine. /// Defaults to `true`. - pub analytics: Option, + pub analytics: Option, /// When `false`, disables feedback collection across Codex product surfaces. /// Defaults to `true`. - pub feedback: Option, + pub feedback: Option, /// Settings for app-specific controls. #[serde(default)] pub apps: Option, /// OTEL configuration. - pub otel: Option, + pub otel: Option, /// Windows-specific configuration. #[serde(default)] @@ -1427,7 +1426,7 @@ pub struct ConfigToml { pub windows_wsl_setup_acknowledged: Option, /// Collection of in-product notices (different from notifications) - /// See [`crate::config::types::Notices`] for more details + /// See [`codex_config::types::Notice`] for more details pub notice: Option, /// Legacy, now use features diff --git a/codex-rs/core/src/config/profile.rs b/codex-rs/core/src/config/profile.rs index fcfda644e0..7dd8598c0b 100644 --- a/codex-rs/core/src/config/profile.rs +++ b/codex-rs/core/src/config/profile.rs @@ -4,9 +4,9 @@ use serde::Deserialize; use serde::Serialize; use crate::config::ToolsToml; -use crate::config::types::ApprovalsReviewer; -use crate::config::types::Personality; -use crate::config::types::WindowsToml; +use codex_config::types::ApprovalsReviewer; +use codex_config::types::Personality; +use codex_config::types::WindowsToml; use codex_features::FeaturesToml; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::SandboxMode; @@ -54,7 +54,7 @@ pub struct ConfigProfile { pub tools_view_image: Option, pub tools: Option, pub web_search: Option, - pub analytics: Option, + pub analytics: Option, #[serde(default)] pub windows: Option, /// Optional feature toggles scoped to this profile. diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index e02a6fc227..10e10e99fc 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -26,14 +26,14 @@ use tracing::warn; use crate::codex::INITIAL_SUBMIT_ID; use crate::config::Config; -use crate::config::types::AppToolApproval; -use crate::config::types::AppsConfigToml; -use crate::config::types::ToolSuggestDiscoverableType; use crate::config_loader::AppsRequirementsToml; use crate::mcp::McpManager; use crate::plugins::AppConnectorId; use crate::plugins::PluginsManager; use crate::plugins::list_tool_suggest_discoverable_plugins; +use codex_config::types::AppToolApproval; +use codex_config::types::AppsConfigToml; +use codex_config::types::ToolSuggestDiscoverableType; use codex_features::Feature; use codex_login::AuthManager; use codex_login::CodexAuth; diff --git a/codex-rs/core/src/connectors_tests.rs b/codex-rs/core/src/connectors_tests.rs index 11ec4b6ca5..893ab243ca 100644 --- a/codex-rs/core/src/connectors_tests.rs +++ b/codex-rs/core/src/connectors_tests.rs @@ -1,16 +1,16 @@ use super::*; use crate::config::CONFIG_TOML_FILE; use crate::config::ConfigBuilder; -use crate::config::types::AppConfig; -use crate::config::types::AppToolConfig; -use crate::config::types::AppToolsConfig; -use crate::config::types::AppsDefaultConfig; use crate::config_loader::AppRequirementToml; use crate::config_loader::AppsRequirementsToml; use crate::config_loader::CloudRequirementsLoader; use crate::config_loader::ConfigLayerStack; use crate::config_loader::ConfigRequirements; use crate::config_loader::ConfigRequirementsToml; +use codex_config::types::AppConfig; +use codex_config::types::AppToolConfig; +use codex_config::types::AppToolsConfig; +use codex_config::types::AppsDefaultConfig; use codex_features::Feature; use codex_mcp::mcp::CODEX_APPS_MCP_SERVER_NAME; use codex_mcp::mcp_connection_manager::ToolInfo; diff --git a/codex-rs/core/src/exec_env.rs b/codex-rs/core/src/exec_env.rs index 83ac8ad379..4dd5532d51 100644 --- a/codex-rs/core/src/exec_env.rs +++ b/codex-rs/core/src/exec_env.rs @@ -1,6 +1,6 @@ -use crate::config::types::EnvironmentVariablePattern; -use crate::config::types::ShellEnvironmentPolicy; -use crate::config::types::ShellEnvironmentPolicyInherit; +use codex_config::types::EnvironmentVariablePattern; +use codex_config::types::ShellEnvironmentPolicy; +use codex_config::types::ShellEnvironmentPolicyInherit; use codex_protocol::ThreadId; use std::collections::HashMap; use std::collections::HashSet; diff --git a/codex-rs/core/src/exec_env_tests.rs b/codex-rs/core/src/exec_env_tests.rs index ec6b5b5bb5..1b486834e9 100644 --- a/codex-rs/core/src/exec_env_tests.rs +++ b/codex-rs/core/src/exec_env_tests.rs @@ -1,5 +1,5 @@ use super::*; -use crate::config::types::ShellEnvironmentPolicyInherit; +use codex_config::types::ShellEnvironmentPolicyInherit; use maplit::hashmap; fn make_vars(pairs: &[(&str, &str)]) -> Vec<(String, String)> { diff --git a/codex-rs/core/src/guardian/review_session.rs b/codex-rs/core/src/guardian/review_session.rs index 88bc795c16..2a63e5b547 100644 --- a/codex-rs/core/src/guardian/review_session.rs +++ b/codex-rs/core/src/guardian/review_session.rs @@ -34,9 +34,9 @@ use crate::config::Constrained; use crate::config::ManagedFeatures; use crate::config::NetworkProxySpec; use crate::config::Permissions; -use crate::config::types::McpServerConfig; use crate::model_provider_info::ModelProviderInfo; use crate::rollout::recorder::RolloutRecorder; +use codex_config::types::McpServerConfig; use codex_features::Feature; use super::GUARDIAN_REVIEW_TIMEOUT; diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index 994f9586d9..344e46d0be 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -19,7 +19,6 @@ use crate::config::Config; use crate::config::edit::ConfigEdit; use crate::config::edit::ConfigEditsBuilder; use crate::config::load_global_mcp_servers; -use crate::config::types::AppToolApproval; use crate::connectors; use crate::guardian::GuardianApprovalRequest; use crate::guardian::GuardianMcpAnnotations; @@ -31,6 +30,7 @@ use crate::mcp_tool_approval_templates::render_mcp_tool_approval_template; use codex_analytics::AppInvocation; use codex_analytics::InvocationType; use codex_analytics::build_track_events_context; +use codex_config::types::AppToolApproval; use codex_features::Feature; use codex_mcp::mcp::CODEX_APPS_MCP_SERVER_NAME; use codex_otel::sanitize_metric_tag_value; @@ -583,7 +583,7 @@ fn custom_mcp_tool_approval_mode( .and_then(|table| table.get("mcp_servers")) .cloned() .and_then(|value| { - HashMap::::deserialize(value).ok() + HashMap::::deserialize(value).ok() }) .and_then(|servers| servers.get(server).cloned()) .and_then(|server| server.tools.get(tool_name).cloned()) @@ -1541,8 +1541,7 @@ fn project_mcp_tool_approval_config_folder(config: &Config, server: &str) -> Opt .and_then(|table| table.get("mcp_servers")) .cloned() .and_then(|value| { - HashMap::::deserialize(value) - .ok() + HashMap::::deserialize(value).ok() })?; if servers.contains_key(server) { layer diff --git a/codex-rs/core/src/mcp_tool_call_tests.rs b/codex-rs/core/src/mcp_tool_call_tests.rs index 8acdf917e5..5bb9194857 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -1,17 +1,17 @@ use super::*; use crate::codex::make_session_and_context; use crate::codex::make_session_and_context_with_rx; -use crate::config::ApprovalsReviewer; use crate::config::ConfigBuilder; use crate::config::ConfigToml; -use crate::config::types::AppConfig; -use crate::config::types::AppToolConfig; -use crate::config::types::AppToolsConfig; -use crate::config::types::AppsConfigToml; -use crate::config::types::McpServerConfig; -use crate::config::types::McpServerToolConfig; use crate::state::ActiveTurn; use codex_config::CONFIG_TOML_FILE; +use codex_config::types::AppConfig; +use codex_config::types::AppToolConfig; +use codex_config::types::AppToolsConfig; +use codex_config::types::ApprovalsReviewer; +use codex_config::types::AppsConfigToml; +use codex_config::types::McpServerConfig; +use codex_config::types::McpServerToolConfig; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; use core_test_support::responses::ev_response_created; diff --git a/codex-rs/core/src/memories/phase1.rs b/codex-rs/core/src/memories/phase1.rs index 921bc9953c..ff050ac5b6 100644 --- a/codex-rs/core/src/memories/phase1.rs +++ b/codex-rs/core/src/memories/phase1.rs @@ -3,7 +3,6 @@ use crate::RolloutRecorder; use crate::codex::Session; use crate::codex::TurnContext; use crate::config::Config; -use crate::config::types::MemoriesConfig; use crate::contextual_user_message::is_memory_excluded_contextual_user_fragment; use crate::error::CodexErr; use crate::memories::metrics; @@ -13,6 +12,7 @@ use crate::memories::prompts::build_stage_one_input_message; use crate::rollout::INTERACTIVE_SESSION_SOURCES; use crate::rollout::policy::should_persist_response_item_for_memories; use codex_api::ResponseEvent; +use codex_config::types::MemoriesConfig; use codex_otel::SessionTelemetry; use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; use codex_protocol::config_types::ServiceTier; diff --git a/codex-rs/core/src/memories/tests.rs b/codex-rs/core/src/memories/tests.rs index 783bac3c37..6255eacdeb 100644 --- a/codex-rs/core/src/memories/tests.rs +++ b/codex-rs/core/src/memories/tests.rs @@ -1,6 +1,5 @@ use super::storage::rebuild_raw_memories_file_from_memories; use super::storage::sync_rollout_summaries_from_memories; -use crate::config::types::DEFAULT_MEMORIES_MAX_RAW_MEMORIES_FOR_CONSOLIDATION; use crate::memories::clear_memory_root_contents; use crate::memories::ensure_layout; use crate::memories::memory_root; @@ -8,6 +7,7 @@ use crate::memories::raw_memories_file; use crate::memories::rollout_summaries_dir; use chrono::TimeZone; use chrono::Utc; +use codex_config::types::DEFAULT_MEMORIES_MAX_RAW_MEMORIES_FOR_CONSOLIDATION; use codex_protocol::ThreadId; use codex_state::Stage1Output; use pretty_assertions::assert_eq; diff --git a/codex-rs/core/src/message_history.rs b/codex-rs/core/src/message_history.rs index d9613e4b8b..e2d5510857 100644 --- a/codex-rs/core/src/message_history.rs +++ b/codex-rs/core/src/message_history.rs @@ -36,7 +36,7 @@ use tokio::fs; use tokio::io::AsyncReadExt; use crate::config::Config; -use crate::config::types::HistoryPersistence; +use codex_config::types::HistoryPersistence; use codex_protocol::ThreadId; #[cfg(unix)] diff --git a/codex-rs/core/src/otel_init.rs b/codex-rs/core/src/otel_init.rs index 9e38cea52d..c732692d62 100644 --- a/codex-rs/core/src/otel_init.rs +++ b/codex-rs/core/src/otel_init.rs @@ -1,6 +1,6 @@ use crate::config::Config; -use crate::config::types::OtelExporterKind as Kind; -use crate::config::types::OtelHttpProtocol as Protocol; +use codex_config::types::OtelExporterKind as Kind; +use codex_config::types::OtelHttpProtocol as Protocol; use codex_features::Feature; use codex_login::default_client::originator; use codex_otel::OtelProvider; diff --git a/codex-rs/core/src/plugins/discoverable.rs b/codex-rs/core/src/plugins/discoverable.rs index f7006185bf..8c630619b2 100644 --- a/codex-rs/core/src/plugins/discoverable.rs +++ b/codex-rs/core/src/plugins/discoverable.rs @@ -7,7 +7,7 @@ use super::PluginCapabilitySummary; use super::PluginReadRequest; use super::PluginsManager; use crate::config::Config; -use crate::config::types::ToolSuggestDiscoverableType; +use codex_config::types::ToolSuggestDiscoverableType; use codex_features::Feature; use codex_tools::DiscoverablePluginInfo; diff --git a/codex-rs/core/src/plugins/manager.rs b/codex-rs/core/src/plugins/manager.rs index 9147675e2f..2b21a23703 100644 --- a/codex-rs/core/src/plugins/manager.rs +++ b/codex-rs/core/src/plugins/manager.rs @@ -33,8 +33,6 @@ use crate::config::ConfigService; use crate::config::ConfigServiceError; use crate::config::edit::ConfigEdit; use crate::config::edit::ConfigEditsBuilder; -use crate::config::types::McpServerConfig; -use crate::config::types::PluginConfig; use crate::config_loader::ConfigLayerStack; use crate::config_rules::SkillConfigRules; use crate::config_rules::resolve_disabled_skill_paths; @@ -44,6 +42,8 @@ use crate::loader::load_skills_from_roots; use codex_analytics::AnalyticsEventsClient; use codex_app_server_protocol::ConfigValueWriteParams; use codex_app_server_protocol::MergeStrategy; +use codex_config::types::McpServerConfig; +use codex_config::types::PluginConfig; use codex_features::Feature; use codex_login::AuthManager; use codex_login::CodexAuth; diff --git a/codex-rs/core/src/plugins/manager_tests.rs b/codex-rs/core/src/plugins/manager_tests.rs index 2089670d73..a9f455bbc4 100644 --- a/codex-rs/core/src/plugins/manager_tests.rs +++ b/codex-rs/core/src/plugins/manager_tests.rs @@ -1,7 +1,6 @@ use super::*; use crate::config::CONFIG_TOML_FILE; use crate::config::ConfigBuilder; -use crate::config::types::McpServerTransportConfig; use crate::config_loader::ConfigLayerEntry; use crate::config_loader::ConfigLayerStack; use crate::config_loader::ConfigRequirements; @@ -14,6 +13,7 @@ use crate::plugins::test_support::write_curated_plugin_sha_with as write_curated use crate::plugins::test_support::write_file; use crate::plugins::test_support::write_openai_curated_marketplace; use codex_app_server_protocol::ConfigLayerSource; +use codex_config::types::McpServerTransportConfig; use codex_login::CodexAuth; use codex_protocol::protocol::Product; use pretty_assertions::assert_eq; diff --git a/codex-rs/core/src/plugins/mod.rs b/codex-rs/core/src/plugins/mod.rs index 3e4902214c..a86502c28c 100644 --- a/codex-rs/core/src/plugins/mod.rs +++ b/codex-rs/core/src/plugins/mod.rs @@ -1,4 +1,4 @@ -use crate::config::types::McpServerConfig; +use codex_config::types::McpServerConfig; mod discoverable; mod injection; diff --git a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs index bb94085876..4fe6ac0e90 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -3,7 +3,6 @@ use crate::ThreadManager; use crate::built_in_model_providers; use crate::codex::make_session_and_context; use crate::config::DEFAULT_AGENT_MAX_DEPTH; -use crate::config::types::ShellEnvironmentPolicy; use crate::function_tool::FunctionCallError; use crate::session_prefix::format_subagent_notification_message; use crate::state::TaskKind; @@ -17,6 +16,7 @@ use crate::tools::handlers::multi_agents_v2::SendMessageHandler as SendMessageHa use crate::tools::handlers::multi_agents_v2::SpawnAgentHandler as SpawnAgentHandlerV2; use crate::tools::handlers::multi_agents_v2::WaitAgentHandler as WaitAgentHandlerV2; use crate::turn_diff_tracker::TurnDiffTracker; +use codex_config::types::ShellEnvironmentPolicy; use codex_features::Feature; use codex_login::AuthManager; use codex_login::CodexAuth; diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 1a9db07084..341db24767 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -1,82 +1,23 @@ use crate::shell::Shell; use crate::shell::ShellType; -use crate::tools::code_mode::PUBLIC_TOOL_NAME; -use crate::tools::code_mode::WAIT_TOOL_NAME; use crate::tools::handlers::agent_jobs::BatchJobHandler; use crate::tools::handlers::multi_agents_common::DEFAULT_WAIT_TIMEOUT_MS; use crate::tools::handlers::multi_agents_common::MAX_WAIT_TIMEOUT_MS; use crate::tools::handlers::multi_agents_common::MIN_WAIT_TIMEOUT_MS; use crate::tools::registry::ToolRegistryBuilder; -use crate::tools::registry::tool_handler_key; use codex_mcp::mcp::CODEX_APPS_MCP_SERVER_NAME; use codex_mcp::mcp_connection_manager::ToolInfo; use codex_protocol::dynamic_tools::DynamicToolSpec; -use codex_protocol::openai_models::ApplyPatchToolType; -use codex_protocol::openai_models::ConfigShellToolType; -use codex_tools::CommandToolOptions; use codex_tools::DiscoverableTool; -use codex_tools::REQUEST_USER_INPUT_TOOL_NAME; -use codex_tools::ShellToolOptions; -use codex_tools::SpawnAgentToolOptions; -use codex_tools::TOOL_SEARCH_DEFAULT_LIMIT; -use codex_tools::TOOL_SEARCH_TOOL_NAME; -use codex_tools::TOOL_SUGGEST_TOOL_NAME; -use codex_tools::ToolSearchAppSource; -use codex_tools::ToolSpec; +use codex_tools::ToolHandlerKind; +use codex_tools::ToolRegistryPlanAppTool; +use codex_tools::ToolRegistryPlanParams; use codex_tools::ToolUserShellType; use codex_tools::ToolsConfig; -use codex_tools::ViewImageToolOptions; use codex_tools::WaitAgentTimeoutOptions; -use codex_tools::WebSearchToolOptions; -use codex_tools::augment_tool_spec_for_code_mode; -use codex_tools::collect_code_mode_tool_definitions; -use codex_tools::collect_tool_search_app_infos; -use codex_tools::collect_tool_suggest_entries; -use codex_tools::create_apply_patch_freeform_tool; -use codex_tools::create_apply_patch_json_tool; -use codex_tools::create_assign_task_tool; -use codex_tools::create_close_agent_tool_v1; -use codex_tools::create_close_agent_tool_v2; -use codex_tools::create_code_mode_tool; -use codex_tools::create_exec_command_tool; -use codex_tools::create_image_generation_tool; -use codex_tools::create_js_repl_reset_tool; -use codex_tools::create_js_repl_tool; -use codex_tools::create_list_agents_tool; -use codex_tools::create_list_dir_tool; -use codex_tools::create_list_mcp_resource_templates_tool; -use codex_tools::create_list_mcp_resources_tool; -use codex_tools::create_local_shell_tool; -use codex_tools::create_read_mcp_resource_tool; -use codex_tools::create_report_agent_job_result_tool; -use codex_tools::create_request_permissions_tool; -use codex_tools::create_request_user_input_tool; -use codex_tools::create_resume_agent_tool; -use codex_tools::create_send_input_tool_v1; -use codex_tools::create_send_message_tool; -use codex_tools::create_shell_command_tool; -use codex_tools::create_shell_tool; -use codex_tools::create_spawn_agent_tool_v1; -use codex_tools::create_spawn_agent_tool_v2; -use codex_tools::create_spawn_agents_on_csv_tool; -use codex_tools::create_test_sync_tool; -use codex_tools::create_tool_search_tool; -use codex_tools::create_tool_suggest_tool; -use codex_tools::create_update_plan_tool; -use codex_tools::create_view_image_tool; -use codex_tools::create_wait_agent_tool_v1; -use codex_tools::create_wait_agent_tool_v2; -use codex_tools::create_wait_tool; -use codex_tools::create_web_search_tool; -use codex_tools::create_write_stdin_tool; -use codex_tools::dynamic_tool_to_responses_api_tool; -use codex_tools::mcp_tool_to_responses_api_tool; -use codex_tools::request_permissions_tool_description; -use codex_tools::request_user_input_tool_description; +use codex_tools::build_tool_registry_plan; use std::collections::HashMap; - -#[cfg(test)] -pub(crate) use codex_tools::mcp_call_tool_result_output_schema; +use std::sync::Arc; pub(crate) fn tool_user_shell_type(user_shell: &Shell) -> ToolUserShellType { match user_shell.shell_type { @@ -88,49 +29,6 @@ pub(crate) fn tool_user_shell_type(user_shell: &Shell) -> ToolUserShellType { } } -fn agent_type_description(config: &ToolsConfig) -> String { - if config.agent_type_description.is_empty() { - crate::agent::role::spawn_tool_spec::build(&std::collections::BTreeMap::new()) - } else { - config.agent_type_description.clone() - } -} - -fn push_tool_spec( - builder: &mut ToolRegistryBuilder, - spec: ToolSpec, - supports_parallel_tool_calls: bool, - code_mode_enabled: bool, -) { - let spec = if code_mode_enabled { - augment_tool_spec_for_code_mode(spec) - } else { - spec - }; - if supports_parallel_tool_calls { - builder.push_spec_with_parallel_support(spec, /*supports_parallel_tool_calls*/ true); - } else { - builder.push_spec(spec); - } -} - -/// Builds the tool registry builder while collecting tool specs for later serialization. -#[cfg(test)] -pub(crate) fn build_specs( - config: &ToolsConfig, - mcp_tools: Option>, - app_tools: Option>, - dynamic_tools: &[DynamicToolSpec], -) -> ToolRegistryBuilder { - build_specs_with_discoverable_tools( - config, - mcp_tools, - app_tools, - /*discoverable_tools*/ None, - dynamic_tools, - ) -} - pub(crate) fn build_specs_with_discoverable_tools( config: &ToolsConfig, mcp_tools: Option>, @@ -168,10 +66,38 @@ pub(crate) fn build_specs_with_discoverable_tools( use crate::tools::handlers::multi_agents_v2::SendMessageHandler as SendMessageHandlerV2; use crate::tools::handlers::multi_agents_v2::SpawnAgentHandler as SpawnAgentHandlerV2; use crate::tools::handlers::multi_agents_v2::WaitAgentHandler as WaitAgentHandlerV2; - use std::sync::Arc; let mut builder = ToolRegistryBuilder::new(); - + let app_tool_sources = app_tools.as_ref().map(|app_tools| { + app_tools + .values() + .map(|tool| ToolRegistryPlanAppTool { + tool_name: tool.tool_name.as_str(), + tool_namespace: tool.tool_namespace.as_str(), + server_name: tool.server_name.as_str(), + connector_name: tool.connector_name.as_deref(), + connector_description: tool.connector_description.as_deref(), + }) + .collect::>() + }); + let default_agent_type_description = + crate::agent::role::spawn_tool_spec::build(&std::collections::BTreeMap::new()); + let plan = build_tool_registry_plan( + config, + ToolRegistryPlanParams { + mcp_tools: mcp_tools.as_ref(), + app_tools: app_tool_sources.as_deref(), + discoverable_tools: discoverable_tools.as_deref(), + dynamic_tools, + default_agent_type_description: &default_agent_type_description, + wait_agent_timeouts: WaitAgentTimeoutOptions { + default_timeout_ms: DEFAULT_WAIT_TIMEOUT_MS, + min_timeout_ms: MIN_WAIT_TIMEOUT_MS, + max_timeout_ms: MAX_WAIT_TIMEOUT_MS, + }, + codex_apps_mcp_server_name: CODEX_APPS_MCP_SERVER_NAME, + }, + ); let shell_handler = Arc::new(ShellHandler); let unified_exec_handler = Arc::new(UnifiedExecHandler); let plan_handler = Arc::new(PlanHandler); @@ -185,468 +111,130 @@ pub(crate) fn build_specs_with_discoverable_tools( let request_user_input_handler = Arc::new(RequestUserInputHandler { default_mode_request_user_input: config.default_mode_request_user_input, }); + let mut tool_search_handler = None; let tool_suggest_handler = Arc::new(ToolSuggestHandler); let code_mode_handler = Arc::new(CodeModeExecuteHandler); let code_mode_wait_handler = Arc::new(CodeModeWaitHandler); let js_repl_handler = Arc::new(JsReplHandler); let js_repl_reset_handler = Arc::new(JsReplResetHandler); - let exec_permission_approvals_enabled = config.exec_permission_approvals_enabled; - if config.code_mode_enabled { - let nested_config = config.for_code_mode_nested_tools(); - let (nested_specs, _) = build_specs_with_discoverable_tools( - &nested_config, - mcp_tools.clone(), - app_tools.clone(), - /*discoverable_tools*/ None, - dynamic_tools, - ) - .build(); - let enabled_tools = collect_code_mode_tool_definitions( - nested_specs - .iter() - .map(|configured_tool| &configured_tool.spec), - ) - .into_iter() - .map(|tool| (tool.name, tool.description)) - .collect::>(); - push_tool_spec( - &mut builder, - create_code_mode_tool(&enabled_tools, config.code_mode_only_enabled), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - builder.register_handler(PUBLIC_TOOL_NAME, code_mode_handler); - push_tool_spec( - &mut builder, - create_wait_tool(), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - builder.register_handler(WAIT_TOOL_NAME, code_mode_wait_handler); - } - - match &config.shell_type { - ConfigShellToolType::Default => { - push_tool_spec( - &mut builder, - create_shell_tool(ShellToolOptions { - exec_permission_approvals_enabled, - }), - /*supports_parallel_tool_calls*/ true, - config.code_mode_enabled, + for spec in plan.specs { + if spec.supports_parallel_tool_calls { + builder.push_spec_with_parallel_support( + spec.spec, /*supports_parallel_tool_calls*/ true, ); - } - ConfigShellToolType::Local => { - push_tool_spec( - &mut builder, - create_local_shell_tool(), - /*supports_parallel_tool_calls*/ true, - config.code_mode_enabled, - ); - } - ConfigShellToolType::UnifiedExec => { - push_tool_spec( - &mut builder, - create_exec_command_tool(CommandToolOptions { - allow_login_shell: config.allow_login_shell, - exec_permission_approvals_enabled, - }), - /*supports_parallel_tool_calls*/ true, - config.code_mode_enabled, - ); - push_tool_spec( - &mut builder, - create_write_stdin_tool(), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - builder.register_handler("exec_command", unified_exec_handler.clone()); - builder.register_handler("write_stdin", unified_exec_handler); - } - ConfigShellToolType::Disabled => { - // Do nothing. - } - ConfigShellToolType::ShellCommand => { - push_tool_spec( - &mut builder, - create_shell_command_tool(CommandToolOptions { - allow_login_shell: config.allow_login_shell, - exec_permission_approvals_enabled, - }), - /*supports_parallel_tool_calls*/ true, - config.code_mode_enabled, - ); - } - } - - if config.shell_type != ConfigShellToolType::Disabled { - // Always register shell aliases so older prompts remain compatible. - builder.register_handler("shell", shell_handler.clone()); - builder.register_handler("container.exec", shell_handler.clone()); - builder.register_handler("local_shell", shell_handler); - builder.register_handler("shell_command", shell_command_handler); - } - - if mcp_tools.is_some() { - push_tool_spec( - &mut builder, - create_list_mcp_resources_tool(), - /*supports_parallel_tool_calls*/ true, - config.code_mode_enabled, - ); - push_tool_spec( - &mut builder, - create_list_mcp_resource_templates_tool(), - /*supports_parallel_tool_calls*/ true, - config.code_mode_enabled, - ); - push_tool_spec( - &mut builder, - create_read_mcp_resource_tool(), - /*supports_parallel_tool_calls*/ true, - config.code_mode_enabled, - ); - builder.register_handler("list_mcp_resources", mcp_resource_handler.clone()); - builder.register_handler("list_mcp_resource_templates", mcp_resource_handler.clone()); - builder.register_handler("read_mcp_resource", mcp_resource_handler); - } - - push_tool_spec( - &mut builder, - create_update_plan_tool(), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - builder.register_handler("update_plan", plan_handler); - - if config.js_repl_enabled { - push_tool_spec( - &mut builder, - create_js_repl_tool(), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - push_tool_spec( - &mut builder, - create_js_repl_reset_tool(), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - builder.register_handler("js_repl", js_repl_handler); - builder.register_handler("js_repl_reset", js_repl_reset_handler); - } - - if config.request_user_input { - push_tool_spec( - &mut builder, - create_request_user_input_tool(request_user_input_tool_description( - config.default_mode_request_user_input, - )), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - builder.register_handler(REQUEST_USER_INPUT_TOOL_NAME, request_user_input_handler); - } - - if config.request_permissions_tool_enabled { - push_tool_spec( - &mut builder, - create_request_permissions_tool(request_permissions_tool_description()), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - builder.register_handler("request_permissions", request_permissions_handler); - } - - if config.search_tool - && let Some(app_tools) = app_tools - { - let search_tool_handler = Arc::new(ToolSearchHandler::new(app_tools.clone())); - let search_app_infos = collect_tool_search_app_infos( - app_tools.values().map(|tool| ToolSearchAppSource { - server_name: &tool.server_name, - connector_name: tool.connector_name.as_deref(), - connector_description: tool.connector_description.as_deref(), - }), - CODEX_APPS_MCP_SERVER_NAME, - ); - push_tool_spec( - &mut builder, - create_tool_search_tool(&search_app_infos, TOOL_SEARCH_DEFAULT_LIMIT), - /*supports_parallel_tool_calls*/ true, - config.code_mode_enabled, - ); - builder.register_handler(TOOL_SEARCH_TOOL_NAME, search_tool_handler); - - for tool in app_tools.values() { - let alias_name = - tool_handler_key(tool.tool_name.as_str(), Some(tool.tool_namespace.as_str())); - - builder.register_handler(alias_name, mcp_handler.clone()); - } - } - - if config.tool_suggest - && let Some(discoverable_tools) = discoverable_tools - .as_ref() - .filter(|tools| !tools.is_empty()) - { - builder.push_spec_with_parallel_support( - create_tool_suggest_tool(&collect_tool_suggest_entries(discoverable_tools)), - /*supports_parallel_tool_calls*/ true, - ); - builder.register_handler(TOOL_SUGGEST_TOOL_NAME, tool_suggest_handler); - } - - if let Some(apply_patch_tool_type) = &config.apply_patch_tool_type { - match apply_patch_tool_type { - ApplyPatchToolType::Freeform => { - push_tool_spec( - &mut builder, - create_apply_patch_freeform_tool(), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - } - ApplyPatchToolType::Function => { - push_tool_spec( - &mut builder, - create_apply_patch_json_tool(), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - } - } - builder.register_handler("apply_patch", apply_patch_handler); - } - - if config - .experimental_supported_tools - .iter() - .any(|tool| tool == "list_dir") - { - let list_dir_handler = Arc::new(ListDirHandler); - push_tool_spec( - &mut builder, - create_list_dir_tool(), - /*supports_parallel_tool_calls*/ true, - config.code_mode_enabled, - ); - builder.register_handler("list_dir", list_dir_handler); - } - - if config - .experimental_supported_tools - .contains(&"test_sync_tool".to_string()) - { - let test_sync_handler = Arc::new(TestSyncHandler); - push_tool_spec( - &mut builder, - create_test_sync_tool(), - /*supports_parallel_tool_calls*/ true, - config.code_mode_enabled, - ); - builder.register_handler("test_sync_tool", test_sync_handler); - } - - if let Some(web_search_tool) = create_web_search_tool(WebSearchToolOptions { - web_search_mode: config.web_search_mode, - web_search_config: config.web_search_config.as_ref(), - web_search_tool_type: config.web_search_tool_type, - }) { - push_tool_spec( - &mut builder, - web_search_tool, - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - } - - if config.image_gen_tool { - push_tool_spec( - &mut builder, - create_image_generation_tool("png"), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - } - - push_tool_spec( - &mut builder, - create_view_image_tool(ViewImageToolOptions { - can_request_original_image_detail: config.can_request_original_image_detail, - }), - /*supports_parallel_tool_calls*/ true, - config.code_mode_enabled, - ); - builder.register_handler("view_image", view_image_handler); - - if config.collab_tools { - if config.multi_agent_v2 { - let agent_type_description = agent_type_description(config); - push_tool_spec( - &mut builder, - create_spawn_agent_tool_v2(SpawnAgentToolOptions { - available_models: &config.available_models, - agent_type_description, - }), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - push_tool_spec( - &mut builder, - create_send_message_tool(), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - push_tool_spec( - &mut builder, - create_assign_task_tool(), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - push_tool_spec( - &mut builder, - create_wait_agent_tool_v2(WaitAgentTimeoutOptions { - default_timeout_ms: DEFAULT_WAIT_TIMEOUT_MS, - min_timeout_ms: MIN_WAIT_TIMEOUT_MS, - max_timeout_ms: MAX_WAIT_TIMEOUT_MS, - }), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - push_tool_spec( - &mut builder, - create_close_agent_tool_v2(), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - push_tool_spec( - &mut builder, - create_list_agents_tool(), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - builder.register_handler("spawn_agent", Arc::new(SpawnAgentHandlerV2)); - builder.register_handler("send_message", Arc::new(SendMessageHandlerV2)); - builder.register_handler("assign_task", Arc::new(AssignTaskHandlerV2)); - builder.register_handler("wait_agent", Arc::new(WaitAgentHandlerV2)); - builder.register_handler("close_agent", Arc::new(CloseAgentHandlerV2)); - builder.register_handler("list_agents", Arc::new(ListAgentsHandlerV2)); } else { - let agent_type_description = agent_type_description(config); - push_tool_spec( - &mut builder, - create_spawn_agent_tool_v1(SpawnAgentToolOptions { - available_models: &config.available_models, - agent_type_description, - }), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - push_tool_spec( - &mut builder, - create_send_input_tool_v1(), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - push_tool_spec( - &mut builder, - create_resume_agent_tool(), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - builder.register_handler("resume_agent", Arc::new(ResumeAgentHandler)); - push_tool_spec( - &mut builder, - create_wait_agent_tool_v1(WaitAgentTimeoutOptions { - default_timeout_ms: DEFAULT_WAIT_TIMEOUT_MS, - min_timeout_ms: MIN_WAIT_TIMEOUT_MS, - max_timeout_ms: MAX_WAIT_TIMEOUT_MS, - }), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - push_tool_spec( - &mut builder, - create_close_agent_tool_v1(), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - builder.register_handler("spawn_agent", Arc::new(SpawnAgentHandler)); - builder.register_handler("send_input", Arc::new(SendInputHandler)); - builder.register_handler("wait_agent", Arc::new(WaitAgentHandler)); - builder.register_handler("close_agent", Arc::new(CloseAgentHandler)); + builder.push_spec(spec.spec); } } - if config.agent_jobs_tools { - let agent_jobs_handler = Arc::new(BatchJobHandler); - push_tool_spec( - &mut builder, - create_spawn_agents_on_csv_tool(), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - builder.register_handler("spawn_agents_on_csv", agent_jobs_handler.clone()); - if config.agent_jobs_worker_tools { - push_tool_spec( - &mut builder, - create_report_agent_job_result_tool(), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - builder.register_handler("report_agent_job_result", agent_jobs_handler); - } - } - - if let Some(mcp_tools) = mcp_tools { - let mut entries: Vec<(String, rmcp::model::Tool)> = mcp_tools.into_iter().collect(); - entries.sort_by(|a, b| a.0.cmp(&b.0)); - - for (name, tool) in entries.into_iter() { - match mcp_tool_to_responses_api_tool(name.clone(), &tool) { - Ok(converted_tool) => { - push_tool_spec( - &mut builder, - ToolSpec::Function(converted_tool), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - builder.register_handler(name, mcp_handler.clone()); + for handler in plan.handlers { + match handler.kind { + ToolHandlerKind::AgentJobs => { + builder.register_handler(handler.name, Arc::new(BatchJobHandler)); + } + ToolHandlerKind::ApplyPatch => { + builder.register_handler(handler.name, apply_patch_handler.clone()); + } + ToolHandlerKind::AssignTaskV2 => { + builder.register_handler(handler.name, Arc::new(AssignTaskHandlerV2)); + } + ToolHandlerKind::CloseAgentV1 => { + builder.register_handler(handler.name, Arc::new(CloseAgentHandler)); + } + ToolHandlerKind::CloseAgentV2 => { + builder.register_handler(handler.name, Arc::new(CloseAgentHandlerV2)); + } + ToolHandlerKind::CodeModeExecute => { + builder.register_handler(handler.name, code_mode_handler.clone()); + } + ToolHandlerKind::CodeModeWait => { + builder.register_handler(handler.name, code_mode_wait_handler.clone()); + } + ToolHandlerKind::DynamicTool => { + builder.register_handler(handler.name, dynamic_tool_handler.clone()); + } + ToolHandlerKind::JsRepl => { + builder.register_handler(handler.name, js_repl_handler.clone()); + } + ToolHandlerKind::JsReplReset => { + builder.register_handler(handler.name, js_repl_reset_handler.clone()); + } + ToolHandlerKind::ListAgentsV2 => { + builder.register_handler(handler.name, Arc::new(ListAgentsHandlerV2)); + } + ToolHandlerKind::ListDir => { + builder.register_handler(handler.name, Arc::new(ListDirHandler)); + } + ToolHandlerKind::Mcp => { + builder.register_handler(handler.name, mcp_handler.clone()); + } + ToolHandlerKind::McpResource => { + builder.register_handler(handler.name, mcp_resource_handler.clone()); + } + ToolHandlerKind::Plan => { + builder.register_handler(handler.name, plan_handler.clone()); + } + ToolHandlerKind::RequestPermissions => { + builder.register_handler(handler.name, request_permissions_handler.clone()); + } + ToolHandlerKind::RequestUserInput => { + builder.register_handler(handler.name, request_user_input_handler.clone()); + } + ToolHandlerKind::ResumeAgentV1 => { + builder.register_handler(handler.name, Arc::new(ResumeAgentHandler)); + } + ToolHandlerKind::SendInputV1 => { + builder.register_handler(handler.name, Arc::new(SendInputHandler)); + } + ToolHandlerKind::SendMessageV2 => { + builder.register_handler(handler.name, Arc::new(SendMessageHandlerV2)); + } + ToolHandlerKind::Shell => { + builder.register_handler(handler.name, shell_handler.clone()); + } + ToolHandlerKind::ShellCommand => { + builder.register_handler(handler.name, shell_command_handler.clone()); + } + ToolHandlerKind::SpawnAgentV1 => { + builder.register_handler(handler.name, Arc::new(SpawnAgentHandler)); + } + ToolHandlerKind::SpawnAgentV2 => { + builder.register_handler(handler.name, Arc::new(SpawnAgentHandlerV2)); + } + ToolHandlerKind::TestSync => { + builder.register_handler(handler.name, Arc::new(TestSyncHandler)); + } + ToolHandlerKind::ToolSearch => { + if tool_search_handler.is_none() { + tool_search_handler = app_tools + .as_ref() + .map(|app_tools| Arc::new(ToolSearchHandler::new(app_tools.clone()))); } - Err(e) => { - tracing::error!("Failed to convert {name:?} MCP tool to OpenAI tool: {e:?}"); + if let Some(tool_search_handler) = tool_search_handler.as_ref() { + builder.register_handler(handler.name, tool_search_handler.clone()); } } + ToolHandlerKind::ToolSuggest => { + builder.register_handler(handler.name, tool_suggest_handler.clone()); + } + ToolHandlerKind::UnifiedExec => { + builder.register_handler(handler.name, unified_exec_handler.clone()); + } + ToolHandlerKind::ViewImage => { + builder.register_handler(handler.name, view_image_handler.clone()); + } + ToolHandlerKind::WaitAgentV1 => { + builder.register_handler(handler.name, Arc::new(WaitAgentHandler)); + } + ToolHandlerKind::WaitAgentV2 => { + builder.register_handler(handler.name, Arc::new(WaitAgentHandlerV2)); + } } } - - if !dynamic_tools.is_empty() { - for tool in dynamic_tools { - match dynamic_tool_to_responses_api_tool(tool) { - Ok(converted_tool) => { - push_tool_spec( - &mut builder, - ToolSpec::Function(converted_tool), - /*supports_parallel_tool_calls*/ false, - config.code_mode_enabled, - ); - builder.register_handler(tool.name.clone(), dynamic_tool_handler.clone()); - } - Err(e) => { - tracing::error!( - "Failed to convert dynamic tool {:?} to OpenAI tool: {e:?}", - tool.name - ); - } - } - } - } - builder } + #[cfg(test)] #[path = "spec_tests.rs"] mod tests; diff --git a/codex-rs/core/src/tools/spec_tests.rs b/codex-rs/core/src/tools/spec_tests.rs index b4b68da25d..4e73be3502 100644 --- a/codex-rs/core/src/tools/spec_tests.rs +++ b/codex-rs/core/src/tools/spec_tests.rs @@ -4,62 +4,35 @@ use crate::models_manager::model_info::with_config_overrides; use crate::shell::Shell; use crate::shell::ShellType; use crate::tools::ToolRouter; +use crate::tools::registry::tool_handler_key; use crate::tools::router::ToolRouterParams; use codex_app_server_protocol::AppInfo; use codex_features::Feature; use codex_features::Features; use codex_mcp::mcp::CODEX_APPS_MCP_SERVER_NAME; -use codex_protocol::config_types::WebSearchConfig; use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WindowsSandboxLevel; -use codex_protocol::models::VIEW_IMAGE_TOOL_NAME; -use codex_protocol::openai_models::InputModality; +use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelsResponse; -use codex_protocol::openai_models::WebSearchToolType; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; -use codex_protocol::protocol::SubAgentSource; -use codex_tools::AdditionalProperties; -use codex_tools::CommandToolOptions; use codex_tools::ConfiguredToolSpec; -use codex_tools::DiscoverablePluginInfo; use codex_tools::DiscoverableTool; -use codex_tools::FreeformTool; use codex_tools::JsonSchema; use codex_tools::ResponsesApiTool; -use codex_tools::ResponsesApiWebSearchFilters; -use codex_tools::ResponsesApiWebSearchUserLocation; use codex_tools::ShellCommandBackendConfig; -use codex_tools::SpawnAgentToolOptions; +use codex_tools::TOOL_SEARCH_TOOL_NAME; +use codex_tools::TOOL_SUGGEST_TOOL_NAME; +use codex_tools::ToolSpec; use codex_tools::ToolsConfig; use codex_tools::ToolsConfigParams; use codex_tools::UnifiedExecShellMode; -use codex_tools::ViewImageToolOptions; -use codex_tools::WaitAgentTimeoutOptions; use codex_tools::ZshForkConfig; -use codex_tools::create_apply_patch_freeform_tool; -use codex_tools::create_close_agent_tool_v1; -use codex_tools::create_close_agent_tool_v2; -use codex_tools::create_exec_command_tool; -use codex_tools::create_request_permissions_tool; -use codex_tools::create_request_user_input_tool; -use codex_tools::create_resume_agent_tool; -use codex_tools::create_send_input_tool_v1; -use codex_tools::create_send_message_tool; -use codex_tools::create_spawn_agent_tool_v1; -use codex_tools::create_spawn_agent_tool_v2; -use codex_tools::create_update_plan_tool; -use codex_tools::create_view_image_tool; -use codex_tools::create_wait_agent_tool_v1; -use codex_tools::create_wait_agent_tool_v2; -use codex_tools::create_write_stdin_tool; +use codex_tools::mcp_call_tool_result_output_schema; use codex_tools::mcp_tool_to_deferred_responses_api_tool; -use codex_tools::request_permissions_tool_description; -use codex_tools::request_user_input_tool_description; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; -use serde_json::json; use std::collections::BTreeMap; use std::path::PathBuf; @@ -169,17 +142,6 @@ fn assert_contains_tool_names(tools: &[ConfiguredToolSpec], expected_subset: &[& } } -fn assert_lacks_tool_name(tools: &[ConfiguredToolSpec], expected_absent: &str) { - let names = tools - .iter() - .map(ConfiguredToolSpec::name) - .collect::>(); - assert!( - !names.contains(&expected_absent), - "expected tool {expected_absent} to be absent; had: {names:?}" - ); -} - fn shell_tool_name(config: &ToolsConfig) -> Option<&'static str> { match config.shell_type { ConfigShellToolType::Default => Some("shell"), @@ -190,27 +152,6 @@ fn shell_tool_name(config: &ToolsConfig) -> Option<&'static str> { } } -fn request_user_input_tool_spec(default_mode_request_user_input: bool) -> ToolSpec { - create_request_user_input_tool(request_user_input_tool_description( - default_mode_request_user_input, - )) -} - -fn spawn_agent_tool_options(config: &ToolsConfig) -> SpawnAgentToolOptions<'_> { - SpawnAgentToolOptions { - available_models: &config.available_models, - agent_type_description: agent_type_description(config), - } -} - -fn wait_agent_timeout_options() -> WaitAgentTimeoutOptions { - WaitAgentTimeoutOptions { - default_timeout_ms: DEFAULT_WAIT_TIMEOUT_MS, - min_timeout_ms: MIN_WAIT_TIMEOUT_MS, - max_timeout_ms: MAX_WAIT_TIMEOUT_MS, - } -} - fn find_tool<'a>(tools: &'a [ConfiguredToolSpec], expected_name: &str) -> &'a ConfiguredToolSpec { tools .iter() @@ -218,45 +159,6 @@ fn find_tool<'a>(tools: &'a [ConfiguredToolSpec], expected_name: &str) -> &'a Co .unwrap_or_else(|| panic!("expected tool {expected_name}")) } -fn strip_descriptions_schema(schema: &mut JsonSchema) { - match schema { - JsonSchema::Boolean { description } - | JsonSchema::String { description } - | JsonSchema::Number { description } => { - *description = None; - } - JsonSchema::Array { items, description } => { - strip_descriptions_schema(items); - *description = None; - } - JsonSchema::Object { - properties, - required: _, - additional_properties, - } => { - for v in properties.values_mut() { - strip_descriptions_schema(v); - } - if let Some(AdditionalProperties::Schema(s)) = additional_properties { - strip_descriptions_schema(s); - } - } - } -} - -fn strip_descriptions_tool(spec: &mut ToolSpec) { - match spec { - ToolSpec::ToolSearch { parameters, .. } => strip_descriptions_schema(parameters), - ToolSpec::Function(ResponsesApiTool { parameters, .. }) => { - strip_descriptions_schema(parameters); - } - ToolSpec::Freeform(_) - | ToolSpec::LocalShell {} - | ToolSpec::ImageGeneration { .. } - | ToolSpec::WebSearch { .. } => {} - } -} - fn model_info_from_models_json(slug: &str) -> ModelInfo { let config = test_config(); let response: ModelsResponse = @@ -269,6 +171,22 @@ fn model_info_from_models_json(slug: &str) -> ModelInfo { with_config_overrides(model, &config) } +/// Builds the tool registry builder while collecting tool specs for later serialization. +fn build_specs( + config: &ToolsConfig, + mcp_tools: Option>, + app_tools: Option>, + dynamic_tools: &[DynamicToolSpec], +) -> ToolRegistryBuilder { + build_specs_with_discoverable_tools( + config, + mcp_tools, + app_tools, + /*discoverable_tools*/ None, + dynamic_tools, + ) +} + #[test] fn model_provided_unified_exec_is_blocked_for_windows_sandboxed_policies() { let mut model_info = model_info_from_models_json("gpt-5-codex"); @@ -293,607 +211,6 @@ fn model_provided_unified_exec_is_blocked_for_windows_sandboxed_policies() { assert_eq!(config.shell_type, expected_shell_type); } -#[test] -fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() { - let model_info = model_info_from_models_json("gpt-5-codex"); - let mut features = Features::with_defaults(); - features.enable(Feature::UnifiedExec); - let available_models = Vec::new(); - let config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Live), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs( - &config, - /*mcp_tools*/ None, - /*app_tools*/ None, - &[], - ) - .build(); - - // Build actual map name -> spec - use std::collections::BTreeMap; - use std::collections::HashSet; - let mut actual: BTreeMap = BTreeMap::from([]); - let mut duplicate_names = Vec::new(); - for t in &tools { - let name = t.name().to_string(); - if actual.insert(name.clone(), t.spec.clone()).is_some() { - duplicate_names.push(name); - } - } - assert!( - duplicate_names.is_empty(), - "duplicate tool entries detected: {duplicate_names:?}" - ); - - // Build expected from the same helpers used by the builder. - let mut expected: BTreeMap = BTreeMap::from([]); - for spec in [ - create_exec_command_tool(CommandToolOptions { - allow_login_shell: true, - exec_permission_approvals_enabled: false, - }), - create_write_stdin_tool(), - create_update_plan_tool(), - request_user_input_tool_spec(/*default_mode_request_user_input*/ false), - create_apply_patch_freeform_tool(), - ToolSpec::WebSearch { - external_web_access: Some(true), - filters: None, - user_location: None, - search_context_size: None, - search_content_types: None, - }, - create_view_image_tool(ViewImageToolOptions { - can_request_original_image_detail: config.can_request_original_image_detail, - }), - ] { - expected.insert(spec.name().to_string(), spec); - } - let collab_specs = if config.multi_agent_v2 { - vec![ - create_spawn_agent_tool_v2(spawn_agent_tool_options(&config)), - create_send_message_tool(), - create_wait_agent_tool_v2(wait_agent_timeout_options()), - create_close_agent_tool_v2(), - ] - } else { - vec![ - create_spawn_agent_tool_v1(spawn_agent_tool_options(&config)), - create_send_input_tool_v1(), - create_wait_agent_tool_v1(wait_agent_timeout_options()), - create_close_agent_tool_v1(), - ] - }; - for spec in collab_specs { - expected.insert(spec.name().to_string(), spec); - } - if !config.multi_agent_v2 { - let spec = create_resume_agent_tool(); - expected.insert(spec.name().to_string(), spec); - } - - if config.exec_permission_approvals_enabled { - let spec = create_request_permissions_tool(request_permissions_tool_description()); - expected.insert(spec.name().to_string(), spec); - } - - // Exact name set match — this is the only test allowed to fail when tools change. - let actual_names: HashSet<_> = actual.keys().cloned().collect(); - let expected_names: HashSet<_> = expected.keys().cloned().collect(); - assert_eq!(actual_names, expected_names, "tool name set mismatch"); - - // Compare specs ignoring human-readable descriptions. - for name in expected.keys() { - let mut a = actual.get(name).expect("present").clone(); - let mut e = expected.get(name).expect("present").clone(); - strip_descriptions_tool(&mut a); - strip_descriptions_tool(&mut e); - assert_eq!(a, e, "spec mismatch for {name}"); - } -} - -#[test] -fn test_build_specs_collab_tools_enabled() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); - let mut features = Features::with_defaults(); - features.enable(Feature::Collab); - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - &[], - ) - .build(); - assert_contains_tool_names( - &tools, - &["spawn_agent", "send_input", "wait_agent", "close_agent"], - ); - assert_lacks_tool_name(&tools, "spawn_agents_on_csv"); - assert_lacks_tool_name(&tools, "list_agents"); - - let spawn_agent = find_tool(&tools, "spawn_agent"); - let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &spawn_agent.spec else { - panic!("spawn_agent should be a function tool"); - }; - let JsonSchema::Object { properties, .. } = parameters else { - panic!("spawn_agent should use object params"); - }; - assert!(properties.contains_key("fork_context")); - assert!(!properties.contains_key("fork_turns")); -} - -#[test] -fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); - let mut features = Features::with_defaults(); - features.enable(Feature::Collab); - features.enable(Feature::MultiAgentV2); - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - &[], - ) - .build(); - assert_contains_tool_names( - &tools, - &[ - "spawn_agent", - "send_message", - "assign_task", - "wait_agent", - "close_agent", - "list_agents", - ], - ); - - let spawn_agent = find_tool(&tools, "spawn_agent"); - let ToolSpec::Function(ResponsesApiTool { - parameters, - output_schema, - .. - }) = &spawn_agent.spec - else { - panic!("spawn_agent should be a function tool"); - }; - let JsonSchema::Object { - properties, - required, - .. - } = parameters - else { - panic!("spawn_agent should use object params"); - }; - assert!(properties.contains_key("task_name")); - assert!(properties.contains_key("message")); - assert!(properties.contains_key("fork_turns")); - assert!(!properties.contains_key("items")); - assert!(!properties.contains_key("fork_context")); - assert_eq!( - required.as_ref(), - Some(&vec!["task_name".to_string(), "message".to_string()]) - ); - let output_schema = output_schema - .as_ref() - .expect("spawn_agent should define output schema"); - assert_eq!( - output_schema["required"], - json!(["agent_id", "task_name", "nickname"]) - ); - - let send_message = find_tool(&tools, "send_message"); - let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &send_message.spec else { - panic!("send_message should be a function tool"); - }; - let JsonSchema::Object { - properties, - required, - .. - } = parameters - else { - panic!("send_message should use object params"); - }; - assert!(properties.contains_key("target")); - assert!(!properties.contains_key("interrupt")); - assert!(properties.contains_key("message")); - assert!(!properties.contains_key("items")); - assert_eq!( - required.as_ref(), - Some(&vec!["target".to_string(), "message".to_string()]) - ); - - let assign_task = find_tool(&tools, "assign_task"); - let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &assign_task.spec else { - panic!("assign_task should be a function tool"); - }; - let JsonSchema::Object { - properties, - required, - .. - } = parameters - else { - panic!("assign_task should use object params"); - }; - assert!(properties.contains_key("target")); - assert!(properties.contains_key("message")); - assert!(!properties.contains_key("items")); - assert_eq!( - required.as_ref(), - Some(&vec!["target".to_string(), "message".to_string()]) - ); - - let wait_agent = find_tool(&tools, "wait_agent"); - let ToolSpec::Function(ResponsesApiTool { - parameters, - output_schema, - .. - }) = &wait_agent.spec - else { - panic!("wait_agent should be a function tool"); - }; - let JsonSchema::Object { - properties, - required, - .. - } = parameters - else { - panic!("wait_agent should use object params"); - }; - assert!(!properties.contains_key("targets")); - assert!(properties.contains_key("timeout_ms")); - assert_eq!(required, &None); - let output_schema = output_schema - .as_ref() - .expect("wait_agent should define output schema"); - assert_eq!( - output_schema["properties"]["message"]["description"], - json!("Brief wait summary without the agent's final content.") - ); - - let list_agents = find_tool(&tools, "list_agents"); - let ToolSpec::Function(ResponsesApiTool { - parameters, - output_schema, - .. - }) = &list_agents.spec - else { - panic!("list_agents should be a function tool"); - }; - let JsonSchema::Object { - properties, - required, - .. - } = parameters - else { - panic!("list_agents should use object params"); - }; - assert!(properties.contains_key("path_prefix")); - assert_eq!(required.as_ref(), None); - let output_schema = output_schema - .as_ref() - .expect("list_agents should define output schema"); - assert_eq!( - output_schema["properties"]["agents"]["items"]["required"], - json!(["agent_name", "agent_status", "last_task_message"]) - ); - assert_lacks_tool_name(&tools, "send_input"); - assert_lacks_tool_name(&tools, "resume_agent"); -} - -#[test] -fn test_build_specs_enable_fanout_enables_agent_jobs_and_collab_tools() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); - let mut features = Features::with_defaults(); - features.enable(Feature::SpawnCsv); - features.normalize_dependencies(); - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - &[], - ) - .build(); - assert_contains_tool_names( - &tools, - &[ - "spawn_agent", - "send_input", - "wait_agent", - "close_agent", - "spawn_agents_on_csv", - ], - ); -} - -#[test] -fn view_image_tool_omits_detail_without_original_detail_feature() { - let config = test_config(); - let mut model_info = - ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); - model_info.supports_image_detail_original = true; - let features = Features::with_defaults(); - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - &[], - ) - .build(); - let view_image = find_tool(&tools, VIEW_IMAGE_TOOL_NAME); - let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &view_image.spec else { - panic!("view_image should be a function tool"); - }; - let JsonSchema::Object { properties, .. } = parameters else { - panic!("view_image should use an object schema"); - }; - assert!(!properties.contains_key("detail")); -} - -#[test] -fn view_image_tool_includes_detail_with_original_detail_feature() { - let config = test_config(); - let mut model_info = - ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); - model_info.supports_image_detail_original = true; - let mut features = Features::with_defaults(); - features.enable(Feature::ImageDetailOriginal); - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - &[], - ) - .build(); - let view_image = find_tool(&tools, VIEW_IMAGE_TOOL_NAME); - let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &view_image.spec else { - panic!("view_image should be a function tool"); - }; - let JsonSchema::Object { properties, .. } = parameters else { - panic!("view_image should use an object schema"); - }; - assert!(properties.contains_key("detail")); - let Some(JsonSchema::String { - description: Some(description), - }) = properties.get("detail") - else { - panic!("view_image detail should include a description"); - }; - assert!(description.contains("only supported value is `original`")); - assert!(description.contains("omit this field for default resized behavior")); -} - -#[test] -fn test_build_specs_agent_job_worker_tools_enabled() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); - let mut features = Features::with_defaults(); - features.enable(Feature::SpawnCsv); - features.normalize_dependencies(); - features.enable(Feature::Sqlite); - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::SubAgent(SubAgentSource::Other( - "agent_job:test".to_string(), - )), - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - &[], - ) - .build(); - assert_contains_tool_names( - &tools, - &[ - "spawn_agent", - "send_input", - "resume_agent", - "wait_agent", - "close_agent", - "spawn_agents_on_csv", - "report_agent_job_result", - ], - ); - assert_lacks_tool_name(&tools, "request_user_input"); -} - -#[test] -fn request_user_input_description_reflects_default_mode_feature_flag() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); - let mut features = Features::with_defaults(); - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - &[], - ) - .build(); - let request_user_input_tool = find_tool(&tools, "request_user_input"); - assert_eq!( - request_user_input_tool.spec, - request_user_input_tool_spec(/*default_mode_request_user_input*/ false) - ); - - features.enable(Feature::DefaultModeRequestUserInput); - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - &[], - ) - .build(); - let request_user_input_tool = find_tool(&tools, "request_user_input"); - assert_eq!( - request_user_input_tool.spec, - request_user_input_tool_spec(/*default_mode_request_user_input*/ true) - ); -} - -#[test] -fn request_permissions_requires_feature_flag() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); - let features = Features::with_defaults(); - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - &[], - ) - .build(); - assert_lacks_tool_name(&tools, "request_permissions"); - - let mut features = Features::with_defaults(); - features.enable(Feature::RequestPermissionsTool); - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - &[], - ) - .build(); - let request_permissions_tool = find_tool(&tools, "request_permissions"); - assert_eq!( - request_permissions_tool.spec, - create_request_permissions_tool(request_permissions_tool_description()) - ); -} - -#[test] -fn request_permissions_tool_is_independent_from_additional_permissions() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); - let mut features = Features::with_defaults(); - features.enable(Feature::ExecPermissionApprovals); - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - &[], - ) - .build(); - - assert_lacks_tool_name(&tools, "request_permissions"); -} - #[test] fn get_memory_requires_feature_flag() { let config = test_config(); @@ -923,153 +240,6 @@ fn get_memory_requires_feature_flag() { ); } -#[test] -fn js_repl_requires_feature_flag() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); - let features = Features::with_defaults(); - - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - &[], - ) - .build(); - - assert!( - !tools.iter().any(|tool| tool.spec.name() == "js_repl"), - "js_repl should be disabled when the feature is off" - ); - assert!( - !tools.iter().any(|tool| tool.spec.name() == "js_repl_reset"), - "js_repl_reset should be disabled when the feature is off" - ); -} - -#[test] -fn js_repl_enabled_adds_tools() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); - let mut features = Features::with_defaults(); - features.enable(Feature::JsRepl); - - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - &[], - ) - .build(); - assert_contains_tool_names(&tools, &["js_repl", "js_repl_reset"]); -} - -#[test] -fn image_generation_tools_require_feature_and_supported_model() { - let config = test_config(); - let mut supported_model_info = - ModelsManager::construct_model_info_offline_for_tests("gpt-5.2", &config); - supported_model_info.slug = "custom/gpt-5.2-variant".to_string(); - let mut unsupported_model_info = supported_model_info.clone(); - unsupported_model_info.input_modalities = vec![InputModality::Text]; - let default_features = Features::with_defaults(); - let mut image_generation_features = default_features.clone(); - image_generation_features.enable(Feature::ImageGeneration); - - let available_models = Vec::new(); - let default_tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &supported_model_info, - available_models: &available_models, - features: &default_features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (default_tools, _) = build_specs( - &default_tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - &[], - ) - .build(); - assert!( - !default_tools - .iter() - .any(|tool| tool.spec.name() == "image_generation"), - "image_generation should be disabled by default" - ); - - let supported_tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &supported_model_info, - available_models: &available_models, - features: &image_generation_features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (supported_tools, _) = build_specs( - &supported_tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - &[], - ) - .build(); - assert_contains_tool_names(&supported_tools, &["image_generation"]); - let image_generation_tool = find_tool(&supported_tools, "image_generation"); - assert_eq!( - serde_json::to_value(&image_generation_tool.spec).expect("serialize image tool"), - serde_json::json!({ - "type": "image_generation", - "output_format": "png" - }) - ); - - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &unsupported_model_info, - available_models: &available_models, - features: &image_generation_features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - &[], - ) - .build(); - assert!( - !tools - .iter() - .any(|tool| tool.spec.name() == "image_generation"), - "image_generation should be disabled for unsupported models" - ); -} - fn assert_model_tools( model_slug: &str, features: &Features, @@ -1121,239 +291,6 @@ fn assert_default_model_tools( assert_model_tools(model_slug, features, web_search_mode, &expected); } -#[test] -fn web_search_mode_cached_sets_external_web_access_false() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); - let features = Features::with_defaults(); - - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - &[], - ) - .build(); - - let tool = find_tool(&tools, "web_search"); - assert_eq!( - tool.spec, - ToolSpec::WebSearch { - external_web_access: Some(false), - filters: None, - user_location: None, - search_context_size: None, - search_content_types: None, - } - ); -} - -#[test] -fn web_search_mode_live_sets_external_web_access_true() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); - let features = Features::with_defaults(); - - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Live), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - &[], - ) - .build(); - - let tool = find_tool(&tools, "web_search"); - assert_eq!( - tool.spec, - ToolSpec::WebSearch { - external_web_access: Some(true), - filters: None, - user_location: None, - search_context_size: None, - search_content_types: None, - } - ); -} - -#[test] -fn web_search_config_is_forwarded_to_tool_spec() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); - let features = Features::with_defaults(); - let web_search_config = WebSearchConfig { - filters: Some(codex_protocol::config_types::WebSearchFilters { - allowed_domains: Some(vec!["example.com".to_string()]), - }), - user_location: Some(codex_protocol::config_types::WebSearchUserLocation { - r#type: codex_protocol::config_types::WebSearchUserLocationType::Approximate, - country: Some("US".to_string()), - region: Some("California".to_string()), - city: Some("San Francisco".to_string()), - timezone: Some("America/Los_Angeles".to_string()), - }), - search_context_size: Some(codex_protocol::config_types::WebSearchContextSize::High), - }; - - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Live), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }) - .with_web_search_config(Some(web_search_config.clone())); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - &[], - ) - .build(); - - let tool = find_tool(&tools, "web_search"); - assert_eq!( - tool.spec, - ToolSpec::WebSearch { - external_web_access: Some(true), - filters: web_search_config - .filters - .map(ResponsesApiWebSearchFilters::from), - user_location: web_search_config - .user_location - .map(ResponsesApiWebSearchUserLocation::from), - search_context_size: web_search_config.search_context_size, - search_content_types: None, - } - ); -} - -#[test] -fn web_search_tool_type_text_and_image_sets_search_content_types() { - let config = test_config(); - let mut model_info = - ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); - model_info.web_search_tool_type = WebSearchToolType::TextAndImage; - let features = Features::with_defaults(); - - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Live), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - &[], - ) - .build(); - - let tool = find_tool(&tools, "web_search"); - assert_eq!( - tool.spec, - ToolSpec::WebSearch { - external_web_access: Some(true), - filters: None, - user_location: None, - search_context_size: None, - search_content_types: Some(vec!["text".to_string(), "image".to_string()]), - } - ); -} - -#[test] -fn mcp_resource_tools_are_hidden_without_mcp_servers() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); - let features = Features::with_defaults(); - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - &[], - ) - .build(); - - assert!( - !tools.iter().any(|tool| matches!( - tool.spec.name(), - "list_mcp_resources" | "list_mcp_resource_templates" | "read_mcp_resource" - )), - "MCP resource tools should be omitted when no MCP servers are configured" - ); -} - -#[test] -fn mcp_resource_tools_are_included_when_mcp_servers_are_present() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); - let features = Features::with_defaults(); - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs( - &tools_config, - Some(HashMap::new()), - /*app_tools*/ None, - &[], - ) - .build(); - - assert_contains_tool_names( - &tools, - &[ - "list_mcp_resources", - "list_mcp_resource_templates", - "read_mcp_resource", - ], - ); -} - #[test] fn test_build_specs_gpt5_codex_default() { let features = Features::with_defaults(); @@ -1661,492 +598,6 @@ fn shell_zsh_fork_prefers_shell_command_over_unified_exec() { ); } -#[test] -#[ignore] -fn test_parallel_support_flags() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); - let mut features = Features::with_defaults(); - features.enable(Feature::UnifiedExec); - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - &[], - ) - .build(); - - assert!(find_tool(&tools, "exec_command").supports_parallel_tool_calls); - assert!(!find_tool(&tools, "write_stdin").supports_parallel_tool_calls); -} - -#[test] -fn test_test_model_info_includes_sync_tool() { - let _config = test_config(); - let mut model_info = model_info_from_models_json("gpt-5-codex"); - model_info.experimental_supported_tools = vec!["test_sync_tool".to_string()]; - let features = Features::with_defaults(); - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - &[], - ) - .build(); - - assert!(tools.iter().any(|tool| tool.name() == "test_sync_tool")); -} - -#[test] -fn test_build_specs_mcp_tools_converted() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("o3", &config); - let mut features = Features::with_defaults(); - features.enable(Feature::UnifiedExec); - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Live), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs( - &tools_config, - Some(HashMap::from([( - "test_server/do_something_cool".to_string(), - mcp_tool( - "do_something_cool", - "Do something cool", - serde_json::json!({ - "type": "object", - "properties": { - "string_argument": { "type": "string" }, - "number_argument": { "type": "number" }, - "object_argument": { - "type": "object", - "properties": { - "string_property": { "type": "string" }, - "number_property": { "type": "number" }, - }, - "required": ["string_property", "number_property"], - "additionalProperties": false, - }, - }, - }), - ), - )])), - /*app_tools*/ None, - &[], - ) - .build(); - - let tool = find_tool(&tools, "test_server/do_something_cool"); - assert_eq!( - &tool.spec, - &ToolSpec::Function(ResponsesApiTool { - name: "test_server/do_something_cool".to_string(), - parameters: JsonSchema::Object { - properties: BTreeMap::from([ - ( - "string_argument".to_string(), - JsonSchema::String { description: None } - ), - ( - "number_argument".to_string(), - JsonSchema::Number { description: None } - ), - ( - "object_argument".to_string(), - JsonSchema::Object { - properties: BTreeMap::from([ - ( - "string_property".to_string(), - JsonSchema::String { description: None } - ), - ( - "number_property".to_string(), - JsonSchema::Number { description: None } - ), - ]), - required: Some(vec![ - "string_property".to_string(), - "number_property".to_string(), - ]), - additional_properties: Some(false.into()), - }, - ), - ]), - required: None, - additional_properties: None, - }, - description: "Do something cool".to_string(), - strict: false, - output_schema: Some(mcp_call_tool_result_output_schema(serde_json::json!({}))), - defer_loading: None, - }) - ); -} - -#[test] -fn test_build_specs_mcp_tools_sorted_by_name() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("o3", &config); - let mut features = Features::with_defaults(); - features.enable(Feature::UnifiedExec); - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - - // Intentionally construct a map with keys that would sort alphabetically. - let tools_map: HashMap = HashMap::from([ - ( - "test_server/do".to_string(), - mcp_tool("a", "a", serde_json::json!({"type": "object"})), - ), - ( - "test_server/something".to_string(), - mcp_tool("b", "b", serde_json::json!({"type": "object"})), - ), - ( - "test_server/cool".to_string(), - mcp_tool("c", "c", serde_json::json!({"type": "object"})), - ), - ]); - - let (tools, _) = build_specs(&tools_config, Some(tools_map), /*app_tools*/ None, &[]).build(); - - // Only assert that the MCP tools themselves are sorted by fully-qualified name. - let mcp_names: Vec<_> = tools - .iter() - .map(|t| t.name().to_string()) - .filter(|n| n.starts_with("test_server/")) - .collect(); - let expected = vec![ - "test_server/cool".to_string(), - "test_server/do".to_string(), - "test_server/something".to_string(), - ]; - assert_eq!(mcp_names, expected); -} - -#[test] -fn search_tool_description_lists_each_codex_apps_connector_once() { - let model_info = search_capable_model_info(); - let mut features = Features::with_defaults(); - features.enable(Feature::Apps); - features.enable(Feature::ToolSearch); - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - - let (tools, _) = build_specs( - &tools_config, - Some(HashMap::from([ - ( - "mcp__codex_apps__calendar_create_event".to_string(), - mcp_tool( - "calendar_create_event", - "Create calendar event", - serde_json::json!({"type": "object"}), - ), - ), - ( - "mcp__rmcp__echo".to_string(), - mcp_tool("echo", "Echo", serde_json::json!({"type": "object"})), - ), - ])), - Some(HashMap::from([ - ( - "mcp__codex_apps__calendar_create_event".to_string(), - ToolInfo { - server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(), - tool_name: "_create_event".to_string(), - tool_namespace: "mcp__codex_apps__calendar".to_string(), - tool: mcp_tool( - "calendar-create-event", - "Create calendar event", - serde_json::json!({"type": "object"}), - ), - connector_id: Some("calendar".to_string()), - connector_name: Some("Calendar".to_string()), - plugin_display_names: Vec::new(), - connector_description: Some( - "Plan events and manage your calendar.".to_string(), - ), - }, - ), - ( - "mcp__codex_apps__calendar_list_events".to_string(), - ToolInfo { - server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(), - tool_name: "_list_events".to_string(), - tool_namespace: "mcp__codex_apps__calendar".to_string(), - tool: mcp_tool( - "calendar-list-events", - "List calendar events", - serde_json::json!({"type": "object"}), - ), - connector_id: Some("calendar".to_string()), - connector_name: Some("Calendar".to_string()), - plugin_display_names: Vec::new(), - connector_description: Some( - "Plan events and manage your calendar.".to_string(), - ), - }, - ), - ( - "mcp__codex_apps__gmail_search_threads".to_string(), - ToolInfo { - server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(), - tool_name: "_search_threads".to_string(), - tool_namespace: "mcp__codex_apps__gmail".to_string(), - tool: mcp_tool( - "gmail-search-threads", - "Search email threads", - serde_json::json!({"type": "object"}), - ), - connector_id: Some("gmail".to_string()), - connector_name: Some("Gmail".to_string()), - plugin_display_names: Vec::new(), - connector_description: Some("Find and summarize email threads.".to_string()), - }, - ), - ( - "mcp__rmcp__echo".to_string(), - ToolInfo { - server_name: "rmcp".to_string(), - tool_name: "echo".to_string(), - tool_namespace: "rmcp".to_string(), - tool: mcp_tool("echo", "Echo", serde_json::json!({"type": "object"})), - connector_id: None, - connector_name: None, - plugin_display_names: Vec::new(), - connector_description: None, - }, - ), - ])), - &[], - ) - .build(); - - let search_tool = find_tool(&tools, TOOL_SEARCH_TOOL_NAME); - let ToolSpec::ToolSearch { description, .. } = &search_tool.spec else { - panic!("expected tool_search tool"); - }; - let description = description.as_str(); - assert!(description.contains("- Calendar: Plan events and manage your calendar.")); - assert!(description.contains("- Gmail: Find and summarize email threads.")); - assert_eq!( - description - .matches("- Calendar: Plan events and manage your calendar.") - .count(), - 1 - ); - assert!(!description.contains("mcp__rmcp__echo")); -} - -#[test] -fn search_tool_requires_model_capability_and_feature_flag() { - let model_info = search_capable_model_info(); - let app_tools = Some(HashMap::from([( - "mcp__codex_apps__calendar_create_event".to_string(), - ToolInfo { - server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(), - tool_name: "calendar_create_event".to_string(), - tool_namespace: "mcp__codex_apps__calendar".to_string(), - tool: mcp_tool( - "calendar_create_event", - "Create calendar event", - serde_json::json!({"type": "object"}), - ), - connector_id: Some("calendar".to_string()), - connector_name: Some("Calendar".to_string()), - connector_description: None, - plugin_display_names: Vec::new(), - }, - )])); - - let features = Features::with_defaults(); - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &ModelInfo { - supports_search_tool: false, - ..model_info.clone() - }, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - app_tools.clone(), - &[], - ) - .build(); - assert_lacks_tool_name(&tools, TOOL_SEARCH_TOOL_NAME); - - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - app_tools.clone(), - &[], - ) - .build(); - assert_lacks_tool_name(&tools, TOOL_SEARCH_TOOL_NAME); - - let mut features = Features::with_defaults(); - features.enable(Feature::ToolSearch); - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, app_tools, &[]).build(); - assert_contains_tool_names(&tools, &[TOOL_SEARCH_TOOL_NAME]); -} - -#[test] -fn tool_suggest_is_not_registered_without_feature_flag() { - let model_info = search_capable_model_info(); - let mut features = Features::with_defaults(); - features.enable(Feature::ToolSearch); - features.enable(Feature::Apps); - features.enable(Feature::Plugins); - features.disable(Feature::ToolSuggest); - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs_with_discoverable_tools( - &tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - Some(vec![discoverable_connector( - "connector_2128aebfecb84f64a069897515042a44", - "Google Calendar", - "Plan events and schedules.", - )]), - &[], - ) - .build(); - - assert!( - !tools - .iter() - .any(|tool| tool.name() == TOOL_SUGGEST_TOOL_NAME) - ); -} - -#[test] -fn tool_suggest_can_be_registered_without_search_tool() { - let model_info = ModelInfo { - supports_search_tool: false, - ..search_capable_model_info() - }; - let mut features = Features::with_defaults(); - features.enable(Feature::Apps); - features.enable(Feature::Plugins); - features.enable(Feature::ToolSuggest); - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - let (tools, _) = build_specs_with_discoverable_tools( - &tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - Some(vec![discoverable_connector( - "connector_2128aebfecb84f64a069897515042a44", - "Google Calendar", - "Plan events and schedules.", - )]), - &[], - ) - .build(); - - assert_contains_tool_names(&tools, &[TOOL_SUGGEST_TOOL_NAME]); - assert_lacks_tool_name(&tools, TOOL_SEARCH_TOOL_NAME); - - let tool_suggest = find_tool(&tools, TOOL_SUGGEST_TOOL_NAME); - let ToolSpec::Function(ResponsesApiTool { description, .. }) = &tool_suggest.spec else { - panic!("expected function tool"); - }; - assert!(description.contains( - "Suggests a missing connector in an installed plugin, or in narrower cases a not installed but discoverable plugin" - )); - assert!(description.contains( - "You've already tried to find a matching available tool for the user's request but couldn't find a good match. This includes `tool_search` (if available) and other means." - )); -} - #[test] fn tool_suggest_requires_apps_and_plugins_features() { let model_info = search_capable_model_info(); @@ -2340,111 +791,6 @@ fn search_tool_registers_namespaced_app_tool_aliases() { assert!(registry.has_handler(alias.as_str(), /*namespace*/ None)); } -#[test] -fn tool_suggest_description_lists_discoverable_tools() { - let model_info = search_capable_model_info(); - let mut features = Features::with_defaults(); - features.enable(Feature::Apps); - features.enable(Feature::Plugins); - features.enable(Feature::ToolSearch); - features.enable(Feature::ToolSuggest); - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - - let discoverable_tools = vec![ - discoverable_connector( - "connector_2128aebfecb84f64a069897515042a44", - "Google Calendar", - "Plan events and schedules.", - ), - discoverable_connector( - "connector_68df038e0ba48191908c8434991bbac2", - "Gmail", - "Find and summarize email threads.", - ), - DiscoverableTool::Plugin(Box::new(DiscoverablePluginInfo { - id: "sample@test".to_string(), - name: "Sample Plugin".to_string(), - description: None, - has_skills: true, - mcp_server_names: vec!["sample-docs".to_string()], - app_connector_ids: vec!["connector_sample".to_string()], - })), - ]; - - let (tools, _) = build_specs_with_discoverable_tools( - &tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - Some(discoverable_tools), - &[], - ) - .build(); - - let tool_suggest = find_tool(&tools, TOOL_SUGGEST_TOOL_NAME); - let ToolSpec::Function(ResponsesApiTool { - description, - parameters, - .. - }) = &tool_suggest.spec - else { - panic!("expected function tool"); - }; - assert!(description.contains( - "Suggests a missing connector in an installed plugin, or in narrower cases a not installed but discoverable plugin" - )); - assert!(description.contains("Google Calendar")); - assert!(description.contains("Gmail")); - assert!(description.contains("Sample Plugin")); - assert!(description.contains("Plan events and schedules.")); - assert!(description.contains("Find and summarize email threads.")); - assert!(description.contains("id: `sample@test`, type: plugin, action: install")); - assert!(description.contains("`action_type`: `install` or `enable`")); - assert!( - description.contains("skills; MCP servers: sample-docs; app connectors: connector_sample") - ); - assert!( - description.contains( - "You've already tried to find a matching available tool for the user's request but couldn't find a good match. This includes `tool_search` (if available) and other means." - ) - ); - assert!(description.contains( - "For connectors/apps that are not installed but needed for an installed plugin, suggest to install them if the task requirements match precisely." - )); - assert!(description.contains( - "For plugins that are not installed but discoverable, only suggest discoverable and installable plugins when the user's intent very explicitly and unambiguously matches that plugin itself." - )); - assert!(description.contains( - "Do not suggest a plugin just because one of its connectors or capabilities seems relevant." - )); - assert!(description.contains( - "Apply the stricter explicit-and-unambiguous rule for *discoverable tools* like plugin install suggestions; *missing tools* like connector install suggestions continue to use the normal clear-fit standard." - )); - assert!(description.contains("DO NOT explore or recommend tools that are not on this list.")); - assert!(!description.contains("{{discoverable_tools}}")); - assert!(!description.contains("tool_search fails to find a good match")); - let JsonSchema::Object { required, .. } = parameters else { - panic!("expected object parameters"); - }; - assert_eq!( - required.as_ref(), - Some(&vec![ - "tool_type".to_string(), - "action_type".to_string(), - "tool_id".to_string(), - "suggest_reason".to_string(), - ]) - ); -} - #[test] fn test_mcp_tool_property_missing_type_defaults_to_string() { let config = test_config(); @@ -2789,95 +1135,6 @@ fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() { ); } -#[test] -fn code_mode_augments_builtin_tool_descriptions_with_typed_sample() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); - let mut features = Features::with_defaults(); - features.enable(Feature::CodeMode); - features.enable(Feature::UnifiedExec); - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - &[], - ) - .build(); - let ToolSpec::Function(ResponsesApiTool { description, .. }) = - &find_tool(&tools, "view_image").spec - else { - panic!("expected function tool"); - }; - - assert_eq!( - description, - "View a local image from the filesystem (only use if given a full filepath by the user, and the image isn't already attached to the thread context within tags).\n\nexec tool declaration:\n```ts\ndeclare const tools: { view_image(args: { path: string; }): Promise<{ detail: string | null; image_url: string; }>; };\n```" - ); -} - -#[test] -fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); - let mut features = Features::with_defaults(); - features.enable(Feature::CodeMode); - features.enable(Feature::UnifiedExec); - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - - let (tools, _) = build_specs( - &tools_config, - Some(HashMap::from([( - "mcp__sample__echo".to_string(), - mcp_tool( - "echo", - "Echo text", - serde_json::json!({ - "type": "object", - "properties": { - "message": {"type": "string"} - }, - "required": ["message"], - "additionalProperties": false - }), - ), - )])), - /*app_tools*/ None, - &[], - ) - .build(); - - let ToolSpec::Function(ResponsesApiTool { description, .. }) = - &find_tool(&tools, "mcp__sample__echo").spec - else { - panic!("expected function tool"); - }; - - assert_eq!( - description, - "Echo text\n\nexec tool declaration:\n```ts\ndeclare const tools: { mcp__sample__echo(args: { message: string; }): Promise<{ _meta?: unknown; content: Array; isError?: boolean; structuredContent?: unknown; }>; };\n```" - ); -} - #[test] fn code_mode_only_restricts_model_tools_to_exec_tools() { let mut features = Features::with_defaults(); @@ -2891,78 +1148,3 @@ fn code_mode_only_restricts_model_tools_to_exec_tools() { &["exec", "wait"], ); } - -#[test] -fn code_mode_only_exec_description_includes_full_nested_tool_details() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); - let mut features = Features::with_defaults(); - features.enable(Feature::CodeMode); - features.enable(Feature::CodeModeOnly); - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - &[], - ) - .build(); - let ToolSpec::Freeform(FreeformTool { description, .. }) = &find_tool(&tools, "exec").spec - else { - panic!("expected freeform tool"); - }; - - assert!(!description.contains("Enabled nested tools:")); - assert!(!description.contains("Nested tool reference:")); - assert!(description.starts_with( - "Use `exec/wait` tool to run all other tools, do not attempt to use any other tools directly" - )); - assert!(description.contains("### `update_plan` (`update_plan`)")); - assert!(description.contains("### `view_image` (`view_image`)")); -} - -#[test] -fn code_mode_exec_description_omits_nested_tool_details_when_not_code_mode_only() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); - let mut features = Features::with_defaults(); - features.enable(Feature::CodeMode); - let available_models = Vec::new(); - let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, - available_models: &available_models, - features: &features, - web_search_mode: Some(WebSearchMode::Cached), - session_source: SessionSource::Cli, - sandbox_policy: &SandboxPolicy::DangerFullAccess, - windows_sandbox_level: WindowsSandboxLevel::Disabled, - }); - - let (tools, _) = build_specs( - &tools_config, - /*mcp_tools*/ None, - /*app_tools*/ None, - &[], - ) - .build(); - let ToolSpec::Freeform(FreeformTool { description, .. }) = &find_tool(&tools, "exec").spec - else { - panic!("expected freeform tool"); - }; - - assert!(!description.starts_with( - "Use `exec/wait` tool to run all other tools, do not attempt to use any other tools directly" - )); - assert!(!description.contains("### `update_plan` (`update_plan`)")); - assert!(!description.contains("### `view_image` (`view_image`)")); -} diff --git a/codex-rs/core/src/windows_sandbox.rs b/codex-rs/core/src/windows_sandbox.rs index 391c59d250..0def23d00c 100644 --- a/codex-rs/core/src/windows_sandbox.rs +++ b/codex-rs/core/src/windows_sandbox.rs @@ -2,7 +2,7 @@ use crate::config::Config; use crate::config::ConfigToml; use crate::config::edit::ConfigEditsBuilder; use crate::config::profile::ConfigProfile; -use crate::config::types::WindowsSandboxModeToml; +use codex_config::types::WindowsSandboxModeToml; use codex_features::Feature; use codex_features::Features; use codex_features::FeaturesToml; diff --git a/codex-rs/core/src/windows_sandbox_tests.rs b/codex-rs/core/src/windows_sandbox_tests.rs index d0ffa07b80..3fa3968389 100644 --- a/codex-rs/core/src/windows_sandbox_tests.rs +++ b/codex-rs/core/src/windows_sandbox_tests.rs @@ -1,5 +1,5 @@ use super::*; -use crate::config::types::WindowsToml; +use codex_config::types::WindowsToml; use codex_features::Features; use codex_features::FeaturesToml; use pretty_assertions::assert_eq; diff --git a/codex-rs/core/tests/suite/code_mode.rs b/codex-rs/core/tests/suite/code_mode.rs index 9b69149e9b..37ee27dd68 100644 --- a/codex-rs/core/tests/suite/code_mode.rs +++ b/codex-rs/core/tests/suite/code_mode.rs @@ -3,8 +3,8 @@ use anyhow::Result; use base64::Engine; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; -use codex_core::config::types::McpServerConfig; -use codex_core::config::types::McpServerTransportConfig; +use codex_config::types::McpServerConfig; +use codex_config::types::McpServerTransportConfig; use codex_features::Feature; use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem; use codex_protocol::dynamic_tools::DynamicToolResponse; diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index 0ca52dbd11..b553c7c649 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use codex_core::config::types::Personality; +use codex_config::types::Personality; use codex_core::models_manager::manager::RefreshStrategy; use codex_features::Feature; use codex_login::CodexAuth; diff --git a/codex-rs/core/tests/suite/model_visible_layout.rs b/codex-rs/core/tests/suite/model_visible_layout.rs index 49f635432f..13e2819141 100644 --- a/codex-rs/core/tests/suite/model_visible_layout.rs +++ b/codex-rs/core/tests/suite/model_visible_layout.rs @@ -4,7 +4,7 @@ use std::fs; use std::sync::Arc; use anyhow::Result; -use codex_core::config::types::Personality; +use codex_config::types::Personality; use codex_features::Feature; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index ae9c47c2ee..14ea86ecb1 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -1,4 +1,4 @@ -use codex_core::config::types::Personality; +use codex_config::types::Personality; use codex_core::models_manager::manager::ModelsManager; use codex_core::models_manager::manager::RefreshStrategy; use codex_features::Feature; diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index 9c741c2c86..d6b9afb7d3 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -9,8 +9,8 @@ use std::time::Duration; use std::time::SystemTime; use std::time::UNIX_EPOCH; -use codex_core::config::types::McpServerConfig; -use codex_core::config::types::McpServerTransportConfig; +use codex_config::types::McpServerConfig; +use codex_config::types::McpServerTransportConfig; use codex_core::models_manager::manager::RefreshStrategy; use codex_login::CodexAuth; diff --git a/codex-rs/core/tests/suite/sqlite_state.rs b/codex-rs/core/tests/suite/sqlite_state.rs index d955f342ad..f35152e185 100644 --- a/codex-rs/core/tests/suite/sqlite_state.rs +++ b/codex-rs/core/tests/suite/sqlite_state.rs @@ -1,6 +1,6 @@ use anyhow::Result; -use codex_core::config::types::McpServerConfig; -use codex_core::config::types::McpServerTransportConfig; +use codex_config::types::McpServerConfig; +use codex_config::types::McpServerTransportConfig; use codex_features::Feature; use codex_protocol::ThreadId; use codex_protocol::dynamic_tools::DynamicToolSpec; diff --git a/codex-rs/core/tests/suite/tool_suggest.rs b/codex-rs/core/tests/suite/tool_suggest.rs index 09c29cb1f6..a65c58587d 100644 --- a/codex-rs/core/tests/suite/tool_suggest.rs +++ b/codex-rs/core/tests/suite/tool_suggest.rs @@ -2,9 +2,9 @@ #![allow(clippy::unwrap_used, clippy::expect_used)] use anyhow::Result; +use codex_config::types::ToolSuggestDiscoverable; +use codex_config::types::ToolSuggestDiscoverableType; use codex_core::config::Config; -use codex_core::config::types::ToolSuggestDiscoverable; -use codex_core::config::types::ToolSuggestDiscoverableType; use codex_features::Feature; use codex_login::CodexAuth; use codex_protocol::openai_models::ModelsResponse; diff --git a/codex-rs/core/tests/suite/truncation.rs b/codex-rs/core/tests/suite/truncation.rs index c3f019dd33..3f4a9e5a64 100644 --- a/codex-rs/core/tests/suite/truncation.rs +++ b/codex-rs/core/tests/suite/truncation.rs @@ -3,8 +3,8 @@ use anyhow::Context; use anyhow::Result; -use codex_core::config::types::McpServerConfig; -use codex_core::config::types::McpServerTransportConfig; +use codex_config::types::McpServerConfig; +use codex_config::types::McpServerTransportConfig; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; @@ -354,8 +354,8 @@ async fn mcp_tool_call_output_exceeds_limit_truncated_for_model() -> Result<()> let mut servers = config.mcp_servers.get().clone(); servers.insert( server_name.to_string(), - codex_core::config::types::McpServerConfig { - transport: codex_core::config::types::McpServerTransportConfig::Stdio { + codex_config::types::McpServerConfig { + transport: codex_config::types::McpServerTransportConfig::Stdio { command: rmcp_test_server_bin, args: Vec::new(), env: None, @@ -719,8 +719,8 @@ async fn mcp_tool_call_output_not_truncated_with_custom_limit() -> Result<()> { let mut servers = config.mcp_servers.get().clone(); servers.insert( server_name.to_string(), - codex_core::config::types::McpServerConfig { - transport: codex_core::config::types::McpServerTransportConfig::Stdio { + codex_config::types::McpServerConfig { + transport: codex_config::types::McpServerTransportConfig::Stdio { command: rmcp_test_server_bin, args: Vec::new(), env: None, diff --git a/codex-rs/linux-sandbox/Cargo.toml b/codex-rs/linux-sandbox/Cargo.toml index c0bfc7aca2..524c574fb0 100644 --- a/codex-rs/linux-sandbox/Cargo.toml +++ b/codex-rs/linux-sandbox/Cargo.toml @@ -17,6 +17,7 @@ workspace = true [target.'cfg(target_os = "linux")'.dependencies] clap = { workspace = true, features = ["derive"] } +codex-config = { workspace = true } codex-core = { workspace = true } codex-protocol = { workspace = true } codex-sandboxing = { workspace = true } diff --git a/codex-rs/linux-sandbox/tests/suite/landlock.rs b/codex-rs/linux-sandbox/tests/suite/landlock.rs index ba26c7e221..bb214905f2 100644 --- a/codex-rs/linux-sandbox/tests/suite/landlock.rs +++ b/codex-rs/linux-sandbox/tests/suite/landlock.rs @@ -1,6 +1,6 @@ #![cfg(target_os = "linux")] #![allow(clippy::unwrap_used)] -use codex_core::config::types::ShellEnvironmentPolicy; +use codex_config::types::ShellEnvironmentPolicy; use codex_core::error::CodexErr; use codex_core::error::Result; use codex_core::error::SandboxErr; diff --git a/codex-rs/linux-sandbox/tests/suite/managed_proxy.rs b/codex-rs/linux-sandbox/tests/suite/managed_proxy.rs index 53d1852c93..256373953e 100644 --- a/codex-rs/linux-sandbox/tests/suite/managed_proxy.rs +++ b/codex-rs/linux-sandbox/tests/suite/managed_proxy.rs @@ -1,7 +1,7 @@ #![cfg(target_os = "linux")] #![allow(clippy::unwrap_used)] -use codex_core::config::types::ShellEnvironmentPolicy; +use codex_config::types::ShellEnvironmentPolicy; use codex_core::exec_env::create_env; use codex_protocol::protocol::SandboxPolicy; use pretty_assertions::assert_eq; diff --git a/codex-rs/mcp-server/Cargo.toml b/codex-rs/mcp-server/Cargo.toml index 5de2a9e5a3..a463d9bbe2 100644 --- a/codex-rs/mcp-server/Cargo.toml +++ b/codex-rs/mcp-server/Cargo.toml @@ -18,6 +18,7 @@ workspace = true [dependencies] anyhow = { workspace = true } codex-arg0 = { workspace = true } +codex-config = { workspace = true } codex-core = { workspace = true } codex-exec-server = { workspace = true } codex-features = { workspace = true } diff --git a/codex-rs/mcp-server/src/lib.rs b/codex-rs/mcp-server/src/lib.rs index e435eab0c9..2596339acd 100644 --- a/codex-rs/mcp-server/src/lib.rs +++ b/codex-rs/mcp-server/src/lib.rs @@ -181,8 +181,8 @@ pub async fn run_main( #[cfg(test)] mod tests { use super::*; + use codex_config::types::OtelExporterKind; use codex_core::config::ConfigBuilder; - use codex_core::config::types::OtelExporterKind; use pretty_assertions::assert_eq; use std::collections::HashMap; use tempfile::TempDir; diff --git a/codex-rs/tools/src/lib.rs b/codex-rs/tools/src/lib.rs index 3bce0fb107..91931e8944 100644 --- a/codex-rs/tools/src/lib.rs +++ b/codex-rs/tools/src/lib.rs @@ -18,6 +18,8 @@ mod responses_api; mod tool_config; mod tool_definition; mod tool_discovery; +mod tool_registry_plan; +mod tool_registry_plan_types; mod tool_spec; mod tool_suggest; mod utility_tool; @@ -107,6 +109,12 @@ pub use tool_discovery::collect_tool_suggest_entries; pub use tool_discovery::create_tool_search_tool; pub use tool_discovery::create_tool_suggest_tool; pub use tool_discovery::filter_tool_suggest_discoverable_tools_for_client; +pub use tool_registry_plan::build_tool_registry_plan; +pub use tool_registry_plan_types::ToolHandlerKind; +pub use tool_registry_plan_types::ToolHandlerSpec; +pub use tool_registry_plan_types::ToolRegistryPlan; +pub use tool_registry_plan_types::ToolRegistryPlanAppTool; +pub use tool_registry_plan_types::ToolRegistryPlanParams; pub use tool_spec::ConfiguredToolSpec; pub use tool_spec::ResponsesApiWebSearchFilters; pub use tool_spec::ResponsesApiWebSearchUserLocation; diff --git a/codex-rs/tools/src/tool_registry_plan.rs b/codex-rs/tools/src/tool_registry_plan.rs new file mode 100644 index 0000000000..314d93f55f --- /dev/null +++ b/codex-rs/tools/src/tool_registry_plan.rs @@ -0,0 +1,492 @@ +use crate::CommandToolOptions; +use crate::REQUEST_USER_INPUT_TOOL_NAME; +use crate::ShellToolOptions; +use crate::SpawnAgentToolOptions; +use crate::TOOL_SEARCH_DEFAULT_LIMIT; +use crate::TOOL_SEARCH_TOOL_NAME; +use crate::TOOL_SUGGEST_TOOL_NAME; +use crate::ToolHandlerKind; +use crate::ToolRegistryPlan; +use crate::ToolRegistryPlanParams; +use crate::ToolSearchAppSource; +use crate::ToolSpec; +use crate::ToolsConfig; +use crate::ViewImageToolOptions; +use crate::WebSearchToolOptions; +use crate::collect_code_mode_tool_definitions; +use crate::collect_tool_search_app_infos; +use crate::collect_tool_suggest_entries; +use crate::create_apply_patch_freeform_tool; +use crate::create_apply_patch_json_tool; +use crate::create_assign_task_tool; +use crate::create_close_agent_tool_v1; +use crate::create_close_agent_tool_v2; +use crate::create_code_mode_tool; +use crate::create_exec_command_tool; +use crate::create_image_generation_tool; +use crate::create_js_repl_reset_tool; +use crate::create_js_repl_tool; +use crate::create_list_agents_tool; +use crate::create_list_dir_tool; +use crate::create_list_mcp_resource_templates_tool; +use crate::create_list_mcp_resources_tool; +use crate::create_local_shell_tool; +use crate::create_read_mcp_resource_tool; +use crate::create_report_agent_job_result_tool; +use crate::create_request_permissions_tool; +use crate::create_request_user_input_tool; +use crate::create_resume_agent_tool; +use crate::create_send_input_tool_v1; +use crate::create_send_message_tool; +use crate::create_shell_command_tool; +use crate::create_shell_tool; +use crate::create_spawn_agent_tool_v1; +use crate::create_spawn_agent_tool_v2; +use crate::create_spawn_agents_on_csv_tool; +use crate::create_test_sync_tool; +use crate::create_tool_search_tool; +use crate::create_tool_suggest_tool; +use crate::create_update_plan_tool; +use crate::create_view_image_tool; +use crate::create_wait_agent_tool_v1; +use crate::create_wait_agent_tool_v2; +use crate::create_wait_tool; +use crate::create_web_search_tool; +use crate::create_write_stdin_tool; +use crate::dynamic_tool_to_responses_api_tool; +use crate::mcp_tool_to_responses_api_tool; +use crate::request_permissions_tool_description; +use crate::request_user_input_tool_description; +use crate::tool_registry_plan_types::agent_type_description; +use codex_protocol::openai_models::ApplyPatchToolType; +use codex_protocol::openai_models::ConfigShellToolType; +use rmcp::model::Tool as McpTool; + +pub fn build_tool_registry_plan( + config: &ToolsConfig, + params: ToolRegistryPlanParams<'_>, +) -> ToolRegistryPlan { + let mut plan = ToolRegistryPlan::new(); + let exec_permission_approvals_enabled = config.exec_permission_approvals_enabled; + + if config.code_mode_enabled { + let nested_config = config.for_code_mode_nested_tools(); + let nested_plan = build_tool_registry_plan( + &nested_config, + ToolRegistryPlanParams { + discoverable_tools: None, + ..params + }, + ); + let enabled_tools = collect_code_mode_tool_definitions( + nested_plan + .specs + .iter() + .map(|configured_tool| &configured_tool.spec), + ) + .into_iter() + .map(|tool| (tool.name, tool.description)) + .collect::>(); + plan.push_spec( + create_code_mode_tool(&enabled_tools, config.code_mode_only_enabled), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + plan.register_handler( + codex_code_mode::PUBLIC_TOOL_NAME, + ToolHandlerKind::CodeModeExecute, + ); + plan.push_spec( + create_wait_tool(), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + plan.register_handler( + codex_code_mode::WAIT_TOOL_NAME, + ToolHandlerKind::CodeModeWait, + ); + } + + match &config.shell_type { + ConfigShellToolType::Default => { + plan.push_spec( + create_shell_tool(ShellToolOptions { + exec_permission_approvals_enabled, + }), + /*supports_parallel_tool_calls*/ true, + config.code_mode_enabled, + ); + } + ConfigShellToolType::Local => { + plan.push_spec( + create_local_shell_tool(), + /*supports_parallel_tool_calls*/ true, + config.code_mode_enabled, + ); + } + ConfigShellToolType::UnifiedExec => { + plan.push_spec( + create_exec_command_tool(CommandToolOptions { + allow_login_shell: config.allow_login_shell, + exec_permission_approvals_enabled, + }), + /*supports_parallel_tool_calls*/ true, + config.code_mode_enabled, + ); + plan.push_spec( + create_write_stdin_tool(), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + plan.register_handler("exec_command", ToolHandlerKind::UnifiedExec); + plan.register_handler("write_stdin", ToolHandlerKind::UnifiedExec); + } + ConfigShellToolType::Disabled => {} + ConfigShellToolType::ShellCommand => { + plan.push_spec( + create_shell_command_tool(CommandToolOptions { + allow_login_shell: config.allow_login_shell, + exec_permission_approvals_enabled, + }), + /*supports_parallel_tool_calls*/ true, + config.code_mode_enabled, + ); + } + } + + if config.shell_type != ConfigShellToolType::Disabled { + plan.register_handler("shell", ToolHandlerKind::Shell); + plan.register_handler("container.exec", ToolHandlerKind::Shell); + plan.register_handler("local_shell", ToolHandlerKind::Shell); + plan.register_handler("shell_command", ToolHandlerKind::ShellCommand); + } + + if params.mcp_tools.is_some() { + plan.push_spec( + create_list_mcp_resources_tool(), + /*supports_parallel_tool_calls*/ true, + config.code_mode_enabled, + ); + plan.push_spec( + create_list_mcp_resource_templates_tool(), + /*supports_parallel_tool_calls*/ true, + config.code_mode_enabled, + ); + plan.push_spec( + create_read_mcp_resource_tool(), + /*supports_parallel_tool_calls*/ true, + config.code_mode_enabled, + ); + plan.register_handler("list_mcp_resources", ToolHandlerKind::McpResource); + plan.register_handler("list_mcp_resource_templates", ToolHandlerKind::McpResource); + plan.register_handler("read_mcp_resource", ToolHandlerKind::McpResource); + } + + plan.push_spec( + create_update_plan_tool(), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + plan.register_handler("update_plan", ToolHandlerKind::Plan); + + if config.js_repl_enabled { + plan.push_spec( + create_js_repl_tool(), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + plan.push_spec( + create_js_repl_reset_tool(), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + plan.register_handler("js_repl", ToolHandlerKind::JsRepl); + plan.register_handler("js_repl_reset", ToolHandlerKind::JsReplReset); + } + + if config.request_user_input { + plan.push_spec( + create_request_user_input_tool(request_user_input_tool_description( + config.default_mode_request_user_input, + )), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + plan.register_handler( + REQUEST_USER_INPUT_TOOL_NAME, + ToolHandlerKind::RequestUserInput, + ); + } + + if config.request_permissions_tool_enabled { + plan.push_spec( + create_request_permissions_tool(request_permissions_tool_description()), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + plan.register_handler("request_permissions", ToolHandlerKind::RequestPermissions); + } + + if config.search_tool + && let Some(app_tools) = params.app_tools + { + let search_app_infos = collect_tool_search_app_infos( + app_tools.iter().map(|tool| ToolSearchAppSource { + server_name: tool.server_name, + connector_name: tool.connector_name, + connector_description: tool.connector_description, + }), + params.codex_apps_mcp_server_name, + ); + plan.push_spec( + create_tool_search_tool(&search_app_infos, TOOL_SEARCH_DEFAULT_LIMIT), + /*supports_parallel_tool_calls*/ true, + config.code_mode_enabled, + ); + plan.register_handler(TOOL_SEARCH_TOOL_NAME, ToolHandlerKind::ToolSearch); + + for tool in app_tools { + plan.register_handler( + format!("{}:{}", tool.tool_namespace, tool.tool_name), + ToolHandlerKind::Mcp, + ); + } + } + + if config.tool_suggest + && let Some(discoverable_tools) = + params.discoverable_tools.filter(|tools| !tools.is_empty()) + { + plan.push_spec( + create_tool_suggest_tool(&collect_tool_suggest_entries(discoverable_tools)), + /*supports_parallel_tool_calls*/ true, + /*code_mode_enabled*/ false, + ); + plan.register_handler(TOOL_SUGGEST_TOOL_NAME, ToolHandlerKind::ToolSuggest); + } + + if let Some(apply_patch_tool_type) = &config.apply_patch_tool_type { + match apply_patch_tool_type { + ApplyPatchToolType::Freeform => { + plan.push_spec( + create_apply_patch_freeform_tool(), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + } + ApplyPatchToolType::Function => { + plan.push_spec( + create_apply_patch_json_tool(), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + } + } + plan.register_handler("apply_patch", ToolHandlerKind::ApplyPatch); + } + + if config + .experimental_supported_tools + .iter() + .any(|tool| tool == "list_dir") + { + plan.push_spec( + create_list_dir_tool(), + /*supports_parallel_tool_calls*/ true, + config.code_mode_enabled, + ); + plan.register_handler("list_dir", ToolHandlerKind::ListDir); + } + + if config + .experimental_supported_tools + .iter() + .any(|tool| tool == "test_sync_tool") + { + plan.push_spec( + create_test_sync_tool(), + /*supports_parallel_tool_calls*/ true, + config.code_mode_enabled, + ); + plan.register_handler("test_sync_tool", ToolHandlerKind::TestSync); + } + + if let Some(web_search_tool) = create_web_search_tool(WebSearchToolOptions { + web_search_mode: config.web_search_mode, + web_search_config: config.web_search_config.as_ref(), + web_search_tool_type: config.web_search_tool_type, + }) { + plan.push_spec( + web_search_tool, + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + } + + if config.image_gen_tool { + plan.push_spec( + create_image_generation_tool("png"), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + } + + plan.push_spec( + create_view_image_tool(ViewImageToolOptions { + can_request_original_image_detail: config.can_request_original_image_detail, + }), + /*supports_parallel_tool_calls*/ true, + config.code_mode_enabled, + ); + plan.register_handler("view_image", ToolHandlerKind::ViewImage); + + if config.collab_tools { + if config.multi_agent_v2 { + let agent_type_description = + agent_type_description(config, params.default_agent_type_description); + plan.push_spec( + create_spawn_agent_tool_v2(SpawnAgentToolOptions { + available_models: &config.available_models, + agent_type_description, + }), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + plan.push_spec( + create_send_message_tool(), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + plan.push_spec( + create_assign_task_tool(), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + plan.push_spec( + create_wait_agent_tool_v2(params.wait_agent_timeouts), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + plan.push_spec( + create_close_agent_tool_v2(), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + plan.push_spec( + create_list_agents_tool(), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + plan.register_handler("spawn_agent", ToolHandlerKind::SpawnAgentV2); + plan.register_handler("send_message", ToolHandlerKind::SendMessageV2); + plan.register_handler("assign_task", ToolHandlerKind::AssignTaskV2); + plan.register_handler("wait_agent", ToolHandlerKind::WaitAgentV2); + plan.register_handler("close_agent", ToolHandlerKind::CloseAgentV2); + plan.register_handler("list_agents", ToolHandlerKind::ListAgentsV2); + } else { + let agent_type_description = + agent_type_description(config, params.default_agent_type_description); + plan.push_spec( + create_spawn_agent_tool_v1(SpawnAgentToolOptions { + available_models: &config.available_models, + agent_type_description, + }), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + plan.push_spec( + create_send_input_tool_v1(), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + plan.push_spec( + create_resume_agent_tool(), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + plan.register_handler("resume_agent", ToolHandlerKind::ResumeAgentV1); + plan.push_spec( + create_wait_agent_tool_v1(params.wait_agent_timeouts), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + plan.push_spec( + create_close_agent_tool_v1(), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + plan.register_handler("spawn_agent", ToolHandlerKind::SpawnAgentV1); + plan.register_handler("send_input", ToolHandlerKind::SendInputV1); + plan.register_handler("wait_agent", ToolHandlerKind::WaitAgentV1); + plan.register_handler("close_agent", ToolHandlerKind::CloseAgentV1); + } + } + + if config.agent_jobs_tools { + plan.push_spec( + create_spawn_agents_on_csv_tool(), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + plan.register_handler("spawn_agents_on_csv", ToolHandlerKind::AgentJobs); + if config.agent_jobs_worker_tools { + plan.push_spec( + create_report_agent_job_result_tool(), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + plan.register_handler("report_agent_job_result", ToolHandlerKind::AgentJobs); + } + } + + if let Some(mcp_tools) = params.mcp_tools { + let mut entries: Vec<(String, &McpTool)> = mcp_tools + .iter() + .map(|(name, tool)| (name.clone(), tool)) + .collect(); + entries.sort_by(|left, right| left.0.cmp(&right.0)); + + for (name, tool) in entries { + match mcp_tool_to_responses_api_tool(name.clone(), tool) { + Ok(converted_tool) => { + plan.push_spec( + ToolSpec::Function(converted_tool), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + plan.register_handler(name, ToolHandlerKind::Mcp); + } + Err(error) => { + tracing::error!( + "Failed to convert {name:?} MCP tool to OpenAI tool: {error:?}" + ); + } + } + } + } + + for tool in params.dynamic_tools { + match dynamic_tool_to_responses_api_tool(tool) { + Ok(converted_tool) => { + plan.push_spec( + ToolSpec::Function(converted_tool), + /*supports_parallel_tool_calls*/ false, + config.code_mode_enabled, + ); + plan.register_handler(tool.name.clone(), ToolHandlerKind::DynamicTool); + } + Err(error) => { + tracing::error!( + "Failed to convert dynamic tool {:?} to OpenAI tool: {error:?}", + tool.name + ); + } + } + } + + plan +} + +#[cfg(test)] +#[path = "tool_registry_plan_tests.rs"] +mod tests; diff --git a/codex-rs/tools/src/tool_registry_plan_tests.rs b/codex-rs/tools/src/tool_registry_plan_tests.rs new file mode 100644 index 0000000000..c2c4a0aea1 --- /dev/null +++ b/codex-rs/tools/src/tool_registry_plan_tests.rs @@ -0,0 +1,1878 @@ +use super::*; +use crate::AdditionalProperties; +use crate::ConfiguredToolSpec; +use crate::DiscoverablePluginInfo; +use crate::DiscoverableTool; +use crate::FreeformTool; +use crate::JsonSchema; +use crate::ResponsesApiTool; +use crate::ResponsesApiWebSearchFilters; +use crate::ResponsesApiWebSearchUserLocation; +use crate::ToolHandlerSpec; +use crate::ToolRegistryPlanAppTool; +use crate::ToolsConfigParams; +use crate::WaitAgentTimeoutOptions; +use crate::mcp_call_tool_result_output_schema; +use codex_app_server_protocol::AppInfo; +use codex_features::Feature; +use codex_features::Features; +use codex_protocol::config_types::WebSearchConfig; +use codex_protocol::config_types::WebSearchMode; +use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::dynamic_tools::DynamicToolSpec; +use codex_protocol::models::VIEW_IMAGE_TOOL_NAME; +use codex_protocol::openai_models::InputModality; +use codex_protocol::openai_models::ModelInfo; +use codex_protocol::openai_models::WebSearchToolType; +use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::protocol::SessionSource; +use codex_protocol::protocol::SubAgentSource; +use pretty_assertions::assert_eq; +use serde_json::json; +use std::collections::BTreeMap; +use std::collections::HashMap; + +const CODEX_APPS_MCP_SERVER_NAME: &str = "codex_apps"; +const DEFAULT_AGENT_TYPE_DESCRIPTION: &str = "Test agent type description."; +const DEFAULT_WAIT_TIMEOUT_MS: i64 = 30_000; +const MIN_WAIT_TIMEOUT_MS: i64 = 10_000; +const MAX_WAIT_TIMEOUT_MS: i64 = 3_600_000; + +#[test] +fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() { + let model_info = model_info(); + let mut features = Features::with_defaults(); + features.enable(Feature::UnifiedExec); + let available_models = Vec::new(); + let config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Live), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs( + &config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + + let mut actual = BTreeMap::new(); + let mut duplicate_names = Vec::new(); + for tool in &tools { + let name = tool.name().to_string(); + if actual.insert(name.clone(), tool.spec.clone()).is_some() { + duplicate_names.push(name); + } + } + assert!( + duplicate_names.is_empty(), + "duplicate tool entries detected: {duplicate_names:?}" + ); + + let mut expected = BTreeMap::new(); + for spec in [ + create_exec_command_tool(CommandToolOptions { + allow_login_shell: true, + exec_permission_approvals_enabled: false, + }), + create_write_stdin_tool(), + create_update_plan_tool(), + request_user_input_tool_spec(/*default_mode_request_user_input*/ false), + create_apply_patch_freeform_tool(), + ToolSpec::WebSearch { + external_web_access: Some(true), + filters: None, + user_location: None, + search_context_size: None, + search_content_types: None, + }, + create_view_image_tool(ViewImageToolOptions { + can_request_original_image_detail: config.can_request_original_image_detail, + }), + ] { + expected.insert(spec.name().to_string(), spec); + } + let collab_specs = if config.multi_agent_v2 { + vec![ + create_spawn_agent_tool_v2(spawn_agent_tool_options(&config)), + create_send_message_tool(), + create_wait_agent_tool_v2(wait_agent_timeout_options()), + create_close_agent_tool_v2(), + ] + } else { + vec![ + create_spawn_agent_tool_v1(spawn_agent_tool_options(&config)), + create_send_input_tool_v1(), + create_wait_agent_tool_v1(wait_agent_timeout_options()), + create_close_agent_tool_v1(), + ] + }; + for spec in collab_specs { + expected.insert(spec.name().to_string(), spec); + } + if !config.multi_agent_v2 { + let spec = create_resume_agent_tool(); + expected.insert(spec.name().to_string(), spec); + } + + if config.exec_permission_approvals_enabled { + let spec = create_request_permissions_tool(request_permissions_tool_description()); + expected.insert(spec.name().to_string(), spec); + } + + assert_eq!( + actual.keys().collect::>(), + expected.keys().collect::>(), + "tool name set mismatch" + ); + + for name in expected.keys() { + let mut actual_spec = actual.get(name).expect("present").clone(); + let mut expected_spec = expected.get(name).expect("present").clone(); + strip_descriptions_tool(&mut actual_spec); + strip_descriptions_tool(&mut expected_spec); + assert_eq!(actual_spec, expected_spec, "spec mismatch for {name}"); + } +} + +#[test] +fn test_build_specs_collab_tools_enabled() { + let model_info = model_info(); + let mut features = Features::with_defaults(); + features.enable(Feature::Collab); + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + + assert_contains_tool_names( + &tools, + &["spawn_agent", "send_input", "wait_agent", "close_agent"], + ); + assert_lacks_tool_name(&tools, "spawn_agents_on_csv"); + assert_lacks_tool_name(&tools, "list_agents"); + + let spawn_agent = find_tool(&tools, "spawn_agent"); + let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &spawn_agent.spec else { + panic!("spawn_agent should be a function tool"); + }; + let JsonSchema::Object { properties, .. } = parameters else { + panic!("spawn_agent should use object params"); + }; + assert!(properties.contains_key("fork_context")); + assert!(!properties.contains_key("fork_turns")); +} + +#[test] +fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() { + let model_info = model_info(); + let mut features = Features::with_defaults(); + features.enable(Feature::Collab); + features.enable(Feature::MultiAgentV2); + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + + assert_contains_tool_names( + &tools, + &[ + "spawn_agent", + "send_message", + "assign_task", + "wait_agent", + "close_agent", + "list_agents", + ], + ); + + let spawn_agent = find_tool(&tools, "spawn_agent"); + let ToolSpec::Function(ResponsesApiTool { + parameters, + output_schema, + .. + }) = &spawn_agent.spec + else { + panic!("spawn_agent should be a function tool"); + }; + let JsonSchema::Object { + properties, + required, + .. + } = parameters + else { + panic!("spawn_agent should use object params"); + }; + assert!(properties.contains_key("task_name")); + assert!(properties.contains_key("message")); + assert!(properties.contains_key("fork_turns")); + assert!(!properties.contains_key("items")); + assert!(!properties.contains_key("fork_context")); + assert_eq!( + required.as_ref(), + Some(&vec!["task_name".to_string(), "message".to_string()]) + ); + let output_schema = output_schema + .as_ref() + .expect("spawn_agent should define output schema"); + assert_eq!( + output_schema["required"], + json!(["agent_id", "task_name", "nickname"]) + ); + + let send_message = find_tool(&tools, "send_message"); + let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &send_message.spec else { + panic!("send_message should be a function tool"); + }; + let JsonSchema::Object { + properties, + required, + .. + } = parameters + else { + panic!("send_message should use object params"); + }; + assert!(properties.contains_key("target")); + assert!(!properties.contains_key("interrupt")); + assert!(properties.contains_key("message")); + assert!(!properties.contains_key("items")); + assert_eq!( + required.as_ref(), + Some(&vec!["target".to_string(), "message".to_string()]) + ); + + let assign_task = find_tool(&tools, "assign_task"); + let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &assign_task.spec else { + panic!("assign_task should be a function tool"); + }; + let JsonSchema::Object { + properties, + required, + .. + } = parameters + else { + panic!("assign_task should use object params"); + }; + assert!(properties.contains_key("target")); + assert!(properties.contains_key("message")); + assert!(!properties.contains_key("items")); + assert_eq!( + required.as_ref(), + Some(&vec!["target".to_string(), "message".to_string()]) + ); + + let wait_agent = find_tool(&tools, "wait_agent"); + let ToolSpec::Function(ResponsesApiTool { + parameters, + output_schema, + .. + }) = &wait_agent.spec + else { + panic!("wait_agent should be a function tool"); + }; + let JsonSchema::Object { + properties, + required, + .. + } = parameters + else { + panic!("wait_agent should use object params"); + }; + assert!(!properties.contains_key("targets")); + assert!(properties.contains_key("timeout_ms")); + assert_eq!(required, &None); + let output_schema = output_schema + .as_ref() + .expect("wait_agent should define output schema"); + assert_eq!( + output_schema["properties"]["message"]["description"], + json!("Brief wait summary without the agent's final content.") + ); + + let list_agents = find_tool(&tools, "list_agents"); + let ToolSpec::Function(ResponsesApiTool { + parameters, + output_schema, + .. + }) = &list_agents.spec + else { + panic!("list_agents should be a function tool"); + }; + let JsonSchema::Object { + properties, + required, + .. + } = parameters + else { + panic!("list_agents should use object params"); + }; + assert!(properties.contains_key("path_prefix")); + assert_eq!(required.as_ref(), None); + let output_schema = output_schema + .as_ref() + .expect("list_agents should define output schema"); + assert_eq!( + output_schema["properties"]["agents"]["items"]["required"], + json!(["agent_name", "agent_status", "last_task_message"]) + ); + assert_lacks_tool_name(&tools, "send_input"); + assert_lacks_tool_name(&tools, "resume_agent"); +} + +#[test] +fn test_build_specs_enable_fanout_enables_agent_jobs_and_collab_tools() { + let model_info = model_info(); + let mut features = Features::with_defaults(); + features.enable(Feature::SpawnCsv); + features.normalize_dependencies(); + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + + assert_contains_tool_names( + &tools, + &[ + "spawn_agent", + "send_input", + "wait_agent", + "close_agent", + "spawn_agents_on_csv", + ], + ); +} + +#[test] +fn view_image_tool_omits_detail_without_original_detail_feature() { + let mut model_info = model_info(); + model_info.supports_image_detail_original = true; + let features = Features::with_defaults(); + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + let view_image = find_tool(&tools, VIEW_IMAGE_TOOL_NAME); + let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &view_image.spec else { + panic!("view_image should be a function tool"); + }; + let JsonSchema::Object { properties, .. } = parameters else { + panic!("view_image should use an object schema"); + }; + assert!(!properties.contains_key("detail")); +} + +#[test] +fn view_image_tool_includes_detail_with_original_detail_feature() { + let mut model_info = model_info(); + model_info.supports_image_detail_original = true; + let mut features = Features::with_defaults(); + features.enable(Feature::ImageDetailOriginal); + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + let view_image = find_tool(&tools, VIEW_IMAGE_TOOL_NAME); + let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = &view_image.spec else { + panic!("view_image should be a function tool"); + }; + let JsonSchema::Object { properties, .. } = parameters else { + panic!("view_image should use an object schema"); + }; + assert!(properties.contains_key("detail")); + let Some(JsonSchema::String { + description: Some(description), + }) = properties.get("detail") + else { + panic!("view_image detail should include a description"); + }; + assert!(description.contains("only supported value is `original`")); + assert!(description.contains("omit this field for default resized behavior")); +} + +#[test] +fn test_build_specs_agent_job_worker_tools_enabled() { + let model_info = model_info(); + let mut features = Features::with_defaults(); + features.enable(Feature::SpawnCsv); + features.normalize_dependencies(); + features.enable(Feature::Sqlite); + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::SubAgent(SubAgentSource::Other( + "agent_job:test".to_string(), + )), + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + + assert_contains_tool_names( + &tools, + &[ + "spawn_agent", + "send_input", + "resume_agent", + "wait_agent", + "close_agent", + "spawn_agents_on_csv", + "report_agent_job_result", + ], + ); + assert_lacks_tool_name(&tools, "request_user_input"); +} + +#[test] +fn request_user_input_description_reflects_default_mode_feature_flag() { + let model_info = model_info(); + let mut features = Features::with_defaults(); + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + let request_user_input_tool = find_tool(&tools, REQUEST_USER_INPUT_TOOL_NAME); + assert_eq!( + request_user_input_tool.spec, + request_user_input_tool_spec(/*default_mode_request_user_input*/ false) + ); + + features.enable(Feature::DefaultModeRequestUserInput); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + let request_user_input_tool = find_tool(&tools, REQUEST_USER_INPUT_TOOL_NAME); + assert_eq!( + request_user_input_tool.spec, + request_user_input_tool_spec(/*default_mode_request_user_input*/ true) + ); +} + +#[test] +fn request_permissions_requires_feature_flag() { + let model_info = model_info(); + let features = Features::with_defaults(); + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + assert_lacks_tool_name(&tools, "request_permissions"); + + let mut features = Features::with_defaults(); + features.enable(Feature::RequestPermissionsTool); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + let request_permissions_tool = find_tool(&tools, "request_permissions"); + assert_eq!( + request_permissions_tool.spec, + create_request_permissions_tool(request_permissions_tool_description()) + ); +} + +#[test] +fn request_permissions_tool_is_independent_from_additional_permissions() { + let model_info = model_info(); + let mut features = Features::with_defaults(); + features.enable(Feature::ExecPermissionApprovals); + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + + assert_lacks_tool_name(&tools, "request_permissions"); +} + +#[test] +fn js_repl_requires_feature_flag() { + let model_info = model_info(); + let features = Features::with_defaults(); + + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + + assert!( + !tools.iter().any(|tool| tool.spec.name() == "js_repl"), + "js_repl should be disabled when the feature is off" + ); + assert!( + !tools.iter().any(|tool| tool.spec.name() == "js_repl_reset"), + "js_repl_reset should be disabled when the feature is off" + ); +} + +#[test] +fn js_repl_enabled_adds_tools() { + let model_info = model_info(); + let mut features = Features::with_defaults(); + features.enable(Feature::JsRepl); + + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + + assert_contains_tool_names(&tools, &["js_repl", "js_repl_reset"]); +} + +#[test] +fn image_generation_tools_require_feature_and_supported_model() { + let supported_model_info = model_info(); + let mut unsupported_model_info = supported_model_info.clone(); + unsupported_model_info.input_modalities = vec![InputModality::Text]; + let default_features = Features::with_defaults(); + let mut image_generation_features = default_features.clone(); + image_generation_features.enable(Feature::ImageGeneration); + + let available_models = Vec::new(); + let default_tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &supported_model_info, + available_models: &available_models, + features: &default_features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (default_tools, _) = build_specs( + &default_tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + assert!( + !default_tools + .iter() + .any(|tool| tool.spec.name() == "image_generation"), + "image_generation should be disabled by default" + ); + + let supported_tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &supported_model_info, + available_models: &available_models, + features: &image_generation_features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (supported_tools, _) = build_specs( + &supported_tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + assert_contains_tool_names(&supported_tools, &["image_generation"]); + let image_generation_tool = find_tool(&supported_tools, "image_generation"); + assert_eq!( + serde_json::to_value(&image_generation_tool.spec).expect("serialize image tool"), + serde_json::json!({ + "type": "image_generation", + "output_format": "png" + }) + ); + + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &unsupported_model_info, + available_models: &available_models, + features: &image_generation_features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + assert!( + !tools + .iter() + .any(|tool| tool.spec.name() == "image_generation"), + "image_generation should be disabled for unsupported models" + ); +} + +#[test] +fn web_search_mode_cached_sets_external_web_access_false() { + let model_info = model_info(); + let features = Features::with_defaults(); + + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + + let tool = find_tool(&tools, "web_search"); + assert_eq!( + tool.spec, + ToolSpec::WebSearch { + external_web_access: Some(false), + filters: None, + user_location: None, + search_context_size: None, + search_content_types: None, + } + ); +} + +#[test] +fn web_search_mode_live_sets_external_web_access_true() { + let model_info = model_info(); + let features = Features::with_defaults(); + + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Live), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + + let tool = find_tool(&tools, "web_search"); + assert_eq!( + tool.spec, + ToolSpec::WebSearch { + external_web_access: Some(true), + filters: None, + user_location: None, + search_context_size: None, + search_content_types: None, + } + ); +} + +#[test] +fn web_search_config_is_forwarded_to_tool_spec() { + let model_info = model_info(); + let features = Features::with_defaults(); + let web_search_config = WebSearchConfig { + filters: Some(codex_protocol::config_types::WebSearchFilters { + allowed_domains: Some(vec!["example.com".to_string()]), + }), + user_location: Some(codex_protocol::config_types::WebSearchUserLocation { + r#type: codex_protocol::config_types::WebSearchUserLocationType::Approximate, + country: Some("US".to_string()), + region: Some("California".to_string()), + city: Some("San Francisco".to_string()), + timezone: Some("America/Los_Angeles".to_string()), + }), + search_context_size: Some(codex_protocol::config_types::WebSearchContextSize::High), + }; + + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Live), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }) + .with_web_search_config(Some(web_search_config.clone())); + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + + let tool = find_tool(&tools, "web_search"); + assert_eq!( + tool.spec, + ToolSpec::WebSearch { + external_web_access: Some(true), + filters: web_search_config + .filters + .map(ResponsesApiWebSearchFilters::from), + user_location: web_search_config + .user_location + .map(ResponsesApiWebSearchUserLocation::from), + search_context_size: web_search_config.search_context_size, + search_content_types: None, + } + ); +} + +#[test] +fn web_search_tool_type_text_and_image_sets_search_content_types() { + let mut model_info = model_info(); + model_info.web_search_tool_type = WebSearchToolType::TextAndImage; + let features = Features::with_defaults(); + + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Live), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + + let tool = find_tool(&tools, "web_search"); + assert_eq!( + tool.spec, + ToolSpec::WebSearch { + external_web_access: Some(true), + filters: None, + user_location: None, + search_context_size: None, + search_content_types: Some(vec!["text".to_string(), "image".to_string()]), + } + ); +} + +#[test] +fn mcp_resource_tools_are_hidden_without_mcp_servers() { + let model_info = model_info(); + let features = Features::with_defaults(); + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + + assert!( + !tools.iter().any(|tool| matches!( + tool.spec.name(), + "list_mcp_resources" | "list_mcp_resource_templates" | "read_mcp_resource" + )), + "MCP resource tools should be omitted when no MCP servers are configured" + ); +} + +#[test] +fn mcp_resource_tools_are_included_when_mcp_servers_are_present() { + let model_info = model_info(); + let features = Features::with_defaults(); + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs( + &tools_config, + Some(HashMap::new()), + /*app_tools*/ None, + &[], + ); + + assert_contains_tool_names( + &tools, + &[ + "list_mcp_resources", + "list_mcp_resource_templates", + "read_mcp_resource", + ], + ); +} + +#[test] +#[ignore] +fn test_parallel_support_flags() { + let model_info = model_info(); + let mut features = Features::with_defaults(); + features.enable(Feature::UnifiedExec); + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + + assert!(find_tool(&tools, "exec_command").supports_parallel_tool_calls); + assert!(!find_tool(&tools, "write_stdin").supports_parallel_tool_calls); +} + +#[test] +fn test_test_model_info_includes_sync_tool() { + let mut model_info = model_info(); + model_info.experimental_supported_tools = vec!["test_sync_tool".to_string()]; + let features = Features::with_defaults(); + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + + assert!(tools.iter().any(|tool| tool.name() == "test_sync_tool")); +} + +#[test] +fn test_build_specs_mcp_tools_converted() { + let model_info = model_info(); + let mut features = Features::with_defaults(); + features.enable(Feature::UnifiedExec); + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Live), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs( + &tools_config, + Some(HashMap::from([( + "test_server/do_something_cool".to_string(), + mcp_tool( + "do_something_cool", + "Do something cool", + serde_json::json!({ + "type": "object", + "properties": { + "string_argument": { "type": "string" }, + "number_argument": { "type": "number" }, + "object_argument": { + "type": "object", + "properties": { + "string_property": { "type": "string" }, + "number_property": { "type": "number" }, + }, + "required": ["string_property", "number_property"], + "additionalProperties": false, + }, + }, + }), + ), + )])), + /*app_tools*/ None, + &[], + ); + + let tool = find_tool(&tools, "test_server/do_something_cool"); + assert_eq!( + &tool.spec, + &ToolSpec::Function(ResponsesApiTool { + name: "test_server/do_something_cool".to_string(), + parameters: JsonSchema::Object { + properties: BTreeMap::from([ + ( + "string_argument".to_string(), + JsonSchema::String { description: None } + ), + ( + "number_argument".to_string(), + JsonSchema::Number { description: None } + ), + ( + "object_argument".to_string(), + JsonSchema::Object { + properties: BTreeMap::from([ + ( + "string_property".to_string(), + JsonSchema::String { description: None } + ), + ( + "number_property".to_string(), + JsonSchema::Number { description: None } + ), + ]), + required: Some(vec![ + "string_property".to_string(), + "number_property".to_string(), + ]), + additional_properties: Some(false.into()), + }, + ), + ]), + required: None, + additional_properties: None, + }, + description: "Do something cool".to_string(), + strict: false, + output_schema: Some(mcp_call_tool_result_output_schema(serde_json::json!({}))), + defer_loading: None, + }) + ); +} + +#[test] +fn test_build_specs_mcp_tools_sorted_by_name() { + let model_info = model_info(); + let mut features = Features::with_defaults(); + features.enable(Feature::UnifiedExec); + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + + let tools_map = HashMap::from([ + ( + "test_server/do".to_string(), + mcp_tool("a", "a", serde_json::json!({"type": "object"})), + ), + ( + "test_server/something".to_string(), + mcp_tool("b", "b", serde_json::json!({"type": "object"})), + ), + ( + "test_server/cool".to_string(), + mcp_tool("c", "c", serde_json::json!({"type": "object"})), + ), + ]); + + let (tools, _) = build_specs(&tools_config, Some(tools_map), /*app_tools*/ None, &[]); + + let mcp_names: Vec<_> = tools + .iter() + .map(|tool| tool.name().to_string()) + .filter(|name| name.starts_with("test_server/")) + .collect(); + let expected = vec![ + "test_server/cool".to_string(), + "test_server/do".to_string(), + "test_server/something".to_string(), + ]; + assert_eq!(mcp_names, expected); +} + +#[test] +fn search_tool_description_lists_each_codex_apps_connector_once() { + let model_info = search_capable_model_info(); + let mut features = Features::with_defaults(); + features.enable(Feature::Apps); + features.enable(Feature::ToolSearch); + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + + let (tools, _) = build_specs( + &tools_config, + Some(HashMap::from([ + ( + "mcp__codex_apps__calendar_create_event".to_string(), + mcp_tool( + "calendar_create_event", + "Create calendar event", + serde_json::json!({"type": "object"}), + ), + ), + ( + "mcp__rmcp__echo".to_string(), + mcp_tool("echo", "Echo", serde_json::json!({"type": "object"})), + ), + ])), + Some(vec![ + app_tool( + "_create_event", + "mcp__codex_apps__calendar", + CODEX_APPS_MCP_SERVER_NAME, + Some("Calendar"), + Some("Plan events and manage your calendar."), + ), + app_tool( + "_list_events", + "mcp__codex_apps__calendar", + CODEX_APPS_MCP_SERVER_NAME, + Some("Calendar"), + Some("Plan events and manage your calendar."), + ), + app_tool( + "_search_threads", + "mcp__codex_apps__gmail", + CODEX_APPS_MCP_SERVER_NAME, + Some("Gmail"), + Some("Find and summarize email threads."), + ), + app_tool( + "echo", "rmcp", "rmcp", /*connector_name*/ None, + /*connector_description*/ None, + ), + ]), + &[], + ); + + let search_tool = find_tool(&tools, TOOL_SEARCH_TOOL_NAME); + let ToolSpec::ToolSearch { description, .. } = &search_tool.spec else { + panic!("expected tool_search tool"); + }; + let description = description.as_str(); + assert!(description.contains("- Calendar: Plan events and manage your calendar.")); + assert!(description.contains("- Gmail: Find and summarize email threads.")); + assert_eq!( + description + .matches("- Calendar: Plan events and manage your calendar.") + .count(), + 1 + ); + assert!(!description.contains("mcp__rmcp__echo")); +} + +#[test] +fn search_tool_requires_model_capability_and_feature_flag() { + let model_info = search_capable_model_info(); + let app_tools = Some(vec![app_tool( + "calendar_create_event", + "mcp__codex_apps__calendar", + CODEX_APPS_MCP_SERVER_NAME, + Some("Calendar"), + /*connector_description*/ None, + )]); + + let features = Features::with_defaults(); + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &ModelInfo { + supports_search_tool: false, + ..model_info.clone() + }, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + app_tools.clone(), + &[], + ); + assert_lacks_tool_name(&tools, TOOL_SEARCH_TOOL_NAME); + + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + app_tools.clone(), + &[], + ); + assert_lacks_tool_name(&tools, TOOL_SEARCH_TOOL_NAME); + + let mut features = Features::with_defaults(); + features.enable(Feature::ToolSearch); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs(&tools_config, /*mcp_tools*/ None, app_tools, &[]); + assert_contains_tool_names(&tools, &[TOOL_SEARCH_TOOL_NAME]); +} + +#[test] +fn tool_suggest_is_not_registered_without_feature_flag() { + let model_info = search_capable_model_info(); + let mut features = Features::with_defaults(); + features.enable(Feature::ToolSearch); + features.enable(Feature::Apps); + features.enable(Feature::Plugins); + features.disable(Feature::ToolSuggest); + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs_with_discoverable_tools( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + Some(vec![discoverable_connector( + "connector_2128aebfecb84f64a069897515042a44", + "Google Calendar", + "Plan events and schedules.", + )]), + &[], + ); + + assert!( + !tools + .iter() + .any(|tool| tool.name() == TOOL_SUGGEST_TOOL_NAME) + ); +} + +#[test] +fn tool_suggest_can_be_registered_without_search_tool() { + let model_info = ModelInfo { + supports_search_tool: false, + ..search_capable_model_info() + }; + let mut features = Features::with_defaults(); + features.enable(Feature::Apps); + features.enable(Feature::Plugins); + features.enable(Feature::ToolSuggest); + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + let (tools, _) = build_specs_with_discoverable_tools( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + Some(vec![discoverable_connector( + "connector_2128aebfecb84f64a069897515042a44", + "Google Calendar", + "Plan events and schedules.", + )]), + &[], + ); + + assert_contains_tool_names(&tools, &[TOOL_SUGGEST_TOOL_NAME]); + assert_lacks_tool_name(&tools, TOOL_SEARCH_TOOL_NAME); + + let tool_suggest = find_tool(&tools, TOOL_SUGGEST_TOOL_NAME); + let ToolSpec::Function(ResponsesApiTool { description, .. }) = &tool_suggest.spec else { + panic!("expected function tool"); + }; + assert!(description.contains( + "Suggests a missing connector in an installed plugin, or in narrower cases a not installed but discoverable plugin" + )); + assert!(description.contains( + "You've already tried to find a matching available tool for the user's request but couldn't find a good match. This includes `tool_search` (if available) and other means." + )); +} + +#[test] +fn tool_suggest_description_lists_discoverable_tools() { + let model_info = search_capable_model_info(); + let mut features = Features::with_defaults(); + features.enable(Feature::Apps); + features.enable(Feature::Plugins); + features.enable(Feature::ToolSearch); + features.enable(Feature::ToolSuggest); + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + + let discoverable_tools = vec![ + discoverable_connector( + "connector_2128aebfecb84f64a069897515042a44", + "Google Calendar", + "Plan events and schedules.", + ), + discoverable_connector( + "connector_68df038e0ba48191908c8434991bbac2", + "Gmail", + "Find and summarize email threads.", + ), + DiscoverableTool::Plugin(Box::new(DiscoverablePluginInfo { + id: "sample@test".to_string(), + name: "Sample Plugin".to_string(), + description: None, + has_skills: true, + mcp_server_names: vec!["sample-docs".to_string()], + app_connector_ids: vec!["connector_sample".to_string()], + })), + ]; + + let (tools, _) = build_specs_with_discoverable_tools( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + Some(discoverable_tools), + &[], + ); + + let tool_suggest = find_tool(&tools, TOOL_SUGGEST_TOOL_NAME); + let ToolSpec::Function(ResponsesApiTool { + description, + parameters, + .. + }) = &tool_suggest.spec + else { + panic!("expected function tool"); + }; + assert!(description.contains( + "Suggests a missing connector in an installed plugin, or in narrower cases a not installed but discoverable plugin" + )); + assert!(description.contains("Google Calendar")); + assert!(description.contains("Gmail")); + assert!(description.contains("Sample Plugin")); + assert!(description.contains("Plan events and schedules.")); + assert!(description.contains("Find and summarize email threads.")); + assert!(description.contains("id: `sample@test`, type: plugin, action: install")); + assert!(description.contains("`action_type`: `install` or `enable`")); + assert!( + description.contains("skills; MCP servers: sample-docs; app connectors: connector_sample") + ); + assert!( + description.contains( + "You've already tried to find a matching available tool for the user's request but couldn't find a good match. This includes `tool_search` (if available) and other means." + ) + ); + assert!(description.contains( + "For connectors/apps that are not installed but needed for an installed plugin, suggest to install them if the task requirements match precisely." + )); + assert!(description.contains( + "For plugins that are not installed but discoverable, only suggest discoverable and installable plugins when the user's intent very explicitly and unambiguously matches that plugin itself." + )); + assert!(description.contains( + "Do not suggest a plugin just because one of its connectors or capabilities seems relevant." + )); + assert!(description.contains( + "Apply the stricter explicit-and-unambiguous rule for *discoverable tools* like plugin install suggestions; *missing tools* like connector install suggestions continue to use the normal clear-fit standard." + )); + assert!(description.contains("DO NOT explore or recommend tools that are not on this list.")); + assert!(!description.contains("{{discoverable_tools}}")); + assert!(!description.contains("tool_search fails to find a good match")); + let JsonSchema::Object { required, .. } = parameters else { + panic!("expected object parameters"); + }; + assert_eq!( + required.as_ref(), + Some(&vec![ + "tool_type".to_string(), + "action_type".to_string(), + "tool_id".to_string(), + "suggest_reason".to_string(), + ]) + ); +} + +#[test] +fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() { + let model_info = model_info(); + let mut features = Features::with_defaults(); + features.enable(Feature::CodeMode); + features.enable(Feature::UnifiedExec); + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + + let (tools, _) = build_specs( + &tools_config, + Some(HashMap::from([( + "mcp__sample__echo".to_string(), + mcp_tool( + "echo", + "Echo text", + serde_json::json!({ + "type": "object", + "properties": { + "message": {"type": "string"} + }, + "required": ["message"], + "additionalProperties": false + }), + ), + )])), + /*app_tools*/ None, + &[], + ); + + let ToolSpec::Function(ResponsesApiTool { description, .. }) = + &find_tool(&tools, "mcp__sample__echo").spec + else { + panic!("expected function tool"); + }; + + assert_eq!( + description, + "Echo text\n\nexec tool declaration:\n```ts\ndeclare const tools: { mcp__sample__echo(args: { message: string; }): Promise<{ _meta?: unknown; content: Array; isError?: boolean; structuredContent?: unknown; }>; };\n```" + ); +} + +#[test] +fn code_mode_augments_builtin_tool_descriptions_with_typed_sample() { + let model_info = model_info(); + let mut features = Features::with_defaults(); + features.enable(Feature::CodeMode); + features.enable(Feature::UnifiedExec); + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + let ToolSpec::Function(ResponsesApiTool { description, .. }) = + &find_tool(&tools, VIEW_IMAGE_TOOL_NAME).spec + else { + panic!("expected function tool"); + }; + + assert_eq!( + description, + "View a local image from the filesystem (only use if given a full filepath by the user, and the image isn't already attached to the thread context within tags).\n\nexec tool declaration:\n```ts\ndeclare const tools: { view_image(args: { path: string; }): Promise<{ detail: string | null; image_url: string; }>; };\n```" + ); +} + +#[test] +fn code_mode_only_exec_description_includes_full_nested_tool_details() { + let model_info = model_info(); + let mut features = Features::with_defaults(); + features.enable(Feature::CodeMode); + features.enable(Feature::CodeModeOnly); + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + let ToolSpec::Freeform(FreeformTool { description, .. }) = &find_tool(&tools, "exec").spec + else { + panic!("expected freeform tool"); + }; + + assert!(!description.contains("Enabled nested tools:")); + assert!(!description.contains("Nested tool reference:")); + assert!(description.starts_with( + "Use `exec/wait` tool to run all other tools, do not attempt to use any other tools directly" + )); + assert!(description.contains("### `update_plan` (`update_plan`)")); + assert!(description.contains("### `view_image` (`view_image`)")); +} + +#[test] +fn code_mode_exec_description_omits_nested_tool_details_when_not_code_mode_only() { + let model_info = model_info(); + let mut features = Features::with_defaults(); + features.enable(Feature::CodeMode); + let available_models = Vec::new(); + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + available_models: &available_models, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + }); + + let (tools, _) = build_specs( + &tools_config, + /*mcp_tools*/ None, + /*app_tools*/ None, + &[], + ); + let ToolSpec::Freeform(FreeformTool { description, .. }) = &find_tool(&tools, "exec").spec + else { + panic!("expected freeform tool"); + }; + + assert!(!description.starts_with( + "Use `exec/wait` tool to run all other tools, do not attempt to use any other tools directly" + )); + assert!(!description.contains("### `update_plan` (`update_plan`)")); + assert!(!description.contains("### `view_image` (`view_image`)")); +} + +fn model_info() -> ModelInfo { + serde_json::from_value(json!({ + "slug": "gpt-5-codex", + "display_name": "GPT-5 Codex", + "description": null, + "supported_reasoning_levels": [], + "shell_type": "shell_command", + "visibility": "list", + "supported_in_api": true, + "priority": 1, + "availability_nux": null, + "upgrade": null, + "base_instructions": "base", + "model_messages": null, + "supports_reasoning_summaries": false, + "default_reasoning_summary": "auto", + "support_verbosity": false, + "default_verbosity": null, + "apply_patch_tool_type": "freeform", + "truncation_policy": { + "mode": "bytes", + "limit": 10000 + }, + "supports_parallel_tool_calls": false, + "supports_image_detail_original": false, + "context_window": null, + "auto_compact_token_limit": null, + "effective_context_window_percent": 95, + "experimental_supported_tools": [], + "input_modalities": ["text", "image"], + "supports_search_tool": false + })) + .expect("deserialize test model") +} + +fn search_capable_model_info() -> ModelInfo { + ModelInfo { + supports_search_tool: true, + ..model_info() + } +} + +fn build_specs<'a>( + config: &ToolsConfig, + mcp_tools: Option>, + app_tools: Option>>, + dynamic_tools: &[DynamicToolSpec], +) -> (Vec, Vec) { + build_specs_with_discoverable_tools( + config, + mcp_tools, + app_tools, + /*discoverable_tools*/ None, + dynamic_tools, + ) +} + +fn build_specs_with_discoverable_tools<'a>( + config: &ToolsConfig, + mcp_tools: Option>, + app_tools: Option>>, + discoverable_tools: Option>, + dynamic_tools: &[DynamicToolSpec], +) -> (Vec, Vec) { + let plan = build_tool_registry_plan( + config, + ToolRegistryPlanParams { + mcp_tools: mcp_tools.as_ref(), + app_tools: app_tools.as_deref(), + discoverable_tools: discoverable_tools.as_deref(), + dynamic_tools, + default_agent_type_description: DEFAULT_AGENT_TYPE_DESCRIPTION, + wait_agent_timeouts: wait_agent_timeout_options(), + codex_apps_mcp_server_name: CODEX_APPS_MCP_SERVER_NAME, + }, + ); + (plan.specs, plan.handlers) +} + +fn mcp_tool(name: &str, description: &str, input_schema: serde_json::Value) -> rmcp::model::Tool { + rmcp::model::Tool { + name: name.to_string().into(), + title: None, + description: Some(description.to_string().into()), + input_schema: std::sync::Arc::new(rmcp::model::object(input_schema)), + output_schema: None, + annotations: None, + execution: None, + icons: None, + meta: None, + } +} + +fn discoverable_connector(id: &str, name: &str, description: &str) -> DiscoverableTool { + let slug = name.replace(' ', "-").to_lowercase(); + DiscoverableTool::Connector(Box::new(AppInfo { + id: id.to_string(), + name: name.to_string(), + description: Some(description.to_string()), + logo_url: None, + logo_url_dark: None, + distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, + install_url: Some(format!("https://chatgpt.com/apps/{slug}/{id}")), + is_accessible: false, + is_enabled: true, + plugin_display_names: Vec::new(), + })) +} + +fn app_tool<'a>( + tool_name: &'a str, + tool_namespace: &'a str, + server_name: &'a str, + connector_name: Option<&'a str>, + connector_description: Option<&'a str>, +) -> ToolRegistryPlanAppTool<'a> { + ToolRegistryPlanAppTool { + tool_name, + tool_namespace, + server_name, + connector_name, + connector_description, + } +} + +fn assert_contains_tool_names(tools: &[ConfiguredToolSpec], expected_subset: &[&str]) { + use std::collections::HashSet; + + let mut names = HashSet::new(); + let mut duplicates = Vec::new(); + for name in tools.iter().map(ConfiguredToolSpec::name) { + if !names.insert(name) { + duplicates.push(name); + } + } + assert!( + duplicates.is_empty(), + "duplicate tool entries detected: {duplicates:?}" + ); + for expected in expected_subset { + assert!( + names.contains(expected), + "expected tool {expected} to be present; had: {names:?}" + ); + } +} + +fn assert_lacks_tool_name(tools: &[ConfiguredToolSpec], expected_absent: &str) { + let names = tools + .iter() + .map(ConfiguredToolSpec::name) + .collect::>(); + assert!( + !names.contains(&expected_absent), + "expected tool {expected_absent} to be absent; had: {names:?}" + ); +} + +fn request_user_input_tool_spec(default_mode_request_user_input: bool) -> ToolSpec { + create_request_user_input_tool(request_user_input_tool_description( + default_mode_request_user_input, + )) +} + +fn spawn_agent_tool_options(config: &ToolsConfig) -> SpawnAgentToolOptions<'_> { + SpawnAgentToolOptions { + available_models: &config.available_models, + agent_type_description: agent_type_description(config, DEFAULT_AGENT_TYPE_DESCRIPTION), + } +} + +fn wait_agent_timeout_options() -> WaitAgentTimeoutOptions { + WaitAgentTimeoutOptions { + default_timeout_ms: DEFAULT_WAIT_TIMEOUT_MS, + min_timeout_ms: MIN_WAIT_TIMEOUT_MS, + max_timeout_ms: MAX_WAIT_TIMEOUT_MS, + } +} + +fn find_tool<'a>(tools: &'a [ConfiguredToolSpec], expected_name: &str) -> &'a ConfiguredToolSpec { + tools + .iter() + .find(|tool| tool.name() == expected_name) + .unwrap_or_else(|| panic!("expected tool {expected_name}")) +} + +fn strip_descriptions_schema(schema: &mut JsonSchema) { + match schema { + JsonSchema::Boolean { description } + | JsonSchema::String { description } + | JsonSchema::Number { description } => { + *description = None; + } + JsonSchema::Array { items, description } => { + strip_descriptions_schema(items); + *description = None; + } + JsonSchema::Object { + properties, + required: _, + additional_properties, + } => { + for value in properties.values_mut() { + strip_descriptions_schema(value); + } + if let Some(AdditionalProperties::Schema(schema)) = additional_properties { + strip_descriptions_schema(schema); + } + } + } +} + +fn strip_descriptions_tool(spec: &mut ToolSpec) { + match spec { + ToolSpec::ToolSearch { parameters, .. } => strip_descriptions_schema(parameters), + ToolSpec::Function(ResponsesApiTool { parameters, .. }) => { + strip_descriptions_schema(parameters); + } + ToolSpec::Freeform(FreeformTool { .. }) + | ToolSpec::LocalShell {} + | ToolSpec::ImageGeneration { .. } + | ToolSpec::WebSearch { .. } => {} + } +} diff --git a/codex-rs/tools/src/tool_registry_plan_types.rs b/codex-rs/tools/src/tool_registry_plan_types.rs new file mode 100644 index 0000000000..bdcb75acd8 --- /dev/null +++ b/codex-rs/tools/src/tool_registry_plan_types.rs @@ -0,0 +1,118 @@ +use crate::ConfiguredToolSpec; +use crate::DiscoverableTool; +use crate::ToolSpec; +use crate::ToolsConfig; +use crate::WaitAgentTimeoutOptions; +use crate::augment_tool_spec_for_code_mode; +use codex_protocol::dynamic_tools::DynamicToolSpec; +use rmcp::model::Tool as McpTool; +use std::collections::HashMap; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ToolHandlerKind { + AgentJobs, + ApplyPatch, + AssignTaskV2, + CloseAgentV1, + CloseAgentV2, + CodeModeExecute, + CodeModeWait, + DynamicTool, + JsRepl, + JsReplReset, + ListAgentsV2, + ListDir, + Mcp, + McpResource, + Plan, + RequestPermissions, + RequestUserInput, + ResumeAgentV1, + SendInputV1, + SendMessageV2, + Shell, + ShellCommand, + SpawnAgentV1, + SpawnAgentV2, + TestSync, + ToolSearch, + ToolSuggest, + UnifiedExec, + ViewImage, + WaitAgentV1, + WaitAgentV2, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ToolHandlerSpec { + pub name: String, + pub kind: ToolHandlerKind, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ToolRegistryPlan { + pub specs: Vec, + pub handlers: Vec, +} + +#[derive(Debug, Clone, Copy)] +pub struct ToolRegistryPlanParams<'a> { + pub mcp_tools: Option<&'a HashMap>, + pub app_tools: Option<&'a [ToolRegistryPlanAppTool<'a>]>, + pub discoverable_tools: Option<&'a [DiscoverableTool]>, + pub dynamic_tools: &'a [DynamicToolSpec], + pub default_agent_type_description: &'a str, + pub wait_agent_timeouts: WaitAgentTimeoutOptions, + pub codex_apps_mcp_server_name: &'a str, +} + +#[derive(Debug, Clone, Copy)] +pub struct ToolRegistryPlanAppTool<'a> { + pub tool_name: &'a str, + pub tool_namespace: &'a str, + pub server_name: &'a str, + pub connector_name: Option<&'a str>, + pub connector_description: Option<&'a str>, +} + +impl ToolRegistryPlan { + pub(crate) fn new() -> Self { + Self { + specs: Vec::new(), + handlers: Vec::new(), + } + } + + pub(crate) fn push_spec( + &mut self, + spec: ToolSpec, + supports_parallel_tool_calls: bool, + code_mode_enabled: bool, + ) { + let spec = if code_mode_enabled { + augment_tool_spec_for_code_mode(spec) + } else { + spec + }; + self.specs + .push(ConfiguredToolSpec::new(spec, supports_parallel_tool_calls)); + } + + pub(crate) fn register_handler(&mut self, name: impl Into, kind: ToolHandlerKind) { + self.handlers.push(ToolHandlerSpec { + name: name.into(), + kind, + }); + } +} + +pub(crate) fn agent_type_description( + config: &ToolsConfig, + default_agent_type_description: &str, +) -> String { + if config.agent_type_description.is_empty() { + default_agent_type_description.to_string() + } else { + config.agent_type_description.clone() + } +} diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index 590257609e..814a7a783b 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -31,6 +31,7 @@ codex-app-server-protocol = { workspace = true } codex-arg0 = { workspace = true } codex-chatgpt = { workspace = true } codex-cloud-requirements = { workspace = true } +codex-config = { workspace = true } codex-core = { workspace = true } codex-features = { workspace = true } codex-feedback = { workspace = true } diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 89f5916e11..6e2e89e195 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -82,13 +82,13 @@ use codex_app_server_protocol::ThreadRollbackResponse; use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnError as AppServerTurnError; use codex_app_server_protocol::TurnStatus; +use codex_config::types::ApprovalsReviewer; +use codex_config::types::ModelAvailabilityNuxConfig; use codex_core::config::Config; use codex_core::config::ConfigBuilder; use codex_core::config::ConfigOverrides; use codex_core::config::edit::ConfigEdit; use codex_core::config::edit::ConfigEditsBuilder; -use codex_core::config::types::ApprovalsReviewer; -use codex_core::config::types::ModelAvailabilityNuxConfig; use codex_core::config_loader::ConfigLayerStackOrdering; use codex_core::message_history; use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; @@ -6188,9 +6188,9 @@ mod tests { use codex_app_server_protocol::TurnStartedNotification; use codex_app_server_protocol::TurnStatus; use codex_app_server_protocol::UserInput as AppServerUserInput; + use codex_config::types::ModelAvailabilityNuxConfig; use codex_core::config::ConfigBuilder; use codex_core::config::ConfigOverrides; - use codex_core::config::types::ModelAvailabilityNuxConfig; use codex_otel::SessionTelemetry; use codex_protocol::ThreadId; use codex_protocol::config_types::CollaborationMode; diff --git a/codex-rs/tui/src/app_command.rs b/codex-rs/tui/src/app_command.rs index 3c43c61862..0646cc297d 100644 --- a/codex-rs/tui/src/app_command.rs +++ b/codex-rs/tui/src/app_command.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use codex_core::config::types::ApprovalsReviewer; +use codex_config::types::ApprovalsReviewer; use codex_protocol::approvals::ElicitationAction; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::Personality; diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 77a8c6748a..d727963f0d 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -31,7 +31,7 @@ use crate::bottom_pane::StatusLineItem; use crate::bottom_pane::TerminalTitleItem; use crate::history_cell::HistoryCell; -use codex_core::config::types::ApprovalsReviewer; +use codex_config::types::ApprovalsReviewer; use codex_features::Feature; use codex_protocol::config_types::CollaborationModeMask; use codex_protocol::config_types::Personality; diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 3c8d068f9b..02429e9d9b 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -92,12 +92,12 @@ use codex_app_server_protocol::TurnCompletedNotification; use codex_app_server_protocol::TurnPlanStepStatus; use codex_app_server_protocol::TurnStatus; use codex_chatgpt::connectors; +use codex_config::types::ApprovalsReviewer; +use codex_config::types::Notifications; +use codex_config::types::WindowsSandboxModeToml; use codex_core::config::Config; use codex_core::config::Constrained; use codex_core::config::ConstraintResult; -use codex_core::config::types::ApprovalsReviewer; -use codex_core::config::types::Notifications; -use codex_core::config::types::WindowsSandboxModeToml; use codex_core::config_loader::ConfigLayerStackOrdering; use codex_core::find_thread_name_by_id; use codex_core::plugins::PluginsManager; diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index e87634aea5..9af2cd30a7 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -79,14 +79,14 @@ pub(super) use codex_app_server_protocol::TurnError as AppServerTurnError; pub(super) use codex_app_server_protocol::TurnStartedNotification; pub(super) use codex_app_server_protocol::TurnStatus as AppServerTurnStatus; pub(super) use codex_app_server_protocol::UserInput as AppServerUserInput; -pub(super) use codex_core::config::ApprovalsReviewer; +pub(super) use codex_config::types::ApprovalsReviewer; +pub(super) use codex_config::types::Notifications; +#[cfg(target_os = "windows")] +pub(super) use codex_config::types::WindowsSandboxModeToml; pub(super) use codex_core::config::Config; pub(super) use codex_core::config::ConfigBuilder; pub(super) use codex_core::config::Constrained; pub(super) use codex_core::config::ConstraintError; -pub(super) use codex_core::config::types::Notifications; -#[cfg(target_os = "windows")] -pub(super) use codex_core::config::types::WindowsSandboxModeToml; pub(super) use codex_core::config_loader::AppRequirementToml; pub(super) use codex_core::config_loader::AppsRequirementsToml; pub(super) use codex_core::config_loader::ConfigLayerStack; diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index badd43b674..cf65f918e5 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -40,8 +40,8 @@ use crate::wrapping::adaptive_wrap_line; use crate::wrapping::adaptive_wrap_lines; use base64::Engine; use codex_app_server_protocol::McpServerStatus; +use codex_config::types::McpServerTransportConfig; use codex_core::config::Config; -use codex_core::config::types::McpServerTransportConfig; #[cfg(test)] use codex_core::mcp::McpManager; #[cfg(test)] @@ -2768,10 +2768,10 @@ mod tests { use crate::exec_cell::CommandOutput; use crate::exec_cell::ExecCall; use crate::exec_cell::ExecCell; + use codex_config::types::McpServerConfig; + use codex_config::types::McpServerDisabledReason; use codex_core::config::Config; use codex_core::config::ConfigBuilder; - use codex_core::config::types::McpServerConfig; - use codex_core::config::types::McpServerDisabledReason; use codex_otel::RuntimeMetricTotals; use codex_otel::RuntimeMetricsSummary; use codex_protocol::ThreadId; diff --git a/codex-rs/tui/src/notifications/mod.rs b/codex-rs/tui/src/notifications/mod.rs index e9a7ac1ddf..ff5e3defae 100644 --- a/codex-rs/tui/src/notifications/mod.rs +++ b/codex-rs/tui/src/notifications/mod.rs @@ -5,7 +5,7 @@ use std::env; use std::io; use bel::BelBackend; -use codex_core::config::types::NotificationMethod; +use codex_config::types::NotificationMethod; use osc9::Osc9Backend; #[derive(Debug)] @@ -74,7 +74,7 @@ fn supports_osc9() -> bool { #[cfg(test)] mod tests { use super::detect_backend; - use codex_core::config::types::NotificationMethod; + use codex_config::types::NotificationMethod; use serial_test::serial; use std::ffi::OsString; diff --git a/codex-rs/tui/src/tui.rs b/codex-rs/tui/src/tui.rs index 9d521d1aa6..0877835ba8 100644 --- a/codex-rs/tui/src/tui.rs +++ b/codex-rs/tui/src/tui.rs @@ -45,7 +45,7 @@ use crate::tui::event_stream::EventBroker; use crate::tui::event_stream::TuiEventStream; #[cfg(unix)] use crate::tui::job_control::SuspendContext; -use codex_core::config::types::NotificationMethod; +use codex_config::types::NotificationMethod; mod event_stream; mod frame_rate_limiter;