use std::path::Path; use crate::JSONRPCNotification; use crate::JSONRPCRequest; use crate::RequestId; use crate::export::GeneratedSchema; use crate::export::write_json_schema; use crate::protocol::v1; use crate::protocol::v2; use codex_experimental_api_macros::ExperimentalApi; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use strum_macros::Display; use ts_rs::TS; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema, TS)] #[ts(type = "string")] pub struct GitSha(pub String); impl GitSha { pub fn new(sha: &str) -> Self { Self(sha.to_string()) } } /// Authentication mode for OpenAI-backed providers. #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)] #[serde(rename_all = "lowercase")] pub enum AuthMode { /// OpenAI API key provided by the caller and stored by Codex. ApiKey, /// ChatGPT OAuth managed by Codex (tokens persisted and refreshed by Codex). Chatgpt, /// [UNSTABLE] FOR OPENAI INTERNAL USE ONLY - DO NOT USE. /// /// ChatGPT auth tokens are supplied by an external host app and are only /// stored in memory. Token refresh must be handled by the external host app. #[serde(rename = "chatgptAuthTokens")] #[ts(rename = "chatgptAuthTokens")] #[strum(serialize = "chatgptAuthTokens")] ChatgptAuthTokens, } macro_rules! experimental_reason_expr { // If a request variant is explicitly marked experimental, that reason wins. (#[experimental($reason:expr)] $params:ident $(, $inspect_params:tt)?) => { Some($reason) }; // `inspect_params: true` is used when a method is mostly stable but needs // field-level gating from its params type (for example, ThreadStart). ($params:ident, true) => { crate::experimental_api::ExperimentalApi::experimental_reason($params) }; ($params:ident $(, $inspect_params:tt)?) => { None }; } macro_rules! experimental_method_entry { (#[experimental($reason:expr)] => $wire:literal) => { $wire }; (#[experimental($reason:expr)]) => { $reason }; ($($tt:tt)*) => { "" }; } macro_rules! experimental_type_entry { (#[experimental($reason:expr)] $ty:ty) => { stringify!($ty) }; ($ty:ty) => { "" }; } /// 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 { ( $( $(#[experimental($reason:expr)])? $(#[doc = $variant_doc:literal])* $variant:ident $(=> $wire:literal)? { params: $(#[$params_meta:meta])* $params:ty, $(inspect_params: $inspect_params:tt,)? response: $response:ty, } ),* $(,)? ) => { /// Request from the client to the server. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(tag = "method", rename_all = "camelCase")] pub enum ClientRequest { $( $(#[doc = $variant_doc])* $(#[serde(rename = $wire)] #[ts(rename = $wire)])? $variant { #[serde(rename = "id")] request_id: RequestId, $(#[$params_meta])* params: $params, }, )* } impl crate::experimental_api::ExperimentalApi for ClientRequest { fn experimental_reason(&self) -> Option<&'static str> { match self { $( Self::$variant { params: _params, .. } => { experimental_reason_expr!( $(#[experimental($reason)])? _params $(, $inspect_params)? ) } )* } } } pub(crate) const EXPERIMENTAL_CLIENT_METHODS: &[&str] = &[ $( experimental_method_entry!($(#[experimental($reason)])? $(=> $wire)?), )* ]; pub(crate) const EXPERIMENTAL_CLIENT_METHOD_PARAM_TYPES: &[&str] = &[ $( experimental_type_entry!($(#[experimental($reason)])? $params), )* ]; pub(crate) const EXPERIMENTAL_CLIENT_METHOD_RESPONSE_TYPES: &[&str] = &[ $( experimental_type_entry!($(#[experimental($reason)])? $response), )* ]; 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(()) } #[allow(clippy::vec_init_then_push)] pub fn export_client_response_schemas( out_dir: &::std::path::Path, ) -> ::anyhow::Result> { let mut schemas = Vec::new(); $( schemas.push(write_json_schema::<$response>(out_dir, stringify!($response))?); )* Ok(schemas) } #[allow(clippy::vec_init_then_push)] pub fn export_client_param_schemas( out_dir: &::std::path::Path, ) -> ::anyhow::Result> { let mut schemas = Vec::new(); $( schemas.push(write_json_schema::<$params>(out_dir, stringify!($params))?); )* Ok(schemas) } }; } client_request_definitions! { Initialize { params: v1::InitializeParams, response: v1::InitializeResponse, }, /// NEW APIs // Thread lifecycle // Uses `inspect_params` because only some fields are experimental. ThreadStart => "thread/start" { params: v2::ThreadStartParams, inspect_params: true, response: v2::ThreadStartResponse, }, ThreadResume => "thread/resume" { params: v2::ThreadResumeParams, inspect_params: true, response: v2::ThreadResumeResponse, }, ThreadFork => "thread/fork" { params: v2::ThreadForkParams, inspect_params: true, response: v2::ThreadForkResponse, }, ThreadArchive => "thread/archive" { params: v2::ThreadArchiveParams, response: v2::ThreadArchiveResponse, }, ThreadUnsubscribe => "thread/unsubscribe" { params: v2::ThreadUnsubscribeParams, response: v2::ThreadUnsubscribeResponse, }, ThreadSetName => "thread/name/set" { params: v2::ThreadSetNameParams, response: v2::ThreadSetNameResponse, }, ThreadUnarchive => "thread/unarchive" { params: v2::ThreadUnarchiveParams, response: v2::ThreadUnarchiveResponse, }, ThreadCompactStart => "thread/compact/start" { params: v2::ThreadCompactStartParams, response: v2::ThreadCompactStartResponse, }, #[experimental("thread/backgroundTerminals/clean")] ThreadBackgroundTerminalsClean => "thread/backgroundTerminals/clean" { params: v2::ThreadBackgroundTerminalsCleanParams, response: v2::ThreadBackgroundTerminalsCleanResponse, }, ThreadRollback => "thread/rollback" { params: v2::ThreadRollbackParams, response: v2::ThreadRollbackResponse, }, ThreadList => "thread/list" { params: v2::ThreadListParams, response: v2::ThreadListResponse, }, ThreadLoadedList => "thread/loaded/list" { params: v2::ThreadLoadedListParams, response: v2::ThreadLoadedListResponse, }, ThreadRead => "thread/read" { params: v2::ThreadReadParams, response: v2::ThreadReadResponse, }, SkillsList => "skills/list" { params: v2::SkillsListParams, response: v2::SkillsListResponse, }, SkillsRemoteList => "skills/remote/list" { params: v2::SkillsRemoteReadParams, response: v2::SkillsRemoteReadResponse, }, SkillsRemoteExport => "skills/remote/export" { params: v2::SkillsRemoteWriteParams, response: v2::SkillsRemoteWriteResponse, }, AppsList => "app/list" { params: v2::AppsListParams, response: v2::AppsListResponse, }, SkillsConfigWrite => "skills/config/write" { params: v2::SkillsConfigWriteParams, response: v2::SkillsConfigWriteResponse, }, TurnStart => "turn/start" { params: v2::TurnStartParams, inspect_params: true, response: v2::TurnStartResponse, }, TurnSteer => "turn/steer" { params: v2::TurnSteerParams, response: v2::TurnSteerResponse, }, TurnInterrupt => "turn/interrupt" { params: v2::TurnInterruptParams, response: v2::TurnInterruptResponse, }, #[experimental("thread/realtime/start")] ThreadRealtimeStart => "thread/realtime/start" { params: v2::ThreadRealtimeStartParams, response: v2::ThreadRealtimeStartResponse, }, #[experimental("thread/realtime/appendAudio")] ThreadRealtimeAppendAudio => "thread/realtime/appendAudio" { params: v2::ThreadRealtimeAppendAudioParams, response: v2::ThreadRealtimeAppendAudioResponse, }, #[experimental("thread/realtime/appendText")] ThreadRealtimeAppendText => "thread/realtime/appendText" { params: v2::ThreadRealtimeAppendTextParams, response: v2::ThreadRealtimeAppendTextResponse, }, #[experimental("thread/realtime/stop")] ThreadRealtimeStop => "thread/realtime/stop" { params: v2::ThreadRealtimeStopParams, response: v2::ThreadRealtimeStopResponse, }, ReviewStart => "review/start" { params: v2::ReviewStartParams, response: v2::ReviewStartResponse, }, ModelList => "model/list" { params: v2::ModelListParams, response: v2::ModelListResponse, }, ExperimentalFeatureList => "experimentalFeature/list" { params: v2::ExperimentalFeatureListParams, response: v2::ExperimentalFeatureListResponse, }, #[experimental("collaborationMode/list")] /// Lists collaboration mode presets. CollaborationModeList => "collaborationMode/list" { params: v2::CollaborationModeListParams, response: v2::CollaborationModeListResponse, }, #[experimental("mock/experimentalMethod")] /// Test-only method used to validate experimental gating. MockExperimentalMethod => "mock/experimentalMethod" { params: v2::MockExperimentalMethodParams, response: v2::MockExperimentalMethodResponse, }, McpServerOauthLogin => "mcpServer/oauth/login" { params: v2::McpServerOauthLoginParams, response: v2::McpServerOauthLoginResponse, }, McpServerRefresh => "config/mcpServer/reload" { params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, response: v2::McpServerRefreshResponse, }, McpServerStatusList => "mcpServerStatus/list" { params: v2::ListMcpServerStatusParams, response: v2::ListMcpServerStatusResponse, }, WindowsSandboxSetupStart => "windowsSandbox/setupStart" { params: v2::WindowsSandboxSetupStartParams, response: v2::WindowsSandboxSetupStartResponse, }, LoginAccount => "account/login/start" { params: v2::LoginAccountParams, inspect_params: true, response: v2::LoginAccountResponse, }, CancelLoginAccount => "account/login/cancel" { params: v2::CancelLoginAccountParams, response: v2::CancelLoginAccountResponse, }, LogoutAccount => "account/logout" { params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, response: v2::LogoutAccountResponse, }, GetAccountRateLimits => "account/rateLimits/read" { params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, response: v2::GetAccountRateLimitsResponse, }, FeedbackUpload => "feedback/upload" { params: v2::FeedbackUploadParams, response: v2::FeedbackUploadResponse, }, /// Execute a command (argv vector) under the server's sandbox. OneOffCommandExec => "command/exec" { params: v2::CommandExecParams, response: v2::CommandExecResponse, }, ConfigRead => "config/read" { params: v2::ConfigReadParams, response: v2::ConfigReadResponse, }, ExternalAgentConfigDetect => "externalAgentConfig/detect" { params: v2::ExternalAgentConfigDetectParams, response: v2::ExternalAgentConfigDetectResponse, }, ExternalAgentConfigImport => "externalAgentConfig/import" { params: v2::ExternalAgentConfigImportParams, response: v2::ExternalAgentConfigImportResponse, }, ConfigValueWrite => "config/value/write" { params: v2::ConfigValueWriteParams, response: v2::ConfigWriteResponse, }, ConfigBatchWrite => "config/batchWrite" { params: v2::ConfigBatchWriteParams, response: v2::ConfigWriteResponse, }, ConfigRequirementsRead => "configRequirements/read" { params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, response: v2::ConfigRequirementsReadResponse, }, GetAccount => "account/read" { params: v2::GetAccountParams, response: v2::GetAccountResponse, }, /// DEPRECATED APIs below NewConversation { params: v1::NewConversationParams, response: v1::NewConversationResponse, }, GetConversationSummary { params: v1::GetConversationSummaryParams, response: v1::GetConversationSummaryResponse, }, /// List recorded Codex conversations (rollouts) with optional pagination and search. ListConversations { params: v1::ListConversationsParams, response: v1::ListConversationsResponse, }, /// Resume a recorded Codex conversation from a rollout file. ResumeConversation { params: v1::ResumeConversationParams, response: v1::ResumeConversationResponse, }, /// Fork a recorded Codex conversation into a new session. ForkConversation { params: v1::ForkConversationParams, response: v1::ForkConversationResponse, }, ArchiveConversation { params: v1::ArchiveConversationParams, response: v1::ArchiveConversationResponse, }, SendUserMessage { params: v1::SendUserMessageParams, response: v1::SendUserMessageResponse, }, SendUserTurn { params: v1::SendUserTurnParams, response: v1::SendUserTurnResponse, }, InterruptConversation { params: v1::InterruptConversationParams, response: v1::InterruptConversationResponse, }, AddConversationListener { params: v1::AddConversationListenerParams, response: v1::AddConversationSubscriptionResponse, }, RemoveConversationListener { params: v1::RemoveConversationListenerParams, response: v1::RemoveConversationSubscriptionResponse, }, GitDiffToRemote { params: v1::GitDiffToRemoteParams, response: v1::GitDiffToRemoteResponse, }, LoginApiKey { params: v1::LoginApiKeyParams, response: v1::LoginApiKeyResponse, }, LoginChatGpt { params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, response: v1::LoginChatGptResponse, }, // DEPRECATED in favor of CancelLoginAccount CancelLoginChatGpt { params: v1::CancelLoginChatGptParams, response: v1::CancelLoginChatGptResponse, }, LogoutChatGpt { params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, response: v1::LogoutChatGptResponse, }, /// DEPRECATED in favor of GetAccount GetAuthStatus { params: v1::GetAuthStatusParams, response: v1::GetAuthStatusResponse, }, GetUserSavedConfig { params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, response: v1::GetUserSavedConfigResponse, }, SetDefaultModel { params: v1::SetDefaultModelParams, response: v1::SetDefaultModelResponse, }, GetUserAgent { params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, response: v1::GetUserAgentResponse, }, UserInfo { params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, response: v1::UserInfoResponse, }, FuzzyFileSearch { params: FuzzyFileSearchParams, response: FuzzyFileSearchResponse, }, #[experimental("fuzzyFileSearch/sessionStart")] FuzzyFileSearchSessionStart => "fuzzyFileSearch/sessionStart" { params: FuzzyFileSearchSessionStartParams, response: FuzzyFileSearchSessionStartResponse, }, #[experimental("fuzzyFileSearch/sessionUpdate")] FuzzyFileSearchSessionUpdate => "fuzzyFileSearch/sessionUpdate" { params: FuzzyFileSearchSessionUpdateParams, response: FuzzyFileSearchSessionUpdateResponse, }, #[experimental("fuzzyFileSearch/sessionStop")] FuzzyFileSearchSessionStop => "fuzzyFileSearch/sessionStop" { params: FuzzyFileSearchSessionStopParams, response: FuzzyFileSearchSessionStopResponse, }, /// Execute a command (argv vector) under the server's sandbox. ExecOneOffCommand { params: v1::ExecOneOffCommandParams, response: v1::ExecOneOffCommandResponse, }, } /// 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 $(=> $wire:literal)? { params: $params:ty, response: $response:ty, } ),* $(,)? ) => { /// Request initiated from the server and sent to the client. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[allow(clippy::large_enum_variant)] #[serde(tag = "method", rename_all = "camelCase")] pub enum ServerRequest { $( $(#[$variant_meta])* $(#[serde(rename = $wire)] #[ts(rename = $wire)])? $variant { #[serde(rename = "id")] request_id: RequestId, params: $params, }, )* } #[derive(Debug, Clone, PartialEq, JsonSchema)] #[allow(clippy::large_enum_variant)] pub enum ServerRequestPayload { $( $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> { $( <$response as ::ts_rs::TS>::export_all_to(out_dir)?; )* Ok(()) } #[allow(clippy::vec_init_then_push)] pub fn export_server_response_schemas( out_dir: &Path, ) -> ::anyhow::Result> { let mut schemas = Vec::new(); $( schemas.push(crate::export::write_json_schema::<$response>( out_dir, concat!(stringify!($variant), "Response"), )?); )* Ok(schemas) } #[allow(clippy::vec_init_then_push)] pub fn export_server_param_schemas( out_dir: &Path, ) -> ::anyhow::Result> { let mut schemas = Vec::new(); $( schemas.push(crate::export::write_json_schema::<$params>( out_dir, concat!(stringify!($variant), "Params"), )?); )* Ok(schemas) } }; } /// Generates `ServerNotification` enum and helpers, including a JSON Schema /// exporter for each notification. macro_rules! server_notification_definitions { ( $( $(#[$variant_meta:meta])* $variant:ident $(=> $wire:literal)? ( $payload:ty ) ),* $(,)? ) => { /// Notification sent from the server to the client. #[derive( Serialize, Deserialize, Debug, Clone, JsonSchema, TS, Display, ExperimentalApi, )] #[serde(tag = "method", content = "params", rename_all = "camelCase")] #[strum(serialize_all = "camelCase")] pub enum ServerNotification { $( $(#[$variant_meta])* $(#[serde(rename = $wire)] #[ts(rename = $wire)] #[strum(serialize = $wire)])? $variant($payload), )* } impl ServerNotification { pub fn to_params(self) -> Result { match self { $(Self::$variant(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)?) } } #[allow(clippy::vec_init_then_push)] pub fn export_server_notification_schemas( out_dir: &::std::path::Path, ) -> ::anyhow::Result> { let mut schemas = Vec::new(); $(schemas.push(crate::export::write_json_schema::<$payload>(out_dir, stringify!($payload))?);)* Ok(schemas) } }; } /// Notifications sent from the client to the server. macro_rules! client_notification_definitions { ( $( $(#[$variant_meta:meta])* $variant:ident $( ( $payload:ty ) )? ),* $(,)? ) => { #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS, Display)] #[serde(tag = "method", content = "params", rename_all = "camelCase")] #[strum(serialize_all = "camelCase")] pub enum ClientNotification { $( $(#[$variant_meta])* $variant $( ( $payload ) )?, )* } pub fn export_client_notification_schemas( _out_dir: &::std::path::Path, ) -> ::anyhow::Result> { let schemas = Vec::new(); $( $(schemas.push(crate::export::write_json_schema::<$payload>(_out_dir, stringify!($payload))?);)? )* Ok(schemas) } }; } 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! { /// NEW APIs /// Sent when approval is requested for a specific command execution. /// This request is used for Turns started via turn/start. CommandExecutionRequestApproval => "item/commandExecution/requestApproval" { params: v2::CommandExecutionRequestApprovalParams, response: v2::CommandExecutionRequestApprovalResponse, }, /// Sent when approval is requested for a specific file change. /// This request is used for Turns started via turn/start. FileChangeRequestApproval => "item/fileChange/requestApproval" { params: v2::FileChangeRequestApprovalParams, response: v2::FileChangeRequestApprovalResponse, }, /// EXPERIMENTAL - Request input from the user for a tool call. ToolRequestUserInput => "item/tool/requestUserInput" { params: v2::ToolRequestUserInputParams, response: v2::ToolRequestUserInputResponse, }, /// Execute a dynamic tool call on the client. DynamicToolCall => "item/tool/call" { params: v2::DynamicToolCallParams, response: v2::DynamicToolCallResponse, }, ChatgptAuthTokensRefresh => "account/chatgptAuthTokens/refresh" { params: v2::ChatgptAuthTokensRefreshParams, response: v2::ChatgptAuthTokensRefreshResponse, }, /// DEPRECATED APIs below /// Request to approve a patch. /// This request is used for Turns started via the legacy APIs (i.e. SendUserTurn, SendUserMessage). ApplyPatchApproval { params: v1::ApplyPatchApprovalParams, response: v1::ApplyPatchApprovalResponse, }, /// Request to exec a command. /// This request is used for Turns started via the legacy APIs (i.e. SendUserTurn, SendUserMessage). ExecCommandApproval { params: v1::ExecCommandApprovalParams, response: v1::ExecCommandApprovalResponse, }, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, 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 pub cancellation_token: Option, } /// Superset of [`codex_file_search::FileMatch`] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] pub struct FuzzyFileSearchResult { pub root: String, pub path: String, pub file_name: String, pub score: u32, pub indices: Option>, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] pub struct FuzzyFileSearchResponse { pub files: Vec, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] pub struct FuzzyFileSearchSessionStartParams { pub session_id: String, pub roots: Vec, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, Default)] pub struct FuzzyFileSearchSessionStartResponse {} #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] pub struct FuzzyFileSearchSessionUpdateParams { pub session_id: String, pub query: String, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, Default)] pub struct FuzzyFileSearchSessionUpdateResponse {} #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] pub struct FuzzyFileSearchSessionStopParams { pub session_id: String, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, Default)] pub struct FuzzyFileSearchSessionStopResponse {} #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] pub struct FuzzyFileSearchSessionUpdatedNotification { pub session_id: String, pub query: String, pub files: Vec, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] pub struct FuzzyFileSearchSessionCompletedNotification { pub session_id: String, } server_notification_definitions! { /// NEW NOTIFICATIONS Error => "error" (v2::ErrorNotification), ThreadStarted => "thread/started" (v2::ThreadStartedNotification), ThreadStatusChanged => "thread/status/changed" (v2::ThreadStatusChangedNotification), ThreadArchived => "thread/archived" (v2::ThreadArchivedNotification), ThreadUnarchived => "thread/unarchived" (v2::ThreadUnarchivedNotification), ThreadClosed => "thread/closed" (v2::ThreadClosedNotification), ThreadNameUpdated => "thread/name/updated" (v2::ThreadNameUpdatedNotification), ThreadTokenUsageUpdated => "thread/tokenUsage/updated" (v2::ThreadTokenUsageUpdatedNotification), TurnStarted => "turn/started" (v2::TurnStartedNotification), TurnCompleted => "turn/completed" (v2::TurnCompletedNotification), TurnDiffUpdated => "turn/diff/updated" (v2::TurnDiffUpdatedNotification), TurnPlanUpdated => "turn/plan/updated" (v2::TurnPlanUpdatedNotification), ItemStarted => "item/started" (v2::ItemStartedNotification), ItemCompleted => "item/completed" (v2::ItemCompletedNotification), /// This event is internal-only. Used by Codex Cloud. RawResponseItemCompleted => "rawResponseItem/completed" (v2::RawResponseItemCompletedNotification), AgentMessageDelta => "item/agentMessage/delta" (v2::AgentMessageDeltaNotification), /// EXPERIMENTAL - proposed plan streaming deltas for plan items. PlanDelta => "item/plan/delta" (v2::PlanDeltaNotification), CommandExecutionOutputDelta => "item/commandExecution/outputDelta" (v2::CommandExecutionOutputDeltaNotification), TerminalInteraction => "item/commandExecution/terminalInteraction" (v2::TerminalInteractionNotification), FileChangeOutputDelta => "item/fileChange/outputDelta" (v2::FileChangeOutputDeltaNotification), McpToolCallProgress => "item/mcpToolCall/progress" (v2::McpToolCallProgressNotification), McpServerOauthLoginCompleted => "mcpServer/oauthLogin/completed" (v2::McpServerOauthLoginCompletedNotification), AccountUpdated => "account/updated" (v2::AccountUpdatedNotification), AccountRateLimitsUpdated => "account/rateLimits/updated" (v2::AccountRateLimitsUpdatedNotification), AppListUpdated => "app/list/updated" (v2::AppListUpdatedNotification), ReasoningSummaryTextDelta => "item/reasoning/summaryTextDelta" (v2::ReasoningSummaryTextDeltaNotification), ReasoningSummaryPartAdded => "item/reasoning/summaryPartAdded" (v2::ReasoningSummaryPartAddedNotification), ReasoningTextDelta => "item/reasoning/textDelta" (v2::ReasoningTextDeltaNotification), /// Deprecated: Use `ContextCompaction` item type instead. ContextCompacted => "thread/compacted" (v2::ContextCompactedNotification), ModelRerouted => "model/rerouted" (v2::ModelReroutedNotification), DeprecationNotice => "deprecationNotice" (v2::DeprecationNoticeNotification), ConfigWarning => "configWarning" (v2::ConfigWarningNotification), FuzzyFileSearchSessionUpdated => "fuzzyFileSearch/sessionUpdated" (FuzzyFileSearchSessionUpdatedNotification), FuzzyFileSearchSessionCompleted => "fuzzyFileSearch/sessionCompleted" (FuzzyFileSearchSessionCompletedNotification), #[experimental("thread/realtime/started")] ThreadRealtimeStarted => "thread/realtime/started" (v2::ThreadRealtimeStartedNotification), #[experimental("thread/realtime/itemAdded")] ThreadRealtimeItemAdded => "thread/realtime/itemAdded" (v2::ThreadRealtimeItemAddedNotification), #[experimental("thread/realtime/outputAudio/delta")] ThreadRealtimeOutputAudioDelta => "thread/realtime/outputAudio/delta" (v2::ThreadRealtimeOutputAudioDeltaNotification), #[experimental("thread/realtime/error")] ThreadRealtimeError => "thread/realtime/error" (v2::ThreadRealtimeErrorNotification), #[experimental("thread/realtime/closed")] ThreadRealtimeClosed => "thread/realtime/closed" (v2::ThreadRealtimeClosedNotification), /// Notifies the user of world-writable directories on Windows, which cannot be protected by the sandbox. WindowsWorldWritableWarning => "windows/worldWritableWarning" (v2::WindowsWorldWritableWarningNotification), WindowsSandboxSetupCompleted => "windowsSandbox/setupCompleted" (v2::WindowsSandboxSetupCompletedNotification), #[serde(rename = "account/login/completed")] #[ts(rename = "account/login/completed")] #[strum(serialize = "account/login/completed")] AccountLoginCompleted(v2::AccountLoginCompletedNotification), /// DEPRECATED NOTIFICATIONS below AuthStatusChange(v1::AuthStatusChangeNotification), /// Deprecated: use `account/login/completed` instead. LoginChatGptComplete(v1::LoginChatGptCompleteNotification), SessionConfigured(v1::SessionConfiguredNotification), } client_notification_definitions! { Initialized, } #[cfg(test)] mod tests { use super::*; use anyhow::Result; use codex_protocol::ThreadId; use codex_protocol::account::PlanType; use codex_protocol::parse_command::ParsedCommand; use codex_protocol::protocol::AskForApproval; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use serde_json::json; use std::path::PathBuf; fn absolute_path(path: &str) -> AbsolutePathBuf { AbsolutePathBuf::from_absolute_path(path).expect("absolute path") } #[test] fn serialize_new_conversation() -> Result<()> { let request = ClientRequest::NewConversation { request_id: RequestId::Integer(42), params: v1::NewConversationParams { model: Some("gpt-5.1-codex-max".to_string()), model_provider: None, profile: None, cwd: None, approval_policy: Some(AskForApproval::OnRequest), sandbox: None, config: None, base_instructions: None, developer_instructions: None, compact_prompt: None, include_apply_patch_tool: None, }, }; assert_eq!( json!({ "method": "newConversation", "id": 42, "params": { "model": "gpt-5.1-codex-max", "modelProvider": null, "profile": null, "cwd": null, "approvalPolicy": "on-request", "sandbox": null, "config": null, "baseInstructions": null, "includeApplyPatchTool": null } }), serde_json::to_value(&request)?, ); Ok(()) } #[test] fn serialize_initialize_with_opt_out_notification_methods() -> Result<()> { let request = ClientRequest::Initialize { request_id: RequestId::Integer(42), params: v1::InitializeParams { client_info: v1::ClientInfo { name: "codex_vscode".to_string(), title: Some("Codex VS Code Extension".to_string()), version: "0.1.0".to_string(), }, capabilities: Some(v1::InitializeCapabilities { experimental_api: true, opt_out_notification_methods: Some(vec![ "codex/event/session_configured".to_string(), "item/agentMessage/delta".to_string(), ]), }), }, }; assert_eq!( json!({ "method": "initialize", "id": 42, "params": { "clientInfo": { "name": "codex_vscode", "title": "Codex VS Code Extension", "version": "0.1.0" }, "capabilities": { "experimentalApi": true, "optOutNotificationMethods": [ "codex/event/session_configured", "item/agentMessage/delta" ] } } }), serde_json::to_value(&request)?, ); Ok(()) } #[test] fn deserialize_initialize_with_opt_out_notification_methods() -> Result<()> { let request: ClientRequest = serde_json::from_value(json!({ "method": "initialize", "id": 42, "params": { "clientInfo": { "name": "codex_vscode", "title": "Codex VS Code Extension", "version": "0.1.0" }, "capabilities": { "experimentalApi": true, "optOutNotificationMethods": [ "codex/event/session_configured", "item/agentMessage/delta" ] } } }))?; assert_eq!( request, ClientRequest::Initialize { request_id: RequestId::Integer(42), params: v1::InitializeParams { client_info: v1::ClientInfo { name: "codex_vscode".to_string(), title: Some("Codex VS Code Extension".to_string()), version: "0.1.0".to_string(), }, capabilities: Some(v1::InitializeCapabilities { experimental_api: true, opt_out_notification_methods: Some(vec![ "codex/event/session_configured".to_string(), "item/agentMessage/delta".to_string(), ]), }), }, } ); Ok(()) } #[test] fn conversation_id_serializes_as_plain_string() -> Result<()> { let id = ThreadId::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: ThreadId = serde_json::from_value(json!("67e55044-10b1-426f-9247-bb680e5fe0c8"))?; assert_eq!( ThreadId::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 = ThreadId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?; let params = v1::ExecCommandApprovalParams { conversation_id, call_id: "call-42".to_string(), approval_id: Some("approval-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", "approvalId": "approval-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(()) } #[test] fn serialize_chatgpt_auth_tokens_refresh_request() -> Result<()> { let request = ServerRequest::ChatgptAuthTokensRefresh { request_id: RequestId::Integer(8), params: v2::ChatgptAuthTokensRefreshParams { reason: v2::ChatgptAuthTokensRefreshReason::Unauthorized, previous_account_id: Some("org-123".to_string()), }, }; assert_eq!( json!({ "method": "account/chatgptAuthTokens/refresh", "id": 8, "params": { "reason": "unauthorized", "previousAccountId": "org-123" } }), serde_json::to_value(&request)?, ); Ok(()) } #[test] fn serialize_get_account_rate_limits() -> Result<()> { let request = ClientRequest::GetAccountRateLimits { request_id: RequestId::Integer(1), params: None, }; assert_eq!( json!({ "method": "account/rateLimits/read", "id": 1, }), serde_json::to_value(&request)?, ); Ok(()) } #[test] fn serialize_config_requirements_read() -> Result<()> { let request = ClientRequest::ConfigRequirementsRead { request_id: RequestId::Integer(1), params: None, }; assert_eq!( json!({ "method": "configRequirements/read", "id": 1, }), serde_json::to_value(&request)?, ); Ok(()) } #[test] fn serialize_account_login_api_key() -> Result<()> { let request = ClientRequest::LoginAccount { request_id: RequestId::Integer(2), params: v2::LoginAccountParams::ApiKey { api_key: "secret".to_string(), }, }; assert_eq!( json!({ "method": "account/login/start", "id": 2, "params": { "type": "apiKey", "apiKey": "secret" } }), serde_json::to_value(&request)?, ); Ok(()) } #[test] fn serialize_account_login_chatgpt() -> Result<()> { let request = ClientRequest::LoginAccount { request_id: RequestId::Integer(3), params: v2::LoginAccountParams::Chatgpt, }; assert_eq!( json!({ "method": "account/login/start", "id": 3, "params": { "type": "chatgpt" } }), serde_json::to_value(&request)?, ); Ok(()) } #[test] fn serialize_account_logout() -> Result<()> { let request = ClientRequest::LogoutAccount { request_id: RequestId::Integer(4), params: None, }; assert_eq!( json!({ "method": "account/logout", "id": 4, }), serde_json::to_value(&request)?, ); Ok(()) } #[test] fn serialize_account_login_chatgpt_auth_tokens() -> Result<()> { let request = ClientRequest::LoginAccount { request_id: RequestId::Integer(5), params: v2::LoginAccountParams::ChatgptAuthTokens { access_token: "access-token".to_string(), chatgpt_account_id: "org-123".to_string(), chatgpt_plan_type: Some("business".to_string()), }, }; assert_eq!( json!({ "method": "account/login/start", "id": 5, "params": { "type": "chatgptAuthTokens", "accessToken": "access-token", "chatgptAccountId": "org-123", "chatgptPlanType": "business" } }), serde_json::to_value(&request)?, ); Ok(()) } #[test] fn serialize_get_account() -> Result<()> { let request = ClientRequest::GetAccount { request_id: RequestId::Integer(6), params: v2::GetAccountParams { refresh_token: false, }, }; assert_eq!( json!({ "method": "account/read", "id": 6, "params": { "refreshToken": false } }), serde_json::to_value(&request)?, ); Ok(()) } #[test] fn account_serializes_fields_in_camel_case() -> Result<()> { let api_key = v2::Account::ApiKey {}; assert_eq!( json!({ "type": "apiKey", }), serde_json::to_value(&api_key)?, ); let chatgpt = v2::Account::Chatgpt { email: "user@example.com".to_string(), plan_type: PlanType::Plus, }; assert_eq!( json!({ "type": "chatgpt", "email": "user@example.com", "planType": "plus", }), serde_json::to_value(&chatgpt)?, ); Ok(()) } #[test] fn serialize_list_models() -> Result<()> { let request = ClientRequest::ModelList { request_id: RequestId::Integer(6), params: v2::ModelListParams::default(), }; assert_eq!( json!({ "method": "model/list", "id": 6, "params": { "limit": null, "cursor": null, "includeHidden": null } }), serde_json::to_value(&request)?, ); Ok(()) } #[test] fn serialize_list_collaboration_modes() -> Result<()> { let request = ClientRequest::CollaborationModeList { request_id: RequestId::Integer(7), params: v2::CollaborationModeListParams::default(), }; assert_eq!( json!({ "method": "collaborationMode/list", "id": 7, "params": {} }), serde_json::to_value(&request)?, ); Ok(()) } #[test] fn serialize_list_apps() -> Result<()> { let request = ClientRequest::AppsList { request_id: RequestId::Integer(8), params: v2::AppsListParams::default(), }; assert_eq!( json!({ "method": "app/list", "id": 8, "params": { "cursor": null, "limit": null, "threadId": null } }), serde_json::to_value(&request)?, ); Ok(()) } #[test] fn serialize_list_experimental_features() -> Result<()> { let request = ClientRequest::ExperimentalFeatureList { request_id: RequestId::Integer(8), params: v2::ExperimentalFeatureListParams::default(), }; assert_eq!( json!({ "method": "experimentalFeature/list", "id": 8, "params": { "cursor": null, "limit": null } }), serde_json::to_value(&request)?, ); Ok(()) } #[test] fn serialize_thread_background_terminals_clean() -> Result<()> { let request = ClientRequest::ThreadBackgroundTerminalsClean { request_id: RequestId::Integer(8), params: v2::ThreadBackgroundTerminalsCleanParams { thread_id: "thr_123".to_string(), }, }; assert_eq!( json!({ "method": "thread/backgroundTerminals/clean", "id": 8, "params": { "threadId": "thr_123" } }), serde_json::to_value(&request)?, ); Ok(()) } #[test] fn serialize_thread_realtime_start() -> Result<()> { let request = ClientRequest::ThreadRealtimeStart { request_id: RequestId::Integer(9), params: v2::ThreadRealtimeStartParams { thread_id: "thr_123".to_string(), prompt: "You are on a call".to_string(), session_id: Some("sess_456".to_string()), }, }; assert_eq!( json!({ "method": "thread/realtime/start", "id": 9, "params": { "threadId": "thr_123", "prompt": "You are on a call", "sessionId": "sess_456" } }), serde_json::to_value(&request)?, ); Ok(()) } #[test] fn serialize_thread_status_changed_notification() -> Result<()> { let notification = ServerNotification::ThreadStatusChanged(v2::ThreadStatusChangedNotification { thread_id: "thr_123".to_string(), status: v2::ThreadStatus::Idle, }); assert_eq!( json!({ "method": "thread/status/changed", "params": { "threadId": "thr_123", "status": { "type": "idle" }, } }), serde_json::to_value(¬ification)?, ); Ok(()) } #[test] fn serialize_thread_realtime_output_audio_delta_notification() -> Result<()> { let notification = ServerNotification::ThreadRealtimeOutputAudioDelta( v2::ThreadRealtimeOutputAudioDeltaNotification { thread_id: "thr_123".to_string(), audio: v2::ThreadRealtimeAudioChunk { data: "AQID".to_string(), sample_rate: 24_000, num_channels: 1, samples_per_channel: Some(512), }, }, ); assert_eq!( json!({ "method": "thread/realtime/outputAudio/delta", "params": { "threadId": "thr_123", "audio": { "data": "AQID", "sampleRate": 24000, "numChannels": 1, "samplesPerChannel": 512 } } }), serde_json::to_value(¬ification)?, ); Ok(()) } #[test] fn mock_experimental_method_is_marked_experimental() { let request = ClientRequest::MockExperimentalMethod { request_id: RequestId::Integer(1), params: v2::MockExperimentalMethodParams::default(), }; let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&request); assert_eq!(reason, Some("mock/experimentalMethod")); } #[test] fn thread_realtime_start_is_marked_experimental() { let request = ClientRequest::ThreadRealtimeStart { request_id: RequestId::Integer(1), params: v2::ThreadRealtimeStartParams { thread_id: "thr_123".to_string(), prompt: "You are on a call".to_string(), session_id: None, }, }; let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&request); assert_eq!(reason, Some("thread/realtime/start")); } #[test] fn thread_realtime_started_notification_is_marked_experimental() { let notification = ServerNotification::ThreadRealtimeStarted(v2::ThreadRealtimeStartedNotification { thread_id: "thr_123".to_string(), session_id: Some("sess_456".to_string()), }); let reason = crate::experimental_api::ExperimentalApi::experimental_reason(¬ification); assert_eq!(reason, Some("thread/realtime/started")); } #[test] fn thread_realtime_output_audio_delta_notification_is_marked_experimental() { let notification = ServerNotification::ThreadRealtimeOutputAudioDelta( v2::ThreadRealtimeOutputAudioDeltaNotification { thread_id: "thr_123".to_string(), audio: v2::ThreadRealtimeAudioChunk { data: "AQID".to_string(), sample_rate: 24_000, num_channels: 1, samples_per_channel: Some(512), }, }, ); let reason = crate::experimental_api::ExperimentalApi::experimental_reason(¬ification); assert_eq!(reason, Some("thread/realtime/outputAudio/delta")); } #[test] fn command_execution_request_approval_additional_permissions_is_marked_experimental() { let params = v2::CommandExecutionRequestApprovalParams { thread_id: "thr_123".to_string(), turn_id: "turn_123".to_string(), item_id: "call_123".to_string(), approval_id: None, reason: None, network_approval_context: None, command: Some("cat file".to_string()), cwd: None, command_actions: None, additional_permissions: Some(v2::AdditionalPermissionProfile { network: None, file_system: Some(v2::AdditionalFileSystemPermissions { read: Some(vec![absolute_path("/tmp/allowed")]), write: None, }), macos: None, }), proposed_execpolicy_amendment: None, proposed_network_policy_amendments: None, available_decisions: None, }; let reason = crate::experimental_api::ExperimentalApi::experimental_reason(¶ms); assert_eq!( reason, Some("item/commandExecution/requestApproval.additionalPermissions") ); } }