From cdc3df3790737db9bf738020df07e78b983ef0cb Mon Sep 17 00:00:00 2001 From: Owen Lin Date: Thu, 30 Oct 2025 16:56:55 -0700 Subject: [PATCH] [app-server] refactor: split API types into v1 and v2 (#6005) Makes it easier to figure out which types are defined in the old vs. new API schema. --- codex-rs/app-server-protocol/src/lib.rs | 4 +- codex-rs/app-server-protocol/src/protocol.rs | 1289 ----------------- .../src/protocol/common.rs | 685 +++++++++ .../app-server-protocol/src/protocol/mod.rs | 6 + .../app-server-protocol/src/protocol/v1.rs | 405 ++++++ .../app-server-protocol/src/protocol/v2.rs | 122 ++ 6 files changed, 1221 insertions(+), 1290 deletions(-) delete mode 100644 codex-rs/app-server-protocol/src/protocol.rs create mode 100644 codex-rs/app-server-protocol/src/protocol/common.rs create mode 100644 codex-rs/app-server-protocol/src/protocol/mod.rs create mode 100644 codex-rs/app-server-protocol/src/protocol/v1.rs create mode 100644 codex-rs/app-server-protocol/src/protocol/v2.rs diff --git a/codex-rs/app-server-protocol/src/lib.rs b/codex-rs/app-server-protocol/src/lib.rs index cc2310bc..9c02ea92 100644 --- a/codex-rs/app-server-protocol/src/lib.rs +++ b/codex-rs/app-server-protocol/src/lib.rs @@ -6,4 +6,6 @@ pub use export::generate_json; pub use export::generate_ts; pub use export::generate_types; pub use jsonrpc_lite::*; -pub use protocol::*; +pub use protocol::common::*; +pub use protocol::v1::*; +pub use protocol::v2::*; diff --git a/codex-rs/app-server-protocol/src/protocol.rs b/codex-rs/app-server-protocol/src/protocol.rs deleted file mode 100644 index 163edcfc..00000000 --- a/codex-rs/app-server-protocol/src/protocol.rs +++ /dev/null @@ -1,1289 +0,0 @@ -use std::collections::HashMap; -use std::path::PathBuf; - -use crate::JSONRPCNotification; -use crate::JSONRPCRequest; -use crate::RequestId; -use codex_protocol::ConversationId; -use codex_protocol::account::PlanType; -use codex_protocol::config_types::ForcedLoginMethod; -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::models::ResponseItem; -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::RateLimitSnapshot; -use codex_protocol::protocol::ReviewDecision; -use codex_protocol::protocol::SandboxCommandAssessment; -use codex_protocol::protocol::SandboxPolicy; -use codex_protocol::protocol::TurnAbortReason; -use paste::paste; -use schemars::JsonSchema; -use serde::Deserialize; -use serde::Serialize; -use strum_macros::Display; -use ts_rs::TS; -use uuid::Uuid; - -#[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()) - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Display, JsonSchema, 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, JsonSchema, 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(()) - } - - pub fn export_client_response_schemas( - out_dir: &::std::path::Path, - ) -> ::anyhow::Result<()> { - $( - crate::export::write_json_schema::<$response>(out_dir, stringify!($response))?; - )* - Ok(()) - } - }; -} - -client_request_definitions! { - /// NEW APIs - #[serde(rename = "model/list")] - #[ts(rename = "model/list")] - ListModels { - params: ListModelsParams, - response: ListModelsResponse, - }, - - #[serde(rename = "account/login")] - #[ts(rename = "account/login")] - LoginAccount { - params: LoginAccountParams, - response: LoginAccountResponse, - }, - - #[serde(rename = "account/logout")] - #[ts(rename = "account/logout")] - LogoutAccount { - params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, - response: LogoutAccountResponse, - }, - - #[serde(rename = "account/rateLimits/read")] - #[ts(rename = "account/rateLimits/read")] - GetAccountRateLimits { - params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, - response: GetAccountRateLimitsResponse, - }, - - #[serde(rename = "feedback/upload")] - #[ts(rename = "feedback/upload")] - UploadFeedback { - params: UploadFeedbackParams, - response: UploadFeedbackResponse, - }, - - #[serde(rename = "account/read")] - #[ts(rename = "account/read")] - GetAccount { - params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, - response: GetAccountResponse, - }, - - /// DEPRECATED APIs below - Initialize { - params: InitializeParams, - response: InitializeResponse, - }, - NewConversation { - params: NewConversationParams, - response: NewConversationResponse, - }, - GetConversationSummary { - params: GetConversationSummaryParams, - response: GetConversationSummaryResponse, - }, - /// 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(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)] -#[serde(tag = "type", rename_all = "camelCase")] -#[ts(tag = "type")] -pub enum Account { - #[serde(rename = "apiKey", rename_all = "camelCase")] - #[ts(rename = "apiKey", rename_all = "camelCase")] - ApiKey { api_key: String }, - - #[serde(rename = "chatgpt", rename_all = "camelCase")] - #[ts(rename = "chatgpt", rename_all = "camelCase")] - ChatGpt { - email: Option, - plan_type: PlanType, - }, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct GetAccountResponse { - pub account: Account, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct InitializeParams { - pub client_info: ClientInfo, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct ClientInfo { - pub name: String, - pub title: Option, - pub version: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct InitializeResponse { - pub user_agent: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct NewConversationParams { - /// Optional override for the model name (e.g. "o3", "o4-mini"). - pub model: Option, - - /// Override the model provider to use for this session. - pub model_provider: Option, - - /// Configuration profile from config.toml to specify default options. - pub profile: Option, - - /// Working directory for the session. If relative, it is resolved against - /// the server process's current working directory. - pub cwd: Option, - - /// Approval policy for shell commands generated by the model: - /// `untrusted`, `on-failure`, `on-request`, `never`. - pub approval_policy: Option, - - /// Sandbox mode: `read-only`, `workspace-write`, or `danger-full-access`. - pub sandbox: Option, - - /// Individual config settings that will override what is in - /// CODEX_HOME/config.toml. - pub config: Option>, - - /// The set of instructions to use instead of the default ones. - pub base_instructions: Option, - - /// Developer instructions that will be sent as a `developer` role message. - #[serde(skip_serializing_if = "Option::is_none")] - pub developer_instructions: Option, - - /// Prompt used during conversation compaction. - #[serde(skip_serializing_if = "Option::is_none")] - pub compact_prompt: Option, - - /// Whether to include the apply patch tool in the conversation. - pub include_apply_patch_tool: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct NewConversationResponse { - pub conversation_id: ConversationId, - pub model: String, - /// Note this could be ignored by the model. - pub reasoning_effort: Option, - pub rollout_path: PathBuf, -} - -#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct ResumeConversationResponse { - pub conversation_id: ConversationId, - pub model: String, - pub initial_messages: Option>, - pub rollout_path: PathBuf, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(untagged)] -pub enum GetConversationSummaryParams { - /// Provide the absolute or CODEX_HOME‑relative rollout path directly. - RolloutPath { - #[serde(rename = "rolloutPath")] - rollout_path: PathBuf, - }, - /// Provide a conversation id; the server will locate the rollout using the - /// same logic as `resumeConversation`. There will be extra latency compared to using the rollout path, - /// as the server needs to locate the rollout path first. - ConversationId { - #[serde(rename = "conversationId")] - conversation_id: ConversationId, - }, -} - -#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct GetConversationSummaryResponse { - pub summary: ConversationSummary, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct ListConversationsParams { - /// Optional page size; defaults to a reasonable server-side value. - pub page_size: Option, - /// Opaque pagination cursor returned by a previous call. - pub cursor: Option, - /// Optional model provider filter (matches against session metadata). - /// - None => filter by the server's default model provider - /// - Some([]) => no filtering, include all providers - /// - Some([...]) => only include sessions with one of the specified providers - pub model_providers: Option>, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, 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. - pub timestamp: Option, - /// Model provider recorded for the session (resolved when absent in metadata). - pub model_provider: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, 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. - pub next_cursor: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct ListModelsParams { - /// Optional page size; defaults to a reasonable server-side value. - pub page_size: Option, - /// Opaque pagination cursor returned by a previous call. - pub cursor: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct Model { - pub id: String, - pub model: String, - pub display_name: String, - pub description: String, - pub supported_reasoning_efforts: Vec, - pub default_reasoning_effort: ReasoningEffort, - // Only one model should be marked as default. - pub is_default: bool, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct ReasoningEffortOption { - pub reasoning_effort: ReasoningEffort, - pub description: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct ListModelsResponse { - 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. - pub next_cursor: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct UploadFeedbackParams { - pub classification: String, - pub reason: Option, - pub conversation_id: Option, - pub include_logs: bool, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct UploadFeedbackResponse { - pub thread_id: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(tag = "type")] -#[ts(tag = "type")] -pub enum LoginAccountParams { - #[serde(rename = "apiKey")] - #[ts(rename = "apiKey")] - ApiKey { - #[serde(rename = "apiKey")] - #[ts(rename = "apiKey")] - api_key: String, - }, - #[serde(rename = "chatgpt")] - #[ts(rename = "chatgpt")] - ChatGpt, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct LoginAccountResponse { - /// Only set if the login method is ChatGPT. - #[schemars(with = "String")] - pub login_id: Option, - - /// URL the client should open in a browser to initiate the OAuth flow. - /// Only set if the login method is ChatGPT. - pub auth_url: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct LogoutAccountResponse {} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct ResumeConversationParams { - /// Absolute path to the rollout JSONL file, when explicitly resuming a known rollout. - pub path: Option, - /// If the rollout path is not known, it can be discovered via the conversation id at the cost of extra latency. - pub conversation_id: Option, - /// if the rollout path or conversation id is not known, it can be resumed from given history - pub history: Option>, - /// Optional overrides to apply when spawning the resumed session. - pub overrides: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct AddConversationSubscriptionResponse { - #[schemars(with = "String")] - pub subscription_id: Uuid, -} - -/// The [`ConversationId`] must match the `rollout_path`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct ArchiveConversationParams { - pub conversation_id: ConversationId, - pub rollout_path: PathBuf, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct ArchiveConversationResponse {} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct RemoveConversationSubscriptionResponse {} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct LoginApiKeyParams { - pub api_key: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct LoginApiKeyResponse {} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct LoginChatGptResponse { - #[schemars(with = "String")] - 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, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct GitDiffToRemoteResponse { - pub sha: GitSha, - pub diff: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct CancelLoginChatGptParams { - #[schemars(with = "String")] - pub login_id: Uuid, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct GitDiffToRemoteParams { - pub cwd: PathBuf, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct CancelLoginChatGptResponse {} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct LogoutChatGptParams {} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct LogoutChatGptResponse {} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct GetAuthStatusParams { - /// If true, include the current auth token (if available) in the response. - pub include_token: Option, - /// If true, attempt to refresh the token before returning status. - pub refresh_token: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, 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. - pub cwd: Option, - /// Optional explicit sandbox policy overriding the server default. - pub sandbox_policy: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct ExecOneOffCommandResponse { - pub exit_code: i32, - pub stdout: String, - pub stderr: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct GetAccountRateLimitsResponse { - pub rate_limits: RateLimitSnapshot, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct GetAuthStatusResponse { - pub auth_method: Option, - 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. - pub requires_openai_auth: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct GetUserAgentResponse { - pub user_agent: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, 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, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct GetUserSavedConfigResponse { - pub config: UserSavedConfig, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct SetDefaultModelParams { - /// If set to None, this means `model` should be cleared in config.toml. - pub model: Option, - /// If set to None, this means `model_reasoning_effort` should be cleared - /// in config.toml. - pub reasoning_effort: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, 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, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct UserSavedConfig { - /// Approvals - pub approval_policy: Option, - pub sandbox_mode: Option, - pub sandbox_settings: Option, - - pub forced_chatgpt_workspace_id: Option, - pub forced_login_method: Option, - - /// Model-specific configuration - pub model: Option, - pub model_reasoning_effort: Option, - pub model_reasoning_summary: Option, - pub model_verbosity: Option, - - /// Tools - pub tools: Option, - - /// Profiles - pub profile: Option, - pub profiles: HashMap, -} - -/// MCP representation of a [`codex_core::config_profile::ConfigProfile`]. -#[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, 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, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct Tools { - pub web_search: Option, - pub view_image: Option, -} - -/// MCP representation of a [`codex_core::config::types::SandboxWorkspaceWrite`]. -#[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct SandboxSettings { - #[serde(default)] - pub writable_roots: Vec, - pub network_access: Option, - pub exclude_tmpdir_env_var: Option, - pub exclude_slash_tmp: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct SendUserMessageParams { - pub conversation_id: ConversationId, - pub items: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, 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, - pub effort: Option, - pub summary: ReasoningSummary, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct SendUserTurnResponse {} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct InterruptConversationParams { - pub conversation_id: ConversationId, -} - -#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct InterruptConversationResponse { - pub abort_reason: TurnAbortReason, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct SendUserMessageResponse {} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct AddConversationListenerParams { - pub conversation_id: ConversationId, - #[serde(default)] - pub experimental_raw_events: bool, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct RemoveConversationListenerParams { - #[schemars(with = "String")] - pub subscription_id: Uuid, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, 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, JsonSchema, 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, JsonSchema)] - 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(()) - } - - pub fn export_server_response_schemas( - out_dir: &::std::path::Path, - ) -> ::anyhow::Result<()> { - paste! { - $(crate::export::write_json_schema::<[<$variant Response>]>(out_dir, stringify!([<$variant Response>]))?;)* - } - 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, JsonSchema, 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). - 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). - pub grant_root: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, 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, - pub reason: Option, - pub risk: Option, - pub parsed_cmd: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -pub struct ExecCommandApprovalResponse { - pub decision: ReviewDecision, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -pub struct ApplyPatchApprovalResponse { - pub decision: ReviewDecision, -} - -#[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")] -pub struct LoginChatGptCompleteNotification { - #[schemars(with = "String")] - pub login_id: Uuid, - pub success: bool, - pub error: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, 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. - 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. - pub initial_messages: Option>, - - pub rollout_path: PathBuf, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct AuthStatusChangeNotification { - /// Current authentication method; omitted if signed out. - pub auth_method: Option, -} - -/// Notification sent from the server to the client. -#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS, Display)] -#[serde(tag = "method", content = "params", rename_all = "camelCase")] -#[strum(serialize_all = "camelCase")] -pub enum ServerNotification { - /// NEW NOTIFICATIONS - #[serde(rename = "account/rateLimits/updated")] - #[ts(rename = "account/rateLimits/updated")] - #[strum(serialize = "account/rateLimits/updated")] - AccountRateLimitsUpdated(RateLimitSnapshot), - - /// DEPRECATED NOTIFICATIONS below - /// 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::AccountRateLimitsUpdated(params) => serde_json::to_value(params), - 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, JsonSchema, 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()), - 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-codex", - "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 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()), - risk: None, - 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", - "risk": null, - "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_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_account_login_api_key() -> Result<()> { - let request = ClientRequest::LoginAccount { - request_id: RequestId::Integer(2), - params: LoginAccountParams::ApiKey { - api_key: "secret".to_string(), - }, - }; - assert_eq!( - json!({ - "method": "account/login", - "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: LoginAccountParams::ChatGpt, - }; - assert_eq!( - json!({ - "method": "account/login", - "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_get_account() -> Result<()> { - let request = ClientRequest::GetAccount { - request_id: RequestId::Integer(5), - params: None, - }; - assert_eq!( - json!({ - "method": "account/read", - "id": 5, - }), - serde_json::to_value(&request)?, - ); - Ok(()) - } - - #[test] - fn account_serializes_fields_in_camel_case() -> Result<()> { - let api_key = Account::ApiKey { - api_key: "secret".to_string(), - }; - assert_eq!( - json!({ - "type": "apiKey", - "apiKey": "secret", - }), - serde_json::to_value(&api_key)?, - ); - - let chatgpt = Account::ChatGpt { - email: Some("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::ListModels { - request_id: RequestId::Integer(6), - params: ListModelsParams::default(), - }; - assert_eq!( - json!({ - "method": "model/list", - "id": 6, - "params": { - "pageSize": null, - "cursor": null - } - }), - serde_json::to_value(&request)?, - ); - Ok(()) - } -} diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs new file mode 100644 index 00000000..dcab5eae --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -0,0 +1,685 @@ +use std::collections::HashMap; +use std::path::PathBuf; + +use crate::JSONRPCNotification; +use crate::JSONRPCRequest; +use crate::RequestId; +use crate::protocol::v1; +use crate::protocol::v2; +use codex_protocol::ConversationId; +use codex_protocol::parse_command::ParsedCommand; +use codex_protocol::protocol::FileChange; +use codex_protocol::protocol::RateLimitSnapshot; +use codex_protocol::protocol::ReviewDecision; +use codex_protocol::protocol::SandboxCommandAssessment; +use paste::paste; +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()) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Display, JsonSchema, 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, JsonSchema, 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(()) + } + + pub fn export_client_response_schemas( + out_dir: &::std::path::Path, + ) -> ::anyhow::Result<()> { + $( + crate::export::write_json_schema::<$response>(out_dir, stringify!($response))?; + )* + Ok(()) + } + }; +} + +client_request_definitions! { + /// NEW APIs + #[serde(rename = "model/list")] + #[ts(rename = "model/list")] + ListModels { + params: v2::ListModelsParams, + response: v2::ListModelsResponse, + }, + + #[serde(rename = "account/login")] + #[ts(rename = "account/login")] + LoginAccount { + params: v2::LoginAccountParams, + response: v2::LoginAccountResponse, + }, + + #[serde(rename = "account/logout")] + #[ts(rename = "account/logout")] + LogoutAccount { + params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, + response: v2::LogoutAccountResponse, + }, + + #[serde(rename = "account/rateLimits/read")] + #[ts(rename = "account/rateLimits/read")] + GetAccountRateLimits { + params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, + response: v2::GetAccountRateLimitsResponse, + }, + + #[serde(rename = "feedback/upload")] + #[ts(rename = "feedback/upload")] + UploadFeedback { + params: v2::UploadFeedbackParams, + response: v2::UploadFeedbackResponse, + }, + + #[serde(rename = "account/read")] + #[ts(rename = "account/read")] + GetAccount { + params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, + response: v2::GetAccountResponse, + }, + + /// DEPRECATED APIs below + Initialize { + params: v1::InitializeParams, + response: v1::InitializeResponse, + }, + 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, + }, + 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, + }, + CancelLoginChatGpt { + params: v1::CancelLoginChatGptParams, + response: v1::CancelLoginChatGptResponse, + }, + LogoutChatGpt { + params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>, + response: v1::LogoutChatGptResponse, + }, + 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, + }, + /// 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 + ),* $(,)? + ) => { + paste! { + /// Request initiated from the server and sent to the client. + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, 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, JsonSchema)] + 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(()) + } + + pub fn export_server_response_schemas( + out_dir: &::std::path::Path, + ) -> ::anyhow::Result<()> { + paste! { + $(crate::export::write_json_schema::<[<$variant Response>]>(out_dir, stringify!([<$variant Response>]))?;)* + } + 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, JsonSchema, 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). + 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). + pub grant_root: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, 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, + pub reason: Option, + pub risk: Option, + pub parsed_cmd: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +pub struct ExecCommandApprovalResponse { + pub decision: ReviewDecision, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +pub struct ApplyPatchApprovalResponse { + pub decision: ReviewDecision, +} + +#[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, +} + +/// Notification sent from the server to the client. +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS, Display)] +#[serde(tag = "method", content = "params", rename_all = "camelCase")] +#[strum(serialize_all = "camelCase")] +pub enum ServerNotification { + /// NEW NOTIFICATIONS + #[serde(rename = "account/rateLimits/updated")] + #[ts(rename = "account/rateLimits/updated")] + #[strum(serialize = "account/rateLimits/updated")] + AccountRateLimitsUpdated(RateLimitSnapshot), + + /// DEPRECATED NOTIFICATIONS below + /// Authentication status changed + AuthStatusChange(v1::AuthStatusChangeNotification), + + /// ChatGPT login flow completed + LoginChatGptComplete(v1::LoginChatGptCompleteNotification), + + /// The special session configured event for a new or resumed conversation. + SessionConfigured(v1::SessionConfiguredNotification), +} + +impl ServerNotification { + pub fn to_params(self) -> Result { + match self { + ServerNotification::AccountRateLimitsUpdated(params) => serde_json::to_value(params), + 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, JsonSchema, 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 codex_protocol::account::PlanType; + use codex_protocol::protocol::AskForApproval; + 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: v1::NewConversationParams { + model: Some("gpt-5-codex".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-codex", + "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 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()), + risk: None, + 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", + "risk": null, + "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_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_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", + "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", + "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_get_account() -> Result<()> { + let request = ClientRequest::GetAccount { + request_id: RequestId::Integer(5), + params: None, + }; + assert_eq!( + json!({ + "method": "account/read", + "id": 5, + }), + serde_json::to_value(&request)?, + ); + Ok(()) + } + + #[test] + fn account_serializes_fields_in_camel_case() -> Result<()> { + let api_key = v2::Account::ApiKey { + api_key: "secret".to_string(), + }; + assert_eq!( + json!({ + "type": "apiKey", + "apiKey": "secret", + }), + serde_json::to_value(&api_key)?, + ); + + let chatgpt = v2::Account::ChatGpt { + email: Some("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::ListModels { + request_id: RequestId::Integer(6), + params: v2::ListModelsParams::default(), + }; + assert_eq!( + json!({ + "method": "model/list", + "id": 6, + "params": { + "pageSize": null, + "cursor": null + } + }), + serde_json::to_value(&request)?, + ); + Ok(()) + } +} diff --git a/codex-rs/app-server-protocol/src/protocol/mod.rs b/codex-rs/app-server-protocol/src/protocol/mod.rs new file mode 100644 index 00000000..11edf04c --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/mod.rs @@ -0,0 +1,6 @@ +// Module declarations for the app-server protocol namespace. +// Exposes protocol pieces used by `lib.rs` via `pub use protocol::common::*;`. + +pub mod common; +pub mod v1; +pub mod v2; diff --git a/codex-rs/app-server-protocol/src/protocol/v1.rs b/codex-rs/app-server-protocol/src/protocol/v1.rs new file mode 100644 index 00000000..8fbcf6b7 --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v1.rs @@ -0,0 +1,405 @@ +use std::collections::HashMap; +use std::path::PathBuf; + +use codex_protocol::ConversationId; +use codex_protocol::config_types::ForcedLoginMethod; +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::models::ResponseItem; +use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::EventMsg; +use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::protocol::TurnAbortReason; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use ts_rs::TS; +use uuid::Uuid; + +// Reuse shared types defined in `common.rs`. +use crate::protocol::common::AuthMode; +use crate::protocol::common::GitSha; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct InitializeParams { + pub client_info: ClientInfo, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct ClientInfo { + pub name: String, + pub title: Option, + pub version: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct InitializeResponse { + pub user_agent: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct NewConversationParams { + pub model: Option, + pub model_provider: Option, + pub profile: Option, + pub cwd: Option, + pub approval_policy: Option, + pub sandbox: Option, + pub config: Option>, + pub base_instructions: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub developer_instructions: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub compact_prompt: Option, + pub include_apply_patch_tool: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct NewConversationResponse { + pub conversation_id: ConversationId, + pub model: String, + pub reasoning_effort: Option, + pub rollout_path: PathBuf, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct ResumeConversationResponse { + pub conversation_id: ConversationId, + pub model: String, + pub initial_messages: Option>, + pub rollout_path: PathBuf, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(untagged)] +pub enum GetConversationSummaryParams { + RolloutPath { + #[serde(rename = "rolloutPath")] + rollout_path: PathBuf, + }, + ConversationId { + #[serde(rename = "conversationId")] + conversation_id: ConversationId, + }, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct GetConversationSummaryResponse { + pub summary: ConversationSummary, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct ListConversationsParams { + pub page_size: Option, + pub cursor: Option, + pub model_providers: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct ConversationSummary { + pub conversation_id: ConversationId, + pub path: PathBuf, + pub preview: String, + pub timestamp: Option, + pub model_provider: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct ListConversationsResponse { + pub items: Vec, + pub next_cursor: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct ResumeConversationParams { + pub path: Option, + pub conversation_id: Option, + pub history: Option>, + pub overrides: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct AddConversationSubscriptionResponse { + #[schemars(with = "String")] + pub subscription_id: Uuid, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct ArchiveConversationParams { + pub conversation_id: ConversationId, + pub rollout_path: PathBuf, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct ArchiveConversationResponse {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct RemoveConversationSubscriptionResponse {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct LoginApiKeyParams { + pub api_key: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct LoginApiKeyResponse {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct LoginChatGptResponse { + #[schemars(with = "String")] + pub login_id: Uuid, + pub auth_url: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct GitDiffToRemoteResponse { + pub sha: GitSha, + pub diff: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct CancelLoginChatGptParams { + #[schemars(with = "String")] + pub login_id: Uuid, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct GitDiffToRemoteParams { + pub cwd: PathBuf, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct CancelLoginChatGptResponse {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct LogoutChatGptParams {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct LogoutChatGptResponse {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct GetAuthStatusParams { + pub include_token: Option, + pub refresh_token: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct ExecOneOffCommandParams { + pub command: Vec, + pub timeout_ms: Option, + pub cwd: Option, + pub sandbox_policy: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct ExecOneOffCommandResponse { + pub exit_code: i32, + pub stdout: String, + pub stderr: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct GetAuthStatusResponse { + pub auth_method: Option, + pub auth_token: Option, + pub requires_openai_auth: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct GetUserAgentResponse { + pub user_agent: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct UserInfoResponse { + pub alleged_user_email: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct GetUserSavedConfigResponse { + pub config: UserSavedConfig, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct SetDefaultModelParams { + pub model: Option, + pub reasoning_effort: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct SetDefaultModelResponse {} + +#[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct UserSavedConfig { + pub approval_policy: Option, + pub sandbox_mode: Option, + pub sandbox_settings: Option, + pub forced_chatgpt_workspace_id: Option, + pub forced_login_method: Option, + pub model: Option, + pub model_reasoning_effort: Option, + pub model_reasoning_summary: Option, + pub model_verbosity: Option, + pub tools: Option, + pub profile: Option, + pub profiles: HashMap, +} + +#[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct Profile { + pub model: Option, + 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, +} + +#[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct Tools { + pub web_search: Option, + pub view_image: Option, +} + +#[derive(Deserialize, Debug, Clone, PartialEq, Serialize, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct SandboxSettings { + #[serde(default)] + pub writable_roots: Vec, + pub network_access: Option, + pub exclude_tmpdir_env_var: Option, + pub exclude_slash_tmp: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct SendUserMessageParams { + pub conversation_id: ConversationId, + pub items: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, 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, + pub effort: Option, + pub summary: ReasoningSummary, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct SendUserTurnResponse {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct InterruptConversationParams { + pub conversation_id: ConversationId, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct InterruptConversationResponse { + pub abort_reason: TurnAbortReason, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct SendUserMessageResponse {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct AddConversationListenerParams { + pub conversation_id: ConversationId, + #[serde(default)] + pub experimental_raw_events: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct RemoveConversationListenerParams { + #[schemars(with = "String")] + pub subscription_id: Uuid, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "type", content = "data")] +pub enum InputItem { + Text { text: String }, + Image { image_url: String }, + LocalImage { path: PathBuf }, +} + +// Deprecated notifications (v1) + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct LoginChatGptCompleteNotification { + #[schemars(with = "String")] + pub login_id: Uuid, + pub success: bool, + pub error: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct SessionConfiguredNotification { + pub session_id: ConversationId, + pub model: String, + pub reasoning_effort: Option, + pub history_log_id: u64, + #[ts(type = "number")] + pub history_entry_count: usize, + pub initial_messages: Option>, + pub rollout_path: PathBuf, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct AuthStatusChangeNotification { + pub auth_method: Option, +} diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs new file mode 100644 index 00000000..ccd89e6a --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -0,0 +1,122 @@ +use codex_protocol::ConversationId; +use codex_protocol::account::PlanType; +use codex_protocol::config_types::ReasoningEffort; +use codex_protocol::protocol::RateLimitSnapshot; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use ts_rs::TS; +use uuid::Uuid; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type")] +pub enum Account { + #[serde(rename = "apiKey", rename_all = "camelCase")] + #[ts(rename = "apiKey", rename_all = "camelCase")] + ApiKey { api_key: String }, + + #[serde(rename = "chatgpt", rename_all = "camelCase")] + #[ts(rename = "chatgpt", rename_all = "camelCase")] + ChatGpt { + email: Option, + plan_type: PlanType, + }, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(tag = "type")] +#[ts(tag = "type")] +pub enum LoginAccountParams { + #[serde(rename = "apiKey")] + #[ts(rename = "apiKey")] + ApiKey { + #[serde(rename = "apiKey")] + #[ts(rename = "apiKey")] + api_key: String, + }, + #[serde(rename = "chatgpt")] + #[ts(rename = "chatgpt")] + ChatGpt, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct LoginAccountResponse { + /// Only set if the login method is ChatGPT. + #[schemars(with = "String")] + pub login_id: Option, + + /// URL the client should open in a browser to initiate the OAuth flow. + /// Only set if the login method is ChatGPT. + pub auth_url: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct LogoutAccountResponse {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct GetAccountRateLimitsResponse { + pub rate_limits: RateLimitSnapshot, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct GetAccountResponse { + pub account: Account, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct ListModelsParams { + /// Optional page size; defaults to a reasonable server-side value. + pub page_size: Option, + /// Opaque pagination cursor returned by a previous call. + pub cursor: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct Model { + pub id: String, + pub model: String, + pub display_name: String, + pub description: String, + pub supported_reasoning_efforts: Vec, + pub default_reasoning_effort: ReasoningEffort, + // Only one model should be marked as default. + pub is_default: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct ReasoningEffortOption { + pub reasoning_effort: ReasoningEffort, + pub description: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct ListModelsResponse { + 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. + pub next_cursor: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct UploadFeedbackParams { + pub classification: String, + pub reason: Option, + pub conversation_id: Option, + pub include_logs: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct UploadFeedbackResponse { + pub thread_id: String, +}