mirror of
https://github.com/openai/codex.git
synced 2026-11-10 21:00:47 +00:00
Merge 67a769daf1 into sapling-pr-archive-bolinfest
This commit is contained in:
@@ -971,6 +971,7 @@ mod tests {
|
||||
match ConfigBuilder::default().build().await {
|
||||
Ok(config) => config,
|
||||
Err(_) => Config::load_default_with_cli_overrides(Vec::new())
|
||||
.await
|
||||
.expect("default config should load"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2891,6 +2891,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mcpAppResourceUri": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -13369,6 +13369,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mcpAppResourceUri": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -11213,6 +11213,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mcpAppResourceUri": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -787,6 +787,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mcpAppResourceUri": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -787,6 +787,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mcpAppResourceUri": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -930,6 +930,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mcpAppResourceUri": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -1444,6 +1444,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mcpAppResourceUri": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -1206,6 +1206,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mcpAppResourceUri": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -1206,6 +1206,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mcpAppResourceUri": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -1206,6 +1206,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mcpAppResourceUri": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -1444,6 +1444,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mcpAppResourceUri": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -1206,6 +1206,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mcpAppResourceUri": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -1444,6 +1444,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mcpAppResourceUri": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -1206,6 +1206,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mcpAppResourceUri": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -1206,6 +1206,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mcpAppResourceUri": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -930,6 +930,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mcpAppResourceUri": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -930,6 +930,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mcpAppResourceUri": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -930,6 +930,12 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mcpAppResourceUri": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -53,7 +53,7 @@ exitCode: number | null,
|
||||
/**
|
||||
* The duration of the command execution in milliseconds.
|
||||
*/
|
||||
durationMs: number | null, } | { "type": "fileChange", id: string, changes: Array<FileUpdateChange>, status: PatchApplyStatus, } | { "type": "mcpToolCall", id: string, server: string, tool: string, status: McpToolCallStatus, arguments: JsonValue, result: McpToolCallResult | null, error: McpToolCallError | null,
|
||||
durationMs: number | null, } | { "type": "fileChange", id: string, changes: Array<FileUpdateChange>, status: PatchApplyStatus, } | { "type": "mcpToolCall", id: string, server: string, tool: string, status: McpToolCallStatus, arguments: JsonValue, mcpAppResourceUri?: string, result: McpToolCallResult | null, error: McpToolCallError | null,
|
||||
/**
|
||||
* The duration of the MCP tool call in milliseconds.
|
||||
*/
|
||||
|
||||
@@ -764,6 +764,7 @@ macro_rules! server_notification_definitions {
|
||||
Display,
|
||||
ExperimentalApi,
|
||||
)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[serde(tag = "method", content = "params", rename_all = "camelCase")]
|
||||
#[strum(serialize_all = "camelCase")]
|
||||
pub enum ServerNotification {
|
||||
|
||||
@@ -502,6 +502,7 @@ impl ThreadHistoryBuilder {
|
||||
.arguments
|
||||
.clone()
|
||||
.unwrap_or(serde_json::Value::Null),
|
||||
mcp_app_resource_uri: payload.mcp_app_resource_uri.clone(),
|
||||
result: None,
|
||||
error: None,
|
||||
duration_ms: None,
|
||||
@@ -518,11 +519,11 @@ impl ThreadHistoryBuilder {
|
||||
let duration_ms = i64::try_from(payload.duration.as_millis()).ok();
|
||||
let (result, error) = match &payload.result {
|
||||
Ok(value) => (
|
||||
Some(McpToolCallResult {
|
||||
Some(Box::new(McpToolCallResult {
|
||||
content: value.content.clone(),
|
||||
structured_content: value.structured_content.clone(),
|
||||
meta: value.meta.clone(),
|
||||
}),
|
||||
})),
|
||||
None,
|
||||
),
|
||||
Err(message) => (
|
||||
@@ -542,6 +543,7 @@ impl ThreadHistoryBuilder {
|
||||
.arguments
|
||||
.clone()
|
||||
.unwrap_or(serde_json::Value::Null),
|
||||
mcp_app_resource_uri: payload.mcp_app_resource_uri.clone(),
|
||||
result,
|
||||
error,
|
||||
duration_ms,
|
||||
@@ -1809,6 +1811,7 @@ mod tests {
|
||||
tool: "lookup".into(),
|
||||
arguments: Some(serde_json::json!({"id":"123"})),
|
||||
},
|
||||
mcp_app_resource_uri: None,
|
||||
duration: Duration::from_millis(8),
|
||||
result: Err("boom".into()),
|
||||
}),
|
||||
@@ -1857,6 +1860,7 @@ mod tests {
|
||||
tool: "lookup".into(),
|
||||
status: McpToolCallStatus::Failed,
|
||||
arguments: serde_json::json!({"id":"123"}),
|
||||
mcp_app_resource_uri: None,
|
||||
result: None,
|
||||
error: Some(McpToolCallError {
|
||||
message: "boom".into(),
|
||||
@@ -1882,6 +1886,7 @@ mod tests {
|
||||
tool: "lookup".into(),
|
||||
arguments: Some(serde_json::json!({"id":"123"})),
|
||||
},
|
||||
mcp_app_resource_uri: Some("ui://widget/lookup.html".into()),
|
||||
duration: Duration::from_millis(8),
|
||||
result: Ok(CallToolResult {
|
||||
content: vec![serde_json::json!({
|
||||
@@ -1911,7 +1916,8 @@ mod tests {
|
||||
tool: "lookup".into(),
|
||||
status: McpToolCallStatus::Completed,
|
||||
arguments: serde_json::json!({"id":"123"}),
|
||||
result: Some(McpToolCallResult {
|
||||
mcp_app_resource_uri: Some("ui://widget/lookup.html".into()),
|
||||
result: Some(Box::new(McpToolCallResult {
|
||||
content: vec![serde_json::json!({
|
||||
"type": "text",
|
||||
"text": "result"
|
||||
@@ -1920,7 +1926,7 @@ mod tests {
|
||||
meta: Some(serde_json::json!({
|
||||
"ui/resourceUri": "ui://widget/lookup.html"
|
||||
})),
|
||||
}),
|
||||
})),
|
||||
error: None,
|
||||
duration_ms: Some(8),
|
||||
}
|
||||
|
||||
@@ -4585,7 +4585,10 @@ pub enum ThreadItem {
|
||||
tool: String,
|
||||
status: McpToolCallStatus,
|
||||
arguments: JsonValue,
|
||||
result: Option<McpToolCallResult>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
mcp_app_resource_uri: Option<String>,
|
||||
result: Option<Box<McpToolCallResult>>,
|
||||
error: Option<McpToolCallError>,
|
||||
/// The duration of the MCP tool call in milliseconds.
|
||||
#[ts(type = "number | null")]
|
||||
|
||||
@@ -2910,6 +2910,7 @@ async fn construct_mcp_tool_call_notification(
|
||||
tool: begin_event.invocation.tool,
|
||||
status: McpToolCallStatus::InProgress,
|
||||
arguments: begin_event.invocation.arguments.unwrap_or(JsonValue::Null),
|
||||
mcp_app_resource_uri: begin_event.mcp_app_resource_uri,
|
||||
result: None,
|
||||
error: None,
|
||||
duration_ms: None,
|
||||
@@ -2936,11 +2937,11 @@ async fn construct_mcp_tool_call_end_notification(
|
||||
|
||||
let (result, error) = match &end_event.result {
|
||||
Ok(value) => (
|
||||
Some(McpToolCallResult {
|
||||
Some(Box::new(McpToolCallResult {
|
||||
content: value.content.clone(),
|
||||
structured_content: value.structured_content.clone(),
|
||||
meta: value.meta.clone(),
|
||||
}),
|
||||
})),
|
||||
None,
|
||||
),
|
||||
Err(message) => (
|
||||
@@ -2957,6 +2958,7 @@ async fn construct_mcp_tool_call_end_notification(
|
||||
tool: end_event.invocation.tool,
|
||||
status,
|
||||
arguments: end_event.invocation.arguments.unwrap_or(JsonValue::Null),
|
||||
mcp_app_resource_uri: end_event.mcp_app_resource_uri,
|
||||
result,
|
||||
error,
|
||||
duration_ms,
|
||||
@@ -4219,6 +4221,7 @@ mod tests {
|
||||
tool: "list_mcp_resources".to_string(),
|
||||
arguments: Some(serde_json::json!({"server": ""})),
|
||||
},
|
||||
mcp_app_resource_uri: Some("ui://widget/list-resources.html".to_string()),
|
||||
};
|
||||
|
||||
let thread_id = ThreadId::new().to_string();
|
||||
@@ -4239,6 +4242,7 @@ mod tests {
|
||||
tool: begin_event.invocation.tool,
|
||||
status: McpToolCallStatus::InProgress,
|
||||
arguments: serde_json::json!({"server": ""}),
|
||||
mcp_app_resource_uri: Some("ui://widget/list-resources.html".to_string()),
|
||||
result: None,
|
||||
error: None,
|
||||
duration_ms: None,
|
||||
@@ -4379,6 +4383,7 @@ mod tests {
|
||||
tool: "list_mcp_resources".to_string(),
|
||||
arguments: None,
|
||||
},
|
||||
mcp_app_resource_uri: None,
|
||||
};
|
||||
|
||||
let thread_id = ThreadId::new().to_string();
|
||||
@@ -4399,6 +4404,7 @@ mod tests {
|
||||
tool: begin_event.invocation.tool,
|
||||
status: McpToolCallStatus::InProgress,
|
||||
arguments: JsonValue::Null,
|
||||
mcp_app_resource_uri: None,
|
||||
result: None,
|
||||
error: None,
|
||||
duration_ms: None,
|
||||
@@ -4430,6 +4436,7 @@ mod tests {
|
||||
tool: "list_mcp_resources".to_string(),
|
||||
arguments: Some(serde_json::json!({"server": ""})),
|
||||
},
|
||||
mcp_app_resource_uri: Some("ui://widget/list-resources.html".to_string()),
|
||||
duration: Duration::from_nanos(92708),
|
||||
result: Ok(result),
|
||||
};
|
||||
@@ -4452,13 +4459,14 @@ mod tests {
|
||||
tool: end_event.invocation.tool,
|
||||
status: McpToolCallStatus::Completed,
|
||||
arguments: serde_json::json!({"server": ""}),
|
||||
result: Some(McpToolCallResult {
|
||||
mcp_app_resource_uri: Some("ui://widget/list-resources.html".to_string()),
|
||||
result: Some(Box::new(McpToolCallResult {
|
||||
content,
|
||||
structured_content: None,
|
||||
meta: Some(serde_json::json!({
|
||||
"ui/resourceUri": "ui://widget/list-resources.html"
|
||||
})),
|
||||
}),
|
||||
})),
|
||||
error: None,
|
||||
duration_ms: Some(0),
|
||||
},
|
||||
@@ -4476,6 +4484,7 @@ mod tests {
|
||||
tool: "list_mcp_resources".to_string(),
|
||||
arguments: None,
|
||||
},
|
||||
mcp_app_resource_uri: None,
|
||||
duration: Duration::from_millis(1),
|
||||
result: Err("boom".to_string()),
|
||||
};
|
||||
@@ -4498,6 +4507,7 @@ mod tests {
|
||||
tool: end_event.invocation.tool,
|
||||
status: McpToolCallStatus::Failed,
|
||||
arguments: JsonValue::Null,
|
||||
mcp_app_resource_uri: None,
|
||||
result: None,
|
||||
error: Some(McpToolCallError {
|
||||
message: "boom".to_string(),
|
||||
|
||||
@@ -2442,6 +2442,7 @@ impl CodexMessageProcessor {
|
||||
))
|
||||
{
|
||||
let trust_target = resolve_root_git_project_for_trust(config.cwd.as_path())
|
||||
.await
|
||||
.unwrap_or_else(|| config.cwd.to_path_buf());
|
||||
let cli_overrides_with_trust;
|
||||
let cli_overrides_for_reload = if let Err(err) =
|
||||
|
||||
@@ -716,6 +716,7 @@ mod tests {
|
||||
match ConfigBuilder::default().build().await {
|
||||
Ok(config) => config,
|
||||
Err(_) => Config::load_default_with_cli_overrides(Vec::new())
|
||||
.await
|
||||
.expect("default config should load"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,12 +432,14 @@ pub async fn run_main_with_transport(
|
||||
Err(err) => {
|
||||
let message = config_warning_from_error("Invalid configuration; using defaults.", &err);
|
||||
config_warnings.push(message);
|
||||
Config::load_default_with_cli_overrides(cli_kv_overrides.clone()).map_err(|e| {
|
||||
std::io::Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
format!("error loading default config after config error: {e}"),
|
||||
)
|
||||
})?
|
||||
Config::load_default_with_cli_overrides(cli_kv_overrides.clone())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
std::io::Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
format!("error loading default config after config error: {e}"),
|
||||
)
|
||||
})?
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -411,11 +411,14 @@ async fn command_exec_streaming_does_not_buffer_output() -> Result<()> {
|
||||
})
|
||||
.await?;
|
||||
|
||||
let delta = read_command_exec_delta(&mut mcp).await?;
|
||||
assert_eq!(delta.process_id, process_id.as_str());
|
||||
assert_eq!(delta.stream, CommandExecOutputStream::Stdout);
|
||||
assert_eq!(STANDARD.decode(&delta.delta_base64)?, b"abcde");
|
||||
assert!(delta.cap_reached);
|
||||
let output = collect_command_exec_output_until(
|
||||
CommandExecDeltaReader::Mcp(&mut mcp),
|
||||
process_id.as_str(),
|
||||
"capped stdout",
|
||||
|_output, delta| delta.stream == CommandExecOutputStream::Stdout && delta.cap_reached,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(output.stdout, "abcde");
|
||||
let terminate_request_id = mcp
|
||||
.send_command_exec_terminate_request(CommandExecTerminateParams {
|
||||
process_id: process_id.clone(),
|
||||
@@ -471,21 +474,13 @@ async fn command_exec_pipe_streams_output_and_accepts_write() -> Result<()> {
|
||||
})
|
||||
.await?;
|
||||
|
||||
let first_stdout = read_command_exec_delta(&mut mcp).await?;
|
||||
let first_stderr = read_command_exec_delta(&mut mcp).await?;
|
||||
let seen = [first_stdout, first_stderr];
|
||||
assert!(
|
||||
seen.iter()
|
||||
.all(|delta| delta.process_id == process_id.as_str())
|
||||
);
|
||||
assert!(seen.iter().any(|delta| {
|
||||
delta.stream == CommandExecOutputStream::Stdout
|
||||
&& delta.delta_base64 == STANDARD.encode("out-start\n")
|
||||
}));
|
||||
assert!(seen.iter().any(|delta| {
|
||||
delta.stream == CommandExecOutputStream::Stderr
|
||||
&& delta.delta_base64 == STANDARD.encode("err-start\n")
|
||||
}));
|
||||
wait_for_command_exec_outputs_contains(
|
||||
&mut mcp,
|
||||
process_id.as_str(),
|
||||
"out-start\n",
|
||||
"err-start\n",
|
||||
)
|
||||
.await?;
|
||||
|
||||
let write_request_id = mcp
|
||||
.send_command_exec_write_request(CommandExecWriteParams {
|
||||
@@ -499,21 +494,13 @@ async fn command_exec_pipe_streams_output_and_accepts_write() -> Result<()> {
|
||||
.await?;
|
||||
assert_eq!(write_response.result, serde_json::json!({}));
|
||||
|
||||
let next_delta = read_command_exec_delta(&mut mcp).await?;
|
||||
let final_delta = read_command_exec_delta(&mut mcp).await?;
|
||||
let seen = [next_delta, final_delta];
|
||||
assert!(
|
||||
seen.iter()
|
||||
.all(|delta| delta.process_id == process_id.as_str())
|
||||
);
|
||||
assert!(seen.iter().any(|delta| {
|
||||
delta.stream == CommandExecOutputStream::Stdout
|
||||
&& delta.delta_base64 == STANDARD.encode("out:hello\n")
|
||||
}));
|
||||
assert!(seen.iter().any(|delta| {
|
||||
delta.stream == CommandExecOutputStream::Stderr
|
||||
&& delta.delta_base64 == STANDARD.encode("err:hello\n")
|
||||
}));
|
||||
wait_for_command_exec_outputs_contains(
|
||||
&mut mcp,
|
||||
process_id.as_str(),
|
||||
"out:hello\n",
|
||||
"err:hello\n",
|
||||
)
|
||||
.await?;
|
||||
|
||||
let response = mcp
|
||||
.read_stream_until_response_message(RequestId::Integer(command_request_id))
|
||||
@@ -562,17 +549,13 @@ async fn command_exec_tty_implies_streaming_and_reports_pty_output() -> Result<(
|
||||
})
|
||||
.await?;
|
||||
|
||||
let started_text = read_command_exec_output_until_contains(
|
||||
wait_for_command_exec_output_contains(
|
||||
&mut mcp,
|
||||
process_id.as_str(),
|
||||
CommandExecOutputStream::Stdout,
|
||||
"tty\n",
|
||||
)
|
||||
.await?;
|
||||
assert!(
|
||||
started_text.contains("tty\n"),
|
||||
"expected TTY startup output, got {started_text:?}"
|
||||
);
|
||||
|
||||
let write_request_id = mcp
|
||||
.send_command_exec_write_request(CommandExecWriteParams {
|
||||
@@ -586,17 +569,13 @@ async fn command_exec_tty_implies_streaming_and_reports_pty_output() -> Result<(
|
||||
.await?;
|
||||
assert_eq!(write_response.result, serde_json::json!({}));
|
||||
|
||||
let echoed_text = read_command_exec_output_until_contains(
|
||||
wait_for_command_exec_output_contains(
|
||||
&mut mcp,
|
||||
process_id.as_str(),
|
||||
CommandExecOutputStream::Stdout,
|
||||
"echo:world\n",
|
||||
)
|
||||
.await?;
|
||||
assert!(
|
||||
echoed_text.contains("echo:world\n"),
|
||||
"expected TTY echo output, got {echoed_text:?}"
|
||||
);
|
||||
|
||||
let response = mcp
|
||||
.read_stream_until_response_message(RequestId::Integer(command_request_id))
|
||||
@@ -643,17 +622,13 @@ async fn command_exec_tty_supports_initial_size_and_resize() -> Result<()> {
|
||||
})
|
||||
.await?;
|
||||
|
||||
let started_text = read_command_exec_output_until_contains(
|
||||
wait_for_command_exec_output_contains(
|
||||
&mut mcp,
|
||||
process_id.as_str(),
|
||||
CommandExecOutputStream::Stdout,
|
||||
"start:31 101\n",
|
||||
)
|
||||
.await?;
|
||||
assert!(
|
||||
started_text.contains("start:31 101\n"),
|
||||
"unexpected initial size output: {started_text:?}"
|
||||
);
|
||||
|
||||
let resize_request_id = mcp
|
||||
.send_command_exec_resize_request(CommandExecResizeParams {
|
||||
@@ -681,17 +656,13 @@ async fn command_exec_tty_supports_initial_size_and_resize() -> Result<()> {
|
||||
.await?;
|
||||
assert_eq!(write_response.result, serde_json::json!({}));
|
||||
|
||||
let resized_text = read_command_exec_output_until_contains(
|
||||
wait_for_command_exec_output_contains(
|
||||
&mut mcp,
|
||||
process_id.as_str(),
|
||||
CommandExecOutputStream::Stdout,
|
||||
"after:45 132\n",
|
||||
)
|
||||
.await?;
|
||||
assert!(
|
||||
resized_text.contains("after:45 132\n"),
|
||||
"unexpected resized output: {resized_text:?}"
|
||||
);
|
||||
|
||||
let response = mcp
|
||||
.read_stream_until_response_message(RequestId::Integer(command_request_id))
|
||||
@@ -744,11 +715,13 @@ async fn command_exec_process_ids_are_connection_scoped_and_disconnect_terminate
|
||||
)
|
||||
.await?;
|
||||
|
||||
let delta = read_command_exec_delta_ws(&mut ws1).await?;
|
||||
assert_eq!(delta.process_id, "shared-process");
|
||||
assert_eq!(delta.stream, CommandExecOutputStream::Stdout);
|
||||
let delta_text = String::from_utf8(STANDARD.decode(&delta.delta_base64)?)?;
|
||||
assert!(delta_text.contains("ready"));
|
||||
collect_command_exec_output_until(
|
||||
CommandExecDeltaReader::Websocket(&mut ws1),
|
||||
"shared-process",
|
||||
"websocket ready output",
|
||||
|output, _delta| output.stdout.contains("ready\n"),
|
||||
)
|
||||
.await?;
|
||||
wait_for_process_marker(&marker, /*should_exist*/ true).await?;
|
||||
|
||||
send_request(
|
||||
@@ -796,31 +769,98 @@ async fn read_command_exec_delta(
|
||||
decode_delta_notification(notification)
|
||||
}
|
||||
|
||||
async fn read_command_exec_output_until_contains(
|
||||
async fn wait_for_command_exec_output_contains(
|
||||
mcp: &mut McpProcess,
|
||||
process_id: &str,
|
||||
stream: CommandExecOutputStream,
|
||||
expected: &str,
|
||||
) -> Result<String> {
|
||||
) -> Result<()> {
|
||||
let stream_name = match stream {
|
||||
CommandExecOutputStream::Stdout => "stdout",
|
||||
CommandExecOutputStream::Stderr => "stderr",
|
||||
};
|
||||
collect_command_exec_output_until(
|
||||
CommandExecDeltaReader::Mcp(mcp),
|
||||
process_id,
|
||||
format!("{stream_name} containing {expected:?}"),
|
||||
|output, _delta| match stream {
|
||||
CommandExecOutputStream::Stdout => output.stdout.contains(expected),
|
||||
CommandExecOutputStream::Stderr => output.stderr.contains(expected),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn wait_for_command_exec_outputs_contains(
|
||||
mcp: &mut McpProcess,
|
||||
process_id: &str,
|
||||
stdout_expected: &str,
|
||||
stderr_expected: &str,
|
||||
) -> Result<()> {
|
||||
collect_command_exec_output_until(
|
||||
CommandExecDeltaReader::Mcp(mcp),
|
||||
process_id,
|
||||
format!("stdout containing {stdout_expected:?} and stderr containing {stderr_expected:?}"),
|
||||
|output, _delta| {
|
||||
output.stdout.contains(stdout_expected) && output.stderr.contains(stderr_expected)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
enum CommandExecDeltaReader<'a> {
|
||||
Mcp(&'a mut McpProcess),
|
||||
Websocket(&'a mut super::connection_handling_websocket::WsClient),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct CollectedCommandExecOutput {
|
||||
stdout: String,
|
||||
stderr: String,
|
||||
}
|
||||
|
||||
async fn collect_command_exec_output_until(
|
||||
mut reader: CommandExecDeltaReader<'_>,
|
||||
process_id: &str,
|
||||
waiting_for: impl Into<String>,
|
||||
mut should_stop: impl FnMut(
|
||||
&CollectedCommandExecOutput,
|
||||
&CommandExecOutputDeltaNotification,
|
||||
) -> bool,
|
||||
) -> Result<CollectedCommandExecOutput> {
|
||||
let waiting_for = waiting_for.into();
|
||||
let deadline = Instant::now() + DEFAULT_READ_TIMEOUT;
|
||||
let mut collected = String::new();
|
||||
let mut output = CollectedCommandExecOutput::default();
|
||||
|
||||
loop {
|
||||
let remaining = deadline.saturating_duration_since(Instant::now());
|
||||
let delta = timeout(remaining, read_command_exec_delta(mcp))
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"timed out waiting for {expected:?} in command/exec output for {process_id}; collected {collected:?}"
|
||||
)
|
||||
})??;
|
||||
let delta = timeout(remaining, async {
|
||||
match &mut reader {
|
||||
CommandExecDeltaReader::Mcp(mcp) => read_command_exec_delta(mcp).await,
|
||||
CommandExecDeltaReader::Websocket(stream) => {
|
||||
read_command_exec_delta_ws(stream).await
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"timed out waiting for {waiting_for} in command/exec output for {process_id}; collected stdout={:?}, stderr={:?}",
|
||||
output.stdout, output.stderr
|
||||
)
|
||||
})??;
|
||||
assert_eq!(delta.process_id, process_id);
|
||||
assert_eq!(delta.stream, stream);
|
||||
|
||||
let delta_text = String::from_utf8(STANDARD.decode(&delta.delta_base64)?)?;
|
||||
collected.push_str(&delta_text.replace('\r', ""));
|
||||
if collected.contains(expected) {
|
||||
return Ok(collected);
|
||||
let delta_text = delta_text.replace('\r', "");
|
||||
match delta.stream {
|
||||
CommandExecOutputStream::Stdout => output.stdout.push_str(&delta_text),
|
||||
CommandExecOutputStream::Stderr => output.stderr.push_str(&delta_text),
|
||||
}
|
||||
if should_stop(&output, &delta) {
|
||||
return Ok(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -717,6 +717,7 @@ model_reasoning_effort = "high"
|
||||
|
||||
let config_toml = std::fs::read_to_string(codex_home.path().join("config.toml"))?;
|
||||
let trusted_root = resolve_root_git_project_for_trust(workspace.path())
|
||||
.await
|
||||
.unwrap_or_else(|| workspace.path().to_path_buf());
|
||||
assert!(config_toml.contains(&persisted_trust_path(&trusted_root)));
|
||||
assert!(config_toml.contains("trust_level = \"trusted\""));
|
||||
@@ -753,8 +754,9 @@ async fn thread_start_with_nested_git_cwd_trusts_repo_root() -> Result<()> {
|
||||
.await??;
|
||||
|
||||
let config_toml = std::fs::read_to_string(codex_home.path().join("config.toml"))?;
|
||||
let trusted_root =
|
||||
resolve_root_git_project_for_trust(&nested).expect("git root should resolve");
|
||||
let trusted_root = resolve_root_git_project_for_trust(&nested)
|
||||
.await
|
||||
.expect("git root should resolve");
|
||||
assert!(config_toml.contains(&persisted_trust_path(&trusted_root)));
|
||||
assert!(!config_toml.contains(&persisted_trust_path(&nested)));
|
||||
|
||||
|
||||
@@ -596,7 +596,7 @@ pub struct GhostSnapshotToml {
|
||||
|
||||
impl ConfigToml {
|
||||
/// Derive the effective sandbox policy from the configuration.
|
||||
pub fn derive_sandbox_policy(
|
||||
pub async fn derive_sandbox_policy(
|
||||
&self,
|
||||
sandbox_mode_override: Option<SandboxMode>,
|
||||
profile_sandbox_mode: Option<SandboxMode>,
|
||||
@@ -610,11 +610,13 @@ impl ConfigToml {
|
||||
let resolved_sandbox_mode = sandbox_mode_override
|
||||
.or(profile_sandbox_mode)
|
||||
.or(self.sandbox_mode)
|
||||
.or_else(|| {
|
||||
.or(if sandbox_mode_was_explicit {
|
||||
None
|
||||
} else {
|
||||
// If no sandbox_mode is set but this directory has a trust decision,
|
||||
// default to workspace-write except on unsandboxed Windows where we
|
||||
// default to read-only.
|
||||
self.get_active_project(resolved_cwd).and_then(|p| {
|
||||
self.get_active_project(resolved_cwd).await.and_then(|p| {
|
||||
if p.is_trusted() || p.is_untrusted() {
|
||||
if cfg!(target_os = "windows")
|
||||
&& windows_sandbox_level == WindowsSandboxLevel::Disabled
|
||||
@@ -676,7 +678,8 @@ impl ConfigToml {
|
||||
|
||||
/// Resolves the cwd to an existing project, or returns None if ConfigToml
|
||||
/// does not contain a project corresponding to cwd or a git repo for cwd
|
||||
pub fn get_active_project(&self, resolved_cwd: &Path) -> Option<ProjectConfig> {
|
||||
pub async fn get_active_project(&self, resolved_cwd: &Path) -> Option<ProjectConfig> {
|
||||
let repo_root = resolve_root_git_project_for_trust(resolved_cwd).await;
|
||||
let projects = self.projects.clone().unwrap_or_default();
|
||||
|
||||
let resolved_cwd_key = project_trust_key(resolved_cwd);
|
||||
@@ -691,8 +694,8 @@ impl ConfigToml {
|
||||
// If cwd lives inside a git repo/worktree, check whether the root git project
|
||||
// (the primary repository working directory) is trusted. This lets
|
||||
// worktrees inherit trust from the main project.
|
||||
if let Some(repo_root) = resolve_root_git_project_for_trust(resolved_cwd) {
|
||||
let repo_root_key = project_trust_key(repo_root.as_path());
|
||||
if let Some(repo_root) = repo_root.as_deref() {
|
||||
let repo_root_key = project_trust_key(repo_root);
|
||||
let repo_root_raw_key = repo_root.to_string_lossy().to_string();
|
||||
if let Some(project_config_for_root) = projects
|
||||
.get(&repo_root_key)
|
||||
|
||||
@@ -78,7 +78,8 @@ async fn apply_role_to_config_inner(
|
||||
role_layer_toml,
|
||||
preserve_current_profile,
|
||||
preserve_current_provider,
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -150,7 +151,7 @@ fn preservation_policy(config: &Config, role_layer_toml: &TomlValue) -> (bool, b
|
||||
mod reload {
|
||||
use super::*;
|
||||
|
||||
pub(super) fn build_next_config(
|
||||
pub(super) async fn build_next_config(
|
||||
config: &Config,
|
||||
role_layer_toml: TomlValue,
|
||||
preserve_current_profile: bool,
|
||||
@@ -171,7 +172,8 @@ mod reload {
|
||||
reload_overrides(config, preserve_current_provider),
|
||||
config.codex_home.clone(),
|
||||
config_layer_stack,
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
if preserve_current_profile {
|
||||
next_config.active_profile = config.active_profile.clone();
|
||||
}
|
||||
|
||||
@@ -457,8 +457,8 @@ fn numbered_mcp_tools(count: usize) -> HashMap<String, ToolInfo> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn tools_config_for_mcp_tool_exposure(search_tool: bool) -> ToolsConfig {
|
||||
let config = test_config();
|
||||
async fn tools_config_for_mcp_tool_exposure(search_tool: bool) -> ToolsConfig {
|
||||
let config = test_config().await;
|
||||
let model_info = ModelsManager::construct_model_info_offline_for_tests(
|
||||
"gpt-5-codex",
|
||||
&config.to_models_manager_config(),
|
||||
@@ -870,7 +870,7 @@ async fn get_base_instructions_no_user_content() {
|
||||
];
|
||||
|
||||
let (session, _turn_context) = make_session_and_context().await;
|
||||
let config = test_config();
|
||||
let config = test_config().await;
|
||||
|
||||
for test_case in test_cases {
|
||||
let model_info = model_info_for_slug(test_case.slug, &config);
|
||||
@@ -1035,10 +1035,10 @@ fn collect_explicit_app_ids_from_skill_items_skips_plain_mentions_with_skill_con
|
||||
assert_eq!(connector_ids, HashSet::<String>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_tool_exposure_directly_exposes_small_effective_tool_sets() {
|
||||
let config = test_config();
|
||||
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true);
|
||||
#[tokio::test]
|
||||
async fn mcp_tool_exposure_directly_exposes_small_effective_tool_sets() {
|
||||
let config = test_config().await;
|
||||
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true).await;
|
||||
let mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD - 1);
|
||||
|
||||
let exposure = build_mcp_tool_exposure(
|
||||
@@ -1057,10 +1057,10 @@ fn mcp_tool_exposure_directly_exposes_small_effective_tool_sets() {
|
||||
assert!(exposure.deferred_tools.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_tool_exposure_searches_large_effective_tool_sets() {
|
||||
let config = test_config();
|
||||
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true);
|
||||
#[tokio::test]
|
||||
async fn mcp_tool_exposure_searches_large_effective_tool_sets() {
|
||||
let config = test_config().await;
|
||||
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true).await;
|
||||
let mcp_tools = numbered_mcp_tools(DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD);
|
||||
|
||||
let exposure = build_mcp_tool_exposure(
|
||||
@@ -1083,10 +1083,10 @@ fn mcp_tool_exposure_searches_large_effective_tool_sets() {
|
||||
assert_eq!(deferred_tool_names, expected_tool_names);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_tool_exposure_directly_exposes_explicit_apps_without_deferred_overlap() {
|
||||
let config = test_config();
|
||||
let tools_config = tools_config_for_mcp_tool_exposure(/*search_tool*/ true);
|
||||
#[tokio::test]
|
||||
async fn mcp_tool_exposure_directly_exposes_explicit_apps_without_deferred_overlap() {
|
||||
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 - 1);
|
||||
mcp_tools.extend([(
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -169,13 +169,14 @@ fn resolve_mcp_oauth_credentials_store_mode(
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn test_config() -> Config {
|
||||
pub(crate) async fn test_config() -> Config {
|
||||
let codex_home = tempfile::tempdir().expect("create temp dir");
|
||||
Config::load_from_base_config_with_overrides(
|
||||
ConfigToml::default(),
|
||||
ConfigOverrides::default(),
|
||||
AbsolutePathBuf::from_absolute_path(codex_home.path()).expect("temp dir should resolve"),
|
||||
)
|
||||
.await
|
||||
.expect("load default test config")
|
||||
}
|
||||
|
||||
@@ -726,6 +727,7 @@ impl ConfigBuilder {
|
||||
codex_home,
|
||||
config_layer_stack,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -786,7 +788,7 @@ impl Config {
|
||||
}
|
||||
|
||||
/// Load a default configuration when user config files are invalid.
|
||||
pub fn load_default_with_cli_overrides(
|
||||
pub async fn load_default_with_cli_overrides(
|
||||
cli_overrides: Vec<(String, TomlValue)>,
|
||||
) -> std::io::Result<Self> {
|
||||
let codex_home = find_codex_home()?;
|
||||
@@ -794,11 +796,12 @@ impl Config {
|
||||
codex_home.to_path_buf(),
|
||||
cli_overrides,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Load a default configuration for a specific Codex home without reading
|
||||
/// user, project, or system config layers.
|
||||
pub fn load_default_with_cli_overrides_for_codex_home(
|
||||
pub async fn load_default_with_cli_overrides_for_codex_home(
|
||||
codex_home: PathBuf,
|
||||
cli_overrides: Vec<(String, TomlValue)>,
|
||||
) -> std::io::Result<Self> {
|
||||
@@ -818,6 +821,7 @@ impl Config {
|
||||
codex_home,
|
||||
ConfigLayerStack::default(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// This is a secondary way of creating [Config], which is appropriate when
|
||||
@@ -1413,22 +1417,24 @@ pub(crate) fn resolve_web_search_mode_for_turn(
|
||||
|
||||
impl Config {
|
||||
#[cfg(test)]
|
||||
fn load_from_base_config_with_overrides(
|
||||
async fn load_from_base_config_with_overrides(
|
||||
cfg: ConfigToml,
|
||||
overrides: ConfigOverrides,
|
||||
codex_home: AbsolutePathBuf,
|
||||
) -> std::io::Result<Self> {
|
||||
// Note this ignores requirements.toml enforcement for tests.
|
||||
let config_layer_stack = ConfigLayerStack::default();
|
||||
Self::load_config_with_layer_stack(cfg, overrides, codex_home, config_layer_stack)
|
||||
Self::load_config_with_layer_stack(cfg, overrides, codex_home, config_layer_stack).await
|
||||
}
|
||||
|
||||
pub(crate) fn load_config_with_layer_stack(
|
||||
pub(crate) async fn load_config_with_layer_stack(
|
||||
cfg: ConfigToml,
|
||||
overrides: ConfigOverrides,
|
||||
codex_home: AbsolutePathBuf,
|
||||
config_layer_stack: ConfigLayerStack,
|
||||
) -> std::io::Result<Self> {
|
||||
// Keep the large config-construction future off small test thread stacks.
|
||||
Box::pin(async move {
|
||||
validate_model_providers(&cfg.model_providers)
|
||||
.map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?;
|
||||
// Ensure that every field of ConfigRequirements is applied to the final
|
||||
@@ -1547,6 +1553,7 @@ impl Config {
|
||||
.collect();
|
||||
let active_project = cfg
|
||||
.get_active_project(resolved_cwd.as_path())
|
||||
.await
|
||||
.unwrap_or(ProjectConfig { trust_level: None });
|
||||
let permission_config_syntax = resolve_permission_config_syntax(
|
||||
&config_layer_stack,
|
||||
@@ -1636,13 +1643,15 @@ impl Config {
|
||||
)
|
||||
} else {
|
||||
let configured_network_proxy_config = NetworkProxyConfig::default();
|
||||
let mut sandbox_policy = cfg.derive_sandbox_policy(
|
||||
sandbox_mode,
|
||||
config_profile.sandbox_mode,
|
||||
windows_sandbox_level,
|
||||
resolved_cwd.as_path(),
|
||||
Some(&constrained_sandbox_policy),
|
||||
);
|
||||
let mut sandbox_policy = cfg
|
||||
.derive_sandbox_policy(
|
||||
sandbox_mode,
|
||||
config_profile.sandbox_mode,
|
||||
windows_sandbox_level,
|
||||
resolved_cwd.as_path(),
|
||||
Some(&constrained_sandbox_policy),
|
||||
)
|
||||
.await;
|
||||
if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = &mut sandbox_policy {
|
||||
for path in &additional_writable_roots {
|
||||
if !writable_roots.iter().any(|existing| existing == path) {
|
||||
@@ -2207,6 +2216,8 @@ impl Config {
|
||||
},
|
||||
};
|
||||
Ok(config)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
fn load_instructions(codex_dir: Option<&AbsolutePathBuf>) -> Option<LoadedUserInstructions> {
|
||||
|
||||
@@ -24,8 +24,8 @@ fn normalize_absolute_path_for_platform_simplifies_windows_verbatim_paths() {
|
||||
assert_eq!(parsed, PathBuf::from(r"D:\c\x\worktrees\2508\swift-base"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restricted_read_implicitly_allows_helper_executables() -> std::io::Result<()> {
|
||||
#[tokio::test]
|
||||
async fn restricted_read_implicitly_allows_helper_executables() -> std::io::Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let cwd = temp_dir.path().join("workspace");
|
||||
let codex_home = temp_dir.path().join(".codex");
|
||||
@@ -64,7 +64,8 @@ fn restricted_read_implicitly_allows_helper_executables() -> std::io::Result<()>
|
||||
..Default::default()
|
||||
},
|
||||
AbsolutePathBuf::from_absolute_path(&codex_home)?,
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
|
||||
let expected_zsh = AbsolutePathBuf::try_from(zsh_path)?;
|
||||
let expected_allowed_arg0_dir = AbsolutePathBuf::try_from(allowed_arg0_dir)?;
|
||||
|
||||
@@ -650,7 +650,7 @@ async fn project_trust_context(
|
||||
let projects = project_trust_config.projects.unwrap_or_default();
|
||||
|
||||
let project_root_key = project_trust_key(project_root.as_path());
|
||||
let repo_root = resolve_root_git_project_for_trust(cwd.as_path());
|
||||
let repo_root = resolve_root_git_project_for_trust(cwd.as_path()).await;
|
||||
let repo_root_key = repo_root.as_ref().map(|root| project_trust_key(root));
|
||||
|
||||
let projects_trust = projects
|
||||
|
||||
@@ -426,10 +426,14 @@ async fn test_get_git_working_tree_state_branch_fallback() {
|
||||
assert_eq!(state.sha, GitSha::new(&remote_sha));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_root_git_project_for_trust_returns_none_outside_repo() {
|
||||
#[tokio::test]
|
||||
async fn resolve_root_git_project_for_trust_returns_none_outside_repo() {
|
||||
let tmp = TempDir::new().expect("tempdir");
|
||||
assert!(resolve_root_git_project_for_trust(tmp.path()).is_none());
|
||||
assert!(
|
||||
resolve_root_git_project_for_trust(tmp.path())
|
||||
.await
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -439,12 +443,15 @@ async fn resolve_root_git_project_for_trust_regular_repo_returns_repo_root() {
|
||||
let expected = std::fs::canonicalize(&repo_path).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
resolve_root_git_project_for_trust(&repo_path),
|
||||
resolve_root_git_project_for_trust(&repo_path).await,
|
||||
Some(expected.clone())
|
||||
);
|
||||
let nested = repo_path.join("sub/dir");
|
||||
std::fs::create_dir_all(&nested).unwrap();
|
||||
assert_eq!(resolve_root_git_project_for_trust(&nested), Some(expected));
|
||||
assert_eq!(
|
||||
resolve_root_git_project_for_trust(&nested).await,
|
||||
Some(expected)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -467,18 +474,20 @@ async fn resolve_root_git_project_for_trust_detects_worktree_and_returns_main_ro
|
||||
.expect("git worktree add");
|
||||
|
||||
let expected = std::fs::canonicalize(&repo_path).ok();
|
||||
let got =
|
||||
resolve_root_git_project_for_trust(&wt_root).and_then(|p| std::fs::canonicalize(p).ok());
|
||||
let got = resolve_root_git_project_for_trust(&wt_root)
|
||||
.await
|
||||
.and_then(|p| std::fs::canonicalize(p).ok());
|
||||
assert_eq!(got, expected);
|
||||
let nested = wt_root.join("nested/sub");
|
||||
std::fs::create_dir_all(&nested).unwrap();
|
||||
let got_nested =
|
||||
resolve_root_git_project_for_trust(&nested).and_then(|p| std::fs::canonicalize(p).ok());
|
||||
let got_nested = resolve_root_git_project_for_trust(&nested)
|
||||
.await
|
||||
.and_then(|p| std::fs::canonicalize(p).ok());
|
||||
assert_eq!(got_nested, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_root_git_project_for_trust_detects_worktree_pointer_without_git_command() {
|
||||
#[tokio::test]
|
||||
async fn resolve_root_git_project_for_trust_detects_worktree_pointer_without_git_command() {
|
||||
let tmp = TempDir::new().expect("tempdir");
|
||||
let repo_root = tmp.path().join("repo");
|
||||
let common_dir = repo_root.join(".git");
|
||||
@@ -495,17 +504,17 @@ fn resolve_root_git_project_for_trust_detects_worktree_pointer_without_git_comma
|
||||
|
||||
let expected = std::fs::canonicalize(&repo_root).unwrap();
|
||||
assert_eq!(
|
||||
resolve_root_git_project_for_trust(&worktree_root),
|
||||
resolve_root_git_project_for_trust(&worktree_root).await,
|
||||
Some(expected.clone())
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_root_git_project_for_trust(&worktree_root.join("nested")),
|
||||
resolve_root_git_project_for_trust(&worktree_root.join("nested")).await,
|
||||
Some(expected)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_root_git_project_for_trust_non_worktrees_gitdir_returns_none() {
|
||||
#[tokio::test]
|
||||
async fn resolve_root_git_project_for_trust_non_worktrees_gitdir_returns_none() {
|
||||
let tmp = TempDir::new().expect("tempdir");
|
||||
let proj = tmp.path().join("proj");
|
||||
std::fs::create_dir_all(proj.join("nested")).unwrap();
|
||||
@@ -520,8 +529,12 @@ fn resolve_root_git_project_for_trust_non_worktrees_gitdir_returns_none() {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(resolve_root_git_project_for_trust(&proj).is_none());
|
||||
assert!(resolve_root_git_project_for_trust(&proj.join("nested")).is_none());
|
||||
assert!(resolve_root_git_project_for_trust(&proj).await.is_none());
|
||||
assert!(
|
||||
resolve_root_git_project_for_trust(&proj.join("nested"))
|
||||
.await
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -823,9 +823,9 @@ async fn interrupt_and_drain_turn(codex: &Codex) -> anyhow::Result<()> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn guardian_review_session_config_change_invalidates_cached_session() {
|
||||
let parent_config = crate::config::test_config();
|
||||
#[tokio::test]
|
||||
async fn guardian_review_session_config_change_invalidates_cached_session() {
|
||||
let parent_config = crate::config::test_config().await;
|
||||
let cached_spawn_config = build_guardian_review_session_config(
|
||||
&parent_config,
|
||||
/*live_network_config*/ None,
|
||||
@@ -855,9 +855,9 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guardian_review_session_config_disables_hooks() {
|
||||
let mut parent_config = crate::config::test_config();
|
||||
#[tokio::test]
|
||||
async fn guardian_review_session_config_disables_hooks() {
|
||||
let mut parent_config = crate::config::test_config().await;
|
||||
parent_config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
|
||||
@@ -1332,8 +1332,8 @@ async fn guardian_review_surfaces_responses_api_errors_in_rejection_reason() ->
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guardian_parallel_reviews_fork_from_last_committed_trunk_history() -> anyhow::Result<()> {
|
||||
#[tokio::test]
|
||||
async fn guardian_parallel_reviews_fork_from_last_committed_trunk_history() -> anyhow::Result<()> {
|
||||
const TEST_STACK_SIZE_BYTES: usize = 2 * 1024 * 1024;
|
||||
|
||||
let handle =
|
||||
@@ -1569,9 +1569,9 @@ fn guardian_parallel_reviews_fork_from_last_committed_trunk_history() -> anyhow:
|
||||
)),
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn guardian_review_session_config_preserves_parent_network_proxy() {
|
||||
let mut parent_config = test_config();
|
||||
#[tokio::test]
|
||||
async fn guardian_review_session_config_preserves_parent_network_proxy() {
|
||||
let mut parent_config = test_config().await;
|
||||
let network = NetworkProxySpec::from_config_and_constraints(
|
||||
NetworkProxyConfig::default(),
|
||||
Some(NetworkConstraints {
|
||||
@@ -1616,9 +1616,9 @@ fn guardian_review_session_config_preserves_parent_network_proxy() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guardian_review_session_config_overrides_parent_developer_instructions() {
|
||||
let mut parent_config = test_config();
|
||||
#[tokio::test]
|
||||
async fn guardian_review_session_config_overrides_parent_developer_instructions() {
|
||||
let mut parent_config = test_config().await;
|
||||
parent_config.developer_instructions =
|
||||
Some("parent or managed config should not replace guardian policy".to_string());
|
||||
|
||||
@@ -1636,9 +1636,9 @@ fn guardian_review_session_config_overrides_parent_developer_instructions() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guardian_review_session_config_uses_live_network_proxy_state() {
|
||||
let mut parent_config = test_config();
|
||||
#[tokio::test]
|
||||
async fn guardian_review_session_config_uses_live_network_proxy_state() {
|
||||
let mut parent_config = test_config().await;
|
||||
let mut parent_network = NetworkProxyConfig::default();
|
||||
parent_network.network.enabled = true;
|
||||
parent_network
|
||||
@@ -1680,9 +1680,9 @@ fn guardian_review_session_config_uses_live_network_proxy_state() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guardian_review_session_config_rejects_pinned_collab_feature() {
|
||||
let mut parent_config = test_config();
|
||||
#[tokio::test]
|
||||
async fn guardian_review_session_config_rejects_pinned_collab_feature() {
|
||||
let mut parent_config = test_config().await;
|
||||
parent_config.features = ManagedFeatures::from_configured(
|
||||
parent_config.features.get().clone(),
|
||||
Some(Sourced {
|
||||
@@ -1708,9 +1708,9 @@ fn guardian_review_session_config_rejects_pinned_collab_feature() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guardian_review_session_config_uses_parent_active_model_instead_of_hardcoded_slug() {
|
||||
let mut parent_config = test_config();
|
||||
#[tokio::test]
|
||||
async fn guardian_review_session_config_uses_parent_active_model_instead_of_hardcoded_slug() {
|
||||
let mut parent_config = test_config().await;
|
||||
parent_config.model = Some("configured-model".to_string());
|
||||
|
||||
let guardian_config = build_guardian_review_session_config_for_test(
|
||||
@@ -1724,8 +1724,8 @@ fn guardian_review_session_config_uses_parent_active_model_instead_of_hardcoded_
|
||||
assert_eq!(guardian_config.model, Some("active-model".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guardian_review_session_config_uses_requirements_guardian_policy_config() {
|
||||
#[tokio::test]
|
||||
async fn guardian_review_session_config_uses_requirements_guardian_policy_config() {
|
||||
let codex_home = tempfile::tempdir().expect("create temp dir");
|
||||
let workspace = tempfile::tempdir().expect("create temp dir");
|
||||
let config_layer_stack = ConfigLayerStack::new(
|
||||
@@ -1748,6 +1748,7 @@ fn guardian_review_session_config_uses_requirements_guardian_policy_config() {
|
||||
codex_home.abs(),
|
||||
config_layer_stack,
|
||||
)
|
||||
.await
|
||||
.expect("load config");
|
||||
|
||||
let guardian_config = build_guardian_review_session_config_for_test(
|
||||
@@ -1766,8 +1767,9 @@ fn guardian_review_session_config_uses_requirements_guardian_policy_config() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guardian_review_session_config_uses_default_guardian_policy_without_requirements_override() {
|
||||
#[tokio::test]
|
||||
async fn guardian_review_session_config_uses_default_guardian_policy_without_requirements_override()
|
||||
{
|
||||
let codex_home = tempfile::tempdir().expect("create temp dir");
|
||||
let workspace = tempfile::tempdir().expect("create temp dir");
|
||||
let config_layer_stack =
|
||||
@@ -1782,6 +1784,7 @@ fn guardian_review_session_config_uses_default_guardian_policy_without_requireme
|
||||
codex_home.abs(),
|
||||
config_layer_stack,
|
||||
)
|
||||
.await
|
||||
.expect("load config");
|
||||
|
||||
let guardian_config = build_guardian_review_session_config_for_test(
|
||||
|
||||
@@ -101,6 +101,9 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
|
||||
let metadata =
|
||||
lookup_mcp_tool_metadata(sess.as_ref(), turn_context.as_ref(), &server, &tool_name).await;
|
||||
let mcp_app_resource_uri = metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.mcp_app_resource_uri.clone());
|
||||
let app_tool_policy = if server == CODEX_APPS_MCP_SERVER_NAME {
|
||||
connectors::app_tool_policy(
|
||||
&turn_context.config,
|
||||
@@ -130,6 +133,7 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
turn_context.as_ref(),
|
||||
&call_id,
|
||||
invocation,
|
||||
mcp_app_resource_uri.clone(),
|
||||
"MCP tool call blocked by app configuration".to_string(),
|
||||
/*already_started*/ false,
|
||||
)
|
||||
@@ -161,6 +165,7 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
let tool_call_begin_event = EventMsg::McpToolCallBegin(McpToolCallBeginEvent {
|
||||
call_id: call_id.clone(),
|
||||
invocation: invocation.clone(),
|
||||
mcp_app_resource_uri: mcp_app_resource_uri.clone(),
|
||||
});
|
||||
notify_mcp_tool_call_event(sess.as_ref(), turn_context.as_ref(), tool_call_begin_event).await;
|
||||
|
||||
@@ -213,6 +218,7 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
let tool_call_end_event = EventMsg::McpToolCallEnd(McpToolCallEndEvent {
|
||||
call_id: call_id.clone(),
|
||||
invocation,
|
||||
mcp_app_resource_uri: mcp_app_resource_uri.clone(),
|
||||
duration,
|
||||
result: result.clone(),
|
||||
});
|
||||
@@ -239,6 +245,7 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
turn_context.as_ref(),
|
||||
&call_id,
|
||||
invocation,
|
||||
mcp_app_resource_uri.clone(),
|
||||
message,
|
||||
/*already_started*/ true,
|
||||
)
|
||||
@@ -254,6 +261,7 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
turn_context.as_ref(),
|
||||
&call_id,
|
||||
invocation,
|
||||
mcp_app_resource_uri.clone(),
|
||||
message,
|
||||
/*already_started*/ true,
|
||||
)
|
||||
@@ -268,6 +276,7 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
turn_context.as_ref(),
|
||||
&call_id,
|
||||
invocation,
|
||||
mcp_app_resource_uri.clone(),
|
||||
message,
|
||||
/*already_started*/ true,
|
||||
)
|
||||
@@ -325,6 +334,7 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
let tool_call_end_event = EventMsg::McpToolCallEnd(McpToolCallEndEvent {
|
||||
call_id: call_id.clone(),
|
||||
invocation,
|
||||
mcp_app_resource_uri,
|
||||
duration,
|
||||
result: result.clone(),
|
||||
});
|
||||
@@ -642,11 +652,14 @@ pub(crate) struct McpToolApprovalMetadata {
|
||||
connector_description: Option<String>,
|
||||
tool_title: Option<String>,
|
||||
tool_description: Option<String>,
|
||||
mcp_app_resource_uri: Option<String>,
|
||||
codex_apps_meta: Option<serde_json::Map<String, serde_json::Value>>,
|
||||
openai_file_input_params: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
const MCP_TOOL_CODEX_APPS_META_KEY: &str = "_codex_apps";
|
||||
const MCP_TOOL_OPENAI_OUTPUT_TEMPLATE_META_KEY: &str = "openai/outputTemplate";
|
||||
const MCP_TOOL_UI_RESOURCE_URI_META_KEY: &str = "ui/resourceUri";
|
||||
|
||||
fn custom_mcp_tool_approval_mode(
|
||||
turn_context: &TurnContext,
|
||||
@@ -1100,6 +1113,7 @@ pub(crate) async fn lookup_mcp_tool_metadata(
|
||||
connector_description,
|
||||
tool_title: tool_info.tool.title,
|
||||
tool_description: tool_info.tool.description.map(std::borrow::Cow::into_owned),
|
||||
mcp_app_resource_uri: get_mcp_app_resource_uri(tool_info.tool.meta.as_deref()),
|
||||
codex_apps_meta: tool_info
|
||||
.tool
|
||||
.meta
|
||||
@@ -1114,6 +1128,26 @@ pub(crate) async fn lookup_mcp_tool_metadata(
|
||||
})
|
||||
}
|
||||
|
||||
fn get_mcp_app_resource_uri(
|
||||
meta: Option<&serde_json::Map<String, serde_json::Value>>,
|
||||
) -> Option<String> {
|
||||
meta.and_then(|meta| {
|
||||
meta.get("ui")
|
||||
.and_then(serde_json::Value::as_object)
|
||||
.and_then(|ui| ui.get("resourceUri"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.or_else(|| {
|
||||
meta.get(MCP_TOOL_UI_RESOURCE_URI_META_KEY)
|
||||
.and_then(serde_json::Value::as_str)
|
||||
})
|
||||
.or_else(|| {
|
||||
meta.get(MCP_TOOL_OPENAI_OUTPUT_TEMPLATE_META_KEY)
|
||||
.and_then(serde_json::Value::as_str)
|
||||
})
|
||||
.map(str::to_string)
|
||||
})
|
||||
}
|
||||
|
||||
async fn lookup_mcp_app_usage_metadata(
|
||||
sess: &Session,
|
||||
server: &str,
|
||||
@@ -1666,6 +1700,7 @@ async fn notify_mcp_tool_call_skip(
|
||||
turn_context: &TurnContext,
|
||||
call_id: &str,
|
||||
invocation: McpInvocation,
|
||||
mcp_app_resource_uri: Option<String>,
|
||||
message: String,
|
||||
already_started: bool,
|
||||
) -> Result<CallToolResult, String> {
|
||||
@@ -1673,6 +1708,7 @@ async fn notify_mcp_tool_call_skip(
|
||||
let tool_call_begin_event = EventMsg::McpToolCallBegin(McpToolCallBeginEvent {
|
||||
call_id: call_id.to_string(),
|
||||
invocation: invocation.clone(),
|
||||
mcp_app_resource_uri: mcp_app_resource_uri.clone(),
|
||||
});
|
||||
notify_mcp_tool_call_event(sess, turn_context, tool_call_begin_event).await;
|
||||
}
|
||||
@@ -1680,6 +1716,7 @@ async fn notify_mcp_tool_call_skip(
|
||||
let tool_call_end_event = EventMsg::McpToolCallEnd(McpToolCallEndEvent {
|
||||
call_id: call_id.to_string(),
|
||||
invocation,
|
||||
mcp_app_resource_uri,
|
||||
duration: Duration::ZERO,
|
||||
result: Err(message.clone()),
|
||||
});
|
||||
|
||||
@@ -59,6 +59,7 @@ fn approval_metadata(
|
||||
connector_description: connector_description.map(str::to_string),
|
||||
tool_title: tool_title.map(str::to_string),
|
||||
tool_description: tool_description.map(str::to_string),
|
||||
mcp_app_resource_uri: None,
|
||||
codex_apps_meta: None,
|
||||
openai_file_input_params: None,
|
||||
}
|
||||
@@ -74,6 +75,35 @@ fn prompt_options(
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_app_resource_uri_reads_known_tool_meta_keys() {
|
||||
let nested = serde_json::json!({
|
||||
"ui": {
|
||||
"resourceUri": "ui://widget/nested.html",
|
||||
},
|
||||
});
|
||||
assert_eq!(
|
||||
get_mcp_app_resource_uri(nested.as_object()),
|
||||
Some("ui://widget/nested.html".to_string())
|
||||
);
|
||||
|
||||
let flat = serde_json::json!({
|
||||
"ui/resourceUri": "ui://widget/flat.html",
|
||||
});
|
||||
assert_eq!(
|
||||
get_mcp_app_resource_uri(flat.as_object()),
|
||||
Some("ui://widget/flat.html".to_string())
|
||||
);
|
||||
|
||||
let output_template = serde_json::json!({
|
||||
"openai/outputTemplate": "ui://widget/output-template.html",
|
||||
});
|
||||
assert_eq!(
|
||||
get_mcp_app_resource_uri(output_template.as_object()),
|
||||
Some("ui://widget/output-template.html".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn approval_required_when_read_only_false_and_destructive() {
|
||||
let annotations = annotations(Some(false), Some(true), /*open_world*/ None);
|
||||
@@ -589,6 +619,7 @@ async fn codex_apps_tool_call_request_meta_includes_turn_metadata_and_codex_apps
|
||||
connector_description: Some("Manage events".to_string()),
|
||||
tool_title: Some("Create Event".to_string()),
|
||||
tool_description: Some("Create a calendar event.".to_string()),
|
||||
mcp_app_resource_uri: None,
|
||||
codex_apps_meta: Some(
|
||||
serde_json::json!({
|
||||
"resource_uri": "connector://calendar/tools/calendar_create_event",
|
||||
@@ -746,6 +777,7 @@ fn guardian_mcp_review_request_includes_annotations_when_present() {
|
||||
connector_description: None,
|
||||
tool_title: None,
|
||||
tool_description: None,
|
||||
mcp_app_resource_uri: None,
|
||||
codex_apps_meta: None,
|
||||
openai_file_input_params: None,
|
||||
};
|
||||
@@ -1272,6 +1304,7 @@ async fn approve_mode_skips_when_annotations_do_not_require_approval() {
|
||||
connector_description: None,
|
||||
tool_title: Some("Read Only Tool".to_string()),
|
||||
tool_description: None,
|
||||
mcp_app_resource_uri: None,
|
||||
codex_apps_meta: None,
|
||||
openai_file_input_params: None,
|
||||
};
|
||||
@@ -1340,6 +1373,7 @@ async fn guardian_mode_skips_auto_when_annotations_do_not_require_approval() {
|
||||
connector_description: None,
|
||||
tool_title: Some("Read Only Tool".to_string()),
|
||||
tool_description: None,
|
||||
mcp_app_resource_uri: None,
|
||||
codex_apps_meta: None,
|
||||
openai_file_input_params: None,
|
||||
};
|
||||
@@ -1411,6 +1445,7 @@ async fn guardian_mode_mcp_denial_returns_rationale_message() {
|
||||
connector_description: None,
|
||||
tool_title: Some("Dangerous Tool".to_string()),
|
||||
tool_description: Some("Reads calendar data.".to_string()),
|
||||
mcp_app_resource_uri: None,
|
||||
codex_apps_meta: None,
|
||||
openai_file_input_params: None,
|
||||
};
|
||||
@@ -1462,6 +1497,7 @@ async fn prompt_mode_waits_for_approval_when_annotations_do_not_require_approval
|
||||
connector_description: None,
|
||||
tool_title: Some("Read Only Tool".to_string()),
|
||||
tool_description: None,
|
||||
mcp_app_resource_uri: None,
|
||||
codex_apps_meta: None,
|
||||
openai_file_input_params: None,
|
||||
};
|
||||
@@ -1539,6 +1575,7 @@ async fn approve_mode_blocks_when_arc_returns_interrupt_for_model() {
|
||||
connector_description: Some("Manage events".to_string()),
|
||||
tool_title: Some("Dangerous Tool".to_string()),
|
||||
tool_description: Some("Performs a risky action.".to_string()),
|
||||
mcp_app_resource_uri: None,
|
||||
codex_apps_meta: None,
|
||||
openai_file_input_params: None,
|
||||
};
|
||||
@@ -1609,6 +1646,7 @@ async fn custom_approve_mode_blocks_when_arc_returns_interrupt_for_model() {
|
||||
connector_description: None,
|
||||
tool_title: Some("Dangerous Tool".to_string()),
|
||||
tool_description: Some("Performs a risky action.".to_string()),
|
||||
mcp_app_resource_uri: None,
|
||||
codex_apps_meta: None,
|
||||
openai_file_input_params: None,
|
||||
};
|
||||
@@ -1679,6 +1717,7 @@ async fn approve_mode_blocks_when_arc_returns_interrupt_without_annotations() {
|
||||
connector_description: Some("Manage events".to_string()),
|
||||
tool_title: Some("Dangerous Tool".to_string()),
|
||||
tool_description: Some("Performs a risky action.".to_string()),
|
||||
mcp_app_resource_uri: None,
|
||||
codex_apps_meta: None,
|
||||
openai_file_input_params: None,
|
||||
};
|
||||
@@ -1757,6 +1796,7 @@ async fn full_access_mode_skips_arc_monitor_for_all_approval_modes() {
|
||||
connector_description: Some("Manage events".to_string()),
|
||||
tool_title: Some("Dangerous Tool".to_string()),
|
||||
tool_description: Some("Performs a risky action.".to_string()),
|
||||
mcp_app_resource_uri: None,
|
||||
codex_apps_meta: None,
|
||||
openai_file_input_params: None,
|
||||
};
|
||||
@@ -1859,6 +1899,7 @@ async fn approve_mode_routes_arc_ask_user_to_guardian_when_guardian_reviewer_is_
|
||||
connector_description: Some("Manage events".to_string()),
|
||||
tool_title: Some("Dangerous Tool".to_string()),
|
||||
tool_description: Some("Performs a risky action.".to_string()),
|
||||
mcp_app_resource_uri: None,
|
||||
codex_apps_meta: None,
|
||||
openai_file_input_params: None,
|
||||
};
|
||||
|
||||
@@ -468,7 +468,7 @@ mod phase2 {
|
||||
impl DispatchHarness {
|
||||
async fn new() -> Self {
|
||||
let codex_home = tempfile::tempdir().expect("create temp codex home");
|
||||
let mut config = test_config();
|
||||
let mut config = test_config().await;
|
||||
config.codex_home =
|
||||
codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(codex_home.path())
|
||||
.expect("codex home is absolute");
|
||||
@@ -895,7 +895,7 @@ mod phase2 {
|
||||
#[tokio::test]
|
||||
async fn dispatch_marks_job_for_retry_when_spawn_agent_fails() {
|
||||
let codex_home = tempfile::tempdir().expect("create temp codex home");
|
||||
let mut config = test_config();
|
||||
let mut config = test_config().await;
|
||||
config.codex_home =
|
||||
codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(codex_home.path())
|
||||
.expect("codex home is absolute");
|
||||
|
||||
@@ -106,7 +106,7 @@ mod tests {
|
||||
async fn build_prompt_input_includes_context_and_user_message() {
|
||||
let codex_home = tempfile::tempdir().expect("create codex home");
|
||||
let cwd = tempfile::tempdir().expect("create cwd");
|
||||
let mut config = test_config();
|
||||
let mut config = test_config().await;
|
||||
config.codex_home =
|
||||
AbsolutePathBuf::from_absolute_path(codex_home.path()).expect("codex home is absolute");
|
||||
config.cwd = AbsolutePathBuf::try_from(cwd.path().to_path_buf()).expect("absolute cwd");
|
||||
|
||||
@@ -62,8 +62,8 @@ pub(crate) async fn build_realtime_startup_context(
|
||||
let history = sess.clone_history().await;
|
||||
let current_thread_section = build_current_thread_section(history.raw_items());
|
||||
let recent_threads = load_recent_threads(sess).await;
|
||||
let recent_work_section = build_recent_work_section(&cwd, &recent_threads);
|
||||
let workspace_section = build_workspace_section_with_user_root(&cwd, home_dir());
|
||||
let recent_work_section = build_recent_work_section(&cwd, &recent_threads).await;
|
||||
let workspace_section = build_workspace_section_with_user_root(&cwd, home_dir()).await;
|
||||
|
||||
if current_thread_section.is_none()
|
||||
&& recent_work_section.is_none()
|
||||
@@ -144,16 +144,18 @@ async fn load_recent_threads(sess: &Session) -> Vec<StoredThread> {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_recent_work_section(cwd: &Path, recent_threads: &[StoredThread]) -> Option<String> {
|
||||
async fn build_recent_work_section(cwd: &Path, recent_threads: &[StoredThread]) -> Option<String> {
|
||||
let mut groups: HashMap<PathBuf, Vec<&StoredThread>> = HashMap::new();
|
||||
for entry in recent_threads {
|
||||
let group =
|
||||
resolve_root_git_project_for_trust(&entry.cwd).unwrap_or_else(|| entry.cwd.clone());
|
||||
let group = resolve_root_git_project_for_trust(&entry.cwd)
|
||||
.await
|
||||
.unwrap_or_else(|| entry.cwd.clone());
|
||||
groups.entry(group).or_default().push(entry);
|
||||
}
|
||||
|
||||
let current_group =
|
||||
resolve_root_git_project_for_trust(cwd).unwrap_or_else(|| cwd.to_path_buf());
|
||||
let current_group = resolve_root_git_project_for_trust(cwd)
|
||||
.await
|
||||
.unwrap_or_else(|| cwd.to_path_buf());
|
||||
let mut groups = groups.into_iter().collect::<Vec<_>>();
|
||||
groups.sort_by(|(left_group, left_entries), (right_group, right_entries)| {
|
||||
let left_latest = left_entries
|
||||
@@ -178,14 +180,13 @@ fn build_recent_work_section(cwd: &Path, recent_threads: &[StoredThread]) -> Opt
|
||||
))
|
||||
});
|
||||
|
||||
let sections = groups
|
||||
.into_iter()
|
||||
.take(MAX_RECENT_WORK_GROUPS)
|
||||
.filter_map(|(group, mut entries)| {
|
||||
entries.sort_by_key(|entry| Reverse(entry.updated_at));
|
||||
format_thread_group(¤t_group, &group, entries)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut sections = Vec::new();
|
||||
for (group, mut entries) in groups.into_iter().take(MAX_RECENT_WORK_GROUPS) {
|
||||
entries.sort_by_key(|entry| Reverse(entry.updated_at));
|
||||
if let Some(section) = format_thread_group(¤t_group, &group, entries).await {
|
||||
sections.push(section);
|
||||
}
|
||||
}
|
||||
(!sections.is_empty()).then(|| sections.join("\n\n"))
|
||||
}
|
||||
|
||||
@@ -306,11 +307,11 @@ pub(crate) fn truncate_realtime_text_to_token_budget(text: &str, budget_tokens:
|
||||
}
|
||||
}
|
||||
|
||||
fn build_workspace_section_with_user_root(
|
||||
async fn build_workspace_section_with_user_root(
|
||||
cwd: &Path,
|
||||
user_root: Option<PathBuf>,
|
||||
) -> Option<String> {
|
||||
let git_root = resolve_root_git_project_for_trust(cwd);
|
||||
let git_root = resolve_root_git_project_for_trust(cwd).await;
|
||||
let cwd_tree = render_tree(cwd);
|
||||
let git_root_tree = git_root
|
||||
.as_ref()
|
||||
@@ -466,13 +467,16 @@ fn format_startup_context_blob(body: &str, budget_tokens: usize) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn format_thread_group(
|
||||
async fn format_thread_group(
|
||||
current_group: &Path,
|
||||
group: &Path,
|
||||
entries: Vec<&StoredThread>,
|
||||
) -> Option<String> {
|
||||
let latest = entries.first()?;
|
||||
let group_label = if resolve_root_git_project_for_trust(latest.cwd.as_path()).is_some() {
|
||||
let group_label = if resolve_root_git_project_for_trust(latest.cwd.as_path())
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
format!("### Git repo: {}", group.display())
|
||||
} else {
|
||||
format!("### Directory: {}", group.display())
|
||||
|
||||
@@ -188,30 +188,31 @@ fn startup_context_blob_is_wrapped_in_tags_and_fits_budget() {
|
||||
assert!(wrapped.len().div_ceil(4) <= 200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_section_requires_meaningful_structure() {
|
||||
#[tokio::test]
|
||||
async fn workspace_section_requires_meaningful_structure() {
|
||||
let cwd = TempDir::new().expect("tempdir");
|
||||
assert_eq!(
|
||||
build_workspace_section_with_user_root(cwd.path(), /*user_root*/ None),
|
||||
build_workspace_section_with_user_root(cwd.path(), /*user_root*/ None).await,
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_section_includes_tree_when_entries_exist() {
|
||||
#[tokio::test]
|
||||
async fn workspace_section_includes_tree_when_entries_exist() {
|
||||
let cwd = TempDir::new().expect("tempdir");
|
||||
fs::create_dir(cwd.path().join("docs")).expect("create docs dir");
|
||||
fs::write(cwd.path().join("README.md"), "hello").expect("write readme");
|
||||
|
||||
let section = build_workspace_section_with_user_root(cwd.path(), /*user_root*/ None)
|
||||
.await
|
||||
.expect("workspace section");
|
||||
assert!(section.contains("Working directory tree:"));
|
||||
assert!(section.contains("- docs/"));
|
||||
assert!(section.contains("- README.md"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_section_includes_user_root_tree_when_distinct() {
|
||||
#[tokio::test]
|
||||
async fn workspace_section_includes_user_root_tree_when_distinct() {
|
||||
let root = TempDir::new().expect("tempdir");
|
||||
let cwd = root.path().join("cwd");
|
||||
let git_root = root.path().join("git");
|
||||
@@ -225,14 +226,15 @@ fn workspace_section_includes_user_root_tree_when_distinct() {
|
||||
fs::write(user_root.join(".zshrc"), "export TEST=1").expect("write home file");
|
||||
|
||||
let section = build_workspace_section_with_user_root(cwd.as_path(), Some(user_root))
|
||||
.await
|
||||
.expect("workspace section");
|
||||
assert!(section.contains("User root tree:"));
|
||||
assert!(section.contains("- code/"));
|
||||
assert!(!section.contains("- .zshrc"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recent_work_section_groups_threads_by_cwd() {
|
||||
#[tokio::test]
|
||||
async fn recent_work_section_groups_threads_by_cwd() {
|
||||
let root = TempDir::new().expect("tempdir");
|
||||
let repo = root.path().join("repo");
|
||||
let workspace_a = repo.join("workspace-a");
|
||||
@@ -268,6 +270,7 @@ fn recent_work_section_groups_threads_by_cwd() {
|
||||
let repo = fs::canonicalize(repo).expect("canonicalize repo");
|
||||
|
||||
let section = build_recent_work_section(current_cwd.as_path(), &recent_threads)
|
||||
.await
|
||||
.expect("recent work section");
|
||||
assert!(section.contains(&format!("### Git repo: {}", repo.display())));
|
||||
assert!(section.contains("Recent sessions: 2"));
|
||||
|
||||
@@ -237,7 +237,7 @@ async fn ignores_session_prefix_messages_when_truncating() {
|
||||
#[tokio::test]
|
||||
async fn shutdown_all_threads_bounded_submits_shutdown_to_every_thread() {
|
||||
let temp_dir = tempdir().expect("tempdir");
|
||||
let mut config = test_config();
|
||||
let mut config = test_config().await;
|
||||
config.codex_home = temp_dir.path().join("codex-home").abs();
|
||||
config.cwd = config.codex_home.abs();
|
||||
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
|
||||
@@ -279,7 +279,7 @@ async fn new_uses_configured_openai_provider_for_model_refresh() {
|
||||
let models_mock = mount_models_once(&server, ModelsResponse { models: vec![] }).await;
|
||||
|
||||
let temp_dir = tempdir().expect("tempdir");
|
||||
let mut config = test_config();
|
||||
let mut config = test_config().await;
|
||||
config.codex_home = temp_dir.path().join("codex-home").abs();
|
||||
config.cwd = config.codex_home.abs();
|
||||
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
|
||||
@@ -422,7 +422,7 @@ fn mixed_response_and_legacy_user_event_history_is_mid_turn() {
|
||||
#[tokio::test]
|
||||
async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_history() {
|
||||
let temp_dir = tempdir().expect("tempdir");
|
||||
let mut config = test_config();
|
||||
let mut config = test_config().await;
|
||||
config.codex_home = temp_dir.path().join("codex-home").abs();
|
||||
config.cwd = config.codex_home.abs();
|
||||
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
|
||||
@@ -525,7 +525,7 @@ async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_histor
|
||||
#[tokio::test]
|
||||
async fn interrupted_fork_snapshot_preserves_explicit_turn_id() {
|
||||
let temp_dir = tempdir().expect("tempdir");
|
||||
let mut config = test_config();
|
||||
let mut config = test_config().await;
|
||||
config.codex_home = temp_dir.path().join("codex-home").abs();
|
||||
config.cwd = config.codex_home.abs();
|
||||
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
|
||||
@@ -618,7 +618,7 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() {
|
||||
#[tokio::test]
|
||||
async fn interrupted_fork_snapshot_uses_persisted_mid_turn_history_without_live_source() {
|
||||
let temp_dir = tempdir().expect("tempdir");
|
||||
let mut config = test_config();
|
||||
let mut config = test_config().await;
|
||||
config.codex_home = temp_dir.path().join("codex-home").abs();
|
||||
config.cwd = config.codex_home.abs();
|
||||
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
|
||||
|
||||
@@ -562,6 +562,7 @@ async fn emit_tool_call_begin(
|
||||
EventMsg::McpToolCallBegin(McpToolCallBeginEvent {
|
||||
call_id: call_id.to_string(),
|
||||
invocation,
|
||||
mcp_app_resource_uri: None,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
@@ -581,6 +582,7 @@ async fn emit_tool_call_end(
|
||||
EventMsg::McpToolCallEnd(McpToolCallEndEvent {
|
||||
call_id: call_id.to_string(),
|
||||
invocation,
|
||||
mcp_app_resource_uri: None,
|
||||
duration,
|
||||
result,
|
||||
}),
|
||||
|
||||
@@ -107,8 +107,8 @@ fn discoverable_connector(id: &str, name: &str, description: &str) -> Discoverab
|
||||
}))
|
||||
}
|
||||
|
||||
fn search_capable_model_info() -> ModelInfo {
|
||||
let config = test_config();
|
||||
async fn search_capable_model_info() -> ModelInfo {
|
||||
let config = test_config().await;
|
||||
let mut model_info = construct_model_info_offline("gpt-5-codex", &config);
|
||||
model_info.supports_search_tool = true;
|
||||
model_info
|
||||
@@ -216,8 +216,8 @@ fn find_namespace_function_tool<'a>(
|
||||
.unwrap_or_else(|| panic!("expected tool {expected_namespace}{expected_name} in namespace"))
|
||||
}
|
||||
|
||||
fn multi_agent_v2_tools_config() -> ToolsConfig {
|
||||
let config = test_config();
|
||||
async fn multi_agent_v2_tools_config() -> ToolsConfig {
|
||||
let config = test_config().await;
|
||||
let model_info = construct_model_info_offline("gpt-5-codex", &config);
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::Collab);
|
||||
@@ -250,8 +250,8 @@ fn multi_agent_v2_spawn_agent_description(tools_config: &ToolsConfig) -> String
|
||||
description.clone()
|
||||
}
|
||||
|
||||
fn model_info_from_models_json(slug: &str) -> ModelInfo {
|
||||
let config = test_config();
|
||||
async fn model_info_from_models_json(slug: &str) -> ModelInfo {
|
||||
let config = test_config().await;
|
||||
let response = bundled_models_response()
|
||||
.unwrap_or_else(|err| panic!("bundled models.json should parse: {err}"));
|
||||
let model = response
|
||||
@@ -295,9 +295,9 @@ fn build_specs_with_unavailable_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");
|
||||
#[tokio::test]
|
||||
async fn model_provided_unified_exec_is_blocked_for_windows_sandboxed_policies() {
|
||||
let mut model_info = model_info_from_models_json("gpt-5-codex").await;
|
||||
model_info.shell_type = ConfigShellToolType::UnifiedExec;
|
||||
let features = Features::with_defaults();
|
||||
let available_models = Vec::new();
|
||||
@@ -320,9 +320,9 @@ fn model_provided_unified_exec_is_blocked_for_windows_sandboxed_policies() {
|
||||
assert_eq!(config.shell_type, expected_shell_type);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_memory_requires_feature_flag() {
|
||||
let config = test_config();
|
||||
#[tokio::test]
|
||||
async fn get_memory_requires_feature_flag() {
|
||||
let config = test_config().await;
|
||||
let model_info = construct_model_info_offline("gpt-5-codex", &config);
|
||||
let mut features = Features::with_defaults();
|
||||
features.disable(Feature::MemoryTool);
|
||||
@@ -350,14 +350,14 @@ fn get_memory_requires_feature_flag() {
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_model_tools(
|
||||
async fn assert_model_tools(
|
||||
model_slug: &str,
|
||||
features: &Features,
|
||||
web_search_mode: Option<WebSearchMode>,
|
||||
expected_tools: &[&str],
|
||||
) {
|
||||
let _config = test_config();
|
||||
let model_info = model_info_from_models_json(model_slug);
|
||||
let _config = test_config().await;
|
||||
let model_info = model_info_from_models_json(model_slug).await;
|
||||
let available_models = Vec::new();
|
||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
@@ -388,7 +388,7 @@ fn assert_model_tools(
|
||||
assert_eq!(&tool_names, &expected_tools,);
|
||||
}
|
||||
|
||||
fn assert_default_model_tools(
|
||||
async fn assert_default_model_tools(
|
||||
model_slug: &str,
|
||||
features: &Features,
|
||||
web_search_mode: Option<WebSearchMode>,
|
||||
@@ -401,11 +401,11 @@ fn assert_default_model_tools(
|
||||
vec![shell_tool]
|
||||
};
|
||||
expected.extend(expected_tail);
|
||||
assert_model_tools(model_slug, features, web_search_mode, &expected);
|
||||
assert_model_tools(model_slug, features, web_search_mode, &expected).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_specs_gpt5_codex_default() {
|
||||
#[tokio::test]
|
||||
async fn test_build_specs_gpt5_codex_default() {
|
||||
let features = Features::with_defaults();
|
||||
assert_default_model_tools(
|
||||
"gpt-5-codex",
|
||||
@@ -424,11 +424,12 @@ fn test_build_specs_gpt5_codex_default() {
|
||||
"wait_agent",
|
||||
"close_agent",
|
||||
],
|
||||
);
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_specs_gpt51_codex_default() {
|
||||
#[tokio::test]
|
||||
async fn test_build_specs_gpt51_codex_default() {
|
||||
let features = Features::with_defaults();
|
||||
assert_default_model_tools(
|
||||
"gpt-5.1-codex",
|
||||
@@ -447,11 +448,12 @@ fn test_build_specs_gpt51_codex_default() {
|
||||
"wait_agent",
|
||||
"close_agent",
|
||||
],
|
||||
);
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_specs_gpt5_codex_unified_exec_web_search() {
|
||||
#[tokio::test]
|
||||
async fn test_build_specs_gpt5_codex_unified_exec_web_search() {
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::UnifiedExec);
|
||||
assert_model_tools(
|
||||
@@ -472,11 +474,12 @@ fn test_build_specs_gpt5_codex_unified_exec_web_search() {
|
||||
"wait_agent",
|
||||
"close_agent",
|
||||
],
|
||||
);
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_specs_gpt51_codex_unified_exec_web_search() {
|
||||
#[tokio::test]
|
||||
async fn test_build_specs_gpt51_codex_unified_exec_web_search() {
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::UnifiedExec);
|
||||
assert_model_tools(
|
||||
@@ -497,11 +500,12 @@ fn test_build_specs_gpt51_codex_unified_exec_web_search() {
|
||||
"wait_agent",
|
||||
"close_agent",
|
||||
],
|
||||
);
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gpt_5_1_codex_max_defaults() {
|
||||
#[tokio::test]
|
||||
async fn test_gpt_5_1_codex_max_defaults() {
|
||||
let features = Features::with_defaults();
|
||||
assert_default_model_tools(
|
||||
"gpt-5.1-codex-max",
|
||||
@@ -520,11 +524,12 @@ fn test_gpt_5_1_codex_max_defaults() {
|
||||
"wait_agent",
|
||||
"close_agent",
|
||||
],
|
||||
);
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_codex_5_1_mini_defaults() {
|
||||
#[tokio::test]
|
||||
async fn test_codex_5_1_mini_defaults() {
|
||||
let features = Features::with_defaults();
|
||||
assert_default_model_tools(
|
||||
"gpt-5.1-codex-mini",
|
||||
@@ -543,11 +548,12 @@ fn test_codex_5_1_mini_defaults() {
|
||||
"wait_agent",
|
||||
"close_agent",
|
||||
],
|
||||
);
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gpt_5_defaults() {
|
||||
#[tokio::test]
|
||||
async fn test_gpt_5_defaults() {
|
||||
let features = Features::with_defaults();
|
||||
assert_default_model_tools(
|
||||
"gpt-5",
|
||||
@@ -565,11 +571,12 @@ fn test_gpt_5_defaults() {
|
||||
"wait_agent",
|
||||
"close_agent",
|
||||
],
|
||||
);
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gpt_5_1_defaults() {
|
||||
#[tokio::test]
|
||||
async fn test_gpt_5_1_defaults() {
|
||||
let features = Features::with_defaults();
|
||||
assert_default_model_tools(
|
||||
"gpt-5.1",
|
||||
@@ -588,11 +595,12 @@ fn test_gpt_5_1_defaults() {
|
||||
"wait_agent",
|
||||
"close_agent",
|
||||
],
|
||||
);
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gpt_5_1_codex_max_unified_exec_web_search() {
|
||||
#[tokio::test]
|
||||
async fn test_gpt_5_1_codex_max_unified_exec_web_search() {
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::UnifiedExec);
|
||||
assert_model_tools(
|
||||
@@ -613,12 +621,13 @@ fn test_gpt_5_1_codex_max_unified_exec_web_search() {
|
||||
"wait_agent",
|
||||
"close_agent",
|
||||
],
|
||||
);
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_specs_default_shell_present() {
|
||||
let config = test_config();
|
||||
#[tokio::test]
|
||||
async fn test_build_specs_default_shell_present() {
|
||||
let config = test_config().await;
|
||||
let model_info = construct_model_info_offline("o3", &config);
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::UnifiedExec);
|
||||
@@ -649,9 +658,9 @@ fn test_build_specs_default_shell_present() {
|
||||
assert_contains_tool_names(&tools, &subset);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_zsh_fork_prefers_shell_command_over_unified_exec() {
|
||||
let config = test_config();
|
||||
#[tokio::test]
|
||||
async fn shell_zsh_fork_prefers_shell_command_over_unified_exec() {
|
||||
let config = test_config().await;
|
||||
let model_info = construct_model_info_offline("o3", &config);
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::UnifiedExec);
|
||||
@@ -713,9 +722,10 @@ fn shell_zsh_fork_prefers_shell_command_over_unified_exec() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_agent_description_omits_usage_hint_when_disabled() {
|
||||
#[tokio::test]
|
||||
async fn spawn_agent_description_omits_usage_hint_when_disabled() {
|
||||
let tools_config = multi_agent_v2_tools_config()
|
||||
.await
|
||||
.with_spawn_agent_usage_hint(/*spawn_agent_usage_hint*/ false);
|
||||
let description = multi_agent_v2_spawn_agent_description(&tools_config);
|
||||
|
||||
@@ -734,11 +744,13 @@ fn spawn_agent_description_omits_usage_hint_when_disabled() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_agent_description_uses_configured_usage_hint_text() {
|
||||
let tools_config = multi_agent_v2_tools_config().with_spawn_agent_usage_hint_text(Some(
|
||||
/*spawn_agent_usage_hint_text*/ "Custom delegation guidance only.".to_string(),
|
||||
));
|
||||
#[tokio::test]
|
||||
async fn spawn_agent_description_uses_configured_usage_hint_text() {
|
||||
let tools_config = multi_agent_v2_tools_config()
|
||||
.await
|
||||
.with_spawn_agent_usage_hint_text(Some(
|
||||
/*spawn_agent_usage_hint_text*/ "Custom delegation guidance only.".to_string(),
|
||||
));
|
||||
let description = multi_agent_v2_spawn_agent_description(&tools_config);
|
||||
|
||||
assert_regex_match(
|
||||
@@ -757,9 +769,9 @@ fn spawn_agent_description_uses_configured_usage_hint_text() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_suggest_requires_apps_and_plugins_features() {
|
||||
let model_info = search_capable_model_info();
|
||||
#[tokio::test]
|
||||
async fn tool_suggest_requires_apps_and_plugins_features() {
|
||||
let model_info = search_capable_model_info().await;
|
||||
let discoverable_tools = Some(vec![discoverable_connector(
|
||||
"connector_2128aebfecb84f64a069897515042a44",
|
||||
"Google Calendar",
|
||||
@@ -804,9 +816,9 @@ fn tool_suggest_requires_apps_and_plugins_features() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_tool_description_handles_no_enabled_mcp_tools() {
|
||||
let model_info = search_capable_model_info();
|
||||
#[tokio::test]
|
||||
async fn search_tool_description_handles_no_enabled_mcp_tools() {
|
||||
let model_info = search_capable_model_info().await;
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::Apps);
|
||||
features.enable(Feature::ToolSearch);
|
||||
@@ -838,9 +850,9 @@ fn search_tool_description_handles_no_enabled_mcp_tools() {
|
||||
assert!(!description.contains("{{source_descriptions}}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_tool_description_falls_back_to_connector_name_without_description() {
|
||||
let model_info = search_capable_model_info();
|
||||
#[tokio::test]
|
||||
async fn search_tool_description_falls_back_to_connector_name_without_description() {
|
||||
let model_info = search_capable_model_info().await;
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::Apps);
|
||||
features.enable(Feature::ToolSearch);
|
||||
@@ -889,9 +901,9 @@ fn search_tool_description_falls_back_to_connector_name_without_description() {
|
||||
assert!(!description.contains("- Calendar:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_tool_registers_namespaced_mcp_tool_aliases() {
|
||||
let model_info = search_capable_model_info();
|
||||
#[tokio::test]
|
||||
async fn search_tool_registers_namespaced_mcp_tool_aliases() {
|
||||
let model_info = search_capable_model_info().await;
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::Apps);
|
||||
features.enable(Feature::ToolSearch);
|
||||
@@ -974,9 +986,9 @@ fn search_tool_registers_namespaced_mcp_tool_aliases() {
|
||||
assert!(registry.has_handler(&mcp_alias));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_mcp_tools_register_namespaced_handlers() {
|
||||
let config = test_config();
|
||||
#[tokio::test]
|
||||
async fn direct_mcp_tools_register_namespaced_handlers() {
|
||||
let config = test_config().await;
|
||||
let model_info = construct_model_info_offline("gpt-5-codex", &config);
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::UnifiedExec);
|
||||
@@ -1011,9 +1023,9 @@ fn direct_mcp_tools_register_namespaced_handlers() {
|
||||
assert!(!registry.has_handler(&ToolName::plain("mcp__test_server__echo")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unavailable_mcp_tools_are_exposed_as_dummy_function_tools() {
|
||||
let config = test_config();
|
||||
#[tokio::test]
|
||||
async fn unavailable_mcp_tools_are_exposed_as_dummy_function_tools() {
|
||||
let config = test_config().await;
|
||||
let model_info = construct_model_info_offline("gpt-5-codex", &config);
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::UnifiedExec);
|
||||
@@ -1060,9 +1072,9 @@ fn unavailable_mcp_tools_are_exposed_as_dummy_function_tools() {
|
||||
assert!(!registry.has_handler(&ToolName::plain("mcp__codex_apps__calendar_create_event")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mcp_tool_property_missing_type_defaults_to_string() {
|
||||
let config = test_config();
|
||||
#[tokio::test]
|
||||
async fn test_mcp_tool_property_missing_type_defaults_to_string() {
|
||||
let config = test_config().await;
|
||||
let model_info = construct_model_info_offline("gpt-5-codex", &config);
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::UnifiedExec);
|
||||
@@ -1123,9 +1135,9 @@ fn test_mcp_tool_property_missing_type_defaults_to_string() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mcp_tool_preserves_integer_schema() {
|
||||
let config = test_config();
|
||||
#[tokio::test]
|
||||
async fn test_mcp_tool_preserves_integer_schema() {
|
||||
let config = test_config().await;
|
||||
let model_info = construct_model_info_offline("gpt-5-codex", &config);
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::UnifiedExec);
|
||||
@@ -1184,9 +1196,9 @@ fn test_mcp_tool_preserves_integer_schema() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mcp_tool_array_without_items_gets_default_string_items() {
|
||||
let config = test_config();
|
||||
#[tokio::test]
|
||||
async fn test_mcp_tool_array_without_items_gets_default_string_items() {
|
||||
let config = test_config().await;
|
||||
let model_info = construct_model_info_offline("gpt-5-codex", &config);
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::UnifiedExec);
|
||||
@@ -1249,9 +1261,9 @@ fn test_mcp_tool_array_without_items_gets_default_string_items() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mcp_tool_anyof_defaults_to_string() {
|
||||
let config = test_config();
|
||||
#[tokio::test]
|
||||
async fn test_mcp_tool_anyof_defaults_to_string() {
|
||||
let config = test_config().await;
|
||||
let model_info = construct_model_info_offline("gpt-5-codex", &config);
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::UnifiedExec);
|
||||
@@ -1318,9 +1330,9 @@ fn test_mcp_tool_anyof_defaults_to_string() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() {
|
||||
let config = test_config();
|
||||
#[tokio::test]
|
||||
async fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() {
|
||||
let config = test_config().await;
|
||||
let model_info = construct_model_info_offline("gpt-5-codex", &config);
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::UnifiedExec);
|
||||
@@ -1433,8 +1445,8 @@ fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_mode_only_restricts_model_tools_to_exec_tools() {
|
||||
#[tokio::test]
|
||||
async fn code_mode_only_restricts_model_tools_to_exec_tools() {
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::CodeMode);
|
||||
features.enable(Feature::CodeModeOnly);
|
||||
@@ -1444,5 +1456,6 @@ fn code_mode_only_restricts_model_tools_to_exec_tools() {
|
||||
&features,
|
||||
Some(WebSearchMode::Live),
|
||||
&["exec", "wait"],
|
||||
);
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ const SERVER_VERSION: &str = "1.0.0";
|
||||
const SEARCHABLE_TOOL_COUNT: usize = 100;
|
||||
pub const CALENDAR_CREATE_EVENT_RESOURCE_URI: &str =
|
||||
"connector://calendar/tools/calendar_create_event";
|
||||
pub const CALENDAR_CREATE_EVENT_MCP_APP_RESOURCE_URI: &str =
|
||||
"ui://widget/calendar-create-event.html";
|
||||
const CALENDAR_LIST_EVENTS_RESOURCE_URI: &str = "connector://calendar/tools/calendar_list_events";
|
||||
pub const DOCUMENT_EXTRACT_TEXT_RESOURCE_URI: &str =
|
||||
"connector://calendar/tools/calendar_extract_text";
|
||||
@@ -206,6 +208,7 @@ impl Respond for CodexAppsJsonRpcResponder {
|
||||
"connector_id": CONNECTOR_ID,
|
||||
"connector_name": self.connector_name.clone(),
|
||||
"connector_description": self.connector_description.clone(),
|
||||
"openai/outputTemplate": CALENDAR_CREATE_EVENT_MCP_APP_RESOURCE_URI,
|
||||
"_codex_apps": {
|
||||
"resource_uri": CALENDAR_CREATE_EVENT_RESOURCE_URI,
|
||||
"contains_mcp_source": true,
|
||||
|
||||
@@ -736,6 +736,7 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> {
|
||||
tool: "image".to_string(),
|
||||
arguments: Some(json!({})),
|
||||
},
|
||||
mcp_app_resource_uri: None,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::apps_test_server::AppsTestServer;
|
||||
use core_test_support::apps_test_server::CALENDAR_CREATE_EVENT_MCP_APP_RESOURCE_URI;
|
||||
use core_test_support::apps_test_server::CALENDAR_CREATE_EVENT_RESOURCE_URI;
|
||||
use core_test_support::responses::ResponsesRequest;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
@@ -93,7 +94,7 @@ fn tool_search_output_tools(request: &ResponsesRequest, call_id: &str) -> Vec<Va
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn configure_apps_without_tool_search(config: &mut Config, apps_base_url: &str) {
|
||||
fn configure_search_capable_apps(config: &mut Config, apps_base_url: &str) {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::Apps)
|
||||
@@ -112,14 +113,18 @@ fn configure_apps_without_tool_search(config: &mut Config, apps_base_url: &str)
|
||||
config.model_catalog = Some(model_catalog);
|
||||
}
|
||||
|
||||
fn configure_apps(config: &mut Config, apps_base_url: &str) {
|
||||
configure_apps_without_tool_search(config, apps_base_url);
|
||||
fn configure_apps_without_tool_search(config: &mut Config, apps_base_url: &str) {
|
||||
configure_search_capable_apps(config, apps_base_url);
|
||||
config
|
||||
.features
|
||||
.enable(Feature::ToolSearch)
|
||||
.disable(Feature::ToolSearch)
|
||||
.expect("test config should allow feature update");
|
||||
}
|
||||
|
||||
fn configure_apps(config: &mut Config, apps_base_url: &str) {
|
||||
configure_search_capable_apps(config, apps_base_url);
|
||||
}
|
||||
|
||||
fn configured_builder(apps_base_url: String) -> TestCodexBuilder {
|
||||
test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
@@ -127,7 +132,7 @@ fn configured_builder(apps_base_url: String) -> TestCodexBuilder {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn search_tool_flag_adds_tool_search() -> Result<()> {
|
||||
async fn search_tool_enabled_by_default_adds_tool_search() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
@@ -185,7 +190,7 @@ async fn search_tool_flag_adds_tool_search() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn tool_search_disabled_by_default_exposes_apps_tools_directly() -> Result<()> {
|
||||
async fn tool_search_disabled_exposes_apps_tools_directly() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
@@ -453,6 +458,19 @@ async fn tool_search_returns_deferred_tools_without_follow_up_tool_injection() -
|
||||
})
|
||||
.await?;
|
||||
|
||||
let EventMsg::McpToolCallBegin(begin) = wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::McpToolCallBegin(_))
|
||||
})
|
||||
.await
|
||||
else {
|
||||
unreachable!("event guard guarantees McpToolCallBegin");
|
||||
};
|
||||
assert_eq!(begin.call_id, "calendar-call-1");
|
||||
assert_eq!(
|
||||
begin.mcp_app_resource_uri.as_deref(),
|
||||
Some(CALENDAR_CREATE_EVENT_MCP_APP_RESOURCE_URI)
|
||||
);
|
||||
|
||||
let EventMsg::McpToolCallEnd(end) = wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::McpToolCallEnd(_))
|
||||
})
|
||||
@@ -461,6 +479,10 @@ async fn tool_search_returns_deferred_tools_without_follow_up_tool_injection() -
|
||||
unreachable!("event guard guarantees McpToolCallEnd");
|
||||
};
|
||||
assert_eq!(end.call_id, "calendar-call-1");
|
||||
assert_eq!(
|
||||
end.mcp_app_resource_uri.as_deref(),
|
||||
Some(CALENDAR_CREATE_EVENT_MCP_APP_RESOURCE_URI)
|
||||
);
|
||||
assert_eq!(
|
||||
end.invocation,
|
||||
McpInvocation {
|
||||
|
||||
@@ -464,6 +464,7 @@ fn mcp_tool_call_begin_and_end_emit_item_events() {
|
||||
tool: "tool_x".to_string(),
|
||||
status: ApiMcpToolCallStatus::InProgress,
|
||||
arguments: json!({ "key": "value" }),
|
||||
mcp_app_resource_uri: None,
|
||||
result: None,
|
||||
error: None,
|
||||
duration_ms: None,
|
||||
@@ -479,11 +480,12 @@ fn mcp_tool_call_begin_and_end_emit_item_events() {
|
||||
tool: "tool_x".to_string(),
|
||||
status: ApiMcpToolCallStatus::Completed,
|
||||
arguments: json!({ "key": "value" }),
|
||||
result: Some(McpToolCallResult {
|
||||
mcp_app_resource_uri: None,
|
||||
result: Some(Box::new(McpToolCallResult {
|
||||
content: Vec::new(),
|
||||
structured_content: None,
|
||||
meta: None,
|
||||
}),
|
||||
})),
|
||||
error: None,
|
||||
duration_ms: Some(1_000),
|
||||
},
|
||||
@@ -547,6 +549,7 @@ fn mcp_tool_call_failure_sets_failed_status() {
|
||||
tool: "tool_y".to_string(),
|
||||
status: ApiMcpToolCallStatus::Failed,
|
||||
arguments: json!({ "param": 42 }),
|
||||
mcp_app_resource_uri: None,
|
||||
result: None,
|
||||
error: Some(McpToolCallError {
|
||||
message: "tool exploded".to_string(),
|
||||
@@ -593,6 +596,7 @@ fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() {
|
||||
tool: "tool_z".to_string(),
|
||||
status: ApiMcpToolCallStatus::InProgress,
|
||||
arguments: serde_json::Value::Null,
|
||||
mcp_app_resource_uri: None,
|
||||
result: None,
|
||||
error: None,
|
||||
duration_ms: None,
|
||||
@@ -608,14 +612,15 @@ fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() {
|
||||
tool: "tool_z".to_string(),
|
||||
status: ApiMcpToolCallStatus::Completed,
|
||||
arguments: serde_json::Value::Null,
|
||||
result: Some(McpToolCallResult {
|
||||
mcp_app_resource_uri: None,
|
||||
result: Some(Box::new(McpToolCallResult {
|
||||
content: vec![json!({
|
||||
"type": "text",
|
||||
"text": "done",
|
||||
})],
|
||||
structured_content: Some(json!({ "status": "ok" })),
|
||||
meta: None,
|
||||
}),
|
||||
})),
|
||||
error: None,
|
||||
duration_ms: Some(10),
|
||||
},
|
||||
|
||||
@@ -788,8 +788,8 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
FeatureSpec {
|
||||
id: Feature::ToolSearch,
|
||||
key: "tool_search",
|
||||
stage: Stage::UnderDevelopment,
|
||||
default_enabled: false,
|
||||
stage: Stage::Stable,
|
||||
default_enabled: true,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::UnavailableDummyTools,
|
||||
|
||||
@@ -128,9 +128,9 @@ fn tool_suggest_is_stable_and_enabled_by_default() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_search_is_under_development_and_disabled_by_default() {
|
||||
assert_eq!(Feature::ToolSearch.stage(), Stage::UnderDevelopment);
|
||||
assert_eq!(Feature::ToolSearch.default_enabled(), false);
|
||||
fn tool_search_is_stable_and_enabled_by_default() {
|
||||
assert_eq!(Feature::ToolSearch.stage(), Stage::Stable);
|
||||
assert_eq!(Feature::ToolSearch.default_enabled(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -618,7 +618,7 @@ async fn diff_against_sha(cwd: &Path, sha: &GitSha) -> Option<String> {
|
||||
/// `[get_git_repo_root]`, but resolves to the root of the main
|
||||
/// repository. Handles worktrees via filesystem inspection without invoking
|
||||
/// the `git` executable.
|
||||
pub fn resolve_root_git_project_for_trust(cwd: &Path) -> Option<PathBuf> {
|
||||
pub async fn resolve_root_git_project_for_trust(cwd: &Path) -> Option<PathBuf> {
|
||||
let base = if cwd.is_dir() { cwd } else { cwd.parent()? };
|
||||
let (repo_root, dot_git) = find_ancestor_git_entry(base)?;
|
||||
if dot_git.is_dir() {
|
||||
|
||||
@@ -2362,6 +2362,9 @@ pub struct McpToolCallBeginEvent {
|
||||
/// Identifier so this can be paired with the McpToolCallEnd event.
|
||||
pub call_id: String,
|
||||
pub invocation: McpInvocation,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub mcp_app_resource_uri: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]
|
||||
@@ -2369,6 +2372,9 @@ pub struct McpToolCallEndEvent {
|
||||
/// Identifier for the corresponding McpToolCallBegin that finished.
|
||||
pub call_id: String,
|
||||
pub invocation: McpInvocation,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub mcp_app_resource_uri: Option<String>,
|
||||
#[ts(type = "string")]
|
||||
pub duration: Duration,
|
||||
/// Result of the tool call. Note this could be an error.
|
||||
|
||||
@@ -1334,7 +1334,7 @@ fn search_tool_description_lists_each_mcp_source_once() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_tool_requires_model_capability_and_feature_flag() {
|
||||
fn search_tool_requires_model_capability_and_enabled_feature() {
|
||||
let model_info = search_capable_model_info();
|
||||
let deferred_mcp_tools = Some(vec![deferred_mcp_tool(
|
||||
"_create_event",
|
||||
@@ -1367,10 +1367,12 @@ fn search_tool_requires_model_capability_and_feature_flag() {
|
||||
);
|
||||
assert_lacks_tool_name(&tools, TOOL_SEARCH_TOOL_NAME);
|
||||
|
||||
let mut features_without_tool_search = Features::with_defaults();
|
||||
features_without_tool_search.disable(Feature::ToolSearch);
|
||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
available_models: &available_models,
|
||||
features: &features,
|
||||
features: &features_without_tool_search,
|
||||
image_generation_tool_auth_allowed: true,
|
||||
web_search_mode: Some(WebSearchMode::Cached),
|
||||
session_source: SessionSource::Cli,
|
||||
@@ -1385,8 +1387,6 @@ fn search_tool_requires_model_capability_and_feature_flag() {
|
||||
);
|
||||
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,
|
||||
|
||||
@@ -1599,6 +1599,7 @@ impl App {
|
||||
self.config.cwd.to_path_buf(),
|
||||
version,
|
||||
)
|
||||
.with_yolo_mode(history_cell::is_yolo_mode(&self.config))
|
||||
.display_lines(width)
|
||||
}
|
||||
|
||||
|
||||
@@ -4759,6 +4759,7 @@ impl ChatWidget {
|
||||
invocation,
|
||||
duration,
|
||||
result,
|
||||
..
|
||||
} = ev;
|
||||
|
||||
let extra_cell = match self
|
||||
@@ -5943,6 +5944,7 @@ impl ChatWidget {
|
||||
server,
|
||||
tool,
|
||||
arguments,
|
||||
mcp_app_resource_uri,
|
||||
result,
|
||||
error,
|
||||
duration_ms,
|
||||
@@ -5955,15 +5957,19 @@ impl ChatWidget {
|
||||
tool,
|
||||
arguments: Some(arguments),
|
||||
},
|
||||
mcp_app_resource_uri,
|
||||
duration: Duration::from_millis(duration_ms.unwrap_or_default().max(0) as u64),
|
||||
result: match (result, error) {
|
||||
(_, Some(error)) => Err(error.message),
|
||||
(Some(result), None) => Ok(codex_protocol::mcp::CallToolResult {
|
||||
content: result.content,
|
||||
structured_content: result.structured_content,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
(Some(result), None) => {
|
||||
let result = *result;
|
||||
Ok(codex_protocol::mcp::CallToolResult {
|
||||
content: result.content,
|
||||
structured_content: result.structured_content,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
(None, None) => Err("MCP tool call completed without a result".to_string()),
|
||||
},
|
||||
});
|
||||
@@ -6453,6 +6459,7 @@ impl ChatWidget {
|
||||
server,
|
||||
tool,
|
||||
arguments,
|
||||
mcp_app_resource_uri,
|
||||
..
|
||||
} => {
|
||||
self.on_mcp_tool_call_begin(McpToolCallBeginEvent {
|
||||
@@ -6462,6 +6469,7 @@ impl ChatWidget {
|
||||
tool,
|
||||
arguments: Some(arguments),
|
||||
},
|
||||
mcp_app_resource_uri,
|
||||
});
|
||||
}
|
||||
ThreadItem::WebSearch { id, .. } => {
|
||||
@@ -9722,14 +9730,17 @@ impl ChatWidget {
|
||||
/// Build a placeholder header cell while the session is configuring.
|
||||
fn placeholder_session_header_cell(config: &Config) -> Box<dyn HistoryCell> {
|
||||
let placeholder_style = Style::default().add_modifier(Modifier::DIM | Modifier::ITALIC);
|
||||
Box::new(history_cell::SessionHeaderHistoryCell::new_with_style(
|
||||
DEFAULT_MODEL_DISPLAY_NAME.to_string(),
|
||||
placeholder_style,
|
||||
/*reasoning_effort*/ None,
|
||||
/*show_fast_status*/ false,
|
||||
config.cwd.to_path_buf(),
|
||||
CODEX_CLI_VERSION,
|
||||
))
|
||||
Box::new(
|
||||
history_cell::SessionHeaderHistoryCell::new_with_style(
|
||||
DEFAULT_MODEL_DISPLAY_NAME.to_string(),
|
||||
placeholder_style,
|
||||
/*reasoning_effort*/ None,
|
||||
/*show_fast_status*/ false,
|
||||
config.cwd.to_path_buf(),
|
||||
CODEX_CLI_VERSION,
|
||||
)
|
||||
.with_yolo_mode(history_cell::is_yolo_mode(config)),
|
||||
)
|
||||
}
|
||||
|
||||
/// Merge the real session info cell with any placeholder header to avoid double boxes.
|
||||
|
||||
@@ -10,6 +10,7 @@ pub(super) async fn test_config() -> Config {
|
||||
.keep();
|
||||
let mut config =
|
||||
Config::load_default_with_cli_overrides_for_codex_home(codex_home.clone(), Vec::new())
|
||||
.await
|
||||
.expect("config");
|
||||
config.codex_home = codex_home.abs();
|
||||
config.sqlite_home = codex_home.clone();
|
||||
|
||||
@@ -61,9 +61,11 @@ use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
use codex_protocol::plan_tool::PlanItemArg;
|
||||
use codex_protocol::plan_tool::StepStatus;
|
||||
use codex_protocol::plan_tool::UpdatePlanArgs;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::FileChange;
|
||||
use codex_protocol::protocol::McpAuthStatus;
|
||||
use codex_protocol::protocol::McpInvocation;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::SessionConfiguredEvent;
|
||||
use codex_protocol::request_user_input::RequestUserInputAnswer;
|
||||
use codex_protocol::request_user_input::RequestUserInputQuestion;
|
||||
@@ -1186,6 +1188,8 @@ pub(crate) fn new_session_info(
|
||||
let SessionConfiguredEvent {
|
||||
model,
|
||||
reasoning_effort,
|
||||
approval_policy,
|
||||
sandbox_policy,
|
||||
..
|
||||
} = event;
|
||||
// Header box rendered as history (so it appears at the very top)
|
||||
@@ -1195,7 +1199,8 @@ pub(crate) fn new_session_info(
|
||||
show_fast_status,
|
||||
config.cwd.to_path_buf(),
|
||||
CODEX_CLI_VERSION,
|
||||
);
|
||||
)
|
||||
.with_yolo_mode(has_yolo_permissions(approval_policy, &sandbox_policy));
|
||||
let mut parts: Vec<Box<dyn HistoryCell>> = vec![Box::new(header)];
|
||||
|
||||
if is_first_event {
|
||||
@@ -1259,6 +1264,17 @@ pub(crate) fn new_session_info(
|
||||
SessionInfoCell(CompositeHistoryCell { parts })
|
||||
}
|
||||
|
||||
pub(crate) fn is_yolo_mode(config: &Config) -> bool {
|
||||
has_yolo_permissions(
|
||||
config.permissions.approval_policy.value(),
|
||||
config.permissions.sandbox_policy.get(),
|
||||
)
|
||||
}
|
||||
|
||||
fn has_yolo_permissions(approval_policy: AskForApproval, sandbox_policy: &SandboxPolicy) -> bool {
|
||||
approval_policy == AskForApproval::Never && *sandbox_policy == SandboxPolicy::DangerFullAccess
|
||||
}
|
||||
|
||||
pub(crate) fn new_user_prompt(
|
||||
message: String,
|
||||
text_elements: Vec<TextElement>,
|
||||
@@ -1281,6 +1297,7 @@ pub(crate) struct SessionHeaderHistoryCell {
|
||||
reasoning_effort: Option<ReasoningEffortConfig>,
|
||||
show_fast_status: bool,
|
||||
directory: PathBuf,
|
||||
yolo_mode: bool,
|
||||
}
|
||||
|
||||
impl SessionHeaderHistoryCell {
|
||||
@@ -1316,9 +1333,15 @@ impl SessionHeaderHistoryCell {
|
||||
reasoning_effort,
|
||||
show_fast_status,
|
||||
directory,
|
||||
yolo_mode: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_yolo_mode(mut self, yolo_mode: bool) -> Self {
|
||||
self.yolo_mode = yolo_mode;
|
||||
self
|
||||
}
|
||||
|
||||
fn format_directory(&self, max_width: Option<usize>) -> String {
|
||||
Self::format_directory_inner(&self.directory, max_width)
|
||||
}
|
||||
@@ -1377,7 +1400,12 @@ impl HistoryCell for SessionHeaderHistoryCell {
|
||||
const CHANGE_MODEL_HINT_COMMAND: &str = "/model";
|
||||
const CHANGE_MODEL_HINT_EXPLANATION: &str = " to change";
|
||||
const DIR_LABEL: &str = "directory:";
|
||||
let label_width = DIR_LABEL.len();
|
||||
const PERMISSIONS_LABEL: &str = "permissions:";
|
||||
let label_width = if self.yolo_mode {
|
||||
DIR_LABEL.len().max(PERMISSIONS_LABEL.len())
|
||||
} else {
|
||||
DIR_LABEL.len()
|
||||
};
|
||||
|
||||
let model_label = format!(
|
||||
"{model_label:<label_width$}",
|
||||
@@ -1411,13 +1439,21 @@ impl HistoryCell for SessionHeaderHistoryCell {
|
||||
let dir = self.format_directory(Some(dir_max_width));
|
||||
let dir_spans = vec![Span::from(dir_prefix).dim(), Span::from(dir)];
|
||||
|
||||
let lines = vec![
|
||||
let mut lines = vec![
|
||||
make_row(title_spans),
|
||||
make_row(Vec::new()),
|
||||
make_row(model_spans),
|
||||
make_row(dir_spans),
|
||||
];
|
||||
|
||||
if self.yolo_mode {
|
||||
let permissions_label = format!("{PERMISSIONS_LABEL:<label_width$}");
|
||||
lines.push(make_row(vec![
|
||||
Span::from(format!("{permissions_label} ")).dim(),
|
||||
"YOLO mode".magenta().bold(),
|
||||
]));
|
||||
}
|
||||
|
||||
with_border(lines)
|
||||
}
|
||||
}
|
||||
@@ -3956,6 +3992,25 @@ mod tests {
|
||||
assert!(!model_line.contains("fast"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
target_os = "windows",
|
||||
ignore = "snapshot path rendering differs on Windows"
|
||||
)]
|
||||
fn session_header_indicates_yolo_mode() {
|
||||
let cell = SessionHeaderHistoryCell::new(
|
||||
"gpt-5".to_string(),
|
||||
/*reasoning_effort*/ None,
|
||||
/*show_fast_status*/ false,
|
||||
test_path_buf("/tmp/project").abs().to_path_buf(),
|
||||
"test",
|
||||
)
|
||||
.with_yolo_mode(/*yolo_mode*/ true);
|
||||
|
||||
let rendered = render_lines(&cell.display_lines(/*width*/ 80)).join("\n");
|
||||
insta::assert_snapshot!(rendered);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_header_directory_center_truncates() {
|
||||
let mut dir = home_dir().expect("home directory");
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::legacy_core::windows_sandbox::WindowsSandboxLevelExt;
|
||||
use codex_app_server_client::AppServerEvent;
|
||||
use codex_app_server_client::AppServerRequestHandle;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_git_utils::resolve_root_git_project_for_trust;
|
||||
#[cfg(target_os = "windows")]
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use crossterm::event::KeyCode;
|
||||
@@ -77,7 +78,7 @@ pub(crate) struct OnboardingResult {
|
||||
}
|
||||
|
||||
impl OnboardingScreen {
|
||||
pub(crate) fn new(tui: &mut Tui, args: OnboardingScreenArgs) -> Self {
|
||||
pub(crate) async fn new(tui: &mut Tui, args: OnboardingScreenArgs) -> Self {
|
||||
let OnboardingScreenArgs {
|
||||
show_trust_screen,
|
||||
show_login_screen,
|
||||
@@ -122,8 +123,12 @@ impl OnboardingScreen {
|
||||
let show_windows_create_sandbox_hint = false;
|
||||
let highlighted = TrustDirectorySelection::Trust;
|
||||
if show_trust_screen {
|
||||
let trust_target = resolve_root_git_project_for_trust(&cwd)
|
||||
.await
|
||||
.unwrap_or_else(|| cwd.clone());
|
||||
steps.push(Step::TrustDirectory(TrustDirectoryWidget {
|
||||
cwd,
|
||||
trust_target,
|
||||
codex_home,
|
||||
show_windows_create_sandbox_hint,
|
||||
should_quit: false,
|
||||
@@ -452,7 +457,7 @@ pub(crate) async fn run_onboarding_app(
|
||||
) -> Result<OnboardingResult> {
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
let mut onboarding_screen = OnboardingScreen::new(tui, args);
|
||||
let mut onboarding_screen = OnboardingScreen::new(tui, args).await;
|
||||
// One-time guard to fully clear the screen after ChatGPT login success message is shown
|
||||
let mut did_full_clear_after_success = false;
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::legacy_core::config::set_project_trust_level;
|
||||
use codex_git_utils::resolve_root_git_project_for_trust;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
@@ -27,6 +26,7 @@ use super::onboarding_screen::StepState;
|
||||
pub(crate) struct TrustDirectoryWidget {
|
||||
pub codex_home: PathBuf,
|
||||
pub cwd: PathBuf,
|
||||
pub trust_target: PathBuf,
|
||||
pub show_windows_create_sandbox_hint: bool,
|
||||
pub should_quit: bool,
|
||||
pub selection: Option<TrustDirectorySelection>,
|
||||
@@ -142,8 +142,7 @@ impl StepStateProvider for TrustDirectoryWidget {
|
||||
|
||||
impl TrustDirectoryWidget {
|
||||
fn handle_trust(&mut self) {
|
||||
let target =
|
||||
resolve_root_git_project_for_trust(&self.cwd).unwrap_or_else(|| self.cwd.clone());
|
||||
let target = self.trust_target.clone();
|
||||
if let Err(e) = set_project_trust_level(&self.codex_home, &target, TrustLevel::Trusted) {
|
||||
tracing::error!("Failed to set project trusted: {e:?}");
|
||||
self.error = Some(format!("Failed to set trust for {}: {e}", target.display()));
|
||||
@@ -182,6 +181,7 @@ mod tests {
|
||||
let mut widget = TrustDirectoryWidget {
|
||||
codex_home: codex_home.path().to_path_buf(),
|
||||
cwd: PathBuf::from("."),
|
||||
trust_target: PathBuf::from("."),
|
||||
show_windows_create_sandbox_hint: false,
|
||||
should_quit: false,
|
||||
selection: None,
|
||||
@@ -207,6 +207,7 @@ mod tests {
|
||||
let widget = TrustDirectoryWidget {
|
||||
codex_home: codex_home.path().to_path_buf(),
|
||||
cwd: PathBuf::from("/workspace/project"),
|
||||
trust_target: PathBuf::from("/workspace/project"),
|
||||
show_windows_create_sandbox_hint: false,
|
||||
should_quit: false,
|
||||
selection: None,
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
---
|
||||
source: tui/src/history_cell.rs
|
||||
expression: rendered
|
||||
---
|
||||
╭───────────────────────────────────────╮
|
||||
│ >_ OpenAI Codex (vtest) │
|
||||
│ │
|
||||
│ model: gpt-5 /model to change │
|
||||
│ directory: /tmp/project │
|
||||
│ permissions: YOLO mode │
|
||||
╰───────────────────────────────────────╯
|
||||
Reference in New Issue
Block a user