use std::collections::HashMap; use std::fmt::Display; use std::path::PathBuf; use codex_core::protocol::FileChange; use codex_core::protocol::ReviewDecision; use mcp_types::RequestId; use serde::Deserialize; use serde::Serialize; use crate::codex_tool_config::CodexToolCallApprovalPolicy; use crate::codex_tool_config::CodexToolCallSandboxMode; use uuid::Uuid; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(transparent)] pub struct ConversationId(pub Uuid); impl Display for ConversationId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } /// Request from the client to the server. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(tag = "method", rename_all = "camelCase")] pub enum ClientRequest { NewConversation { #[serde(rename = "id")] request_id: RequestId, params: NewConversationParams, }, SendUserMessage { #[serde(rename = "id")] request_id: RequestId, params: SendUserMessageParams, }, InterruptConversation { #[serde(rename = "id")] request_id: RequestId, params: InterruptConversationParams, }, AddConversationListener { #[serde(rename = "id")] request_id: RequestId, params: AddConversationListenerParams, }, RemoveConversationListener { #[serde(rename = "id")] request_id: RequestId, params: RemoveConversationListenerParams, }, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] pub struct NewConversationParams { /// Optional override for the model name (e.g. "o3", "o4-mini"). #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, /// Configuration profile from config.toml to specify default options. #[serde(skip_serializing_if = "Option::is_none")] pub profile: Option, /// Working directory for the session. If relative, it is resolved against /// the server process's current working directory. #[serde(skip_serializing_if = "Option::is_none")] pub cwd: Option, /// Approval policy for shell commands generated by the model: /// `untrusted`, `on-failure`, `on-request`, `never`. #[serde(skip_serializing_if = "Option::is_none")] pub approval_policy: Option, /// Sandbox mode: `read-only`, `workspace-write`, or `danger-full-access`. #[serde(skip_serializing_if = "Option::is_none")] pub sandbox: Option, /// Individual config settings that will override what is in /// CODEX_HOME/config.toml. #[serde(skip_serializing_if = "Option::is_none")] pub config: Option>, /// The set of instructions to use instead of the default ones. #[serde(skip_serializing_if = "Option::is_none")] pub base_instructions: Option, /// Whether to include the plan tool in the conversation. #[serde(skip_serializing_if = "Option::is_none")] pub include_plan_tool: Option, /// Whether to include the apply patch tool in the conversation. #[serde(skip_serializing_if = "Option::is_none")] pub include_apply_patch_tool: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct NewConversationResponse { pub conversation_id: ConversationId, pub model: String, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct AddConversationSubscriptionResponse { pub subscription_id: Uuid, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct RemoveConversationSubscriptionResponse {} #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct SendUserMessageParams { pub conversation_id: ConversationId, pub items: Vec, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct InterruptConversationParams { pub conversation_id: ConversationId, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct InterruptConversationResponse {} #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct SendUserMessageResponse {} #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct AddConversationListenerParams { pub conversation_id: ConversationId, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct RemoveConversationListenerParams { pub subscription_id: Uuid, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(rename_all = "camelCase")] #[serde(tag = "type", content = "data")] pub enum InputItem { Text { text: String, }, /// Pre‑encoded data: URI image. Image { image_url: String, }, /// Local image path provided by the user. This will be converted to an /// `Image` variant (base64 data URL) during request serialization. LocalImage { path: PathBuf, }, } // TODO(mbolin): Need test to ensure these constants match the enum variants. pub const APPLY_PATCH_APPROVAL_METHOD: &str = "applyPatchApproval"; pub const EXEC_COMMAND_APPROVAL_METHOD: &str = "execCommandApproval"; /// Request initiated from the server and sent to the client. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(tag = "method", rename_all = "camelCase")] pub enum ServerRequest { /// Request to approve a patch. ApplyPatchApproval { #[serde(rename = "id")] request_id: RequestId, params: ApplyPatchApprovalParams, }, /// Request to exec a command. ExecCommandApproval { #[serde(rename = "id")] request_id: RequestId, params: ExecCommandApprovalParams, }, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct ApplyPatchApprovalParams { pub conversation_id: ConversationId, /// Use to correlate this with [codex_core::protocol::PatchApplyBeginEvent] /// and [codex_core::protocol::PatchApplyEndEvent]. pub call_id: String, pub file_changes: HashMap, /// Optional explanatory reason (e.g. request for extra write access). #[serde(skip_serializing_if = "Option::is_none")] pub reason: Option, /// When set, the agent is asking the user to allow writes under this root /// for the remainder of the session (unclear if this is honored today). #[serde(skip_serializing_if = "Option::is_none")] pub grant_root: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct ExecCommandApprovalParams { pub conversation_id: ConversationId, /// Use to correlate this with [codex_core::protocol::ExecCommandBeginEvent] /// and [codex_core::protocol::ExecCommandEndEvent]. pub call_id: String, pub command: Vec, pub cwd: PathBuf, #[serde(skip_serializing_if = "Option::is_none")] pub reason: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct ExecCommandApprovalResponse { pub decision: ReviewDecision, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct ApplyPatchApprovalResponse { pub decision: ReviewDecision, } #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; use serde_json::json; #[test] fn serialize_new_conversation() { let request = ClientRequest::NewConversation { request_id: RequestId::Integer(42), params: NewConversationParams { model: Some("gpt-5".to_string()), profile: None, cwd: None, approval_policy: Some(CodexToolCallApprovalPolicy::OnRequest), sandbox: None, config: None, base_instructions: None, include_plan_tool: None, include_apply_patch_tool: None, }, }; assert_eq!( json!({ "method": "newConversation", "id": 42, "params": { "model": "gpt-5", "approvalPolicy": "on-request" } }), serde_json::to_value(&request).unwrap(), ); } }