use std::collections::HashMap; use std::path::PathBuf; use crate::JSONRPCNotification; use crate::JSONRPCRequest; use crate::RequestId; use codex_protocol::ConversationId; use codex_protocol::config_types::ReasoningEffort; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::SandboxMode; use codex_protocol::config_types::Verbosity; use codex_protocol::parse_command::ParsedCommand; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::FileChange; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::TurnAbortReason; use paste::paste; use serde::Deserialize; use serde::Serialize; use strum_macros::Display; use ts_rs::TS; use uuid::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, } /// Generates an `enum ClientRequest` where each variant is a request that the /// client can send to the server. Each variant has associated `params` and /// `response` types. Also generates a `export_client_responses()` function to /// export all response types to TypeScript. macro_rules! client_request_definitions { ( $( $(#[$variant_meta:meta])* $variant:ident { params: $(#[$params_meta:meta])* $params:ty, response: $response:ty, } ),* $(,)? ) => { /// Request from the client to the server. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(tag = "method", rename_all = "camelCase")] pub enum ClientRequest { $( $(#[$variant_meta])* $variant { #[serde(rename = "id")] request_id: RequestId, $(#[$params_meta])* params: $params, }, )* } pub fn export_client_responses( out_dir: &::std::path::Path, ) -> ::std::result::Result<(), ::ts_rs::ExportError> { $( <$response as ::ts_rs::TS>::export_all_to(out_dir)?; )* Ok(()) } }; } client_request_definitions! { Initialize { params: InitializeParams, response: InitializeResponse, }, NewConversation { params: NewConversationParams, response: NewConversationResponse, }, /// List recorded Codex conversations (rollouts) with optional pagination and search. ListConversations { params: ListConversationsParams, response: ListConversationsResponse, }, /// Resume a recorded Codex conversation from a rollout file. ResumeConversation { params: ResumeConversationParams, response: ResumeConversationResponse, }, ArchiveConversation { params: ArchiveConversationParams, response: ArchiveConversationResponse, }, SendUserMessage { params: SendUserMessageParams, response: SendUserMessageResponse, }, SendUserTurn { params: SendUserTurnParams, response: SendUserTurnResponse, }, InterruptConversation { params: InterruptConversationParams, response: InterruptConversationResponse, }, AddConversationListener { params: AddConversationListenerParams, response: AddConversationSubscriptionResponse, }, RemoveConversationListener { params: RemoveConversationListenerParams, response: RemoveConversationSubscriptionResponse, }, GitDiffToRemote { params: GitDiffToRemoteParams, response: GitDiffToRemoteResponse, }, LoginApiKey { params: LoginApiKeyParams, response: LoginApiKeyResponse, }, LoginChatGpt { params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, response: LoginChatGptResponse, }, CancelLoginChatGpt { params: CancelLoginChatGptParams, response: CancelLoginChatGptResponse, }, LogoutChatGpt { params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, response: LogoutChatGptResponse, }, GetAuthStatus { params: GetAuthStatusParams, response: GetAuthStatusResponse, }, GetUserSavedConfig { params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, response: GetUserSavedConfigResponse, }, SetDefaultModel { params: SetDefaultModelParams, response: SetDefaultModelResponse, }, GetUserAgent { params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, response: GetUserAgentResponse, }, UserInfo { params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, response: UserInfoResponse, }, FuzzyFileSearch { params: FuzzyFileSearchParams, response: FuzzyFileSearchResponse, }, /// Execute a command (argv vector) under the server's sandbox. ExecOneOffCommand { params: ExecOneOffCommandParams, response: ExecOneOffCommandResponse, }, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, TS)] #[serde(rename_all = "camelCase")] pub struct InitializeParams { pub client_info: ClientInfo, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, TS)] #[serde(rename_all = "camelCase")] pub struct ClientInfo { pub name: String, #[serde(skip_serializing_if = "Option::is_none")] pub title: Option, pub version: String, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct InitializeResponse { pub user_agent: String, } #[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 ExecOneOffCommandResponse { 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, }, } /// Generates an `enum ServerRequest` where each variant is a request that the /// server can send to the client along with the corresponding params and /// response types. It also generates helper types used by the app/server /// infrastructure (payload enum, request constructor, and export helpers). macro_rules! server_request_definitions { ( $( $(#[$variant_meta:meta])* $variant:ident ),* $(,)? ) => { paste! { /// 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 { $( $(#[$variant_meta])* $variant { #[serde(rename = "id")] request_id: RequestId, params: [<$variant Params>], }, )* } #[derive(Debug, Clone, PartialEq)] pub enum ServerRequestPayload { $( $variant([<$variant Params>]), )* } impl ServerRequestPayload { pub fn request_with_id(self, request_id: RequestId) -> ServerRequest { match self { $(Self::$variant(params) => ServerRequest::$variant { request_id, params },)* } } } } pub fn export_server_responses( out_dir: &::std::path::Path, ) -> ::std::result::Result<(), ::ts_rs::ExportError> { paste! { $(<[<$variant Response>] as ::ts_rs::TS>::export_all_to(out_dir)?;)* } Ok(()) } }; } impl TryFrom for ServerRequest { type Error = serde_json::Error; fn try_from(value: JSONRPCRequest) -> Result { serde_json::from_value(serde_json::to_value(value)?) } } server_request_definitions! { /// Request to approve a patch. ApplyPatchApproval, /// Request to exec a command. ExecCommandApproval, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] 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)] #[serde(rename_all = "camelCase")] 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, pub parsed_cmd: Vec, } #[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 file_name: 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, TS)] #[serde(rename_all = "camelCase")] pub struct SessionConfiguredNotification { /// Name left as session_id instead of conversation_id for backwards compatibility. pub session_id: ConversationId, /// Tell the client what model is being queried. pub model: String, /// The effort the model is putting into reasoning about the user's request. #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, /// Identifier of the history log file (inode on Unix, 0 otherwise). pub history_log_id: u64, /// Current number of entries in the history log. #[ts(type = "number")] pub history_entry_count: usize, /// Optional initial messages (as events) for resumed sessions. /// When present, UIs can use these to seed the history. #[serde(skip_serializing_if = "Option::is_none")] pub initial_messages: Option>, pub rollout_path: PathBuf, } #[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, } /// Notification sent from the server to the client. #[derive(Serialize, Deserialize, Debug, Clone, 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), /// The special session configured event for a new or resumed conversation. SessionConfigured(SessionConfiguredNotification), } 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), ServerNotification::SessionConfigured(params) => serde_json::to_value(params), } } } impl TryFrom for ServerNotification { type Error = serde_json::Error; fn try_from(value: JSONRPCNotification) -> Result { serde_json::from_value(serde_json::to_value(value)?) } } /// Notification sent from the client to the server. #[derive(Serialize, Deserialize, Debug, Clone, TS, Display)] #[serde(tag = "method", content = "params", rename_all = "camelCase")] #[strum(serialize_all = "camelCase")] pub enum ClientNotification { Initialized, } #[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 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(()) } #[test] fn serialize_client_notification() -> Result<()> { let notification = ClientNotification::Initialized; // Note there is no "params" field for this notification. assert_eq!( json!({ "method": "initialized", }), serde_json::to_value(¬ification)?, ); Ok(()) } #[test] fn serialize_server_request() -> Result<()> { let conversation_id = ConversationId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?; let params = ExecCommandApprovalParams { conversation_id, call_id: "call-42".to_string(), command: vec!["echo".to_string(), "hello".to_string()], cwd: PathBuf::from("/tmp"), reason: Some("because tests".to_string()), parsed_cmd: vec![ParsedCommand::Unknown { cmd: "echo hello".to_string(), }], }; let request = ServerRequest::ExecCommandApproval { request_id: RequestId::Integer(7), params: params.clone(), }; assert_eq!( json!({ "method": "execCommandApproval", "id": 7, "params": { "conversationId": "67e55044-10b1-426f-9247-bb680e5fe0c8", "callId": "call-42", "command": ["echo", "hello"], "cwd": "/tmp", "reason": "because tests", "parsedCmd": [ { "type": "unknown", "cmd": "echo hello" } ] } }), serde_json::to_value(&request)?, ); let payload = ServerRequestPayload::ExecCommandApproval(params); assert_eq!(payload.request_with_id(RequestId::Integer(7)), request); Ok(()) } }