Merge origin/main into rhan/surface-updates and resolve guardian tests conflict

This commit is contained in:
Roy Han
2026-03-16 17:42:39 -07:00
1516 changed files with 150231 additions and 6961 deletions

View File

@@ -159,15 +159,7 @@ async fn auto_compaction_remote_emits_started_and_completed_items() -> Result<()
AuthCredentialsStoreMode::File,
)?;
let server_base_url = format!("{}/v1", server.uri());
let mut mcp = McpProcess::new_with_env(
codex_home.path(),
&[
("OPENAI_BASE_URL", Some(server_base_url.as_str())),
("OPENAI_API_KEY", None),
],
)
.await?;
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let thread_id = start_thread(&mut mcp).await?;

View File

@@ -61,6 +61,7 @@ async fn thread_start_injects_dynamic_tools_into_model_requests() -> Result<()>
name: "demo_tool".to_string(),
description: "Demo dynamic tool".to_string(),
input_schema: input_schema.clone(),
defer_loading: false,
};
// Thread start injects dynamic tools into the thread's tool registry.
@@ -118,6 +119,78 @@ async fn thread_start_injects_dynamic_tools_into_model_requests() -> Result<()>
Ok(())
}
#[tokio::test]
async fn thread_start_keeps_hidden_dynamic_tools_out_of_model_requests() -> Result<()> {
let responses = vec![create_final_assistant_message_sse_response("Done")?];
let server = create_mock_responses_server_sequence_unchecked(responses).await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let dynamic_tool = DynamicToolSpec {
name: "hidden_tool".to_string(),
description: "Hidden dynamic tool".to_string(),
input_schema: json!({
"type": "object",
"properties": {
"city": { "type": "string" }
},
"required": ["city"],
"additionalProperties": false,
}),
defer_loading: true,
};
let thread_req = mcp
.send_thread_start_request(ThreadStartParams {
dynamic_tools: Some(vec![dynamic_tool.clone()]),
..Default::default()
})
.await?;
let thread_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(thread_req)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
let turn_req = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id,
input: vec![V2UserInput::Text {
text: "Hello".to_string(),
text_elements: Vec::new(),
}],
..Default::default()
})
.await?;
let turn_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
)
.await??;
let _turn: TurnStartResponse = to_response::<TurnStartResponse>(turn_resp)?;
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/completed"),
)
.await??;
let bodies = responses_bodies(&server).await?;
assert!(
bodies
.iter()
.all(|body| find_tool(body, &dynamic_tool.name).is_none()),
"hidden dynamic tool should not be sent to the model"
);
Ok(())
}
/// Exercises the full dynamic tool call path (server request, client response, model output).
#[tokio::test]
async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Result<()> {
@@ -154,6 +227,7 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res
"required": ["city"],
"additionalProperties": false,
}),
defer_loading: false,
};
let thread_req = mcp
@@ -322,6 +396,7 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<(
"required": ["city"],
"additionalProperties": false,
}),
defer_loading: false,
};
let thread_req = mcp

View File

@@ -407,7 +407,10 @@ async fn plugin_list_returns_plugin_interface_with_absolute_asset_paths() -> Res
"websiteURL": "https://openai.com/",
"privacyPolicyURL": "https://openai.com/policies/row-privacy-policy/",
"termsOfServiceURL": "https://openai.com/policies/row-terms-of-use/",
"defaultPrompt": "Starter prompt for trying a plugin",
"defaultPrompt": [
"Starter prompt for trying a plugin",
"Find my next action"
],
"brandColor": "#3B82F6",
"composerIcon": "./assets/icon.png",
"logo": "./assets/logo.png",
@@ -466,6 +469,13 @@ async fn plugin_list_returns_plugin_interface_with_absolute_asset_paths() -> Res
interface.terms_of_service_url.as_deref(),
Some("https://openai.com/policies/row-terms-of-use/")
);
assert_eq!(
interface.default_prompt,
Some(vec![
"Starter prompt for trying a plugin".to_string(),
"Find my next action".to_string()
])
);
assert_eq!(
interface.composer_icon,
Some(AbsolutePathBuf::try_from(
@@ -488,6 +498,72 @@ async fn plugin_list_returns_plugin_interface_with_absolute_asset_paths() -> Res
Ok(())
}
#[tokio::test]
async fn plugin_list_accepts_legacy_string_default_prompt() -> Result<()> {
let codex_home = TempDir::new()?;
let repo_root = TempDir::new()?;
let plugin_root = repo_root.path().join("plugins/demo-plugin");
std::fs::create_dir_all(repo_root.path().join(".git"))?;
std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;
std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?;
std::fs::write(
repo_root.path().join(".agents/plugins/marketplace.json"),
r#"{
"name": "codex-curated",
"plugins": [
{
"name": "demo-plugin",
"source": {
"source": "local",
"path": "./plugins/demo-plugin"
}
}
]
}"#,
)?;
std::fs::write(
plugin_root.join(".codex-plugin/plugin.json"),
r##"{
"name": "demo-plugin",
"interface": {
"defaultPrompt": "Starter prompt for trying a plugin"
}
}"##,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_plugin_list_request(PluginListParams {
cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]),
force_remote_sync: false,
})
.await?;
let response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let response: PluginListResponse = to_response(response)?;
let plugin = response
.marketplaces
.iter()
.flat_map(|marketplace| marketplace.plugins.iter())
.find(|plugin| plugin.name == "demo-plugin")
.expect("expected demo-plugin entry");
assert_eq!(
plugin
.interface
.as_ref()
.and_then(|interface| interface.default_prompt.clone()),
Some(vec!["Starter prompt for trying a plugin".to_string()])
);
Ok(())
}
#[tokio::test]
async fn plugin_list_force_remote_sync_returns_remote_sync_error_on_fail_open() -> Result<()> {
let codex_home = TempDir::new()?;

View File

@@ -58,7 +58,10 @@ async fn plugin_read_returns_plugin_details_with_bundle_contents() -> Result<()>
"websiteURL": "https://openai.com/",
"privacyPolicyURL": "https://openai.com/policies/row-privacy-policy/",
"termsOfServiceURL": "https://openai.com/policies/row-terms-of-use/",
"defaultPrompt": "Starter prompt for trying a plugin",
"defaultPrompt": [
"Draft the reply",
"Find my next action"
],
"brandColor": "#3B82F6",
"composerIcon": "./assets/icon.png",
"logo": "./assets/logo.png",
@@ -162,6 +165,18 @@ enabled = true
.and_then(|interface| interface.category.as_deref()),
Some("Design")
);
assert_eq!(
response
.plugin
.summary
.interface
.as_ref()
.and_then(|interface| interface.default_prompt.clone()),
Some(vec![
"Draft the reply".to_string(),
"Find my next action".to_string()
])
);
assert_eq!(response.plugin.skills.len(), 1);
assert_eq!(
response.plugin.skills[0].name,
@@ -183,6 +198,70 @@ enabled = true
Ok(())
}
#[tokio::test]
async fn plugin_read_accepts_legacy_string_default_prompt() -> Result<()> {
let codex_home = TempDir::new()?;
let repo_root = TempDir::new()?;
let plugin_root = repo_root.path().join("plugins/demo-plugin");
std::fs::create_dir_all(repo_root.path().join(".git"))?;
std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;
std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?;
std::fs::write(
repo_root.path().join(".agents/plugins/marketplace.json"),
r#"{
"name": "codex-curated",
"plugins": [
{
"name": "demo-plugin",
"source": {
"source": "local",
"path": "./plugins/demo-plugin"
}
}
]
}"#,
)?;
std::fs::write(
plugin_root.join(".codex-plugin/plugin.json"),
r##"{
"name": "demo-plugin",
"interface": {
"defaultPrompt": "Starter prompt for trying a plugin"
}
}"##,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_plugin_read_request(PluginReadParams {
marketplace_path: AbsolutePathBuf::try_from(
repo_root.path().join(".agents/plugins/marketplace.json"),
)?,
plugin_name: "demo-plugin".to_string(),
})
.await?;
let response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let response: PluginReadResponse = to_response(response)?;
assert_eq!(
response
.plugin
.summary
.interface
.as_ref()
.and_then(|interface| interface.default_prompt.clone()),
Some(vec!["Starter prompt for trying a plugin".to_string()])
);
Ok(())
}
#[tokio::test]
async fn plugin_read_returns_invalid_request_when_plugin_is_missing() -> Result<()> {
let codex_home = TempDir::new()?;

View File

@@ -94,7 +94,6 @@ async fn request_permissions_round_trip() -> Result<()> {
read: None,
write: Some(vec![requested_writes[0].clone()]),
}),
macos: None,
},
scope: PermissionGrantScope::Turn,
})?,