Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions codex-rs/core/src/thread_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,15 @@ impl ThreadManager {
.await
}

pub async fn resume_thread_from_rollout_with_auth(
&self,
config: Config,
rollout_path: PathBuf,
) -> CodexResult<NewThread> {
self.resume_thread_from_rollout(config, rollout_path, Arc::clone(&self.state.auth_manager))
.await
}

pub async fn resume_thread_with_history(
&self,
config: Config,
Expand Down
36 changes: 34 additions & 2 deletions codex-rs/mcp-server/src/codex_tool_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ pub struct CodexToolCallParam {
/// Prompt used when compacting the conversation.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub compact_prompt: Option<String>,

/// Optional path to a rollout file to resume the session from.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resume_path: Option<PathBuf>,
}

/// Custom enum mirroring [`AskForApproval`], but has an extra dependency on
Expand Down Expand Up @@ -153,7 +157,7 @@ impl CodexToolCallParam {
pub async fn into_config(
self,
codex_linux_sandbox_exe: Option<PathBuf>,
) -> std::io::Result<(String, Config)> {
) -> std::io::Result<(String, Config, Option<PathBuf>)> {
let Self {
prompt,
model,
Expand All @@ -165,6 +169,7 @@ impl CodexToolCallParam {
base_instructions,
developer_instructions,
compact_prompt,
resume_path,
} = self;

// Build the `ConfigOverrides` recognized by codex-core.
Expand All @@ -190,7 +195,7 @@ impl CodexToolCallParam {
let cfg =
Config::load_with_cli_overrides_and_harness_overrides(cli_overrides, overrides).await?;

Ok((prompt, cfg))
Ok((prompt, cfg, resume_path))
}
}

Expand Down Expand Up @@ -325,6 +330,10 @@ mod tests {
"description": "The *initial user prompt* to start the Codex conversation.",
"type": "string"
},
"resume-path": {
"description": "Optional path to a rollout file to resume the session from.",
"type": "string"
},
"sandbox": {
"description": "Sandbox mode: `read-only`, `workspace-write`, or `danger-full-access`.",
"enum": [
Expand Down Expand Up @@ -407,4 +416,27 @@ mod tests {
});
assert_eq!(expected_tool_json, tool_json);
}

#[test]
fn codex_tool_accepts_pre_change_payload() {
let payload = serde_json::json!({
"prompt": "hello",
"model": "gpt-5.2",
"sandbox": "workspace-write",
"approval-policy": "untrusted",
});

let parsed: CodexToolCallParam = serde_json::from_value(payload).expect("payload parses");
assert_eq!(parsed.prompt, "hello");
assert_eq!(parsed.model.as_deref(), Some("gpt-5.2"));
assert_eq!(
parsed.sandbox,
Some(CodexToolCallSandboxMode::WorkspaceWrite)
);
assert_eq!(
parsed.approval_policy,
Some(CodexToolCallApprovalPolicy::Untrusted)
);
assert!(parsed.resume_path.is_none());
}
}
53 changes: 38 additions & 15 deletions codex-rs/mcp-server/src/codex_tool_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//! and to make future feature-growth easier to manage.

use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;

use crate::exec_approval::handle_exec_approval_request;
Expand Down Expand Up @@ -66,6 +67,7 @@ pub async fn run_codex_tool_session(
id: RequestId,
initial_prompt: String,
config: CodexConfig,
resume_path: Option<PathBuf>,
outgoing: Arc<OutgoingMessageSender>,
thread_manager: Arc<ThreadManager>,
running_requests_id_to_codex_uuid: Arc<Mutex<HashMap<RequestId, ThreadId>>>,
Expand All @@ -74,21 +76,42 @@ pub async fn run_codex_tool_session(
thread_id,
thread,
session_configured,
} = match thread_manager.start_thread(config).await {
Ok(res) => res,
Err(e) => {
let result = CallToolResult {
content: vec![ContentBlock::TextContent(TextContent {
r#type: "text".to_string(),
text: format!("Failed to start Codex session: {e}"),
annotations: None,
})],
is_error: Some(true),
structured_content: None,
};
outgoing.send_response(id.clone(), result).await;
return;
}
} = match resume_path {
Some(path) => match thread_manager
.resume_thread_from_rollout_with_auth(config, path)
.await
{
Ok(res) => res,
Err(e) => {
let result = CallToolResult {
content: vec![ContentBlock::TextContent(TextContent {
r#type: "text".to_string(),
text: format!("Failed to resume Codex session: {e}"),
annotations: None,
})],
is_error: Some(true),
structured_content: None,
};
outgoing.send_response(id.clone(), result).await;
return;
}
},
None => match thread_manager.start_thread(config).await {
Ok(res) => res,
Err(e) => {
let result = CallToolResult {
content: vec![ContentBlock::TextContent(TextContent {
r#type: "text".to_string(),
text: format!("Failed to start Codex session: {e}"),
annotations: None,
})],
is_error: Some(true),
structured_content: None,
};
outgoing.send_response(id.clone(), result).await;
return;
}
},
};

let session_configured_event = Event {
Expand Down
68 changes: 35 additions & 33 deletions codex-rs/mcp-server/src/message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,20 +347,36 @@ impl MessageProcessor {
}
}
async fn handle_tool_call_codex(&self, id: RequestId, arguments: Option<serde_json::Value>) {
let (initial_prompt, config): (String, Config) = match arguments {
Some(json_val) => match serde_json::from_value::<CodexToolCallParam>(json_val) {
Ok(tool_cfg) => match tool_cfg
.into_config(self.codex_linux_sandbox_exe.clone())
.await
{
Ok(cfg) => cfg,
let (initial_prompt, config, resume_path): (String, Config, Option<PathBuf>) =
match arguments {
Some(json_val) => match serde_json::from_value::<CodexToolCallParam>(json_val) {
Ok(tool_cfg) => match tool_cfg
.into_config(self.codex_linux_sandbox_exe.clone())
.await
{
Ok(cfg) => cfg,
Err(e) => {
let result = CallToolResult {
content: vec![ContentBlock::TextContent(TextContent {
r#type: "text".to_owned(),
text: format!(
"Failed to load Codex configuration from overrides: {e}"
),
annotations: None,
})],
is_error: Some(true),
structured_content: None,
};
self.send_response::<mcp_types::CallToolRequest>(id, result)
.await;
return;
}
},
Err(e) => {
let result = CallToolResult {
content: vec![ContentBlock::TextContent(TextContent {
r#type: "text".to_owned(),
text: format!(
"Failed to load Codex configuration from overrides: {e}"
),
text: format!("Failed to parse configuration for Codex tool: {e}"),
annotations: None,
})],
is_error: Some(true),
Expand All @@ -371,38 +387,23 @@ impl MessageProcessor {
return;
}
},
Err(e) => {
None => {
let result = CallToolResult {
content: vec![ContentBlock::TextContent(TextContent {
r#type: "text".to_owned(),
text: format!("Failed to parse configuration for Codex tool: {e}"),
annotations: None,
})],
r#type: "text".to_string(),
text:
"Missing arguments for codex tool-call; the `prompt` field is required."
.to_string(),
annotations: None,
})],
is_error: Some(true),
structured_content: None,
};
self.send_response::<mcp_types::CallToolRequest>(id, result)
.await;
return;
}
},
None => {
let result = CallToolResult {
content: vec![ContentBlock::TextContent(TextContent {
r#type: "text".to_string(),
text:
"Missing arguments for codex tool-call; the `prompt` field is required."
.to_string(),
annotations: None,
})],
is_error: Some(true),
structured_content: None,
};
self.send_response::<mcp_types::CallToolRequest>(id, result)
.await;
return;
}
};
};

// Clone outgoing and server to move into async task.
let outgoing = self.outgoing.clone();
Expand All @@ -417,6 +418,7 @@ impl MessageProcessor {
id,
initial_prompt,
config,
resume_path,
outgoing,
thread_manager,
running_requests_id_to_codex_uuid,
Expand Down
Loading