Compare commits

...

4 Commits

Author SHA1 Message Date
Liang-Ting Jiang
5a04eab75f Honor OpenAI file upload config for codex apps 2026-04-17 17:04:40 -07:00
Liang-Ting Jiang
f0c1c2951b Directly expose builtin codex apps tools 2026-04-17 16:55:55 -07:00
Liang-Ting Jiang
27db0443d2 cont 2026-04-15 20:17:30 -07:00
Liang-Ting Jiang
6717d23c57 WIP: add Codex Apps library MCP integration 2026-04-15 20:17:28 -07:00
17 changed files with 391 additions and 27 deletions

1
codex-rs/Cargo.lock generated
View File

@@ -1987,6 +1987,7 @@ dependencies = [
"serde_json",
"serial_test",
"sha1",
"sha2",
"shlex",
"similar",
"tempfile",

View File

@@ -753,7 +753,7 @@ impl MessageProcessor {
| ClientRequest::TurnSteer { request_id, .. } = &codex_request
{
self.analytics_events_client.track_request(
connection_id.0,
connection_request_id.connection_id.0,
request_id.clone(),
codex_request.clone(),
);

View File

@@ -19,6 +19,19 @@ const OPENAI_FILE_FINALIZE_TIMEOUT: Duration = Duration::from_secs(30);
const OPENAI_FILE_FINALIZE_RETRY_DELAY: Duration = Duration::from_millis(250);
const OPENAI_FILE_USE_CASE: &str = "codex";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OpenAiFileUploadOptions {
pub store_in_library: bool,
}
impl Default for OpenAiFileUploadOptions {
fn default() -> Self {
Self {
store_in_library: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UploadedOpenAiFile {
pub file_id: String,
@@ -98,6 +111,7 @@ pub async fn upload_local_file(
base_url: &str,
auth: &impl AuthProvider,
path: &Path,
options: &OpenAiFileUploadOptions,
) -> Result<UploadedOpenAiFile, OpenAiFileError> {
let metadata = tokio::fs::metadata(path)
.await
@@ -129,12 +143,16 @@ pub async fn upload_local_file(
.unwrap_or("file")
.to_string();
let create_url = format!("{}/files", base_url.trim_end_matches('/'));
let mut create_request = serde_json::json!({
"file_name": file_name,
"file_size": metadata.len(),
"use_case": OPENAI_FILE_USE_CASE,
});
if options.store_in_library {
create_request["store_in_library"] = serde_json::json!(true);
}
let create_response = authorized_request(auth, reqwest::Method::POST, &create_url)
.json(&serde_json::json!({
"file_name": file_name,
"file_size": metadata.len(),
"use_case": OPENAI_FILE_USE_CASE,
}))
.json(&create_request)
.send()
.await
.map_err(|source| OpenAiFileError::Request {
@@ -350,9 +368,14 @@ mod tests {
let path = dir.path().join("hello.txt");
tokio::fs::write(&path, b"hello").await.expect("write file");
let uploaded = upload_local_file(&base_url, &chatgpt_auth(), &path)
.await
.expect("upload succeeds");
let uploaded = upload_local_file(
&base_url,
&chatgpt_auth(),
&path,
&OpenAiFileUploadOptions::default(),
)
.await
.expect("upload succeeds");
assert_eq!(uploaded.file_id, "file_123");
assert_eq!(uploaded.uri, "sediment://file_123");

View File

@@ -54,6 +54,7 @@ pub use crate::endpoint::ResponsesWebsocketClient;
pub use crate::endpoint::ResponsesWebsocketConnection;
pub use crate::endpoint::session_update_session_json;
pub use crate::error::ApiError;
pub use crate::files::OpenAiFileUploadOptions;
pub use crate::files::upload_local_file;
pub use crate::provider::Provider;
pub use crate::provider::RetryConfig;

View File

@@ -0,0 +1,16 @@
pub const SEARCH_LIBRARY_FILES_TOOL_NAME: &str = "search_library_files";
pub const LIST_LIBRARY_DIRECTORY_NODES_TOOL_NAME: &str = "list_library_directory_nodes";
pub const DOWNLOAD_LIBRARY_FILE_TOOL_NAME: &str = "download_library_file";
pub const CREATE_LIBRARY_FILE_TOOL_NAME: &str = "create_library_file";
pub const WRITEBACK_LIBRARY_FILE_TOOL_NAME: &str = "writeback_library_file";
pub fn is_codex_apps_library_tool(tool_name: &str) -> bool {
matches!(
tool_name,
SEARCH_LIBRARY_FILES_TOOL_NAME
| LIST_LIBRARY_DIRECTORY_NODES_TOOL_NAME
| DOWNLOAD_LIBRARY_FILE_TOOL_NAME
| CREATE_LIBRARY_FILE_TOOL_NAME
| WRITEBACK_LIBRARY_FILE_TOOL_NAME
)
}

View File

@@ -1,7 +1,14 @@
pub(crate) mod codex_apps_library_tools;
pub(crate) mod mcp;
pub(crate) mod mcp_connection_manager;
pub(crate) mod mcp_tool_names;
pub use codex_apps_library_tools::CREATE_LIBRARY_FILE_TOOL_NAME;
pub use codex_apps_library_tools::DOWNLOAD_LIBRARY_FILE_TOOL_NAME;
pub use codex_apps_library_tools::LIST_LIBRARY_DIRECTORY_NODES_TOOL_NAME;
pub use codex_apps_library_tools::SEARCH_LIBRARY_FILES_TOOL_NAME;
pub use codex_apps_library_tools::WRITEBACK_LIBRARY_FILE_TOOL_NAME;
pub use codex_apps_library_tools::is_codex_apps_library_tool;
pub use mcp::CODEX_APPS_MCP_SERVER_NAME;
pub use mcp::McpAuthStatusEntry;
pub use mcp::McpConfig;

View File

@@ -727,7 +727,10 @@ async fn list_all_tools_does_not_block_when_startup_snapshot_cache_hit_is_empty(
let timeout_result =
tokio::time::timeout(Duration::from_millis(10), manager.list_all_tools()).await;
let tools = timeout_result.expect("cache-hit startup snapshot should not block");
assert!(tools.is_empty());
assert!(
tools.is_empty(),
"empty startup snapshots should not synthesize codex apps tools"
);
}
#[tokio::test]

View File

@@ -97,6 +97,7 @@ rmcp = { workspace = true, default-features = false, features = [
] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
sha2 = { workspace = true }
sha1 = { workspace = true }
shlex = { workspace = true }
similar = { workspace = true }

View File

@@ -7052,12 +7052,7 @@ async fn run_sampling_request(
let _code_mode_worker = sess
.services
.code_mode_service
.start_turn_worker(
&sess,
&turn_context,
Arc::clone(&router),
Arc::clone(&turn_diff_tracker),
)
.start_turn_worker(&sess, &turn_context, Arc::clone(&turn_diff_tracker))
.await;
let mut retries = 0;
let mut initial_input = Some(input);

View File

@@ -0,0 +1,37 @@
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
use codex_mcp::ToolInfo as McpToolInfo;
use serde_json::Map;
use serde_json::Value;
pub(crate) const CODEX_APPS_META_KEY: &str = "_codex_apps";
const CODEX_APPS_PROVIDER_BUILTIN: &str = "builtin";
const CODEX_APPS_META_PROVIDER_KEY: &str = "provider";
const CODEX_APPS_META_DIRECT_EXPOSE_KEY: &str = "direct_expose";
pub(crate) fn is_direct_exposed_codex_apps_builtin_tool_info(tool: &McpToolInfo) -> bool {
if tool.server_name != CODEX_APPS_MCP_SERVER_NAME || tool.connector_id.is_some() {
return false;
}
let Some(codex_apps_meta) = codex_apps_meta_from_tool_info(tool) else {
return false;
};
codex_apps_meta
.get(CODEX_APPS_META_PROVIDER_KEY)
.and_then(Value::as_str)
== Some(CODEX_APPS_PROVIDER_BUILTIN)
&& codex_apps_meta
.get(CODEX_APPS_META_DIRECT_EXPOSE_KEY)
.and_then(Value::as_bool)
== Some(true)
}
fn codex_apps_meta_from_tool_info(tool: &McpToolInfo) -> Option<&Map<String, Value>> {
tool.tool
.meta
.as_ref()
.and_then(|meta| meta.get(CODEX_APPS_META_KEY))
.and_then(Value::as_object)
}

View File

@@ -410,6 +410,16 @@ fn make_mcp_tool(
tool_name: &str,
connector_id: Option<&str>,
connector_name: Option<&str>,
) -> ToolInfo {
make_mcp_tool_with_meta(server_name, tool_name, connector_id, connector_name, None)
}
fn make_mcp_tool_with_meta(
server_name: &str,
tool_name: &str,
connector_id: Option<&str>,
connector_name: Option<&str>,
meta: Option<JsonObject>,
) -> ToolInfo {
let tool_namespace = if server_name == CODEX_APPS_MCP_SERVER_NAME {
connector_name
@@ -434,7 +444,7 @@ fn make_mcp_tool(
annotations: None,
execution: None,
icons: None,
meta: None,
meta: meta.map(rmcp::model::Meta),
},
connector_id: connector_id.map(str::to_string),
connector_name: connector_name.map(str::to_string),
@@ -443,6 +453,18 @@ fn make_mcp_tool(
}
}
fn direct_exposed_builtin_codex_apps_meta() -> JsonObject {
serde_json::json!({
"_codex_apps": {
"provider": "builtin",
"direct_expose": true,
},
})
.as_object()
.cloned()
.expect("tool metadata object")
}
fn numbered_mcp_tools(count: usize) -> HashMap<String, ToolInfo> {
(0..count)
.map(|index| {
@@ -1131,6 +1153,72 @@ async fn mcp_tool_exposure_directly_exposes_explicit_apps_without_deferred_overl
assert!(deferred_tools.contains_key("mcp__rmcp__tool_0"));
}
#[tokio::test]
async fn mcp_tool_exposure_directly_exposes_builtin_codex_apps_tools_marked_for_direct_exposure() {
let config = test_config().await;
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true).await;
let mcp_tools = HashMap::from([(
"mcp__codex_apps__builtin_search_file".to_string(),
make_mcp_tool_with_meta(
CODEX_APPS_MCP_SERVER_NAME,
"builtin_search_file",
/*connector_id*/ None,
/*connector_name*/ None,
Some(direct_exposed_builtin_codex_apps_meta()),
),
)]);
let exposure = build_mcp_tool_exposure(
&mcp_tools,
/*connectors*/ None,
&[],
&config,
&tools_config,
);
assert_eq!(
exposure.direct_tools.into_keys().collect::<Vec<_>>(),
vec!["mcp__codex_apps__builtin_search_file".to_string()]
);
assert!(exposure.deferred_tools.is_none());
}
#[tokio::test]
async fn mcp_tool_exposure_keeps_direct_exposed_builtin_codex_apps_tools_out_of_deferred_sets() {
let config = test_config().await;
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true).await;
let mut mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD);
mcp_tools.insert(
"mcp__codex_apps__builtin_search_file".to_string(),
make_mcp_tool_with_meta(
CODEX_APPS_MCP_SERVER_NAME,
"builtin_search_file",
/*connector_id*/ None,
/*connector_name*/ None,
Some(direct_exposed_builtin_codex_apps_meta()),
),
);
let exposure = build_mcp_tool_exposure(
&mcp_tools,
/*connectors*/ None,
&[],
&config,
&tools_config,
);
assert_eq!(
exposure.direct_tools.into_keys().collect::<Vec<_>>(),
vec!["mcp__codex_apps__builtin_search_file".to_string()]
);
let deferred_tools = exposure
.deferred_tools
.as_ref()
.expect("large tool sets should be discoverable through tool_search");
assert!(!deferred_tools.contains_key("mcp__codex_apps__builtin_search_file"));
assert!(deferred_tools.contains_key("mcp__rmcp__tool_0"));
}
#[tokio::test]
async fn reconstruct_history_matches_live_compactions() {
let (session, turn_context) = make_session_and_context().await;

View File

@@ -21,6 +21,7 @@ mod compact_remote;
pub use codex_thread::CodexThread;
pub use codex_thread::ThreadConfigSnapshot;
mod agent;
mod codex_apps_mcp_tools;
mod codex_delegate;
mod command_canonicalization;
mod commit_attribution;

View File

@@ -13,6 +13,7 @@
use crate::codex::Session;
use crate::codex::TurnContext;
use codex_api::CoreAuthProvider;
use codex_api::OpenAiFileUploadOptions;
use codex_api::upload_local_file;
use codex_login::CodexAuth;
use serde_json::Value as JsonValue;
@@ -22,6 +23,7 @@ pub(crate) async fn rewrite_mcp_tool_arguments_for_openai_files(
turn_context: &TurnContext,
arguments_value: Option<JsonValue>,
openai_file_input_params: Option<&[String]>,
upload_options: Option<&OpenAiFileUploadOptions>,
) -> Result<Option<JsonValue>, String> {
let Some(openai_file_input_params) = openai_file_input_params else {
return Ok(arguments_value);
@@ -40,9 +42,14 @@ pub(crate) async fn rewrite_mcp_tool_arguments_for_openai_files(
let Some(value) = arguments.get(field_name) else {
continue;
};
let Some(uploaded_value) =
rewrite_argument_value_for_openai_files(turn_context, auth.as_ref(), field_name, value)
.await?
let Some(uploaded_value) = rewrite_argument_value_for_openai_files(
turn_context,
auth.as_ref(),
field_name,
value,
upload_options,
)
.await?
else {
continue;
};
@@ -61,6 +68,7 @@ async fn rewrite_argument_value_for_openai_files(
auth: Option<&CodexAuth>,
field_name: &str,
value: &JsonValue,
upload_options: Option<&OpenAiFileUploadOptions>,
) -> Result<Option<JsonValue>, String> {
match value {
JsonValue::String(path_or_file_ref) => {
@@ -70,6 +78,7 @@ async fn rewrite_argument_value_for_openai_files(
field_name,
/*index*/ None,
path_or_file_ref,
upload_options,
)
.await?;
Ok(Some(rewritten))
@@ -86,6 +95,7 @@ async fn rewrite_argument_value_for_openai_files(
field_name,
Some(index),
path_or_file_ref,
upload_options,
)
.await?;
rewritten_values.push(rewritten);
@@ -102,6 +112,7 @@ async fn build_uploaded_local_argument_value(
field_name: &str,
index: Option<usize>,
file_path: &str,
upload_options: Option<&OpenAiFileUploadOptions>,
) -> Result<JsonValue, String> {
let resolved_path = turn_context.resolve_path(Some(file_path.to_string()));
let Some(auth) = auth else {
@@ -116,10 +127,12 @@ async fn build_uploaded_local_argument_value(
token: Some(token_data.access_token),
account_id: token_data.account_id,
};
let default_upload_options = OpenAiFileUploadOptions::default();
let uploaded = upload_local_file(
turn_context.config.chatgpt_base_url.trim_end_matches('/'),
&upload_auth,
&resolved_path,
upload_options.unwrap_or(&default_upload_options),
)
.await
.map_err(|error| match index {
@@ -159,6 +172,7 @@ mod tests {
&Arc::new(turn_context),
arguments.clone(),
/*openai_file_input_params*/ None,
/*upload_options*/ None,
)
.await
.expect("rewrite should succeed");
@@ -230,6 +244,7 @@ mod tests {
"file",
/*index*/ None,
"file_report.csv",
/*upload_options*/ None,
)
.await
.expect("rewrite should upload the local file");
@@ -247,6 +262,92 @@ mod tests {
);
}
#[tokio::test]
async fn build_uploaded_local_argument_value_honors_upload_options() {
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::body_json;
use wiremock::matchers::header;
use wiremock::matchers::method;
use wiremock::matchers::path;
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/backend-api/files"))
.and(header("chatgpt-account-id", "account_id"))
.and(body_json(serde_json::json!({
"file_name": "library.txt",
"file_size": 7,
"use_case": "codex",
"store_in_library": true,
})))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"file_id": "file_library",
"upload_url": format!("{}/upload/file_library", server.uri()),
})))
.expect(1)
.mount(&server)
.await;
Mock::given(method("PUT"))
.and(path("/upload/file_library"))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/backend-api/files/file_library/uploaded"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"status": "success",
"download_url": format!("{}/download/file_library", server.uri()),
"file_name": "library.txt",
"mime_type": "text/plain",
"file_size_bytes": 7,
})))
.expect(1)
.mount(&server)
.await;
let (_, mut turn_context) = make_session_and_context().await;
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
let dir = tempdir().expect("temp dir");
let local_path = dir.path().join("library.txt");
tokio::fs::write(&local_path, b"library")
.await
.expect("write local file");
turn_context.cwd = AbsolutePathBuf::try_from(dir.path()).expect("absolute path");
let mut config = (*turn_context.config).clone();
config.chatgpt_base_url = format!("{}/backend-api", server.uri());
turn_context.config = Arc::new(config);
let upload_options = OpenAiFileUploadOptions {
store_in_library: true,
};
let rewritten = build_uploaded_local_argument_value(
&turn_context,
Some(&auth),
"file",
/*index*/ None,
"library.txt",
Some(&upload_options),
)
.await
.expect("rewrite should upload the local file");
assert_eq!(
rewritten,
serde_json::json!({
"download_url": format!("{}/download/file_library", server.uri()),
"file_id": "file_library",
"mime_type": "text/plain",
"file_name": "library.txt",
"uri": "sediment://file_library",
"file_size_bytes": 7,
})
);
}
#[tokio::test]
async fn rewrite_argument_value_for_openai_files_rewrites_scalar_path() {
use wiremock::Mock;
@@ -309,6 +410,7 @@ mod tests {
Some(&auth),
"file",
&serde_json::json!("file_report.csv"),
/*upload_options*/ None,
)
.await
.expect("rewrite should succeed");
@@ -423,6 +525,7 @@ mod tests {
Some(&auth),
"files",
&serde_json::json!(["one.csv", "two.csv"]),
/*upload_options*/ None,
)
.await
.expect("rewrite should succeed");
@@ -463,6 +566,7 @@ mod tests {
"file": "/definitely/missing/file.csv",
})),
Some(&["file".to_string()]),
/*upload_options*/ None,
)
.await
.expect_err("missing file should fail");

View File

@@ -14,6 +14,7 @@ use crate::arc_monitor::ArcMonitorOutcome;
use crate::arc_monitor::monitor_action;
use crate::codex::Session;
use crate::codex::TurnContext;
use crate::codex_apps_mcp_tools::CODEX_APPS_META_KEY;
use crate::config::Config;
use crate::config::edit::ConfigEdit;
use crate::config::edit::ConfigEditsBuilder;
@@ -33,6 +34,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_api::OpenAiFileUploadOptions;
use codex_config::types::AppToolApproval;
use codex_features::Feature;
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
@@ -465,6 +467,7 @@ async fn execute_mcp_tool_call(
turn_context,
arguments_value,
metadata.and_then(|metadata| metadata.openai_file_input_params.as_deref()),
metadata.and_then(|metadata| metadata.openai_file_upload_options.as_ref()),
)
.await?;
let request_meta =
@@ -644,9 +647,33 @@ pub(crate) struct McpToolApprovalMetadata {
tool_description: Option<String>,
codex_apps_meta: Option<serde_json::Map<String, serde_json::Value>>,
openai_file_input_params: Option<Vec<String>>,
openai_file_upload_options: Option<OpenAiFileUploadOptions>,
}
const MCP_TOOL_CODEX_APPS_META_KEY: &str = "_codex_apps";
const MCP_TOOL_OPENAI_FILE_UPLOAD_CONFIG_KEY: &str = "openai/fileUploadConfig";
#[derive(Debug, Clone, Deserialize)]
struct RawOpenAiFileUploadConfig {
#[serde(default)]
store_in_library: bool,
}
fn parse_openai_file_upload_options(
meta: Option<&serde_json::Map<String, serde_json::Value>>,
) -> Option<OpenAiFileUploadOptions> {
let raw = meta?
.get(MCP_TOOL_OPENAI_FILE_UPLOAD_CONFIG_KEY)
.cloned()
.and_then(|value| serde_json::from_value::<RawOpenAiFileUploadConfig>(value).ok())?;
if !raw.store_in_library {
return None;
}
Some(OpenAiFileUploadOptions {
store_in_library: true,
})
}
fn custom_mcp_tool_approval_mode(
turn_context: &TurnContext,
@@ -688,7 +715,7 @@ fn build_mcp_tool_call_request_meta(
metadata.and_then(|metadata| metadata.codex_apps_meta.clone())
{
request_meta.insert(
MCP_TOOL_CODEX_APPS_META_KEY.to_string(),
CODEX_APPS_META_KEY.to_string(),
serde_json::Value::Object(codex_apps_meta),
);
}
@@ -1104,13 +1131,16 @@ pub(crate) async fn lookup_mcp_tool_metadata(
.tool
.meta
.as_ref()
.and_then(|meta| meta.get(MCP_TOOL_CODEX_APPS_META_KEY))
.and_then(|meta| meta.get(CODEX_APPS_META_KEY))
.and_then(serde_json::Value::as_object)
.cloned(),
openai_file_input_params: Some(declared_openai_file_input_param_names(
tool_info.tool.meta.as_deref(),
))
.filter(|params| !params.is_empty()),
openai_file_upload_options: parse_openai_file_upload_options(
tool_info.tool.meta.as_deref(),
),
})
}

View File

@@ -61,6 +61,7 @@ fn approval_metadata(
tool_description: tool_description.map(str::to_string),
codex_apps_meta: None,
openai_file_input_params: None,
openai_file_upload_options: None,
}
}
@@ -74,6 +75,24 @@ fn prompt_options(
}
}
#[test]
fn parse_openai_file_upload_options_reads_store_in_library_config() {
let meta = serde_json::json!({
"openai/fileUploadConfig": {
"store_in_library": true,
}
});
let parsed = parse_openai_file_upload_options(meta.as_object());
assert_eq!(
parsed,
Some(codex_api::OpenAiFileUploadOptions {
store_in_library: true,
})
);
}
#[test]
fn approval_required_when_read_only_false_and_destructive() {
let annotations = annotations(Some(false), Some(true), /*open_world*/ None);
@@ -600,6 +619,7 @@ async fn codex_apps_tool_call_request_meta_includes_turn_metadata_and_codex_apps
.expect("_codex_apps metadata should be an object"),
),
openai_file_input_params: None,
openai_file_upload_options: None,
};
assert_eq!(
@@ -610,7 +630,7 @@ async fn codex_apps_tool_call_request_meta_includes_turn_metadata_and_codex_apps
),
Some(serde_json::json!({
crate::X_CODEX_TURN_METADATA_HEADER: expected_turn_metadata,
MCP_TOOL_CODEX_APPS_META_KEY: {
crate::codex_apps_mcp_tools::CODEX_APPS_META_KEY: {
"resource_uri": "connector://calendar/tools/calendar_create_event",
"contains_mcp_source": true,
"connector_id": "calendar",
@@ -748,6 +768,7 @@ fn guardian_mcp_review_request_includes_annotations_when_present() {
tool_description: None,
codex_apps_meta: None,
openai_file_input_params: None,
openai_file_upload_options: None,
};
let request = build_guardian_mcp_tool_review_request("call-1", &invocation, Some(&metadata));
@@ -1274,6 +1295,7 @@ async fn approve_mode_skips_when_annotations_do_not_require_approval() {
tool_description: None,
codex_apps_meta: None,
openai_file_input_params: None,
openai_file_upload_options: None,
};
let decision = maybe_request_mcp_tool_approval(
@@ -1342,6 +1364,7 @@ async fn guardian_mode_skips_auto_when_annotations_do_not_require_approval() {
tool_description: None,
codex_apps_meta: None,
openai_file_input_params: None,
openai_file_upload_options: None,
};
let decision = maybe_request_mcp_tool_approval(
@@ -1413,6 +1436,7 @@ async fn guardian_mode_mcp_denial_returns_rationale_message() {
tool_description: Some("Reads calendar data.".to_string()),
codex_apps_meta: None,
openai_file_input_params: None,
openai_file_upload_options: None,
};
let decision = maybe_request_mcp_tool_approval(
@@ -1464,6 +1488,7 @@ async fn prompt_mode_waits_for_approval_when_annotations_do_not_require_approval
tool_description: None,
codex_apps_meta: None,
openai_file_input_params: None,
openai_file_upload_options: None,
};
let mut approval_task = {
@@ -1541,6 +1566,7 @@ async fn approve_mode_blocks_when_arc_returns_interrupt_for_model() {
tool_description: Some("Performs a risky action.".to_string()),
codex_apps_meta: None,
openai_file_input_params: None,
openai_file_upload_options: None,
};
let decision = maybe_request_mcp_tool_approval(
@@ -1611,6 +1637,7 @@ async fn custom_approve_mode_blocks_when_arc_returns_interrupt_for_model() {
tool_description: Some("Performs a risky action.".to_string()),
codex_apps_meta: None,
openai_file_input_params: None,
openai_file_upload_options: None,
};
let decision = maybe_request_mcp_tool_approval(
@@ -1681,6 +1708,7 @@ async fn approve_mode_blocks_when_arc_returns_interrupt_without_annotations() {
tool_description: Some("Performs a risky action.".to_string()),
codex_apps_meta: None,
openai_file_input_params: None,
openai_file_upload_options: None,
};
let decision = maybe_request_mcp_tool_approval(
@@ -1759,6 +1787,7 @@ async fn full_access_mode_skips_arc_monitor_for_all_approval_modes() {
tool_description: Some("Performs a risky action.".to_string()),
codex_apps_meta: None,
openai_file_input_params: None,
openai_file_upload_options: None,
};
for approval_mode in [
@@ -1861,6 +1890,7 @@ async fn approve_mode_routes_arc_ask_user_to_guardian_when_guardian_reviewer_is_
tool_description: Some("Performs a risky action.".to_string()),
codex_apps_meta: None,
openai_file_input_params: None,
openai_file_upload_options: None,
};
let decision = maybe_request_mcp_tool_approval(

View File

@@ -6,6 +6,7 @@ use codex_mcp::ToolInfo as McpToolInfo;
use codex_mcp::filter_non_codex_apps_mcp_tools_only;
use codex_tools::ToolsConfig;
use crate::codex_apps_mcp_tools::is_direct_exposed_codex_apps_builtin_tool_info;
use crate::config::Config;
use crate::connectors;
@@ -24,6 +25,9 @@ pub(crate) fn build_mcp_tool_exposure(
tools_config: &ToolsConfig,
) -> McpToolExposure {
let mut deferred_tools = filter_non_codex_apps_mcp_tools_only(all_mcp_tools);
deferred_tools.extend(filter_direct_exposed_builtin_codex_apps_tools(
all_mcp_tools,
));
if let Some(connectors) = connectors {
deferred_tools.extend(filter_codex_apps_mcp_tools(
all_mcp_tools,
@@ -45,12 +49,31 @@ pub(crate) fn build_mcp_tool_exposure(
deferred_tools.remove(direct_tool_name);
}
let mut direct_tools = filter_direct_exposed_builtin_codex_apps_tools(all_mcp_tools);
direct_tools.extend(filter_codex_apps_mcp_tools(
all_mcp_tools,
explicitly_enabled_connectors,
config,
));
for direct_tool_name in direct_tools.keys() {
deferred_tools.remove(direct_tool_name);
}
McpToolExposure {
direct_tools,
deferred_tools: (!deferred_tools.is_empty()).then_some(deferred_tools),
}
}
fn filter_direct_exposed_builtin_codex_apps_tools(
mcp_tools: &HashMap<String, McpToolInfo>,
) -> HashMap<String, McpToolInfo> {
mcp_tools
.iter()
.filter(|(_, tool)| is_direct_exposed_codex_apps_builtin_tool_info(tool))
.map(|(name, tool)| (name.clone(), tool.clone()))
.collect()
}
fn filter_codex_apps_mcp_tools(
mcp_tools: &HashMap<String, McpToolInfo>,
connectors: &[connectors::AppInfo],

View File

@@ -91,7 +91,6 @@ impl CodeModeService {
&self,
session: &Arc<Session>,
turn: &Arc<TurnContext>,
router: Arc<ToolRouter>,
tracker: SharedTurnDiffTracker,
) -> Option<codex_code_mode::CodeModeTurnWorker> {
if !turn.features.enabled(Feature::CodeMode) {
@@ -102,8 +101,13 @@ impl CodeModeService {
session: Arc::clone(session),
turn: Arc::clone(turn),
};
let tool_runtime =
ToolCallRuntime::new(router, Arc::clone(session), Arc::clone(turn), tracker);
let nested_router = Arc::new(build_nested_router(&exec).await);
let tool_runtime = ToolCallRuntime::new(
nested_router,
Arc::clone(session),
Arc::clone(turn),
tracker,
);
let host = Arc::new(CoreTurnHost { exec, tool_runtime });
Some(self.inner.start_turn_worker(host))
}