use std::collections::HashMap; use std::fmt::Display; use std::path::PathBuf; use crate::config_types::ReasoningEffort; use crate::config_types::ReasoningSummary; use crate::config_types::SandboxMode; use crate::config_types::Verbosity; use crate::protocol::AskForApproval; use crate::protocol::EventMsg; use crate::protocol::FileChange; use crate::protocol::ReviewDecision; use crate::protocol::SandboxPolicy; use crate::protocol::TurnAbortReason; use mcp_types::RequestId; use serde::Deserialize; use serde::Serialize; use strum_macros::Display; use ts_rs::TS; use uuid::Uuid; #[derive(Debug, Clone, Copy, PartialEq, Eq, TS, Hash)] #[ts(type = "string")] pub struct ConversationId { uuid: Uuid, } impl ConversationId { pub fn new() -> Self { Self { uuid: Uuid::now_v7(), } } pub fn from_string(s: &str) -> Result { Ok(Self { uuid: Uuid::parse_str(s)?, }) } } impl Default for ConversationId { fn default() -> Self { Self::new() } } impl Display for ConversationId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.uuid) } } impl Serialize for ConversationId { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { serializer.collect_str(&self.uuid) } } impl<'de> Deserialize<'de> for ConversationId { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let value = String::deserialize(deserializer)?; let uuid = Uuid::parse_str(&value).map_err(serde::de::Error::custom)?; Ok(Self { uuid }) } } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, TS)] #[ts(type = "string")] pub struct GitSha(pub String); impl GitSha { pub fn new(sha: &str) -> Self { Self(sha.to_string()) } } #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Display, TS)] #[serde(rename_all = "lowercase")] pub enum AuthMode { ApiKey, ChatGPT, } /// Request from the client to the server. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(tag = "method", rename_all = "camelCase")] pub enum ClientRequest { NewConversation { #[serde(rename = "id")] request_id: RequestId, params: NewConversationParams, }, /// List recorded Codex conversations (rollouts) with optional pagination and search. ListConversations { #[serde(rename = "id")] request_id: RequestId, params: ListConversationsParams, }, /// Resume a recorded Codex conversation from a rollout file. ResumeConversation { #[serde(rename = "id")] request_id: RequestId, params: ResumeConversationParams, }, ArchiveConversation { #[serde(rename = "id")] request_id: RequestId, params: ArchiveConversationParams, }, SendUserMessage { #[serde(rename = "id")] request_id: RequestId, params: SendUserMessageParams, }, SendUserTurn { #[serde(rename = "id")] request_id: RequestId, params: SendUserTurnParams, }, 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, }, GitDiffToRemote { #[serde(rename = "id")] request_id: RequestId, params: GitDiffToRemoteParams, }, LoginApiKey { #[serde(rename = "id")] request_id: RequestId, params: LoginApiKeyParams, }, LoginChatGpt { #[serde(rename = "id")] request_id: RequestId, }, CancelLoginChatGpt { #[serde(rename = "id")] request_id: RequestId, params: CancelLoginChatGptParams, }, LogoutChatGpt { #[serde(rename = "id")] request_id: RequestId, }, GetAuthStatus { #[serde(rename = "id")] request_id: RequestId, params: GetAuthStatusParams, }, GetUserSavedConfig { #[serde(rename = "id")] request_id: RequestId, }, SetDefaultModel { #[serde(rename = "id")] request_id: RequestId, params: SetDefaultModelParams, }, GetUserAgent { #[serde(rename = "id")] request_id: RequestId, }, UserInfo { #[serde(rename = "id")] request_id: RequestId, }, FuzzyFileSearch { #[serde(rename = "id")] request_id: RequestId, params: FuzzyFileSearchParams, }, /// Execute a command (argv vector) under the server's sandbox. ExecOneOffCommand { #[serde(rename = "id")] request_id: RequestId, params: ExecOneOffCommandParams, }, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, TS)] #[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, TS)] #[serde(rename_all = "camelCase")] pub struct NewConversationResponse { pub conversation_id: ConversationId, pub model: String, /// Note this could be ignored by the model. #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, pub rollout_path: PathBuf, } #[derive(Serialize, Deserialize, Debug, Clone, TS)] #[serde(rename_all = "camelCase")] pub struct ResumeConversationResponse { pub conversation_id: ConversationId, pub model: String, #[serde(skip_serializing_if = "Option::is_none")] pub initial_messages: Option>, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, TS)] #[serde(rename_all = "camelCase")] pub struct ListConversationsParams { /// Optional page size; defaults to a reasonable server-side value. #[serde(skip_serializing_if = "Option::is_none")] pub page_size: Option, /// Opaque pagination cursor returned by a previous call. #[serde(skip_serializing_if = "Option::is_none")] pub cursor: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct ConversationSummary { pub conversation_id: ConversationId, pub path: PathBuf, pub preview: String, /// RFC3339 timestamp string for the session start, if available. #[serde(skip_serializing_if = "Option::is_none")] pub timestamp: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct ListConversationsResponse { pub items: Vec, /// Opaque cursor to pass to the next call to continue after the last item. /// if None, there are no more items to return. #[serde(skip_serializing_if = "Option::is_none")] pub next_cursor: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct ResumeConversationParams { /// Absolute path to the rollout JSONL file. pub path: PathBuf, /// Optional overrides to apply when spawning the resumed session. #[serde(skip_serializing_if = "Option::is_none")] pub overrides: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct AddConversationSubscriptionResponse { pub subscription_id: Uuid, } /// The [`ConversationId`] must match the `rollout_path`. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct ArchiveConversationParams { pub conversation_id: ConversationId, pub rollout_path: PathBuf, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct ArchiveConversationResponse {} #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct RemoveConversationSubscriptionResponse {} #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct LoginApiKeyParams { pub api_key: String, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct LoginApiKeyResponse {} #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct LoginChatGptResponse { pub login_id: Uuid, /// URL the client should open in a browser to initiate the OAuth flow. pub auth_url: String, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct GitDiffToRemoteResponse { pub sha: GitSha, pub diff: String, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct CancelLoginChatGptParams { pub login_id: Uuid, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct GitDiffToRemoteParams { pub cwd: PathBuf, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct CancelLoginChatGptResponse {} #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct LogoutChatGptParams {} #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct LogoutChatGptResponse {} #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct GetAuthStatusParams { /// If true, include the current auth token (if available) in the response. #[serde(skip_serializing_if = "Option::is_none")] pub include_token: Option, /// If true, attempt to refresh the token before returning status. #[serde(skip_serializing_if = "Option::is_none")] pub refresh_token: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct ExecOneOffCommandParams { /// Command argv to execute. pub command: Vec, /// Timeout of the command in milliseconds. /// If not specified, a sensible default is used server-side. pub timeout_ms: Option, /// Optional working directory for the process. Defaults to server config cwd. #[serde(skip_serializing_if = "Option::is_none")] pub cwd: Option, /// Optional explicit sandbox policy overriding the server default. #[serde(skip_serializing_if = "Option::is_none")] pub sandbox_policy: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct ExecArbitraryCommandResponse { pub exit_code: i32, pub stdout: String, pub stderr: String, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct GetAuthStatusResponse { #[serde(skip_serializing_if = "Option::is_none")] pub auth_method: Option, #[serde(skip_serializing_if = "Option::is_none")] pub auth_token: Option, // Indicates that auth method must be valid to use the server. // This can be false if using a custom provider that is configured // with requires_openai_auth == false. #[serde(skip_serializing_if = "Option::is_none")] pub requires_openai_auth: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct GetUserAgentResponse { pub user_agent: String, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct UserInfoResponse { /// Note: `alleged_user_email` is not currently verified. We read it from /// the local auth.json, which the user could theoretically modify. In the /// future, we may add logic to verify the email against the server before /// returning it. pub alleged_user_email: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct GetUserSavedConfigResponse { pub config: UserSavedConfig, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct SetDefaultModelParams { /// If set to None, this means `model` should be cleared in config.toml. #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, /// If set to None, this means `model_reasoning_effort` should be cleared /// in config.toml. #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct SetDefaultModelResponse {} /// UserSavedConfig contains a subset of the config. It is meant to expose mcp /// client-configurable settings that can be specified in the NewConversation /// and SendUserTurn requests. #[derive(Deserialize, Debug, Clone, PartialEq, Serialize, TS)] #[serde(rename_all = "camelCase")] pub struct UserSavedConfig { /// Approvals #[serde(skip_serializing_if = "Option::is_none")] pub approval_policy: Option, #[serde(skip_serializing_if = "Option::is_none")] pub sandbox_mode: Option, #[serde(skip_serializing_if = "Option::is_none")] pub sandbox_settings: Option, /// Model-specific configuration #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, #[serde(skip_serializing_if = "Option::is_none")] pub model_reasoning_effort: Option, #[serde(skip_serializing_if = "Option::is_none")] pub model_reasoning_summary: Option, #[serde(skip_serializing_if = "Option::is_none")] pub model_verbosity: Option, /// Tools #[serde(skip_serializing_if = "Option::is_none")] pub tools: Option, /// Profiles #[serde(skip_serializing_if = "Option::is_none")] pub profile: Option, #[serde(default)] pub profiles: HashMap, } /// MCP representation of a [`codex_core::config_profile::ConfigProfile`]. #[derive(Deserialize, Debug, Clone, PartialEq, Serialize, TS)] #[serde(rename_all = "camelCase")] pub struct Profile { pub model: Option, /// The key in the `model_providers` map identifying the /// [`ModelProviderInfo`] to use. pub model_provider: Option, pub approval_policy: Option, pub model_reasoning_effort: Option, pub model_reasoning_summary: Option, pub model_verbosity: Option, pub chatgpt_base_url: Option, } /// MCP representation of a [`codex_core::config::ToolsToml`]. #[derive(Deserialize, Debug, Clone, PartialEq, Serialize, TS)] #[serde(rename_all = "camelCase")] pub struct Tools { #[serde(skip_serializing_if = "Option::is_none")] pub web_search: Option, #[serde(skip_serializing_if = "Option::is_none")] pub view_image: Option, } /// MCP representation of a [`codex_core::config_types::SandboxWorkspaceWrite`]. #[derive(Deserialize, Debug, Clone, PartialEq, Serialize, TS)] #[serde(rename_all = "camelCase")] pub struct SandboxSettings { #[serde(default)] pub writable_roots: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub network_access: Option, #[serde(skip_serializing_if = "Option::is_none")] pub exclude_tmpdir_env_var: Option, #[serde(skip_serializing_if = "Option::is_none")] pub exclude_slash_tmp: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct SendUserMessageParams { pub conversation_id: ConversationId, pub items: Vec, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct SendUserTurnParams { pub conversation_id: ConversationId, pub items: Vec, pub cwd: PathBuf, pub approval_policy: AskForApproval, pub sandbox_policy: SandboxPolicy, pub model: String, #[serde(skip_serializing_if = "Option::is_none")] pub effort: Option, pub summary: ReasoningSummary, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct SendUserTurnResponse {} #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct InterruptConversationParams { pub conversation_id: ConversationId, } #[derive(Serialize, Deserialize, Debug, Clone, TS)] #[serde(rename_all = "camelCase")] pub struct InterruptConversationResponse { pub abort_reason: TurnAbortReason, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct SendUserMessageResponse {} #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct AddConversationListenerParams { pub conversation_id: ConversationId, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct RemoveConversationListenerParams { pub subscription_id: Uuid, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[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, TS)] #[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, TS)] 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, TS)] 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, TS)] pub struct ExecCommandApprovalResponse { pub decision: ReviewDecision, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] pub struct ApplyPatchApprovalResponse { pub decision: ReviewDecision, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] pub struct FuzzyFileSearchParams { pub query: String, pub roots: Vec, // if provided, will cancel any previous request that used the same value #[serde(skip_serializing_if = "Option::is_none")] pub cancellation_token: Option, } /// Superset of [`codex_file_search::FileMatch`] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] pub struct FuzzyFileSearchResult { pub root: String, pub path: String, pub score: u32, #[serde(skip_serializing_if = "Option::is_none")] pub indices: Option>, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] pub struct FuzzyFileSearchResponse { pub files: Vec, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct LoginChatGptCompleteNotification { pub login_id: Uuid, pub success: bool, #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct AuthStatusChangeNotification { /// Current authentication method; omitted if signed out. #[serde(skip_serializing_if = "Option::is_none")] pub auth_method: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS, Display)] #[serde(tag = "method", content = "params", rename_all = "camelCase")] #[strum(serialize_all = "camelCase")] pub enum ServerNotification { /// Authentication status changed AuthStatusChange(AuthStatusChangeNotification), /// ChatGPT login flow completed LoginChatGptComplete(LoginChatGptCompleteNotification), } impl ServerNotification { pub fn to_params(self) -> Result { match self { ServerNotification::AuthStatusChange(params) => serde_json::to_value(params), ServerNotification::LoginChatGptComplete(params) => serde_json::to_value(params), } } } #[cfg(test)] mod tests { use super::*; use anyhow::Result; use pretty_assertions::assert_eq; use serde_json::json; #[test] fn serialize_new_conversation() -> Result<()> { let request = ClientRequest::NewConversation { request_id: RequestId::Integer(42), params: NewConversationParams { model: Some("gpt-5-codex".to_string()), profile: None, cwd: None, approval_policy: Some(AskForApproval::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-codex", "approvalPolicy": "on-request" } }), serde_json::to_value(&request)?, ); Ok(()) } #[test] fn test_conversation_id_default_is_not_zeroes() { let id = ConversationId::default(); assert_ne!(id.uuid, Uuid::nil()); } #[test] fn conversation_id_serializes_as_plain_string() -> Result<()> { let id = ConversationId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?; assert_eq!( json!("67e55044-10b1-426f-9247-bb680e5fe0c8"), serde_json::to_value(id)? ); Ok(()) } #[test] fn conversation_id_deserializes_from_plain_string() -> Result<()> { let id: ConversationId = serde_json::from_value(json!("67e55044-10b1-426f-9247-bb680e5fe0c8"))?; assert_eq!( ConversationId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?, id, ); Ok(()) } }