diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 046bab211c..ae54b610f5 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -6074,4 +6074,4 @@ } ], "title": "ServerNotification" -} +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index dafa256aec..8e0e2c39b4 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -18553,4 +18553,4 @@ }, "title": "CodexAppServerProtocol", "type": "object" -} +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index f028f9b3ea..67aaf9e389 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -16438,4 +16438,4 @@ }, "title": "CodexAppServerProtocolV2", "type": "object" -} +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json index 4ec0e10bc9..6909415c2a 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json @@ -1393,4 +1393,4 @@ ], "title": "ItemCompletedNotification", "type": "object" -} +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json index 8684989356..758ceba32d 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json @@ -1393,4 +1393,4 @@ ], "title": "ItemStartedNotification", "type": "object" -} +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs deleted file mode 100644 index 3328cd2516..0000000000 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ /dev/null @@ -1,11953 +0,0 @@ -use std::collections::BTreeMap; -use std::collections::HashMap; -use std::num::NonZeroUsize; -use std::path::PathBuf; - -use crate::RequestId; -use crate::protocol::common::AuthMode; -use crate::protocol::item_builders::convert_patch_changes; -use codex_experimental_api_macros::ExperimentalApi; -use codex_protocol::account::PlanType; -use codex_protocol::account::ProviderAccount; -use codex_protocol::approvals::ElicitationRequest as CoreElicitationRequest; -use codex_protocol::approvals::ExecPolicyAmendment as CoreExecPolicyAmendment; -use codex_protocol::approvals::GuardianAssessmentAction as CoreGuardianAssessmentAction; -use codex_protocol::approvals::GuardianAssessmentDecisionSource as CoreGuardianAssessmentDecisionSource; -use codex_protocol::approvals::GuardianCommandSource as CoreGuardianCommandSource; -use codex_protocol::approvals::NetworkApprovalContext as CoreNetworkApprovalContext; -use codex_protocol::approvals::NetworkApprovalProtocol as CoreNetworkApprovalProtocol; -use codex_protocol::approvals::NetworkPolicyAmendment as CoreNetworkPolicyAmendment; -use codex_protocol::approvals::NetworkPolicyRuleAction as CoreNetworkPolicyRuleAction; -use codex_protocol::config_types::ApprovalsReviewer as CoreApprovalsReviewer; -use codex_protocol::config_types::CollaborationMode; -use codex_protocol::config_types::CollaborationModeMask as CoreCollaborationModeMask; -use codex_protocol::config_types::ForcedLoginMethod; -use codex_protocol::config_types::ModeKind; -use codex_protocol::config_types::Personality; -use codex_protocol::config_types::ReasoningSummary; -use codex_protocol::config_types::SandboxMode as CoreSandboxMode; -use codex_protocol::config_types::ServiceTier; -use codex_protocol::config_types::Verbosity; -use codex_protocol::config_types::WebSearchMode; -use codex_protocol::config_types::WebSearchToolConfig; -use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent; -use codex_protocol::items::McpToolCallError as CoreMcpToolCallError; -use codex_protocol::items::McpToolCallStatus as CoreMcpToolCallStatus; -use codex_protocol::items::TurnItem as CoreTurnItem; -use codex_protocol::mcp::CallToolResult as CoreMcpCallToolResult; -use codex_protocol::mcp::Resource as McpResource; -pub use codex_protocol::mcp::ResourceContent as McpResourceContent; -use codex_protocol::mcp::ResourceTemplate as McpResourceTemplate; -use codex_protocol::mcp::Tool as McpTool; -use codex_protocol::memory_citation::MemoryCitation as CoreMemoryCitation; -use codex_protocol::memory_citation::MemoryCitationEntry as CoreMemoryCitationEntry; -use codex_protocol::models::ActivePermissionProfile as CoreActivePermissionProfile; -use codex_protocol::models::ActivePermissionProfileModification as CoreActivePermissionProfileModification; -use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile; -use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions; -use codex_protocol::models::ManagedFileSystemPermissions as CoreManagedFileSystemPermissions; -use codex_protocol::models::MessagePhase; -use codex_protocol::models::NetworkPermissions as CoreNetworkPermissions; -use codex_protocol::models::PermissionProfile as CorePermissionProfile; -use codex_protocol::models::ResponseItem; -use codex_protocol::openai_models::InputModality; -use codex_protocol::openai_models::ModelAvailabilityNux as CoreModelAvailabilityNux; -use codex_protocol::openai_models::ReasoningEffort; -use codex_protocol::openai_models::default_input_modalities; -use codex_protocol::parse_command::ParsedCommand as CoreParsedCommand; -use codex_protocol::permissions::FileSystemAccessMode as CoreFileSystemAccessMode; -use codex_protocol::permissions::FileSystemPath as CoreFileSystemPath; -use codex_protocol::permissions::FileSystemSandboxEntry as CoreFileSystemSandboxEntry; -use codex_protocol::permissions::FileSystemSpecialPath as CoreFileSystemSpecialPath; -use codex_protocol::permissions::NetworkSandboxPolicy as CoreNetworkSandboxPolicy; -use codex_protocol::plan_tool::PlanItemArg as CorePlanItemArg; -use codex_protocol::plan_tool::StepStatus as CorePlanStepStatus; -use codex_protocol::protocol::AgentStatus as CoreAgentStatus; -use codex_protocol::protocol::AskForApproval as CoreAskForApproval; -use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo; -use codex_protocol::protocol::CreditsSnapshot as CoreCreditsSnapshot; -use codex_protocol::protocol::ExecCommandSource as CoreExecCommandSource; -use codex_protocol::protocol::ExecCommandStatus as CoreExecCommandStatus; -use codex_protocol::protocol::GranularApprovalConfig as CoreGranularApprovalConfig; -use codex_protocol::protocol::GuardianRiskLevel as CoreGuardianRiskLevel; -use codex_protocol::protocol::GuardianUserAuthorization as CoreGuardianUserAuthorization; -use codex_protocol::protocol::HookEventName as CoreHookEventName; -use codex_protocol::protocol::HookExecutionMode as CoreHookExecutionMode; -use codex_protocol::protocol::HookHandlerType as CoreHookHandlerType; -use codex_protocol::protocol::HookOutputEntry as CoreHookOutputEntry; -use codex_protocol::protocol::HookOutputEntryKind as CoreHookOutputEntryKind; -use codex_protocol::protocol::HookRunStatus as CoreHookRunStatus; -use codex_protocol::protocol::HookRunSummary as CoreHookRunSummary; -use codex_protocol::protocol::HookScope as CoreHookScope; -use codex_protocol::protocol::HookSource as CoreHookSource; -use codex_protocol::protocol::HookTrustStatus as CoreHookTrustStatus; -use codex_protocol::protocol::ModelRerouteReason as CoreModelRerouteReason; -use codex_protocol::protocol::ModelVerification as CoreModelVerification; -use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess; -use codex_protocol::protocol::NonSteerableTurnKind as CoreNonSteerableTurnKind; -use codex_protocol::protocol::PatchApplyStatus as CorePatchApplyStatus; -use codex_protocol::protocol::RateLimitReachedType as CoreRateLimitReachedType; -use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot; -use codex_protocol::protocol::RateLimitWindow as CoreRateLimitWindow; -use codex_protocol::protocol::RealtimeAudioFrame as CoreRealtimeAudioFrame; -use codex_protocol::protocol::RealtimeConversationVersion; -use codex_protocol::protocol::RealtimeOutputModality; -use codex_protocol::protocol::RealtimeVoice; -use codex_protocol::protocol::RealtimeVoicesList; -use codex_protocol::protocol::ReviewDecision as CoreReviewDecision; -use codex_protocol::protocol::SessionSource as CoreSessionSource; -use codex_protocol::protocol::SkillDependencies as CoreSkillDependencies; -use codex_protocol::protocol::SkillInterface as CoreSkillInterface; -use codex_protocol::protocol::SkillMetadata as CoreSkillMetadata; -use codex_protocol::protocol::SkillScope as CoreSkillScope; -use codex_protocol::protocol::SkillToolDependency as CoreSkillToolDependency; -use codex_protocol::protocol::SubAgentSource as CoreSubAgentSource; -use codex_protocol::protocol::ThreadGoalStatus as CoreThreadGoalStatus; -use codex_protocol::protocol::TokenUsage as CoreTokenUsage; -use codex_protocol::protocol::TokenUsageInfo as CoreTokenUsageInfo; -use codex_protocol::request_permissions::PermissionGrantScope as CorePermissionGrantScope; -use codex_protocol::request_permissions::RequestPermissionProfile as CoreRequestPermissionProfile; -use codex_protocol::user_input::ByteRange as CoreByteRange; -use codex_protocol::user_input::TextElement as CoreTextElement; -use codex_protocol::user_input::UserInput as CoreUserInput; -use codex_utils_absolute_path::AbsolutePathBuf; -use schemars::JsonSchema; -use schemars::r#gen::SchemaGenerator; -use schemars::schema::InstanceType; -use schemars::schema::Metadata; -use schemars::schema::Schema; -use schemars::schema::SchemaObject; -use serde::Deserialize; -use serde::Serialize; -use serde_json::Value as JsonValue; -use serde_with::serde_as; -use thiserror::Error; -use ts_rs::TS; - -// Macro to declare a camelCased API v2 enum mirroring a core enum which -// tends to use either snake_case or kebab-case. -macro_rules! v2_enum_from_core { - ( - $(#[$enum_meta:meta])* - pub enum $Name:ident from $Src:path { - $( $(#[$variant_meta:meta])* $Variant:ident ),+ $(,)? - } - ) => { - #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] - $(#[$enum_meta])* - #[serde(rename_all = "camelCase")] - #[ts(export_to = "v2/")] - pub enum $Name { - $( $(#[$variant_meta])* $Variant ),+ - } - - impl $Name { - pub fn to_core(self) -> $Src { - match self { $( $Name::$Variant => <$Src>::$Variant ),+ } - } - } - - impl From<$Src> for $Name { - fn from(value: $Src) -> Self { - match value { $( <$Src>::$Variant => $Name::$Variant ),+ } - } - } - }; -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum NonSteerableTurnKind { - Review, - Compact, -} - -/// This translation layer make sure that we expose codex error code in camel case. -/// -/// When an upstream HTTP status is available (for example, from the Responses API or a provider), -/// it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum CodexErrorInfo { - ContextWindowExceeded, - UsageLimitExceeded, - ServerOverloaded, - CyberPolicy, - HttpConnectionFailed { - #[serde(rename = "httpStatusCode")] - #[ts(rename = "httpStatusCode")] - http_status_code: Option, - }, - /// Failed to connect to the response SSE stream. - ResponseStreamConnectionFailed { - #[serde(rename = "httpStatusCode")] - #[ts(rename = "httpStatusCode")] - http_status_code: Option, - }, - InternalServerError, - Unauthorized, - BadRequest, - ThreadRollbackFailed, - SandboxError, - /// The response SSE stream disconnected in the middle of a turn before completion. - ResponseStreamDisconnected { - #[serde(rename = "httpStatusCode")] - #[ts(rename = "httpStatusCode")] - http_status_code: Option, - }, - /// Reached the retry limit for responses. - ResponseTooManyFailedAttempts { - #[serde(rename = "httpStatusCode")] - #[ts(rename = "httpStatusCode")] - http_status_code: Option, - }, - /// Returned when `turn/start` or `turn/steer` is submitted while the current active turn - /// cannot accept same-turn steering, for example `/review` or manual `/compact`. - ActiveTurnNotSteerable { - #[serde(rename = "turnKind")] - #[ts(rename = "turnKind")] - turn_kind: NonSteerableTurnKind, - }, - Other, -} - -impl From for CodexErrorInfo { - fn from(value: CoreCodexErrorInfo) -> Self { - match value { - CoreCodexErrorInfo::ContextWindowExceeded => CodexErrorInfo::ContextWindowExceeded, - CoreCodexErrorInfo::UsageLimitExceeded => CodexErrorInfo::UsageLimitExceeded, - CoreCodexErrorInfo::ServerOverloaded => CodexErrorInfo::ServerOverloaded, - CoreCodexErrorInfo::CyberPolicy => CodexErrorInfo::CyberPolicy, - CoreCodexErrorInfo::HttpConnectionFailed { http_status_code } => { - CodexErrorInfo::HttpConnectionFailed { http_status_code } - } - CoreCodexErrorInfo::ResponseStreamConnectionFailed { http_status_code } => { - CodexErrorInfo::ResponseStreamConnectionFailed { http_status_code } - } - CoreCodexErrorInfo::InternalServerError => CodexErrorInfo::InternalServerError, - CoreCodexErrorInfo::Unauthorized => CodexErrorInfo::Unauthorized, - CoreCodexErrorInfo::BadRequest => CodexErrorInfo::BadRequest, - CoreCodexErrorInfo::ThreadRollbackFailed => CodexErrorInfo::ThreadRollbackFailed, - CoreCodexErrorInfo::SandboxError => CodexErrorInfo::SandboxError, - CoreCodexErrorInfo::ResponseStreamDisconnected { http_status_code } => { - CodexErrorInfo::ResponseStreamDisconnected { http_status_code } - } - CoreCodexErrorInfo::ResponseTooManyFailedAttempts { http_status_code } => { - CodexErrorInfo::ResponseTooManyFailedAttempts { http_status_code } - } - CoreCodexErrorInfo::ActiveTurnNotSteerable { turn_kind } => { - CodexErrorInfo::ActiveTurnNotSteerable { - turn_kind: turn_kind.into(), - } - } - CoreCodexErrorInfo::Other => CodexErrorInfo::Other, - } - } -} - -impl From for NonSteerableTurnKind { - fn from(value: CoreNonSteerableTurnKind) -> Self { - match value { - CoreNonSteerableTurnKind::Review => Self::Review, - CoreNonSteerableTurnKind::Compact => Self::Compact, - } - } -} - -#[derive( - Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS, ExperimentalApi, -)] -#[serde(rename_all = "kebab-case")] -#[ts(rename_all = "kebab-case", export_to = "v2/")] -pub enum AskForApproval { - #[serde(rename = "untrusted")] - #[ts(rename = "untrusted")] - UnlessTrusted, - OnFailure, - OnRequest, - #[experimental("askForApproval.granular")] - Granular { - sandbox_approval: bool, - rules: bool, - #[serde(default)] - skill_approval: bool, - #[serde(default)] - request_permissions: bool, - mcp_elicitations: bool, - }, - Never, -} - -impl AskForApproval { - pub fn to_core(self) -> CoreAskForApproval { - match self { - AskForApproval::UnlessTrusted => CoreAskForApproval::UnlessTrusted, - AskForApproval::OnFailure => CoreAskForApproval::OnFailure, - AskForApproval::OnRequest => CoreAskForApproval::OnRequest, - AskForApproval::Granular { - sandbox_approval, - rules, - skill_approval, - request_permissions, - mcp_elicitations, - } => CoreAskForApproval::Granular(CoreGranularApprovalConfig { - sandbox_approval, - rules, - skill_approval, - request_permissions, - mcp_elicitations, - }), - AskForApproval::Never => CoreAskForApproval::Never, - } - } -} - -impl From for AskForApproval { - fn from(value: CoreAskForApproval) -> Self { - match value { - CoreAskForApproval::UnlessTrusted => AskForApproval::UnlessTrusted, - CoreAskForApproval::OnFailure => AskForApproval::OnFailure, - CoreAskForApproval::OnRequest => AskForApproval::OnRequest, - CoreAskForApproval::Granular(granular_config) => AskForApproval::Granular { - sandbox_approval: granular_config.sandbox_approval, - rules: granular_config.rules, - skill_approval: granular_config.skill_approval, - request_permissions: granular_config.request_permissions, - mcp_elicitations: granular_config.mcp_elicitations, - }, - CoreAskForApproval::Never => AskForApproval::Never, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, TS)] -#[ts( - type = r#""user" | "auto_review" | "guardian_subagent""#, - export_to = "v2/" -)] -/// Configures who approval requests are routed to for review. Examples -/// include sandbox escapes, blocked network access, MCP approval prompts, and -/// ARC escalations. Defaults to `user`. `auto_review` uses a carefully -/// prompted subagent to gather relevant context and apply a risk-based -/// decision framework before approving or denying the request. -pub enum ApprovalsReviewer { - #[serde(rename = "user")] - User, - #[serde(rename = "guardian_subagent", alias = "auto_review")] - AutoReview, -} - -impl JsonSchema for ApprovalsReviewer { - fn schema_name() -> String { - "ApprovalsReviewer".to_string() - } - - fn json_schema(_generator: &mut SchemaGenerator) -> Schema { - string_enum_schema_with_description( - &["user", "auto_review", "guardian_subagent"], - "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.", - ) - } -} - -fn string_enum_schema_with_description(values: &[&str], description: &str) -> Schema { - let mut schema = SchemaObject { - instance_type: Some(InstanceType::String.into()), - metadata: Some(Box::new(Metadata { - description: Some(description.to_string()), - ..Default::default() - })), - ..Default::default() - }; - schema.enum_values = Some( - values - .iter() - .map(|value| JsonValue::String((*value).to_string())) - .collect(), - ); - Schema::Object(schema) -} - -impl ApprovalsReviewer { - pub fn to_core(self) -> CoreApprovalsReviewer { - match self { - ApprovalsReviewer::User => CoreApprovalsReviewer::User, - ApprovalsReviewer::AutoReview => CoreApprovalsReviewer::AutoReview, - } - } -} - -impl From for ApprovalsReviewer { - fn from(value: CoreApprovalsReviewer) -> Self { - match value { - CoreApprovalsReviewer::User => ApprovalsReviewer::User, - CoreApprovalsReviewer::AutoReview => ApprovalsReviewer::AutoReview, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "kebab-case")] -#[ts(rename_all = "kebab-case", export_to = "v2/")] -pub enum SandboxMode { - ReadOnly, - WorkspaceWrite, - DangerFullAccess, -} - -impl SandboxMode { - pub fn to_core(self) -> CoreSandboxMode { - match self { - SandboxMode::ReadOnly => CoreSandboxMode::ReadOnly, - SandboxMode::WorkspaceWrite => CoreSandboxMode::WorkspaceWrite, - SandboxMode::DangerFullAccess => CoreSandboxMode::DangerFullAccess, - } - } -} - -impl From for SandboxMode { - fn from(value: CoreSandboxMode) -> Self { - match value { - CoreSandboxMode::ReadOnly => SandboxMode::ReadOnly, - CoreSandboxMode::WorkspaceWrite => SandboxMode::WorkspaceWrite, - CoreSandboxMode::DangerFullAccess => SandboxMode::DangerFullAccess, - } - } -} - -v2_enum_from_core!( - pub enum ReviewDelivery from codex_protocol::protocol::ReviewDelivery { - Inline, Detached - } -); - -v2_enum_from_core!( - pub enum McpAuthStatus from codex_protocol::protocol::McpAuthStatus { - Unsupported, - NotLoggedIn, - BearerToken, - OAuth - } -); - -v2_enum_from_core!( - pub enum ModelRerouteReason from CoreModelRerouteReason { - HighRiskCyberActivity - } -); - -v2_enum_from_core!( - pub enum ModelVerification from CoreModelVerification { - TrustedAccessForCyber - } -); - -v2_enum_from_core!( - pub enum HookEventName from CoreHookEventName { - PreToolUse, PermissionRequest, PostToolUse, SessionStart, UserPromptSubmit, Stop - } -); - -v2_enum_from_core!( - pub enum HookHandlerType from CoreHookHandlerType { - Command, Prompt, Agent - } -); - -v2_enum_from_core!( - pub enum HookExecutionMode from CoreHookExecutionMode { - Sync, Async - } -); - -v2_enum_from_core!( - pub enum HookScope from CoreHookScope { - Thread, Turn - } -); - -v2_enum_from_core!( - pub enum HookSource from CoreHookSource { - System, - User, - Project, - Mdm, - SessionFlags, - Plugin, - CloudRequirements, - LegacyManagedConfigFile, - LegacyManagedConfigMdm, - Unknown, - } -); - -v2_enum_from_core!( - pub enum HookTrustStatus from CoreHookTrustStatus { - Managed, Untrusted, Trusted, Modified - } -); - -fn default_hook_source() -> HookSource { - HookSource::Unknown -} - -v2_enum_from_core!( - pub enum HookRunStatus from CoreHookRunStatus { - Running, Completed, Failed, Blocked, Stopped - } -); - -v2_enum_from_core!( - pub enum HookOutputEntryKind from CoreHookOutputEntryKind { - Warning, Stop, Feedback, Context, Error - } -); - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(rename_all = "camelCase", export_to = "v2/")] -pub enum ThreadStartSource { - Startup, - Clear, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct HookOutputEntry { - pub kind: HookOutputEntryKind, - pub text: String, -} - -impl From for HookOutputEntry { - fn from(value: CoreHookOutputEntry) -> Self { - Self { - kind: value.kind.into(), - text: value.text, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct HookRunSummary { - pub id: String, - pub event_name: HookEventName, - pub handler_type: HookHandlerType, - pub execution_mode: HookExecutionMode, - pub scope: HookScope, - pub source_path: AbsolutePathBuf, - #[serde(default = "default_hook_source")] - pub source: HookSource, - pub display_order: i64, - pub status: HookRunStatus, - pub status_message: Option, - pub started_at: i64, - pub completed_at: Option, - pub duration_ms: Option, - pub entries: Vec, -} - -impl From for HookRunSummary { - fn from(value: CoreHookRunSummary) -> Self { - Self { - id: value.id, - event_name: value.event_name.into(), - handler_type: value.handler_type.into(), - execution_mode: value.execution_mode.into(), - scope: value.scope.into(), - source_path: value.source_path, - source: value.source.into(), - display_order: value.display_order, - status: value.status.into(), - status_message: value.status_message, - started_at: value.started_at, - completed_at: value.completed_at, - duration_ms: value.duration_ms, - entries: value.entries.into_iter().map(Into::into).collect(), - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(tag = "type", rename_all = "camelCase")] -#[ts(tag = "type")] -#[ts(export_to = "v2/")] -pub enum ConfigLayerSource { - /// Managed preferences layer delivered by MDM (macOS only). - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - Mdm { - domain: String, - key: String, - }, - - /// Managed config layer from a file (usually `managed_config.toml`). - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - System { - /// This is the path to the system config.toml file, though it is not - /// guaranteed to exist. - file: AbsolutePathBuf, - }, - - /// User config layer from $CODEX_HOME/config.toml. This layer is special - /// in that it is expected to be: - /// - writable by the user - /// - generally outside the workspace directory - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - User { - /// This is the path to the user's config.toml file, though it is not - /// guaranteed to exist. - file: AbsolutePathBuf, - }, - - /// Path to a .codex/ folder within a project. There could be multiple of - /// these between `cwd` and the project/repo root. - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - Project { - dot_codex_folder: AbsolutePathBuf, - }, - - /// Session-layer overrides supplied via `-c`/`--config`. - SessionFlags, - - /// `managed_config.toml` was designed to be a config that was loaded - /// as the last layer on top of everything else. This scheme did not quite - /// work out as intended, but we keep this variant as a "best effort" while - /// we phase out `managed_config.toml` in favor of `requirements.toml`. - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - LegacyManagedConfigTomlFromFile { - file: AbsolutePathBuf, - }, - - LegacyManagedConfigTomlFromMdm, -} - -impl ConfigLayerSource { - /// A settings from a layer with a higher precedence will override a setting - /// from a layer with a lower precedence. - pub fn precedence(&self) -> i16 { - match self { - ConfigLayerSource::Mdm { .. } => 0, - ConfigLayerSource::System { .. } => 10, - ConfigLayerSource::User { .. } => 20, - ConfigLayerSource::Project { .. } => 25, - ConfigLayerSource::SessionFlags => 30, - ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => 40, - ConfigLayerSource::LegacyManagedConfigTomlFromMdm => 50, - } - } -} - -/// Compares [ConfigLayerSource] by precedence, so `A < B` means settings from -/// layer `A` will be overridden by settings from layer `B`. -impl PartialOrd for ConfigLayerSource { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.precedence().cmp(&other.precedence())) - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] -#[ts(export_to = "v2/")] -pub struct SandboxWorkspaceWrite { - #[serde(default)] - pub writable_roots: Vec, - #[serde(default)] - pub network_access: bool, - #[serde(default)] - pub exclude_tmpdir_env_var: bool, - #[serde(default)] - pub exclude_slash_tmp: bool, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] -#[ts(export_to = "v2/")] -pub struct ToolsV2 { - pub web_search: Option, - pub view_image: Option, -} - -#[derive(Serialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct DynamicToolSpec { - #[ts(optional)] - pub namespace: Option, - pub name: String, - pub description: String, - pub input_schema: JsonValue, - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub defer_loading: bool, -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct DynamicToolSpecDe { - namespace: Option, - name: String, - description: String, - input_schema: JsonValue, - defer_loading: Option, - expose_to_context: Option, -} - -impl<'de> Deserialize<'de> for DynamicToolSpec { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let DynamicToolSpecDe { - namespace, - name, - description, - input_schema, - defer_loading, - expose_to_context, - } = DynamicToolSpecDe::deserialize(deserializer)?; - - Ok(Self { - namespace, - name, - description, - input_schema, - defer_loading: defer_loading - .unwrap_or_else(|| expose_to_context.map(|visible| !visible).unwrap_or(false)), - }) - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] -#[serde(rename_all = "snake_case")] -#[ts(export_to = "v2/")] -pub struct ProfileV2 { - pub model: Option, - pub model_provider: Option, - #[experimental(nested)] - pub approval_policy: Option, - /// [UNSTABLE] Optional profile-level override for where approval requests - /// are routed for review. If omitted, the enclosing config default is - /// used. - #[experimental("config/read.approvalsReviewer")] - pub approvals_reviewer: Option, - pub service_tier: Option, - pub model_reasoning_effort: Option, - pub model_reasoning_summary: Option, - pub model_verbosity: Option, - pub web_search: Option, - pub tools: Option, - pub chatgpt_base_url: Option, - #[serde(default, flatten)] - pub additional: HashMap, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] -#[ts(export_to = "v2/")] -pub struct AnalyticsConfig { - pub enabled: Option, - #[serde(default, flatten)] - pub additional: HashMap, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] -#[ts(export_to = "v2/")] -pub enum AppToolApproval { - Auto, - Prompt, - Approve, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] -#[ts(export_to = "v2/")] -pub struct AppsDefaultConfig { - #[serde(default = "default_enabled")] - pub enabled: bool, - #[serde(default = "default_enabled")] - pub destructive_enabled: bool, - #[serde(default = "default_enabled")] - pub open_world_enabled: bool, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] -#[ts(export_to = "v2/")] -pub struct AppToolConfig { - pub enabled: Option, - pub approval_mode: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] -#[ts(export_to = "v2/")] -pub struct AppToolsConfig { - #[serde(default, flatten)] - pub tools: HashMap, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] -#[ts(export_to = "v2/")] -pub struct AppConfig { - #[serde(default = "default_enabled")] - pub enabled: bool, - pub destructive_enabled: Option, - pub open_world_enabled: Option, - pub default_tools_approval_mode: Option, - pub default_tools_enabled: Option, - pub tools: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] -#[ts(export_to = "v2/")] -pub struct AppsConfig { - #[serde(default, rename = "_default")] - pub default: Option, - #[serde(default, flatten)] - pub apps: HashMap, -} - -const fn default_enabled() -> bool { - true -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] -#[serde(rename_all = "snake_case")] -#[ts(export_to = "v2/")] -pub struct Config { - pub model: Option, - pub review_model: Option, - pub model_context_window: Option, - pub model_auto_compact_token_limit: Option, - pub model_provider: Option, - #[experimental(nested)] - pub approval_policy: Option, - /// [UNSTABLE] Optional default for where approval requests are routed for - /// review. - #[experimental("config/read.approvalsReviewer")] - pub approvals_reviewer: Option, - pub sandbox_mode: Option, - pub sandbox_workspace_write: Option, - pub forced_chatgpt_workspace_id: Option, - pub forced_login_method: Option, - pub web_search: Option, - pub tools: Option, - pub profile: Option, - #[experimental(nested)] - #[serde(default)] - pub profiles: HashMap, - pub instructions: Option, - pub developer_instructions: Option, - pub compact_prompt: Option, - pub model_reasoning_effort: Option, - pub model_reasoning_summary: Option, - pub model_verbosity: Option, - pub service_tier: Option, - pub analytics: Option, - #[experimental("config/read.apps")] - #[serde(default)] - pub apps: Option, - #[serde(default, flatten)] - pub additional: HashMap, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ConfigLayerMetadata { - pub name: ConfigLayerSource, - pub version: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ConfigLayer { - pub name: ConfigLayerSource, - pub version: String, - pub config: JsonValue, - #[serde(skip_serializing_if = "Option::is_none")] - pub disabled_reason: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum MergeStrategy { - Replace, - Upsert, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum WriteStatus { - Ok, - OkOverridden, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct OverriddenMetadata { - pub message: String, - pub overriding_layer: ConfigLayerMetadata, - pub effective_value: JsonValue, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ConfigWriteResponse { - pub status: WriteStatus, - pub version: String, - /// Canonical path to the config file that was written. - pub file_path: AbsolutePathBuf, - pub overridden_metadata: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum ConfigWriteErrorCode { - ConfigLayerReadonly, - ConfigVersionConflict, - ConfigValidationError, - ConfigPathNotFound, - ConfigSchemaUnknownKey, - UserLayerNotFound, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ConfigReadParams { - #[serde(default)] - pub include_layers: bool, - /// Optional working directory to resolve project config layers. If specified, - /// return the effective config as seen from that directory (i.e., including any - /// project layers between `cwd` and the project/repo root). - #[ts(optional = nullable)] - pub cwd: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ConfigReadResponse { - #[experimental(nested)] - pub config: Config, - pub origins: HashMap, - #[serde(skip_serializing_if = "Option::is_none")] - pub layers: Option>, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ConfigRequirements { - #[experimental(nested)] - pub allowed_approval_policies: Option>, - #[experimental("configRequirements/read.allowedApprovalsReviewers")] - pub allowed_approvals_reviewers: Option>, - pub allowed_sandbox_modes: Option>, - pub allowed_web_search_modes: Option>, - pub feature_requirements: Option>, - #[experimental("configRequirements/read.hooks")] - pub hooks: Option, - pub enforce_residency: Option, - #[experimental("configRequirements/read.network")] - pub network: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ManagedHooksRequirements { - pub managed_dir: Option, - pub windows_managed_dir: Option, - #[serde(rename = "PreToolUse")] - #[ts(rename = "PreToolUse")] - pub pre_tool_use: Vec, - #[serde(rename = "PermissionRequest")] - #[ts(rename = "PermissionRequest")] - pub permission_request: Vec, - #[serde(rename = "PostToolUse")] - #[ts(rename = "PostToolUse")] - pub post_tool_use: Vec, - #[serde(rename = "SessionStart")] - #[ts(rename = "SessionStart")] - pub session_start: Vec, - #[serde(rename = "UserPromptSubmit")] - #[ts(rename = "UserPromptSubmit")] - pub user_prompt_submit: Vec, - #[serde(rename = "Stop")] - #[ts(rename = "Stop")] - pub stop: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ConfiguredHookMatcherGroup { - pub matcher: Option, - pub hooks: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(tag = "type")] -#[ts(tag = "type", export_to = "v2/")] -pub enum ConfiguredHookHandler { - #[serde(rename = "command")] - #[ts(rename = "command")] - Command { - command: String, - #[serde(rename = "timeoutSec")] - #[ts(rename = "timeoutSec")] - timeout_sec: Option, - r#async: bool, - #[serde(rename = "statusMessage")] - #[ts(rename = "statusMessage")] - status_message: Option, - }, - #[serde(rename = "prompt")] - #[ts(rename = "prompt")] - Prompt {}, - #[serde(rename = "agent")] - #[ts(rename = "agent")] - Agent {}, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct NetworkRequirements { - pub enabled: Option, - pub http_port: Option, - pub socks_port: Option, - pub allow_upstream_proxy: Option, - pub dangerously_allow_non_loopback_proxy: Option, - pub dangerously_allow_all_unix_sockets: Option, - /// Canonical network permission map for `experimental_network`. - pub domains: Option>, - /// When true, only managed allowlist entries are respected while managed - /// network enforcement is active. - pub managed_allowed_domains_only: Option, - /// Legacy compatibility view derived from `domains`. - pub allowed_domains: Option>, - /// Legacy compatibility view derived from `domains`. - pub denied_domains: Option>, - /// Canonical unix socket permission map for `experimental_network`. - pub unix_sockets: Option>, - /// Legacy compatibility view derived from `unix_sockets`. - pub allow_unix_sockets: Option>, - pub allow_local_binding: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "lowercase")] -#[ts(export_to = "v2/")] -pub enum NetworkDomainPermission { - Allow, - Deny, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "lowercase")] -#[ts(export_to = "v2/")] -pub enum NetworkUnixSocketPermission { - Allow, - None, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum ResidencyRequirement { - Us, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ConfigRequirementsReadResponse { - /// Null if no requirements are configured (e.g. no requirements.toml/MDM entries). - #[experimental(nested)] - pub requirements: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, JsonSchema, TS)] -#[ts(export_to = "v2/")] -pub enum ExternalAgentConfigMigrationItemType { - #[serde(rename = "AGENTS_MD")] - #[ts(rename = "AGENTS_MD")] - AgentsMd, - #[serde(rename = "CONFIG")] - #[ts(rename = "CONFIG")] - Config, - #[serde(rename = "SKILLS")] - #[ts(rename = "SKILLS")] - Skills, - #[serde(rename = "PLUGINS")] - #[ts(rename = "PLUGINS")] - Plugins, - #[serde(rename = "MCP_SERVER_CONFIG")] - #[ts(rename = "MCP_SERVER_CONFIG")] - McpServerConfig, - #[serde(rename = "SUBAGENTS")] - #[ts(rename = "SUBAGENTS")] - Subagents, - #[serde(rename = "HOOKS")] - #[ts(rename = "HOOKS")] - Hooks, - #[serde(rename = "COMMANDS")] - #[ts(rename = "COMMANDS")] - Commands, - #[serde(rename = "SESSIONS")] - #[ts(rename = "SESSIONS")] - Sessions, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct PluginsMigration { - #[serde(rename = "marketplaceName")] - #[ts(rename = "marketplaceName")] - pub marketplace_name: String, - #[serde(rename = "pluginNames")] - #[ts(rename = "pluginNames")] - pub plugin_names: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct SessionMigration { - pub path: PathBuf, - pub cwd: PathBuf, - pub title: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct McpServerMigration { - pub name: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct HookMigration { - pub name: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct SubagentMigration { - pub name: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct CommandMigration { - pub name: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct MigrationDetails { - #[serde(default)] - pub plugins: Vec, - #[serde(default)] - pub sessions: Vec, - #[serde(default)] - pub mcp_servers: Vec, - #[serde(default)] - pub hooks: Vec, - #[serde(default)] - pub subagents: Vec, - #[serde(default)] - pub commands: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ExternalAgentConfigMigrationItem { - pub item_type: ExternalAgentConfigMigrationItemType, - pub description: String, - /// Null or empty means home-scoped migration; non-empty means repo-scoped migration. - pub cwd: Option, - pub details: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ExternalAgentConfigDetectResponse { - pub items: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ExternalAgentConfigDetectParams { - /// If true, include detection under the user's home (~/.claude, ~/.codex, etc.). - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub include_home: bool, - /// Zero or more working directories to include for repo-scoped detection. - #[ts(optional = nullable)] - pub cwds: Option>, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ExternalAgentConfigImportParams { - pub migration_items: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ExternalAgentConfigImportResponse {} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ExternalAgentConfigImportCompletedNotification {} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ConfigValueWriteParams { - pub key_path: String, - pub value: JsonValue, - pub merge_strategy: MergeStrategy, - /// Path to the config file to write; defaults to the user's `config.toml` when omitted. - #[ts(optional = nullable)] - pub file_path: Option, - #[ts(optional = nullable)] - pub expected_version: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ConfigBatchWriteParams { - pub edits: Vec, - /// Path to the config file to write; defaults to the user's `config.toml` when omitted. - #[ts(optional = nullable)] - pub file_path: Option, - #[ts(optional = nullable)] - pub expected_version: Option, - /// When true, hot-reload the updated user config into all loaded threads after writing. - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub reload_user_config: bool, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ConfigEdit { - pub key_path: String, - pub value: JsonValue, - pub merge_strategy: MergeStrategy, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum CommandExecutionApprovalDecision { - /// User approved the command. - Accept, - /// User approved the command and future prompts in the same session-scoped - /// approval cache should run without prompting. - AcceptForSession, - /// User approved the command, and wants to apply the proposed execpolicy amendment so future - /// matching commands can run without prompting. - AcceptWithExecpolicyAmendment { - execpolicy_amendment: ExecPolicyAmendment, - }, - /// User chose a persistent network policy rule (allow/deny) for this host. - ApplyNetworkPolicyAmendment { - network_policy_amendment: NetworkPolicyAmendment, - }, - /// User denied the command. The agent will continue the turn. - Decline, - /// User denied the command. The turn will also be immediately interrupted. - Cancel, -} - -impl From for CommandExecutionApprovalDecision { - fn from(value: CoreReviewDecision) -> Self { - match value { - CoreReviewDecision::Approved => Self::Accept, - CoreReviewDecision::ApprovedExecpolicyAmendment { - proposed_execpolicy_amendment, - } => Self::AcceptWithExecpolicyAmendment { - execpolicy_amendment: proposed_execpolicy_amendment.into(), - }, - CoreReviewDecision::ApprovedForSession => Self::AcceptForSession, - CoreReviewDecision::NetworkPolicyAmendment { - network_policy_amendment, - } => Self::ApplyNetworkPolicyAmendment { - network_policy_amendment: network_policy_amendment.into(), - }, - CoreReviewDecision::Abort => Self::Cancel, - CoreReviewDecision::Denied => Self::Decline, - CoreReviewDecision::TimedOut => Self::Decline, - } - } -} - -v2_enum_from_core! { - pub enum NetworkApprovalProtocol from CoreNetworkApprovalProtocol { - Http, - Https, - Socks5Tcp, - Socks5Udp, - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct NetworkApprovalContext { - pub host: String, - pub protocol: NetworkApprovalProtocol, -} - -impl From for NetworkApprovalContext { - fn from(value: CoreNetworkApprovalContext) -> Self { - Self { - host: value.host, - protocol: value.protocol.into(), - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct AdditionalFileSystemPermissions { - /// This will be removed in favor of `entries`. - pub read: Option>, - /// This will be removed in favor of `entries`. - pub write: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub glob_scan_max_depth: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub entries: Option>, -} - -impl From for AdditionalFileSystemPermissions { - fn from(value: CoreFileSystemPermissions) -> Self { - if let Some((read, write)) = value.legacy_read_write_roots() { - let mut entries = Vec::with_capacity( - read.as_ref().map_or(0, Vec::len) + write.as_ref().map_or(0, Vec::len), - ); - if let Some(paths) = read.as_ref() { - entries.extend(paths.iter().map(|path| FileSystemSandboxEntry { - path: FileSystemPath::Path { path: path.clone() }, - access: FileSystemAccessMode::Read, - })); - } - if let Some(paths) = write.as_ref() { - entries.extend(paths.iter().map(|path| FileSystemSandboxEntry { - path: FileSystemPath::Path { path: path.clone() }, - access: FileSystemAccessMode::Write, - })); - } - Self { - read, - write, - glob_scan_max_depth: None, - entries: Some(entries), - } - } else { - Self { - read: None, - write: None, - glob_scan_max_depth: value.glob_scan_max_depth, - entries: Some( - value - .entries - .into_iter() - .map(FileSystemSandboxEntry::from) - .collect(), - ), - } - } - } -} - -impl From for CoreFileSystemPermissions { - fn from(value: AdditionalFileSystemPermissions) -> Self { - let mut permissions = if let Some(entries) = value.entries { - Self { - entries: entries - .into_iter() - .map(CoreFileSystemSandboxEntry::from) - .collect(), - glob_scan_max_depth: None, - } - } else { - CoreFileSystemPermissions::from_read_write_roots(value.read, value.write) - }; - permissions.glob_scan_max_depth = value.glob_scan_max_depth; - permissions - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct AdditionalNetworkPermissions { - pub enabled: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct PermissionProfileNetworkPermissions { - pub enabled: bool, -} - -impl From for AdditionalNetworkPermissions { - fn from(value: CoreNetworkPermissions) -> Self { - Self { - enabled: value.enabled, - } - } -} - -impl From for CoreNetworkPermissions { - fn from(value: AdditionalNetworkPermissions) -> Self { - Self { - enabled: value.enabled, - } - } -} - -impl From for PermissionProfileNetworkPermissions { - fn from(value: CoreNetworkSandboxPolicy) -> Self { - Self { - enabled: value.is_enabled(), - } - } -} - -impl From for CoreNetworkSandboxPolicy { - fn from(value: PermissionProfileNetworkPermissions) -> Self { - if value.enabled { - Self::Enabled - } else { - Self::Restricted - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[serde(deny_unknown_fields)] -#[ts(export_to = "v2/")] -pub struct RequestPermissionProfile { - pub network: Option, - pub file_system: Option, -} - -impl From for RequestPermissionProfile { - fn from(value: CoreRequestPermissionProfile) -> Self { - Self { - network: value.network.map(AdditionalNetworkPermissions::from), - file_system: value.file_system.map(AdditionalFileSystemPermissions::from), - } - } -} - -impl From for CoreRequestPermissionProfile { - fn from(value: RequestPermissionProfile) -> Self { - Self { - network: value.network.map(CoreNetworkPermissions::from), - file_system: value.file_system.map(CoreFileSystemPermissions::from), - } - } -} - -v2_enum_from_core!( - pub enum FileSystemAccessMode from CoreFileSystemAccessMode { - Read, - Write, - None - } -); - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(tag = "kind", rename_all = "snake_case")] -#[ts(tag = "kind")] -#[ts(export_to = "v2/")] -pub enum FileSystemSpecialPath { - Root, - Minimal, - #[serde(alias = "current_working_directory")] - ProjectRoots { - subpath: Option, - }, - Tmpdir, - SlashTmp, - Unknown { - path: String, - subpath: Option, - }, -} - -impl From for FileSystemSpecialPath { - fn from(value: CoreFileSystemSpecialPath) -> Self { - match value { - CoreFileSystemSpecialPath::Root => Self::Root, - CoreFileSystemSpecialPath::Minimal => Self::Minimal, - CoreFileSystemSpecialPath::ProjectRoots { subpath } => Self::ProjectRoots { subpath }, - CoreFileSystemSpecialPath::Tmpdir => Self::Tmpdir, - CoreFileSystemSpecialPath::SlashTmp => Self::SlashTmp, - CoreFileSystemSpecialPath::Unknown { path, subpath } => Self::Unknown { path, subpath }, - } - } -} - -impl From for CoreFileSystemSpecialPath { - fn from(value: FileSystemSpecialPath) -> Self { - match value { - FileSystemSpecialPath::Root => Self::Root, - FileSystemSpecialPath::Minimal => Self::Minimal, - FileSystemSpecialPath::ProjectRoots { subpath } => Self::ProjectRoots { subpath }, - FileSystemSpecialPath::Tmpdir => Self::Tmpdir, - FileSystemSpecialPath::SlashTmp => Self::SlashTmp, - FileSystemSpecialPath::Unknown { path, subpath } => Self::Unknown { path, subpath }, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(tag = "type", rename_all = "snake_case")] -#[ts(tag = "type")] -#[ts(export_to = "v2/")] -pub enum FileSystemPath { - Path { path: AbsolutePathBuf }, - GlobPattern { pattern: String }, - Special { value: FileSystemSpecialPath }, -} - -impl From for FileSystemPath { - fn from(value: CoreFileSystemPath) -> Self { - match value { - CoreFileSystemPath::Path { path } => Self::Path { path }, - CoreFileSystemPath::GlobPattern { pattern } => Self::GlobPattern { pattern }, - CoreFileSystemPath::Special { value } => Self::Special { - value: value.into(), - }, - } - } -} - -impl From for CoreFileSystemPath { - fn from(value: FileSystemPath) -> Self { - match value { - FileSystemPath::Path { path } => Self::Path { path }, - FileSystemPath::GlobPattern { pattern } => Self::GlobPattern { pattern }, - FileSystemPath::Special { value } => Self::Special { - value: value.into(), - }, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct FileSystemSandboxEntry { - pub path: FileSystemPath, - pub access: FileSystemAccessMode, -} - -impl From for FileSystemSandboxEntry { - fn from(value: CoreFileSystemSandboxEntry) -> Self { - Self { - path: value.path.into(), - access: value.access.into(), - } - } -} - -impl From for CoreFileSystemSandboxEntry { - fn from(value: FileSystemSandboxEntry) -> Self { - Self { - path: value.path.into(), - access: value.access.to_core(), - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(tag = "type", rename_all = "camelCase")] -#[ts(tag = "type")] -#[ts(export_to = "v2/")] -pub enum PermissionProfileFileSystemPermissions { - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - Restricted { - entries: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - glob_scan_max_depth: Option, - }, - Unrestricted, -} - -impl From for PermissionProfileFileSystemPermissions { - fn from(value: CoreManagedFileSystemPermissions) -> Self { - match value { - CoreManagedFileSystemPermissions::Restricted { - entries, - glob_scan_max_depth, - } => Self::Restricted { - entries: entries - .into_iter() - .map(FileSystemSandboxEntry::from) - .collect(), - glob_scan_max_depth, - }, - CoreManagedFileSystemPermissions::Unrestricted => Self::Unrestricted, - } - } -} - -impl From for CoreManagedFileSystemPermissions { - fn from(value: PermissionProfileFileSystemPermissions) -> Self { - match value { - PermissionProfileFileSystemPermissions::Restricted { - entries, - glob_scan_max_depth, - } => Self::Restricted { - entries: entries - .into_iter() - .map(CoreFileSystemSandboxEntry::from) - .collect(), - glob_scan_max_depth, - }, - PermissionProfileFileSystemPermissions::Unrestricted => Self::Unrestricted, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(tag = "type", rename_all = "camelCase")] -#[ts(tag = "type")] -#[ts(export_to = "v2/")] -pub enum PermissionProfile { - /// Codex owns sandbox construction for this profile. - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - Managed { - network: PermissionProfileNetworkPermissions, - file_system: PermissionProfileFileSystemPermissions, - }, - /// Do not apply an outer sandbox. - Disabled, - /// Filesystem isolation is enforced by an external caller. - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - External { - network: PermissionProfileNetworkPermissions, - }, -} - -impl From for PermissionProfile { - fn from(value: CorePermissionProfile) -> Self { - match value { - CorePermissionProfile::Managed { - file_system, - network, - } => Self::Managed { - network: network.into(), - file_system: file_system.into(), - }, - CorePermissionProfile::Disabled => Self::Disabled, - CorePermissionProfile::External { network } => Self::External { - network: network.into(), - }, - } - } -} - -impl From for CorePermissionProfile { - fn from(value: PermissionProfile) -> Self { - match value { - PermissionProfile::Managed { - file_system, - network, - } => Self::Managed { - file_system: file_system.into(), - network: network.into(), - }, - PermissionProfile::Disabled => Self::Disabled, - PermissionProfile::External { network } => Self::External { - network: network.into(), - }, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ActivePermissionProfile { - /// Identifier from `default_permissions` or the implicit built-in default, - /// such as `:workspace` or a user-defined `[permissions.]` profile. - pub id: String, - /// Parent profile identifier once permissions profiles support - /// inheritance. This is currently always `null`. - #[serde(default)] - pub extends: Option, - /// Bounded user-requested modifications applied on top of the named - /// profile, if any. - #[serde(default)] - pub modifications: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(tag = "type", rename_all = "camelCase")] -#[ts(tag = "type")] -#[ts(export_to = "v2/")] -pub enum ActivePermissionProfileModification { - /// Additional concrete directory that should be writable. - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - AdditionalWritableRoot { path: AbsolutePathBuf }, -} - -impl From for ActivePermissionProfileModification { - fn from(value: CoreActivePermissionProfileModification) -> Self { - match value { - CoreActivePermissionProfileModification::AdditionalWritableRoot { path } => { - Self::AdditionalWritableRoot { path } - } - } - } -} - -impl From for CoreActivePermissionProfileModification { - fn from(value: ActivePermissionProfileModification) -> Self { - match value { - ActivePermissionProfileModification::AdditionalWritableRoot { path } => { - Self::AdditionalWritableRoot { path } - } - } - } -} - -impl From for ActivePermissionProfile { - fn from(value: CoreActivePermissionProfile) -> Self { - Self { - id: value.id, - extends: value.extends, - modifications: value - .modifications - .into_iter() - .map(ActivePermissionProfileModification::from) - .collect(), - } - } -} - -impl From for CoreActivePermissionProfile { - fn from(value: ActivePermissionProfile) -> Self { - Self { - id: value.id, - extends: value.extends, - modifications: value - .modifications - .into_iter() - .map(CoreActivePermissionProfileModification::from) - .collect(), - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(tag = "type", rename_all = "camelCase")] -#[ts(tag = "type")] -#[ts(export_to = "v2/")] -pub enum PermissionProfileSelectionParams { - /// Select a named built-in or user-defined profile and optionally apply - /// bounded modifications that Codex knows how to validate. - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - Profile { - id: String, - #[ts(optional = nullable)] - modifications: Option>, - }, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(tag = "type", rename_all = "camelCase")] -#[ts(tag = "type")] -#[ts(export_to = "v2/")] -pub enum PermissionProfileModificationParams { - /// Additional concrete directory that should be writable. - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - AdditionalWritableRoot { path: AbsolutePathBuf }, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct AdditionalPermissionProfile { - /// Partial overlay used for per-command permission requests. - pub network: Option, - pub file_system: Option, -} - -impl From for AdditionalPermissionProfile { - fn from(value: CoreAdditionalPermissionProfile) -> Self { - Self { - network: value.network.map(AdditionalNetworkPermissions::from), - file_system: value.file_system.map(AdditionalFileSystemPermissions::from), - } - } -} - -impl From for CoreAdditionalPermissionProfile { - fn from(value: AdditionalPermissionProfile) -> Self { - Self { - network: value.network.map(CoreNetworkPermissions::from), - file_system: value.file_system.map(CoreFileSystemPermissions::from), - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct GrantedPermissionProfile { - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub network: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub file_system: Option, -} - -impl From for CoreAdditionalPermissionProfile { - fn from(value: GrantedPermissionProfile) -> Self { - Self { - network: value.network.map(CoreNetworkPermissions::from), - file_system: value.file_system.map(CoreFileSystemPermissions::from), - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum FileChangeApprovalDecision { - /// User approved the file changes. - Accept, - /// User approved the file changes and future changes to the same files should run without prompting. - AcceptForSession, - /// User denied the file changes. The agent will continue the turn. - Decline, - /// User denied the file changes. The turn will also be immediately interrupted. - Cancel, -} - -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum NetworkAccess { - #[default] - Restricted, - Enabled, -} - -#[derive(Serialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(tag = "type", rename_all = "camelCase")] -#[ts(tag = "type")] -#[ts(export_to = "v2/")] -pub enum SandboxPolicy { - DangerFullAccess, - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - ReadOnly { - #[serde(default)] - network_access: bool, - }, - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - ExternalSandbox { - #[serde(default)] - network_access: NetworkAccess, - }, - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - WorkspaceWrite { - #[serde(default)] - writable_roots: Vec, - #[serde(default)] - network_access: bool, - #[serde(default)] - exclude_tmpdir_env_var: bool, - #[serde(default)] - exclude_slash_tmp: bool, - }, -} - -#[derive(Deserialize)] -#[serde(tag = "type", rename_all = "camelCase")] -enum SandboxPolicyDeserialize { - DangerFullAccess, - #[serde(rename_all = "camelCase")] - ReadOnly { - #[serde(default)] - network_access: bool, - #[serde(default)] - access: Option, - }, - #[serde(rename_all = "camelCase")] - ExternalSandbox { - #[serde(default)] - network_access: NetworkAccess, - }, - #[serde(rename_all = "camelCase")] - WorkspaceWrite { - #[serde(default)] - writable_roots: Vec, - #[serde(default)] - read_only_access: Option, - #[serde(default)] - network_access: bool, - #[serde(default)] - exclude_tmpdir_env_var: bool, - #[serde(default)] - exclude_slash_tmp: bool, - }, -} - -#[derive(Deserialize)] -#[serde(tag = "type", rename_all = "camelCase")] -enum LegacyReadOnlyAccess { - FullAccess, - Restricted, -} - -impl<'de> Deserialize<'de> for SandboxPolicy { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - match SandboxPolicyDeserialize::deserialize(deserializer)? { - SandboxPolicyDeserialize::DangerFullAccess => Ok(SandboxPolicy::DangerFullAccess), - SandboxPolicyDeserialize::ReadOnly { - network_access, - access, - } => { - if matches!(access, Some(LegacyReadOnlyAccess::Restricted)) { - return Err(serde::de::Error::custom( - "readOnly.access is no longer supported; use permissionProfile for restricted reads", - )); - } - Ok(SandboxPolicy::ReadOnly { network_access }) - } - SandboxPolicyDeserialize::ExternalSandbox { network_access } => { - Ok(SandboxPolicy::ExternalSandbox { network_access }) - } - SandboxPolicyDeserialize::WorkspaceWrite { - writable_roots, - read_only_access, - network_access, - exclude_tmpdir_env_var, - exclude_slash_tmp, - } => { - if matches!(read_only_access, Some(LegacyReadOnlyAccess::Restricted)) { - return Err(serde::de::Error::custom( - "workspaceWrite.readOnlyAccess is no longer supported; use permissionProfile for restricted reads", - )); - } - Ok(SandboxPolicy::WorkspaceWrite { - writable_roots, - network_access, - exclude_tmpdir_env_var, - exclude_slash_tmp, - }) - } - } - } -} - -impl SandboxPolicy { - pub fn to_core(&self) -> codex_protocol::protocol::SandboxPolicy { - match self { - SandboxPolicy::DangerFullAccess => { - codex_protocol::protocol::SandboxPolicy::DangerFullAccess - } - SandboxPolicy::ReadOnly { network_access } => { - codex_protocol::protocol::SandboxPolicy::ReadOnly { - network_access: *network_access, - } - } - SandboxPolicy::ExternalSandbox { network_access } => { - codex_protocol::protocol::SandboxPolicy::ExternalSandbox { - network_access: match network_access { - NetworkAccess::Restricted => CoreNetworkAccess::Restricted, - NetworkAccess::Enabled => CoreNetworkAccess::Enabled, - }, - } - } - SandboxPolicy::WorkspaceWrite { - writable_roots, - network_access, - exclude_tmpdir_env_var, - exclude_slash_tmp, - } => codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { - writable_roots: writable_roots.clone(), - network_access: *network_access, - exclude_tmpdir_env_var: *exclude_tmpdir_env_var, - exclude_slash_tmp: *exclude_slash_tmp, - }, - } - } -} - -impl From for SandboxPolicy { - fn from(value: codex_protocol::protocol::SandboxPolicy) -> Self { - match value { - codex_protocol::protocol::SandboxPolicy::DangerFullAccess => { - SandboxPolicy::DangerFullAccess - } - codex_protocol::protocol::SandboxPolicy::ReadOnly { network_access } => { - SandboxPolicy::ReadOnly { network_access } - } - codex_protocol::protocol::SandboxPolicy::ExternalSandbox { network_access } => { - SandboxPolicy::ExternalSandbox { - network_access: match network_access { - CoreNetworkAccess::Restricted => NetworkAccess::Restricted, - CoreNetworkAccess::Enabled => NetworkAccess::Enabled, - }, - } - } - codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { - writable_roots, - network_access, - exclude_tmpdir_env_var, - exclude_slash_tmp, - } => SandboxPolicy::WorkspaceWrite { - writable_roots, - network_access, - exclude_tmpdir_env_var, - exclude_slash_tmp, - }, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(transparent)] -#[ts(type = "Array", export_to = "v2/")] -pub struct ExecPolicyAmendment { - pub command: Vec, -} - -impl ExecPolicyAmendment { - pub fn into_core(self) -> CoreExecPolicyAmendment { - CoreExecPolicyAmendment::new(self.command) - } -} - -impl From for ExecPolicyAmendment { - fn from(value: CoreExecPolicyAmendment) -> Self { - Self { - command: value.command().to_vec(), - } - } -} - -v2_enum_from_core!( - pub enum NetworkPolicyRuleAction from CoreNetworkPolicyRuleAction { - Allow, Deny - } -); - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct NetworkPolicyAmendment { - pub host: String, - pub action: NetworkPolicyRuleAction, -} - -impl NetworkPolicyAmendment { - pub fn into_core(self) -> CoreNetworkPolicyAmendment { - CoreNetworkPolicyAmendment { - host: self.host, - action: self.action.to_core(), - } - } -} - -impl From for NetworkPolicyAmendment { - fn from(value: CoreNetworkPolicyAmendment) -> Self { - Self { - host: value.host, - action: NetworkPolicyRuleAction::from(value.action), - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(tag = "type", rename_all = "camelCase")] -#[ts(tag = "type")] -#[ts(export_to = "v2/")] -pub enum CommandAction { - Read { - command: String, - name: String, - path: AbsolutePathBuf, - }, - ListFiles { - command: String, - path: Option, - }, - Search { - command: String, - query: Option, - path: Option, - }, - Unknown { - command: String, - }, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(rename_all = "camelCase", export_to = "v2/")] -#[derive(Default)] -pub enum SessionSource { - Cli, - #[serde(rename = "vscode")] - #[ts(rename = "vscode")] - #[default] - VsCode, - Exec, - AppServer, - Custom(String), - SubAgent(CoreSubAgentSource), - #[serde(other)] - Unknown, -} - -impl From for SessionSource { - fn from(value: CoreSessionSource) -> Self { - match value { - CoreSessionSource::Cli => SessionSource::Cli, - CoreSessionSource::VSCode => SessionSource::VsCode, - CoreSessionSource::Exec => SessionSource::Exec, - CoreSessionSource::Mcp => SessionSource::AppServer, - CoreSessionSource::Custom(source) => SessionSource::Custom(source), - // We do not want to render those at the app-server level. - CoreSessionSource::Internal(_) => SessionSource::Unknown, - CoreSessionSource::SubAgent(sub) => SessionSource::SubAgent(sub), - CoreSessionSource::Unknown => SessionSource::Unknown, - } - } -} - -impl From for CoreSessionSource { - fn from(value: SessionSource) -> Self { - match value { - SessionSource::Cli => CoreSessionSource::Cli, - SessionSource::VsCode => CoreSessionSource::VSCode, - SessionSource::Exec => CoreSessionSource::Exec, - SessionSource::AppServer => CoreSessionSource::Mcp, - SessionSource::Custom(source) => CoreSessionSource::Custom(source), - SessionSource::SubAgent(sub) => CoreSessionSource::SubAgent(sub), - SessionSource::Unknown => CoreSessionSource::Unknown, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct GitInfo { - pub sha: Option, - pub branch: Option, - pub origin_url: Option, -} - -impl CommandAction { - pub fn into_core(self) -> CoreParsedCommand { - match self { - CommandAction::Read { - command: cmd, - name, - path, - } => CoreParsedCommand::Read { - cmd, - name, - path: path.into_path_buf(), - }, - CommandAction::ListFiles { command: cmd, path } => { - CoreParsedCommand::ListFiles { cmd, path } - } - CommandAction::Search { - command: cmd, - query, - path, - } => CoreParsedCommand::Search { cmd, query, path }, - CommandAction::Unknown { command: cmd } => CoreParsedCommand::Unknown { cmd }, - } - } -} - -impl CommandAction { - pub fn from_core_with_cwd(value: CoreParsedCommand, cwd: &AbsolutePathBuf) -> Self { - match value { - CoreParsedCommand::Read { cmd, name, path } => CommandAction::Read { - command: cmd, - name, - path: cwd.join(path), - }, - CoreParsedCommand::ListFiles { cmd, path } => { - CommandAction::ListFiles { command: cmd, path } - } - CoreParsedCommand::Search { cmd, query, path } => CommandAction::Search { - command: cmd, - query, - path, - }, - CoreParsedCommand::Unknown { cmd } => CommandAction::Unknown { command: cmd }, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(tag = "type", rename_all = "camelCase")] -#[ts(tag = "type")] -#[ts(export_to = "v2/")] -pub enum Account { - #[serde(rename = "apiKey", rename_all = "camelCase")] - #[ts(rename = "apiKey", rename_all = "camelCase")] - ApiKey {}, - - #[serde(rename = "chatgpt", rename_all = "camelCase")] - #[ts(rename = "chatgpt", rename_all = "camelCase")] - Chatgpt { email: String, plan_type: PlanType }, - - #[serde(rename = "amazonBedrock", rename_all = "camelCase")] - #[ts(rename = "amazonBedrock", rename_all = "camelCase")] - AmazonBedrock {}, -} - -impl From for Account { - fn from(account: ProviderAccount) -> Self { - match account { - ProviderAccount::ApiKey => Self::ApiKey {}, - ProviderAccount::Chatgpt { email, plan_type } => Self::Chatgpt { email, plan_type }, - ProviderAccount::AmazonBedrock => Self::AmazonBedrock {}, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] -#[serde(tag = "type")] -#[ts(tag = "type")] -#[ts(export_to = "v2/")] -pub enum LoginAccountParams { - #[serde(rename = "apiKey", rename_all = "camelCase")] - #[ts(rename = "apiKey", rename_all = "camelCase")] - ApiKey { - #[serde(rename = "apiKey")] - #[ts(rename = "apiKey")] - api_key: String, - }, - #[serde(rename = "chatgpt", rename_all = "camelCase")] - #[ts(rename = "chatgpt", rename_all = "camelCase")] - Chatgpt { - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - codex_streamlined_login: bool, - }, - #[serde(rename = "chatgptDeviceCode")] - #[ts(rename = "chatgptDeviceCode")] - ChatgptDeviceCode, - /// [UNSTABLE] FOR OPENAI INTERNAL USE ONLY - DO NOT USE. - /// The access token must contain the same scopes that Codex-managed ChatGPT auth tokens have. - #[experimental("account/login/start.chatgptAuthTokens")] - #[serde(rename = "chatgptAuthTokens", rename_all = "camelCase")] - #[ts(rename = "chatgptAuthTokens", rename_all = "camelCase")] - ChatgptAuthTokens { - /// Access token (JWT) supplied by the client. - /// This token is used for backend API requests and email extraction. - access_token: String, - /// Workspace/account identifier supplied by the client. - chatgpt_account_id: String, - /// Optional plan type supplied by the client. - /// - /// When `null`, Codex attempts to derive the plan type from access-token - /// claims. If unavailable, the plan defaults to `unknown`. - #[ts(optional = nullable)] - chatgpt_plan_type: Option, - }, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(tag = "type", rename_all = "camelCase")] -#[ts(tag = "type")] -#[ts(export_to = "v2/")] -pub enum LoginAccountResponse { - #[serde(rename = "apiKey", rename_all = "camelCase")] - #[ts(rename = "apiKey", rename_all = "camelCase")] - ApiKey {}, - #[serde(rename = "chatgpt", rename_all = "camelCase")] - #[ts(rename = "chatgpt", rename_all = "camelCase")] - Chatgpt { - // Use plain String for identifiers to avoid TS/JSON Schema quirks around uuid-specific types. - // Convert to/from UUIDs at the application layer as needed. - login_id: String, - /// URL the client should open in a browser to initiate the OAuth flow. - auth_url: String, - }, - #[serde(rename = "chatgptDeviceCode", rename_all = "camelCase")] - #[ts(rename = "chatgptDeviceCode", rename_all = "camelCase")] - ChatgptDeviceCode { - // Use plain String for identifiers to avoid TS/JSON Schema quirks around uuid-specific types. - // Convert to/from UUIDs at the application layer as needed. - login_id: String, - /// URL the client should open in a browser to complete device code authorization. - verification_url: String, - /// One-time code the user must enter after signing in. - user_code: String, - }, - #[serde(rename = "chatgptAuthTokens", rename_all = "camelCase")] - #[ts(rename = "chatgptAuthTokens", rename_all = "camelCase")] - ChatgptAuthTokens {}, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct CancelLoginAccountParams { - pub login_id: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum CancelLoginAccountStatus { - Canceled, - NotFound, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct CancelLoginAccountResponse { - pub status: CancelLoginAccountStatus, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct LogoutAccountResponse {} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum ChatgptAuthTokensRefreshReason { - /// Codex attempted a backend request and received `401 Unauthorized`. - Unauthorized, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ChatgptAuthTokensRefreshParams { - pub reason: ChatgptAuthTokensRefreshReason, - /// Workspace/account identifier that Codex was previously using. - /// - /// Clients that manage multiple accounts/workspaces can use this as a hint - /// to refresh the token for the correct workspace. - /// - /// This may be `null` when the prior auth state did not include a workspace - /// identifier (`chatgpt_account_id`). - #[ts(optional = nullable)] - pub previous_account_id: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ChatgptAuthTokensRefreshResponse { - pub access_token: String, - pub chatgpt_account_id: String, - pub chatgpt_plan_type: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct GetAccountRateLimitsResponse { - /// Backward-compatible single-bucket view; mirrors the historical payload. - pub rate_limits: RateLimitSnapshot, - /// Multi-bucket view keyed by metered `limit_id` (for example, `codex`). - pub rate_limits_by_limit_id: Option>, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct SendAddCreditsNudgeEmailParams { - pub credit_type: AddCreditsNudgeCreditType, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] -#[ts(export_to = "v2/", rename_all = "snake_case")] -pub enum AddCreditsNudgeCreditType { - Credits, - UsageLimit, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct SendAddCreditsNudgeEmailResponse { - pub status: AddCreditsNudgeEmailStatus, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] -#[ts(export_to = "v2/", rename_all = "snake_case")] -pub enum AddCreditsNudgeEmailStatus { - Sent, - CooldownActive, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct GetAccountParams { - /// When `true`, requests a proactive token refresh before returning. - /// - /// In managed auth mode this triggers the normal refresh-token flow. In - /// external auth mode this flag is ignored. Clients should refresh tokens - /// themselves and call `account/login/start` with `chatgptAuthTokens`. - #[serde(default)] - pub refresh_token: bool, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct GetAccountResponse { - pub account: Option, - pub requires_openai_auth: bool, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ModelProviderCapabilitiesReadParams {} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ModelProviderCapabilitiesReadResponse { - pub namespace_tools: bool, - pub image_generation: bool, - pub web_search: bool, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ModelListParams { - /// Opaque pagination cursor returned by a previous call. - #[ts(optional = nullable)] - pub cursor: Option, - /// Optional page size; defaults to a reasonable server-side value. - #[ts(optional = nullable)] - pub limit: Option, - /// When true, include models that are hidden from the default picker list. - #[ts(optional = nullable)] - pub include_hidden: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ModelAvailabilityNux { - pub message: String, -} - -impl From for ModelAvailabilityNux { - fn from(value: CoreModelAvailabilityNux) -> Self { - Self { - message: value.message, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ModelServiceTier { - pub id: String, - pub name: String, - pub description: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct Model { - pub id: String, - pub model: String, - pub upgrade: Option, - pub upgrade_info: Option, - pub availability_nux: Option, - pub display_name: String, - pub description: String, - pub hidden: bool, - pub supported_reasoning_efforts: Vec, - pub default_reasoning_effort: ReasoningEffort, - #[serde(default = "default_input_modalities")] - pub input_modalities: Vec, - #[serde(default)] - pub supports_personality: bool, - /// Deprecated: use `serviceTiers` instead. - #[serde(default)] - pub additional_speed_tiers: Vec, - #[serde(default)] - pub service_tiers: Vec, - // Only one model should be marked as default. - pub is_default: bool, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ModelUpgradeInfo { - pub model: String, - pub upgrade_copy: Option, - pub model_link: Option, - pub migration_markdown: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ReasoningEffortOption { - pub reasoning_effort: ReasoningEffort, - pub description: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ModelListResponse { - pub data: 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, -} - -/// EXPERIMENTAL - list collaboration mode presets. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct CollaborationModeListParams {} - -/// EXPERIMENTAL - collaboration mode preset metadata for clients. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct CollaborationModeMask { - pub name: String, - pub mode: Option, - pub model: Option, - #[serde(rename = "reasoning_effort")] - #[ts(rename = "reasoning_effort")] - pub reasoning_effort: Option>, -} - -impl From for CollaborationModeMask { - fn from(value: CoreCollaborationModeMask) -> Self { - Self { - name: value.name, - mode: value.mode, - model: value.model, - reasoning_effort: value.reasoning_effort, - } - } -} - -/// EXPERIMENTAL - collaboration mode presets response. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct CollaborationModeListResponse { - pub data: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ExperimentalFeatureListParams { - /// Opaque pagination cursor returned by a previous call. - #[ts(optional = nullable)] - pub cursor: Option, - /// Optional page size; defaults to a reasonable server-side value. - #[ts(optional = nullable)] - pub limit: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum ExperimentalFeatureStage { - /// Feature is available for user testing and feedback. - Beta, - /// Feature is still being built and not ready for broad use. - UnderDevelopment, - /// Feature is production-ready. - Stable, - /// Feature is deprecated and should be avoided. - Deprecated, - /// Feature flag is retained only for backwards compatibility. - Removed, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ExperimentalFeature { - /// Stable key used in config.toml and CLI flag toggles. - pub name: String, - /// Lifecycle stage of this feature flag. - pub stage: ExperimentalFeatureStage, - /// User-facing display name shown in the experimental features UI. - /// Null when this feature is not in beta. - pub display_name: Option, - /// Short summary describing what the feature does. - /// Null when this feature is not in beta. - pub description: Option, - /// Announcement copy shown to users when the feature is introduced. - /// Null when this feature is not in beta. - pub announcement: Option, - /// Whether this feature is currently enabled in the loaded config. - pub enabled: bool, - /// Whether this feature is enabled by default. - pub default_enabled: bool, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ExperimentalFeatureListResponse { - pub data: 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, Eq, Default, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ExperimentalFeatureEnablementSetParams { - /// Process-wide runtime feature enablement keyed by canonical feature name. - /// - /// Only named features are updated. Omitted features are left unchanged. - /// Send an empty map for a no-op. - pub enablement: std::collections::BTreeMap, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ExperimentalFeatureEnablementSetResponse { - /// Feature enablement entries updated by this request. - pub enablement: std::collections::BTreeMap, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ListMcpServerStatusParams { - /// Opaque pagination cursor returned by a previous call. - #[ts(optional = nullable)] - pub cursor: Option, - /// Optional page size; defaults to a server-defined value. - #[ts(optional = nullable)] - pub limit: Option, - /// Controls how much MCP inventory data to fetch for each server. - /// Defaults to `Full` when omitted. - #[ts(optional = nullable)] - pub detail: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(rename_all = "camelCase", export_to = "v2/")] -pub enum McpServerStatusDetail { - Full, - ToolsAndAuthOnly, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct McpServerStatus { - pub name: String, - pub tools: std::collections::HashMap, - pub resources: Vec, - pub resource_templates: Vec, - pub auth_status: McpAuthStatus, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ListMcpServerStatusResponse { - pub data: 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")] -#[ts(export_to = "v2/")] -pub struct McpResourceReadParams { - #[ts(optional = nullable)] - pub thread_id: Option, - pub server: String, - pub uri: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct McpResourceReadResponse { - pub contents: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct McpServerToolCallParams { - pub thread_id: String, - pub server: String, - pub tool: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub arguments: Option, - #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub meta: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct McpServerToolCallResponse { - pub content: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub structured_content: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub is_error: Option, - #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub meta: Option, -} - -impl From for McpServerToolCallResponse { - fn from(result: CoreMcpCallToolResult) -> Self { - Self { - content: result.content, - structured_content: result.structured_content, - is_error: result.is_error, - meta: result.meta, - } - } -} - -impl From for McpToolCallResult { - fn from(result: CoreMcpCallToolResult) -> Self { - Self { - content: result.content, - structured_content: result.structured_content, - meta: result.meta, - } - } -} - -impl From for McpToolCallError { - fn from(error: CoreMcpToolCallError) -> Self { - Self { - message: error.message, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -/// EXPERIMENTAL - list available apps/connectors. -pub struct AppsListParams { - /// Opaque pagination cursor returned by a previous call. - #[ts(optional = nullable)] - pub cursor: Option, - /// Optional page size; defaults to a reasonable server-side value. - #[ts(optional = nullable)] - pub limit: Option, - /// Optional thread id used to evaluate app feature gating from that thread's config. - #[ts(optional = nullable)] - pub thread_id: Option, - /// When true, bypass app caches and fetch the latest data from sources. - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub force_refetch: bool, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -/// EXPERIMENTAL - app metadata returned by app-list APIs. -pub struct AppBranding { - pub category: Option, - pub developer: Option, - pub website: Option, - pub privacy_policy: Option, - pub terms_of_service: Option, - pub is_discoverable_app: bool, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct AppReview { - pub status: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct AppScreenshot { - pub url: Option, - #[serde(alias = "file_id")] - pub file_id: Option, - #[serde(alias = "user_prompt")] - pub user_prompt: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct AppMetadata { - pub review: Option, - pub categories: Option>, - pub sub_categories: Option>, - pub seo_description: Option, - pub screenshots: Option>, - pub developer: Option, - pub version: Option, - pub version_id: Option, - pub version_notes: Option, - pub first_party_type: Option, - pub first_party_requires_install: Option, - pub show_in_composer_when_unlinked: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -/// EXPERIMENTAL - app metadata returned by app-list APIs. -pub struct AppInfo { - pub id: String, - pub name: String, - pub description: Option, - pub logo_url: Option, - pub logo_url_dark: Option, - pub distribution_channel: Option, - pub branding: Option, - pub app_metadata: Option, - pub labels: Option>, - pub install_url: Option, - #[serde(default)] - pub is_accessible: bool, - /// Whether this app is enabled in config.toml. - /// Example: - /// ```toml - /// [apps.bad_app] - /// enabled = false - /// ``` - #[serde(default = "default_enabled")] - pub is_enabled: bool, - #[serde(default)] - pub plugin_display_names: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -/// EXPERIMENTAL - app metadata summary for plugin responses. -pub struct AppSummary { - pub id: String, - pub name: String, - pub description: Option, - pub install_url: Option, - pub needs_auth: bool, -} - -impl From for AppSummary { - fn from(value: AppInfo) -> Self { - Self { - id: value.id, - name: value.name, - description: value.description, - install_url: value.install_url, - needs_auth: false, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -/// EXPERIMENTAL - app list response. -pub struct AppsListResponse { - pub data: 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")] -#[ts(export_to = "v2/")] -/// EXPERIMENTAL - notification emitted when the app list changes. -pub struct AppListUpdatedNotification { - pub data: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct McpServerRefreshParams {} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct McpServerRefreshResponse {} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct McpServerOauthLoginParams { - pub name: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional = nullable)] - pub scopes: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional = nullable)] - pub timeout_secs: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct McpServerOauthLoginResponse { - pub authorization_url: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct FeedbackUploadParams { - pub classification: String, - #[ts(optional = nullable)] - pub reason: Option, - #[ts(optional = nullable)] - pub thread_id: Option, - pub include_logs: bool, - #[ts(optional = nullable)] - pub extra_log_files: Option>, - #[ts(optional = nullable)] - pub tags: Option>, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct FeedbackUploadResponse { - pub thread_id: String, -} - -/// Device-key algorithm reported at enrollment and signing boundaries. -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] -#[ts(rename_all = "snake_case", export_to = "v2/")] -pub enum DeviceKeyAlgorithm { - EcdsaP256Sha256, -} - -/// Platform protection class for a controller-local device key. -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] -#[ts(rename_all = "snake_case", export_to = "v2/")] -pub enum DeviceKeyProtectionClass { - HardwareSecureEnclave, - HardwareTpm, - OsProtectedNonextractable, -} - -/// Protection policy for creating or loading a controller-local device key. -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] -#[ts(rename_all = "snake_case", export_to = "v2/")] -pub enum DeviceKeyProtectionPolicy { - HardwareOnly, - AllowOsProtectedNonextractable, -} - -/// Create a controller-local device key with a random key id. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct DeviceKeyCreateParams { - /// Defaults to `hardware_only` when omitted. - #[ts(optional = nullable)] - pub protection_policy: Option, - pub account_user_id: String, - pub client_id: String, -} - -/// Device-key metadata and public key returned by create/public APIs. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct DeviceKeyCreateResponse { - pub key_id: String, - /// SubjectPublicKeyInfo DER encoded as base64. - pub public_key_spki_der_base64: String, - pub algorithm: DeviceKeyAlgorithm, - pub protection_class: DeviceKeyProtectionClass, -} - -/// Fetch a controller-local device key public key by id. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct DeviceKeyPublicParams { - pub key_id: String, -} - -/// Device-key public metadata returned by `device/key/public`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct DeviceKeyPublicResponse { - pub key_id: String, - /// SubjectPublicKeyInfo DER encoded as base64. - pub public_key_spki_der_base64: String, - pub algorithm: DeviceKeyAlgorithm, - pub protection_class: DeviceKeyProtectionClass, -} - -/// Current remote-control connection status and environment id exposed to clients. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct RemoteControlStatusChangedNotification { - pub status: RemoteControlConnectionStatus, - pub environment_id: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(rename_all = "camelCase", export_to = "v2/")] -pub enum RemoteControlConnectionStatus { - Disabled, - Connecting, - Connected, - Errored, -} - -/// Audience for a remote-control client connection device-key proof. -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] -#[ts(rename_all = "snake_case", export_to = "v2/")] -pub enum RemoteControlClientConnectionAudience { - RemoteControlClientWebsocket, -} - -/// Audience for a remote-control client enrollment device-key proof. -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] -#[ts(rename_all = "snake_case", export_to = "v2/")] -pub enum RemoteControlClientEnrollmentAudience { - RemoteControlClientEnrollment, -} - -/// Structured payloads accepted by `device/key/sign`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(tag = "type", rename_all = "camelCase")] -#[ts(tag = "type", export_to = "v2/")] -pub enum DeviceKeySignPayload { - /// Payload bound to one remote-control controller websocket `/client` connection challenge. - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - RemoteControlClientConnection { - nonce: String, - audience: RemoteControlClientConnectionAudience, - /// Backend-issued websocket session id that this proof authorizes. - session_id: String, - /// Origin of the backend endpoint that issued the challenge and will verify this proof. - target_origin: String, - /// Websocket route path that this proof authorizes. - target_path: String, - account_user_id: String, - client_id: String, - /// Remote-control token expiration as Unix seconds. - #[ts(type = "number")] - token_expires_at: i64, - /// SHA-256 of the controller-scoped remote-control token, encoded as unpadded base64url. - token_sha256_base64url: String, - /// Must contain exactly `remote_control_controller_websocket`. - scopes: Vec, - }, - /// Payload bound to a remote-control client `/client/enroll` ownership challenge. - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - RemoteControlClientEnrollment { - nonce: String, - audience: RemoteControlClientEnrollmentAudience, - /// Backend-issued enrollment challenge id that this proof authorizes. - challenge_id: String, - /// Origin of the backend endpoint that issued the challenge and will verify this proof. - target_origin: String, - /// HTTP route path that this proof authorizes. - target_path: String, - account_user_id: String, - client_id: String, - /// SHA-256 of the requested device identity operation, encoded as unpadded base64url. - device_identity_sha256_base64url: String, - /// Enrollment challenge expiration as Unix seconds. - #[ts(type = "number")] - challenge_expires_at: i64, - }, -} - -/// Sign an accepted structured payload with a controller-local device key. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct DeviceKeySignParams { - pub key_id: String, - pub payload: DeviceKeySignPayload, -} - -/// ASN.1 DER signature returned by `device/key/sign`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct DeviceKeySignResponse { - /// ECDSA signature DER encoded as base64. - pub signature_der_base64: String, - /// Exact bytes signed by the device key, encoded as base64. Verifiers must verify this byte - /// string directly and must not reserialize `payload`. - pub signed_payload_base64: String, - pub algorithm: DeviceKeyAlgorithm, -} - -/// Read a file from the host filesystem. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct FsReadFileParams { - /// Absolute path to read. - pub path: AbsolutePathBuf, -} - -/// Base64-encoded file contents returned by `fs/readFile`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct FsReadFileResponse { - /// File contents encoded as base64. - pub data_base64: String, -} - -/// Write a file on the host filesystem. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct FsWriteFileParams { - /// Absolute path to write. - pub path: AbsolutePathBuf, - /// File contents encoded as base64. - pub data_base64: String, -} - -/// Successful response for `fs/writeFile`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct FsWriteFileResponse {} - -/// Create a directory on the host filesystem. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct FsCreateDirectoryParams { - /// Absolute directory path to create. - pub path: AbsolutePathBuf, - /// Whether parent directories should also be created. Defaults to `true`. - #[ts(optional = nullable)] - pub recursive: Option, -} - -/// Successful response for `fs/createDirectory`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct FsCreateDirectoryResponse {} - -/// Request metadata for an absolute path. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct FsGetMetadataParams { - /// Absolute path to inspect. - pub path: AbsolutePathBuf, -} - -/// Metadata returned by `fs/getMetadata`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct FsGetMetadataResponse { - /// Whether the path resolves to a directory. - pub is_directory: bool, - /// Whether the path resolves to a regular file. - pub is_file: bool, - /// Whether the path itself is a symbolic link. - pub is_symlink: bool, - /// File creation time in Unix milliseconds when available, otherwise `0`. - #[ts(type = "number")] - pub created_at_ms: i64, - /// File modification time in Unix milliseconds when available, otherwise `0`. - #[ts(type = "number")] - pub modified_at_ms: i64, -} - -/// List direct child names for a directory. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct FsReadDirectoryParams { - /// Absolute directory path to read. - pub path: AbsolutePathBuf, -} - -/// A directory entry returned by `fs/readDirectory`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct FsReadDirectoryEntry { - /// Direct child entry name only, not an absolute or relative path. - pub file_name: String, - /// Whether this entry resolves to a directory. - pub is_directory: bool, - /// Whether this entry resolves to a regular file. - pub is_file: bool, -} - -/// Directory entries returned by `fs/readDirectory`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct FsReadDirectoryResponse { - /// Direct child entries in the requested directory. - pub entries: Vec, -} - -/// Remove a file or directory tree from the host filesystem. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct FsRemoveParams { - /// Absolute path to remove. - pub path: AbsolutePathBuf, - /// Whether directory removal should recurse. Defaults to `true`. - #[ts(optional = nullable)] - pub recursive: Option, - /// Whether missing paths should be ignored. Defaults to `true`. - #[ts(optional = nullable)] - pub force: Option, -} - -/// Successful response for `fs/remove`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct FsRemoveResponse {} - -/// Copy a file or directory tree on the host filesystem. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct FsCopyParams { - /// Absolute source path. - pub source_path: AbsolutePathBuf, - /// Absolute destination path. - pub destination_path: AbsolutePathBuf, - /// Required for directory copies; ignored for file copies. - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub recursive: bool, -} - -/// Successful response for `fs/copy`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct FsCopyResponse {} - -/// Start filesystem watch notifications for an absolute path. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct FsWatchParams { - /// Connection-scoped watch identifier used for `fs/unwatch` and `fs/changed`. - pub watch_id: String, - /// Absolute file or directory path to watch. - pub path: AbsolutePathBuf, -} - -/// Successful response for `fs/watch`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct FsWatchResponse { - /// Canonicalized path associated with the watch. - pub path: AbsolutePathBuf, -} - -/// Stop filesystem watch notifications for a prior `fs/watch`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct FsUnwatchParams { - /// Watch identifier previously provided to `fs/watch`. - pub watch_id: String, -} - -/// Successful response for `fs/unwatch`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct FsUnwatchResponse {} - -/// Filesystem watch notification emitted for `fs/watch` subscribers. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct FsChangedNotification { - /// Watch identifier previously provided to `fs/watch`. - pub watch_id: String, - /// File or directory paths associated with this event. - pub changed_paths: Vec, -} - -/// PTY size in character cells for `command/exec` PTY sessions. -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct CommandExecTerminalSize { - /// Terminal height in character cells. - pub rows: u16, - /// Terminal width in character cells. - pub cols: u16, -} - -/// Run a standalone command (argv vector) in the server sandbox without -/// creating a thread or turn. -/// -/// The final `command/exec` response is deferred until the process exits and is -/// sent only after all `command/exec/outputDelta` notifications for that -/// connection have been emitted. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct CommandExecParams { - /// Command argv vector. Empty arrays are rejected. - pub command: Vec, - /// Optional client-supplied, connection-scoped process id. - /// - /// Required for `tty`, `streamStdin`, `streamStdoutStderr`, and follow-up - /// `command/exec/write`, `command/exec/resize`, and - /// `command/exec/terminate` calls. When omitted, buffered execution gets an - /// internal id that is not exposed to the client. - #[ts(optional = nullable)] - pub process_id: Option, - /// Enable PTY mode. - /// - /// This implies `streamStdin` and `streamStdoutStderr`. - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub tty: bool, - /// Allow follow-up `command/exec/write` requests to write stdin bytes. - /// - /// Requires a client-supplied `processId`. - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub stream_stdin: bool, - /// Stream stdout/stderr via `command/exec/outputDelta` notifications. - /// - /// Streamed bytes are not duplicated into the final response and require a - /// client-supplied `processId`. - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub stream_stdout_stderr: bool, - /// Optional per-stream stdout/stderr capture cap in bytes. - /// - /// When omitted, the server default applies. Cannot be combined with - /// `disableOutputCap`. - #[ts(type = "number | null")] - #[ts(optional = nullable)] - pub output_bytes_cap: Option, - /// Disable stdout/stderr capture truncation for this request. - /// - /// Cannot be combined with `outputBytesCap`. - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub disable_output_cap: bool, - /// Disable the timeout entirely for this request. - /// - /// Cannot be combined with `timeoutMs`. - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub disable_timeout: bool, - /// Optional timeout in milliseconds. - /// - /// When omitted, the server default applies. Cannot be combined with - /// `disableTimeout`. - #[ts(type = "number | null")] - #[ts(optional = nullable)] - pub timeout_ms: Option, - /// Optional working directory. Defaults to the server cwd. - #[ts(optional = nullable)] - pub cwd: Option, - /// Optional environment overrides merged into the server-computed - /// environment. - /// - /// Matching names override inherited values. Set a key to `null` to unset - /// an inherited variable. - #[ts(optional = nullable)] - pub env: Option>>, - /// Optional initial PTY size in character cells. Only valid when `tty` is - /// true. - #[ts(optional = nullable)] - pub size: Option, - /// Optional sandbox policy for this command. - /// - /// Uses the same shape as thread/turn execution sandbox configuration and - /// defaults to the user's configured policy when omitted. Cannot be - /// combined with `permissionProfile`. - #[ts(optional = nullable)] - pub sandbox_policy: Option, - /// Optional full permissions profile for this command. - /// - /// Defaults to the user's configured permissions when omitted. Cannot be - /// combined with `sandboxPolicy`. - #[experimental("command/exec.permissionProfile")] - #[ts(optional = nullable)] - pub permission_profile: Option, -} - -/// Final buffered result for `command/exec`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct CommandExecResponse { - /// Process exit code. - pub exit_code: i32, - /// Buffered stdout capture. - /// - /// Empty when stdout was streamed via `command/exec/outputDelta`. - pub stdout: String, - /// Buffered stderr capture. - /// - /// Empty when stderr was streamed via `command/exec/outputDelta`. - pub stderr: String, -} - -/// Write stdin bytes to a running `command/exec` session, close stdin, or -/// both. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct CommandExecWriteParams { - /// Client-supplied, connection-scoped `processId` from the original - /// `command/exec` request. - pub process_id: String, - /// Optional base64-encoded stdin bytes to write. - #[ts(optional = nullable)] - pub delta_base64: Option, - /// Close stdin after writing `deltaBase64`, if present. - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub close_stdin: bool, -} - -/// Empty success response for `command/exec/write`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct CommandExecWriteResponse {} - -/// Terminate a running `command/exec` session. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct CommandExecTerminateParams { - /// Client-supplied, connection-scoped `processId` from the original - /// `command/exec` request. - pub process_id: String, -} - -/// Empty success response for `command/exec/terminate`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct CommandExecTerminateResponse {} - -/// Resize a running PTY-backed `command/exec` session. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct CommandExecResizeParams { - /// Client-supplied, connection-scoped `processId` from the original - /// `command/exec` request. - pub process_id: String, - /// New PTY size in character cells. - pub size: CommandExecTerminalSize, -} - -/// Empty success response for `command/exec/resize`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct CommandExecResizeResponse {} - -/// Stream label for `command/exec/outputDelta` notifications. -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum CommandExecOutputStream { - /// stdout stream. PTY mode multiplexes terminal output here. - Stdout, - /// stderr stream. - Stderr, -} - -/// PTY size in character cells for `process/spawn` PTY sessions. -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ProcessTerminalSize { - /// Terminal height in character cells. - pub rows: u16, - /// Terminal width in character cells. - pub cols: u16, -} - -/// Spawn a standalone process (argv vector) without a Codex sandbox on the host -/// where the app server is running. -/// -/// `process/spawn` returns after the process has started and the connection-scoped -/// `processHandle` has been registered. Process output and exit are reported via -/// `process/outputDelta` and `process/exited` notifications. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ProcessSpawnParams { - /// Command argv vector. Empty arrays are rejected. - pub command: Vec, - /// Client-supplied, connection-scoped process handle. - /// - /// Duplicate active handles are rejected on the same connection. The same - /// handle can be reused after the prior process exits. - pub process_handle: String, - /// Absolute working directory for the process. - pub cwd: AbsolutePathBuf, - /// Enable PTY mode. - /// - /// This implies `streamStdin` and `streamStdoutStderr`. - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub tty: bool, - /// Allow follow-up `process/writeStdin` requests to write stdin bytes. - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub stream_stdin: bool, - /// Stream stdout/stderr via `process/outputDelta` notifications. - /// - /// Streamed bytes are not duplicated into the `process/exited` notification. - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub stream_stdout_stderr: bool, - /// Optional per-stream stdout/stderr capture cap in bytes. - /// - /// When omitted, the server default applies. Set to `null` to disable the - /// cap. - #[serde( - default, - deserialize_with = "super::serde_helpers::deserialize_double_option", - serialize_with = "super::serde_helpers::serialize_double_option", - skip_serializing_if = "Option::is_none" - )] - #[ts(type = "number | null")] - #[ts(optional = nullable)] - pub output_bytes_cap: Option>, - /// Optional timeout in milliseconds. - /// - /// When omitted, the server default applies. Set to `null` to disable the - /// timeout. - #[serde( - default, - deserialize_with = "super::serde_helpers::deserialize_double_option", - serialize_with = "super::serde_helpers::serialize_double_option", - skip_serializing_if = "Option::is_none" - )] - #[ts(type = "number | null")] - #[ts(optional = nullable)] - pub timeout_ms: Option>, - /// Optional environment overrides merged into the app-server process - /// environment. - /// - /// Matching names override inherited values. Set a key to `null` to unset - /// an inherited variable. - #[ts(optional = nullable)] - pub env: Option>>, - /// Optional initial PTY size in character cells. Only valid when `tty` is - /// true. - #[ts(optional = nullable)] - pub size: Option, -} - -/// Successful response for `process/spawn`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ProcessSpawnResponse {} - -/// Write stdin bytes to a running `process/spawn` session, close stdin, or -/// both. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ProcessWriteStdinParams { - /// Client-supplied, connection-scoped `processHandle` from `process/spawn`. - pub process_handle: String, - /// Optional base64-encoded stdin bytes to write. - #[ts(optional = nullable)] - pub delta_base64: Option, - /// Close stdin after writing `deltaBase64`, if present. - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub close_stdin: bool, -} - -/// Empty success response for `process/writeStdin`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ProcessWriteStdinResponse {} - -/// Terminate a running `process/spawn` session. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ProcessKillParams { - /// Client-supplied, connection-scoped `processHandle` from `process/spawn`. - pub process_handle: String, -} - -/// Empty success response for `process/kill`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ProcessKillResponse {} - -/// Resize a running PTY-backed `process/spawn` session. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ProcessResizePtyParams { - /// Client-supplied, connection-scoped `processHandle` from `process/spawn`. - pub process_handle: String, - /// New PTY size in character cells. - pub size: ProcessTerminalSize, -} - -/// Empty success response for `process/resizePty`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ProcessResizePtyResponse {} - -/// Stream label for `process/outputDelta` notifications. -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum ProcessOutputStream { - /// stdout stream. PTY mode multiplexes terminal output here. - Stdout, - /// stderr stream. - Stderr, -} - -/// Base64-encoded output chunk emitted for a streaming `process/spawn` request. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ProcessOutputDeltaNotification { - /// Client-supplied, connection-scoped `processHandle` from `process/spawn`. - pub process_handle: String, - /// Output stream this chunk belongs to. - pub stream: ProcessOutputStream, - /// Base64-encoded output bytes. - pub delta_base64: String, - /// True on the final streamed chunk for this stream when output was - /// truncated by `outputBytesCap`. - pub cap_reached: bool, -} - -/// Final process exit notification for `process/spawn`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ProcessExitedNotification { - /// Client-supplied, connection-scoped `processHandle` from `process/spawn`. - pub process_handle: String, - /// Process exit code. - pub exit_code: i32, - /// Buffered stdout capture. - /// - /// Empty when stdout was streamed via `process/outputDelta`. - pub stdout: String, - /// Whether stdout reached `outputBytesCap`. - /// - /// In streaming mode, stdout is empty and cap state is also reported on the - /// final stdout `process/outputDelta` notification. - pub stdout_cap_reached: bool, - /// Buffered stderr capture. - /// - /// Empty when stderr was streamed via `process/outputDelta`. - pub stderr: String, - /// Whether stderr reached `outputBytesCap`. - /// - /// In streaming mode, stderr is empty and cap state is also reported on the - /// final stderr `process/outputDelta` notification. - pub stderr_cap_reached: bool, -} - -// === Threads, Turns, and Items === -// Thread APIs -#[derive( - Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS, ExperimentalApi, -)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadStartParams { - #[ts(optional = nullable)] - pub model: Option, - #[ts(optional = nullable)] - pub model_provider: Option, - #[serde( - default, - deserialize_with = "super::serde_helpers::deserialize_double_option", - serialize_with = "super::serde_helpers::serialize_double_option", - skip_serializing_if = "Option::is_none" - )] - #[ts(optional = nullable)] - pub service_tier: Option>, - #[ts(optional = nullable)] - pub cwd: Option, - #[experimental(nested)] - #[ts(optional = nullable)] - pub approval_policy: Option, - /// Override where approval requests are routed for review on this thread - /// and subsequent turns. - #[ts(optional = nullable)] - pub approvals_reviewer: Option, - #[ts(optional = nullable)] - pub sandbox: Option, - /// Named profile selection for this thread. Cannot be combined with - /// `sandbox`. Use bounded `modifications` for supported turn/thread - /// adjustments instead of replacing the full permissions profile. - #[experimental("thread/start.permissions")] - #[ts(optional = nullable)] - pub permissions: Option, - #[ts(optional = nullable)] - pub config: Option>, - #[ts(optional = nullable)] - pub service_name: Option, - #[ts(optional = nullable)] - pub base_instructions: Option, - #[ts(optional = nullable)] - pub developer_instructions: Option, - #[ts(optional = nullable)] - pub personality: Option, - #[ts(optional = nullable)] - pub ephemeral: Option, - #[ts(optional = nullable)] - pub session_start_source: Option, - /// Optional sticky environments for this thread. - /// - /// Omitted selects the default environment when environment access is - /// enabled. Empty disables environment access for turns that do not - /// provide a turn override. Non-empty selects the first environment as the - /// current turn environment. - #[experimental("thread/start.environments")] - #[ts(optional = nullable)] - pub environments: Option>, - #[experimental("thread/start.dynamicTools")] - #[ts(optional = nullable)] - pub dynamic_tools: Option>, - /// Test-only experimental field used to validate experimental gating and - /// schema filtering behavior in a stable way. - #[experimental("thread/start.mockExperimentalField")] - #[ts(optional = nullable)] - pub mock_experimental_field: Option, - /// If true, opt into emitting raw Responses API items on the event stream. - /// This is for internal use only (e.g. Codex Cloud). - #[experimental("thread/start.experimentalRawEvents")] - #[serde(default)] - pub experimental_raw_events: bool, - /// Deprecated and ignored by app-server. Kept only so older clients can - /// continue sending the field while rollout persistence always uses the - /// limited history policy. - #[experimental("thread/start.persistFullHistory")] - #[serde(default)] - pub persist_extended_history: bool, -} - -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct MockExperimentalMethodParams { - /// Test-only payload field. - #[ts(optional = nullable)] - pub value: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct MockExperimentalMethodResponse { - /// Echoes the input `value`. - pub echoed: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadStartResponse { - pub thread: Thread, - pub model: String, - pub model_provider: String, - pub service_tier: Option, - pub cwd: AbsolutePathBuf, - /// Instruction source files currently loaded for this thread. - #[serde(default)] - pub instruction_sources: Vec, - #[experimental(nested)] - pub approval_policy: AskForApproval, - /// Reviewer currently used for approval requests on this thread. - pub approvals_reviewer: ApprovalsReviewer, - /// Legacy sandbox policy retained for compatibility. Experimental clients - /// should prefer `permissionProfile` when they need exact runtime - /// permissions. - pub sandbox: SandboxPolicy, - /// Full active permissions for this thread. `activePermissionProfile` - /// carries display/provenance metadata for this runtime profile. - #[experimental("thread/start.permissionProfile")] - #[serde(default)] - pub permission_profile: Option, - /// Named or implicit built-in profile that produced the active - /// permissions, when known. - #[experimental("thread/start.activePermissionProfile")] - #[serde(default)] - pub active_permission_profile: Option, - pub reasoning_effort: Option, -} - -#[derive( - Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS, ExperimentalApi, -)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -/// There are three ways to resume a thread: -/// 1. By thread_id: load the thread from disk by thread_id and resume it. -/// 2. By history: instantiate the thread from memory and resume it. -/// 3. By path: load the thread from disk by path and resume it. -/// -/// The precedence is: history > path > thread_id. -/// If using history or path, the thread_id param will be ignored. -/// -/// Prefer using thread_id whenever possible. -pub struct ThreadResumeParams { - pub thread_id: String, - - /// [UNSTABLE] FOR CODEX CLOUD - DO NOT USE. - /// If specified, the thread will be resumed with the provided history - /// instead of loaded from disk. - #[experimental("thread/resume.history")] - #[ts(optional = nullable)] - pub history: Option>, - - /// [UNSTABLE] Specify the rollout path to resume from. - /// If specified, the thread_id param will be ignored. - #[experimental("thread/resume.path")] - #[ts(optional = nullable)] - pub path: Option, - - /// Configuration overrides for the resumed thread, if any. - #[ts(optional = nullable)] - pub model: Option, - #[ts(optional = nullable)] - pub model_provider: Option, - #[serde( - default, - deserialize_with = "super::serde_helpers::deserialize_double_option", - serialize_with = "super::serde_helpers::serialize_double_option", - skip_serializing_if = "Option::is_none" - )] - #[ts(optional = nullable)] - pub service_tier: Option>, - #[ts(optional = nullable)] - pub cwd: Option, - #[experimental(nested)] - #[ts(optional = nullable)] - pub approval_policy: Option, - /// Override where approval requests are routed for review on this thread - /// and subsequent turns. - #[ts(optional = nullable)] - pub approvals_reviewer: Option, - #[ts(optional = nullable)] - pub sandbox: Option, - /// Named profile selection for the resumed thread. Cannot be combined - /// with `sandbox`. Use bounded `modifications` for supported thread - /// adjustments instead of replacing the full permissions profile. - #[experimental("thread/resume.permissions")] - #[ts(optional = nullable)] - pub permissions: Option, - #[ts(optional = nullable)] - pub config: Option>, - #[ts(optional = nullable)] - pub base_instructions: Option, - #[ts(optional = nullable)] - pub developer_instructions: Option, - #[ts(optional = nullable)] - pub personality: Option, - /// When true, return only thread metadata and live-resume state without - /// populating `thread.turns`. This is useful when the client plans to call - /// `thread/turns/list` immediately after resuming. - #[experimental("thread/resume.excludeTurns")] - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub exclude_turns: bool, - /// Deprecated and ignored by app-server. Kept only so older clients can - /// continue sending the field while rollout persistence always uses the - /// limited history policy. - #[experimental("thread/resume.persistFullHistory")] - #[serde(default)] - pub persist_extended_history: bool, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadResumeResponse { - pub thread: Thread, - pub model: String, - pub model_provider: String, - pub service_tier: Option, - pub cwd: AbsolutePathBuf, - /// Instruction source files currently loaded for this thread. - #[serde(default)] - pub instruction_sources: Vec, - #[experimental(nested)] - pub approval_policy: AskForApproval, - /// Reviewer currently used for approval requests on this thread. - pub approvals_reviewer: ApprovalsReviewer, - /// Legacy sandbox policy retained for compatibility. Experimental clients - /// should prefer `permissionProfile` when they need exact runtime - /// permissions. - pub sandbox: SandboxPolicy, - /// Full active permissions for this thread. `activePermissionProfile` - /// carries display/provenance metadata for this runtime profile. - #[experimental("thread/resume.permissionProfile")] - #[serde(default)] - pub permission_profile: Option, - /// Named or implicit built-in profile that produced the active - /// permissions, when known. - #[experimental("thread/resume.activePermissionProfile")] - #[serde(default)] - pub active_permission_profile: Option, - pub reasoning_effort: Option, -} - -#[derive( - Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS, ExperimentalApi, -)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -/// There are two ways to fork a thread: -/// 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. -/// 2. By path: load the thread from disk by path and fork it into a new thread. -/// -/// If using path, the thread_id param will be ignored. -/// -/// Prefer using thread_id whenever possible. -pub struct ThreadForkParams { - pub thread_id: String, - - /// [UNSTABLE] Specify the rollout path to fork from. - /// If specified, the thread_id param will be ignored. - #[experimental("thread/fork.path")] - #[ts(optional = nullable)] - pub path: Option, - - /// Configuration overrides for the forked thread, if any. - #[ts(optional = nullable)] - pub model: Option, - #[ts(optional = nullable)] - pub model_provider: Option, - #[serde( - default, - deserialize_with = "super::serde_helpers::deserialize_double_option", - serialize_with = "super::serde_helpers::serialize_double_option", - skip_serializing_if = "Option::is_none" - )] - #[ts(optional = nullable)] - pub service_tier: Option>, - #[ts(optional = nullable)] - pub cwd: Option, - #[experimental(nested)] - #[ts(optional = nullable)] - pub approval_policy: Option, - /// Override where approval requests are routed for review on this thread - /// and subsequent turns. - #[ts(optional = nullable)] - pub approvals_reviewer: Option, - #[ts(optional = nullable)] - pub sandbox: Option, - /// Named profile selection for the forked thread. Cannot be combined with - /// `sandbox`. Use bounded `modifications` for supported thread - /// adjustments instead of replacing the full permissions profile. - #[experimental("thread/fork.permissions")] - #[ts(optional = nullable)] - pub permissions: Option, - #[ts(optional = nullable)] - pub config: Option>, - #[ts(optional = nullable)] - pub base_instructions: Option, - #[ts(optional = nullable)] - pub developer_instructions: Option, - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub ephemeral: bool, - /// When true, return only thread metadata and live fork state without - /// populating `thread.turns`. This is useful when the client plans to call - /// `thread/turns/list` immediately after forking. - #[experimental("thread/fork.excludeTurns")] - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub exclude_turns: bool, - /// Deprecated and ignored by app-server. Kept only so older clients can - /// continue sending the field while rollout persistence always uses the - /// limited history policy. - #[experimental("thread/fork.persistFullHistory")] - #[serde(default)] - pub persist_extended_history: bool, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadForkResponse { - pub thread: Thread, - pub model: String, - pub model_provider: String, - pub service_tier: Option, - pub cwd: AbsolutePathBuf, - /// Instruction source files currently loaded for this thread. - #[serde(default)] - pub instruction_sources: Vec, - #[experimental(nested)] - pub approval_policy: AskForApproval, - /// Reviewer currently used for approval requests on this thread. - pub approvals_reviewer: ApprovalsReviewer, - /// Legacy sandbox policy retained for compatibility. Experimental clients - /// should prefer `permissionProfile` when they need exact runtime - /// permissions. - pub sandbox: SandboxPolicy, - /// Full active permissions for this thread. `activePermissionProfile` - /// carries display/provenance metadata for this runtime profile. - #[experimental("thread/fork.permissionProfile")] - #[serde(default)] - pub permission_profile: Option, - /// Named or implicit built-in profile that produced the active - /// permissions, when known. - #[experimental("thread/fork.activePermissionProfile")] - #[serde(default)] - pub active_permission_profile: Option, - pub reasoning_effort: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadArchiveParams { - pub thread_id: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadArchiveResponse {} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadUnsubscribeParams { - pub thread_id: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadUnsubscribeResponse { - pub status: ThreadUnsubscribeStatus, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum ThreadUnsubscribeStatus { - NotLoaded, - NotSubscribed, - Unsubscribed, -} - -/// Parameters for `thread/increment_elicitation`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadIncrementElicitationParams { - /// Thread whose out-of-band elicitation counter should be incremented. - pub thread_id: String, -} - -/// Response for `thread/increment_elicitation`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadIncrementElicitationResponse { - /// Current out-of-band elicitation count after the increment. - pub count: u64, - /// Whether timeout accounting is paused after applying the increment. - pub paused: bool, -} - -/// Parameters for `thread/decrement_elicitation`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadDecrementElicitationParams { - /// Thread whose out-of-band elicitation counter should be decremented. - pub thread_id: String, -} - -/// Response for `thread/decrement_elicitation`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadDecrementElicitationResponse { - /// Current out-of-band elicitation count after the decrement. - pub count: u64, - /// Whether timeout accounting remains paused after applying the decrement. - pub paused: bool, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadSetNameParams { - pub thread_id: String, - pub name: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadUnarchiveParams { - pub thread_id: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadSetNameResponse {} - -v2_enum_from_core! { - pub enum ThreadGoalStatus from CoreThreadGoalStatus { - Active, - Paused, - BudgetLimited, - Complete, - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadGoal { - pub thread_id: String, - pub objective: String, - pub status: ThreadGoalStatus, - #[ts(type = "number | null")] - pub token_budget: Option, - #[ts(type = "number")] - pub tokens_used: i64, - #[ts(type = "number")] - pub time_used_seconds: i64, - #[ts(type = "number")] - pub created_at: i64, - #[ts(type = "number")] - pub updated_at: i64, -} - -impl From for ThreadGoal { - fn from(value: codex_protocol::protocol::ThreadGoal) -> Self { - Self { - thread_id: value.thread_id.to_string(), - objective: value.objective, - status: value.status.into(), - token_budget: value.token_budget, - tokens_used: value.tokens_used, - time_used_seconds: value.time_used_seconds, - created_at: value.created_at, - updated_at: value.updated_at, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadGoalSetParams { - pub thread_id: String, - #[ts(optional = nullable)] - pub objective: Option, - #[ts(optional = nullable)] - pub status: Option, - #[serde( - default, - deserialize_with = "super::serde_helpers::deserialize_double_option", - serialize_with = "super::serde_helpers::serialize_double_option", - skip_serializing_if = "Option::is_none" - )] - #[ts(optional = nullable, type = "number | null")] - pub token_budget: Option>, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadGoalSetResponse { - pub goal: ThreadGoal, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadGoalGetParams { - pub thread_id: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadGoalGetResponse { - pub goal: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadGoalClearParams { - pub thread_id: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadGoalClearResponse { - pub cleared: bool, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadMetadataUpdateParams { - pub thread_id: String, - /// Patch the stored Git metadata for this thread. - /// Omit a field to leave it unchanged, set it to `null` to clear it, or - /// provide a string to replace the stored value. - #[ts(optional = nullable)] - pub git_info: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadMetadataGitInfoUpdateParams { - /// Omit to leave the stored commit unchanged, set to `null` to clear it, - /// or provide a non-empty string to replace it. - #[serde( - default, - skip_serializing_if = "Option::is_none", - serialize_with = "super::serde_helpers::serialize_double_option", - deserialize_with = "super::serde_helpers::deserialize_double_option" - )] - #[ts(optional = nullable, type = "string | null")] - pub sha: Option>, - /// Omit to leave the stored branch unchanged, set to `null` to clear it, - /// or provide a non-empty string to replace it. - #[serde( - default, - skip_serializing_if = "Option::is_none", - serialize_with = "super::serde_helpers::serialize_double_option", - deserialize_with = "super::serde_helpers::deserialize_double_option" - )] - #[ts(optional = nullable, type = "string | null")] - pub branch: Option>, - /// Omit to leave the stored origin URL unchanged, set to `null` to clear it, - /// or provide a non-empty string to replace it. - #[serde( - default, - skip_serializing_if = "Option::is_none", - serialize_with = "super::serde_helpers::serialize_double_option", - deserialize_with = "super::serde_helpers::deserialize_double_option" - )] - #[ts(optional = nullable, type = "string | null")] - pub origin_url: Option>, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadMetadataUpdateResponse { - pub thread: Thread, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "lowercase")] -#[ts(rename_all = "lowercase")] -pub enum ThreadMemoryMode { - Enabled, - Disabled, -} - -impl ThreadMemoryMode { - pub fn as_str(self) -> &'static str { - match self { - Self::Enabled => "enabled", - Self::Disabled => "disabled", - } - } - - pub fn to_core(self) -> codex_protocol::protocol::ThreadMemoryMode { - match self { - Self::Enabled => codex_protocol::protocol::ThreadMemoryMode::Enabled, - Self::Disabled => codex_protocol::protocol::ThreadMemoryMode::Disabled, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadMemoryModeSetParams { - pub thread_id: String, - pub mode: ThreadMemoryMode, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadMemoryModeSetResponse {} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct MemoryResetResponse {} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadUnarchiveResponse { - pub thread: Thread, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadCompactStartParams { - pub thread_id: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadCompactStartResponse {} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadShellCommandParams { - pub thread_id: String, - /// Shell command string evaluated by the thread's configured shell. - /// Unlike `command/exec`, this intentionally preserves shell syntax - /// such as pipes, redirects, and quoting. This runs unsandboxed with full - /// access rather than inheriting the thread sandbox policy. - pub command: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadShellCommandResponse {} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadApproveGuardianDeniedActionParams { - pub thread_id: String, - /// Serialized `codex_protocol::protocol::GuardianAssessmentEvent`. - pub event: JsonValue, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadApproveGuardianDeniedActionResponse {} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadBackgroundTerminalsCleanParams { - pub thread_id: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadBackgroundTerminalsCleanResponse {} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadRollbackParams { - pub thread_id: String, - /// The number of turns to drop from the end of the thread. Must be >= 1. - /// - /// This only modifies the thread's history and does not revert local file changes - /// that have been made by the agent. Clients are responsible for reverting these changes. - pub num_turns: u32, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadRollbackResponse { - /// The updated thread after applying the rollback, with `turns` populated. - /// - /// The ThreadItems stored in each Turn are lossy since we explicitly do not - /// persist all agent interactions, such as command executions. This is the same - /// behavior as `thread/resume`. - pub thread: Thread, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadListParams { - /// Opaque pagination cursor returned by a previous call. - #[ts(optional = nullable)] - pub cursor: Option, - /// Optional page size; defaults to a reasonable server-side value. - #[ts(optional = nullable)] - pub limit: Option, - /// Optional sort key; defaults to created_at. - #[ts(optional = nullable)] - pub sort_key: Option, - /// Optional sort direction; defaults to descending (newest first). - #[ts(optional = nullable)] - pub sort_direction: Option, - /// Optional provider filter; when set, only sessions recorded under these - /// providers are returned. When present but empty, includes all providers. - #[ts(optional = nullable)] - pub model_providers: Option>, - /// Optional source filter; when set, only sessions from these source kinds - /// are returned. When omitted or empty, defaults to interactive sources. - #[ts(optional = nullable)] - pub source_kinds: Option>, - /// Optional archived filter; when set to true, only archived threads are returned. - /// If false or null, only non-archived threads are returned. - #[ts(optional = nullable)] - pub archived: Option, - /// Optional cwd filter or filters; when set, only threads whose session cwd - /// exactly matches one of these paths are returned. - #[ts(optional = nullable, type = "string | Array | null")] - pub cwd: Option, - /// If true, return from the state DB without scanning JSONL rollouts to - /// repair thread metadata. Omitted or false preserves scan-and-repair - /// behavior. - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub use_state_db_only: bool, - /// Optional substring filter for the extracted thread title. - #[ts(optional = nullable)] - pub search_term: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] -#[serde(untagged)] -pub enum ThreadListCwdFilter { - One(String), - Many(Vec), -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(rename_all = "camelCase", export_to = "v2/")] -pub enum ThreadSourceKind { - Cli, - #[serde(rename = "vscode")] - #[ts(rename = "vscode")] - VsCode, - Exec, - AppServer, - SubAgent, - SubAgentReview, - SubAgentCompact, - SubAgentThreadSpawn, - SubAgentOther, - Unknown, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] -#[ts(export_to = "v2/")] -pub enum ThreadSortKey { - CreatedAt, - UpdatedAt, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] -#[ts(export_to = "v2/")] -pub enum SortDirection { - Asc, - Desc, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadListResponse { - pub data: 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, - /// Opaque cursor to pass as `cursor` when reversing `sortDirection`. - /// This is only populated when the page contains at least one thread. - /// Use it with the opposite `sortDirection`; for timestamp sorts it anchors - /// at the start of the page timestamp so same-second updates are not skipped. - pub backwards_cursor: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadLoadedListParams { - /// Opaque pagination cursor returned by a previous call. - #[ts(optional = nullable)] - pub cursor: Option, - /// Optional page size; defaults to no limit. - #[ts(optional = nullable)] - pub limit: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadLoadedListResponse { - /// Thread ids for sessions currently loaded in memory. - pub data: 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(tag = "type", rename_all = "camelCase")] -#[ts(tag = "type")] -#[ts(export_to = "v2/")] -pub enum ThreadStatus { - NotLoaded, - Idle, - SystemError, - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - Active { - active_flags: Vec, - }, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum ThreadActiveFlag { - WaitingOnApproval, - WaitingOnUserInput, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadReadParams { - pub thread_id: String, - /// When true, include turns and their items from rollout history. - #[serde(default)] - pub include_turns: bool, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadReadResponse { - pub thread: Thread, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadTurnsListParams { - pub thread_id: String, - /// Opaque cursor to pass to the next call to continue after the last turn. - #[ts(optional = nullable)] - pub cursor: Option, - /// Optional turn page size. - #[ts(optional = nullable)] - pub limit: Option, - /// Optional turn pagination direction; defaults to descending. - #[ts(optional = nullable)] - pub sort_direction: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadTurnsListResponse { - pub data: Vec, - /// Opaque cursor to pass to the next call to continue after the last turn. - /// if None, there are no more turns to return. - pub next_cursor: Option, - /// Opaque cursor to pass as `cursor` when reversing `sortDirection`. - /// This is only populated when the page contains at least one turn. - /// Use it with the opposite `sortDirection` to include the anchor turn again - /// and catch updates to that turn. - pub backwards_cursor: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct SkillsListParams { - /// When empty, defaults to the current session working directory. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub cwds: Vec, - - /// When true, bypass the skills cache and re-scan skills from disk. - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub force_reload: bool, - - /// Optional per-cwd extra roots to scan as user-scoped skills. - #[serde(default)] - #[ts(optional = nullable)] - pub per_cwd_extra_user_roots: Option>, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct SkillsListExtraRootsForCwd { - pub cwd: PathBuf, - pub extra_user_roots: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct SkillsListResponse { - pub data: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct HooksListParams { - /// When empty, defaults to the current session working directory. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub cwds: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct HooksListResponse { - pub data: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct MarketplaceAddParams { - pub source: String, - #[ts(optional = nullable)] - pub ref_name: Option, - #[ts(optional = nullable)] - pub sparse_paths: Option>, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct MarketplaceAddResponse { - pub marketplace_name: String, - pub installed_root: AbsolutePathBuf, - pub already_added: bool, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct MarketplaceRemoveParams { - pub marketplace_name: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct MarketplaceRemoveResponse { - pub marketplace_name: String, - pub installed_root: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct MarketplaceUpgradeParams { - #[ts(optional = nullable)] - pub marketplace_name: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct MarketplaceUpgradeResponse { - pub selected_marketplaces: Vec, - pub upgraded_roots: Vec, - pub errors: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct MarketplaceUpgradeErrorInfo { - pub marketplace_name: String, - pub message: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct PluginListParams { - /// Optional working directories used to discover repo marketplaces. When omitted, - /// only home-scoped marketplaces and the official curated marketplace are considered. - #[ts(optional = nullable)] - pub cwds: Option>, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct PluginListResponse { - pub marketplaces: Vec, - #[serde(default)] - pub marketplace_load_errors: Vec, - #[serde(default)] - pub featured_plugin_ids: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct MarketplaceLoadErrorInfo { - pub marketplace_path: AbsolutePathBuf, - pub message: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct PluginReadParams { - #[ts(optional = nullable)] - pub marketplace_path: Option, - #[ts(optional = nullable)] - pub remote_marketplace_name: Option, - pub plugin_name: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct PluginReadResponse { - pub plugin: PluginDetail, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct PluginSkillReadParams { - pub remote_marketplace_name: String, - pub remote_plugin_id: String, - pub skill_name: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct PluginSkillReadResponse { - pub contents: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct PluginShareSaveParams { - pub plugin_path: AbsolutePathBuf, - #[ts(optional = nullable)] - pub remote_plugin_id: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct PluginShareSaveResponse { - pub remote_plugin_id: String, - pub share_url: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct PluginShareListParams {} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct PluginShareListResponse { - pub data: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct PluginShareDeleteParams { - pub remote_plugin_id: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct PluginShareDeleteResponse {} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct PluginShareListItem { - pub plugin: PluginSummary, - pub share_url: String, - pub local_plugin_path: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] -#[ts(rename_all = "snake_case")] -#[ts(export_to = "v2/")] -pub enum SkillScope { - User, - Repo, - System, - Admin, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct SkillMetadata { - pub name: String, - pub description: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - /// Legacy short_description from SKILL.md. Prefer SKILL.json interface.short_description. - pub short_description: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub interface: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub dependencies: Option, - pub path: AbsolutePathBuf, - pub scope: SkillScope, - pub enabled: bool, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct SkillInterface { - #[ts(optional)] - pub display_name: Option, - #[ts(optional)] - pub short_description: Option, - #[ts(optional)] - pub icon_small: Option, - #[ts(optional)] - pub icon_large: Option, - #[ts(optional)] - pub brand_color: Option, - #[ts(optional)] - pub default_prompt: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct SkillDependencies { - pub tools: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct SkillToolDependency { - #[serde(rename = "type")] - #[ts(rename = "type")] - pub r#type: String, - pub value: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub description: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub transport: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub command: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub url: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct SkillErrorInfo { - pub path: PathBuf, - pub message: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct SkillsListEntry { - pub cwd: PathBuf, - pub skills: Vec, - pub errors: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct HooksListEntry { - pub cwd: PathBuf, - pub hooks: Vec, - pub warnings: Vec, - pub errors: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct HookMetadata { - pub key: String, - pub event_name: HookEventName, - pub handler_type: HookHandlerType, - pub matcher: Option, - pub command: Option, - pub timeout_sec: u64, - pub status_message: Option, - pub source_path: AbsolutePathBuf, - pub source: HookSource, - pub plugin_id: Option, - pub display_order: i64, - pub enabled: bool, - pub is_managed: bool, - pub current_hash: String, - pub trust_status: HookTrustStatus, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct HookErrorInfo { - pub path: PathBuf, - pub message: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct PluginMarketplaceEntry { - pub name: String, - /// Local marketplace file path when the marketplace is backed by a local file. - /// Remote-only catalog marketplaces do not have a local path. - pub path: Option, - pub interface: Option, - pub plugins: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct MarketplaceInterface { - pub display_name: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[ts(export_to = "v2/")] -pub enum PluginInstallPolicy { - #[serde(rename = "NOT_AVAILABLE")] - #[ts(rename = "NOT_AVAILABLE")] - NotAvailable, - #[serde(rename = "AVAILABLE")] - #[ts(rename = "AVAILABLE")] - Available, - #[serde(rename = "INSTALLED_BY_DEFAULT")] - #[ts(rename = "INSTALLED_BY_DEFAULT")] - InstalledByDefault, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[ts(export_to = "v2/")] -pub enum PluginAuthPolicy { - #[serde(rename = "ON_INSTALL")] - #[ts(rename = "ON_INSTALL")] - OnInstall, - #[serde(rename = "ON_USE")] - #[ts(rename = "ON_USE")] - OnUse, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, JsonSchema, TS)] -#[ts(export_to = "v2/")] -pub enum PluginAvailability { - /// Plugin-service currently sends `"ENABLED"` for available remote plugins. - /// Codex app-server exposes `"AVAILABLE"` in its API; the alias keeps - /// decoding compatible with that upstream response. - #[serde(rename = "AVAILABLE", alias = "ENABLED")] - #[ts(rename = "AVAILABLE")] - #[default] - Available, - #[serde(rename = "DISABLED_BY_ADMIN")] - #[ts(rename = "DISABLED_BY_ADMIN")] - DisabledByAdmin, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct PluginSummary { - pub id: String, - pub name: String, - pub source: PluginSource, - pub installed: bool, - pub enabled: bool, - pub install_policy: PluginInstallPolicy, - pub auth_policy: PluginAuthPolicy, - /// Availability state for installing and using the plugin. - #[serde(default)] - pub availability: PluginAvailability, - pub interface: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct PluginDetail { - pub marketplace_name: String, - pub marketplace_path: Option, - pub summary: PluginSummary, - pub description: Option, - pub skills: Vec, - pub apps: Vec, - pub mcp_servers: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct SkillSummary { - pub name: String, - pub description: String, - pub short_description: Option, - pub interface: Option, - pub path: Option, - pub enabled: bool, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct PluginInterface { - pub display_name: Option, - pub short_description: Option, - pub long_description: Option, - pub developer_name: Option, - pub category: Option, - pub capabilities: Vec, - pub website_url: Option, - pub privacy_policy_url: Option, - pub terms_of_service_url: Option, - /// Starter prompts for the plugin. Capped at 3 entries with a maximum of - /// 128 characters per entry. - pub default_prompt: Option>, - pub brand_color: Option, - /// Local composer icon path, resolved from the installed plugin package. - pub composer_icon: Option, - /// Remote composer icon URL from the plugin catalog. - pub composer_icon_url: Option, - /// Local logo path, resolved from the installed plugin package. - pub logo: Option, - /// Remote logo URL from the plugin catalog. - pub logo_url: Option, - /// Local screenshot paths, resolved from the installed plugin package. - pub screenshots: Vec, - /// Remote screenshot URLs from the plugin catalog. - pub screenshot_urls: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(tag = "type", rename_all = "camelCase")] -#[ts(tag = "type")] -#[ts(export_to = "v2/")] -pub enum PluginSource { - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - Local { path: AbsolutePathBuf }, - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - Git { - url: String, - path: Option, - ref_name: Option, - sha: Option, - }, - /// The plugin is available in the remote catalog. Download metadata is - /// kept server-side and is not exposed through the app-server API. - Remote, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct SkillsConfigWriteParams { - /// Path-based selector. - #[ts(optional = nullable)] - pub path: Option, - /// Name-based selector. - #[ts(optional = nullable)] - pub name: Option, - pub enabled: bool, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct SkillsConfigWriteResponse { - pub effective_enabled: bool, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct PluginInstallParams { - #[ts(optional = nullable)] - pub marketplace_path: Option, - #[ts(optional = nullable)] - pub remote_marketplace_name: Option, - pub plugin_name: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct PluginInstallResponse { - pub auth_policy: PluginAuthPolicy, - pub apps_needing_auth: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct PluginUninstallParams { - pub plugin_id: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct PluginUninstallResponse {} - -impl From for SkillMetadata { - fn from(value: CoreSkillMetadata) -> Self { - Self { - name: value.name, - description: value.description, - short_description: value.short_description, - interface: value.interface.map(SkillInterface::from), - dependencies: value.dependencies.map(SkillDependencies::from), - path: value.path, - scope: value.scope.into(), - enabled: true, - } - } -} - -impl From for SkillInterface { - fn from(value: CoreSkillInterface) -> Self { - Self { - display_name: value.display_name, - short_description: value.short_description, - brand_color: value.brand_color, - default_prompt: value.default_prompt, - icon_small: value.icon_small, - icon_large: value.icon_large, - } - } -} - -impl From for SkillDependencies { - fn from(value: CoreSkillDependencies) -> Self { - Self { - tools: value - .tools - .into_iter() - .map(SkillToolDependency::from) - .collect(), - } - } -} - -impl From for SkillToolDependency { - fn from(value: CoreSkillToolDependency) -> Self { - Self { - r#type: value.r#type, - value: value.value, - description: value.description, - transport: value.transport, - command: value.command, - url: value.url, - } - } -} - -impl From for SkillScope { - fn from(value: CoreSkillScope) -> Self { - match value { - CoreSkillScope::User => Self::User, - CoreSkillScope::Repo => Self::Repo, - CoreSkillScope::System => Self::System, - CoreSkillScope::Admin => Self::Admin, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct Thread { - pub id: String, - /// Source thread id when this thread was created by forking another thread. - pub forked_from_id: Option, - /// Usually the first user message in the thread, if available. - pub preview: String, - /// Whether the thread is ephemeral and should not be materialized on disk. - pub ephemeral: bool, - /// Model provider used for this thread (for example, 'openai'). - pub model_provider: String, - /// Unix timestamp (in seconds) when the thread was created. - #[ts(type = "number")] - pub created_at: i64, - /// Unix timestamp (in seconds) when the thread was last updated. - #[ts(type = "number")] - pub updated_at: i64, - /// Current runtime status for the thread. - pub status: ThreadStatus, - /// [UNSTABLE] Path to the thread on disk. - pub path: Option, - /// Working directory captured for the thread. - pub cwd: AbsolutePathBuf, - /// Version of the CLI that created the thread. - pub cli_version: String, - /// Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.). - pub source: SessionSource, - /// Optional random unique nickname assigned to an AgentControl-spawned sub-agent. - pub agent_nickname: Option, - /// Optional role (agent_role) assigned to an AgentControl-spawned sub-agent. - pub agent_role: Option, - /// Optional Git metadata captured when the thread was created. - pub git_info: Option, - /// Optional user-facing thread title. - pub name: Option, - /// Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` - /// (when `includeTurns` is true) responses. - /// For all other responses and notifications returning a Thread, - /// the turns field will be an empty list. - pub turns: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct AccountUpdatedNotification { - pub auth_mode: Option, - pub plan_type: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadTokenUsageUpdatedNotification { - pub thread_id: String, - pub turn_id: String, - pub token_usage: ThreadTokenUsage, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadTokenUsage { - pub total: TokenUsageBreakdown, - pub last: TokenUsageBreakdown, - // TODO(aibrahim): make this not optional - #[ts(type = "number | null")] - pub model_context_window: Option, -} - -impl From for ThreadTokenUsage { - fn from(value: CoreTokenUsageInfo) -> Self { - Self { - total: value.total_token_usage.into(), - last: value.last_token_usage.into(), - model_context_window: value.model_context_window, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct TokenUsageBreakdown { - #[ts(type = "number")] - pub total_tokens: i64, - #[ts(type = "number")] - pub input_tokens: i64, - #[ts(type = "number")] - pub cached_input_tokens: i64, - #[ts(type = "number")] - pub output_tokens: i64, - #[ts(type = "number")] - pub reasoning_output_tokens: i64, -} - -impl From for TokenUsageBreakdown { - fn from(value: CoreTokenUsage) -> Self { - Self { - total_tokens: value.total_tokens, - input_tokens: value.input_tokens, - cached_input_tokens: value.cached_input_tokens, - output_tokens: value.output_tokens, - reasoning_output_tokens: value.reasoning_output_tokens, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct Turn { - pub id: String, - /// Thread items currently included in this turn payload. - pub items: Vec, - /// Describes how much of `items` has been loaded for this turn. - #[serde(default)] - pub items_view: TurnItemsView, - pub status: TurnStatus, - /// Only populated when the Turn's status is failed. - pub error: Option, - /// Unix timestamp (in seconds) when the turn started. - #[ts(type = "number | null")] - pub started_at: Option, - /// Unix timestamp (in seconds) when the turn completed. - #[ts(type = "number | null")] - pub completed_at: Option, - /// Duration between turn start and completion in milliseconds, if known. - #[ts(type = "number | null")] - pub duration_ms: Option, -} - -#[derive(Default, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum TurnItemsView { - /// `items` was not loaded for this turn. The field is intentionally empty. - NotLoaded, - /// `items` contains only a display summary for this turn. - Summary, - /// `items` contains every ThreadItem available from persisted app-server history for this turn. - #[default] - Full, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct MemoryCitation { - pub entries: Vec, - pub thread_ids: Vec, -} - -impl From for MemoryCitation { - fn from(value: CoreMemoryCitation) -> Self { - Self { - entries: value.entries.into_iter().map(Into::into).collect(), - thread_ids: value.rollout_ids, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct MemoryCitationEntry { - pub path: String, - pub line_start: u32, - pub line_end: u32, - pub note: String, -} - -impl From for MemoryCitationEntry { - fn from(value: CoreMemoryCitationEntry) -> Self { - Self { - path: value.path, - line_start: value.line_start, - line_end: value.line_end, - note: value.note, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, Error)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -#[error("{message}")] -pub struct TurnError { - pub message: String, - pub codex_error_info: Option, - #[serde(default)] - pub additional_details: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ErrorNotification { - pub error: TurnError, - // Set to true if the error is transient and the app-server process will automatically retry. - // If true, this will not interrupt a turn. - pub will_retry: bool, - pub thread_id: String, - pub turn_id: String, -} - -/// EXPERIMENTAL - thread realtime audio chunk. -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadRealtimeAudioChunk { - pub data: String, - pub sample_rate: u32, - pub num_channels: u16, - pub samples_per_channel: Option, - pub item_id: Option, -} - -impl From for ThreadRealtimeAudioChunk { - fn from(value: CoreRealtimeAudioFrame) -> Self { - let CoreRealtimeAudioFrame { - data, - sample_rate, - num_channels, - samples_per_channel, - item_id, - } = value; - Self { - data, - sample_rate, - num_channels, - samples_per_channel, - item_id, - } - } -} - -impl From for CoreRealtimeAudioFrame { - fn from(value: ThreadRealtimeAudioChunk) -> Self { - let ThreadRealtimeAudioChunk { - data, - sample_rate, - num_channels, - samples_per_channel, - item_id, - } = value; - Self { - data, - sample_rate, - num_channels, - samples_per_channel, - item_id, - } - } -} - -/// EXPERIMENTAL - start a thread-scoped realtime session. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadRealtimeStartParams { - pub thread_id: String, - /// Selects text or audio output for the realtime session. Transport and voice stay - /// independent so clients can choose how they connect separately from what the model emits. - pub output_modality: RealtimeOutputModality, - #[serde( - default, - deserialize_with = "super::serde_helpers::deserialize_double_option", - serialize_with = "super::serde_helpers::serialize_double_option", - skip_serializing_if = "Option::is_none" - )] - #[ts(optional = nullable)] - pub prompt: Option>, - #[ts(optional = nullable)] - pub realtime_session_id: Option, - #[ts(optional = nullable)] - pub transport: Option, - #[ts(optional = nullable)] - pub voice: Option, -} - -/// EXPERIMENTAL - transport used by thread realtime. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(tag = "type", rename_all = "camelCase")] -#[ts(export_to = "v2/", tag = "type")] -pub enum ThreadRealtimeStartTransport { - Websocket, - Webrtc { - /// SDP offer generated by a WebRTC RTCPeerConnection after configuring audio and the - /// realtime events data channel. - sdp: String, - }, -} - -/// EXPERIMENTAL - response for starting thread realtime. -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadRealtimeStartResponse {} - -/// EXPERIMENTAL - append audio input to thread realtime. -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadRealtimeAppendAudioParams { - pub thread_id: String, - pub audio: ThreadRealtimeAudioChunk, -} - -/// EXPERIMENTAL - response for appending realtime audio input. -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadRealtimeAppendAudioResponse {} - -/// EXPERIMENTAL - append text input to thread realtime. -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadRealtimeAppendTextParams { - pub thread_id: String, - pub text: String, -} - -/// EXPERIMENTAL - response for appending realtime text input. -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadRealtimeAppendTextResponse {} - -/// EXPERIMENTAL - stop thread realtime. -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadRealtimeStopParams { - pub thread_id: String, -} - -/// EXPERIMENTAL - response for stopping thread realtime. -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadRealtimeStopResponse {} - -/// EXPERIMENTAL - list voices supported by thread realtime. -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadRealtimeListVoicesParams {} - -/// EXPERIMENTAL - response for listing supported realtime voices. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadRealtimeListVoicesResponse { - pub voices: RealtimeVoicesList, -} - -/// EXPERIMENTAL - emitted when thread realtime startup is accepted. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadRealtimeStartedNotification { - pub thread_id: String, - pub realtime_session_id: Option, - pub version: RealtimeConversationVersion, -} - -/// EXPERIMENTAL - raw non-audio thread realtime item emitted by the backend. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadRealtimeItemAddedNotification { - pub thread_id: String, - pub item: JsonValue, -} - -/// EXPERIMENTAL - flat transcript delta emitted whenever realtime -/// transcript text changes. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadRealtimeTranscriptDeltaNotification { - pub thread_id: String, - pub role: String, - /// Live transcript delta from the realtime event. - pub delta: String, -} - -/// EXPERIMENTAL - final transcript text emitted when realtime completes -/// a transcript part. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadRealtimeTranscriptDoneNotification { - pub thread_id: String, - pub role: String, - /// Final complete text for the transcript part. - pub text: String, -} - -/// EXPERIMENTAL - streamed output audio emitted by thread realtime. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadRealtimeOutputAudioDeltaNotification { - pub thread_id: String, - pub audio: ThreadRealtimeAudioChunk, -} - -/// EXPERIMENTAL - emitted with the remote SDP for a WebRTC realtime session. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadRealtimeSdpNotification { - pub thread_id: String, - pub sdp: String, -} - -/// EXPERIMENTAL - emitted when thread realtime encounters an error. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadRealtimeErrorNotification { - pub thread_id: String, - pub message: String, -} - -/// EXPERIMENTAL - emitted when thread realtime transport closes. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadRealtimeClosedNotification { - pub thread_id: String, - pub reason: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum TurnStatus { - Completed, - Interrupted, - Failed, - InProgress, -} - -// Turn APIs -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct TurnEnvironmentParams { - pub environment_id: String, - pub cwd: AbsolutePathBuf, -} - -#[derive( - Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS, ExperimentalApi, -)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct TurnStartParams { - pub thread_id: String, - pub input: Vec, - /// Optional turn-scoped Responses API client metadata. - #[experimental("turn/start.responsesapiClientMetadata")] - #[ts(optional = nullable)] - pub responsesapi_client_metadata: Option>, - /// Optional turn-scoped environments. - /// - /// Omitted uses the thread sticky environments. Empty disables - /// environment access for this turn. Non-empty selects the first - /// environment as the current turn environment for this turn. - #[experimental("turn/start.environments")] - #[ts(optional = nullable)] - pub environments: Option>, - /// Override the working directory for this turn and subsequent turns. - #[ts(optional = nullable)] - pub cwd: Option, - /// Override the approval policy for this turn and subsequent turns. - #[experimental(nested)] - #[ts(optional = nullable)] - pub approval_policy: Option, - /// Override where approval requests are routed for review on this turn and - /// subsequent turns. - #[ts(optional = nullable)] - pub approvals_reviewer: Option, - /// Override the sandbox policy for this turn and subsequent turns. - #[ts(optional = nullable)] - pub sandbox_policy: Option, - /// Select a named permissions profile for this turn and subsequent turns. - /// Cannot be combined with `sandboxPolicy`. Use bounded `modifications` - /// for supported turn adjustments instead of replacing the full - /// permissions profile. - #[experimental("turn/start.permissions")] - #[ts(optional = nullable)] - pub permissions: Option, - /// Override the model for this turn and subsequent turns. - #[ts(optional = nullable)] - pub model: Option, - /// Override the service tier for this turn and subsequent turns. - #[serde( - default, - deserialize_with = "super::serde_helpers::deserialize_double_option", - serialize_with = "super::serde_helpers::serialize_double_option", - skip_serializing_if = "Option::is_none" - )] - #[ts(optional = nullable)] - pub service_tier: Option>, - /// Override the reasoning effort for this turn and subsequent turns. - #[ts(optional = nullable)] - pub effort: Option, - /// Override the reasoning summary for this turn and subsequent turns. - #[ts(optional = nullable)] - pub summary: Option, - /// Override the personality for this turn and subsequent turns. - #[ts(optional = nullable)] - pub personality: Option, - /// Optional JSON Schema used to constrain the final assistant message for - /// this turn. - #[ts(optional = nullable)] - pub output_schema: Option, - - /// EXPERIMENTAL - Set a pre-set collaboration mode. - /// Takes precedence over model, reasoning_effort, and developer instructions if set. - /// - /// For `collaboration_mode.settings.developer_instructions`, `null` means - /// "use the built-in instructions for the selected mode". - #[experimental("turn/start.collaborationMode")] - #[ts(optional = nullable)] - pub collaboration_mode: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ReviewStartParams { - pub thread_id: String, - pub target: ReviewTarget, - - /// Where to run the review: inline (default) on the current thread or - /// detached on a new thread (returned in `reviewThreadId`). - #[serde(default)] - #[ts(optional = nullable)] - pub delivery: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ReviewStartResponse { - pub turn: Turn, - /// Identifies the thread where the review runs. - /// - /// For inline reviews, this is the original thread id. - /// For detached reviews, this is the id of the new review thread. - pub review_thread_id: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(tag = "type", rename_all = "camelCase")] -#[ts(tag = "type", export_to = "v2/")] -pub enum ReviewTarget { - /// Review the working tree: staged, unstaged, and untracked files. - UncommittedChanges, - - /// Review changes between the current branch and the given base branch. - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - BaseBranch { branch: String }, - - /// Review the changes introduced by a specific commit. - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - Commit { - sha: String, - /// Optional human-readable label (e.g., commit subject) for UIs. - title: Option, - }, - - /// Arbitrary instructions, equivalent to the old free-form prompt. - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - Custom { instructions: String }, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct TurnStartResponse { - pub turn: Turn, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadInjectItemsParams { - pub thread_id: String, - /// Raw Responses API items to append to the thread's model-visible history. - pub items: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadInjectItemsResponse {} - -#[derive( - Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS, ExperimentalApi, -)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct TurnSteerParams { - pub thread_id: String, - pub input: Vec, - /// Optional turn-scoped Responses API client metadata. - #[experimental("turn/steer.responsesapiClientMetadata")] - #[ts(optional = nullable)] - pub responsesapi_client_metadata: Option>, - /// Required active turn id precondition. The request fails when it does not - /// match the currently active turn. - pub expected_turn_id: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct TurnSteerResponse { - pub turn_id: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct TurnInterruptParams { - pub thread_id: String, - pub turn_id: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct TurnInterruptResponse {} - -// User input types -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ByteRange { - pub start: usize, - pub end: usize, -} - -impl From for ByteRange { - fn from(value: CoreByteRange) -> Self { - Self { - start: value.start, - end: value.end, - } - } -} - -impl From for CoreByteRange { - fn from(value: ByteRange) -> Self { - Self { - start: value.start, - end: value.end, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct TextElement { - /// Byte range in the parent `text` buffer that this element occupies. - pub byte_range: ByteRange, - /// Optional human-readable placeholder for the element, displayed in the UI. - placeholder: Option, -} - -impl TextElement { - pub fn new(byte_range: ByteRange, placeholder: Option) -> Self { - Self { - byte_range, - placeholder, - } - } - - pub fn set_placeholder(&mut self, placeholder: Option) { - self.placeholder = placeholder; - } - - pub fn placeholder(&self) -> Option<&str> { - self.placeholder.as_deref() - } -} - -impl From for TextElement { - fn from(value: CoreTextElement) -> Self { - Self::new( - value.byte_range.into(), - value._placeholder_for_conversion_only().map(str::to_string), - ) - } -} - -impl From for CoreTextElement { - fn from(value: TextElement) -> Self { - Self::new(value.byte_range.into(), value.placeholder) - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(tag = "type", rename_all = "camelCase")] -#[ts(tag = "type")] -#[ts(export_to = "v2/")] -pub enum UserInput { - Text { - text: String, - /// UI-defined spans within `text` used to render or persist special elements. - #[serde(default)] - text_elements: Vec, - }, - Image { - url: String, - }, - LocalImage { - path: PathBuf, - }, - Skill { - name: String, - path: PathBuf, - }, - Mention { - name: String, - path: String, - }, -} - -impl UserInput { - pub fn into_core(self) -> CoreUserInput { - match self { - UserInput::Text { - text, - text_elements, - } => CoreUserInput::Text { - text, - text_elements: text_elements.into_iter().map(Into::into).collect(), - }, - UserInput::Image { url } => CoreUserInput::Image { image_url: url }, - UserInput::LocalImage { path } => CoreUserInput::LocalImage { path }, - UserInput::Skill { name, path } => CoreUserInput::Skill { name, path }, - UserInput::Mention { name, path } => CoreUserInput::Mention { name, path }, - } - } -} - -impl From for UserInput { - fn from(value: CoreUserInput) -> Self { - match value { - CoreUserInput::Text { - text, - text_elements, - } => UserInput::Text { - text, - text_elements: text_elements.into_iter().map(Into::into).collect(), - }, - CoreUserInput::Image { image_url } => UserInput::Image { url: image_url }, - CoreUserInput::LocalImage { path } => UserInput::LocalImage { path }, - CoreUserInput::Skill { name, path } => UserInput::Skill { name, path }, - CoreUserInput::Mention { name, path } => UserInput::Mention { name, path }, - _ => unreachable!("unsupported user input variant"), - } - } -} - -impl UserInput { - pub fn text_char_count(&self) -> usize { - match self { - UserInput::Text { text, .. } => text.chars().count(), - UserInput::Image { .. } - | UserInput::LocalImage { .. } - | UserInput::Skill { .. } - | UserInput::Mention { .. } => 0, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(tag = "type", rename_all = "camelCase")] -#[ts(tag = "type")] -#[ts(export_to = "v2/")] -pub enum ThreadItem { - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - UserMessage { id: String, content: Vec }, - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - HookPrompt { - id: String, - fragments: Vec, - }, - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - AgentMessage { - id: String, - text: String, - #[serde(default)] - phase: Option, - #[serde(default)] - memory_citation: Option, - }, - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - /// EXPERIMENTAL - proposed plan item content. The completed plan item is - /// authoritative and may not match the concatenation of `PlanDelta` text. - Plan { id: String, text: String }, - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - Reasoning { - id: String, - #[serde(default)] - summary: Vec, - #[serde(default)] - content: Vec, - }, - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - CommandExecution { - id: String, - /// The command to be executed. - command: String, - /// The command's working directory. - cwd: AbsolutePathBuf, - /// Identifier for the underlying PTY process (when available). - process_id: Option, - #[serde(default)] - source: CommandExecutionSource, - status: CommandExecutionStatus, - /// A best-effort parsing of the command to understand the action(s) it will perform. - /// This returns a list of CommandAction objects because a single shell command may - /// be composed of many commands piped together. - command_actions: Vec, - /// The command's output, aggregated from stdout and stderr. - aggregated_output: Option, - /// The command's exit code. - exit_code: Option, - /// The duration of the command execution in milliseconds. - #[ts(type = "number | null")] - duration_ms: Option, - }, - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - FileChange { - id: String, - changes: Vec, - status: PatchApplyStatus, - }, - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - McpToolCall { - id: String, - server: String, - tool: String, - status: McpToolCallStatus, - arguments: JsonValue, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - mcp_app_resource_uri: Option, - result: Option>, - error: Option, - /// The duration of the MCP tool call in milliseconds. - #[ts(type = "number | null")] - duration_ms: Option, - }, - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - DynamicToolCall { - id: String, - namespace: Option, - tool: String, - arguments: JsonValue, - status: DynamicToolCallStatus, - content_items: Option>, - success: Option, - /// The duration of the dynamic tool call in milliseconds. - #[ts(type = "number | null")] - duration_ms: Option, - }, - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - CollabAgentToolCall { - /// Unique identifier for this collab tool call. - id: String, - /// Name of the collab tool that was invoked. - tool: CollabAgentTool, - /// Current status of the collab tool call. - status: CollabAgentToolCallStatus, - /// Thread ID of the agent issuing the collab request. - sender_thread_id: String, - /// Thread ID of the receiving agent, when applicable. In case of spawn operation, - /// this corresponds to the newly spawned agent. - receiver_thread_ids: Vec, - /// Prompt text sent as part of the collab tool call, when available. - prompt: Option, - /// Model requested for the spawned agent, when applicable. - model: Option, - /// Reasoning effort requested for the spawned agent, when applicable. - reasoning_effort: Option, - /// Last known status of the target agents, when available. - agents_states: HashMap, - }, - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - WebSearch { - id: String, - query: String, - action: Option, - }, - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - ImageView { id: String, path: AbsolutePathBuf }, - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - ImageGeneration { - id: String, - status: String, - revised_prompt: Option, - result: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - saved_path: Option, - }, - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - EnteredReviewMode { id: String, review: String }, - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - ExitedReviewMode { id: String, review: String }, - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - ContextCompaction { id: String }, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(rename_all = "camelCase", export_to = "v2/")] -pub struct HookPromptFragment { - pub text: String, - pub hook_run_id: String, -} - -impl ThreadItem { - pub fn id(&self) -> &str { - match self { - ThreadItem::UserMessage { id, .. } - | ThreadItem::HookPrompt { id, .. } - | ThreadItem::AgentMessage { id, .. } - | ThreadItem::Plan { id, .. } - | ThreadItem::Reasoning { id, .. } - | ThreadItem::CommandExecution { id, .. } - | ThreadItem::FileChange { id, .. } - | ThreadItem::McpToolCall { id, .. } - | ThreadItem::DynamicToolCall { id, .. } - | ThreadItem::CollabAgentToolCall { id, .. } - | ThreadItem::WebSearch { id, .. } - | ThreadItem::ImageView { id, .. } - | ThreadItem::ImageGeneration { id, .. } - | ThreadItem::EnteredReviewMode { id, .. } - | ThreadItem::ExitedReviewMode { id, .. } - | ThreadItem::ContextCompaction { id, .. } => id, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -/// [UNSTABLE] Lifecycle state for an approval auto-review. -pub enum GuardianApprovalReviewStatus { - InProgress, - Approved, - Denied, - TimedOut, - Aborted, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -/// [UNSTABLE] Source that produced a terminal approval auto-review decision. -pub enum AutoReviewDecisionSource { - Agent, -} - -impl From for AutoReviewDecisionSource { - fn from(value: CoreGuardianAssessmentDecisionSource) -> Self { - match value { - CoreGuardianAssessmentDecisionSource::Agent => Self::Agent, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "lowercase")] -#[ts(export_to = "v2/")] -/// [UNSTABLE] Risk level assigned by approval auto-review. -pub enum GuardianRiskLevel { - Low, - Medium, - High, - Critical, -} - -impl From for GuardianRiskLevel { - fn from(value: CoreGuardianRiskLevel) -> Self { - match value { - CoreGuardianRiskLevel::Low => Self::Low, - CoreGuardianRiskLevel::Medium => Self::Medium, - CoreGuardianRiskLevel::High => Self::High, - CoreGuardianRiskLevel::Critical => Self::Critical, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "lowercase")] -#[ts(export_to = "v2/")] -/// [UNSTABLE] Authorization level assigned by approval auto-review. -pub enum GuardianUserAuthorization { - Unknown, - Low, - Medium, - High, -} - -impl From for GuardianUserAuthorization { - fn from(value: CoreGuardianUserAuthorization) -> Self { - match value { - CoreGuardianUserAuthorization::Unknown => Self::Unknown, - CoreGuardianUserAuthorization::Low => Self::Low, - CoreGuardianUserAuthorization::Medium => Self::Medium, - CoreGuardianUserAuthorization::High => Self::High, - } - } -} - -/// [UNSTABLE] Temporary approval auto-review payload used by -/// `item/autoApprovalReview/*` notifications. This shape is expected to change -/// soon. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct GuardianApprovalReview { - pub status: GuardianApprovalReviewStatus, - pub risk_level: Option, - pub user_authorization: Option, - pub rationale: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum GuardianCommandSource { - Shell, - UnifiedExec, -} - -impl From for GuardianCommandSource { - fn from(value: CoreGuardianCommandSource) -> Self { - match value { - CoreGuardianCommandSource::Shell => Self::Shell, - CoreGuardianCommandSource::UnifiedExec => Self::UnifiedExec, - } - } -} - -impl From for CoreGuardianCommandSource { - fn from(value: GuardianCommandSource) -> Self { - match value { - GuardianCommandSource::Shell => Self::Shell, - GuardianCommandSource::UnifiedExec => Self::UnifiedExec, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct GuardianCommandReviewAction { - pub source: GuardianCommandSource, - pub command: String, - pub cwd: AbsolutePathBuf, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct GuardianExecveReviewAction { - pub source: GuardianCommandSource, - pub program: String, - pub argv: Vec, - pub cwd: AbsolutePathBuf, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct GuardianApplyPatchReviewAction { - pub cwd: AbsolutePathBuf, - pub files: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct GuardianNetworkAccessReviewAction { - pub target: String, - pub host: String, - pub protocol: NetworkApprovalProtocol, - pub port: u16, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct GuardianMcpToolCallReviewAction { - pub server: String, - pub tool_name: String, - pub connector_id: Option, - pub connector_name: Option, - pub tool_title: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct GuardianRequestPermissionsReviewAction { - pub reason: Option, - pub permissions: RequestPermissionProfile, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(tag = "type", rename_all = "camelCase")] -#[ts(tag = "type", rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum GuardianApprovalReviewAction { - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - Command { - source: GuardianCommandSource, - command: String, - cwd: AbsolutePathBuf, - }, - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - Execve { - source: GuardianCommandSource, - program: String, - argv: Vec, - cwd: AbsolutePathBuf, - }, - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - ApplyPatch { - cwd: AbsolutePathBuf, - files: Vec, - }, - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - NetworkAccess { - target: String, - host: String, - protocol: NetworkApprovalProtocol, - port: u16, - }, - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - McpToolCall { - server: String, - tool_name: String, - connector_id: Option, - connector_name: Option, - tool_title: Option, - }, - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - RequestPermissions { - reason: Option, - permissions: RequestPermissionProfile, - }, -} - -impl From for GuardianApprovalReviewAction { - fn from(value: CoreGuardianAssessmentAction) -> Self { - match value { - CoreGuardianAssessmentAction::Command { - source, - command, - cwd, - } => Self::Command { - source: source.into(), - command, - cwd, - }, - CoreGuardianAssessmentAction::Execve { - source, - program, - argv, - cwd, - } => Self::Execve { - source: source.into(), - program, - argv, - cwd, - }, - CoreGuardianAssessmentAction::ApplyPatch { cwd, files } => { - Self::ApplyPatch { cwd, files } - } - CoreGuardianAssessmentAction::NetworkAccess { - target, - host, - protocol, - port, - } => Self::NetworkAccess { - target, - host, - protocol: protocol.into(), - port, - }, - CoreGuardianAssessmentAction::McpToolCall { - server, - tool_name, - connector_id, - connector_name, - tool_title, - } => Self::McpToolCall { - server, - tool_name, - connector_id, - connector_name, - tool_title, - }, - CoreGuardianAssessmentAction::RequestPermissions { - reason, - permissions, - } => Self::RequestPermissions { - reason, - permissions: permissions.into(), - }, - } - } -} - -impl From for CoreGuardianAssessmentAction { - fn from(value: GuardianApprovalReviewAction) -> Self { - match value { - GuardianApprovalReviewAction::Command { - source, - command, - cwd, - } => Self::Command { - source: source.into(), - command, - cwd, - }, - GuardianApprovalReviewAction::Execve { - source, - program, - argv, - cwd, - } => Self::Execve { - source: source.into(), - program, - argv, - cwd, - }, - GuardianApprovalReviewAction::ApplyPatch { cwd, files } => { - Self::ApplyPatch { cwd, files } - } - GuardianApprovalReviewAction::NetworkAccess { - target, - host, - protocol, - port, - } => Self::NetworkAccess { - target, - host, - protocol: protocol.to_core(), - port, - }, - GuardianApprovalReviewAction::McpToolCall { - server, - tool_name, - connector_id, - connector_name, - tool_title, - } => Self::McpToolCall { - server, - tool_name, - connector_id, - connector_name, - tool_title, - }, - GuardianApprovalReviewAction::RequestPermissions { - reason, - permissions, - } => Self::RequestPermissions { - reason, - permissions: permissions.into(), - }, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(tag = "type", rename_all = "camelCase")] -#[ts(tag = "type", rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum WebSearchAction { - Search { - query: Option, - queries: Option>, - }, - OpenPage { - url: Option, - }, - FindInPage { - url: Option, - pattern: Option, - }, - #[serde(other)] - Other, -} - -impl From for WebSearchAction { - fn from(value: codex_protocol::models::WebSearchAction) -> Self { - match value { - codex_protocol::models::WebSearchAction::Search { query, queries } => { - WebSearchAction::Search { query, queries } - } - codex_protocol::models::WebSearchAction::OpenPage { url } => { - WebSearchAction::OpenPage { url } - } - codex_protocol::models::WebSearchAction::FindInPage { url, pattern } => { - WebSearchAction::FindInPage { url, pattern } - } - codex_protocol::models::WebSearchAction::Other => WebSearchAction::Other, - } - } -} - -impl From for ThreadItem { - fn from(value: CoreTurnItem) -> Self { - match value { - CoreTurnItem::UserMessage(user) => ThreadItem::UserMessage { - id: user.id, - content: user.content.into_iter().map(UserInput::from).collect(), - }, - CoreTurnItem::HookPrompt(hook_prompt) => ThreadItem::HookPrompt { - id: hook_prompt.id, - fragments: hook_prompt - .fragments - .into_iter() - .map(HookPromptFragment::from) - .collect(), - }, - CoreTurnItem::AgentMessage(agent) => { - let text = agent - .content - .into_iter() - .map(|entry| match entry { - CoreAgentMessageContent::Text { text } => text, - }) - .collect::(); - ThreadItem::AgentMessage { - id: agent.id, - text, - phase: agent.phase, - memory_citation: agent.memory_citation.map(Into::into), - } - } - CoreTurnItem::Plan(plan) => ThreadItem::Plan { - id: plan.id, - text: plan.text, - }, - CoreTurnItem::Reasoning(reasoning) => ThreadItem::Reasoning { - id: reasoning.id, - summary: reasoning.summary_text, - content: reasoning.raw_content, - }, - CoreTurnItem::WebSearch(search) => ThreadItem::WebSearch { - id: search.id, - query: search.query, - action: Some(WebSearchAction::from(search.action)), - }, - CoreTurnItem::ImageView(image) => ThreadItem::ImageView { - id: image.id, - path: image.path, - }, - CoreTurnItem::ImageGeneration(image) => ThreadItem::ImageGeneration { - id: image.id, - status: image.status, - revised_prompt: image.revised_prompt, - result: image.result, - saved_path: image.saved_path, - }, - CoreTurnItem::FileChange(file_change) => ThreadItem::FileChange { - id: file_change.id, - changes: convert_patch_changes(&file_change.changes), - status: file_change - .status - .as_ref() - .map(PatchApplyStatus::from) - .unwrap_or(PatchApplyStatus::InProgress), - }, - CoreTurnItem::McpToolCall(mcp) => { - let duration_ms = mcp - .duration - .and_then(|duration| i64::try_from(duration.as_millis()).ok()); - - ThreadItem::McpToolCall { - id: mcp.id, - server: mcp.server, - tool: mcp.tool, - status: McpToolCallStatus::from(mcp.status), - arguments: mcp.arguments, - mcp_app_resource_uri: mcp.mcp_app_resource_uri, - result: mcp.result.map(McpToolCallResult::from).map(Box::new), - error: mcp.error.map(McpToolCallError::from), - duration_ms, - } - } - CoreTurnItem::ContextCompaction(compaction) => { - ThreadItem::ContextCompaction { id: compaction.id } - } - } - } -} - -impl From for HookPromptFragment { - fn from(value: codex_protocol::items::HookPromptFragment) -> Self { - Self { - text: value.text, - hook_run_id: value.hook_run_id, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum CommandExecutionStatus { - InProgress, - Completed, - Failed, - Declined, -} - -impl From for CommandExecutionStatus { - fn from(value: CoreExecCommandStatus) -> Self { - Self::from(&value) - } -} - -impl From<&CoreExecCommandStatus> for CommandExecutionStatus { - fn from(value: &CoreExecCommandStatus) -> Self { - match value { - CoreExecCommandStatus::Completed => CommandExecutionStatus::Completed, - CoreExecCommandStatus::Failed => CommandExecutionStatus::Failed, - CoreExecCommandStatus::Declined => CommandExecutionStatus::Declined, - } - } -} - -v2_enum_from_core! { - #[derive(Default)] - pub enum CommandExecutionSource from CoreExecCommandSource { - #[default] - Agent, - UserShell, - UnifiedExecStartup, - UnifiedExecInteraction, - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum CollabAgentTool { - SpawnAgent, - SendInput, - ResumeAgent, - Wait, - CloseAgent, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct FileUpdateChange { - pub path: String, - pub kind: PatchChangeKind, - pub diff: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(tag = "type", rename_all = "camelCase")] -#[ts(tag = "type")] -#[ts(export_to = "v2/")] -pub enum PatchChangeKind { - Add, - Delete, - Update { move_path: Option }, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum PatchApplyStatus { - InProgress, - Completed, - Failed, - Declined, -} - -impl From for PatchApplyStatus { - fn from(value: CorePatchApplyStatus) -> Self { - Self::from(&value) - } -} - -impl From<&CorePatchApplyStatus> for PatchApplyStatus { - fn from(value: &CorePatchApplyStatus) -> Self { - match value { - CorePatchApplyStatus::Completed => PatchApplyStatus::Completed, - CorePatchApplyStatus::Failed => PatchApplyStatus::Failed, - CorePatchApplyStatus::Declined => PatchApplyStatus::Declined, - } - } -} - -impl From for McpToolCallStatus { - fn from(value: CoreMcpToolCallStatus) -> Self { - match value { - CoreMcpToolCallStatus::InProgress => McpToolCallStatus::InProgress, - CoreMcpToolCallStatus::Completed => McpToolCallStatus::Completed, - CoreMcpToolCallStatus::Failed => McpToolCallStatus::Failed, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum McpToolCallStatus { - InProgress, - Completed, - Failed, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum DynamicToolCallStatus { - InProgress, - Completed, - Failed, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum CollabAgentToolCallStatus { - InProgress, - Completed, - Failed, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum CollabAgentStatus { - PendingInit, - Running, - Interrupted, - Completed, - Errored, - Shutdown, - NotFound, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct CollabAgentState { - pub status: CollabAgentStatus, - pub message: Option, -} - -impl From for CollabAgentState { - fn from(value: CoreAgentStatus) -> Self { - match value { - CoreAgentStatus::PendingInit => Self { - status: CollabAgentStatus::PendingInit, - message: None, - }, - CoreAgentStatus::Running => Self { - status: CollabAgentStatus::Running, - message: None, - }, - CoreAgentStatus::Interrupted => Self { - status: CollabAgentStatus::Interrupted, - message: None, - }, - CoreAgentStatus::Completed(message) => Self { - status: CollabAgentStatus::Completed, - message, - }, - CoreAgentStatus::Errored(message) => Self { - status: CollabAgentStatus::Errored, - message: Some(message), - }, - CoreAgentStatus::Shutdown => Self { - status: CollabAgentStatus::Shutdown, - message: None, - }, - CoreAgentStatus::NotFound => Self { - status: CollabAgentStatus::NotFound, - message: None, - }, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct McpToolCallResult { - // NOTE: `rmcp::model::Content` (and its `RawContent` variants) would be a more precise Rust - // representation of MCP content blocks. We intentionally use `serde_json::Value` here because - // this crate exports JSON schema + TS types (`schemars`/`ts-rs`), and the rmcp model types - // aren't set up to be schema/TS friendly (and would introduce heavier coupling to rmcp's Rust - // representations). Using `JsonValue` keeps the payload wire-shaped and easy to export. - pub content: Vec, - pub structured_content: Option, - #[serde(rename = "_meta")] - #[ts(rename = "_meta")] - pub meta: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct McpToolCallError { - pub message: String, -} - -// === Server Notifications === -// Thread/Turn lifecycle notifications and item progress events -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadStartedNotification { - pub thread: Thread, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadStatusChangedNotification { - pub thread_id: String, - pub status: ThreadStatus, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadArchivedNotification { - pub thread_id: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadUnarchivedNotification { - pub thread_id: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadClosedNotification { - pub thread_id: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -/// Notification emitted when watched local skill files change. -/// -/// Treat this as an invalidation signal and re-run `skills/list` with the -/// client's current parameters when refreshed skill metadata is needed. -pub struct SkillsChangedNotification {} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadNameUpdatedNotification { - pub thread_id: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub thread_name: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadGoalUpdatedNotification { - pub thread_id: String, - pub turn_id: Option, - pub goal: ThreadGoal, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ThreadGoalClearedNotification { - pub thread_id: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct TurnStartedNotification { - pub thread_id: String, - pub turn: Turn, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct HookStartedNotification { - pub thread_id: String, - pub turn_id: Option, - pub run: HookRunSummary, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct Usage { - pub input_tokens: i32, - pub cached_input_tokens: i32, - pub output_tokens: i32, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct TurnCompletedNotification { - pub thread_id: String, - pub turn: Turn, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct HookCompletedNotification { - pub thread_id: String, - pub turn_id: Option, - pub run: HookRunSummary, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -/// Notification that the turn-level unified diff has changed. -/// Contains the latest aggregated diff across all file changes in the turn. -pub struct TurnDiffUpdatedNotification { - pub thread_id: String, - pub turn_id: String, - pub diff: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct TurnPlanUpdatedNotification { - pub thread_id: String, - pub turn_id: String, - pub explanation: Option, - pub plan: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct TurnPlanStep { - pub step: String, - pub status: TurnPlanStepStatus, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum TurnPlanStepStatus { - Pending, - InProgress, - Completed, -} - -impl From for TurnPlanStep { - fn from(value: CorePlanItemArg) -> Self { - Self { - step: value.step, - status: value.status.into(), - } - } -} - -impl From for TurnPlanStepStatus { - fn from(value: CorePlanStepStatus) -> Self { - match value { - CorePlanStepStatus::Pending => Self::Pending, - CorePlanStepStatus::InProgress => Self::InProgress, - CorePlanStepStatus::Completed => Self::Completed, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ItemStartedNotification { - pub item: ThreadItem, - pub thread_id: String, - pub turn_id: String, - /// Unix timestamp (in milliseconds) when this item lifecycle started. - #[ts(type = "number")] - pub started_at_ms: i64, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -/// [UNSTABLE] Temporary notification payload for approval auto-review. This -/// shape is expected to change soon. -pub struct ItemGuardianApprovalReviewStartedNotification { - pub thread_id: String, - pub turn_id: String, - /// Stable identifier for this review. - pub review_id: String, - /// Identifier for the reviewed item or tool call when one exists. - /// - /// In most cases, one review maps to one target item. The exceptions are - /// - execve reviews, where a single command may contain multiple execve - /// calls to review (only possible when using the shell_zsh_fork feature) - /// - network policy reviews, where there is no target item - /// - /// A network call is triggered by a CommandExecution item, so having a - /// target_item_id set to the CommandExecution item would be misleading - /// because the review is about the network call, not the command execution. - /// Therefore, target_item_id is set to None for network policy reviews. - pub target_item_id: Option, - pub review: GuardianApprovalReview, - pub action: GuardianApprovalReviewAction, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -/// [UNSTABLE] Temporary notification payload for approval auto-review. This -/// shape is expected to change soon. -pub struct ItemGuardianApprovalReviewCompletedNotification { - pub thread_id: String, - pub turn_id: String, - /// Stable identifier for this review. - pub review_id: String, - /// Identifier for the reviewed item or tool call when one exists. - /// - /// In most cases, one review maps to one target item. The exceptions are - /// - execve reviews, where a single command may contain multiple execve - /// calls to review (only possible when using the shell_zsh_fork feature) - /// - network policy reviews, where there is no target item - /// - /// A network call is triggered by a CommandExecution item, so having a - /// target_item_id set to the CommandExecution item would be misleading - /// because the review is about the network call, not the command execution. - /// Therefore, target_item_id is set to None for network policy reviews. - pub target_item_id: Option, - pub decision_source: AutoReviewDecisionSource, - pub review: GuardianApprovalReview, - pub action: GuardianApprovalReviewAction, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ItemCompletedNotification { - pub item: ThreadItem, - pub thread_id: String, - pub turn_id: String, - /// Unix timestamp (in milliseconds) when this item lifecycle completed. - #[ts(type = "number")] - pub completed_at_ms: i64, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct RawResponseItemCompletedNotification { - pub thread_id: String, - pub turn_id: String, - pub item: ResponseItem, -} - -// Item-specific progress notifications -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct AgentMessageDeltaNotification { - pub thread_id: String, - pub turn_id: String, - pub item_id: String, - pub delta: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -/// EXPERIMENTAL - proposed plan streaming deltas for plan items. Clients should -/// not assume concatenated deltas match the completed plan item content. -pub struct PlanDeltaNotification { - pub thread_id: String, - pub turn_id: String, - pub item_id: String, - pub delta: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ReasoningSummaryTextDeltaNotification { - pub thread_id: String, - pub turn_id: String, - pub item_id: String, - pub delta: String, - #[ts(type = "number")] - pub summary_index: i64, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ReasoningSummaryPartAddedNotification { - pub thread_id: String, - pub turn_id: String, - pub item_id: String, - #[ts(type = "number")] - pub summary_index: i64, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ReasoningTextDeltaNotification { - pub thread_id: String, - pub turn_id: String, - pub item_id: String, - pub delta: String, - #[ts(type = "number")] - pub content_index: i64, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct TerminalInteractionNotification { - pub thread_id: String, - pub turn_id: String, - pub item_id: String, - pub process_id: String, - pub stdin: String, -} - -#[serde_as] -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct CommandExecutionOutputDeltaNotification { - pub thread_id: String, - pub turn_id: String, - pub item_id: String, - pub delta: String, -} - -/// Base64-encoded output chunk emitted for a streaming `command/exec` request. -/// -/// These notifications are connection-scoped. If the originating connection -/// closes, the server terminates the process. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct CommandExecOutputDeltaNotification { - /// Client-supplied, connection-scoped `processId` from the original - /// `command/exec` request. - pub process_id: String, - /// Output stream for this chunk. - pub stream: CommandExecOutputStream, - /// Base64-encoded output bytes. - pub delta_base64: String, - /// `true` on the final streamed chunk for a stream when `outputBytesCap` - /// truncated later output on that stream. - pub cap_reached: bool, -} - -/// Deprecated legacy notification for `apply_patch` textual output. -/// -/// The server no longer emits this notification. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct FileChangeOutputDeltaNotification { - pub thread_id: String, - pub turn_id: String, - pub item_id: String, - pub delta: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct FileChangePatchUpdatedNotification { - pub thread_id: String, - pub turn_id: String, - pub item_id: String, - pub changes: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ServerRequestResolvedNotification { - pub thread_id: String, - pub request_id: RequestId, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct McpToolCallProgressNotification { - pub thread_id: String, - pub turn_id: String, - pub item_id: String, - pub message: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct McpServerOauthLoginCompletedNotification { - pub name: String, - pub success: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub error: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum McpServerStartupState { - Starting, - Ready, - Failed, - Cancelled, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct McpServerStatusUpdatedNotification { - pub name: String, - pub status: McpServerStartupState, - pub error: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct WindowsWorldWritableWarningNotification { - pub sample_paths: Vec, - pub extra_count: usize, - pub failed_scan: bool, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum WindowsSandboxSetupMode { - Elevated, - Unelevated, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum WindowsSandboxReadiness { - Ready, - NotConfigured, - UpdateRequired, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct WindowsSandboxSetupStartParams { - pub mode: WindowsSandboxSetupMode, - #[ts(optional = nullable)] - pub cwd: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct WindowsSandboxSetupStartResponse { - pub started: bool, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct WindowsSandboxReadinessResponse { - pub status: WindowsSandboxReadiness, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct WindowsSandboxSetupCompletedNotification { - pub mode: WindowsSandboxSetupMode, - pub success: bool, - pub error: Option, -} - -/// Deprecated: Use `ContextCompaction` item type instead. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ContextCompactedNotification { - pub thread_id: String, - pub turn_id: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct CommandExecutionRequestApprovalParams { - pub thread_id: String, - pub turn_id: String, - pub item_id: String, - /// Unique identifier for this specific approval callback. - /// - /// For regular shell/unified_exec approvals, this is null. - /// - /// For zsh-exec-bridge subcommand approvals, multiple callbacks can belong to - /// one parent `itemId`, so `approvalId` is a distinct opaque callback id - /// (a UUID) used to disambiguate routing. - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional = nullable)] - pub approval_id: Option, - /// Optional explanatory reason (e.g. request for network access). - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional = nullable)] - pub reason: Option, - /// Optional context for a managed-network approval prompt. - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional = nullable)] - pub network_approval_context: Option, - /// The command to be executed. - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional = nullable)] - pub command: Option, - /// The command's working directory. - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional = nullable)] - pub cwd: Option, - /// Best-effort parsed command actions for friendly display. - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional = nullable)] - pub command_actions: Option>, - /// Optional additional permissions requested for this command. - #[experimental("item/commandExecution/requestApproval.additionalPermissions")] - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional = nullable)] - pub additional_permissions: Option, - /// Optional proposed execpolicy amendment to allow similar commands without prompting. - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional = nullable)] - pub proposed_execpolicy_amendment: Option, - /// Optional proposed network policy amendments (allow/deny host) for future requests. - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional = nullable)] - pub proposed_network_policy_amendments: Option>, - /// Ordered list of decisions the client may present for this prompt. - #[experimental("item/commandExecution/requestApproval.availableDecisions")] - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional = nullable)] - pub available_decisions: Option>, -} - -impl CommandExecutionRequestApprovalParams { - pub fn strip_experimental_fields(&mut self) { - // TODO: Avoid hardcoding individual experimental fields here. - // We need a generic outbound compatibility design for stripping or - // otherwise handling experimental server->client payloads. - self.additional_permissions = None; - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct CommandExecutionRequestApprovalResponse { - pub decision: CommandExecutionApprovalDecision, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct FileChangeRequestApprovalParams { - pub thread_id: String, - pub turn_id: String, - pub item_id: String, - /// Optional explanatory reason (e.g. request for extra write access). - #[ts(optional = nullable)] - pub reason: Option, - /// [UNSTABLE] 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). - #[ts(optional = nullable)] - pub grant_root: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[ts(export_to = "v2/")] -pub struct FileChangeRequestApprovalResponse { - pub decision: FileChangeApprovalDecision, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub enum McpServerElicitationAction { - Accept, - Decline, - Cancel, -} - -impl McpServerElicitationAction { - pub fn to_core(self) -> codex_protocol::approvals::ElicitationAction { - match self { - Self::Accept => codex_protocol::approvals::ElicitationAction::Accept, - Self::Decline => codex_protocol::approvals::ElicitationAction::Decline, - Self::Cancel => codex_protocol::approvals::ElicitationAction::Cancel, - } - } -} - -impl From for rmcp::model::ElicitationAction { - fn from(value: McpServerElicitationAction) -> Self { - match value { - McpServerElicitationAction::Accept => Self::Accept, - McpServerElicitationAction::Decline => Self::Decline, - McpServerElicitationAction::Cancel => Self::Cancel, - } - } -} - -impl From for McpServerElicitationAction { - fn from(value: rmcp::model::ElicitationAction) -> Self { - match value { - rmcp::model::ElicitationAction::Accept => Self::Accept, - rmcp::model::ElicitationAction::Decline => Self::Decline, - rmcp::model::ElicitationAction::Cancel => Self::Cancel, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct McpServerElicitationRequestParams { - pub thread_id: String, - /// Active Codex turn when this elicitation was observed, if app-server could correlate one. - /// - /// This is nullable because MCP models elicitation as a standalone server-to-client request - /// identified by the MCP server request id. It may be triggered during a turn, but turn - /// context is app-server correlation rather than part of the protocol identity of the - /// elicitation itself. - pub turn_id: Option, - pub server_name: String, - #[serde(flatten)] - pub request: McpServerElicitationRequest, - // TODO: When core can correlate an elicitation with an MCP tool call, expose the associated - // McpToolCall item id here as an optional field. The current core event does not carry that - // association. -} - -/// Typed form schema for MCP `elicitation/create` requests. -/// -/// This matches the `requestedSchema` shape from the MCP 2025-11-25 -/// `ElicitRequestFormParams` schema. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -#[ts(export_to = "v2/")] -pub struct McpElicitationSchema { - #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")] - #[ts(optional, rename = "$schema")] - pub schema_uri: Option, - #[serde(rename = "type")] - #[ts(rename = "type")] - pub type_: McpElicitationObjectType, - pub properties: BTreeMap, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub required: Option>, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "lowercase")] -#[ts(export_to = "v2/")] -pub enum McpElicitationObjectType { - Object, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(untagged)] -#[ts(export_to = "v2/")] -pub enum McpElicitationPrimitiveSchema { - Enum(McpElicitationEnumSchema), - String(McpElicitationStringSchema), - Number(McpElicitationNumberSchema), - Boolean(McpElicitationBooleanSchema), -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -#[ts(export_to = "v2/")] -pub struct McpElicitationStringSchema { - #[serde(rename = "type")] - #[ts(rename = "type")] - pub type_: McpElicitationStringType, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub title: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub description: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub min_length: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub max_length: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub format: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub default: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "lowercase")] -#[ts(export_to = "v2/")] -pub enum McpElicitationStringType { - String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "kebab-case")] -#[ts(rename_all = "kebab-case", export_to = "v2/")] -pub enum McpElicitationStringFormat { - Email, - Uri, - Date, - DateTime, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -#[ts(export_to = "v2/")] -pub struct McpElicitationNumberSchema { - #[serde(rename = "type")] - #[ts(rename = "type")] - pub type_: McpElicitationNumberType, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub title: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub description: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub minimum: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub maximum: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub default: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "lowercase")] -#[ts(export_to = "v2/")] -pub enum McpElicitationNumberType { - Number, - Integer, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -#[ts(export_to = "v2/")] -pub struct McpElicitationBooleanSchema { - #[serde(rename = "type")] - #[ts(rename = "type")] - pub type_: McpElicitationBooleanType, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub title: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub description: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub default: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "lowercase")] -#[ts(export_to = "v2/")] -pub enum McpElicitationBooleanType { - Boolean, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(untagged)] -#[ts(export_to = "v2/")] -pub enum McpElicitationEnumSchema { - SingleSelect(McpElicitationSingleSelectEnumSchema), - MultiSelect(McpElicitationMultiSelectEnumSchema), - Legacy(McpElicitationLegacyTitledEnumSchema), -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -#[ts(export_to = "v2/")] -pub struct McpElicitationLegacyTitledEnumSchema { - #[serde(rename = "type")] - #[ts(rename = "type")] - pub type_: McpElicitationStringType, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub title: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub description: Option, - #[serde(rename = "enum")] - #[ts(rename = "enum")] - pub enum_: Vec, - #[serde(rename = "enumNames", skip_serializing_if = "Option::is_none")] - #[ts(optional, rename = "enumNames")] - pub enum_names: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub default: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(untagged)] -#[ts(export_to = "v2/")] -pub enum McpElicitationSingleSelectEnumSchema { - Untitled(McpElicitationUntitledSingleSelectEnumSchema), - Titled(McpElicitationTitledSingleSelectEnumSchema), -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -#[ts(export_to = "v2/")] -pub struct McpElicitationUntitledSingleSelectEnumSchema { - #[serde(rename = "type")] - #[ts(rename = "type")] - pub type_: McpElicitationStringType, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub title: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub description: Option, - #[serde(rename = "enum")] - #[ts(rename = "enum")] - pub enum_: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub default: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -#[ts(export_to = "v2/")] -pub struct McpElicitationTitledSingleSelectEnumSchema { - #[serde(rename = "type")] - #[ts(rename = "type")] - pub type_: McpElicitationStringType, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub title: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub description: Option, - #[serde(rename = "oneOf")] - #[ts(rename = "oneOf")] - pub one_of: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub default: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(untagged)] -#[ts(export_to = "v2/")] -pub enum McpElicitationMultiSelectEnumSchema { - Untitled(McpElicitationUntitledMultiSelectEnumSchema), - Titled(McpElicitationTitledMultiSelectEnumSchema), -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -#[ts(export_to = "v2/")] -pub struct McpElicitationUntitledMultiSelectEnumSchema { - #[serde(rename = "type")] - #[ts(rename = "type")] - pub type_: McpElicitationArrayType, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub title: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub description: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub min_items: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub max_items: Option, - pub items: McpElicitationUntitledEnumItems, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub default: Option>, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -#[ts(export_to = "v2/")] -pub struct McpElicitationTitledMultiSelectEnumSchema { - #[serde(rename = "type")] - #[ts(rename = "type")] - pub type_: McpElicitationArrayType, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub title: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub description: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub min_items: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub max_items: Option, - pub items: McpElicitationTitledEnumItems, - #[serde(skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub default: Option>, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "lowercase")] -#[ts(export_to = "v2/")] -pub enum McpElicitationArrayType { - Array, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(deny_unknown_fields)] -#[ts(export_to = "v2/")] -pub struct McpElicitationUntitledEnumItems { - #[serde(rename = "type")] - #[ts(rename = "type")] - pub type_: McpElicitationStringType, - #[serde(rename = "enum")] - #[ts(rename = "enum")] - pub enum_: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(deny_unknown_fields)] -#[ts(export_to = "v2/")] -pub struct McpElicitationTitledEnumItems { - #[serde(rename = "anyOf", alias = "oneOf")] - #[ts(rename = "anyOf")] - pub any_of: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(deny_unknown_fields)] -#[ts(export_to = "v2/")] -pub struct McpElicitationConstOption { - #[serde(rename = "const")] - #[ts(rename = "const")] - pub const_: String, - pub title: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(tag = "mode", rename_all = "camelCase")] -#[ts(tag = "mode")] -#[ts(export_to = "v2/")] -pub enum McpServerElicitationRequest { - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - Form { - #[serde(rename = "_meta")] - #[ts(rename = "_meta")] - meta: Option, - message: String, - requested_schema: McpElicitationSchema, - }, - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - Url { - #[serde(rename = "_meta")] - #[ts(rename = "_meta")] - meta: Option, - message: String, - url: String, - elicitation_id: String, - }, -} - -impl TryFrom for McpServerElicitationRequest { - type Error = serde_json::Error; - - fn try_from(value: CoreElicitationRequest) -> Result { - match value { - CoreElicitationRequest::Form { - meta, - message, - requested_schema, - } => Ok(Self::Form { - meta, - message, - requested_schema: serde_json::from_value(requested_schema)?, - }), - CoreElicitationRequest::Url { - meta, - message, - url, - elicitation_id, - } => Ok(Self::Url { - meta, - message, - url, - elicitation_id, - }), - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct McpServerElicitationRequestResponse { - pub action: McpServerElicitationAction, - /// Structured user input for accepted elicitations, mirroring RMCP `CreateElicitationResult`. - /// - /// This is nullable because decline/cancel responses have no content. - pub content: Option, - /// Optional client metadata for form-mode action handling. - #[serde(rename = "_meta")] - #[ts(rename = "_meta")] - pub meta: Option, -} - -impl From for rmcp::model::CreateElicitationResult { - fn from(value: McpServerElicitationRequestResponse) -> Self { - Self { - action: value.action.into(), - content: value.content, - } - } -} - -impl From for McpServerElicitationRequestResponse { - fn from(value: rmcp::model::CreateElicitationResult) -> Self { - Self { - action: value.action.into(), - content: value.content, - meta: None, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct DynamicToolCallParams { - pub thread_id: String, - pub turn_id: String, - pub call_id: String, - pub namespace: Option, - pub tool: String, - pub arguments: JsonValue, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct PermissionsRequestApprovalParams { - pub thread_id: String, - pub turn_id: String, - pub item_id: String, - pub cwd: AbsolutePathBuf, - pub reason: Option, - pub permissions: RequestPermissionProfile, -} - -v2_enum_from_core!( - #[derive(Default)] - pub enum PermissionGrantScope from CorePermissionGrantScope { - #[default] - Turn, - Session - } -); - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct PermissionsRequestApprovalResponse { - pub permissions: GrantedPermissionProfile, - #[serde(default)] - pub scope: PermissionGrantScope, - /// Review every subsequent command in this turn before normal sandboxed execution. - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub strict_auto_review: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct DynamicToolCallResponse { - pub content_items: Vec, - pub success: bool, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(tag = "type", rename_all = "camelCase")] -#[ts(tag = "type")] -#[ts(export_to = "v2/")] -pub enum DynamicToolCallOutputContentItem { - #[serde(rename_all = "camelCase")] - InputText { text: String }, - #[serde(rename_all = "camelCase")] - InputImage { image_url: String }, -} - -impl From - for codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem -{ - fn from(item: DynamicToolCallOutputContentItem) -> Self { - match item { - DynamicToolCallOutputContentItem::InputText { text } => Self::InputText { text }, - DynamicToolCallOutputContentItem::InputImage { image_url } => { - Self::InputImage { image_url } - } - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -/// EXPERIMENTAL. Defines a single selectable option for request_user_input. -pub struct ToolRequestUserInputOption { - pub label: String, - pub description: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -/// EXPERIMENTAL. Represents one request_user_input question and its required options. -pub struct ToolRequestUserInputQuestion { - pub id: String, - pub header: String, - pub question: String, - #[serde(default)] - pub is_other: bool, - #[serde(default)] - pub is_secret: bool, - pub options: Option>, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -/// EXPERIMENTAL. Params sent with a request_user_input event. -pub struct ToolRequestUserInputParams { - pub thread_id: String, - pub turn_id: String, - pub item_id: String, - pub questions: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -/// EXPERIMENTAL. Captures a user's answer to a request_user_input question. -pub struct ToolRequestUserInputAnswer { - pub answers: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -/// EXPERIMENTAL. Response payload mapping question ids to answers. -pub struct ToolRequestUserInputResponse { - pub answers: HashMap, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct AccountRateLimitsUpdatedNotification { - pub rate_limits: RateLimitSnapshot, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct RateLimitSnapshot { - pub limit_id: Option, - pub limit_name: Option, - pub primary: Option, - pub secondary: Option, - pub credits: Option, - pub plan_type: Option, - pub rate_limit_reached_type: Option, -} - -impl From for RateLimitSnapshot { - fn from(value: CoreRateLimitSnapshot) -> Self { - Self { - limit_id: value.limit_id, - limit_name: value.limit_name, - primary: value.primary.map(RateLimitWindow::from), - secondary: value.secondary.map(RateLimitWindow::from), - credits: value.credits.map(CreditsSnapshot::from), - plan_type: value.plan_type, - rate_limit_reached_type: value - .rate_limit_reached_type - .map(RateLimitReachedType::from), - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] -#[ts(export_to = "v2/", rename_all = "snake_case")] -pub enum RateLimitReachedType { - RateLimitReached, - WorkspaceOwnerCreditsDepleted, - WorkspaceMemberCreditsDepleted, - WorkspaceOwnerUsageLimitReached, - WorkspaceMemberUsageLimitReached, -} - -impl From for RateLimitReachedType { - fn from(value: CoreRateLimitReachedType) -> Self { - match value { - CoreRateLimitReachedType::RateLimitReached => Self::RateLimitReached, - CoreRateLimitReachedType::WorkspaceOwnerCreditsDepleted => { - Self::WorkspaceOwnerCreditsDepleted - } - CoreRateLimitReachedType::WorkspaceMemberCreditsDepleted => { - Self::WorkspaceMemberCreditsDepleted - } - CoreRateLimitReachedType::WorkspaceOwnerUsageLimitReached => { - Self::WorkspaceOwnerUsageLimitReached - } - CoreRateLimitReachedType::WorkspaceMemberUsageLimitReached => { - Self::WorkspaceMemberUsageLimitReached - } - } - } -} - -impl From for CoreRateLimitReachedType { - fn from(value: RateLimitReachedType) -> Self { - match value { - RateLimitReachedType::RateLimitReached => Self::RateLimitReached, - RateLimitReachedType::WorkspaceOwnerCreditsDepleted => { - Self::WorkspaceOwnerCreditsDepleted - } - RateLimitReachedType::WorkspaceMemberCreditsDepleted => { - Self::WorkspaceMemberCreditsDepleted - } - RateLimitReachedType::WorkspaceOwnerUsageLimitReached => { - Self::WorkspaceOwnerUsageLimitReached - } - RateLimitReachedType::WorkspaceMemberUsageLimitReached => { - Self::WorkspaceMemberUsageLimitReached - } - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct RateLimitWindow { - pub used_percent: i32, - #[ts(type = "number | null")] - pub window_duration_mins: Option, - #[ts(type = "number | null")] - pub resets_at: Option, -} - -impl From for RateLimitWindow { - fn from(value: CoreRateLimitWindow) -> Self { - Self { - used_percent: value.used_percent.round() as i32, - window_duration_mins: value.window_minutes, - resets_at: value.resets_at, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct CreditsSnapshot { - pub has_credits: bool, - pub unlimited: bool, - pub balance: Option, -} - -impl From for CreditsSnapshot { - fn from(value: CoreCreditsSnapshot) -> Self { - Self { - has_credits: value.has_credits, - unlimited: value.unlimited, - balance: value.balance, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct AccountLoginCompletedNotification { - // Use plain String for identifiers to avoid TS/JSON Schema quirks around uuid-specific types. - // Convert to/from UUIDs at the application layer as needed. - pub login_id: Option, - pub success: bool, - pub error: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ModelReroutedNotification { - pub thread_id: String, - pub turn_id: String, - pub from_model: String, - pub to_model: String, - pub reason: ModelRerouteReason, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ModelVerificationNotification { - pub thread_id: String, - pub turn_id: String, - pub verifications: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct DeprecationNoticeNotification { - /// Concise summary of what is deprecated. - pub summary: String, - /// Optional extra guidance, such as migration steps or rationale. - pub details: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct WarningNotification { - /// Optional thread target when the warning applies to a specific thread. - pub thread_id: Option, - /// Concise warning message for the user. - pub message: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct GuardianWarningNotification { - /// Thread target for the guardian warning. - pub thread_id: String, - /// Concise guardian warning message for the user. - pub message: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct TextPosition { - /// 1-based line number. - pub line: usize, - /// 1-based column number (in Unicode scalar values). - pub column: usize, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct TextRange { - pub start: TextPosition, - pub end: TextPosition, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct ConfigWarningNotification { - /// Concise summary of the warning. - pub summary: String, - /// Optional extra guidance or error details. - pub details: Option, - /// Optional path to the config file that triggered the warning. - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub path: Option, - /// Optional range for the error location inside the config file. - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub range: Option, -} - -#[cfg(test)] -mod tests { - use super::*; - use codex_protocol::items::AgentMessageContent; - use codex_protocol::items::AgentMessageItem; - use codex_protocol::items::FileChangeItem; - use codex_protocol::items::ImageViewItem; - use codex_protocol::items::McpToolCallItem; - use codex_protocol::items::McpToolCallStatus as CoreMcpToolCallStatus; - use codex_protocol::items::ReasoningItem; - use codex_protocol::items::TurnItem; - use codex_protocol::items::UserMessageItem; - use codex_protocol::items::WebSearchItem; - use codex_protocol::mcp::CallToolResult; - use codex_protocol::models::WebSearchAction as CoreWebSearchAction; - use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess; - use codex_protocol::user_input::UserInput as CoreUserInput; - use codex_utils_absolute_path::test_support::PathBufExt; - use codex_utils_absolute_path::test_support::test_path_buf; - use pretty_assertions::assert_eq; - use serde_json::json; - use std::num::NonZeroUsize; - use std::path::PathBuf; - use std::time::Duration; - - fn absolute_path_string(path: &str) -> String { - let path = format!("/{}", path.trim_start_matches('/')); - test_path_buf(&path).display().to_string() - } - - fn absolute_path(path: &str) -> AbsolutePathBuf { - let path = format!("/{}", path.trim_start_matches('/')); - test_path_buf(&path).abs() - } - - fn test_absolute_path() -> AbsolutePathBuf { - absolute_path("readable") - } - - #[test] - fn approvals_reviewer_serializes_auto_review_and_accepts_legacy_guardian_subagent() { - assert_eq!( - serde_json::to_string(&ApprovalsReviewer::User).expect("serialize reviewer"), - "\"user\"" - ); - assert_eq!( - serde_json::to_string(&ApprovalsReviewer::AutoReview).expect("serialize reviewer"), - "\"guardian_subagent\"" - ); - - for value in ["user", "auto_review", "guardian_subagent"] { - let json = format!("\"{value}\""); - let reviewer: ApprovalsReviewer = - serde_json::from_str(&json).expect("deserialize reviewer"); - let expected = if value == "user" { - ApprovalsReviewer::User - } else { - ApprovalsReviewer::AutoReview - }; - assert_eq!(expected, reviewer); - } - } - - #[test] - fn turn_defaults_legacy_missing_items_view_to_full() { - let turn: Turn = serde_json::from_value(json!({ - "id": "turn_123", - "items": [], - "status": "completed", - "error": null, - "startedAt": null, - "completedAt": null, - "durationMs": null, - })) - .expect("legacy turn should deserialize"); - - assert_eq!(turn.items_view, TurnItemsView::Full); - } - - #[test] - fn thread_list_params_accepts_single_cwd() { - let params = serde_json::from_value::(json!({ - "cwd": "/workspace", - })) - .expect("single cwd should deserialize"); - - assert_eq!( - params.cwd, - Some(ThreadListCwdFilter::One("/workspace".to_string())) - ); - assert!(!params.use_state_db_only); - } - - #[test] - fn thread_list_params_accepts_multiple_cwds() { - let params = serde_json::from_value::(json!({ - "cwd": ["/workspace", "/other-workspace"], - })) - .expect("cwd array should deserialize"); - - assert_eq!( - params.cwd, - Some(ThreadListCwdFilter::Many(vec![ - "/workspace".to_string(), - "/other-workspace".to_string(), - ])) - ); - } - - #[test] - fn thread_list_params_accepts_state_db_only_flag() { - let params = serde_json::from_value::(json!({ - "useStateDbOnly": true, - })) - .expect("state db only flag should deserialize"); - - assert!(params.use_state_db_only); - } - - #[test] - fn collab_agent_state_maps_interrupted_status() { - assert_eq!( - CollabAgentState::from(CoreAgentStatus::Interrupted), - CollabAgentState { - status: CollabAgentStatus::Interrupted, - message: None, - } - ); - } - - #[test] - fn external_agent_config_plugins_details_round_trip() { - let item: ExternalAgentConfigMigrationItem = serde_json::from_value(json!({ - "itemType": "PLUGINS", - "description": "Install supported plugins from Claude settings", - "cwd": absolute_path_string("repo"), - "details": { - "plugins": [ - { - "marketplaceName": "team-marketplace", - "pluginNames": ["asana"] - } - ] - } - })) - .expect("plugins migration item should deserialize"); - - assert_eq!( - item, - ExternalAgentConfigMigrationItem { - item_type: ExternalAgentConfigMigrationItemType::Plugins, - description: "Install supported plugins from Claude settings".to_string(), - cwd: Some(PathBuf::from(absolute_path_string("repo"))), - details: Some(MigrationDetails { - plugins: vec![PluginsMigration { - marketplace_name: "team-marketplace".to_string(), - plugin_names: vec!["asana".to_string()], - }], - ..Default::default() - }), - } - ); - } - - #[test] - fn external_agent_config_import_params_accept_legacy_plugin_details() { - let params: ExternalAgentConfigImportParams = serde_json::from_value(json!({ - "migrationItems": [{ - "itemType": "PLUGINS", - "description": "Install supported plugins from Claude settings", - "cwd": absolute_path_string("repo"), - "details": { - "plugins": [ - { - "marketplaceName": "team-marketplace", - "pluginNames": ["asana"] - } - ] - } - }] - })) - .expect("legacy plugin import params should deserialize"); - - assert_eq!( - params, - ExternalAgentConfigImportParams { - migration_items: vec![ExternalAgentConfigMigrationItem { - item_type: ExternalAgentConfigMigrationItemType::Plugins, - description: "Install supported plugins from Claude settings".to_string(), - cwd: Some(PathBuf::from(absolute_path_string("repo"))), - details: Some(MigrationDetails { - plugins: vec![PluginsMigration { - marketplace_name: "team-marketplace".to_string(), - plugin_names: vec!["asana".to_string()], - }], - ..Default::default() - }), - }], - } - ); - } - - #[test] - fn command_execution_request_approval_rejects_relative_additional_permission_paths() { - let err = serde_json::from_value::(json!({ - "threadId": "thr_123", - "turnId": "turn_123", - "itemId": "call_123", - "command": "cat file", - "cwd": absolute_path_string("tmp"), - "commandActions": null, - "reason": null, - "networkApprovalContext": null, - "additionalPermissions": { - "network": null, - "fileSystem": { - "read": ["relative/path"], - "write": null - } - }, - "proposedExecpolicyAmendment": null, - "proposedNetworkPolicyAmendments": null, - "availableDecisions": null - })) - .expect_err("relative additional permission paths should fail"); - assert!( - err.to_string() - .contains("AbsolutePathBuf deserialized without a base path"), - "unexpected error: {err}" - ); - } - - #[test] - fn permissions_request_approval_uses_request_permission_profile() { - let read_only_path = if cfg!(windows) { - r"C:\tmp\read-only" - } else { - "/tmp/read-only" - }; - let read_write_path = if cfg!(windows) { - r"C:\tmp\read-write" - } else { - "/tmp/read-write" - }; - let params = serde_json::from_value::(json!({ - "threadId": "thr_123", - "turnId": "turn_123", - "itemId": "call_123", - "cwd": absolute_path_string("repo"), - "reason": "Select a workspace root", - "permissions": { - "network": { - "enabled": true, - }, - "fileSystem": { - "read": [read_only_path], - "write": [read_write_path], - }, - }, - })) - .expect("permissions request should deserialize"); - - assert_eq!(params.cwd, absolute_path("repo")); - assert_eq!( - params.permissions, - RequestPermissionProfile { - network: Some(AdditionalNetworkPermissions { - enabled: Some(true), - }), - file_system: Some(AdditionalFileSystemPermissions { - read: Some(vec![ - AbsolutePathBuf::try_from(PathBuf::from(read_only_path)) - .expect("path must be absolute"), - ]), - write: Some(vec![ - AbsolutePathBuf::try_from(PathBuf::from(read_write_path)) - .expect("path must be absolute"), - ]), - glob_scan_max_depth: None, - entries: None, - }), - } - ); - - assert_eq!( - CoreRequestPermissionProfile::from(params.permissions), - CoreRequestPermissionProfile { - network: Some(CoreNetworkPermissions { - enabled: Some(true), - }), - file_system: Some(CoreFileSystemPermissions::from_read_write_roots( - Some(vec![ - AbsolutePathBuf::try_from(PathBuf::from(read_only_path)) - .expect("path must be absolute"), - ]), - Some(vec![ - AbsolutePathBuf::try_from(PathBuf::from(read_write_path)) - .expect("path must be absolute"), - ]), - )), - } - ); - } - - #[test] - fn permissions_request_approval_rejects_macos_permissions() { - let err = serde_json::from_value::(json!({ - "threadId": "thr_123", - "turnId": "turn_123", - "itemId": "call_123", - "cwd": absolute_path_string("repo"), - "reason": "Select a workspace root", - "permissions": { - "network": null, - "fileSystem": null, - "macos": { - "preferences": "read_only", - "automations": "none", - "launchServices": false, - "accessibility": false, - "calendar": false, - "reminders": false, - "contacts": "none", - }, - }, - })) - .expect_err("permissions request should reject macos permissions"); - - assert!( - err.to_string().contains("unknown field `macos`"), - "unexpected error: {err}" - ); - } - - #[test] - fn additional_file_system_permissions_preserves_canonical_entries() { - let core_permissions = CoreFileSystemPermissions { - entries: vec![ - CoreFileSystemSandboxEntry { - path: CoreFileSystemPath::Special { - value: CoreFileSystemSpecialPath::Root, - }, - access: CoreFileSystemAccessMode::Write, - }, - CoreFileSystemSandboxEntry { - path: CoreFileSystemPath::GlobPattern { - pattern: "**/*.env".to_string(), - }, - access: CoreFileSystemAccessMode::None, - }, - ], - glob_scan_max_depth: NonZeroUsize::new(2), - }; - - let permissions = AdditionalFileSystemPermissions::from(core_permissions.clone()); - assert_eq!( - permissions, - AdditionalFileSystemPermissions { - read: None, - write: None, - glob_scan_max_depth: NonZeroUsize::new(2), - entries: Some(vec![ - FileSystemSandboxEntry { - path: FileSystemPath::Special { - value: FileSystemSpecialPath::Root, - }, - access: FileSystemAccessMode::Write, - }, - FileSystemSandboxEntry { - path: FileSystemPath::GlobPattern { - pattern: "**/*.env".to_string(), - }, - access: FileSystemAccessMode::None, - }, - ]), - } - ); - assert_eq!( - CoreFileSystemPermissions::from(permissions), - core_permissions - ); - } - - #[test] - fn additional_file_system_permissions_populates_entries_for_legacy_roots() { - let read_only_path = absolute_path("read-only"); - let read_write_path = absolute_path("read-write"); - let core_permissions = CoreFileSystemPermissions::from_read_write_roots( - Some(vec![read_only_path.clone()]), - Some(vec![read_write_path.clone()]), - ); - - let permissions = AdditionalFileSystemPermissions::from(core_permissions.clone()); - - assert_eq!( - permissions, - AdditionalFileSystemPermissions { - read: Some(vec![read_only_path.clone()]), - write: Some(vec![read_write_path.clone()]), - glob_scan_max_depth: None, - entries: Some(vec![ - FileSystemSandboxEntry { - path: FileSystemPath::Path { - path: read_only_path, - }, - access: FileSystemAccessMode::Read, - }, - FileSystemSandboxEntry { - path: FileSystemPath::Path { - path: read_write_path, - }, - access: FileSystemAccessMode::Write, - }, - ]), - } - ); - assert_eq!( - CoreFileSystemPermissions::from(permissions), - core_permissions - ); - } - - #[test] - fn additional_file_system_permissions_rejects_zero_glob_scan_depth() { - serde_json::from_value::(json!({ - "read": null, - "write": null, - "globScanMaxDepth": 0, - "entries": [], - })) - .expect_err("zero glob scan depth should fail deserialization"); - } - - #[test] - fn permission_profile_file_system_permissions_preserves_glob_scan_depth() { - let core_permissions = CoreManagedFileSystemPermissions::Restricted { - entries: vec![CoreFileSystemSandboxEntry { - path: CoreFileSystemPath::GlobPattern { - pattern: "**/*.env".to_string(), - }, - access: CoreFileSystemAccessMode::None, - }], - glob_scan_max_depth: NonZeroUsize::new(2), - }; - - let permissions = PermissionProfileFileSystemPermissions::from(core_permissions.clone()); - - assert_eq!( - permissions, - PermissionProfileFileSystemPermissions::Restricted { - entries: vec![FileSystemSandboxEntry { - path: FileSystemPath::GlobPattern { - pattern: "**/*.env".to_string(), - }, - access: FileSystemAccessMode::None, - }], - glob_scan_max_depth: NonZeroUsize::new(2), - } - ); - assert_eq!( - CoreManagedFileSystemPermissions::from(permissions), - core_permissions - ); - } - - #[test] - fn permission_profile_file_system_permissions_rejects_zero_glob_scan_depth() { - serde_json::from_value::(json!({ - "type": "restricted", - "entries": [], - "globScanMaxDepth": 0, - })) - .expect_err("zero glob scan depth should fail deserialization"); - } - - #[test] - fn legacy_current_working_directory_special_path_deserializes_as_project_roots() { - let special_path = serde_json::from_value::(json!({ - "kind": "current_working_directory", - })) - .expect("legacy cwd special path should deserialize"); - - assert_eq!( - special_path, - FileSystemSpecialPath::ProjectRoots { subpath: None } - ); - assert_eq!( - serde_json::to_value(&special_path).expect("serialize special path"), - json!({ - "kind": "project_roots", - "subpath": null, - }) - ); - } - - #[test] - fn permissions_request_approval_response_uses_granted_permission_profile_without_macos() { - let read_only_path = if cfg!(windows) { - r"C:\tmp\read-only" - } else { - "/tmp/read-only" - }; - let read_write_path = if cfg!(windows) { - r"C:\tmp\read-write" - } else { - "/tmp/read-write" - }; - let response = serde_json::from_value::(json!({ - "permissions": { - "network": { - "enabled": true, - }, - "fileSystem": { - "read": [read_only_path], - "write": [read_write_path], - }, - }, - })) - .expect("permissions response should deserialize"); - - assert_eq!( - response.permissions, - GrantedPermissionProfile { - network: Some(AdditionalNetworkPermissions { - enabled: Some(true), - }), - file_system: Some(AdditionalFileSystemPermissions { - read: Some(vec![ - AbsolutePathBuf::try_from(PathBuf::from(read_only_path)) - .expect("path must be absolute"), - ]), - write: Some(vec![ - AbsolutePathBuf::try_from(PathBuf::from(read_write_path)) - .expect("path must be absolute"), - ]), - glob_scan_max_depth: None, - entries: None, - }), - } - ); - - assert_eq!( - CoreAdditionalPermissionProfile::from(response.permissions), - CoreAdditionalPermissionProfile { - network: Some(CoreNetworkPermissions { - enabled: Some(true), - }), - file_system: Some(CoreFileSystemPermissions::from_read_write_roots( - Some(vec![ - AbsolutePathBuf::try_from(PathBuf::from(read_only_path)) - .expect("path must be absolute"), - ]), - Some(vec![ - AbsolutePathBuf::try_from(PathBuf::from(read_write_path)) - .expect("path must be absolute"), - ]), - )), - } - ); - } - - #[test] - fn permissions_request_approval_response_defaults_scope_to_turn() { - let response = serde_json::from_value::(json!({ - "permissions": {}, - })) - .expect("response should deserialize"); - - assert_eq!(response.scope, PermissionGrantScope::Turn); - assert_eq!(response.strict_auto_review, None); - } - - #[test] - fn permissions_request_approval_response_accepts_strict_auto_review() { - let response = serde_json::from_value::(json!({ - "permissions": {}, - "strictAutoReview": true, - })) - .expect("response should deserialize"); - - assert_eq!(response.strict_auto_review, Some(true)); - } - - #[test] - fn fs_get_metadata_response_round_trips_minimal_fields() { - let response = FsGetMetadataResponse { - is_directory: false, - is_file: true, - is_symlink: false, - created_at_ms: 123, - modified_at_ms: 456, - }; - - let value = serde_json::to_value(&response).expect("serialize fs/getMetadata response"); - assert_eq!( - value, - json!({ - "isDirectory": false, - "isFile": true, - "isSymlink": false, - "createdAtMs": 123, - "modifiedAtMs": 456, - }) - ); - - let decoded = serde_json::from_value::(value) - .expect("deserialize fs/getMetadata response"); - assert_eq!(decoded, response); - } - - #[test] - fn fs_read_file_response_round_trips_base64_data() { - let response = FsReadFileResponse { - data_base64: "aGVsbG8=".to_string(), - }; - - let value = serde_json::to_value(&response).expect("serialize fs/readFile response"); - assert_eq!( - value, - json!({ - "dataBase64": "aGVsbG8=", - }) - ); - - let decoded = serde_json::from_value::(value) - .expect("deserialize fs/readFile response"); - assert_eq!(decoded, response); - } - - #[test] - fn fs_read_file_params_round_trip() { - let params = FsReadFileParams { - path: absolute_path("tmp/example.txt"), - }; - - let value = serde_json::to_value(¶ms).expect("serialize fs/readFile params"); - assert_eq!( - value, - json!({ - "path": absolute_path_string("tmp/example.txt"), - }) - ); - - let decoded = serde_json::from_value::(value) - .expect("deserialize fs/readFile params"); - assert_eq!(decoded, params); - } - - #[test] - fn device_key_create_params_round_trip_uses_protection_policy() { - let params = DeviceKeyCreateParams { - protection_policy: None, - account_user_id: "account-user-1".to_string(), - client_id: "cli_123".to_string(), - }; - - let value = serde_json::to_value(¶ms).expect("serialize device/key/create params"); - assert_eq!( - value, - json!({ - "accountUserId": "account-user-1", - "clientId": "cli_123", - "protectionPolicy": null, - }) - ); - - let decoded = serde_json::from_value::(value) - .expect("deserialize device/key/create params"); - assert_eq!(decoded, params); - - let params = DeviceKeyCreateParams { - protection_policy: Some(DeviceKeyProtectionPolicy::AllowOsProtectedNonextractable), - account_user_id: "account-user-1".to_string(), - client_id: "cli_123".to_string(), - }; - let value = serde_json::to_value(¶ms) - .expect("serialize device/key/create params with protection policy"); - assert_eq!( - value, - json!({ - "accountUserId": "account-user-1", - "clientId": "cli_123", - "protectionPolicy": "allow_os_protected_nonextractable", - }) - ); - } - - #[test] - fn device_key_create_response_round_trips_protection_class() { - let response = DeviceKeyCreateResponse { - key_id: "dk_123".to_string(), - public_key_spki_der_base64: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE".to_string(), - algorithm: DeviceKeyAlgorithm::EcdsaP256Sha256, - protection_class: DeviceKeyProtectionClass::OsProtectedNonextractable, - }; - - let value = serde_json::to_value(&response).expect("serialize device/key/create response"); - assert_eq!( - value, - json!({ - "keyId": "dk_123", - "publicKeySpkiDerBase64": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE", - "algorithm": "ecdsa_p256_sha256", - "protectionClass": "os_protected_nonextractable", - }) - ); - - let decoded = serde_json::from_value::(value) - .expect("deserialize device/key/create response"); - assert_eq!(decoded, response); - } - - #[test] - fn device_key_sign_params_round_trip_uses_accepted_payload_enum() { - let params = DeviceKeySignParams { - key_id: "dk_123".to_string(), - payload: DeviceKeySignPayload::RemoteControlClientConnection { - nonce: "nonce-1".to_string(), - audience: RemoteControlClientConnectionAudience::RemoteControlClientWebsocket, - session_id: "wssess_123".to_string(), - target_origin: "https://chatgpt.com".to_string(), - target_path: "/api/codex/remote/control/client".to_string(), - account_user_id: "account-user-1".to_string(), - client_id: "cli_123".to_string(), - token_sha256_base64url: "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU".to_string(), - token_expires_at: 1_700_000_000, - scopes: vec!["remote_control_controller_websocket".to_string()], - }, - }; - - let value = serde_json::to_value(¶ms).expect("serialize device/key/sign params"); - assert_eq!( - value, - json!({ - "keyId": "dk_123", - "payload": { - "type": "remoteControlClientConnection", - "nonce": "nonce-1", - "audience": "remote_control_client_websocket", - "sessionId": "wssess_123", - "targetOrigin": "https://chatgpt.com", - "targetPath": "/api/codex/remote/control/client", - "accountUserId": "account-user-1", - "clientId": "cli_123", - "tokenSha256Base64url": "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU", - "tokenExpiresAt": 1_700_000_000, - "scopes": ["remote_control_controller_websocket"], - }, - }) - ); - - let decoded = serde_json::from_value::(value) - .expect("deserialize device/key/sign params"); - assert_eq!(decoded, params); - } - - #[test] - fn device_key_sign_params_round_trip_uses_enrollment_payload() { - let params = DeviceKeySignParams { - key_id: "dk_123".to_string(), - payload: DeviceKeySignPayload::RemoteControlClientEnrollment { - nonce: "nonce-1".to_string(), - audience: RemoteControlClientEnrollmentAudience::RemoteControlClientEnrollment, - challenge_id: "rch_123".to_string(), - target_origin: "https://chatgpt.com".to_string(), - target_path: "/wham/remote/control/client/enroll".to_string(), - account_user_id: "account-user-1".to_string(), - client_id: "cli_123".to_string(), - device_identity_sha256_base64url: "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU" - .to_string(), - challenge_expires_at: 1_700_000_000, - }, - }; - - let value = serde_json::to_value(¶ms) - .expect("serialize device/key/sign params with enrollment payload"); - assert_eq!( - value, - json!({ - "keyId": "dk_123", - "payload": { - "type": "remoteControlClientEnrollment", - "nonce": "nonce-1", - "audience": "remote_control_client_enrollment", - "challengeId": "rch_123", - "targetOrigin": "https://chatgpt.com", - "targetPath": "/wham/remote/control/client/enroll", - "accountUserId": "account-user-1", - "clientId": "cli_123", - "deviceIdentitySha256Base64url": "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU", - "challengeExpiresAt": 1_700_000_000, - }, - }) - ); - - let decoded = serde_json::from_value::(value) - .expect("deserialize device/key/sign params with enrollment payload"); - assert_eq!(decoded, params); - } - - #[test] - fn device_key_sign_response_returns_signed_payload_bytes() { - let response = DeviceKeySignResponse { - signature_der_base64: "MEUCIQD".to_string(), - signed_payload_base64: "eyJkb21haW4iOiJjb2RleA".to_string(), - algorithm: DeviceKeyAlgorithm::EcdsaP256Sha256, - }; - - let value = serde_json::to_value(&response).expect("serialize device/key/sign response"); - assert_eq!( - value, - json!({ - "signatureDerBase64": "MEUCIQD", - "signedPayloadBase64": "eyJkb21haW4iOiJjb2RleA", - "algorithm": "ecdsa_p256_sha256", - }) - ); - - let decoded = serde_json::from_value::(value) - .expect("deserialize device/key/sign response"); - assert_eq!(decoded, response); - } - - #[test] - fn fs_create_directory_params_round_trip_with_default_recursive() { - let params = FsCreateDirectoryParams { - path: absolute_path("tmp/example"), - recursive: None, - }; - - let value = serde_json::to_value(¶ms).expect("serialize fs/createDirectory params"); - assert_eq!( - value, - json!({ - "path": absolute_path_string("tmp/example"), - "recursive": null, - }) - ); - - let decoded = serde_json::from_value::(value) - .expect("deserialize fs/createDirectory params"); - assert_eq!(decoded, params); - } - - #[test] - fn fs_write_file_params_round_trip_with_base64_data() { - let params = FsWriteFileParams { - path: absolute_path("tmp/example.bin"), - data_base64: "AAE=".to_string(), - }; - - let value = serde_json::to_value(¶ms).expect("serialize fs/writeFile params"); - assert_eq!( - value, - json!({ - "path": absolute_path_string("tmp/example.bin"), - "dataBase64": "AAE=", - }) - ); - - let decoded = serde_json::from_value::(value) - .expect("deserialize fs/writeFile params"); - assert_eq!(decoded, params); - } - - #[test] - fn fs_copy_params_round_trip_with_recursive_directory_copy() { - let params = FsCopyParams { - source_path: absolute_path("tmp/source"), - destination_path: absolute_path("tmp/destination"), - recursive: true, - }; - - let value = serde_json::to_value(¶ms).expect("serialize fs/copy params"); - assert_eq!( - value, - json!({ - "sourcePath": absolute_path_string("tmp/source"), - "destinationPath": absolute_path_string("tmp/destination"), - "recursive": true, - }) - ); - - let decoded = - serde_json::from_value::(value).expect("deserialize fs/copy params"); - assert_eq!(decoded, params); - } - - #[test] - fn thread_shell_command_params_round_trip() { - let params = ThreadShellCommandParams { - thread_id: "thr_123".to_string(), - command: "printf 'hello world\\n'".to_string(), - }; - - let value = serde_json::to_value(¶ms).expect("serialize thread/shellCommand params"); - assert_eq!( - value, - json!({ - "threadId": "thr_123", - "command": "printf 'hello world\\n'", - }) - ); - - let decoded = serde_json::from_value::(value) - .expect("deserialize thread/shellCommand params"); - assert_eq!(decoded, params); - } - - #[test] - fn thread_shell_command_response_round_trip() { - let response = ThreadShellCommandResponse {}; - - let value = - serde_json::to_value(&response).expect("serialize thread/shellCommand response"); - assert_eq!(value, json!({})); - - let decoded = serde_json::from_value::(value) - .expect("deserialize thread/shellCommand response"); - assert_eq!(decoded, response); - } - - #[test] - fn fs_changed_notification_round_trips() { - let notification = FsChangedNotification { - watch_id: "0195ec6b-1d6f-7c2e-8c7a-56f2c4a8b9d1".to_string(), - changed_paths: vec![ - absolute_path("tmp/repo/.git/HEAD"), - absolute_path("tmp/repo/.git/FETCH_HEAD"), - ], - }; - - let value = serde_json::to_value(¬ification).expect("serialize fs/changed notification"); - assert_eq!( - value, - json!({ - "watchId": "0195ec6b-1d6f-7c2e-8c7a-56f2c4a8b9d1", - "changedPaths": [ - absolute_path_string("tmp/repo/.git/HEAD"), - absolute_path_string("tmp/repo/.git/FETCH_HEAD"), - ], - }) - ); - - let decoded = serde_json::from_value::(value) - .expect("deserialize fs/changed notification"); - assert_eq!(decoded, notification); - } - - #[test] - fn command_exec_params_default_optional_streaming_flags() { - let params = serde_json::from_value::(json!({ - "command": ["ls", "-la"], - "timeoutMs": 1000, - "cwd": "/tmp" - })) - .expect("command/exec payload should deserialize"); - - assert_eq!( - params, - CommandExecParams { - command: vec!["ls".to_string(), "-la".to_string()], - process_id: None, - tty: false, - stream_stdin: false, - stream_stdout_stderr: false, - output_bytes_cap: None, - disable_output_cap: false, - disable_timeout: false, - timeout_ms: Some(1000), - cwd: Some(PathBuf::from("/tmp")), - env: None, - size: None, - sandbox_policy: None, - permission_profile: None, - } - ); - } - - #[test] - fn command_exec_params_round_trips_disable_timeout() { - let params = CommandExecParams { - command: vec!["sleep".to_string(), "30".to_string()], - process_id: Some("sleep-1".to_string()), - tty: false, - stream_stdin: false, - stream_stdout_stderr: false, - output_bytes_cap: None, - disable_output_cap: false, - disable_timeout: true, - timeout_ms: None, - cwd: None, - env: None, - size: None, - sandbox_policy: None, - permission_profile: None, - }; - - let value = serde_json::to_value(¶ms).expect("serialize command/exec params"); - assert_eq!( - value, - json!({ - "command": ["sleep", "30"], - "processId": "sleep-1", - "disableTimeout": true, - "timeoutMs": null, - "cwd": null, - "env": null, - "size": null, - "sandboxPolicy": null, - "permissionProfile": null, - "outputBytesCap": null, - }) - ); - - let decoded = - serde_json::from_value::(value).expect("deserialize round-trip"); - assert_eq!(decoded, params); - } - - #[test] - fn process_spawn_params_round_trips_without_sandbox_policy() { - let params = ProcessSpawnParams { - command: vec!["sleep".to_string(), "30".to_string()], - process_handle: "sleep-1".to_string(), - cwd: test_absolute_path(), - tty: false, - stream_stdin: false, - stream_stdout_stderr: false, - output_bytes_cap: None, - timeout_ms: None, - env: None, - size: None, - }; - - let value = serde_json::to_value(¶ms).expect("serialize process/spawn params"); - assert_eq!( - value, - json!({ - "command": ["sleep", "30"], - "processHandle": "sleep-1", - "cwd": absolute_path_string("readable"), - "env": null, - "size": null, - }) - ); - - let decoded = - serde_json::from_value::(value).expect("deserialize round-trip"); - assert_eq!(decoded, params); - } - - #[test] - fn process_spawn_params_distinguish_omitted_null_and_value_limits() { - let base = json!({ - "command": ["sleep", "30"], - "processHandle": "sleep-1", - "cwd": absolute_path_string("readable"), - }); - - let expected_omitted = ProcessSpawnParams { - command: vec!["sleep".to_string(), "30".to_string()], - process_handle: "sleep-1".to_string(), - cwd: test_absolute_path(), - tty: false, - stream_stdin: false, - stream_stdout_stderr: false, - output_bytes_cap: None, - timeout_ms: None, - env: None, - size: None, - }; - let decoded = - serde_json::from_value::(base).expect("deserialize omitted limits"); - assert_eq!(decoded, expected_omitted); - - let decoded = serde_json::from_value::(json!({ - "command": ["sleep", "30"], - "processHandle": "sleep-1", - "cwd": absolute_path_string("readable"), - "outputBytesCap": null, - "timeoutMs": null, - })) - .expect("deserialize disabled limits"); - assert_eq!( - decoded, - ProcessSpawnParams { - output_bytes_cap: Some(None), - timeout_ms: Some(None), - ..expected_omitted.clone() - } - ); - - let decoded = serde_json::from_value::(json!({ - "command": ["sleep", "30"], - "processHandle": "sleep-1", - "cwd": absolute_path_string("readable"), - "outputBytesCap": 123, - "timeoutMs": 456, - })) - .expect("deserialize explicit limits"); - assert_eq!( - decoded, - ProcessSpawnParams { - output_bytes_cap: Some(Some(123)), - timeout_ms: Some(Some(456)), - ..expected_omitted - } - ); - } - - #[test] - fn command_exec_params_round_trips_disable_output_cap() { - let params = CommandExecParams { - command: vec!["yes".to_string()], - process_id: Some("yes-1".to_string()), - tty: false, - stream_stdin: false, - stream_stdout_stderr: true, - output_bytes_cap: None, - disable_output_cap: true, - disable_timeout: false, - timeout_ms: None, - cwd: None, - env: None, - size: None, - sandbox_policy: None, - permission_profile: None, - }; - - let value = serde_json::to_value(¶ms).expect("serialize command/exec params"); - assert_eq!( - value, - json!({ - "command": ["yes"], - "processId": "yes-1", - "streamStdoutStderr": true, - "outputBytesCap": null, - "disableOutputCap": true, - "timeoutMs": null, - "cwd": null, - "env": null, - "size": null, - "sandboxPolicy": null, - "permissionProfile": null, - }) - ); - - let decoded = - serde_json::from_value::(value).expect("deserialize round-trip"); - assert_eq!(decoded, params); - } - - #[test] - fn command_exec_params_round_trips_env_overrides_and_unsets() { - let params = CommandExecParams { - command: vec!["printenv".to_string(), "FOO".to_string()], - process_id: Some("env-1".to_string()), - tty: false, - stream_stdin: false, - stream_stdout_stderr: false, - output_bytes_cap: None, - disable_output_cap: false, - disable_timeout: false, - timeout_ms: None, - cwd: None, - env: Some(HashMap::from([ - ("FOO".to_string(), Some("override".to_string())), - ("BAR".to_string(), Some("added".to_string())), - ("BAZ".to_string(), None), - ])), - size: None, - sandbox_policy: None, - permission_profile: None, - }; - - let value = serde_json::to_value(¶ms).expect("serialize command/exec params"); - assert_eq!( - value, - json!({ - "command": ["printenv", "FOO"], - "processId": "env-1", - "outputBytesCap": null, - "timeoutMs": null, - "cwd": null, - "env": { - "FOO": "override", - "BAR": "added", - "BAZ": null, - }, - "size": null, - "sandboxPolicy": null, - "permissionProfile": null, - }) - ); - - let decoded = - serde_json::from_value::(value).expect("deserialize round-trip"); - assert_eq!(decoded, params); - } - - #[test] - fn command_exec_write_round_trips_close_only_payload() { - let params = CommandExecWriteParams { - process_id: "proc-7".to_string(), - delta_base64: None, - close_stdin: true, - }; - - let value = serde_json::to_value(¶ms).expect("serialize command/exec/write params"); - assert_eq!( - value, - json!({ - "processId": "proc-7", - "deltaBase64": null, - "closeStdin": true, - }) - ); - - let decoded = serde_json::from_value::(value) - .expect("deserialize round-trip"); - assert_eq!(decoded, params); - } - - #[test] - fn command_exec_terminate_round_trips() { - let params = CommandExecTerminateParams { - process_id: "proc-8".to_string(), - }; - - let value = serde_json::to_value(¶ms).expect("serialize command/exec/terminate params"); - assert_eq!( - value, - json!({ - "processId": "proc-8", - }) - ); - - let decoded = serde_json::from_value::(value) - .expect("deserialize round-trip"); - assert_eq!(decoded, params); - } - - #[test] - fn command_exec_params_round_trip_with_size() { - let params = CommandExecParams { - command: vec!["top".to_string()], - process_id: Some("pty-1".to_string()), - tty: true, - stream_stdin: false, - stream_stdout_stderr: false, - output_bytes_cap: None, - disable_output_cap: false, - disable_timeout: false, - timeout_ms: None, - cwd: None, - env: None, - size: Some(CommandExecTerminalSize { - rows: 40, - cols: 120, - }), - sandbox_policy: None, - permission_profile: None, - }; - - let value = serde_json::to_value(¶ms).expect("serialize command/exec params"); - assert_eq!( - value, - json!({ - "command": ["top"], - "processId": "pty-1", - "tty": true, - "outputBytesCap": null, - "timeoutMs": null, - "cwd": null, - "env": null, - "size": { - "rows": 40, - "cols": 120, - }, - "sandboxPolicy": null, - "permissionProfile": null, - }) - ); - - let decoded = - serde_json::from_value::(value).expect("deserialize round-trip"); - assert_eq!(decoded, params); - } - - #[test] - fn command_exec_resize_round_trips() { - let params = CommandExecResizeParams { - process_id: "proc-9".to_string(), - size: CommandExecTerminalSize { - rows: 50, - cols: 160, - }, - }; - - let value = serde_json::to_value(¶ms).expect("serialize command/exec/resize params"); - assert_eq!( - value, - json!({ - "processId": "proc-9", - "size": { - "rows": 50, - "cols": 160, - }, - }) - ); - - let decoded = serde_json::from_value::(value) - .expect("deserialize round-trip"); - assert_eq!(decoded, params); - } - - #[test] - fn command_exec_output_delta_round_trips() { - let notification = CommandExecOutputDeltaNotification { - process_id: "proc-1".to_string(), - stream: CommandExecOutputStream::Stdout, - delta_base64: "AQI=".to_string(), - cap_reached: false, - }; - - let value = serde_json::to_value(¬ification) - .expect("serialize command/exec/outputDelta notification"); - assert_eq!( - value, - json!({ - "processId": "proc-1", - "stream": "stdout", - "deltaBase64": "AQI=", - "capReached": false, - }) - ); - - let decoded = serde_json::from_value::(value) - .expect("deserialize round-trip"); - assert_eq!(decoded, notification); - } - - #[test] - fn process_control_params_round_trip() { - let write = ProcessWriteStdinParams { - process_handle: "proc-7".to_string(), - delta_base64: None, - close_stdin: true, - }; - let value = serde_json::to_value(&write).expect("serialize process/writeStdin params"); - assert_eq!( - value, - json!({ - "processHandle": "proc-7", - "deltaBase64": null, - "closeStdin": true, - }) - ); - let decoded = serde_json::from_value::(value) - .expect("deserialize process/writeStdin params"); - assert_eq!(decoded, write); - - let resize = ProcessResizePtyParams { - process_handle: "proc-7".to_string(), - size: ProcessTerminalSize { - rows: 50, - cols: 160, - }, - }; - let value = serde_json::to_value(&resize).expect("serialize process/resizePty params"); - assert_eq!( - value, - json!({ - "processHandle": "proc-7", - "size": { - "rows": 50, - "cols": 160, - }, - }) - ); - let decoded = serde_json::from_value::(value) - .expect("deserialize process/resizePty params"); - assert_eq!(decoded, resize); - - let kill = ProcessKillParams { - process_handle: "proc-7".to_string(), - }; - let value = serde_json::to_value(&kill).expect("serialize process/kill params"); - assert_eq!( - value, - json!({ - "processHandle": "proc-7", - }) - ); - let decoded = - serde_json::from_value::(value).expect("deserialize process/kill"); - assert_eq!(decoded, kill); - } - - #[test] - fn process_notifications_round_trip() { - let delta = ProcessOutputDeltaNotification { - process_handle: "proc-1".to_string(), - stream: ProcessOutputStream::Stdout, - delta_base64: "AQI=".to_string(), - cap_reached: false, - }; - let value = serde_json::to_value(&delta).expect("serialize process/outputDelta"); - assert_eq!( - value, - json!({ - "processHandle": "proc-1", - "stream": "stdout", - "deltaBase64": "AQI=", - "capReached": false, - }) - ); - let decoded = serde_json::from_value::(value) - .expect("deserialize process/outputDelta"); - assert_eq!(decoded, delta); - - let exited = ProcessExitedNotification { - process_handle: "proc-1".to_string(), - exit_code: 0, - stdout: "out".to_string(), - stdout_cap_reached: false, - stderr: "err".to_string(), - stderr_cap_reached: true, - }; - let value = serde_json::to_value(&exited).expect("serialize process/exited"); - assert_eq!( - value, - json!({ - "processHandle": "proc-1", - "exitCode": 0, - "stdout": "out", - "stdoutCapReached": false, - "stderr": "err", - "stderrCapReached": true, - }) - ); - let decoded = serde_json::from_value::(value) - .expect("deserialize process/exited"); - assert_eq!(decoded, exited); - } - - #[test] - fn command_execution_output_delta_round_trips() { - let notification = CommandExecutionOutputDeltaNotification { - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - item_id: "item-1".to_string(), - delta: "\u{fffd}a\n".to_string(), - }; - - let value = serde_json::to_value(¬ification) - .expect("serialize item/commandExecution/outputDelta notification"); - assert_eq!( - value, - json!({ - "threadId": "thread-1", - "turnId": "turn-1", - "itemId": "item-1", - "delta": "\u{fffd}a\n", - }) - ); - - let decoded = serde_json::from_value::(value) - .expect("deserialize round-trip"); - assert_eq!(decoded, notification); - } - - #[test] - fn sandbox_policy_round_trips_external_sandbox_network_access() { - let v2_policy = SandboxPolicy::ExternalSandbox { - network_access: NetworkAccess::Enabled, - }; - - let core_policy = v2_policy.to_core(); - assert_eq!( - core_policy, - codex_protocol::protocol::SandboxPolicy::ExternalSandbox { - network_access: CoreNetworkAccess::Enabled, - } - ); - - let back_to_v2 = SandboxPolicy::from(core_policy); - assert_eq!(back_to_v2, v2_policy); - } - - #[test] - fn sandbox_policy_round_trips_read_only_network_access() { - let v2_policy = SandboxPolicy::ReadOnly { - network_access: true, - }; - - let core_policy = v2_policy.to_core(); - assert_eq!( - core_policy, - codex_protocol::protocol::SandboxPolicy::ReadOnly { - network_access: true, - } - ); - - let back_to_v2 = SandboxPolicy::from(core_policy); - assert_eq!(back_to_v2, v2_policy); - } - - #[test] - fn ask_for_approval_granular_round_trips_request_permissions_flag() { - let v2_policy = AskForApproval::Granular { - sandbox_approval: true, - rules: false, - skill_approval: false, - request_permissions: true, - mcp_elicitations: false, - }; - - let core_policy = v2_policy.to_core(); - assert_eq!( - core_policy, - CoreAskForApproval::Granular(CoreGranularApprovalConfig { - sandbox_approval: true, - rules: false, - skill_approval: false, - request_permissions: true, - mcp_elicitations: false, - }) - ); - - let back_to_v2 = AskForApproval::from(core_policy); - assert_eq!(back_to_v2, v2_policy); - } - - #[test] - fn ask_for_approval_granular_defaults_missing_optional_flags_to_false() { - let decoded = serde_json::from_value::(serde_json::json!({ - "granular": { - "sandbox_approval": true, - "rules": false, - "mcp_elicitations": true, - } - })) - .expect("granular approval policy should deserialize"); - - assert_eq!( - decoded, - AskForApproval::Granular { - sandbox_approval: true, - rules: false, - skill_approval: false, - request_permissions: false, - mcp_elicitations: true, - } - ); - } - - #[test] - fn ask_for_approval_granular_is_marked_experimental() { - let reason = crate::experimental_api::ExperimentalApi::experimental_reason( - &AskForApproval::Granular { - sandbox_approval: true, - rules: false, - skill_approval: false, - request_permissions: false, - mcp_elicitations: true, - }, - ); - - assert_eq!(reason, Some("askForApproval.granular")); - assert_eq!( - crate::experimental_api::ExperimentalApi::experimental_reason( - &AskForApproval::OnRequest, - ), - None - ); - } - - #[test] - fn profile_v2_granular_approval_policy_is_marked_experimental() { - let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&ProfileV2 { - model: None, - model_provider: None, - approval_policy: Some(AskForApproval::Granular { - sandbox_approval: true, - rules: false, - skill_approval: false, - request_permissions: true, - mcp_elicitations: false, - }), - approvals_reviewer: None, - service_tier: None, - model_reasoning_effort: None, - model_reasoning_summary: None, - model_verbosity: None, - web_search: None, - tools: None, - chatgpt_base_url: None, - additional: HashMap::new(), - }); - - assert_eq!(reason, Some("askForApproval.granular")); - } - - #[test] - fn config_granular_approval_policy_is_marked_experimental() { - let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&Config { - model: None, - review_model: None, - model_context_window: None, - model_auto_compact_token_limit: None, - model_provider: None, - approval_policy: Some(AskForApproval::Granular { - sandbox_approval: false, - rules: true, - skill_approval: false, - request_permissions: false, - mcp_elicitations: true, - }), - approvals_reviewer: None, - sandbox_mode: None, - sandbox_workspace_write: None, - forced_chatgpt_workspace_id: None, - forced_login_method: None, - web_search: None, - tools: None, - profile: None, - profiles: HashMap::new(), - instructions: None, - developer_instructions: None, - compact_prompt: None, - model_reasoning_effort: None, - model_reasoning_summary: None, - model_verbosity: None, - service_tier: None, - analytics: None, - apps: None, - additional: HashMap::new(), - }); - - assert_eq!(reason, Some("askForApproval.granular")); - } - - #[test] - fn config_approvals_reviewer_is_marked_experimental() { - let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&Config { - model: None, - review_model: None, - model_context_window: None, - model_auto_compact_token_limit: None, - model_provider: None, - approval_policy: None, - approvals_reviewer: Some(ApprovalsReviewer::AutoReview), - sandbox_mode: None, - sandbox_workspace_write: None, - forced_chatgpt_workspace_id: None, - forced_login_method: None, - web_search: None, - tools: None, - profile: None, - profiles: HashMap::new(), - instructions: None, - developer_instructions: None, - compact_prompt: None, - model_reasoning_effort: None, - model_reasoning_summary: None, - model_verbosity: None, - service_tier: None, - analytics: None, - apps: None, - additional: HashMap::new(), - }); - - assert_eq!(reason, Some("config/read.approvalsReviewer")); - } - - #[test] - fn config_nested_profile_granular_approval_policy_is_marked_experimental() { - let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&Config { - model: None, - review_model: None, - model_context_window: None, - model_auto_compact_token_limit: None, - model_provider: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_mode: None, - sandbox_workspace_write: None, - forced_chatgpt_workspace_id: None, - forced_login_method: None, - web_search: None, - tools: None, - profile: None, - profiles: HashMap::from([( - "default".to_string(), - ProfileV2 { - model: None, - model_provider: None, - approval_policy: Some(AskForApproval::Granular { - sandbox_approval: true, - rules: false, - skill_approval: false, - request_permissions: false, - mcp_elicitations: true, - }), - approvals_reviewer: None, - service_tier: None, - model_reasoning_effort: None, - model_reasoning_summary: None, - model_verbosity: None, - web_search: None, - tools: None, - chatgpt_base_url: None, - additional: HashMap::new(), - }, - )]), - instructions: None, - developer_instructions: None, - compact_prompt: None, - model_reasoning_effort: None, - model_reasoning_summary: None, - model_verbosity: None, - service_tier: None, - analytics: None, - apps: None, - additional: HashMap::new(), - }); - - assert_eq!(reason, Some("askForApproval.granular")); - } - - #[test] - fn config_nested_profile_approvals_reviewer_is_marked_experimental() { - let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&Config { - model: None, - review_model: None, - model_context_window: None, - model_auto_compact_token_limit: None, - model_provider: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_mode: None, - sandbox_workspace_write: None, - forced_chatgpt_workspace_id: None, - forced_login_method: None, - web_search: None, - tools: None, - profile: None, - profiles: HashMap::from([( - "default".to_string(), - ProfileV2 { - model: None, - model_provider: None, - approval_policy: None, - approvals_reviewer: Some(ApprovalsReviewer::AutoReview), - service_tier: None, - model_reasoning_effort: None, - model_reasoning_summary: None, - model_verbosity: None, - web_search: None, - tools: None, - chatgpt_base_url: None, - additional: HashMap::new(), - }, - )]), - instructions: None, - developer_instructions: None, - compact_prompt: None, - model_reasoning_effort: None, - model_reasoning_summary: None, - model_verbosity: None, - service_tier: None, - analytics: None, - apps: None, - additional: HashMap::new(), - }); - - assert_eq!(reason, Some("config/read.approvalsReviewer")); - } - - #[test] - fn config_requirements_granular_allowed_approval_policy_is_marked_experimental() { - let reason = - crate::experimental_api::ExperimentalApi::experimental_reason(&ConfigRequirements { - allowed_approval_policies: Some(vec![AskForApproval::Granular { - sandbox_approval: true, - rules: true, - skill_approval: false, - request_permissions: false, - mcp_elicitations: false, - }]), - allowed_approvals_reviewers: None, - allowed_sandbox_modes: None, - allowed_web_search_modes: None, - feature_requirements: None, - hooks: None, - enforce_residency: None, - network: None, - }); - - assert_eq!(reason, Some("askForApproval.granular")); - } - - #[test] - fn client_request_thread_start_granular_approval_policy_is_marked_experimental() { - let reason = crate::experimental_api::ExperimentalApi::experimental_reason( - &crate::ClientRequest::ThreadStart { - request_id: crate::RequestId::Integer(1), - params: ThreadStartParams { - approval_policy: Some(AskForApproval::Granular { - sandbox_approval: true, - rules: false, - skill_approval: false, - request_permissions: true, - mcp_elicitations: false, - }), - ..Default::default() - }, - }, - ); - - assert_eq!(reason, Some("askForApproval.granular")); - } - - #[test] - fn client_request_thread_resume_granular_approval_policy_is_marked_experimental() { - let reason = crate::experimental_api::ExperimentalApi::experimental_reason( - &crate::ClientRequest::ThreadResume { - request_id: crate::RequestId::Integer(2), - params: ThreadResumeParams { - thread_id: "thr_123".to_string(), - approval_policy: Some(AskForApproval::Granular { - sandbox_approval: false, - rules: true, - skill_approval: false, - request_permissions: false, - mcp_elicitations: true, - }), - ..Default::default() - }, - }, - ); - - assert_eq!(reason, Some("askForApproval.granular")); - } - - #[test] - fn client_request_thread_fork_granular_approval_policy_is_marked_experimental() { - let reason = crate::experimental_api::ExperimentalApi::experimental_reason( - &crate::ClientRequest::ThreadFork { - request_id: crate::RequestId::Integer(3), - params: ThreadForkParams { - thread_id: "thr_456".to_string(), - approval_policy: Some(AskForApproval::Granular { - sandbox_approval: true, - rules: false, - skill_approval: false, - request_permissions: false, - mcp_elicitations: true, - }), - ..Default::default() - }, - }, - ); - - assert_eq!(reason, Some("askForApproval.granular")); - } - - #[test] - fn client_request_turn_start_granular_approval_policy_is_marked_experimental() { - let reason = crate::experimental_api::ExperimentalApi::experimental_reason( - &crate::ClientRequest::TurnStart { - request_id: crate::RequestId::Integer(4), - params: TurnStartParams { - thread_id: "thr_123".to_string(), - input: Vec::new(), - approval_policy: Some(AskForApproval::Granular { - sandbox_approval: false, - rules: true, - skill_approval: false, - request_permissions: false, - mcp_elicitations: true, - }), - ..Default::default() - }, - }, - ); - - assert_eq!(reason, Some("askForApproval.granular")); - } - - #[test] - fn mcp_server_elicitation_response_round_trips_rmcp_result() { - let rmcp_result = rmcp::model::CreateElicitationResult { - action: rmcp::model::ElicitationAction::Accept, - content: Some(json!({ - "confirmed": true, - })), - }; - - let v2_response = McpServerElicitationRequestResponse::from(rmcp_result.clone()); - assert_eq!( - v2_response, - McpServerElicitationRequestResponse { - action: McpServerElicitationAction::Accept, - content: Some(json!({ - "confirmed": true, - })), - meta: None, - } - ); - assert_eq!( - rmcp::model::CreateElicitationResult::from(v2_response), - rmcp_result - ); - } - - #[test] - fn mcp_server_elicitation_request_from_core_url_request() { - let request = McpServerElicitationRequest::try_from(CoreElicitationRequest::Url { - meta: None, - message: "Finish sign-in".to_string(), - url: "https://example.com/complete".to_string(), - elicitation_id: "elicitation-123".to_string(), - }) - .expect("URL request should convert"); - - assert_eq!( - request, - McpServerElicitationRequest::Url { - meta: None, - message: "Finish sign-in".to_string(), - url: "https://example.com/complete".to_string(), - elicitation_id: "elicitation-123".to_string(), - } - ); - } - - #[test] - fn mcp_server_elicitation_request_from_core_form_request() { - let request = McpServerElicitationRequest::try_from(CoreElicitationRequest::Form { - meta: None, - message: "Allow this request?".to_string(), - requested_schema: json!({ - "type": "object", - "properties": { - "confirmed": { - "type": "boolean", - } - }, - "required": ["confirmed"], - }), - }) - .expect("form request should convert"); - - let expected_schema: McpElicitationSchema = serde_json::from_value(json!({ - "type": "object", - "properties": { - "confirmed": { - "type": "boolean", - } - }, - "required": ["confirmed"], - })) - .expect("expected schema should deserialize"); - - assert_eq!( - request, - McpServerElicitationRequest::Form { - meta: None, - message: "Allow this request?".to_string(), - requested_schema: expected_schema, - } - ); - } - - #[test] - fn mcp_elicitation_schema_matches_mcp_2025_11_25_primitives() { - let schema: McpElicitationSchema = serde_json::from_value(json!({ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "email": { - "type": "string", - "title": "Email", - "description": "Work email address", - "format": "email", - "default": "dev@example.com", - }, - "count": { - "type": "integer", - "title": "Count", - "description": "How many items to create", - "minimum": 1, - "maximum": 5, - "default": 3, - }, - "confirmed": { - "type": "boolean", - "title": "Confirm", - "description": "Approve the pending action", - "default": true, - }, - "legacyChoice": { - "type": "string", - "title": "Action", - "description": "Legacy titled enum form", - "enum": ["allow", "deny"], - "enumNames": ["Allow", "Deny"], - "default": "allow", - }, - }, - "required": ["email", "confirmed"], - })) - .expect("schema should deserialize"); - - assert_eq!( - schema, - McpElicitationSchema { - schema_uri: Some("https://json-schema.org/draft/2020-12/schema".to_string()), - type_: McpElicitationObjectType::Object, - properties: BTreeMap::from([ - ( - "confirmed".to_string(), - McpElicitationPrimitiveSchema::Boolean(McpElicitationBooleanSchema { - type_: McpElicitationBooleanType::Boolean, - title: Some("Confirm".to_string()), - description: Some("Approve the pending action".to_string()), - default: Some(true), - }), - ), - ( - "count".to_string(), - McpElicitationPrimitiveSchema::Number(McpElicitationNumberSchema { - type_: McpElicitationNumberType::Integer, - title: Some("Count".to_string()), - description: Some("How many items to create".to_string()), - minimum: Some(1.0), - maximum: Some(5.0), - default: Some(3.0), - }), - ), - ( - "email".to_string(), - McpElicitationPrimitiveSchema::String(McpElicitationStringSchema { - type_: McpElicitationStringType::String, - title: Some("Email".to_string()), - description: Some("Work email address".to_string()), - min_length: None, - max_length: None, - format: Some(McpElicitationStringFormat::Email), - default: Some("dev@example.com".to_string()), - }), - ), - ( - "legacyChoice".to_string(), - McpElicitationPrimitiveSchema::Enum(McpElicitationEnumSchema::Legacy( - McpElicitationLegacyTitledEnumSchema { - type_: McpElicitationStringType::String, - title: Some("Action".to_string()), - description: Some("Legacy titled enum form".to_string()), - enum_: vec!["allow".to_string(), "deny".to_string()], - enum_names: Some(vec!["Allow".to_string(), "Deny".to_string(),]), - default: Some("allow".to_string()), - }, - )), - ), - ]), - required: Some(vec!["email".to_string(), "confirmed".to_string()]), - } - ); - } - - #[test] - fn mcp_server_elicitation_request_rejects_null_core_form_schema() { - let result = McpServerElicitationRequest::try_from(CoreElicitationRequest::Form { - meta: Some(json!({ - "persist": "session", - })), - message: "Allow this request?".to_string(), - requested_schema: JsonValue::Null, - }); - - assert!(result.is_err()); - } - - #[test] - fn mcp_server_elicitation_request_rejects_invalid_core_form_schema() { - let result = McpServerElicitationRequest::try_from(CoreElicitationRequest::Form { - meta: None, - message: "Allow this request?".to_string(), - requested_schema: json!({ - "type": "object", - "properties": { - "confirmed": { - "type": "object", - } - }, - }), - }); - - assert!(result.is_err()); - } - - #[test] - fn mcp_server_elicitation_response_serializes_nullable_content() { - let response = McpServerElicitationRequestResponse { - action: McpServerElicitationAction::Decline, - content: None, - meta: None, - }; - - assert_eq!( - serde_json::to_value(response).expect("response should serialize"), - json!({ - "action": "decline", - "content": null, - "_meta": null, - }) - ); - } - - #[test] - fn sandbox_policy_round_trips_workspace_write_access() { - let v2_policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: true, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - }; - - let core_policy = v2_policy.to_core(); - assert_eq!( - core_policy, - codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: true, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - } - ); - - let back_to_v2 = SandboxPolicy::from(core_policy); - assert_eq!(back_to_v2, v2_policy); - } - - #[test] - fn sandbox_policy_deserializes_legacy_read_only_full_access_field() { - let policy = serde_json::from_value::(json!({ - "type": "readOnly", - "access": { - "type": "fullAccess" - }, - "networkAccess": true - })) - .expect("read-only policy should ignore legacy fullAccess field"); - assert_eq!( - policy, - SandboxPolicy::ReadOnly { - network_access: true - } - ); - } - - #[test] - fn sandbox_policy_deserializes_legacy_workspace_write_full_access_field() { - let writable_root = absolute_path("/workspace"); - let policy = serde_json::from_value::(json!({ - "type": "workspaceWrite", - "writableRoots": [writable_root], - "readOnlyAccess": { - "type": "fullAccess" - }, - "networkAccess": true, - "excludeTmpdirEnvVar": true, - "excludeSlashTmp": true - })) - .expect("workspace-write policy should ignore legacy fullAccess field"); - assert_eq!( - policy, - SandboxPolicy::WorkspaceWrite { - writable_roots: vec![absolute_path("/workspace")], - network_access: true, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - } - ); - } - - #[test] - fn sandbox_policy_rejects_legacy_read_only_restricted_access_field() { - let err = serde_json::from_value::(json!({ - "type": "readOnly", - "access": { - "type": "restricted", - "includePlatformDefaults": false, - "readableRoots": [] - } - })) - .expect_err("read-only policy should reject removed restricted access field"); - assert!(err.to_string().contains("readOnly.access")); - } - - #[test] - fn sandbox_policy_rejects_legacy_workspace_write_restricted_read_access_field() { - let err = serde_json::from_value::(json!({ - "type": "workspaceWrite", - "writableRoots": [], - "readOnlyAccess": { - "type": "restricted", - "includePlatformDefaults": false, - "readableRoots": [] - }, - "networkAccess": false, - "excludeTmpdirEnvVar": false, - "excludeSlashTmp": false - })) - .expect_err("workspace-write policy should reject removed restricted readOnlyAccess field"); - assert!(err.to_string().contains("workspaceWrite.readOnlyAccess")); - } - - #[test] - fn automatic_approval_review_deserializes_aborted_status() { - let review: GuardianApprovalReview = serde_json::from_value(json!({ - "status": "aborted", - "riskLevel": null, - "userAuthorization": null, - "rationale": null - })) - .expect("aborted automatic review should deserialize"); - assert_eq!( - review, - GuardianApprovalReview { - status: GuardianApprovalReviewStatus::Aborted, - risk_level: None, - user_authorization: None, - rationale: None, - } - ); - } - - #[test] - fn guardian_approval_review_action_round_trips_command_shape() { - let value = json!({ - "type": "command", - "source": "shell", - "command": "rm -rf /tmp/example.sqlite", - "cwd": absolute_path_string("tmp"), - }); - let action: GuardianApprovalReviewAction = - serde_json::from_value(value.clone()).expect("guardian review action"); - - assert_eq!( - action, - GuardianApprovalReviewAction::Command { - source: GuardianCommandSource::Shell, - command: "rm -rf /tmp/example.sqlite".to_string(), - cwd: absolute_path("tmp"), - } - ); - assert_eq!( - serde_json::to_value(&action).expect("serialize guardian review action"), - value - ); - } - - #[test] - fn network_requirements_deserializes_legacy_fields() { - let requirements: NetworkRequirements = serde_json::from_value(json!({ - "allowedDomains": ["api.openai.com"], - "deniedDomains": ["blocked.example.com"], - "allowUnixSockets": ["/tmp/proxy.sock"] - })) - .expect("legacy network requirements should deserialize"); - - assert_eq!( - requirements, - NetworkRequirements { - enabled: None, - http_port: None, - socks_port: None, - allow_upstream_proxy: None, - dangerously_allow_non_loopback_proxy: None, - dangerously_allow_all_unix_sockets: None, - domains: None, - managed_allowed_domains_only: None, - allowed_domains: Some(vec!["api.openai.com".to_string()]), - denied_domains: Some(vec!["blocked.example.com".to_string()]), - unix_sockets: None, - allow_unix_sockets: Some(vec!["/tmp/proxy.sock".to_string()]), - allow_local_binding: None, - } - ); - } - - #[test] - fn network_requirements_serializes_canonical_and_legacy_fields() { - let requirements = NetworkRequirements { - enabled: Some(true), - http_port: Some(8080), - socks_port: Some(1080), - allow_upstream_proxy: Some(false), - dangerously_allow_non_loopback_proxy: Some(false), - dangerously_allow_all_unix_sockets: Some(true), - domains: Some(BTreeMap::from([ - ("api.openai.com".to_string(), NetworkDomainPermission::Allow), - ( - "blocked.example.com".to_string(), - NetworkDomainPermission::Deny, - ), - ])), - managed_allowed_domains_only: Some(true), - allowed_domains: Some(vec!["api.openai.com".to_string()]), - denied_domains: Some(vec!["blocked.example.com".to_string()]), - unix_sockets: Some(BTreeMap::from([ - ( - "/tmp/proxy.sock".to_string(), - NetworkUnixSocketPermission::Allow, - ), - ( - "/tmp/ignored.sock".to_string(), - NetworkUnixSocketPermission::None, - ), - ])), - allow_unix_sockets: Some(vec!["/tmp/proxy.sock".to_string()]), - allow_local_binding: Some(true), - }; - - assert_eq!( - serde_json::to_value(requirements).expect("network requirements should serialize"), - json!({ - "enabled": true, - "httpPort": 8080, - "socksPort": 1080, - "allowUpstreamProxy": false, - "dangerouslyAllowNonLoopbackProxy": false, - "dangerouslyAllowAllUnixSockets": true, - "domains": { - "api.openai.com": "allow", - "blocked.example.com": "deny" - }, - "managedAllowedDomainsOnly": true, - "allowedDomains": ["api.openai.com"], - "deniedDomains": ["blocked.example.com"], - "unixSockets": { - "/tmp/ignored.sock": "none", - "/tmp/proxy.sock": "allow" - }, - "allowUnixSockets": ["/tmp/proxy.sock"], - "allowLocalBinding": true - }) - ); - } - - #[test] - fn core_turn_item_into_thread_item_converts_supported_variants() { - let user_item = TurnItem::UserMessage(UserMessageItem { - id: "user-1".to_string(), - content: vec![ - CoreUserInput::Text { - text: "hello".to_string(), - text_elements: Vec::new(), - }, - CoreUserInput::Image { - image_url: "https://example.com/image.png".to_string(), - }, - CoreUserInput::LocalImage { - path: PathBuf::from("local/image.png"), - }, - CoreUserInput::Skill { - name: "skill-creator".to_string(), - path: PathBuf::from("/repo/.codex/skills/skill-creator/SKILL.md"), - }, - CoreUserInput::Mention { - name: "Demo App".to_string(), - path: "app://demo-app".to_string(), - }, - ], - }); - - assert_eq!( - ThreadItem::from(user_item), - ThreadItem::UserMessage { - id: "user-1".to_string(), - content: vec![ - UserInput::Text { - text: "hello".to_string(), - text_elements: Vec::new(), - }, - UserInput::Image { - url: "https://example.com/image.png".to_string(), - }, - UserInput::LocalImage { - path: PathBuf::from("local/image.png"), - }, - UserInput::Skill { - name: "skill-creator".to_string(), - path: PathBuf::from("/repo/.codex/skills/skill-creator/SKILL.md"), - }, - UserInput::Mention { - name: "Demo App".to_string(), - path: "app://demo-app".to_string(), - }, - ], - } - ); - - let agent_item = TurnItem::AgentMessage(AgentMessageItem { - id: "agent-1".to_string(), - content: vec![ - AgentMessageContent::Text { - text: "Hello ".to_string(), - }, - AgentMessageContent::Text { - text: "world".to_string(), - }, - ], - phase: None, - memory_citation: None, - }); - - assert_eq!( - ThreadItem::from(agent_item), - ThreadItem::AgentMessage { - id: "agent-1".to_string(), - text: "Hello world".to_string(), - phase: None, - memory_citation: None, - } - ); - - let agent_item_with_phase = TurnItem::AgentMessage(AgentMessageItem { - id: "agent-2".to_string(), - content: vec![AgentMessageContent::Text { - text: "final".to_string(), - }], - phase: Some(MessagePhase::FinalAnswer), - memory_citation: Some(CoreMemoryCitation { - entries: vec![CoreMemoryCitationEntry { - path: "MEMORY.md".to_string(), - line_start: 1, - line_end: 2, - note: "summary".to_string(), - }], - rollout_ids: vec!["rollout-1".to_string()], - }), - }); - - assert_eq!( - ThreadItem::from(agent_item_with_phase), - ThreadItem::AgentMessage { - id: "agent-2".to_string(), - text: "final".to_string(), - phase: Some(MessagePhase::FinalAnswer), - memory_citation: Some(MemoryCitation { - entries: vec![MemoryCitationEntry { - path: "MEMORY.md".to_string(), - line_start: 1, - line_end: 2, - note: "summary".to_string(), - }], - thread_ids: vec!["rollout-1".to_string()], - }), - } - ); - - let reasoning_item = TurnItem::Reasoning(ReasoningItem { - id: "reasoning-1".to_string(), - summary_text: vec!["line one".to_string(), "line two".to_string()], - raw_content: vec![], - }); - - assert_eq!( - ThreadItem::from(reasoning_item), - ThreadItem::Reasoning { - id: "reasoning-1".to_string(), - summary: vec!["line one".to_string(), "line two".to_string()], - content: vec![], - } - ); - - let search_item = TurnItem::WebSearch(WebSearchItem { - id: "search-1".to_string(), - query: "docs".to_string(), - action: CoreWebSearchAction::Search { - query: Some("docs".to_string()), - queries: None, - }, - }); - - assert_eq!( - ThreadItem::from(search_item), - ThreadItem::WebSearch { - id: "search-1".to_string(), - query: "docs".to_string(), - action: Some(WebSearchAction::Search { - query: Some("docs".to_string()), - queries: None, - }), - } - ); - - let image_view_item = TurnItem::ImageView(ImageViewItem { - id: "view-image-1".to_string(), - path: test_path_buf("/tmp/view-image.png").abs(), - }); - - assert_eq!( - ThreadItem::from(image_view_item), - ThreadItem::ImageView { - id: "view-image-1".to_string(), - path: test_path_buf("/tmp/view-image.png").abs(), - } - ); - - let file_change_item = TurnItem::FileChange(FileChangeItem { - id: "patch-1".to_string(), - changes: [( - PathBuf::from("README.md"), - codex_protocol::protocol::FileChange::Add { - content: "hello\n".to_string(), - }, - )] - .into_iter() - .collect(), - status: Some(codex_protocol::protocol::PatchApplyStatus::Completed), - auto_approved: None, - stdout: Some("Done!".to_string()), - stderr: Some(String::new()), - }); - - assert_eq!( - ThreadItem::from(file_change_item), - ThreadItem::FileChange { - id: "patch-1".to_string(), - changes: vec![FileUpdateChange { - path: "README.md".to_string(), - kind: PatchChangeKind::Add, - diff: "hello\n".to_string(), - }], - status: PatchApplyStatus::Completed, - } - ); - - let mcp_tool_call_item = TurnItem::McpToolCall(McpToolCallItem { - id: "mcp-1".to_string(), - server: "server".to_string(), - tool: "tool".to_string(), - arguments: json!({"arg": "value"}), - mcp_app_resource_uri: Some("app://connector".to_string()), - status: CoreMcpToolCallStatus::InProgress, - result: None, - error: None, - duration: None, - }); - - assert_eq!( - ThreadItem::from(mcp_tool_call_item), - ThreadItem::McpToolCall { - id: "mcp-1".to_string(), - server: "server".to_string(), - tool: "tool".to_string(), - status: McpToolCallStatus::InProgress, - arguments: json!({"arg": "value"}), - mcp_app_resource_uri: Some("app://connector".to_string()), - result: None, - error: None, - duration_ms: None, - } - ); - - let completed_mcp_tool_call_item = TurnItem::McpToolCall(McpToolCallItem { - id: "mcp-2".to_string(), - server: "server".to_string(), - tool: "tool".to_string(), - arguments: JsonValue::Null, - mcp_app_resource_uri: None, - status: CoreMcpToolCallStatus::Completed, - result: Some(CallToolResult { - content: vec![json!({"type": "text", "text": "ok"})], - structured_content: Some(json!({"ok": true})), - is_error: Some(false), - meta: Some(json!({"trace": "1"})), - }), - error: None, - duration: Some(Duration::from_millis(42)), - }); - - assert_eq!( - ThreadItem::from(completed_mcp_tool_call_item), - ThreadItem::McpToolCall { - id: "mcp-2".to_string(), - server: "server".to_string(), - tool: "tool".to_string(), - status: McpToolCallStatus::Completed, - arguments: JsonValue::Null, - mcp_app_resource_uri: None, - result: Some(Box::new(McpToolCallResult { - content: vec![json!({"type": "text", "text": "ok"})], - structured_content: Some(json!({"ok": true})), - meta: Some(json!({"trace": "1"})), - })), - error: None, - duration_ms: Some(42), - } - ); - } - - #[test] - fn skills_list_params_serialization_uses_force_reload() { - assert_eq!( - serde_json::to_value(SkillsListParams { - cwds: Vec::new(), - force_reload: false, - per_cwd_extra_user_roots: None, - }) - .unwrap(), - json!({ - "perCwdExtraUserRoots": null, - }), - ); - - assert_eq!( - serde_json::to_value(SkillsListParams { - cwds: vec![PathBuf::from("/repo")], - force_reload: true, - per_cwd_extra_user_roots: Some(vec![SkillsListExtraRootsForCwd { - cwd: PathBuf::from("/repo"), - extra_user_roots: vec![ - PathBuf::from("/shared/skills"), - PathBuf::from("/tmp/x") - ], - }]), - }) - .unwrap(), - json!({ - "cwds": ["/repo"], - "forceReload": true, - "perCwdExtraUserRoots": [ - { - "cwd": "/repo", - "extraUserRoots": ["/shared/skills", "/tmp/x"], - } - ], - }), - ); - } - - #[test] - fn plugin_source_serializes_local_git_and_remote_variants() { - let local_path = if cfg!(windows) { - r"C:\plugins\linear" - } else { - "/plugins/linear" - }; - let local_path = AbsolutePathBuf::try_from(PathBuf::from(local_path)).unwrap(); - let local_path_json = local_path.as_path().display().to_string(); - - assert_eq!( - serde_json::to_value(PluginSource::Local { path: local_path }).unwrap(), - json!({ - "type": "local", - "path": local_path_json, - }), - ); - - assert_eq!( - serde_json::to_value(PluginSource::Git { - url: "https://github.com/openai/example.git".to_string(), - path: Some("plugins/example".to_string()), - ref_name: Some("main".to_string()), - sha: Some("abc123".to_string()), - }) - .unwrap(), - json!({ - "type": "git", - "url": "https://github.com/openai/example.git", - "path": "plugins/example", - "refName": "main", - "sha": "abc123", - }), - ); - - assert_eq!( - serde_json::to_value(PluginSource::Remote).unwrap(), - json!({ - "type": "remote", - }), - ); - } - - #[test] - fn marketplace_add_params_serialization_uses_optional_ref_name_and_sparse_paths() { - assert_eq!( - serde_json::to_value(MarketplaceAddParams { - source: "owner/repo".to_string(), - ref_name: None, - sparse_paths: None, - }) - .unwrap(), - json!({ - "source": "owner/repo", - "refName": null, - "sparsePaths": null, - }), - ); - - assert_eq!( - serde_json::to_value(MarketplaceAddParams { - source: "owner/repo".to_string(), - ref_name: Some("main".to_string()), - sparse_paths: Some(vec!["plugins/foo".to_string()]), - }) - .unwrap(), - json!({ - "source": "owner/repo", - "refName": "main", - "sparsePaths": ["plugins/foo"], - }), - ); - } - - #[test] - fn marketplace_upgrade_params_serialization_uses_optional_marketplace_name() { - assert_eq!( - serde_json::to_value(MarketplaceUpgradeParams { - marketplace_name: None, - }) - .unwrap(), - json!({ - "marketplaceName": null, - }), - ); - - assert_eq!( - serde_json::from_value::(json!({})).unwrap(), - MarketplaceUpgradeParams { - marketplace_name: None, - }, - ); - - assert_eq!( - serde_json::to_value(MarketplaceUpgradeParams { - marketplace_name: Some("debug".to_string()), - }) - .unwrap(), - json!({ - "marketplaceName": "debug", - }), - ); - } - - #[test] - fn plugin_marketplace_entry_serializes_remote_only_path_as_null() { - assert_eq!( - serde_json::to_value(PluginMarketplaceEntry { - name: "openai-curated".to_string(), - path: None, - interface: None, - plugins: Vec::new(), - }) - .unwrap(), - json!({ - "name": "openai-curated", - "path": null, - "interface": null, - "plugins": [], - }), - ); - } - - #[test] - fn plugin_interface_serializes_local_paths_and_remote_urls_separately() { - let composer_icon = if cfg!(windows) { - r"C:\plugins\linear\icon.png" - } else { - "/plugins/linear/icon.png" - }; - let composer_icon = AbsolutePathBuf::try_from(PathBuf::from(composer_icon)).unwrap(); - let composer_icon_json = composer_icon.as_path().display().to_string(); - - let interface = PluginInterface { - display_name: Some("Linear".to_string()), - short_description: None, - long_description: None, - developer_name: None, - category: Some("Productivity".to_string()), - capabilities: Vec::new(), - website_url: None, - privacy_policy_url: None, - terms_of_service_url: None, - default_prompt: None, - brand_color: None, - composer_icon: Some(composer_icon), - composer_icon_url: Some("https://example.com/linear/icon.png".to_string()), - logo: None, - logo_url: Some("https://example.com/linear/logo.png".to_string()), - screenshots: Vec::new(), - screenshot_urls: vec!["https://example.com/linear/screenshot.png".to_string()], - }; - - assert_eq!( - serde_json::to_value(interface).unwrap(), - json!({ - "displayName": "Linear", - "shortDescription": null, - "longDescription": null, - "developerName": null, - "category": "Productivity", - "capabilities": [], - "websiteUrl": null, - "privacyPolicyUrl": null, - "termsOfServiceUrl": null, - "defaultPrompt": null, - "brandColor": null, - "composerIcon": composer_icon_json, - "composerIconUrl": "https://example.com/linear/icon.png", - "logo": null, - "logoUrl": "https://example.com/linear/logo.png", - "screenshots": [], - "screenshotUrls": ["https://example.com/linear/screenshot.png"], - }), - ); - } - - #[test] - fn plugin_list_params_ignore_removed_force_remote_sync_field() { - assert_eq!( - serde_json::from_value::(json!({ - "cwds": null, - "forceRemoteSync": true, - })) - .unwrap(), - PluginListParams { cwds: None }, - ); - } - - #[test] - fn plugin_read_params_serialization_uses_install_source_fields() { - let marketplace_path = if cfg!(windows) { - r"C:\plugins\marketplace.json" - } else { - "/plugins/marketplace.json" - }; - let marketplace_path = AbsolutePathBuf::try_from(PathBuf::from(marketplace_path)).unwrap(); - let marketplace_path_json = marketplace_path.as_path().display().to_string(); - assert_eq!( - serde_json::to_value(PluginReadParams { - marketplace_path: Some(marketplace_path.clone()), - remote_marketplace_name: None, - plugin_name: "gmail".to_string(), - }) - .unwrap(), - json!({ - "marketplacePath": marketplace_path_json, - "remoteMarketplaceName": null, - "pluginName": "gmail", - }), - ); - - assert_eq!( - serde_json::from_value::(json!({ - "marketplacePath": marketplace_path_json, - "pluginName": "gmail", - "forceRemoteSync": true, - })) - .unwrap(), - PluginReadParams { - marketplace_path: Some(marketplace_path), - remote_marketplace_name: None, - plugin_name: "gmail".to_string(), - }, - ); - - assert_eq!( - serde_json::from_value::(json!({ - "remoteMarketplaceName": "openai-curated", - "pluginName": "gmail", - })) - .unwrap(), - PluginReadParams { - marketplace_path: None, - remote_marketplace_name: Some("openai-curated".to_string()), - plugin_name: "gmail".to_string(), - }, - ); - } - - #[test] - fn plugin_install_params_serialization_omits_force_remote_sync() { - let marketplace_path = if cfg!(windows) { - r"C:\plugins\marketplace.json" - } else { - "/plugins/marketplace.json" - }; - let marketplace_path = AbsolutePathBuf::try_from(PathBuf::from(marketplace_path)).unwrap(); - let marketplace_path_json = marketplace_path.as_path().display().to_string(); - assert_eq!( - serde_json::to_value(PluginInstallParams { - marketplace_path: Some(marketplace_path.clone()), - remote_marketplace_name: None, - plugin_name: "gmail".to_string(), - }) - .unwrap(), - json!({ - "marketplacePath": marketplace_path_json, - "remoteMarketplaceName": null, - "pluginName": "gmail", - }), - ); - - assert_eq!( - serde_json::from_value::(json!({ - "marketplacePath": marketplace_path_json, - "pluginName": "gmail", - "forceRemoteSync": true, - })) - .unwrap(), - PluginInstallParams { - marketplace_path: Some(marketplace_path), - remote_marketplace_name: None, - plugin_name: "gmail".to_string(), - }, - ); - - assert_eq!( - serde_json::from_value::(json!({ - "remoteMarketplaceName": "openai-curated", - "pluginName": "gmail", - "forceRemoteSync": true, - })) - .unwrap(), - PluginInstallParams { - marketplace_path: None, - remote_marketplace_name: Some("openai-curated".to_string()), - plugin_name: "gmail".to_string(), - }, - ); - } - - #[test] - fn plugin_skill_read_params_serialization_uses_remote_plugin_id() { - assert_eq!( - serde_json::to_value(PluginSkillReadParams { - remote_marketplace_name: "chatgpt-global".to_string(), - remote_plugin_id: "plugins~Plugin_00000000000000000000000000000000".to_string(), - skill_name: "plan-work".to_string(), - }) - .unwrap(), - json!({ - "remoteMarketplaceName": "chatgpt-global", - "remotePluginId": "plugins~Plugin_00000000000000000000000000000000", - "skillName": "plan-work", - }), - ); - } - - #[test] - fn plugin_share_params_and_response_serialization_use_camel_case_fields() { - let plugin_path = if cfg!(windows) { - r"C:\plugins\gmail" - } else { - "/plugins/gmail" - }; - let plugin_path = AbsolutePathBuf::try_from(PathBuf::from(plugin_path)).unwrap(); - let plugin_path_json = plugin_path.as_path().display().to_string(); - - assert_eq!( - serde_json::to_value(PluginShareSaveParams { - plugin_path: plugin_path.clone(), - remote_plugin_id: None, - }) - .unwrap(), - json!({ - "pluginPath": plugin_path_json, - "remotePluginId": null, - }), - ); - - assert_eq!( - serde_json::to_value(PluginShareSaveParams { - plugin_path, - remote_plugin_id: Some( - "plugins~Plugin_00000000000000000000000000000000".to_string(), - ), - }) - .unwrap(), - json!({ - "pluginPath": plugin_path_json, - "remotePluginId": "plugins~Plugin_00000000000000000000000000000000", - }), - ); - - assert_eq!( - serde_json::to_value(PluginShareSaveResponse { - remote_plugin_id: "plugins~Plugin_00000000000000000000000000000000".to_string(), - share_url: String::new(), - }) - .unwrap(), - json!({ - "remotePluginId": "plugins~Plugin_00000000000000000000000000000000", - "shareUrl": "", - }), - ); - - assert_eq!( - serde_json::from_value::(json!({})).unwrap(), - PluginShareListParams {}, - ); - - assert_eq!( - serde_json::to_value(PluginShareDeleteParams { - remote_plugin_id: "plugins~Plugin_00000000000000000000000000000000".to_string(), - }) - .unwrap(), - json!({ - "remotePluginId": "plugins~Plugin_00000000000000000000000000000000", - }), - ); - } - - #[test] - fn plugin_share_list_response_serializes_share_items() { - assert_eq!( - serde_json::to_value(PluginShareListResponse { - data: vec![PluginShareListItem { - plugin: PluginSummary { - id: "plugins~Plugin_00000000000000000000000000000000".to_string(), - name: "gmail".to_string(), - source: PluginSource::Remote, - installed: false, - enabled: false, - install_policy: PluginInstallPolicy::Available, - auth_policy: PluginAuthPolicy::OnUse, - availability: PluginAvailability::Available, - interface: None, - }, - share_url: "https://chatgpt.example/plugins/share/share-key-1".to_string(), - local_plugin_path: None, - }], - }) - .unwrap(), - json!({ - "data": [{ - "plugin": { - "id": "plugins~Plugin_00000000000000000000000000000000", - "name": "gmail", - "source": { "type": "remote" }, - "installed": false, - "enabled": false, - "installPolicy": "AVAILABLE", - "authPolicy": "ON_USE", - "availability": "AVAILABLE", - "interface": null, - }, - "shareUrl": "https://chatgpt.example/plugins/share/share-key-1", - "localPluginPath": null, - }], - }), - ); - } - - #[test] - fn plugin_summary_defaults_missing_availability_to_available() { - let summary: PluginSummary = serde_json::from_value(json!({ - "id": "plugins~Plugin_00000000000000000000000000000000", - "name": "gmail", - "source": { "type": "remote" }, - "installed": false, - "enabled": false, - "installPolicy": "AVAILABLE", - "authPolicy": "ON_USE", - "interface": null, - })) - .unwrap(); - - assert_eq!(summary.availability, PluginAvailability::Available); - } - - #[test] - fn plugin_availability_deserializes_enabled_alias() { - let availability: PluginAvailability = serde_json::from_value(json!("ENABLED")).unwrap(); - - assert_eq!(availability, PluginAvailability::Available); - assert_eq!( - serde_json::to_value(availability).unwrap(), - json!("AVAILABLE") - ); - } - - #[test] - fn plugin_uninstall_params_serialization_omits_force_remote_sync() { - assert_eq!( - serde_json::to_value(PluginUninstallParams { - plugin_id: "gmail@openai-curated".to_string(), - }) - .unwrap(), - json!({ - "pluginId": "gmail@openai-curated", - }), - ); - - assert_eq!( - serde_json::from_value::(json!({ - "pluginId": "gmail@openai-curated", - "forceRemoteSync": true, - })) - .unwrap(), - PluginUninstallParams { - plugin_id: "gmail@openai-curated".to_string(), - }, - ); - - assert_eq!( - serde_json::to_value(PluginUninstallParams { - plugin_id: "plugins~Plugin_gmail".to_string(), - }) - .unwrap(), - json!({ - "pluginId": "plugins~Plugin_gmail", - }), - ); - - assert_eq!( - serde_json::from_value::(json!({ - "pluginId": "plugins~Plugin_gmail", - "forceRemoteSync": true, - })) - .unwrap(), - PluginUninstallParams { - plugin_id: "plugins~Plugin_gmail".to_string(), - }, - ); - } - - #[test] - fn marketplace_remove_response_serializes_nullable_installed_root() { - let installed_root = if cfg!(windows) { - r"C:\marketplaces\debug" - } else { - "/tmp/marketplaces/debug" - }; - let installed_root = AbsolutePathBuf::try_from(PathBuf::from(installed_root)).unwrap(); - let installed_root_json = installed_root.as_path().display().to_string(); - assert_eq!( - serde_json::to_value(MarketplaceRemoveResponse { - marketplace_name: "debug".to_string(), - installed_root: Some(installed_root), - }) - .unwrap(), - json!({ - "marketplaceName": "debug", - "installedRoot": installed_root_json, - }), - ); - - assert_eq!( - serde_json::to_value(MarketplaceRemoveResponse { - marketplace_name: "debug".to_string(), - installed_root: None, - }) - .unwrap(), - json!({ - "marketplaceName": "debug", - "installedRoot": null, - }), - ); - } - - #[test] - fn marketplace_upgrade_response_serializes_camel_case_fields() { - let upgraded_root = if cfg!(windows) { - r"C:\marketplaces\debug" - } else { - "/tmp/marketplaces/debug" - }; - let upgraded_root = AbsolutePathBuf::try_from(PathBuf::from(upgraded_root)).unwrap(); - let upgraded_root_json = upgraded_root.as_path().display().to_string(); - - assert_eq!( - serde_json::to_value(MarketplaceUpgradeResponse { - selected_marketplaces: vec!["debug".to_string()], - upgraded_roots: vec![upgraded_root], - errors: vec![MarketplaceUpgradeErrorInfo { - marketplace_name: "broken".to_string(), - message: "failed to clone".to_string(), - }], - }) - .unwrap(), - json!({ - "selectedMarketplaces": ["debug"], - "upgradedRoots": [upgraded_root_json], - "errors": [{ - "marketplaceName": "broken", - "message": "failed to clone", - }], - }), - ); - } - - #[test] - fn codex_error_info_serializes_http_status_code_in_camel_case() { - let value = CodexErrorInfo::ResponseTooManyFailedAttempts { - http_status_code: Some(401), - }; - - assert_eq!( - serde_json::to_value(value).unwrap(), - json!({ - "responseTooManyFailedAttempts": { - "httpStatusCode": 401 - } - }) - ); - } - - #[test] - fn codex_error_info_serializes_cyber_policy_in_camel_case() { - assert_eq!( - serde_json::to_value(CodexErrorInfo::CyberPolicy).unwrap(), - json!("cyberPolicy") - ); - } - - #[test] - fn codex_error_info_serializes_active_turn_not_steerable_turn_kind_in_camel_case() { - let value = CodexErrorInfo::ActiveTurnNotSteerable { - turn_kind: NonSteerableTurnKind::Review, - }; - - assert_eq!( - serde_json::to_value(value).unwrap(), - json!({ - "activeTurnNotSteerable": { - "turnKind": "review" - } - }) - ); - } - - #[test] - fn dynamic_tool_response_serializes_content_items() { - let value = serde_json::to_value(DynamicToolCallResponse { - content_items: vec![DynamicToolCallOutputContentItem::InputText { - text: "dynamic-ok".to_string(), - }], - success: true, - }) - .unwrap(); - - assert_eq!( - value, - json!({ - "contentItems": [ - { - "type": "inputText", - "text": "dynamic-ok" - } - ], - "success": true, - }) - ); - } - - #[test] - fn dynamic_tool_response_serializes_text_and_image_content_items() { - let value = serde_json::to_value(DynamicToolCallResponse { - content_items: vec![ - DynamicToolCallOutputContentItem::InputText { - text: "dynamic-ok".to_string(), - }, - DynamicToolCallOutputContentItem::InputImage { - image_url: "data:image/png;base64,AAA".to_string(), - }, - ], - success: true, - }) - .unwrap(); - - assert_eq!( - value, - json!({ - "contentItems": [ - { - "type": "inputText", - "text": "dynamic-ok" - }, - { - "type": "inputImage", - "imageUrl": "data:image/png;base64,AAA" - } - ], - "success": true, - }) - ); - } - - #[test] - fn dynamic_tool_spec_deserializes_defer_loading() { - let value = json!({ - "name": "lookup_ticket", - "description": "Fetch a ticket", - "inputSchema": { - "type": "object", - "properties": { - "id": { "type": "string" } - } - }, - "deferLoading": true, - }); - - let actual: DynamicToolSpec = serde_json::from_value(value).expect("deserialize"); - - assert_eq!( - actual, - DynamicToolSpec { - namespace: None, - name: "lookup_ticket".to_string(), - description: "Fetch a ticket".to_string(), - input_schema: json!({ - "type": "object", - "properties": { - "id": { "type": "string" } - } - }), - defer_loading: true, - } - ); - } - - #[test] - fn dynamic_tool_spec_legacy_expose_to_context_inverts_to_defer_loading() { - let value = json!({ - "name": "lookup_ticket", - "description": "Fetch a ticket", - "inputSchema": { - "type": "object", - "properties": {} - }, - "exposeToContext": false, - }); - - let actual: DynamicToolSpec = serde_json::from_value(value).expect("deserialize"); - - assert!(actual.defer_loading); - } - - #[test] - fn thread_start_params_preserve_explicit_null_service_tier() { - let params: ThreadStartParams = serde_json::from_value(json!({ "serviceTier": null })) - .expect("params should deserialize"); - assert_eq!(params.service_tier, Some(None)); - - let serialized = serde_json::to_value(¶ms).expect("params should serialize"); - assert_eq!( - serialized.get("serviceTier"), - Some(&serde_json::Value::Null) - ); - - let serialized_without_override = - serde_json::to_value(ThreadStartParams::default()).expect("params should serialize"); - assert_eq!(serialized_without_override.get("serviceTier"), None); - } - - #[test] - fn thread_lifecycle_responses_default_missing_compat_fields() { - let response = json!({ - "thread": { - "id": "thread-id", - "forkedFromId": null, - "preview": "", - "ephemeral": false, - "modelProvider": "openai", - "createdAt": 1, - "updatedAt": 1, - "status": { "type": "idle" }, - "path": null, - "cwd": absolute_path_string("tmp"), - "cliVersion": "0.0.0", - "source": "exec", - "agentNickname": null, - "agentRole": null, - "gitInfo": null, - "name": null, - "turns": [] - }, - "model": "gpt-5", - "modelProvider": "openai", - "serviceTier": null, - "cwd": absolute_path_string("tmp"), - "approvalPolicy": "on-failure", - "approvalsReviewer": "user", - "sandbox": { "type": "dangerFullAccess" }, - "reasoningEffort": null - }); - - let start: ThreadStartResponse = - serde_json::from_value(response.clone()).expect("thread/start response"); - let resume: ThreadResumeResponse = - serde_json::from_value(response.clone()).expect("thread/resume response"); - let fork: ThreadForkResponse = - serde_json::from_value(response).expect("thread/fork response"); - - assert_eq!(start.instruction_sources, Vec::::new()); - assert_eq!(resume.instruction_sources, Vec::::new()); - assert_eq!(fork.instruction_sources, Vec::::new()); - assert_eq!(start.permission_profile, None); - assert_eq!(resume.permission_profile, None); - assert_eq!(fork.permission_profile, None); - assert_eq!(start.active_permission_profile, None); - assert_eq!(resume.active_permission_profile, None); - assert_eq!(fork.active_permission_profile, None); - } - - #[test] - fn turn_start_params_preserve_explicit_null_service_tier() { - let params: TurnStartParams = serde_json::from_value(json!({ - "threadId": "thread_123", - "input": [], - "serviceTier": null - })) - .expect("params should deserialize"); - assert_eq!(params.service_tier, Some(None)); - - let serialized = serde_json::to_value(¶ms).expect("params should serialize"); - assert_eq!( - serialized.get("serviceTier"), - Some(&serde_json::Value::Null) - ); - - let without_override = TurnStartParams { - thread_id: "thread_123".to_string(), - input: vec![], - responsesapi_client_metadata: None, - environments: None, - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permissions: None, - model: None, - service_tier: None, - effort: None, - summary: None, - output_schema: None, - collaboration_mode: None, - personality: None, - }; - let serialized_without_override = - serde_json::to_value(&without_override).expect("params should serialize"); - assert_eq!(serialized_without_override.get("serviceTier"), None); - } - - #[test] - fn turn_start_params_round_trip_environments() { - let cwd = test_absolute_path(); - let params: TurnStartParams = serde_json::from_value(json!({ - "threadId": "thread_123", - "input": [], - "environments": [ - { - "environmentId": "local", - "cwd": cwd - } - ], - })) - .expect("params should deserialize"); - - assert_eq!( - params.environments, - Some(vec![TurnEnvironmentParams { - environment_id: "local".to_string(), - cwd: cwd.clone(), - }]) - ); - assert_eq!( - crate::experimental_api::ExperimentalApi::experimental_reason(¶ms), - Some("turn/start.environments") - ); - - let serialized = serde_json::to_value(¶ms).expect("params should serialize"); - assert_eq!( - serialized.get("environments"), - Some(&json!([ - { - "environmentId": "local", - "cwd": cwd - } - ])) - ); - } - - #[test] - fn turn_start_params_preserve_empty_environments() { - let params: TurnStartParams = serde_json::from_value(json!({ - "threadId": "thread_123", - "input": [], - "environments": [], - })) - .expect("params should deserialize"); - - assert_eq!(params.environments, Some(Vec::new())); - assert_eq!( - crate::experimental_api::ExperimentalApi::experimental_reason(¶ms), - Some("turn/start.environments") - ); - - let serialized = serde_json::to_value(¶ms).expect("params should serialize"); - assert_eq!(serialized.get("environments"), Some(&json!([]))); - } - - #[test] - fn turn_start_params_treat_null_or_omitted_environments_as_default() { - let null_environments: TurnStartParams = serde_json::from_value(json!({ - "threadId": "thread_123", - "input": [], - "environments": null, - })) - .expect("params should deserialize"); - let omitted_environments: TurnStartParams = serde_json::from_value(json!({ - "threadId": "thread_123", - "input": [], - })) - .expect("params should deserialize"); - - assert_eq!(null_environments.environments, None); - assert_eq!(omitted_environments.environments, None); - assert_eq!( - crate::experimental_api::ExperimentalApi::experimental_reason(&null_environments), - None - ); - assert_eq!( - crate::experimental_api::ExperimentalApi::experimental_reason(&omitted_environments), - None - ); - } - - #[test] - fn turn_start_params_reject_relative_environment_cwd() { - let err = serde_json::from_value::(json!({ - "threadId": "thread_123", - "input": [], - "environments": [ - { - "environmentId": "local", - "cwd": "relative" - } - ], - })) - .expect_err("relative environment cwd should fail"); - - assert!( - err.to_string() - .contains("AbsolutePathBuf deserialized without a base path"), - "unexpected error: {err}" - ); - } -} diff --git a/codex-rs/app-server-protocol/src/protocol/v2/account.rs b/codex-rs/app-server-protocol/src/protocol/v2/account.rs new file mode 100644 index 0000000000..efb4a26f60 --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/account.rs @@ -0,0 +1,383 @@ +use crate::protocol::common::AuthMode; +use codex_experimental_api_macros::ExperimentalApi; +use codex_protocol::account::PlanType; +use codex_protocol::account::ProviderAccount; +use codex_protocol::protocol::CreditsSnapshot as CoreCreditsSnapshot; +use codex_protocol::protocol::RateLimitReachedType as CoreRateLimitReachedType; +use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot; +use codex_protocol::protocol::RateLimitWindow as CoreRateLimitWindow; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use std::collections::HashMap; +use ts_rs::TS; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type")] +#[ts(export_to = "v2/")] +pub enum Account { + #[serde(rename = "apiKey", rename_all = "camelCase")] + #[ts(rename = "apiKey", rename_all = "camelCase")] + ApiKey {}, + + #[serde(rename = "chatgpt", rename_all = "camelCase")] + #[ts(rename = "chatgpt", rename_all = "camelCase")] + Chatgpt { email: String, plan_type: PlanType }, + + #[serde(rename = "amazonBedrock", rename_all = "camelCase")] + #[ts(rename = "amazonBedrock", rename_all = "camelCase")] + AmazonBedrock {}, +} + +impl From for Account { + fn from(account: ProviderAccount) -> Self { + match account { + ProviderAccount::ApiKey => Self::ApiKey {}, + ProviderAccount::Chatgpt { email, plan_type } => Self::Chatgpt { email, plan_type }, + ProviderAccount::AmazonBedrock => Self::AmazonBedrock {}, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] +#[serde(tag = "type")] +#[ts(tag = "type")] +#[ts(export_to = "v2/")] +pub enum LoginAccountParams { + #[serde(rename = "apiKey", rename_all = "camelCase")] + #[ts(rename = "apiKey", rename_all = "camelCase")] + ApiKey { + #[serde(rename = "apiKey")] + #[ts(rename = "apiKey")] + api_key: String, + }, + #[serde(rename = "chatgpt", rename_all = "camelCase")] + #[ts(rename = "chatgpt", rename_all = "camelCase")] + Chatgpt { + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + codex_streamlined_login: bool, + }, + #[serde(rename = "chatgptDeviceCode")] + #[ts(rename = "chatgptDeviceCode")] + ChatgptDeviceCode, + /// [UNSTABLE] FOR OPENAI INTERNAL USE ONLY - DO NOT USE. + /// The access token must contain the same scopes that Codex-managed ChatGPT auth tokens have. + #[experimental("account/login/start.chatgptAuthTokens")] + #[serde(rename = "chatgptAuthTokens", rename_all = "camelCase")] + #[ts(rename = "chatgptAuthTokens", rename_all = "camelCase")] + ChatgptAuthTokens { + /// Access token (JWT) supplied by the client. + /// This token is used for backend API requests and email extraction. + access_token: String, + /// Workspace/account identifier supplied by the client. + chatgpt_account_id: String, + /// Optional plan type supplied by the client. + /// + /// When `null`, Codex attempts to derive the plan type from access-token + /// claims. If unavailable, the plan defaults to `unknown`. + #[ts(optional = nullable)] + chatgpt_plan_type: Option, + }, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type")] +#[ts(export_to = "v2/")] +pub enum LoginAccountResponse { + #[serde(rename = "apiKey", rename_all = "camelCase")] + #[ts(rename = "apiKey", rename_all = "camelCase")] + ApiKey {}, + #[serde(rename = "chatgpt", rename_all = "camelCase")] + #[ts(rename = "chatgpt", rename_all = "camelCase")] + Chatgpt { + // Use plain String for identifiers to avoid TS/JSON Schema quirks around uuid-specific types. + // Convert to/from UUIDs at the application layer as needed. + login_id: String, + /// URL the client should open in a browser to initiate the OAuth flow. + auth_url: String, + }, + #[serde(rename = "chatgptDeviceCode", rename_all = "camelCase")] + #[ts(rename = "chatgptDeviceCode", rename_all = "camelCase")] + ChatgptDeviceCode { + // Use plain String for identifiers to avoid TS/JSON Schema quirks around uuid-specific types. + // Convert to/from UUIDs at the application layer as needed. + login_id: String, + /// URL the client should open in a browser to complete device code authorization. + verification_url: String, + /// One-time code the user must enter after signing in. + user_code: String, + }, + #[serde(rename = "chatgptAuthTokens", rename_all = "camelCase")] + #[ts(rename = "chatgptAuthTokens", rename_all = "camelCase")] + ChatgptAuthTokens {}, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CancelLoginAccountParams { + pub login_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum CancelLoginAccountStatus { + Canceled, + NotFound, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CancelLoginAccountResponse { + pub status: CancelLoginAccountStatus, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct LogoutAccountResponse {} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum ChatgptAuthTokensRefreshReason { + /// Codex attempted a backend request and received `401 Unauthorized`. + Unauthorized, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ChatgptAuthTokensRefreshParams { + pub reason: ChatgptAuthTokensRefreshReason, + /// Workspace/account identifier that Codex was previously using. + /// + /// Clients that manage multiple accounts/workspaces can use this as a hint + /// to refresh the token for the correct workspace. + /// + /// This may be `null` when the prior auth state did not include a workspace + /// identifier (`chatgpt_account_id`). + #[ts(optional = nullable)] + pub previous_account_id: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ChatgptAuthTokensRefreshResponse { + pub access_token: String, + pub chatgpt_account_id: String, + pub chatgpt_plan_type: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct GetAccountRateLimitsResponse { + /// Backward-compatible single-bucket view; mirrors the historical payload. + pub rate_limits: RateLimitSnapshot, + /// Multi-bucket view keyed by metered `limit_id` (for example, `codex`). + pub rate_limits_by_limit_id: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SendAddCreditsNudgeEmailParams { + pub credit_type: AddCreditsNudgeCreditType, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/", rename_all = "snake_case")] +pub enum AddCreditsNudgeCreditType { + Credits, + UsageLimit, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SendAddCreditsNudgeEmailResponse { + pub status: AddCreditsNudgeEmailStatus, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/", rename_all = "snake_case")] +pub enum AddCreditsNudgeEmailStatus { + Sent, + CooldownActive, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct GetAccountParams { + /// When `true`, requests a proactive token refresh before returning. + /// + /// In managed auth mode this triggers the normal refresh-token flow. In + /// external auth mode this flag is ignored. Clients should refresh tokens + /// themselves and call `account/login/start` with `chatgptAuthTokens`. + #[serde(default)] + pub refresh_token: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct GetAccountResponse { + pub account: Option, + pub requires_openai_auth: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AccountUpdatedNotification { + pub auth_mode: Option, + pub plan_type: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AccountRateLimitsUpdatedNotification { + pub rate_limits: RateLimitSnapshot, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct RateLimitSnapshot { + pub limit_id: Option, + pub limit_name: Option, + pub primary: Option, + pub secondary: Option, + pub credits: Option, + pub plan_type: Option, + pub rate_limit_reached_type: Option, +} + +impl From for RateLimitSnapshot { + fn from(value: CoreRateLimitSnapshot) -> Self { + Self { + limit_id: value.limit_id, + limit_name: value.limit_name, + primary: value.primary.map(RateLimitWindow::from), + secondary: value.secondary.map(RateLimitWindow::from), + credits: value.credits.map(CreditsSnapshot::from), + plan_type: value.plan_type, + rate_limit_reached_type: value + .rate_limit_reached_type + .map(RateLimitReachedType::from), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/", rename_all = "snake_case")] +pub enum RateLimitReachedType { + RateLimitReached, + WorkspaceOwnerCreditsDepleted, + WorkspaceMemberCreditsDepleted, + WorkspaceOwnerUsageLimitReached, + WorkspaceMemberUsageLimitReached, +} + +impl From for RateLimitReachedType { + fn from(value: CoreRateLimitReachedType) -> Self { + match value { + CoreRateLimitReachedType::RateLimitReached => Self::RateLimitReached, + CoreRateLimitReachedType::WorkspaceOwnerCreditsDepleted => { + Self::WorkspaceOwnerCreditsDepleted + } + CoreRateLimitReachedType::WorkspaceMemberCreditsDepleted => { + Self::WorkspaceMemberCreditsDepleted + } + CoreRateLimitReachedType::WorkspaceOwnerUsageLimitReached => { + Self::WorkspaceOwnerUsageLimitReached + } + CoreRateLimitReachedType::WorkspaceMemberUsageLimitReached => { + Self::WorkspaceMemberUsageLimitReached + } + } + } +} + +impl From for CoreRateLimitReachedType { + fn from(value: RateLimitReachedType) -> Self { + match value { + RateLimitReachedType::RateLimitReached => Self::RateLimitReached, + RateLimitReachedType::WorkspaceOwnerCreditsDepleted => { + Self::WorkspaceOwnerCreditsDepleted + } + RateLimitReachedType::WorkspaceMemberCreditsDepleted => { + Self::WorkspaceMemberCreditsDepleted + } + RateLimitReachedType::WorkspaceOwnerUsageLimitReached => { + Self::WorkspaceOwnerUsageLimitReached + } + RateLimitReachedType::WorkspaceMemberUsageLimitReached => { + Self::WorkspaceMemberUsageLimitReached + } + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct RateLimitWindow { + pub used_percent: i32, + #[ts(type = "number | null")] + pub window_duration_mins: Option, + #[ts(type = "number | null")] + pub resets_at: Option, +} + +impl From for RateLimitWindow { + fn from(value: CoreRateLimitWindow) -> Self { + Self { + used_percent: value.used_percent.round() as i32, + window_duration_mins: value.window_minutes, + resets_at: value.resets_at, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CreditsSnapshot { + pub has_credits: bool, + pub unlimited: bool, + pub balance: Option, +} + +impl From for CreditsSnapshot { + fn from(value: CoreCreditsSnapshot) -> Self { + Self { + has_credits: value.has_credits, + unlimited: value.unlimited, + balance: value.balance, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AccountLoginCompletedNotification { + // Use plain String for identifiers to avoid TS/JSON Schema quirks around uuid-specific types. + // Convert to/from UUIDs at the application layer as needed. + pub login_id: Option, + pub success: bool, + pub error: Option, +} diff --git a/codex-rs/app-server-protocol/src/protocol/v2/apps.rs b/codex-rs/app-server-protocol/src/protocol/v2/apps.rs new file mode 100644 index 0000000000..9f46525e6c --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/apps.rs @@ -0,0 +1,146 @@ +use super::shared::default_enabled; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use std::collections::HashMap; +use ts_rs::TS; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +/// EXPERIMENTAL - list available apps/connectors. +pub struct AppsListParams { + /// Opaque pagination cursor returned by a previous call. + #[ts(optional = nullable)] + pub cursor: Option, + /// Optional page size; defaults to a reasonable server-side value. + #[ts(optional = nullable)] + pub limit: Option, + /// Optional thread id used to evaluate app feature gating from that thread's config. + #[ts(optional = nullable)] + pub thread_id: Option, + /// When true, bypass app caches and fetch the latest data from sources. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub force_refetch: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +/// EXPERIMENTAL - app metadata returned by app-list APIs. +pub struct AppBranding { + pub category: Option, + pub developer: Option, + pub website: Option, + pub privacy_policy: Option, + pub terms_of_service: Option, + pub is_discoverable_app: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AppReview { + pub status: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AppScreenshot { + pub url: Option, + #[serde(alias = "file_id")] + pub file_id: Option, + #[serde(alias = "user_prompt")] + pub user_prompt: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AppMetadata { + pub review: Option, + pub categories: Option>, + pub sub_categories: Option>, + pub seo_description: Option, + pub screenshots: Option>, + pub developer: Option, + pub version: Option, + pub version_id: Option, + pub version_notes: Option, + pub first_party_type: Option, + pub first_party_requires_install: Option, + pub show_in_composer_when_unlinked: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +/// EXPERIMENTAL - app metadata returned by app-list APIs. +pub struct AppInfo { + pub id: String, + pub name: String, + pub description: Option, + pub logo_url: Option, + pub logo_url_dark: Option, + pub distribution_channel: Option, + pub branding: Option, + pub app_metadata: Option, + pub labels: Option>, + pub install_url: Option, + #[serde(default)] + pub is_accessible: bool, + /// Whether this app is enabled in config.toml. + /// Example: + /// ```toml + /// [apps.bad_app] + /// enabled = false + /// ``` + #[serde(default = "default_enabled")] + pub is_enabled: bool, + #[serde(default)] + pub plugin_display_names: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +/// EXPERIMENTAL - app metadata summary for plugin responses. +pub struct AppSummary { + pub id: String, + pub name: String, + pub description: Option, + pub install_url: Option, + pub needs_auth: bool, +} + +impl From for AppSummary { + fn from(value: AppInfo) -> Self { + Self { + id: value.id, + name: value.name, + description: value.description, + install_url: value.install_url, + needs_auth: false, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +/// EXPERIMENTAL - app list response. +pub struct AppsListResponse { + pub data: 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")] +#[ts(export_to = "v2/")] +/// EXPERIMENTAL - notification emitted when the app list changes. +pub struct AppListUpdatedNotification { + pub data: Vec, +} diff --git a/codex-rs/app-server-protocol/src/protocol/v2/collaboration_mode.rs b/codex-rs/app-server-protocol/src/protocol/v2/collaboration_mode.rs new file mode 100644 index 0000000000..b013bc13d4 --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/collaboration_mode.rs @@ -0,0 +1,45 @@ +use codex_protocol::config_types::CollaborationModeMask as CoreCollaborationModeMask; +use codex_protocol::config_types::ModeKind; +use codex_protocol::openai_models::ReasoningEffort; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use ts_rs::TS; + +/// EXPERIMENTAL - list collaboration mode presets. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CollaborationModeListParams {} + +/// EXPERIMENTAL - collaboration mode preset metadata for clients. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CollaborationModeMask { + pub name: String, + pub mode: Option, + pub model: Option, + #[serde(rename = "reasoning_effort")] + #[ts(rename = "reasoning_effort")] + pub reasoning_effort: Option>, +} + +impl From for CollaborationModeMask { + fn from(value: CoreCollaborationModeMask) -> Self { + Self { + name: value.name, + mode: value.mode, + model: value.model, + reasoning_effort: value.reasoning_effort, + } + } +} + +/// EXPERIMENTAL - collaboration mode presets response. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CollaborationModeListResponse { + pub data: Vec, +} diff --git a/codex-rs/app-server-protocol/src/protocol/v2/command_exec.rs b/codex-rs/app-server-protocol/src/protocol/v2/command_exec.rs new file mode 100644 index 0000000000..ff0cecf4f9 --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/command_exec.rs @@ -0,0 +1,214 @@ +use super::PermissionProfile; +use super::SandboxPolicy; +use codex_experimental_api_macros::ExperimentalApi; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use std::collections::HashMap; +use std::path::PathBuf; +use ts_rs::TS; + +/// PTY size in character cells for `command/exec` PTY sessions. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CommandExecTerminalSize { + /// Terminal height in character cells. + pub rows: u16, + /// Terminal width in character cells. + pub cols: u16, +} + +/// Run a standalone command (argv vector) in the server sandbox without +/// creating a thread or turn. +/// +/// The final `command/exec` response is deferred until the process exits and is +/// sent only after all `command/exec/outputDelta` notifications for that +/// connection have been emitted. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CommandExecParams { + /// Command argv vector. Empty arrays are rejected. + pub command: Vec, + /// Optional client-supplied, connection-scoped process id. + /// + /// Required for `tty`, `streamStdin`, `streamStdoutStderr`, and follow-up + /// `command/exec/write`, `command/exec/resize`, and + /// `command/exec/terminate` calls. When omitted, buffered execution gets an + /// internal id that is not exposed to the client. + #[ts(optional = nullable)] + pub process_id: Option, + /// Enable PTY mode. + /// + /// This implies `streamStdin` and `streamStdoutStderr`. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub tty: bool, + /// Allow follow-up `command/exec/write` requests to write stdin bytes. + /// + /// Requires a client-supplied `processId`. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub stream_stdin: bool, + /// Stream stdout/stderr via `command/exec/outputDelta` notifications. + /// + /// Streamed bytes are not duplicated into the final response and require a + /// client-supplied `processId`. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub stream_stdout_stderr: bool, + /// Optional per-stream stdout/stderr capture cap in bytes. + /// + /// When omitted, the server default applies. Cannot be combined with + /// `disableOutputCap`. + #[ts(type = "number | null")] + #[ts(optional = nullable)] + pub output_bytes_cap: Option, + /// Disable stdout/stderr capture truncation for this request. + /// + /// Cannot be combined with `outputBytesCap`. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub disable_output_cap: bool, + /// Disable the timeout entirely for this request. + /// + /// Cannot be combined with `timeoutMs`. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub disable_timeout: bool, + /// Optional timeout in milliseconds. + /// + /// When omitted, the server default applies. Cannot be combined with + /// `disableTimeout`. + #[ts(type = "number | null")] + #[ts(optional = nullable)] + pub timeout_ms: Option, + /// Optional working directory. Defaults to the server cwd. + #[ts(optional = nullable)] + pub cwd: Option, + /// Optional environment overrides merged into the server-computed + /// environment. + /// + /// Matching names override inherited values. Set a key to `null` to unset + /// an inherited variable. + #[ts(optional = nullable)] + pub env: Option>>, + /// Optional initial PTY size in character cells. Only valid when `tty` is + /// true. + #[ts(optional = nullable)] + pub size: Option, + /// Optional sandbox policy for this command. + /// + /// Uses the same shape as thread/turn execution sandbox configuration and + /// defaults to the user's configured policy when omitted. Cannot be + /// combined with `permissionProfile`. + #[ts(optional = nullable)] + pub sandbox_policy: Option, + /// Optional full permissions profile for this command. + /// + /// Defaults to the user's configured permissions when omitted. Cannot be + /// combined with `sandboxPolicy`. + #[experimental("command/exec.permissionProfile")] + #[ts(optional = nullable)] + pub permission_profile: Option, +} + +/// Final buffered result for `command/exec`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CommandExecResponse { + /// Process exit code. + pub exit_code: i32, + /// Buffered stdout capture. + /// + /// Empty when stdout was streamed via `command/exec/outputDelta`. + pub stdout: String, + /// Buffered stderr capture. + /// + /// Empty when stderr was streamed via `command/exec/outputDelta`. + pub stderr: String, +} + +/// Write stdin bytes to a running `command/exec` session, close stdin, or +/// both. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CommandExecWriteParams { + /// Client-supplied, connection-scoped `processId` from the original + /// `command/exec` request. + pub process_id: String, + /// Optional base64-encoded stdin bytes to write. + #[ts(optional = nullable)] + pub delta_base64: Option, + /// Close stdin after writing `deltaBase64`, if present. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub close_stdin: bool, +} + +/// Empty success response for `command/exec/write`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CommandExecWriteResponse {} + +/// Terminate a running `command/exec` session. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CommandExecTerminateParams { + /// Client-supplied, connection-scoped `processId` from the original + /// `command/exec` request. + pub process_id: String, +} + +/// Empty success response for `command/exec/terminate`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CommandExecTerminateResponse {} + +/// Resize a running PTY-backed `command/exec` session. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CommandExecResizeParams { + /// Client-supplied, connection-scoped `processId` from the original + /// `command/exec` request. + pub process_id: String, + /// New PTY size in character cells. + pub size: CommandExecTerminalSize, +} + +/// Empty success response for `command/exec/resize`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CommandExecResizeResponse {} + +/// Stream label for `command/exec/outputDelta` notifications. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum CommandExecOutputStream { + /// stdout stream. PTY mode multiplexes terminal output here. + Stdout, + /// stderr stream. + Stderr, +} +/// Base64-encoded output chunk emitted for a streaming `command/exec` request. +/// +/// These notifications are connection-scoped. If the originating connection +/// closes, the server terminates the process. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CommandExecOutputDeltaNotification { + /// Client-supplied, connection-scoped `processId` from the original + /// `command/exec` request. + pub process_id: String, + /// Output stream for this chunk. + pub stream: CommandExecOutputStream, + /// Base64-encoded output bytes. + pub delta_base64: String, + /// `true` on the final streamed chunk for a stream when `outputBytesCap` + /// truncated later output on that stream. + pub cap_reached: bool, +} diff --git a/codex-rs/app-server-protocol/src/protocol/v2/config.rs b/codex-rs/app-server-protocol/src/protocol/v2/config.rs new file mode 100644 index 0000000000..1c1ef35d62 --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -0,0 +1,703 @@ +use super::ApprovalsReviewer; +use super::AskForApproval; +use super::SandboxMode; +use super::shared::default_enabled; +use codex_experimental_api_macros::ExperimentalApi; +use codex_protocol::config_types::ForcedLoginMethod; +use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::config_types::ServiceTier; +use codex_protocol::config_types::Verbosity; +use codex_protocol::config_types::WebSearchMode; +use codex_protocol::config_types::WebSearchToolConfig; +use codex_protocol::openai_models::ReasoningEffort; +use codex_utils_absolute_path::AbsolutePathBuf; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use serde_json::Value as JsonValue; +use std::collections::BTreeMap; +use std::collections::HashMap; +use std::path::PathBuf; +use ts_rs::TS; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type")] +#[ts(export_to = "v2/")] +pub enum ConfigLayerSource { + /// Managed preferences layer delivered by MDM (macOS only). + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + Mdm { + domain: String, + key: String, + }, + + /// Managed config layer from a file (usually `managed_config.toml`). + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + System { + /// This is the path to the system config.toml file, though it is not + /// guaranteed to exist. + file: AbsolutePathBuf, + }, + + /// User config layer from $CODEX_HOME/config.toml. This layer is special + /// in that it is expected to be: + /// - writable by the user + /// - generally outside the workspace directory + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + User { + /// This is the path to the user's config.toml file, though it is not + /// guaranteed to exist. + file: AbsolutePathBuf, + }, + + /// Path to a .codex/ folder within a project. There could be multiple of + /// these between `cwd` and the project/repo root. + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + Project { + dot_codex_folder: AbsolutePathBuf, + }, + + /// Session-layer overrides supplied via `-c`/`--config`. + SessionFlags, + + /// `managed_config.toml` was designed to be a config that was loaded + /// as the last layer on top of everything else. This scheme did not quite + /// work out as intended, but we keep this variant as a "best effort" while + /// we phase out `managed_config.toml` in favor of `requirements.toml`. + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + LegacyManagedConfigTomlFromFile { + file: AbsolutePathBuf, + }, + + LegacyManagedConfigTomlFromMdm, +} + +impl ConfigLayerSource { + /// A settings from a layer with a higher precedence will override a setting + /// from a layer with a lower precedence. + pub fn precedence(&self) -> i16 { + match self { + ConfigLayerSource::Mdm { .. } => 0, + ConfigLayerSource::System { .. } => 10, + ConfigLayerSource::User { .. } => 20, + ConfigLayerSource::Project { .. } => 25, + ConfigLayerSource::SessionFlags => 30, + ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => 40, + ConfigLayerSource::LegacyManagedConfigTomlFromMdm => 50, + } + } +} + +/// Compares [ConfigLayerSource] by precedence, so `A < B` means settings from +/// layer `A` will be overridden by settings from layer `B`. +impl PartialOrd for ConfigLayerSource { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.precedence().cmp(&other.precedence())) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/")] +pub struct SandboxWorkspaceWrite { + #[serde(default)] + pub writable_roots: Vec, + #[serde(default)] + pub network_access: bool, + #[serde(default)] + pub exclude_tmpdir_env_var: bool, + #[serde(default)] + pub exclude_slash_tmp: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/")] +pub struct ToolsV2 { + pub web_search: Option, + pub view_image: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/")] +pub struct ProfileV2 { + pub model: Option, + pub model_provider: Option, + #[experimental(nested)] + pub approval_policy: Option, + /// [UNSTABLE] Optional profile-level override for where approval requests + /// are routed for review. If omitted, the enclosing config default is + /// used. + #[experimental("config/read.approvalsReviewer")] + pub approvals_reviewer: Option, + pub service_tier: Option, + pub model_reasoning_effort: Option, + pub model_reasoning_summary: Option, + pub model_verbosity: Option, + pub web_search: Option, + pub tools: Option, + pub chatgpt_base_url: Option, + #[serde(default, flatten)] + pub additional: HashMap, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/")] +pub struct AnalyticsConfig { + pub enabled: Option, + #[serde(default, flatten)] + pub additional: HashMap, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/")] +pub enum AppToolApproval { + Auto, + Prompt, + Approve, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/")] +pub struct AppsDefaultConfig { + #[serde(default = "default_enabled")] + pub enabled: bool, + #[serde(default = "default_enabled")] + pub destructive_enabled: bool, + #[serde(default = "default_enabled")] + pub open_world_enabled: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/")] +pub struct AppToolConfig { + pub enabled: Option, + pub approval_mode: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/")] +pub struct AppToolsConfig { + #[serde(default, flatten)] + pub tools: HashMap, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/")] +pub struct AppConfig { + #[serde(default = "default_enabled")] + pub enabled: bool, + pub destructive_enabled: Option, + pub open_world_enabled: Option, + pub default_tools_approval_mode: Option, + pub default_tools_enabled: Option, + pub tools: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/")] +pub struct AppsConfig { + #[serde(default, rename = "_default")] + pub default: Option, + #[serde(default, flatten)] + pub apps: HashMap, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/")] +pub struct Config { + pub model: Option, + pub review_model: Option, + pub model_context_window: Option, + pub model_auto_compact_token_limit: Option, + pub model_provider: Option, + #[experimental(nested)] + pub approval_policy: Option, + /// [UNSTABLE] Optional default for where approval requests are routed for + /// review. + #[experimental("config/read.approvalsReviewer")] + pub approvals_reviewer: Option, + pub sandbox_mode: Option, + pub sandbox_workspace_write: Option, + pub forced_chatgpt_workspace_id: Option, + pub forced_login_method: Option, + pub web_search: Option, + pub tools: Option, + pub profile: Option, + #[experimental(nested)] + #[serde(default)] + pub profiles: HashMap, + pub instructions: Option, + pub developer_instructions: Option, + pub compact_prompt: Option, + pub model_reasoning_effort: Option, + pub model_reasoning_summary: Option, + pub model_verbosity: Option, + pub service_tier: Option, + pub analytics: Option, + #[experimental("config/read.apps")] + #[serde(default)] + pub apps: Option, + #[serde(default, flatten)] + pub additional: HashMap, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ConfigLayerMetadata { + pub name: ConfigLayerSource, + pub version: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ConfigLayer { + pub name: ConfigLayerSource, + pub version: String, + pub config: JsonValue, + #[serde(skip_serializing_if = "Option::is_none")] + pub disabled_reason: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum MergeStrategy { + Replace, + Upsert, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum WriteStatus { + Ok, + OkOverridden, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct OverriddenMetadata { + pub message: String, + pub overriding_layer: ConfigLayerMetadata, + pub effective_value: JsonValue, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ConfigWriteResponse { + pub status: WriteStatus, + pub version: String, + /// Canonical path to the config file that was written. + pub file_path: AbsolutePathBuf, + pub overridden_metadata: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum ConfigWriteErrorCode { + ConfigLayerReadonly, + ConfigVersionConflict, + ConfigValidationError, + ConfigPathNotFound, + ConfigSchemaUnknownKey, + UserLayerNotFound, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ConfigReadParams { + #[serde(default)] + pub include_layers: bool, + /// Optional working directory to resolve project config layers. If specified, + /// return the effective config as seen from that directory (i.e., including any + /// project layers between `cwd` and the project/repo root). + #[ts(optional = nullable)] + pub cwd: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ConfigReadResponse { + #[experimental(nested)] + pub config: Config, + pub origins: HashMap, + #[serde(skip_serializing_if = "Option::is_none")] + pub layers: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ConfigRequirements { + #[experimental(nested)] + pub allowed_approval_policies: Option>, + #[experimental("configRequirements/read.allowedApprovalsReviewers")] + pub allowed_approvals_reviewers: Option>, + pub allowed_sandbox_modes: Option>, + pub allowed_web_search_modes: Option>, + pub feature_requirements: Option>, + #[experimental("configRequirements/read.hooks")] + pub hooks: Option, + pub enforce_residency: Option, + #[experimental("configRequirements/read.network")] + pub network: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ManagedHooksRequirements { + pub managed_dir: Option, + pub windows_managed_dir: Option, + #[serde(rename = "PreToolUse")] + #[ts(rename = "PreToolUse")] + pub pre_tool_use: Vec, + #[serde(rename = "PermissionRequest")] + #[ts(rename = "PermissionRequest")] + pub permission_request: Vec, + #[serde(rename = "PostToolUse")] + #[ts(rename = "PostToolUse")] + pub post_tool_use: Vec, + #[serde(rename = "SessionStart")] + #[ts(rename = "SessionStart")] + pub session_start: Vec, + #[serde(rename = "UserPromptSubmit")] + #[ts(rename = "UserPromptSubmit")] + pub user_prompt_submit: Vec, + #[serde(rename = "Stop")] + #[ts(rename = "Stop")] + pub stop: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ConfiguredHookMatcherGroup { + pub matcher: Option, + pub hooks: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(tag = "type")] +#[ts(tag = "type", export_to = "v2/")] +pub enum ConfiguredHookHandler { + #[serde(rename = "command")] + #[ts(rename = "command")] + Command { + command: String, + #[serde(rename = "timeoutSec")] + #[ts(rename = "timeoutSec")] + timeout_sec: Option, + r#async: bool, + #[serde(rename = "statusMessage")] + #[ts(rename = "statusMessage")] + status_message: Option, + }, + #[serde(rename = "prompt")] + #[ts(rename = "prompt")] + Prompt {}, + #[serde(rename = "agent")] + #[ts(rename = "agent")] + Agent {}, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct NetworkRequirements { + pub enabled: Option, + pub http_port: Option, + pub socks_port: Option, + pub allow_upstream_proxy: Option, + pub dangerously_allow_non_loopback_proxy: Option, + pub dangerously_allow_all_unix_sockets: Option, + /// Canonical network permission map for `experimental_network`. + pub domains: Option>, + /// When true, only managed allowlist entries are respected while managed + /// network enforcement is active. + pub managed_allowed_domains_only: Option, + /// Legacy compatibility view derived from `domains`. + pub allowed_domains: Option>, + /// Legacy compatibility view derived from `domains`. + pub denied_domains: Option>, + /// Canonical unix socket permission map for `experimental_network`. + pub unix_sockets: Option>, + /// Legacy compatibility view derived from `unix_sockets`. + pub allow_unix_sockets: Option>, + pub allow_local_binding: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "lowercase")] +#[ts(export_to = "v2/")] +pub enum NetworkDomainPermission { + Allow, + Deny, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "lowercase")] +#[ts(export_to = "v2/")] +pub enum NetworkUnixSocketPermission { + Allow, + None, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum ResidencyRequirement { + Us, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ConfigRequirementsReadResponse { + /// Null if no requirements are configured (e.g. no requirements.toml/MDM entries). + #[experimental(nested)] + pub requirements: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, JsonSchema, TS)] +#[ts(export_to = "v2/")] +pub enum ExternalAgentConfigMigrationItemType { + #[serde(rename = "AGENTS_MD")] + #[ts(rename = "AGENTS_MD")] + AgentsMd, + #[serde(rename = "CONFIG")] + #[ts(rename = "CONFIG")] + Config, + #[serde(rename = "SKILLS")] + #[ts(rename = "SKILLS")] + Skills, + #[serde(rename = "PLUGINS")] + #[ts(rename = "PLUGINS")] + Plugins, + #[serde(rename = "MCP_SERVER_CONFIG")] + #[ts(rename = "MCP_SERVER_CONFIG")] + McpServerConfig, + #[serde(rename = "SUBAGENTS")] + #[ts(rename = "SUBAGENTS")] + Subagents, + #[serde(rename = "HOOKS")] + #[ts(rename = "HOOKS")] + Hooks, + #[serde(rename = "COMMANDS")] + #[ts(rename = "COMMANDS")] + Commands, + #[serde(rename = "SESSIONS")] + #[ts(rename = "SESSIONS")] + Sessions, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PluginsMigration { + #[serde(rename = "marketplaceName")] + #[ts(rename = "marketplaceName")] + pub marketplace_name: String, + #[serde(rename = "pluginNames")] + #[ts(rename = "pluginNames")] + pub plugin_names: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SessionMigration { + pub path: PathBuf, + pub cwd: PathBuf, + pub title: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpServerMigration { + pub name: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct HookMigration { + pub name: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SubagentMigration { + pub name: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CommandMigration { + pub name: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct MigrationDetails { + #[serde(default)] + pub plugins: Vec, + #[serde(default)] + pub sessions: Vec, + #[serde(default)] + pub mcp_servers: Vec, + #[serde(default)] + pub hooks: Vec, + #[serde(default)] + pub subagents: Vec, + #[serde(default)] + pub commands: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ExternalAgentConfigMigrationItem { + pub item_type: ExternalAgentConfigMigrationItemType, + pub description: String, + /// Null or empty means home-scoped migration; non-empty means repo-scoped migration. + pub cwd: Option, + pub details: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ExternalAgentConfigDetectResponse { + pub items: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ExternalAgentConfigDetectParams { + /// If true, include detection under the user's home (~/.claude, ~/.codex, etc.). + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub include_home: bool, + /// Zero or more working directories to include for repo-scoped detection. + #[ts(optional = nullable)] + pub cwds: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ExternalAgentConfigImportParams { + pub migration_items: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ExternalAgentConfigImportResponse {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ExternalAgentConfigImportCompletedNotification {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ConfigValueWriteParams { + pub key_path: String, + pub value: JsonValue, + pub merge_strategy: MergeStrategy, + /// Path to the config file to write; defaults to the user's `config.toml` when omitted. + #[ts(optional = nullable)] + pub file_path: Option, + #[ts(optional = nullable)] + pub expected_version: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ConfigBatchWriteParams { + pub edits: Vec, + /// Path to the config file to write; defaults to the user's `config.toml` when omitted. + #[ts(optional = nullable)] + pub file_path: Option, + #[ts(optional = nullable)] + pub expected_version: Option, + /// When true, hot-reload the updated user config into all loaded threads after writing. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub reload_user_config: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ConfigEdit { + pub key_path: String, + pub value: JsonValue, + pub merge_strategy: MergeStrategy, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct TextPosition { + /// 1-based line number. + pub line: usize, + /// 1-based column number (in Unicode scalar values). + pub column: usize, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct TextRange { + pub start: TextPosition, + pub end: TextPosition, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ConfigWarningNotification { + /// Concise summary of the warning. + pub summary: String, + /// Optional extra guidance or error details. + pub details: Option, + /// Optional path to the config file that triggered the warning. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub path: Option, + /// Optional range for the error location inside the config file. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub range: Option, +} diff --git a/codex-rs/app-server-protocol/src/protocol/v2/device_key.rs b/codex-rs/app-server-protocol/src/protocol/v2/device_key.rs new file mode 100644 index 0000000000..3330996c1c --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/device_key.rs @@ -0,0 +1,181 @@ +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use ts_rs::TS; + +/// Device-key algorithm reported at enrollment and signing boundaries. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(rename_all = "snake_case", export_to = "v2/")] +pub enum DeviceKeyAlgorithm { + EcdsaP256Sha256, +} + +/// Platform protection class for a controller-local device key. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(rename_all = "snake_case", export_to = "v2/")] +pub enum DeviceKeyProtectionClass { + HardwareSecureEnclave, + HardwareTpm, + OsProtectedNonextractable, +} + +/// Protection policy for creating or loading a controller-local device key. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(rename_all = "snake_case", export_to = "v2/")] +pub enum DeviceKeyProtectionPolicy { + HardwareOnly, + AllowOsProtectedNonextractable, +} + +/// Create a controller-local device key with a random key id. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct DeviceKeyCreateParams { + /// Defaults to `hardware_only` when omitted. + #[ts(optional = nullable)] + pub protection_policy: Option, + pub account_user_id: String, + pub client_id: String, +} + +/// Device-key metadata and public key returned by create/public APIs. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct DeviceKeyCreateResponse { + pub key_id: String, + /// SubjectPublicKeyInfo DER encoded as base64. + pub public_key_spki_der_base64: String, + pub algorithm: DeviceKeyAlgorithm, + pub protection_class: DeviceKeyProtectionClass, +} + +/// Fetch a controller-local device key public key by id. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct DeviceKeyPublicParams { + pub key_id: String, +} + +/// Device-key public metadata returned by `device/key/public`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct DeviceKeyPublicResponse { + pub key_id: String, + /// SubjectPublicKeyInfo DER encoded as base64. + pub public_key_spki_der_base64: String, + pub algorithm: DeviceKeyAlgorithm, + pub protection_class: DeviceKeyProtectionClass, +} + +/// Current remote-control connection status and environment id exposed to clients. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct RemoteControlStatusChangedNotification { + pub status: RemoteControlConnectionStatus, + pub environment_id: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase", export_to = "v2/")] +pub enum RemoteControlConnectionStatus { + Disabled, + Connecting, + Connected, + Errored, +} + +/// Audience for a remote-control client connection device-key proof. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(rename_all = "snake_case", export_to = "v2/")] +pub enum RemoteControlClientConnectionAudience { + RemoteControlClientWebsocket, +} + +/// Audience for a remote-control client enrollment device-key proof. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(rename_all = "snake_case", export_to = "v2/")] +pub enum RemoteControlClientEnrollmentAudience { + RemoteControlClientEnrollment, +} + +/// Structured payloads accepted by `device/key/sign`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type", export_to = "v2/")] +pub enum DeviceKeySignPayload { + /// Payload bound to one remote-control controller websocket `/client` connection challenge. + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + RemoteControlClientConnection { + nonce: String, + audience: RemoteControlClientConnectionAudience, + /// Backend-issued websocket session id that this proof authorizes. + session_id: String, + /// Origin of the backend endpoint that issued the challenge and will verify this proof. + target_origin: String, + /// Websocket route path that this proof authorizes. + target_path: String, + account_user_id: String, + client_id: String, + /// Remote-control token expiration as Unix seconds. + #[ts(type = "number")] + token_expires_at: i64, + /// SHA-256 of the controller-scoped remote-control token, encoded as unpadded base64url. + token_sha256_base64url: String, + /// Must contain exactly `remote_control_controller_websocket`. + scopes: Vec, + }, + /// Payload bound to a remote-control client `/client/enroll` ownership challenge. + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + RemoteControlClientEnrollment { + nonce: String, + audience: RemoteControlClientEnrollmentAudience, + /// Backend-issued enrollment challenge id that this proof authorizes. + challenge_id: String, + /// Origin of the backend endpoint that issued the challenge and will verify this proof. + target_origin: String, + /// HTTP route path that this proof authorizes. + target_path: String, + account_user_id: String, + client_id: String, + /// SHA-256 of the requested device identity operation, encoded as unpadded base64url. + device_identity_sha256_base64url: String, + /// Enrollment challenge expiration as Unix seconds. + #[ts(type = "number")] + challenge_expires_at: i64, + }, +} + +/// Sign an accepted structured payload with a controller-local device key. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct DeviceKeySignParams { + pub key_id: String, + pub payload: DeviceKeySignPayload, +} + +/// ASN.1 DER signature returned by `device/key/sign`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct DeviceKeySignResponse { + /// ECDSA signature DER encoded as base64. + pub signature_der_base64: String, + /// Exact bytes signed by the device key, encoded as base64. Verifiers must verify this byte + /// string directly and must not reserialize `payload`. + pub signed_payload_base64: String, + pub algorithm: DeviceKeyAlgorithm, +} diff --git a/codex-rs/app-server-protocol/src/protocol/v2/experimental_feature.rs b/codex-rs/app-server-protocol/src/protocol/v2/experimental_feature.rs new file mode 100644 index 0000000000..6adc21b6ef --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/experimental_feature.rs @@ -0,0 +1,85 @@ +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use std::collections::BTreeMap; +use ts_rs::TS; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ExperimentalFeatureListParams { + /// Opaque pagination cursor returned by a previous call. + #[ts(optional = nullable)] + pub cursor: Option, + /// Optional page size; defaults to a reasonable server-side value. + #[ts(optional = nullable)] + pub limit: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum ExperimentalFeatureStage { + /// Feature is available for user testing and feedback. + Beta, + /// Feature is still being built and not ready for broad use. + UnderDevelopment, + /// Feature is production-ready. + Stable, + /// Feature is deprecated and should be avoided. + Deprecated, + /// Feature flag is retained only for backwards compatibility. + Removed, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ExperimentalFeature { + /// Stable key used in config.toml and CLI flag toggles. + pub name: String, + /// Lifecycle stage of this feature flag. + pub stage: ExperimentalFeatureStage, + /// User-facing display name shown in the experimental features UI. + /// Null when this feature is not in beta. + pub display_name: Option, + /// Short summary describing what the feature does. + /// Null when this feature is not in beta. + pub description: Option, + /// Announcement copy shown to users when the feature is introduced. + /// Null when this feature is not in beta. + pub announcement: Option, + /// Whether this feature is currently enabled in the loaded config. + pub enabled: bool, + /// Whether this feature is enabled by default. + pub default_enabled: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ExperimentalFeatureListResponse { + pub data: 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, Eq, Default, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ExperimentalFeatureEnablementSetParams { + /// Process-wide runtime feature enablement keyed by canonical feature name. + /// + /// Only named features are updated. Omitted features are left unchanged. + /// Send an empty map for a no-op. + pub enablement: BTreeMap, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ExperimentalFeatureEnablementSetResponse { + /// Feature enablement entries updated by this request. + pub enablement: BTreeMap, +} diff --git a/codex-rs/app-server-protocol/src/protocol/v2/feedback.rs b/codex-rs/app-server-protocol/src/protocol/v2/feedback.rs new file mode 100644 index 0000000000..aaf966a4bf --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/feedback.rs @@ -0,0 +1,29 @@ +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use std::collections::BTreeMap; +use std::path::PathBuf; +use ts_rs::TS; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FeedbackUploadParams { + pub classification: String, + #[ts(optional = nullable)] + pub reason: Option, + #[ts(optional = nullable)] + pub thread_id: Option, + pub include_logs: bool, + #[ts(optional = nullable)] + pub extra_log_files: Option>, + #[ts(optional = nullable)] + pub tags: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FeedbackUploadResponse { + pub thread_id: String, +} diff --git a/codex-rs/app-server-protocol/src/protocol/v2/fs.rs b/codex-rs/app-server-protocol/src/protocol/v2/fs.rs new file mode 100644 index 0000000000..0132c6b284 --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/fs.rs @@ -0,0 +1,204 @@ +use codex_utils_absolute_path::AbsolutePathBuf; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use ts_rs::TS; + +/// Read a file from the host filesystem. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsReadFileParams { + /// Absolute path to read. + pub path: AbsolutePathBuf, +} + +/// Base64-encoded file contents returned by `fs/readFile`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsReadFileResponse { + /// File contents encoded as base64. + pub data_base64: String, +} + +/// Write a file on the host filesystem. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsWriteFileParams { + /// Absolute path to write. + pub path: AbsolutePathBuf, + /// File contents encoded as base64. + pub data_base64: String, +} + +/// Successful response for `fs/writeFile`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsWriteFileResponse {} + +/// Create a directory on the host filesystem. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsCreateDirectoryParams { + /// Absolute directory path to create. + pub path: AbsolutePathBuf, + /// Whether parent directories should also be created. Defaults to `true`. + #[ts(optional = nullable)] + pub recursive: Option, +} + +/// Successful response for `fs/createDirectory`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsCreateDirectoryResponse {} + +/// Request metadata for an absolute path. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsGetMetadataParams { + /// Absolute path to inspect. + pub path: AbsolutePathBuf, +} + +/// Metadata returned by `fs/getMetadata`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsGetMetadataResponse { + /// Whether the path resolves to a directory. + pub is_directory: bool, + /// Whether the path resolves to a regular file. + pub is_file: bool, + /// Whether the path itself is a symbolic link. + pub is_symlink: bool, + /// File creation time in Unix milliseconds when available, otherwise `0`. + #[ts(type = "number")] + pub created_at_ms: i64, + /// File modification time in Unix milliseconds when available, otherwise `0`. + #[ts(type = "number")] + pub modified_at_ms: i64, +} + +/// List direct child names for a directory. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsReadDirectoryParams { + /// Absolute directory path to read. + pub path: AbsolutePathBuf, +} + +/// A directory entry returned by `fs/readDirectory`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsReadDirectoryEntry { + /// Direct child entry name only, not an absolute or relative path. + pub file_name: String, + /// Whether this entry resolves to a directory. + pub is_directory: bool, + /// Whether this entry resolves to a regular file. + pub is_file: bool, +} + +/// Directory entries returned by `fs/readDirectory`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsReadDirectoryResponse { + /// Direct child entries in the requested directory. + pub entries: Vec, +} + +/// Remove a file or directory tree from the host filesystem. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsRemoveParams { + /// Absolute path to remove. + pub path: AbsolutePathBuf, + /// Whether directory removal should recurse. Defaults to `true`. + #[ts(optional = nullable)] + pub recursive: Option, + /// Whether missing paths should be ignored. Defaults to `true`. + #[ts(optional = nullable)] + pub force: Option, +} + +/// Successful response for `fs/remove`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsRemoveResponse {} + +/// Copy a file or directory tree on the host filesystem. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsCopyParams { + /// Absolute source path. + pub source_path: AbsolutePathBuf, + /// Absolute destination path. + pub destination_path: AbsolutePathBuf, + /// Required for directory copies; ignored for file copies. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub recursive: bool, +} + +/// Successful response for `fs/copy`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsCopyResponse {} + +/// Start filesystem watch notifications for an absolute path. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsWatchParams { + /// Connection-scoped watch identifier used for `fs/unwatch` and `fs/changed`. + pub watch_id: String, + /// Absolute file or directory path to watch. + pub path: AbsolutePathBuf, +} + +/// Successful response for `fs/watch`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsWatchResponse { + /// Canonicalized path associated with the watch. + pub path: AbsolutePathBuf, +} + +/// Stop filesystem watch notifications for a prior `fs/watch`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsUnwatchParams { + /// Watch identifier previously provided to `fs/watch`. + pub watch_id: String, +} + +/// Successful response for `fs/unwatch`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsUnwatchResponse {} + +/// Filesystem watch notification emitted for `fs/watch` subscribers. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsChangedNotification { + /// Watch identifier previously provided to `fs/watch`. + pub watch_id: String, + /// File or directory paths associated with this event. + pub changed_paths: Vec, +} diff --git a/codex-rs/app-server-protocol/src/protocol/v2/hook.rs b/codex-rs/app-server-protocol/src/protocol/v2/hook.rs new file mode 100644 index 0000000000..e0f74b5ded --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/hook.rs @@ -0,0 +1,154 @@ +use super::shared::v2_enum_from_core; +use codex_protocol::protocol::HookEventName as CoreHookEventName; +use codex_protocol::protocol::HookExecutionMode as CoreHookExecutionMode; +use codex_protocol::protocol::HookHandlerType as CoreHookHandlerType; +use codex_protocol::protocol::HookOutputEntry as CoreHookOutputEntry; +use codex_protocol::protocol::HookOutputEntryKind as CoreHookOutputEntryKind; +use codex_protocol::protocol::HookRunStatus as CoreHookRunStatus; +use codex_protocol::protocol::HookRunSummary as CoreHookRunSummary; +use codex_protocol::protocol::HookScope as CoreHookScope; +use codex_protocol::protocol::HookSource as CoreHookSource; +use codex_protocol::protocol::HookTrustStatus as CoreHookTrustStatus; +use codex_utils_absolute_path::AbsolutePathBuf; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use ts_rs::TS; + +v2_enum_from_core!( + pub enum HookEventName from CoreHookEventName { + PreToolUse, PermissionRequest, PostToolUse, SessionStart, UserPromptSubmit, Stop + } +); + +v2_enum_from_core!( + pub enum HookHandlerType from CoreHookHandlerType { + Command, Prompt, Agent + } +); + +v2_enum_from_core!( + pub enum HookExecutionMode from CoreHookExecutionMode { + Sync, Async + } +); + +v2_enum_from_core!( + pub enum HookScope from CoreHookScope { + Thread, Turn + } +); + +v2_enum_from_core!( + pub enum HookSource from CoreHookSource { + System, + User, + Project, + Mdm, + SessionFlags, + Plugin, + CloudRequirements, + LegacyManagedConfigFile, + LegacyManagedConfigMdm, + Unknown, + } +); + +v2_enum_from_core!( + pub enum HookTrustStatus from CoreHookTrustStatus { + Managed, Untrusted, Trusted, Modified + } +); + +fn default_hook_source() -> HookSource { + HookSource::Unknown +} + +v2_enum_from_core!( + pub enum HookRunStatus from CoreHookRunStatus { + Running, Completed, Failed, Blocked, Stopped + } +); + +v2_enum_from_core!( + pub enum HookOutputEntryKind from CoreHookOutputEntryKind { + Warning, Stop, Feedback, Context, Error + } +); + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct HookOutputEntry { + pub kind: HookOutputEntryKind, + pub text: String, +} + +impl From for HookOutputEntry { + fn from(value: CoreHookOutputEntry) -> Self { + Self { + kind: value.kind.into(), + text: value.text, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct HookRunSummary { + pub id: String, + pub event_name: HookEventName, + pub handler_type: HookHandlerType, + pub execution_mode: HookExecutionMode, + pub scope: HookScope, + pub source_path: AbsolutePathBuf, + #[serde(default = "default_hook_source")] + pub source: HookSource, + pub display_order: i64, + pub status: HookRunStatus, + pub status_message: Option, + pub started_at: i64, + pub completed_at: Option, + pub duration_ms: Option, + pub entries: Vec, +} + +impl From for HookRunSummary { + fn from(value: CoreHookRunSummary) -> Self { + Self { + id: value.id, + event_name: value.event_name.into(), + handler_type: value.handler_type.into(), + execution_mode: value.execution_mode.into(), + scope: value.scope.into(), + source_path: value.source_path, + source: value.source.into(), + display_order: value.display_order, + status: value.status.into(), + status_message: value.status_message, + started_at: value.started_at, + completed_at: value.completed_at, + duration_ms: value.duration_ms, + entries: value.entries.into_iter().map(Into::into).collect(), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct HookStartedNotification { + pub thread_id: String, + pub turn_id: Option, + pub run: HookRunSummary, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct HookCompletedNotification { + pub thread_id: String, + pub turn_id: Option, + pub run: HookRunSummary, +} diff --git a/codex-rs/app-server-protocol/src/protocol/v2/item.rs b/codex-rs/app-server-protocol/src/protocol/v2/item.rs new file mode 100644 index 0000000000..2c3a926c91 --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/item.rs @@ -0,0 +1,1432 @@ +use super::AdditionalPermissionProfile; +use super::ExecPolicyAmendment; +use super::McpToolCallError; +use super::McpToolCallResult; +use super::NetworkApprovalContext; +use super::NetworkApprovalProtocol; +use super::NetworkPolicyAmendment; +use super::RequestPermissionProfile; +use super::UserInput; +use super::shared::v2_enum_from_core; +use crate::protocol::item_builders::convert_patch_changes; +use codex_experimental_api_macros::ExperimentalApi; +use codex_protocol::approvals::GuardianAssessmentAction as CoreGuardianAssessmentAction; +use codex_protocol::approvals::GuardianAssessmentDecisionSource as CoreGuardianAssessmentDecisionSource; +use codex_protocol::approvals::GuardianCommandSource as CoreGuardianCommandSource; +use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent; +use codex_protocol::items::McpToolCallStatus as CoreMcpToolCallStatus; +use codex_protocol::items::TurnItem as CoreTurnItem; +use codex_protocol::memory_citation::MemoryCitation as CoreMemoryCitation; +use codex_protocol::memory_citation::MemoryCitationEntry as CoreMemoryCitationEntry; +use codex_protocol::models::MessagePhase; +use codex_protocol::models::ResponseItem; +use codex_protocol::openai_models::ReasoningEffort; +use codex_protocol::parse_command::ParsedCommand as CoreParsedCommand; +use codex_protocol::protocol::AgentStatus as CoreAgentStatus; +use codex_protocol::protocol::ExecCommandSource as CoreExecCommandSource; +use codex_protocol::protocol::ExecCommandStatus as CoreExecCommandStatus; +use codex_protocol::protocol::GuardianRiskLevel as CoreGuardianRiskLevel; +use codex_protocol::protocol::GuardianUserAuthorization as CoreGuardianUserAuthorization; +use codex_protocol::protocol::PatchApplyStatus as CorePatchApplyStatus; +use codex_protocol::protocol::ReviewDecision as CoreReviewDecision; +use codex_utils_absolute_path::AbsolutePathBuf; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use serde_json::Value as JsonValue; +use serde_with::serde_as; +use std::collections::HashMap; +use std::path::PathBuf; +use ts_rs::TS; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum CommandExecutionApprovalDecision { + /// User approved the command. + Accept, + /// User approved the command and future prompts in the same session-scoped + /// approval cache should run without prompting. + AcceptForSession, + /// User approved the command, and wants to apply the proposed execpolicy amendment so future + /// matching commands can run without prompting. + AcceptWithExecpolicyAmendment { + execpolicy_amendment: ExecPolicyAmendment, + }, + /// User chose a persistent network policy rule (allow/deny) for this host. + ApplyNetworkPolicyAmendment { + network_policy_amendment: NetworkPolicyAmendment, + }, + /// User denied the command. The agent will continue the turn. + Decline, + /// User denied the command. The turn will also be immediately interrupted. + Cancel, +} + +impl From for CommandExecutionApprovalDecision { + fn from(value: CoreReviewDecision) -> Self { + match value { + CoreReviewDecision::Approved => Self::Accept, + CoreReviewDecision::ApprovedExecpolicyAmendment { + proposed_execpolicy_amendment, + } => Self::AcceptWithExecpolicyAmendment { + execpolicy_amendment: proposed_execpolicy_amendment.into(), + }, + CoreReviewDecision::ApprovedForSession => Self::AcceptForSession, + CoreReviewDecision::NetworkPolicyAmendment { + network_policy_amendment, + } => Self::ApplyNetworkPolicyAmendment { + network_policy_amendment: network_policy_amendment.into(), + }, + CoreReviewDecision::Abort => Self::Cancel, + CoreReviewDecision::Denied => Self::Decline, + CoreReviewDecision::TimedOut => Self::Decline, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum FileChangeApprovalDecision { + /// User approved the file changes. + Accept, + /// User approved the file changes and future changes to the same files should run without prompting. + AcceptForSession, + /// User denied the file changes. The agent will continue the turn. + Decline, + /// User denied the file changes. The turn will also be immediately interrupted. + Cancel, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type")] +#[ts(export_to = "v2/")] +pub enum CommandAction { + Read { + command: String, + name: String, + path: AbsolutePathBuf, + }, + ListFiles { + command: String, + path: Option, + }, + Search { + command: String, + query: Option, + path: Option, + }, + Unknown { + command: String, + }, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct MemoryCitation { + pub entries: Vec, + pub thread_ids: Vec, +} + +impl From for MemoryCitation { + fn from(value: CoreMemoryCitation) -> Self { + Self { + entries: value.entries.into_iter().map(Into::into).collect(), + thread_ids: value.rollout_ids, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct MemoryCitationEntry { + pub path: String, + pub line_start: u32, + pub line_end: u32, + pub note: String, +} + +impl From for MemoryCitationEntry { + fn from(value: CoreMemoryCitationEntry) -> Self { + Self { + path: value.path, + line_start: value.line_start, + line_end: value.line_end, + note: value.note, + } + } +} + +impl CommandAction { + pub fn into_core(self) -> CoreParsedCommand { + match self { + CommandAction::Read { + command: cmd, + name, + path, + } => CoreParsedCommand::Read { + cmd, + name, + path: path.into_path_buf(), + }, + CommandAction::ListFiles { command: cmd, path } => { + CoreParsedCommand::ListFiles { cmd, path } + } + CommandAction::Search { + command: cmd, + query, + path, + } => CoreParsedCommand::Search { cmd, query, path }, + CommandAction::Unknown { command: cmd } => CoreParsedCommand::Unknown { cmd }, + } + } + + pub fn from_core_with_cwd(value: CoreParsedCommand, cwd: &AbsolutePathBuf) -> Self { + match value { + CoreParsedCommand::Read { cmd, name, path } => CommandAction::Read { + command: cmd, + name, + path: cwd.join(path), + }, + CoreParsedCommand::ListFiles { cmd, path } => { + CommandAction::ListFiles { command: cmd, path } + } + CoreParsedCommand::Search { cmd, query, path } => CommandAction::Search { + command: cmd, + query, + path, + }, + CoreParsedCommand::Unknown { cmd } => CommandAction::Unknown { command: cmd }, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type")] +#[ts(export_to = "v2/")] +pub enum ThreadItem { + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + UserMessage { id: String, content: Vec }, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + HookPrompt { + id: String, + fragments: Vec, + }, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + AgentMessage { + id: String, + text: String, + #[serde(default)] + phase: Option, + #[serde(default)] + memory_citation: Option, + }, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + /// EXPERIMENTAL - proposed plan item content. The completed plan item is + /// authoritative and may not match the concatenation of `PlanDelta` text. + Plan { id: String, text: String }, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + Reasoning { + id: String, + #[serde(default)] + summary: Vec, + #[serde(default)] + content: Vec, + }, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + CommandExecution { + id: String, + /// The command to be executed. + command: String, + /// The command's working directory. + cwd: AbsolutePathBuf, + /// Identifier for the underlying PTY process (when available). + process_id: Option, + #[serde(default)] + source: CommandExecutionSource, + status: CommandExecutionStatus, + /// A best-effort parsing of the command to understand the action(s) it will perform. + /// This returns a list of CommandAction objects because a single shell command may + /// be composed of many commands piped together. + command_actions: Vec, + /// The command's output, aggregated from stdout and stderr. + aggregated_output: Option, + /// The command's exit code. + exit_code: Option, + /// The duration of the command execution in milliseconds. + #[ts(type = "number | null")] + duration_ms: Option, + }, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + FileChange { + id: String, + changes: Vec, + status: PatchApplyStatus, + }, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + McpToolCall { + id: String, + server: String, + tool: String, + status: McpToolCallStatus, + arguments: JsonValue, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + mcp_app_resource_uri: Option, + result: Option>, + error: Option, + /// The duration of the MCP tool call in milliseconds. + #[ts(type = "number | null")] + duration_ms: Option, + }, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + DynamicToolCall { + id: String, + namespace: Option, + tool: String, + arguments: JsonValue, + status: DynamicToolCallStatus, + content_items: Option>, + success: Option, + /// The duration of the dynamic tool call in milliseconds. + #[ts(type = "number | null")] + duration_ms: Option, + }, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + CollabAgentToolCall { + /// Unique identifier for this collab tool call. + id: String, + /// Name of the collab tool that was invoked. + tool: CollabAgentTool, + /// Current status of the collab tool call. + status: CollabAgentToolCallStatus, + /// Thread ID of the agent issuing the collab request. + sender_thread_id: String, + /// Thread ID of the receiving agent, when applicable. In case of spawn operation, + /// this corresponds to the newly spawned agent. + receiver_thread_ids: Vec, + /// Prompt text sent as part of the collab tool call, when available. + prompt: Option, + /// Model requested for the spawned agent, when applicable. + model: Option, + /// Reasoning effort requested for the spawned agent, when applicable. + reasoning_effort: Option, + /// Last known status of the target agents, when available. + agents_states: HashMap, + }, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + WebSearch { + id: String, + query: String, + action: Option, + }, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + ImageView { id: String, path: AbsolutePathBuf }, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + ImageGeneration { + id: String, + status: String, + revised_prompt: Option, + result: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + saved_path: Option, + }, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + EnteredReviewMode { id: String, review: String }, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + ExitedReviewMode { id: String, review: String }, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + ContextCompaction { id: String }, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase", export_to = "v2/")] +pub struct HookPromptFragment { + pub text: String, + pub hook_run_id: String, +} + +impl ThreadItem { + pub fn id(&self) -> &str { + match self { + ThreadItem::UserMessage { id, .. } + | ThreadItem::HookPrompt { id, .. } + | ThreadItem::AgentMessage { id, .. } + | ThreadItem::Plan { id, .. } + | ThreadItem::Reasoning { id, .. } + | ThreadItem::CommandExecution { id, .. } + | ThreadItem::FileChange { id, .. } + | ThreadItem::McpToolCall { id, .. } + | ThreadItem::DynamicToolCall { id, .. } + | ThreadItem::CollabAgentToolCall { id, .. } + | ThreadItem::WebSearch { id, .. } + | ThreadItem::ImageView { id, .. } + | ThreadItem::ImageGeneration { id, .. } + | ThreadItem::EnteredReviewMode { id, .. } + | ThreadItem::ExitedReviewMode { id, .. } + | ThreadItem::ContextCompaction { id, .. } => id, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +/// [UNSTABLE] Lifecycle state for an approval auto-review. +pub enum GuardianApprovalReviewStatus { + InProgress, + Approved, + Denied, + TimedOut, + Aborted, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +/// [UNSTABLE] Source that produced a terminal approval auto-review decision. +pub enum AutoReviewDecisionSource { + Agent, +} + +impl From for AutoReviewDecisionSource { + fn from(value: CoreGuardianAssessmentDecisionSource) -> Self { + match value { + CoreGuardianAssessmentDecisionSource::Agent => Self::Agent, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "lowercase")] +#[ts(export_to = "v2/")] +/// [UNSTABLE] Risk level assigned by approval auto-review. +pub enum GuardianRiskLevel { + Low, + Medium, + High, + Critical, +} + +impl From for GuardianRiskLevel { + fn from(value: CoreGuardianRiskLevel) -> Self { + match value { + CoreGuardianRiskLevel::Low => Self::Low, + CoreGuardianRiskLevel::Medium => Self::Medium, + CoreGuardianRiskLevel::High => Self::High, + CoreGuardianRiskLevel::Critical => Self::Critical, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "lowercase")] +#[ts(export_to = "v2/")] +/// [UNSTABLE] Authorization level assigned by approval auto-review. +pub enum GuardianUserAuthorization { + Unknown, + Low, + Medium, + High, +} + +impl From for GuardianUserAuthorization { + fn from(value: CoreGuardianUserAuthorization) -> Self { + match value { + CoreGuardianUserAuthorization::Unknown => Self::Unknown, + CoreGuardianUserAuthorization::Low => Self::Low, + CoreGuardianUserAuthorization::Medium => Self::Medium, + CoreGuardianUserAuthorization::High => Self::High, + } + } +} + +/// [UNSTABLE] Temporary approval auto-review payload used by +/// `item/autoApprovalReview/*` notifications. This shape is expected to change +/// soon. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct GuardianApprovalReview { + pub status: GuardianApprovalReviewStatus, + pub risk_level: Option, + pub user_authorization: Option, + pub rationale: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum GuardianCommandSource { + Shell, + UnifiedExec, +} + +impl From for GuardianCommandSource { + fn from(value: CoreGuardianCommandSource) -> Self { + match value { + CoreGuardianCommandSource::Shell => Self::Shell, + CoreGuardianCommandSource::UnifiedExec => Self::UnifiedExec, + } + } +} + +impl From for CoreGuardianCommandSource { + fn from(value: GuardianCommandSource) -> Self { + match value { + GuardianCommandSource::Shell => Self::Shell, + GuardianCommandSource::UnifiedExec => Self::UnifiedExec, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct GuardianCommandReviewAction { + pub source: GuardianCommandSource, + pub command: String, + pub cwd: AbsolutePathBuf, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct GuardianExecveReviewAction { + pub source: GuardianCommandSource, + pub program: String, + pub argv: Vec, + pub cwd: AbsolutePathBuf, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct GuardianApplyPatchReviewAction { + pub cwd: AbsolutePathBuf, + pub files: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct GuardianNetworkAccessReviewAction { + pub target: String, + pub host: String, + pub protocol: NetworkApprovalProtocol, + pub port: u16, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct GuardianMcpToolCallReviewAction { + pub server: String, + pub tool_name: String, + pub connector_id: Option, + pub connector_name: Option, + pub tool_title: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct GuardianRequestPermissionsReviewAction { + pub reason: Option, + pub permissions: RequestPermissionProfile, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type", rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum GuardianApprovalReviewAction { + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + Command { + source: GuardianCommandSource, + command: String, + cwd: AbsolutePathBuf, + }, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + Execve { + source: GuardianCommandSource, + program: String, + argv: Vec, + cwd: AbsolutePathBuf, + }, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + ApplyPatch { + cwd: AbsolutePathBuf, + files: Vec, + }, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + NetworkAccess { + target: String, + host: String, + protocol: NetworkApprovalProtocol, + port: u16, + }, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + McpToolCall { + server: String, + tool_name: String, + connector_id: Option, + connector_name: Option, + tool_title: Option, + }, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + RequestPermissions { + reason: Option, + permissions: RequestPermissionProfile, + }, +} + +impl From for GuardianApprovalReviewAction { + fn from(value: CoreGuardianAssessmentAction) -> Self { + match value { + CoreGuardianAssessmentAction::Command { + source, + command, + cwd, + } => Self::Command { + source: source.into(), + command, + cwd, + }, + CoreGuardianAssessmentAction::Execve { + source, + program, + argv, + cwd, + } => Self::Execve { + source: source.into(), + program, + argv, + cwd, + }, + CoreGuardianAssessmentAction::ApplyPatch { cwd, files } => { + Self::ApplyPatch { cwd, files } + } + CoreGuardianAssessmentAction::NetworkAccess { + target, + host, + protocol, + port, + } => Self::NetworkAccess { + target, + host, + protocol: protocol.into(), + port, + }, + CoreGuardianAssessmentAction::McpToolCall { + server, + tool_name, + connector_id, + connector_name, + tool_title, + } => Self::McpToolCall { + server, + tool_name, + connector_id, + connector_name, + tool_title, + }, + CoreGuardianAssessmentAction::RequestPermissions { + reason, + permissions, + } => Self::RequestPermissions { + reason, + permissions: permissions.into(), + }, + } + } +} + +impl From for CoreGuardianAssessmentAction { + fn from(value: GuardianApprovalReviewAction) -> Self { + match value { + GuardianApprovalReviewAction::Command { + source, + command, + cwd, + } => Self::Command { + source: source.into(), + command, + cwd, + }, + GuardianApprovalReviewAction::Execve { + source, + program, + argv, + cwd, + } => Self::Execve { + source: source.into(), + program, + argv, + cwd, + }, + GuardianApprovalReviewAction::ApplyPatch { cwd, files } => { + Self::ApplyPatch { cwd, files } + } + GuardianApprovalReviewAction::NetworkAccess { + target, + host, + protocol, + port, + } => Self::NetworkAccess { + target, + host, + protocol: protocol.to_core(), + port, + }, + GuardianApprovalReviewAction::McpToolCall { + server, + tool_name, + connector_id, + connector_name, + tool_title, + } => Self::McpToolCall { + server, + tool_name, + connector_id, + connector_name, + tool_title, + }, + GuardianApprovalReviewAction::RequestPermissions { + reason, + permissions, + } => Self::RequestPermissions { + reason, + permissions: permissions.into(), + }, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type", rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum WebSearchAction { + Search { + query: Option, + queries: Option>, + }, + OpenPage { + url: Option, + }, + FindInPage { + url: Option, + pattern: Option, + }, + #[serde(other)] + Other, +} + +impl From for WebSearchAction { + fn from(value: codex_protocol::models::WebSearchAction) -> Self { + match value { + codex_protocol::models::WebSearchAction::Search { query, queries } => { + WebSearchAction::Search { query, queries } + } + codex_protocol::models::WebSearchAction::OpenPage { url } => { + WebSearchAction::OpenPage { url } + } + codex_protocol::models::WebSearchAction::FindInPage { url, pattern } => { + WebSearchAction::FindInPage { url, pattern } + } + codex_protocol::models::WebSearchAction::Other => WebSearchAction::Other, + } + } +} + +impl From for ThreadItem { + fn from(value: CoreTurnItem) -> Self { + match value { + CoreTurnItem::UserMessage(user) => ThreadItem::UserMessage { + id: user.id, + content: user.content.into_iter().map(UserInput::from).collect(), + }, + CoreTurnItem::HookPrompt(hook_prompt) => ThreadItem::HookPrompt { + id: hook_prompt.id, + fragments: hook_prompt + .fragments + .into_iter() + .map(HookPromptFragment::from) + .collect(), + }, + CoreTurnItem::AgentMessage(agent) => { + let text = agent + .content + .into_iter() + .map(|entry| match entry { + CoreAgentMessageContent::Text { text } => text, + }) + .collect::(); + ThreadItem::AgentMessage { + id: agent.id, + text, + phase: agent.phase, + memory_citation: agent.memory_citation.map(Into::into), + } + } + CoreTurnItem::Plan(plan) => ThreadItem::Plan { + id: plan.id, + text: plan.text, + }, + CoreTurnItem::Reasoning(reasoning) => ThreadItem::Reasoning { + id: reasoning.id, + summary: reasoning.summary_text, + content: reasoning.raw_content, + }, + CoreTurnItem::WebSearch(search) => ThreadItem::WebSearch { + id: search.id, + query: search.query, + action: Some(WebSearchAction::from(search.action)), + }, + CoreTurnItem::ImageView(image) => ThreadItem::ImageView { + id: image.id, + path: image.path, + }, + CoreTurnItem::ImageGeneration(image) => ThreadItem::ImageGeneration { + id: image.id, + status: image.status, + revised_prompt: image.revised_prompt, + result: image.result, + saved_path: image.saved_path, + }, + CoreTurnItem::FileChange(file_change) => ThreadItem::FileChange { + id: file_change.id, + changes: convert_patch_changes(&file_change.changes), + status: file_change + .status + .as_ref() + .map(PatchApplyStatus::from) + .unwrap_or(PatchApplyStatus::InProgress), + }, + CoreTurnItem::McpToolCall(mcp) => { + let duration_ms = mcp + .duration + .and_then(|duration| i64::try_from(duration.as_millis()).ok()); + + ThreadItem::McpToolCall { + id: mcp.id, + server: mcp.server, + tool: mcp.tool, + status: McpToolCallStatus::from(mcp.status), + arguments: mcp.arguments, + mcp_app_resource_uri: mcp.mcp_app_resource_uri, + result: mcp.result.map(McpToolCallResult::from).map(Box::new), + error: mcp.error.map(McpToolCallError::from), + duration_ms, + } + } + CoreTurnItem::ContextCompaction(compaction) => { + ThreadItem::ContextCompaction { id: compaction.id } + } + } + } +} + +impl From for HookPromptFragment { + fn from(value: codex_protocol::items::HookPromptFragment) -> Self { + Self { + text: value.text, + hook_run_id: value.hook_run_id, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum CommandExecutionStatus { + InProgress, + Completed, + Failed, + Declined, +} + +impl From for CommandExecutionStatus { + fn from(value: CoreExecCommandStatus) -> Self { + Self::from(&value) + } +} + +impl From<&CoreExecCommandStatus> for CommandExecutionStatus { + fn from(value: &CoreExecCommandStatus) -> Self { + match value { + CoreExecCommandStatus::Completed => CommandExecutionStatus::Completed, + CoreExecCommandStatus::Failed => CommandExecutionStatus::Failed, + CoreExecCommandStatus::Declined => CommandExecutionStatus::Declined, + } + } +} + +v2_enum_from_core! { + #[derive(Default)] + pub enum CommandExecutionSource from CoreExecCommandSource { + #[default] + Agent, + UserShell, + UnifiedExecStartup, + UnifiedExecInteraction, + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum CollabAgentTool { + SpawnAgent, + SendInput, + ResumeAgent, + Wait, + CloseAgent, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FileUpdateChange { + pub path: String, + pub kind: PatchChangeKind, + pub diff: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type")] +#[ts(export_to = "v2/")] +pub enum PatchChangeKind { + Add, + Delete, + Update { move_path: Option }, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum PatchApplyStatus { + InProgress, + Completed, + Failed, + Declined, +} + +impl From for PatchApplyStatus { + fn from(value: CorePatchApplyStatus) -> Self { + Self::from(&value) + } +} + +impl From<&CorePatchApplyStatus> for PatchApplyStatus { + fn from(value: &CorePatchApplyStatus) -> Self { + match value { + CorePatchApplyStatus::Completed => PatchApplyStatus::Completed, + CorePatchApplyStatus::Failed => PatchApplyStatus::Failed, + CorePatchApplyStatus::Declined => PatchApplyStatus::Declined, + } + } +} + +impl From for McpToolCallStatus { + fn from(value: CoreMcpToolCallStatus) -> Self { + match value { + CoreMcpToolCallStatus::InProgress => McpToolCallStatus::InProgress, + CoreMcpToolCallStatus::Completed => McpToolCallStatus::Completed, + CoreMcpToolCallStatus::Failed => McpToolCallStatus::Failed, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum McpToolCallStatus { + InProgress, + Completed, + Failed, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum DynamicToolCallStatus { + InProgress, + Completed, + Failed, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum CollabAgentToolCallStatus { + InProgress, + Completed, + Failed, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum CollabAgentStatus { + PendingInit, + Running, + Interrupted, + Completed, + Errored, + Shutdown, + NotFound, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CollabAgentState { + pub status: CollabAgentStatus, + pub message: Option, +} + +impl From for CollabAgentState { + fn from(value: CoreAgentStatus) -> Self { + match value { + CoreAgentStatus::PendingInit => Self { + status: CollabAgentStatus::PendingInit, + message: None, + }, + CoreAgentStatus::Running => Self { + status: CollabAgentStatus::Running, + message: None, + }, + CoreAgentStatus::Interrupted => Self { + status: CollabAgentStatus::Interrupted, + message: None, + }, + CoreAgentStatus::Completed(message) => Self { + status: CollabAgentStatus::Completed, + message, + }, + CoreAgentStatus::Errored(message) => Self { + status: CollabAgentStatus::Errored, + message: Some(message), + }, + CoreAgentStatus::Shutdown => Self { + status: CollabAgentStatus::Shutdown, + message: None, + }, + CoreAgentStatus::NotFound => Self { + status: CollabAgentStatus::NotFound, + message: None, + }, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ItemStartedNotification { + pub item: ThreadItem, + pub thread_id: String, + pub turn_id: String, + /// Unix timestamp (in milliseconds) when this item lifecycle started. + #[ts(type = "number")] + pub started_at_ms: i64, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +/// [UNSTABLE] Temporary notification payload for approval auto-review. This +/// shape is expected to change soon. +pub struct ItemGuardianApprovalReviewStartedNotification { + pub thread_id: String, + pub turn_id: String, + /// Stable identifier for this review. + pub review_id: String, + /// Identifier for the reviewed item or tool call when one exists. + /// + /// In most cases, one review maps to one target item. The exceptions are + /// - execve reviews, where a single command may contain multiple execve + /// calls to review (only possible when using the shell_zsh_fork feature) + /// - network policy reviews, where there is no target item + /// + /// A network call is triggered by a CommandExecution item, so having a + /// target_item_id set to the CommandExecution item would be misleading + /// because the review is about the network call, not the command execution. + /// Therefore, target_item_id is set to None for network policy reviews. + pub target_item_id: Option, + pub review: GuardianApprovalReview, + pub action: GuardianApprovalReviewAction, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +/// [UNSTABLE] Temporary notification payload for approval auto-review. This +/// shape is expected to change soon. +pub struct ItemGuardianApprovalReviewCompletedNotification { + pub thread_id: String, + pub turn_id: String, + /// Stable identifier for this review. + pub review_id: String, + /// Identifier for the reviewed item or tool call when one exists. + /// + /// In most cases, one review maps to one target item. The exceptions are + /// - execve reviews, where a single command may contain multiple execve + /// calls to review (only possible when using the shell_zsh_fork feature) + /// - network policy reviews, where there is no target item + /// + /// A network call is triggered by a CommandExecution item, so having a + /// target_item_id set to the CommandExecution item would be misleading + /// because the review is about the network call, not the command execution. + /// Therefore, target_item_id is set to None for network policy reviews. + pub target_item_id: Option, + pub decision_source: AutoReviewDecisionSource, + pub review: GuardianApprovalReview, + pub action: GuardianApprovalReviewAction, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ItemCompletedNotification { + pub item: ThreadItem, + pub thread_id: String, + pub turn_id: String, + /// Unix timestamp (in milliseconds) when this item lifecycle completed. + #[ts(type = "number")] + pub completed_at_ms: i64, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct RawResponseItemCompletedNotification { + pub thread_id: String, + pub turn_id: String, + pub item: ResponseItem, +} + +// Item-specific progress notifications +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AgentMessageDeltaNotification { + pub thread_id: String, + pub turn_id: String, + pub item_id: String, + pub delta: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +/// EXPERIMENTAL - proposed plan streaming deltas for plan items. Clients should +/// not assume concatenated deltas match the completed plan item content. +pub struct PlanDeltaNotification { + pub thread_id: String, + pub turn_id: String, + pub item_id: String, + pub delta: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ReasoningSummaryTextDeltaNotification { + pub thread_id: String, + pub turn_id: String, + pub item_id: String, + pub delta: String, + #[ts(type = "number")] + pub summary_index: i64, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ReasoningSummaryPartAddedNotification { + pub thread_id: String, + pub turn_id: String, + pub item_id: String, + #[ts(type = "number")] + pub summary_index: i64, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ReasoningTextDeltaNotification { + pub thread_id: String, + pub turn_id: String, + pub item_id: String, + pub delta: String, + #[ts(type = "number")] + pub content_index: i64, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct TerminalInteractionNotification { + pub thread_id: String, + pub turn_id: String, + pub item_id: String, + pub process_id: String, + pub stdin: String, +} + +#[serde_as] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CommandExecutionOutputDeltaNotification { + pub thread_id: String, + pub turn_id: String, + pub item_id: String, + pub delta: String, +} +/// Deprecated legacy notification for `apply_patch` textual output. +/// +/// The server no longer emits this notification. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FileChangeOutputDeltaNotification { + pub thread_id: String, + pub turn_id: String, + pub item_id: String, + pub delta: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FileChangePatchUpdatedNotification { + pub thread_id: String, + pub turn_id: String, + pub item_id: String, + pub changes: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CommandExecutionRequestApprovalParams { + pub thread_id: String, + pub turn_id: String, + pub item_id: String, + /// Unique identifier for this specific approval callback. + /// + /// For regular shell/unified_exec approvals, this is null. + /// + /// For zsh-exec-bridge subcommand approvals, multiple callbacks can belong to + /// one parent `itemId`, so `approvalId` is a distinct opaque callback id + /// (a UUID) used to disambiguate routing. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub approval_id: Option, + /// Optional explanatory reason (e.g. request for network access). + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub reason: Option, + /// Optional context for a managed-network approval prompt. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub network_approval_context: Option, + /// The command to be executed. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub command: Option, + /// The command's working directory. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub cwd: Option, + /// Best-effort parsed command actions for friendly display. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub command_actions: Option>, + /// Optional additional permissions requested for this command. + #[experimental("item/commandExecution/requestApproval.additionalPermissions")] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub additional_permissions: Option, + /// Optional proposed execpolicy amendment to allow similar commands without prompting. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub proposed_execpolicy_amendment: Option, + /// Optional proposed network policy amendments (allow/deny host) for future requests. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub proposed_network_policy_amendments: Option>, + /// Ordered list of decisions the client may present for this prompt. + #[experimental("item/commandExecution/requestApproval.availableDecisions")] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub available_decisions: Option>, +} + +impl CommandExecutionRequestApprovalParams { + pub fn strip_experimental_fields(&mut self) { + // TODO: Avoid hardcoding individual experimental fields here. + // We need a generic outbound compatibility design for stripping or + // otherwise handling experimental server->client payloads. + self.additional_permissions = None; + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CommandExecutionRequestApprovalResponse { + pub decision: CommandExecutionApprovalDecision, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FileChangeRequestApprovalParams { + pub thread_id: String, + pub turn_id: String, + pub item_id: String, + /// Optional explanatory reason (e.g. request for extra write access). + #[ts(optional = nullable)] + pub reason: Option, + /// [UNSTABLE] 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). + #[ts(optional = nullable)] + pub grant_root: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[ts(export_to = "v2/")] +pub struct FileChangeRequestApprovalResponse { + pub decision: FileChangeApprovalDecision, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct DynamicToolCallParams { + pub thread_id: String, + pub turn_id: String, + pub call_id: String, + pub namespace: Option, + pub tool: String, + pub arguments: JsonValue, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct DynamicToolCallResponse { + pub content_items: Vec, + pub success: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type")] +#[ts(export_to = "v2/")] +pub enum DynamicToolCallOutputContentItem { + #[serde(rename_all = "camelCase")] + InputText { text: String }, + #[serde(rename_all = "camelCase")] + InputImage { image_url: String }, +} + +impl From + for codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem +{ + fn from(item: DynamicToolCallOutputContentItem) -> Self { + match item { + DynamicToolCallOutputContentItem::InputText { text } => Self::InputText { text }, + DynamicToolCallOutputContentItem::InputImage { image_url } => { + Self::InputImage { image_url } + } + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +/// EXPERIMENTAL. Defines a single selectable option for request_user_input. +pub struct ToolRequestUserInputOption { + pub label: String, + pub description: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +/// EXPERIMENTAL. Represents one request_user_input question and its required options. +pub struct ToolRequestUserInputQuestion { + pub id: String, + pub header: String, + pub question: String, + #[serde(default)] + pub is_other: bool, + #[serde(default)] + pub is_secret: bool, + pub options: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +/// EXPERIMENTAL. Params sent with a request_user_input event. +pub struct ToolRequestUserInputParams { + pub thread_id: String, + pub turn_id: String, + pub item_id: String, + pub questions: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +/// EXPERIMENTAL. Captures a user's answer to a request_user_input question. +pub struct ToolRequestUserInputAnswer { + pub answers: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +/// EXPERIMENTAL. Response payload mapping question ids to answers. +pub struct ToolRequestUserInputResponse { + pub answers: HashMap, +} diff --git a/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs b/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs new file mode 100644 index 0000000000..9fd9384076 --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs @@ -0,0 +1,703 @@ +use super::shared::v2_enum_from_core; +use codex_protocol::approvals::ElicitationRequest as CoreElicitationRequest; +use codex_protocol::items::McpToolCallError as CoreMcpToolCallError; +use codex_protocol::mcp::CallToolResult as CoreMcpCallToolResult; +use codex_protocol::mcp::Resource as McpResource; +pub use codex_protocol::mcp::ResourceContent as McpResourceContent; +use codex_protocol::mcp::ResourceTemplate as McpResourceTemplate; +use codex_protocol::mcp::Tool as McpTool; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use serde_json::Value as JsonValue; +use std::collections::BTreeMap; +use ts_rs::TS; + +v2_enum_from_core!( + pub enum McpAuthStatus from codex_protocol::protocol::McpAuthStatus { + Unsupported, + NotLoggedIn, + BearerToken, + OAuth + } +); + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ListMcpServerStatusParams { + /// Opaque pagination cursor returned by a previous call. + #[ts(optional = nullable)] + pub cursor: Option, + /// Optional page size; defaults to a server-defined value. + #[ts(optional = nullable)] + pub limit: Option, + /// Controls how much MCP inventory data to fetch for each server. + /// Defaults to `Full` when omitted. + #[ts(optional = nullable)] + pub detail: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase", export_to = "v2/")] +pub enum McpServerStatusDetail { + Full, + ToolsAndAuthOnly, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpServerStatus { + pub name: String, + pub tools: std::collections::HashMap, + pub resources: Vec, + pub resource_templates: Vec, + pub auth_status: McpAuthStatus, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ListMcpServerStatusResponse { + pub data: 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")] +#[ts(export_to = "v2/")] +pub struct McpResourceReadParams { + #[ts(optional = nullable)] + pub thread_id: Option, + pub server: String, + pub uri: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpResourceReadResponse { + pub contents: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpServerToolCallParams { + pub thread_id: String, + pub server: String, + pub tool: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub arguments: Option, + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub meta: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpServerToolCallResponse { + pub content: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub structured_content: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub is_error: Option, + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub meta: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpToolCallResult { + // NOTE: `rmcp::model::Content` (and its `RawContent` variants) would be a more precise Rust + // representation of MCP content blocks. We intentionally use `serde_json::Value` here because + // this crate exports JSON schema + TS types (`schemars`/`ts-rs`), and the rmcp model types + // aren't set up to be schema/TS friendly (and would introduce heavier coupling to rmcp's Rust + // representations). Using `JsonValue` keeps the payload wire-shaped and easy to export. + pub content: Vec, + pub structured_content: Option, + #[serde(rename = "_meta")] + #[ts(rename = "_meta")] + pub meta: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpToolCallError { + pub message: String, +} + +impl From for McpServerToolCallResponse { + fn from(result: CoreMcpCallToolResult) -> Self { + Self { + content: result.content, + structured_content: result.structured_content, + is_error: result.is_error, + meta: result.meta, + } + } +} + +impl From for McpToolCallResult { + fn from(result: CoreMcpCallToolResult) -> Self { + Self { + content: result.content, + structured_content: result.structured_content, + meta: result.meta, + } + } +} + +impl From for McpToolCallError { + fn from(error: CoreMcpToolCallError) -> Self { + Self { + message: error.message, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpServerRefreshParams {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpServerRefreshResponse {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpServerOauthLoginParams { + pub name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub scopes: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub timeout_secs: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpServerOauthLoginResponse { + pub authorization_url: String, +} +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpToolCallProgressNotification { + pub thread_id: String, + pub turn_id: String, + pub item_id: String, + pub message: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpServerOauthLoginCompletedNotification { + pub name: String, + pub success: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub error: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum McpServerStartupState { + Starting, + Ready, + Failed, + Cancelled, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpServerStatusUpdatedNotification { + pub name: String, + pub status: McpServerStartupState, + pub error: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum McpServerElicitationAction { + Accept, + Decline, + Cancel, +} + +impl McpServerElicitationAction { + pub fn to_core(self) -> codex_protocol::approvals::ElicitationAction { + match self { + Self::Accept => codex_protocol::approvals::ElicitationAction::Accept, + Self::Decline => codex_protocol::approvals::ElicitationAction::Decline, + Self::Cancel => codex_protocol::approvals::ElicitationAction::Cancel, + } + } +} + +impl From for rmcp::model::ElicitationAction { + fn from(value: McpServerElicitationAction) -> Self { + match value { + McpServerElicitationAction::Accept => Self::Accept, + McpServerElicitationAction::Decline => Self::Decline, + McpServerElicitationAction::Cancel => Self::Cancel, + } + } +} + +impl From for McpServerElicitationAction { + fn from(value: rmcp::model::ElicitationAction) -> Self { + match value { + rmcp::model::ElicitationAction::Accept => Self::Accept, + rmcp::model::ElicitationAction::Decline => Self::Decline, + rmcp::model::ElicitationAction::Cancel => Self::Cancel, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpServerElicitationRequestParams { + pub thread_id: String, + /// Active Codex turn when this elicitation was observed, if app-server could correlate one. + /// + /// This is nullable because MCP models elicitation as a standalone server-to-client request + /// identified by the MCP server request id. It may be triggered during a turn, but turn + /// context is app-server correlation rather than part of the protocol identity of the + /// elicitation itself. + pub turn_id: Option, + pub server_name: String, + #[serde(flatten)] + pub request: McpServerElicitationRequest, + // TODO: When core can correlate an elicitation with an MCP tool call, expose the associated + // McpToolCall item id here as an optional field. The current core event does not carry that + // association. +} + +/// Typed form schema for MCP `elicitation/create` requests. +/// +/// This matches the `requestedSchema` shape from the MCP 2025-11-25 +/// `ElicitRequestFormParams` schema. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[ts(export_to = "v2/")] +pub struct McpElicitationSchema { + #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")] + #[ts(optional, rename = "$schema")] + pub schema_uri: Option, + #[serde(rename = "type")] + #[ts(rename = "type")] + pub type_: McpElicitationObjectType, + pub properties: BTreeMap, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub required: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "lowercase")] +#[ts(export_to = "v2/")] +pub enum McpElicitationObjectType { + Object, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(untagged)] +#[ts(export_to = "v2/")] +pub enum McpElicitationPrimitiveSchema { + Enum(McpElicitationEnumSchema), + String(McpElicitationStringSchema), + Number(McpElicitationNumberSchema), + Boolean(McpElicitationBooleanSchema), +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[ts(export_to = "v2/")] +pub struct McpElicitationStringSchema { + #[serde(rename = "type")] + #[ts(rename = "type")] + pub type_: McpElicitationStringType, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub min_length: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub max_length: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub format: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub default: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "lowercase")] +#[ts(export_to = "v2/")] +pub enum McpElicitationStringType { + String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "kebab-case")] +#[ts(rename_all = "kebab-case", export_to = "v2/")] +pub enum McpElicitationStringFormat { + Email, + Uri, + Date, + DateTime, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[ts(export_to = "v2/")] +pub struct McpElicitationNumberSchema { + #[serde(rename = "type")] + #[ts(rename = "type")] + pub type_: McpElicitationNumberType, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub minimum: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub maximum: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub default: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "lowercase")] +#[ts(export_to = "v2/")] +pub enum McpElicitationNumberType { + Number, + Integer, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[ts(export_to = "v2/")] +pub struct McpElicitationBooleanSchema { + #[serde(rename = "type")] + #[ts(rename = "type")] + pub type_: McpElicitationBooleanType, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub default: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "lowercase")] +#[ts(export_to = "v2/")] +pub enum McpElicitationBooleanType { + Boolean, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(untagged)] +#[ts(export_to = "v2/")] +pub enum McpElicitationEnumSchema { + SingleSelect(McpElicitationSingleSelectEnumSchema), + MultiSelect(McpElicitationMultiSelectEnumSchema), + Legacy(McpElicitationLegacyTitledEnumSchema), +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[ts(export_to = "v2/")] +pub struct McpElicitationLegacyTitledEnumSchema { + #[serde(rename = "type")] + #[ts(rename = "type")] + pub type_: McpElicitationStringType, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub description: Option, + #[serde(rename = "enum")] + #[ts(rename = "enum")] + pub enum_: Vec, + #[serde(rename = "enumNames", skip_serializing_if = "Option::is_none")] + #[ts(optional, rename = "enumNames")] + pub enum_names: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub default: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(untagged)] +#[ts(export_to = "v2/")] +pub enum McpElicitationSingleSelectEnumSchema { + Untitled(McpElicitationUntitledSingleSelectEnumSchema), + Titled(McpElicitationTitledSingleSelectEnumSchema), +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[ts(export_to = "v2/")] +pub struct McpElicitationUntitledSingleSelectEnumSchema { + #[serde(rename = "type")] + #[ts(rename = "type")] + pub type_: McpElicitationStringType, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub description: Option, + #[serde(rename = "enum")] + #[ts(rename = "enum")] + pub enum_: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub default: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[ts(export_to = "v2/")] +pub struct McpElicitationTitledSingleSelectEnumSchema { + #[serde(rename = "type")] + #[ts(rename = "type")] + pub type_: McpElicitationStringType, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub description: Option, + #[serde(rename = "oneOf")] + #[ts(rename = "oneOf")] + pub one_of: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub default: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(untagged)] +#[ts(export_to = "v2/")] +pub enum McpElicitationMultiSelectEnumSchema { + Untitled(McpElicitationUntitledMultiSelectEnumSchema), + Titled(McpElicitationTitledMultiSelectEnumSchema), +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[ts(export_to = "v2/")] +pub struct McpElicitationUntitledMultiSelectEnumSchema { + #[serde(rename = "type")] + #[ts(rename = "type")] + pub type_: McpElicitationArrayType, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub min_items: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub max_items: Option, + pub items: McpElicitationUntitledEnumItems, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub default: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[ts(export_to = "v2/")] +pub struct McpElicitationTitledMultiSelectEnumSchema { + #[serde(rename = "type")] + #[ts(rename = "type")] + pub type_: McpElicitationArrayType, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub min_items: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub max_items: Option, + pub items: McpElicitationTitledEnumItems, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub default: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "lowercase")] +#[ts(export_to = "v2/")] +pub enum McpElicitationArrayType { + Array, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(deny_unknown_fields)] +#[ts(export_to = "v2/")] +pub struct McpElicitationUntitledEnumItems { + #[serde(rename = "type")] + #[ts(rename = "type")] + pub type_: McpElicitationStringType, + #[serde(rename = "enum")] + #[ts(rename = "enum")] + pub enum_: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(deny_unknown_fields)] +#[ts(export_to = "v2/")] +pub struct McpElicitationTitledEnumItems { + #[serde(rename = "anyOf", alias = "oneOf")] + #[ts(rename = "anyOf")] + pub any_of: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(deny_unknown_fields)] +#[ts(export_to = "v2/")] +pub struct McpElicitationConstOption { + #[serde(rename = "const")] + #[ts(rename = "const")] + pub const_: String, + pub title: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(tag = "mode", rename_all = "camelCase")] +#[ts(tag = "mode")] +#[ts(export_to = "v2/")] +pub enum McpServerElicitationRequest { + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + Form { + #[serde(rename = "_meta")] + #[ts(rename = "_meta")] + meta: Option, + message: String, + requested_schema: McpElicitationSchema, + }, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + Url { + #[serde(rename = "_meta")] + #[ts(rename = "_meta")] + meta: Option, + message: String, + url: String, + elicitation_id: String, + }, +} + +impl TryFrom for McpServerElicitationRequest { + type Error = serde_json::Error; + + fn try_from(value: CoreElicitationRequest) -> Result { + match value { + CoreElicitationRequest::Form { + meta, + message, + requested_schema, + } => Ok(Self::Form { + meta, + message, + requested_schema: serde_json::from_value(requested_schema)?, + }), + CoreElicitationRequest::Url { + meta, + message, + url, + elicitation_id, + } => Ok(Self::Url { + meta, + message, + url, + elicitation_id, + }), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpServerElicitationRequestResponse { + pub action: McpServerElicitationAction, + /// Structured user input for accepted elicitations, mirroring RMCP `CreateElicitationResult`. + /// + /// This is nullable because decline/cancel responses have no content. + pub content: Option, + /// Optional client metadata for form-mode action handling. + #[serde(rename = "_meta")] + #[ts(rename = "_meta")] + pub meta: Option, +} + +impl From for rmcp::model::CreateElicitationResult { + fn from(value: McpServerElicitationRequestResponse) -> Self { + Self { + action: value.action.into(), + content: value.content, + } + } +} + +impl From for McpServerElicitationRequestResponse { + fn from(value: rmcp::model::CreateElicitationResult) -> Self { + Self { + action: value.action.into(), + content: value.content, + meta: None, + } + } +} diff --git a/codex-rs/app-server-protocol/src/protocol/v2/mod.rs b/codex-rs/app-server-protocol/src/protocol/v2/mod.rs new file mode 100644 index 0000000000..df8a363f82 --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/mod.rs @@ -0,0 +1,53 @@ +mod shared; + +mod account; +mod apps; +mod collaboration_mode; +mod command_exec; +mod config; +mod device_key; +mod experimental_feature; +mod feedback; +mod fs; +mod hook; +mod item; +mod mcp; +mod model; +mod notification; +mod permissions; +mod plugin; +mod process; +mod realtime; +mod review; +mod thread; +mod thread_data; +mod turn; +mod windows_sandbox; + +pub use account::*; +pub use apps::*; +pub use collaboration_mode::*; +pub use command_exec::*; +pub use config::*; +pub use device_key::*; +pub use experimental_feature::*; +pub use feedback::*; +pub use fs::*; +pub use hook::*; +pub use item::*; +pub use mcp::*; +pub use model::*; +pub use notification::*; +pub use permissions::*; +pub use plugin::*; +pub use process::*; +pub use realtime::*; +pub use review::*; +pub use shared::*; +pub use thread::*; +pub use thread_data::*; +pub use turn::*; +pub use windows_sandbox::*; + +#[cfg(test)] +mod tests; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/model.rs b/codex-rs/app-server-protocol/src/protocol/v2/model.rs new file mode 100644 index 0000000000..cd139e9c4b --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/model.rs @@ -0,0 +1,151 @@ +use super::shared::v2_enum_from_core; +use codex_protocol::openai_models::InputModality; +use codex_protocol::openai_models::ModelAvailabilityNux as CoreModelAvailabilityNux; +use codex_protocol::openai_models::ReasoningEffort; +use codex_protocol::openai_models::default_input_modalities; +use codex_protocol::protocol::ModelRerouteReason as CoreModelRerouteReason; +use codex_protocol::protocol::ModelVerification as CoreModelVerification; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use ts_rs::TS; + +v2_enum_from_core!( + pub enum ModelRerouteReason from CoreModelRerouteReason { + HighRiskCyberActivity + } +); + +v2_enum_from_core!( + pub enum ModelVerification from CoreModelVerification { + TrustedAccessForCyber + } +); + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ModelProviderCapabilitiesReadParams {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ModelProviderCapabilitiesReadResponse { + pub namespace_tools: bool, + pub image_generation: bool, + pub web_search: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ModelListParams { + /// Opaque pagination cursor returned by a previous call. + #[ts(optional = nullable)] + pub cursor: Option, + /// Optional page size; defaults to a reasonable server-side value. + #[ts(optional = nullable)] + pub limit: Option, + /// When true, include models that are hidden from the default picker list. + #[ts(optional = nullable)] + pub include_hidden: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ModelAvailabilityNux { + pub message: String, +} + +impl From for ModelAvailabilityNux { + fn from(value: CoreModelAvailabilityNux) -> Self { + Self { + message: value.message, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ModelServiceTier { + pub id: String, + pub name: String, + pub description: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct Model { + pub id: String, + pub model: String, + pub upgrade: Option, + pub upgrade_info: Option, + pub availability_nux: Option, + pub display_name: String, + pub description: String, + pub hidden: bool, + pub supported_reasoning_efforts: Vec, + pub default_reasoning_effort: ReasoningEffort, + #[serde(default = "default_input_modalities")] + pub input_modalities: Vec, + #[serde(default)] + pub supports_personality: bool, + /// Deprecated: use `serviceTiers` instead. + #[serde(default)] + pub additional_speed_tiers: Vec, + #[serde(default)] + pub service_tiers: Vec, + // Only one model should be marked as default. + pub is_default: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ModelUpgradeInfo { + pub model: String, + pub upgrade_copy: Option, + pub model_link: Option, + pub migration_markdown: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ReasoningEffortOption { + pub reasoning_effort: ReasoningEffort, + pub description: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ModelListResponse { + pub data: 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, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ModelReroutedNotification { + pub thread_id: String, + pub turn_id: String, + pub from_model: String, + pub to_model: String, + pub reason: ModelRerouteReason, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ModelVerificationNotification { + pub thread_id: String, + pub turn_id: String, + pub verifications: Vec, +} diff --git a/codex-rs/app-server-protocol/src/protocol/v2/notification.rs b/codex-rs/app-server-protocol/src/protocol/v2/notification.rs new file mode 100644 index 0000000000..8289cf5683 --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/notification.rs @@ -0,0 +1,56 @@ +use super::TurnError; +use crate::RequestId; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use ts_rs::TS; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct DeprecationNoticeNotification { + /// Concise summary of what is deprecated. + pub summary: String, + /// Optional extra guidance, such as migration steps or rationale. + pub details: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct WarningNotification { + /// Optional thread target when the warning applies to a specific thread. + pub thread_id: Option, + /// Concise warning message for the user. + pub message: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct GuardianWarningNotification { + /// Thread target for the guardian warning. + pub thread_id: String, + /// Concise guardian warning message for the user. + pub message: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ErrorNotification { + pub error: TurnError, + // Set to true if the error is transient and the app-server process will automatically retry. + // If true, this will not interrupt a turn. + pub will_retry: bool, + pub thread_id: String, + pub turn_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ServerRequestResolvedNotification { + pub thread_id: String, + pub request_id: RequestId, +} diff --git a/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs b/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs new file mode 100644 index 0000000000..8ce47e58cb --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs @@ -0,0 +1,854 @@ +use super::shared::v2_enum_from_core; +use codex_protocol::approvals::ExecPolicyAmendment as CoreExecPolicyAmendment; +use codex_protocol::approvals::NetworkApprovalContext as CoreNetworkApprovalContext; +use codex_protocol::approvals::NetworkApprovalProtocol as CoreNetworkApprovalProtocol; +use codex_protocol::approvals::NetworkPolicyAmendment as CoreNetworkPolicyAmendment; +use codex_protocol::approvals::NetworkPolicyRuleAction as CoreNetworkPolicyRuleAction; +use codex_protocol::models::ActivePermissionProfile as CoreActivePermissionProfile; +use codex_protocol::models::ActivePermissionProfileModification as CoreActivePermissionProfileModification; +use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile; +use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions; +use codex_protocol::models::ManagedFileSystemPermissions as CoreManagedFileSystemPermissions; +use codex_protocol::models::NetworkPermissions as CoreNetworkPermissions; +use codex_protocol::models::PermissionProfile as CorePermissionProfile; +use codex_protocol::permissions::FileSystemAccessMode as CoreFileSystemAccessMode; +use codex_protocol::permissions::FileSystemPath as CoreFileSystemPath; +use codex_protocol::permissions::FileSystemSandboxEntry as CoreFileSystemSandboxEntry; +use codex_protocol::permissions::FileSystemSpecialPath as CoreFileSystemSpecialPath; +use codex_protocol::permissions::NetworkSandboxPolicy as CoreNetworkSandboxPolicy; +use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess; +use codex_protocol::request_permissions::PermissionGrantScope as CorePermissionGrantScope; +use codex_protocol::request_permissions::RequestPermissionProfile as CoreRequestPermissionProfile; +use codex_utils_absolute_path::AbsolutePathBuf; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use std::num::NonZeroUsize; +use std::path::PathBuf; +use ts_rs::TS; + +v2_enum_from_core! { + pub enum NetworkApprovalProtocol from CoreNetworkApprovalProtocol { + Http, + Https, + Socks5Tcp, + Socks5Udp, + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct NetworkApprovalContext { + pub host: String, + pub protocol: NetworkApprovalProtocol, +} + +impl From for NetworkApprovalContext { + fn from(value: CoreNetworkApprovalContext) -> Self { + Self { + host: value.host, + protocol: value.protocol.into(), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AdditionalFileSystemPermissions { + /// This will be removed in favor of `entries`. + pub read: Option>, + /// This will be removed in favor of `entries`. + pub write: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub glob_scan_max_depth: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub entries: Option>, +} + +impl From for AdditionalFileSystemPermissions { + fn from(value: CoreFileSystemPermissions) -> Self { + if let Some((read, write)) = value.legacy_read_write_roots() { + let mut entries = Vec::with_capacity( + read.as_ref().map_or(0, Vec::len) + write.as_ref().map_or(0, Vec::len), + ); + if let Some(paths) = read.as_ref() { + entries.extend(paths.iter().map(|path| FileSystemSandboxEntry { + path: FileSystemPath::Path { path: path.clone() }, + access: FileSystemAccessMode::Read, + })); + } + if let Some(paths) = write.as_ref() { + entries.extend(paths.iter().map(|path| FileSystemSandboxEntry { + path: FileSystemPath::Path { path: path.clone() }, + access: FileSystemAccessMode::Write, + })); + } + Self { + read, + write, + glob_scan_max_depth: None, + entries: Some(entries), + } + } else { + Self { + read: None, + write: None, + glob_scan_max_depth: value.glob_scan_max_depth, + entries: Some( + value + .entries + .into_iter() + .map(FileSystemSandboxEntry::from) + .collect(), + ), + } + } + } +} + +impl From for CoreFileSystemPermissions { + fn from(value: AdditionalFileSystemPermissions) -> Self { + let mut permissions = if let Some(entries) = value.entries { + Self { + entries: entries + .into_iter() + .map(CoreFileSystemSandboxEntry::from) + .collect(), + glob_scan_max_depth: None, + } + } else { + CoreFileSystemPermissions::from_read_write_roots(value.read, value.write) + }; + permissions.glob_scan_max_depth = value.glob_scan_max_depth; + permissions + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AdditionalNetworkPermissions { + pub enabled: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PermissionProfileNetworkPermissions { + pub enabled: bool, +} + +impl From for AdditionalNetworkPermissions { + fn from(value: CoreNetworkPermissions) -> Self { + Self { + enabled: value.enabled, + } + } +} + +impl From for CoreNetworkPermissions { + fn from(value: AdditionalNetworkPermissions) -> Self { + Self { + enabled: value.enabled, + } + } +} + +impl From for PermissionProfileNetworkPermissions { + fn from(value: CoreNetworkSandboxPolicy) -> Self { + Self { + enabled: value.is_enabled(), + } + } +} + +impl From for CoreNetworkSandboxPolicy { + fn from(value: PermissionProfileNetworkPermissions) -> Self { + if value.enabled { + Self::Enabled + } else { + Self::Restricted + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +#[ts(export_to = "v2/")] +pub struct RequestPermissionProfile { + pub network: Option, + pub file_system: Option, +} + +impl From for RequestPermissionProfile { + fn from(value: CoreRequestPermissionProfile) -> Self { + Self { + network: value.network.map(AdditionalNetworkPermissions::from), + file_system: value.file_system.map(AdditionalFileSystemPermissions::from), + } + } +} + +impl From for CoreRequestPermissionProfile { + fn from(value: RequestPermissionProfile) -> Self { + Self { + network: value.network.map(CoreNetworkPermissions::from), + file_system: value.file_system.map(CoreFileSystemPermissions::from), + } + } +} + +v2_enum_from_core!( + pub enum FileSystemAccessMode from CoreFileSystemAccessMode { + Read, + Write, + None + } +); + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(tag = "kind", rename_all = "snake_case")] +#[ts(tag = "kind")] +#[ts(export_to = "v2/")] +pub enum FileSystemSpecialPath { + Root, + Minimal, + #[serde(alias = "current_working_directory")] + ProjectRoots { + subpath: Option, + }, + Tmpdir, + SlashTmp, + Unknown { + path: String, + subpath: Option, + }, +} + +impl From for FileSystemSpecialPath { + fn from(value: CoreFileSystemSpecialPath) -> Self { + match value { + CoreFileSystemSpecialPath::Root => Self::Root, + CoreFileSystemSpecialPath::Minimal => Self::Minimal, + CoreFileSystemSpecialPath::ProjectRoots { subpath } => Self::ProjectRoots { subpath }, + CoreFileSystemSpecialPath::Tmpdir => Self::Tmpdir, + CoreFileSystemSpecialPath::SlashTmp => Self::SlashTmp, + CoreFileSystemSpecialPath::Unknown { path, subpath } => Self::Unknown { path, subpath }, + } + } +} + +impl From for CoreFileSystemSpecialPath { + fn from(value: FileSystemSpecialPath) -> Self { + match value { + FileSystemSpecialPath::Root => Self::Root, + FileSystemSpecialPath::Minimal => Self::Minimal, + FileSystemSpecialPath::ProjectRoots { subpath } => Self::ProjectRoots { subpath }, + FileSystemSpecialPath::Tmpdir => Self::Tmpdir, + FileSystemSpecialPath::SlashTmp => Self::SlashTmp, + FileSystemSpecialPath::Unknown { path, subpath } => Self::Unknown { path, subpath }, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "snake_case")] +#[ts(tag = "type")] +#[ts(export_to = "v2/")] +pub enum FileSystemPath { + Path { path: AbsolutePathBuf }, + GlobPattern { pattern: String }, + Special { value: FileSystemSpecialPath }, +} + +impl From for FileSystemPath { + fn from(value: CoreFileSystemPath) -> Self { + match value { + CoreFileSystemPath::Path { path } => Self::Path { path }, + CoreFileSystemPath::GlobPattern { pattern } => Self::GlobPattern { pattern }, + CoreFileSystemPath::Special { value } => Self::Special { + value: value.into(), + }, + } + } +} + +impl From for CoreFileSystemPath { + fn from(value: FileSystemPath) -> Self { + match value { + FileSystemPath::Path { path } => Self::Path { path }, + FileSystemPath::GlobPattern { pattern } => Self::GlobPattern { pattern }, + FileSystemPath::Special { value } => Self::Special { + value: value.into(), + }, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FileSystemSandboxEntry { + pub path: FileSystemPath, + pub access: FileSystemAccessMode, +} + +impl From for FileSystemSandboxEntry { + fn from(value: CoreFileSystemSandboxEntry) -> Self { + Self { + path: value.path.into(), + access: value.access.into(), + } + } +} + +impl From for CoreFileSystemSandboxEntry { + fn from(value: FileSystemSandboxEntry) -> Self { + Self { + path: value.path.into(), + access: value.access.to_core(), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type")] +#[ts(export_to = "v2/")] +pub enum PermissionProfileFileSystemPermissions { + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + Restricted { + entries: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + glob_scan_max_depth: Option, + }, + Unrestricted, +} + +impl From for PermissionProfileFileSystemPermissions { + fn from(value: CoreManagedFileSystemPermissions) -> Self { + match value { + CoreManagedFileSystemPermissions::Restricted { + entries, + glob_scan_max_depth, + } => Self::Restricted { + entries: entries + .into_iter() + .map(FileSystemSandboxEntry::from) + .collect(), + glob_scan_max_depth, + }, + CoreManagedFileSystemPermissions::Unrestricted => Self::Unrestricted, + } + } +} + +impl From for CoreManagedFileSystemPermissions { + fn from(value: PermissionProfileFileSystemPermissions) -> Self { + match value { + PermissionProfileFileSystemPermissions::Restricted { + entries, + glob_scan_max_depth, + } => Self::Restricted { + entries: entries + .into_iter() + .map(CoreFileSystemSandboxEntry::from) + .collect(), + glob_scan_max_depth, + }, + PermissionProfileFileSystemPermissions::Unrestricted => Self::Unrestricted, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type")] +#[ts(export_to = "v2/")] +pub enum PermissionProfile { + /// Codex owns sandbox construction for this profile. + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + Managed { + network: PermissionProfileNetworkPermissions, + file_system: PermissionProfileFileSystemPermissions, + }, + /// Do not apply an outer sandbox. + Disabled, + /// Filesystem isolation is enforced by an external caller. + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + External { + network: PermissionProfileNetworkPermissions, + }, +} + +impl From for PermissionProfile { + fn from(value: CorePermissionProfile) -> Self { + match value { + CorePermissionProfile::Managed { + file_system, + network, + } => Self::Managed { + network: network.into(), + file_system: file_system.into(), + }, + CorePermissionProfile::Disabled => Self::Disabled, + CorePermissionProfile::External { network } => Self::External { + network: network.into(), + }, + } + } +} + +impl From for CorePermissionProfile { + fn from(value: PermissionProfile) -> Self { + match value { + PermissionProfile::Managed { + file_system, + network, + } => Self::Managed { + file_system: file_system.into(), + network: network.into(), + }, + PermissionProfile::Disabled => Self::Disabled, + PermissionProfile::External { network } => Self::External { + network: network.into(), + }, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ActivePermissionProfile { + /// Identifier from `default_permissions` or the implicit built-in default, + /// such as `:workspace` or a user-defined `[permissions.]` profile. + pub id: String, + /// Parent profile identifier once permissions profiles support + /// inheritance. This is currently always `null`. + #[serde(default)] + pub extends: Option, + /// Bounded user-requested modifications applied on top of the named + /// profile, if any. + #[serde(default)] + pub modifications: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type")] +#[ts(export_to = "v2/")] +pub enum ActivePermissionProfileModification { + /// Additional concrete directory that should be writable. + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + AdditionalWritableRoot { path: AbsolutePathBuf }, +} + +impl From for ActivePermissionProfileModification { + fn from(value: CoreActivePermissionProfileModification) -> Self { + match value { + CoreActivePermissionProfileModification::AdditionalWritableRoot { path } => { + Self::AdditionalWritableRoot { path } + } + } + } +} + +impl From for CoreActivePermissionProfileModification { + fn from(value: ActivePermissionProfileModification) -> Self { + match value { + ActivePermissionProfileModification::AdditionalWritableRoot { path } => { + Self::AdditionalWritableRoot { path } + } + } + } +} + +impl From for ActivePermissionProfile { + fn from(value: CoreActivePermissionProfile) -> Self { + Self { + id: value.id, + extends: value.extends, + modifications: value + .modifications + .into_iter() + .map(ActivePermissionProfileModification::from) + .collect(), + } + } +} + +impl From for CoreActivePermissionProfile { + fn from(value: ActivePermissionProfile) -> Self { + Self { + id: value.id, + extends: value.extends, + modifications: value + .modifications + .into_iter() + .map(CoreActivePermissionProfileModification::from) + .collect(), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type")] +#[ts(export_to = "v2/")] +pub enum PermissionProfileSelectionParams { + /// Select a named built-in or user-defined profile and optionally apply + /// bounded modifications that Codex knows how to validate. + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + Profile { + id: String, + #[ts(optional = nullable)] + modifications: Option>, + }, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type")] +#[ts(export_to = "v2/")] +pub enum PermissionProfileModificationParams { + /// Additional concrete directory that should be writable. + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + AdditionalWritableRoot { path: AbsolutePathBuf }, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AdditionalPermissionProfile { + /// Partial overlay used for per-command permission requests. + pub network: Option, + pub file_system: Option, +} + +impl From for AdditionalPermissionProfile { + fn from(value: CoreAdditionalPermissionProfile) -> Self { + Self { + network: value.network.map(AdditionalNetworkPermissions::from), + file_system: value.file_system.map(AdditionalFileSystemPermissions::from), + } + } +} + +impl From for CoreAdditionalPermissionProfile { + fn from(value: AdditionalPermissionProfile) -> Self { + Self { + network: value.network.map(CoreNetworkPermissions::from), + file_system: value.file_system.map(CoreFileSystemPermissions::from), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct GrantedPermissionProfile { + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub network: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub file_system: Option, +} + +impl From for CoreAdditionalPermissionProfile { + fn from(value: GrantedPermissionProfile) -> Self { + Self { + network: value.network.map(CoreNetworkPermissions::from), + file_system: value.file_system.map(CoreFileSystemPermissions::from), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum NetworkAccess { + #[default] + Restricted, + Enabled, +} + +#[derive(Serialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type")] +#[ts(export_to = "v2/")] +pub enum SandboxPolicy { + DangerFullAccess, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + ReadOnly { + #[serde(default)] + network_access: bool, + }, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + ExternalSandbox { + #[serde(default)] + network_access: NetworkAccess, + }, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + WorkspaceWrite { + #[serde(default)] + writable_roots: Vec, + #[serde(default)] + network_access: bool, + #[serde(default)] + exclude_tmpdir_env_var: bool, + #[serde(default)] + exclude_slash_tmp: bool, + }, +} + +#[derive(Deserialize)] +#[serde(tag = "type", rename_all = "camelCase")] +enum SandboxPolicyDeserialize { + DangerFullAccess, + #[serde(rename_all = "camelCase")] + ReadOnly { + #[serde(default)] + network_access: bool, + #[serde(default)] + access: Option, + }, + #[serde(rename_all = "camelCase")] + ExternalSandbox { + #[serde(default)] + network_access: NetworkAccess, + }, + #[serde(rename_all = "camelCase")] + WorkspaceWrite { + #[serde(default)] + writable_roots: Vec, + #[serde(default)] + read_only_access: Option, + #[serde(default)] + network_access: bool, + #[serde(default)] + exclude_tmpdir_env_var: bool, + #[serde(default)] + exclude_slash_tmp: bool, + }, +} + +#[derive(Deserialize)] +#[serde(tag = "type", rename_all = "camelCase")] +enum LegacyReadOnlyAccess { + FullAccess, + Restricted, +} + +impl<'de> Deserialize<'de> for SandboxPolicy { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + match SandboxPolicyDeserialize::deserialize(deserializer)? { + SandboxPolicyDeserialize::DangerFullAccess => Ok(SandboxPolicy::DangerFullAccess), + SandboxPolicyDeserialize::ReadOnly { + network_access, + access, + } => { + if matches!(access, Some(LegacyReadOnlyAccess::Restricted)) { + return Err(serde::de::Error::custom( + "readOnly.access is no longer supported; use permissionProfile for restricted reads", + )); + } + Ok(SandboxPolicy::ReadOnly { network_access }) + } + SandboxPolicyDeserialize::ExternalSandbox { network_access } => { + Ok(SandboxPolicy::ExternalSandbox { network_access }) + } + SandboxPolicyDeserialize::WorkspaceWrite { + writable_roots, + read_only_access, + network_access, + exclude_tmpdir_env_var, + exclude_slash_tmp, + } => { + if matches!(read_only_access, Some(LegacyReadOnlyAccess::Restricted)) { + return Err(serde::de::Error::custom( + "workspaceWrite.readOnlyAccess is no longer supported; use permissionProfile for restricted reads", + )); + } + Ok(SandboxPolicy::WorkspaceWrite { + writable_roots, + network_access, + exclude_tmpdir_env_var, + exclude_slash_tmp, + }) + } + } + } +} + +impl SandboxPolicy { + pub fn to_core(&self) -> codex_protocol::protocol::SandboxPolicy { + match self { + SandboxPolicy::DangerFullAccess => { + codex_protocol::protocol::SandboxPolicy::DangerFullAccess + } + SandboxPolicy::ReadOnly { network_access } => { + codex_protocol::protocol::SandboxPolicy::ReadOnly { + network_access: *network_access, + } + } + SandboxPolicy::ExternalSandbox { network_access } => { + codex_protocol::protocol::SandboxPolicy::ExternalSandbox { + network_access: match network_access { + NetworkAccess::Restricted => CoreNetworkAccess::Restricted, + NetworkAccess::Enabled => CoreNetworkAccess::Enabled, + }, + } + } + SandboxPolicy::WorkspaceWrite { + writable_roots, + network_access, + exclude_tmpdir_env_var, + exclude_slash_tmp, + } => codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { + writable_roots: writable_roots.clone(), + network_access: *network_access, + exclude_tmpdir_env_var: *exclude_tmpdir_env_var, + exclude_slash_tmp: *exclude_slash_tmp, + }, + } + } +} + +impl From for SandboxPolicy { + fn from(value: codex_protocol::protocol::SandboxPolicy) -> Self { + match value { + codex_protocol::protocol::SandboxPolicy::DangerFullAccess => { + SandboxPolicy::DangerFullAccess + } + codex_protocol::protocol::SandboxPolicy::ReadOnly { network_access } => { + SandboxPolicy::ReadOnly { network_access } + } + codex_protocol::protocol::SandboxPolicy::ExternalSandbox { network_access } => { + SandboxPolicy::ExternalSandbox { + network_access: match network_access { + CoreNetworkAccess::Restricted => NetworkAccess::Restricted, + CoreNetworkAccess::Enabled => NetworkAccess::Enabled, + }, + } + } + codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { + writable_roots, + network_access, + exclude_tmpdir_env_var, + exclude_slash_tmp, + } => SandboxPolicy::WorkspaceWrite { + writable_roots, + network_access, + exclude_tmpdir_env_var, + exclude_slash_tmp, + }, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(transparent)] +#[ts(type = "Array", export_to = "v2/")] +pub struct ExecPolicyAmendment { + pub command: Vec, +} + +impl ExecPolicyAmendment { + pub fn into_core(self) -> CoreExecPolicyAmendment { + CoreExecPolicyAmendment::new(self.command) + } +} + +impl From for ExecPolicyAmendment { + fn from(value: CoreExecPolicyAmendment) -> Self { + Self { + command: value.command().to_vec(), + } + } +} + +v2_enum_from_core!( + pub enum NetworkPolicyRuleAction from CoreNetworkPolicyRuleAction { + Allow, Deny + } +); + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct NetworkPolicyAmendment { + pub host: String, + pub action: NetworkPolicyRuleAction, +} + +impl NetworkPolicyAmendment { + pub fn into_core(self) -> CoreNetworkPolicyAmendment { + CoreNetworkPolicyAmendment { + host: self.host, + action: self.action.to_core(), + } + } +} + +impl From for NetworkPolicyAmendment { + fn from(value: CoreNetworkPolicyAmendment) -> Self { + Self { + host: value.host, + action: NetworkPolicyRuleAction::from(value.action), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PermissionsRequestApprovalParams { + pub thread_id: String, + pub turn_id: String, + pub item_id: String, + pub cwd: AbsolutePathBuf, + pub reason: Option, + pub permissions: RequestPermissionProfile, +} + +v2_enum_from_core!( + #[derive(Default)] + pub enum PermissionGrantScope from CorePermissionGrantScope { + #[default] + Turn, + Session + } +); + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PermissionsRequestApprovalResponse { + pub permissions: GrantedPermissionProfile, + #[serde(default)] + pub scope: PermissionGrantScope, + /// Review every subsequent command in this turn before normal sandboxed execution. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub strict_auto_review: Option, +} diff --git a/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs b/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs new file mode 100644 index 0000000000..09f9325317 --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs @@ -0,0 +1,649 @@ +use super::AppSummary; +use super::HookEventName; +use super::HookHandlerType; +use super::HookSource; +use super::HookTrustStatus; +use codex_protocol::protocol::SkillDependencies as CoreSkillDependencies; +use codex_protocol::protocol::SkillInterface as CoreSkillInterface; +use codex_protocol::protocol::SkillMetadata as CoreSkillMetadata; +use codex_protocol::protocol::SkillScope as CoreSkillScope; +use codex_protocol::protocol::SkillToolDependency as CoreSkillToolDependency; +use codex_utils_absolute_path::AbsolutePathBuf; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use std::path::PathBuf; +use ts_rs::TS; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SkillsListParams { + /// When empty, defaults to the current session working directory. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub cwds: Vec, + + /// When true, bypass the skills cache and re-scan skills from disk. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub force_reload: bool, + + /// Optional per-cwd extra roots to scan as user-scoped skills. + #[serde(default)] + #[ts(optional = nullable)] + pub per_cwd_extra_user_roots: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SkillsListExtraRootsForCwd { + pub cwd: PathBuf, + pub extra_user_roots: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SkillsListResponse { + pub data: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct HooksListParams { + /// When empty, defaults to the current session working directory. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub cwds: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct HooksListResponse { + pub data: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct MarketplaceAddParams { + pub source: String, + #[ts(optional = nullable)] + pub ref_name: Option, + #[ts(optional = nullable)] + pub sparse_paths: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct MarketplaceAddResponse { + pub marketplace_name: String, + pub installed_root: AbsolutePathBuf, + pub already_added: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct MarketplaceRemoveParams { + pub marketplace_name: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct MarketplaceRemoveResponse { + pub marketplace_name: String, + pub installed_root: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct MarketplaceUpgradeParams { + #[ts(optional = nullable)] + pub marketplace_name: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct MarketplaceUpgradeResponse { + pub selected_marketplaces: Vec, + pub upgraded_roots: Vec, + pub errors: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct MarketplaceUpgradeErrorInfo { + pub marketplace_name: String, + pub message: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PluginListParams { + /// Optional working directories used to discover repo marketplaces. When omitted, + /// only home-scoped marketplaces and the official curated marketplace are considered. + #[ts(optional = nullable)] + pub cwds: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PluginListResponse { + pub marketplaces: Vec, + #[serde(default)] + pub marketplace_load_errors: Vec, + #[serde(default)] + pub featured_plugin_ids: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct MarketplaceLoadErrorInfo { + pub marketplace_path: AbsolutePathBuf, + pub message: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PluginReadParams { + #[ts(optional = nullable)] + pub marketplace_path: Option, + #[ts(optional = nullable)] + pub remote_marketplace_name: Option, + pub plugin_name: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PluginReadResponse { + pub plugin: PluginDetail, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PluginSkillReadParams { + pub remote_marketplace_name: String, + pub remote_plugin_id: String, + pub skill_name: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PluginSkillReadResponse { + pub contents: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PluginShareSaveParams { + pub plugin_path: AbsolutePathBuf, + #[ts(optional = nullable)] + pub remote_plugin_id: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PluginShareSaveResponse { + pub remote_plugin_id: String, + pub share_url: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PluginShareListParams {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PluginShareListResponse { + pub data: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PluginShareDeleteParams { + pub remote_plugin_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PluginShareDeleteResponse {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PluginShareListItem { + pub plugin: PluginSummary, + pub share_url: String, + pub local_plugin_path: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(rename_all = "snake_case")] +#[ts(export_to = "v2/")] +pub enum SkillScope { + User, + Repo, + System, + Admin, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SkillMetadata { + pub name: String, + pub description: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + /// Legacy short_description from SKILL.md. Prefer SKILL.json interface.short_description. + pub short_description: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub interface: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub dependencies: Option, + pub path: AbsolutePathBuf, + pub scope: SkillScope, + pub enabled: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SkillInterface { + #[ts(optional)] + pub display_name: Option, + #[ts(optional)] + pub short_description: Option, + #[ts(optional)] + pub icon_small: Option, + #[ts(optional)] + pub icon_large: Option, + #[ts(optional)] + pub brand_color: Option, + #[ts(optional)] + pub default_prompt: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SkillDependencies { + pub tools: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SkillToolDependency { + #[serde(rename = "type")] + #[ts(rename = "type")] + pub r#type: String, + pub value: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub description: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub transport: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub command: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub url: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SkillErrorInfo { + pub path: PathBuf, + pub message: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SkillsListEntry { + pub cwd: PathBuf, + pub skills: Vec, + pub errors: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct HooksListEntry { + pub cwd: PathBuf, + pub hooks: Vec, + pub warnings: Vec, + pub errors: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct HookMetadata { + pub key: String, + pub event_name: HookEventName, + pub handler_type: HookHandlerType, + pub matcher: Option, + pub command: Option, + pub timeout_sec: u64, + pub status_message: Option, + pub source_path: AbsolutePathBuf, + pub source: HookSource, + pub plugin_id: Option, + pub display_order: i64, + pub enabled: bool, + pub is_managed: bool, + pub current_hash: String, + pub trust_status: HookTrustStatus, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct HookErrorInfo { + pub path: PathBuf, + pub message: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PluginMarketplaceEntry { + pub name: String, + /// Local marketplace file path when the marketplace is backed by a local file. + /// Remote-only catalog marketplaces do not have a local path. + pub path: Option, + pub interface: Option, + pub plugins: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct MarketplaceInterface { + pub display_name: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[ts(export_to = "v2/")] +pub enum PluginInstallPolicy { + #[serde(rename = "NOT_AVAILABLE")] + #[ts(rename = "NOT_AVAILABLE")] + NotAvailable, + #[serde(rename = "AVAILABLE")] + #[ts(rename = "AVAILABLE")] + Available, + #[serde(rename = "INSTALLED_BY_DEFAULT")] + #[ts(rename = "INSTALLED_BY_DEFAULT")] + InstalledByDefault, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[ts(export_to = "v2/")] +pub enum PluginAuthPolicy { + #[serde(rename = "ON_INSTALL")] + #[ts(rename = "ON_INSTALL")] + OnInstall, + #[serde(rename = "ON_USE")] + #[ts(rename = "ON_USE")] + OnUse, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, JsonSchema, TS)] +#[ts(export_to = "v2/")] +pub enum PluginAvailability { + /// Plugin-service currently sends `"ENABLED"` for available remote plugins. + /// Codex app-server exposes `"AVAILABLE"` in its API; the alias keeps + /// decoding compatible with that upstream response. + #[serde(rename = "AVAILABLE", alias = "ENABLED")] + #[ts(rename = "AVAILABLE")] + #[default] + Available, + #[serde(rename = "DISABLED_BY_ADMIN")] + #[ts(rename = "DISABLED_BY_ADMIN")] + DisabledByAdmin, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PluginSummary { + pub id: String, + pub name: String, + pub source: PluginSource, + pub installed: bool, + pub enabled: bool, + pub install_policy: PluginInstallPolicy, + pub auth_policy: PluginAuthPolicy, + /// Availability state for installing and using the plugin. + #[serde(default)] + pub availability: PluginAvailability, + pub interface: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PluginDetail { + pub marketplace_name: String, + pub marketplace_path: Option, + pub summary: PluginSummary, + pub description: Option, + pub skills: Vec, + pub apps: Vec, + pub mcp_servers: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SkillSummary { + pub name: String, + pub description: String, + pub short_description: Option, + pub interface: Option, + pub path: Option, + pub enabled: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PluginInterface { + pub display_name: Option, + pub short_description: Option, + pub long_description: Option, + pub developer_name: Option, + pub category: Option, + pub capabilities: Vec, + pub website_url: Option, + pub privacy_policy_url: Option, + pub terms_of_service_url: Option, + /// Starter prompts for the plugin. Capped at 3 entries with a maximum of + /// 128 characters per entry. + pub default_prompt: Option>, + pub brand_color: Option, + /// Local composer icon path, resolved from the installed plugin package. + pub composer_icon: Option, + /// Remote composer icon URL from the plugin catalog. + pub composer_icon_url: Option, + /// Local logo path, resolved from the installed plugin package. + pub logo: Option, + /// Remote logo URL from the plugin catalog. + pub logo_url: Option, + /// Local screenshot paths, resolved from the installed plugin package. + pub screenshots: Vec, + /// Remote screenshot URLs from the plugin catalog. + pub screenshot_urls: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type")] +#[ts(export_to = "v2/")] +pub enum PluginSource { + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + Local { path: AbsolutePathBuf }, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + Git { + url: String, + path: Option, + ref_name: Option, + sha: Option, + }, + /// The plugin is available in the remote catalog. Download metadata is + /// kept server-side and is not exposed through the app-server API. + Remote, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SkillsConfigWriteParams { + /// Path-based selector. + #[ts(optional = nullable)] + pub path: Option, + /// Name-based selector. + #[ts(optional = nullable)] + pub name: Option, + pub enabled: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SkillsConfigWriteResponse { + pub effective_enabled: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PluginInstallParams { + #[ts(optional = nullable)] + pub marketplace_path: Option, + #[ts(optional = nullable)] + pub remote_marketplace_name: Option, + pub plugin_name: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PluginInstallResponse { + pub auth_policy: PluginAuthPolicy, + pub apps_needing_auth: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PluginUninstallParams { + pub plugin_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct PluginUninstallResponse {} + +impl From for SkillMetadata { + fn from(value: CoreSkillMetadata) -> Self { + Self { + name: value.name, + description: value.description, + short_description: value.short_description, + interface: value.interface.map(SkillInterface::from), + dependencies: value.dependencies.map(SkillDependencies::from), + path: value.path, + scope: value.scope.into(), + enabled: true, + } + } +} + +impl From for SkillInterface { + fn from(value: CoreSkillInterface) -> Self { + Self { + display_name: value.display_name, + short_description: value.short_description, + brand_color: value.brand_color, + default_prompt: value.default_prompt, + icon_small: value.icon_small, + icon_large: value.icon_large, + } + } +} + +impl From for SkillDependencies { + fn from(value: CoreSkillDependencies) -> Self { + Self { + tools: value + .tools + .into_iter() + .map(SkillToolDependency::from) + .collect(), + } + } +} + +impl From for SkillToolDependency { + fn from(value: CoreSkillToolDependency) -> Self { + Self { + r#type: value.r#type, + value: value.value, + description: value.description, + transport: value.transport, + command: value.command, + url: value.url, + } + } +} + +impl From for SkillScope { + fn from(value: CoreSkillScope) -> Self { + match value { + CoreSkillScope::User => Self::User, + CoreSkillScope::Repo => Self::Repo, + CoreSkillScope::System => Self::System, + CoreSkillScope::Admin => Self::Admin, + } + } +} +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +/// Notification emitted when watched local skill files change. +/// +/// Treat this as an invalidation signal and re-run `skills/list` with the +/// client's current parameters when refreshed skill metadata is needed. +pub struct SkillsChangedNotification {} diff --git a/codex-rs/app-server-protocol/src/protocol/v2/process.rs b/codex-rs/app-server-protocol/src/protocol/v2/process.rs new file mode 100644 index 0000000000..b70847165e --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/process.rs @@ -0,0 +1,204 @@ +use codex_utils_absolute_path::AbsolutePathBuf; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use std::collections::HashMap; +use ts_rs::TS; + +/// PTY size in character cells for `process/spawn` PTY sessions. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ProcessTerminalSize { + /// Terminal height in character cells. + pub rows: u16, + /// Terminal width in character cells. + pub cols: u16, +} + +/// Spawn a standalone process (argv vector) without a Codex sandbox on the host +/// where the app server is running. +/// +/// `process/spawn` returns after the process has started and the connection-scoped +/// `processHandle` has been registered. Process output and exit are reported via +/// `process/outputDelta` and `process/exited` notifications. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ProcessSpawnParams { + /// Command argv vector. Empty arrays are rejected. + pub command: Vec, + /// Client-supplied, connection-scoped process handle. + /// + /// Duplicate active handles are rejected on the same connection. The same + /// handle can be reused after the prior process exits. + pub process_handle: String, + /// Absolute working directory for the process. + pub cwd: AbsolutePathBuf, + /// Enable PTY mode. + /// + /// This implies `streamStdin` and `streamStdoutStderr`. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub tty: bool, + /// Allow follow-up `process/writeStdin` requests to write stdin bytes. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub stream_stdin: bool, + /// Stream stdout/stderr via `process/outputDelta` notifications. + /// + /// Streamed bytes are not duplicated into the `process/exited` notification. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub stream_stdout_stderr: bool, + /// Optional per-stream stdout/stderr capture cap in bytes. + /// + /// When omitted, the server default applies. Set to `null` to disable the + /// cap. + #[serde( + default, + deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option", + serialize_with = "crate::protocol::serde_helpers::serialize_double_option", + skip_serializing_if = "Option::is_none" + )] + #[ts(type = "number | null")] + #[ts(optional = nullable)] + pub output_bytes_cap: Option>, + /// Optional timeout in milliseconds. + /// + /// When omitted, the server default applies. Set to `null` to disable the + /// timeout. + #[serde( + default, + deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option", + serialize_with = "crate::protocol::serde_helpers::serialize_double_option", + skip_serializing_if = "Option::is_none" + )] + #[ts(type = "number | null")] + #[ts(optional = nullable)] + pub timeout_ms: Option>, + /// Optional environment overrides merged into the app-server process + /// environment. + /// + /// Matching names override inherited values. Set a key to `null` to unset + /// an inherited variable. + #[ts(optional = nullable)] + pub env: Option>>, + /// Optional initial PTY size in character cells. Only valid when `tty` is + /// true. + #[ts(optional = nullable)] + pub size: Option, +} + +/// Successful response for `process/spawn`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ProcessSpawnResponse {} + +/// Write stdin bytes to a running `process/spawn` session, close stdin, or +/// both. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ProcessWriteStdinParams { + /// Client-supplied, connection-scoped `processHandle` from `process/spawn`. + pub process_handle: String, + /// Optional base64-encoded stdin bytes to write. + #[ts(optional = nullable)] + pub delta_base64: Option, + /// Close stdin after writing `deltaBase64`, if present. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub close_stdin: bool, +} + +/// Empty success response for `process/writeStdin`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ProcessWriteStdinResponse {} + +/// Terminate a running `process/spawn` session. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ProcessKillParams { + /// Client-supplied, connection-scoped `processHandle` from `process/spawn`. + pub process_handle: String, +} + +/// Empty success response for `process/kill`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ProcessKillResponse {} + +/// Resize a running PTY-backed `process/spawn` session. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ProcessResizePtyParams { + /// Client-supplied, connection-scoped `processHandle` from `process/spawn`. + pub process_handle: String, + /// New PTY size in character cells. + pub size: ProcessTerminalSize, +} + +/// Empty success response for `process/resizePty`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ProcessResizePtyResponse {} + +/// Stream label for `process/outputDelta` notifications. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum ProcessOutputStream { + /// stdout stream. PTY mode multiplexes terminal output here. + Stdout, + /// stderr stream. + Stderr, +} + +/// Base64-encoded output chunk emitted for a streaming `process/spawn` request. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ProcessOutputDeltaNotification { + /// Client-supplied, connection-scoped `processHandle` from `process/spawn`. + pub process_handle: String, + /// Output stream this chunk belongs to. + pub stream: ProcessOutputStream, + /// Base64-encoded output bytes. + pub delta_base64: String, + /// True on the final streamed chunk for this stream when output was + /// truncated by `outputBytesCap`. + pub cap_reached: bool, +} + +/// Final process exit notification for `process/spawn`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ProcessExitedNotification { + /// Client-supplied, connection-scoped `processHandle` from `process/spawn`. + pub process_handle: String, + /// Process exit code. + pub exit_code: i32, + /// Buffered stdout capture. + /// + /// Empty when stdout was streamed via `process/outputDelta`. + pub stdout: String, + /// Whether stdout reached `outputBytesCap`. + /// + /// In streaming mode, stdout is empty and cap state is also reported on the + /// final stdout `process/outputDelta` notification. + pub stdout_cap_reached: bool, + /// Buffered stderr capture. + /// + /// Empty when stderr was streamed via `process/outputDelta`. + pub stderr: String, + /// Whether stderr reached `outputBytesCap`. + /// + /// In streaming mode, stderr is empty and cap state is also reported on the + /// final stderr `process/outputDelta` notification. + pub stderr_cap_reached: bool, +} diff --git a/codex-rs/app-server-protocol/src/protocol/v2/realtime.rs b/codex-rs/app-server-protocol/src/protocol/v2/realtime.rs new file mode 100644 index 0000000000..c6ea0744de --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/realtime.rs @@ -0,0 +1,241 @@ +use codex_protocol::protocol::RealtimeAudioFrame as CoreRealtimeAudioFrame; +use codex_protocol::protocol::RealtimeConversationVersion; +use codex_protocol::protocol::RealtimeOutputModality; +use codex_protocol::protocol::RealtimeVoice; +use codex_protocol::protocol::RealtimeVoicesList; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use serde_json::Value as JsonValue; +use ts_rs::TS; + +/// EXPERIMENTAL - thread realtime audio chunk. +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeAudioChunk { + pub data: String, + pub sample_rate: u32, + pub num_channels: u16, + pub samples_per_channel: Option, + pub item_id: Option, +} + +impl From for ThreadRealtimeAudioChunk { + fn from(value: CoreRealtimeAudioFrame) -> Self { + let CoreRealtimeAudioFrame { + data, + sample_rate, + num_channels, + samples_per_channel, + item_id, + } = value; + Self { + data, + sample_rate, + num_channels, + samples_per_channel, + item_id, + } + } +} + +impl From for CoreRealtimeAudioFrame { + fn from(value: ThreadRealtimeAudioChunk) -> Self { + let ThreadRealtimeAudioChunk { + data, + sample_rate, + num_channels, + samples_per_channel, + item_id, + } = value; + Self { + data, + sample_rate, + num_channels, + samples_per_channel, + item_id, + } + } +} + +/// EXPERIMENTAL - start a thread-scoped realtime session. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeStartParams { + pub thread_id: String, + /// Selects text or audio output for the realtime session. Transport and voice stay + /// independent so clients can choose how they connect separately from what the model emits. + pub output_modality: RealtimeOutputModality, + #[serde( + default, + deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option", + serialize_with = "crate::protocol::serde_helpers::serialize_double_option", + skip_serializing_if = "Option::is_none" + )] + #[ts(optional = nullable)] + pub prompt: Option>, + #[ts(optional = nullable)] + pub realtime_session_id: Option, + #[ts(optional = nullable)] + pub transport: Option, + #[ts(optional = nullable)] + pub voice: Option, +} + +/// EXPERIMENTAL - transport used by thread realtime. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(export_to = "v2/", tag = "type")] +pub enum ThreadRealtimeStartTransport { + Websocket, + Webrtc { + /// SDP offer generated by a WebRTC RTCPeerConnection after configuring audio and the + /// realtime events data channel. + sdp: String, + }, +} + +/// EXPERIMENTAL - response for starting thread realtime. +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeStartResponse {} + +/// EXPERIMENTAL - append audio input to thread realtime. +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeAppendAudioParams { + pub thread_id: String, + pub audio: ThreadRealtimeAudioChunk, +} + +/// EXPERIMENTAL - response for appending realtime audio input. +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeAppendAudioResponse {} + +/// EXPERIMENTAL - append text input to thread realtime. +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeAppendTextParams { + pub thread_id: String, + pub text: String, +} + +/// EXPERIMENTAL - response for appending realtime text input. +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeAppendTextResponse {} + +/// EXPERIMENTAL - stop thread realtime. +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeStopParams { + pub thread_id: String, +} + +/// EXPERIMENTAL - response for stopping thread realtime. +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeStopResponse {} + +/// EXPERIMENTAL - list voices supported by thread realtime. +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeListVoicesParams {} + +/// EXPERIMENTAL - response for listing supported realtime voices. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeListVoicesResponse { + pub voices: RealtimeVoicesList, +} + +/// EXPERIMENTAL - emitted when thread realtime startup is accepted. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeStartedNotification { + pub thread_id: String, + pub realtime_session_id: Option, + pub version: RealtimeConversationVersion, +} + +/// EXPERIMENTAL - raw non-audio thread realtime item emitted by the backend. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeItemAddedNotification { + pub thread_id: String, + pub item: JsonValue, +} + +/// EXPERIMENTAL - flat transcript delta emitted whenever realtime +/// transcript text changes. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeTranscriptDeltaNotification { + pub thread_id: String, + pub role: String, + /// Live transcript delta from the realtime event. + pub delta: String, +} + +/// EXPERIMENTAL - final transcript text emitted when realtime completes +/// a transcript part. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeTranscriptDoneNotification { + pub thread_id: String, + pub role: String, + /// Final complete text for the transcript part. + pub text: String, +} + +/// EXPERIMENTAL - streamed output audio emitted by thread realtime. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeOutputAudioDeltaNotification { + pub thread_id: String, + pub audio: ThreadRealtimeAudioChunk, +} + +/// EXPERIMENTAL - emitted with the remote SDP for a WebRTC realtime session. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeSdpNotification { + pub thread_id: String, + pub sdp: String, +} + +/// EXPERIMENTAL - emitted when thread realtime encounters an error. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeErrorNotification { + pub thread_id: String, + pub message: String, +} + +/// EXPERIMENTAL - emitted when thread realtime transport closes. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRealtimeClosedNotification { + pub thread_id: String, + pub reason: Option, +} diff --git a/codex-rs/app-server-protocol/src/protocol/v2/review.rs b/codex-rs/app-server-protocol/src/protocol/v2/review.rs new file mode 100644 index 0000000000..82ec5b6f59 --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/review.rs @@ -0,0 +1,65 @@ +use super::Turn; +use super::shared::v2_enum_from_core; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use ts_rs::TS; + +v2_enum_from_core!( + pub enum ReviewDelivery from codex_protocol::protocol::ReviewDelivery { + Inline, Detached + } +); + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ReviewStartParams { + pub thread_id: String, + pub target: ReviewTarget, + + /// Where to run the review: inline (default) on the current thread or + /// detached on a new thread (returned in `reviewThreadId`). + #[serde(default)] + #[ts(optional = nullable)] + pub delivery: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ReviewStartResponse { + pub turn: Turn, + /// Identifies the thread where the review runs. + /// + /// For inline reviews, this is the original thread id. + /// For detached reviews, this is the id of the new review thread. + pub review_thread_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type", export_to = "v2/")] +pub enum ReviewTarget { + /// Review the working tree: staged, unstaged, and untracked files. + UncommittedChanges, + + /// Review changes between the current branch and the given base branch. + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + BaseBranch { branch: String }, + + /// Review the changes introduced by a specific commit. + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + Commit { + sha: String, + /// Optional human-readable label (e.g., commit subject) for UIs. + title: Option, + }, + + /// Arbitrary instructions, equivalent to the old free-form prompt. + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + Custom { instructions: String }, +} diff --git a/codex-rs/app-server-protocol/src/protocol/v2/shared.rs b/codex-rs/app-server-protocol/src/protocol/v2/shared.rs new file mode 100644 index 0000000000..9ec1fb80cb --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/shared.rs @@ -0,0 +1,316 @@ +use codex_experimental_api_macros::ExperimentalApi; +use codex_protocol::config_types::ApprovalsReviewer as CoreApprovalsReviewer; +use codex_protocol::config_types::SandboxMode as CoreSandboxMode; +use codex_protocol::protocol::AskForApproval as CoreAskForApproval; +use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo; +use codex_protocol::protocol::GranularApprovalConfig as CoreGranularApprovalConfig; +use codex_protocol::protocol::NonSteerableTurnKind as CoreNonSteerableTurnKind; +use schemars::JsonSchema; +use schemars::r#gen::SchemaGenerator; +use schemars::schema::InstanceType; +use schemars::schema::Metadata; +use schemars::schema::Schema; +use schemars::schema::SchemaObject; +use serde::Deserialize; +use serde::Serialize; +use serde_json::Value as JsonValue; +use ts_rs::TS; + +// Macro to declare a camelCased API v2 enum mirroring a core enum which +// tends to use either snake_case or kebab-case. +macro_rules! v2_enum_from_core { + ( + $(#[$enum_meta:meta])* + pub enum $Name:ident from $Src:path { + $( $(#[$variant_meta:meta])* $Variant:ident ),+ $(,)? + } + ) => { + #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] + $(#[$enum_meta])* + #[serde(rename_all = "camelCase")] + #[ts(export_to = "v2/")] + pub enum $Name { + $( $(#[$variant_meta])* $Variant ),+ + } + + impl $Name { + pub fn to_core(self) -> $Src { + match self { $( $Name::$Variant => <$Src>::$Variant ),+ } + } + } + + impl From<$Src> for $Name { + fn from(value: $Src) -> Self { + match value { $( <$Src>::$Variant => $Name::$Variant ),+ } + } + } + }; +} + +pub(super) use v2_enum_from_core; + +pub(super) const fn default_enabled() -> bool { + true +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum NonSteerableTurnKind { + Review, + Compact, +} + +/// This translation layer make sure that we expose codex error code in camel case. +/// +/// When an upstream HTTP status is available (for example, from the Responses API or a provider), +/// it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum CodexErrorInfo { + ContextWindowExceeded, + UsageLimitExceeded, + ServerOverloaded, + CyberPolicy, + HttpConnectionFailed { + #[serde(rename = "httpStatusCode")] + #[ts(rename = "httpStatusCode")] + http_status_code: Option, + }, + /// Failed to connect to the response SSE stream. + ResponseStreamConnectionFailed { + #[serde(rename = "httpStatusCode")] + #[ts(rename = "httpStatusCode")] + http_status_code: Option, + }, + InternalServerError, + Unauthorized, + BadRequest, + ThreadRollbackFailed, + SandboxError, + /// The response SSE stream disconnected in the middle of a turn before completion. + ResponseStreamDisconnected { + #[serde(rename = "httpStatusCode")] + #[ts(rename = "httpStatusCode")] + http_status_code: Option, + }, + /// Reached the retry limit for responses. + ResponseTooManyFailedAttempts { + #[serde(rename = "httpStatusCode")] + #[ts(rename = "httpStatusCode")] + http_status_code: Option, + }, + /// Returned when `turn/start` or `turn/steer` is submitted while the current active turn + /// cannot accept same-turn steering, for example `/review` or manual `/compact`. + ActiveTurnNotSteerable { + #[serde(rename = "turnKind")] + #[ts(rename = "turnKind")] + turn_kind: NonSteerableTurnKind, + }, + Other, +} + +impl From for CodexErrorInfo { + fn from(value: CoreCodexErrorInfo) -> Self { + match value { + CoreCodexErrorInfo::ContextWindowExceeded => CodexErrorInfo::ContextWindowExceeded, + CoreCodexErrorInfo::UsageLimitExceeded => CodexErrorInfo::UsageLimitExceeded, + CoreCodexErrorInfo::ServerOverloaded => CodexErrorInfo::ServerOverloaded, + CoreCodexErrorInfo::CyberPolicy => CodexErrorInfo::CyberPolicy, + CoreCodexErrorInfo::HttpConnectionFailed { http_status_code } => { + CodexErrorInfo::HttpConnectionFailed { http_status_code } + } + CoreCodexErrorInfo::ResponseStreamConnectionFailed { http_status_code } => { + CodexErrorInfo::ResponseStreamConnectionFailed { http_status_code } + } + CoreCodexErrorInfo::InternalServerError => CodexErrorInfo::InternalServerError, + CoreCodexErrorInfo::Unauthorized => CodexErrorInfo::Unauthorized, + CoreCodexErrorInfo::BadRequest => CodexErrorInfo::BadRequest, + CoreCodexErrorInfo::ThreadRollbackFailed => CodexErrorInfo::ThreadRollbackFailed, + CoreCodexErrorInfo::SandboxError => CodexErrorInfo::SandboxError, + CoreCodexErrorInfo::ResponseStreamDisconnected { http_status_code } => { + CodexErrorInfo::ResponseStreamDisconnected { http_status_code } + } + CoreCodexErrorInfo::ResponseTooManyFailedAttempts { http_status_code } => { + CodexErrorInfo::ResponseTooManyFailedAttempts { http_status_code } + } + CoreCodexErrorInfo::ActiveTurnNotSteerable { turn_kind } => { + CodexErrorInfo::ActiveTurnNotSteerable { + turn_kind: turn_kind.into(), + } + } + CoreCodexErrorInfo::Other => CodexErrorInfo::Other, + } + } +} + +impl From for NonSteerableTurnKind { + fn from(value: CoreNonSteerableTurnKind) -> Self { + match value { + CoreNonSteerableTurnKind::Review => Self::Review, + CoreNonSteerableTurnKind::Compact => Self::Compact, + } + } +} + +#[derive( + Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS, ExperimentalApi, +)] +#[serde(rename_all = "kebab-case")] +#[ts(rename_all = "kebab-case", export_to = "v2/")] +pub enum AskForApproval { + #[serde(rename = "untrusted")] + #[ts(rename = "untrusted")] + UnlessTrusted, + OnFailure, + OnRequest, + #[experimental("askForApproval.granular")] + Granular { + sandbox_approval: bool, + rules: bool, + #[serde(default)] + skill_approval: bool, + #[serde(default)] + request_permissions: bool, + mcp_elicitations: bool, + }, + Never, +} + +impl AskForApproval { + pub fn to_core(self) -> CoreAskForApproval { + match self { + AskForApproval::UnlessTrusted => CoreAskForApproval::UnlessTrusted, + AskForApproval::OnFailure => CoreAskForApproval::OnFailure, + AskForApproval::OnRequest => CoreAskForApproval::OnRequest, + AskForApproval::Granular { + sandbox_approval, + rules, + skill_approval, + request_permissions, + mcp_elicitations, + } => CoreAskForApproval::Granular(CoreGranularApprovalConfig { + sandbox_approval, + rules, + skill_approval, + request_permissions, + mcp_elicitations, + }), + AskForApproval::Never => CoreAskForApproval::Never, + } + } +} + +impl From for AskForApproval { + fn from(value: CoreAskForApproval) -> Self { + match value { + CoreAskForApproval::UnlessTrusted => AskForApproval::UnlessTrusted, + CoreAskForApproval::OnFailure => AskForApproval::OnFailure, + CoreAskForApproval::OnRequest => AskForApproval::OnRequest, + CoreAskForApproval::Granular(granular_config) => AskForApproval::Granular { + sandbox_approval: granular_config.sandbox_approval, + rules: granular_config.rules, + skill_approval: granular_config.skill_approval, + request_permissions: granular_config.request_permissions, + mcp_elicitations: granular_config.mcp_elicitations, + }, + CoreAskForApproval::Never => AskForApproval::Never, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, TS)] +#[ts( + type = r#""user" | "auto_review" | "guardian_subagent""#, + export_to = "v2/" +)] +/// Configures who approval requests are routed to for review. Examples +/// include sandbox escapes, blocked network access, MCP approval prompts, and +/// ARC escalations. Defaults to `user`. `auto_review` uses a carefully +/// prompted subagent to gather relevant context and apply a risk-based +/// decision framework before approving or denying the request. +pub enum ApprovalsReviewer { + #[serde(rename = "user")] + User, + #[serde(rename = "guardian_subagent", alias = "auto_review")] + AutoReview, +} + +impl JsonSchema for ApprovalsReviewer { + fn schema_name() -> String { + "ApprovalsReviewer".to_string() + } + + fn json_schema(_generator: &mut SchemaGenerator) -> Schema { + string_enum_schema_with_description( + &["user", "auto_review", "guardian_subagent"], + "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.", + ) + } +} + +fn string_enum_schema_with_description(values: &[&str], description: &str) -> Schema { + let mut schema = SchemaObject { + instance_type: Some(InstanceType::String.into()), + metadata: Some(Box::new(Metadata { + description: Some(description.to_string()), + ..Default::default() + })), + ..Default::default() + }; + schema.enum_values = Some( + values + .iter() + .map(|value| JsonValue::String((*value).to_string())) + .collect(), + ); + Schema::Object(schema) +} + +impl ApprovalsReviewer { + pub fn to_core(self) -> CoreApprovalsReviewer { + match self { + ApprovalsReviewer::User => CoreApprovalsReviewer::User, + ApprovalsReviewer::AutoReview => CoreApprovalsReviewer::AutoReview, + } + } +} + +impl From for ApprovalsReviewer { + fn from(value: CoreApprovalsReviewer) -> Self { + match value { + CoreApprovalsReviewer::User => ApprovalsReviewer::User, + CoreApprovalsReviewer::AutoReview => ApprovalsReviewer::AutoReview, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "kebab-case")] +#[ts(rename_all = "kebab-case", export_to = "v2/")] +pub enum SandboxMode { + ReadOnly, + WorkspaceWrite, + DangerFullAccess, +} + +impl SandboxMode { + pub fn to_core(self) -> CoreSandboxMode { + match self { + SandboxMode::ReadOnly => CoreSandboxMode::ReadOnly, + SandboxMode::WorkspaceWrite => CoreSandboxMode::WorkspaceWrite, + SandboxMode::DangerFullAccess => CoreSandboxMode::DangerFullAccess, + } + } +} + +impl From for SandboxMode { + fn from(value: CoreSandboxMode) -> Self { + match value { + CoreSandboxMode::ReadOnly => SandboxMode::ReadOnly, + CoreSandboxMode::WorkspaceWrite => SandboxMode::WorkspaceWrite, + CoreSandboxMode::DangerFullAccess => SandboxMode::DangerFullAccess, + } + } +} diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs new file mode 100644 index 0000000000..5314c73b00 --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -0,0 +1,3566 @@ +use super::*; +use codex_protocol::approvals::ElicitationRequest as CoreElicitationRequest; +use codex_protocol::items::AgentMessageContent; +use codex_protocol::items::AgentMessageItem; +use codex_protocol::items::FileChangeItem; +use codex_protocol::items::ImageViewItem; +use codex_protocol::items::McpToolCallItem; +use codex_protocol::items::McpToolCallStatus as CoreMcpToolCallStatus; +use codex_protocol::items::ReasoningItem; +use codex_protocol::items::TurnItem; +use codex_protocol::items::UserMessageItem; +use codex_protocol::items::WebSearchItem; +use codex_protocol::mcp::CallToolResult; +use codex_protocol::memory_citation::MemoryCitation as CoreMemoryCitation; +use codex_protocol::memory_citation::MemoryCitationEntry as CoreMemoryCitationEntry; +use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile; +use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions; +use codex_protocol::models::ManagedFileSystemPermissions as CoreManagedFileSystemPermissions; +use codex_protocol::models::MessagePhase; +use codex_protocol::models::NetworkPermissions as CoreNetworkPermissions; +use codex_protocol::models::WebSearchAction as CoreWebSearchAction; +use codex_protocol::permissions::FileSystemAccessMode as CoreFileSystemAccessMode; +use codex_protocol::permissions::FileSystemPath as CoreFileSystemPath; +use codex_protocol::permissions::FileSystemSandboxEntry as CoreFileSystemSandboxEntry; +use codex_protocol::permissions::FileSystemSpecialPath as CoreFileSystemSpecialPath; +use codex_protocol::protocol::AgentStatus as CoreAgentStatus; +use codex_protocol::protocol::AskForApproval as CoreAskForApproval; +use codex_protocol::protocol::GranularApprovalConfig as CoreGranularApprovalConfig; +use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess; +use codex_protocol::request_permissions::RequestPermissionProfile as CoreRequestPermissionProfile; +use codex_protocol::user_input::UserInput as CoreUserInput; +use codex_utils_absolute_path::AbsolutePathBuf; +use codex_utils_absolute_path::test_support::PathBufExt; +use codex_utils_absolute_path::test_support::test_path_buf; +use pretty_assertions::assert_eq; +use serde_json::Value as JsonValue; +use serde_json::json; +use std::collections::BTreeMap; +use std::collections::HashMap; +use std::num::NonZeroUsize; +use std::path::PathBuf; +use std::time::Duration; + +fn absolute_path_string(path: &str) -> String { + let path = format!("/{}", path.trim_start_matches('/')); + test_path_buf(&path).display().to_string() +} + +fn absolute_path(path: &str) -> AbsolutePathBuf { + let path = format!("/{}", path.trim_start_matches('/')); + test_path_buf(&path).abs() +} + +fn test_absolute_path() -> AbsolutePathBuf { + absolute_path("readable") +} + +#[test] +fn approvals_reviewer_serializes_auto_review_and_accepts_legacy_guardian_subagent() { + assert_eq!( + serde_json::to_string(&ApprovalsReviewer::User).expect("serialize reviewer"), + "\"user\"" + ); + assert_eq!( + serde_json::to_string(&ApprovalsReviewer::AutoReview).expect("serialize reviewer"), + "\"guardian_subagent\"" + ); + + for value in ["user", "auto_review", "guardian_subagent"] { + let json = format!("\"{value}\""); + let reviewer: ApprovalsReviewer = + serde_json::from_str(&json).expect("deserialize reviewer"); + let expected = if value == "user" { + ApprovalsReviewer::User + } else { + ApprovalsReviewer::AutoReview + }; + assert_eq!(expected, reviewer); + } +} + +#[test] +fn turn_defaults_legacy_missing_items_view_to_full() { + let turn: Turn = serde_json::from_value(json!({ + "id": "turn_123", + "items": [], + "status": "completed", + "error": null, + "startedAt": null, + "completedAt": null, + "durationMs": null, + })) + .expect("legacy turn should deserialize"); + + assert_eq!(turn.items_view, TurnItemsView::Full); +} + +#[test] +fn thread_list_params_accepts_single_cwd() { + let params = serde_json::from_value::(json!({ + "cwd": "/workspace", + })) + .expect("single cwd should deserialize"); + + assert_eq!( + params.cwd, + Some(ThreadListCwdFilter::One("/workspace".to_string())) + ); + assert!(!params.use_state_db_only); +} + +#[test] +fn thread_list_params_accepts_multiple_cwds() { + let params = serde_json::from_value::(json!({ + "cwd": ["/workspace", "/other-workspace"], + })) + .expect("cwd array should deserialize"); + + assert_eq!( + params.cwd, + Some(ThreadListCwdFilter::Many(vec![ + "/workspace".to_string(), + "/other-workspace".to_string(), + ])) + ); +} + +#[test] +fn thread_list_params_accepts_state_db_only_flag() { + let params = serde_json::from_value::(json!({ + "useStateDbOnly": true, + })) + .expect("state db only flag should deserialize"); + + assert!(params.use_state_db_only); +} + +#[test] +fn collab_agent_state_maps_interrupted_status() { + assert_eq!( + CollabAgentState::from(CoreAgentStatus::Interrupted), + CollabAgentState { + status: CollabAgentStatus::Interrupted, + message: None, + } + ); +} + +#[test] +fn external_agent_config_plugins_details_round_trip() { + let item: ExternalAgentConfigMigrationItem = serde_json::from_value(json!({ + "itemType": "PLUGINS", + "description": "Install supported plugins from Claude settings", + "cwd": absolute_path_string("repo"), + "details": { + "plugins": [ + { + "marketplaceName": "team-marketplace", + "pluginNames": ["asana"] + } + ] + } + })) + .expect("plugins migration item should deserialize"); + + assert_eq!( + item, + ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::Plugins, + description: "Install supported plugins from Claude settings".to_string(), + cwd: Some(PathBuf::from(absolute_path_string("repo"))), + details: Some(MigrationDetails { + plugins: vec![PluginsMigration { + marketplace_name: "team-marketplace".to_string(), + plugin_names: vec!["asana".to_string()], + }], + ..Default::default() + }), + } + ); +} + +#[test] +fn external_agent_config_import_params_accept_legacy_plugin_details() { + let params: ExternalAgentConfigImportParams = serde_json::from_value(json!({ + "migrationItems": [{ + "itemType": "PLUGINS", + "description": "Install supported plugins from Claude settings", + "cwd": absolute_path_string("repo"), + "details": { + "plugins": [ + { + "marketplaceName": "team-marketplace", + "pluginNames": ["asana"] + } + ] + } + }] + })) + .expect("legacy plugin import params should deserialize"); + + assert_eq!( + params, + ExternalAgentConfigImportParams { + migration_items: vec![ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::Plugins, + description: "Install supported plugins from Claude settings".to_string(), + cwd: Some(PathBuf::from(absolute_path_string("repo"))), + details: Some(MigrationDetails { + plugins: vec![PluginsMigration { + marketplace_name: "team-marketplace".to_string(), + plugin_names: vec!["asana".to_string()], + }], + ..Default::default() + }), + }], + } + ); +} + +#[test] +fn command_execution_request_approval_rejects_relative_additional_permission_paths() { + let err = serde_json::from_value::(json!({ + "threadId": "thr_123", + "turnId": "turn_123", + "itemId": "call_123", + "command": "cat file", + "cwd": absolute_path_string("tmp"), + "commandActions": null, + "reason": null, + "networkApprovalContext": null, + "additionalPermissions": { + "network": null, + "fileSystem": { + "read": ["relative/path"], + "write": null + } + }, + "proposedExecpolicyAmendment": null, + "proposedNetworkPolicyAmendments": null, + "availableDecisions": null + })) + .expect_err("relative additional permission paths should fail"); + assert!( + err.to_string() + .contains("AbsolutePathBuf deserialized without a base path"), + "unexpected error: {err}" + ); +} + +#[test] +fn permissions_request_approval_uses_request_permission_profile() { + let read_only_path = if cfg!(windows) { + r"C:\tmp\read-only" + } else { + "/tmp/read-only" + }; + let read_write_path = if cfg!(windows) { + r"C:\tmp\read-write" + } else { + "/tmp/read-write" + }; + let params = serde_json::from_value::(json!({ + "threadId": "thr_123", + "turnId": "turn_123", + "itemId": "call_123", + "cwd": absolute_path_string("repo"), + "reason": "Select a workspace root", + "permissions": { + "network": { + "enabled": true, + }, + "fileSystem": { + "read": [read_only_path], + "write": [read_write_path], + }, + }, + })) + .expect("permissions request should deserialize"); + + assert_eq!(params.cwd, absolute_path("repo")); + assert_eq!( + params.permissions, + RequestPermissionProfile { + network: Some(AdditionalNetworkPermissions { + enabled: Some(true), + }), + file_system: Some(AdditionalFileSystemPermissions { + read: Some(vec![ + AbsolutePathBuf::try_from(PathBuf::from(read_only_path)) + .expect("path must be absolute"), + ]), + write: Some(vec![ + AbsolutePathBuf::try_from(PathBuf::from(read_write_path)) + .expect("path must be absolute"), + ]), + glob_scan_max_depth: None, + entries: None, + }), + } + ); + + assert_eq!( + CoreRequestPermissionProfile::from(params.permissions), + CoreRequestPermissionProfile { + network: Some(CoreNetworkPermissions { + enabled: Some(true), + }), + file_system: Some(CoreFileSystemPermissions::from_read_write_roots( + Some(vec![ + AbsolutePathBuf::try_from(PathBuf::from(read_only_path)) + .expect("path must be absolute"), + ]), + Some(vec![ + AbsolutePathBuf::try_from(PathBuf::from(read_write_path)) + .expect("path must be absolute"), + ]), + )), + } + ); +} + +#[test] +fn permissions_request_approval_rejects_macos_permissions() { + let err = serde_json::from_value::(json!({ + "threadId": "thr_123", + "turnId": "turn_123", + "itemId": "call_123", + "cwd": absolute_path_string("repo"), + "reason": "Select a workspace root", + "permissions": { + "network": null, + "fileSystem": null, + "macos": { + "preferences": "read_only", + "automations": "none", + "launchServices": false, + "accessibility": false, + "calendar": false, + "reminders": false, + "contacts": "none", + }, + }, + })) + .expect_err("permissions request should reject macos permissions"); + + assert!( + err.to_string().contains("unknown field `macos`"), + "unexpected error: {err}" + ); +} + +#[test] +fn additional_file_system_permissions_preserves_canonical_entries() { + let core_permissions = CoreFileSystemPermissions { + entries: vec![ + CoreFileSystemSandboxEntry { + path: CoreFileSystemPath::Special { + value: CoreFileSystemSpecialPath::Root, + }, + access: CoreFileSystemAccessMode::Write, + }, + CoreFileSystemSandboxEntry { + path: CoreFileSystemPath::GlobPattern { + pattern: "**/*.env".to_string(), + }, + access: CoreFileSystemAccessMode::None, + }, + ], + glob_scan_max_depth: NonZeroUsize::new(2), + }; + + let permissions = AdditionalFileSystemPermissions::from(core_permissions.clone()); + assert_eq!( + permissions, + AdditionalFileSystemPermissions { + read: None, + write: None, + glob_scan_max_depth: NonZeroUsize::new(2), + entries: Some(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Write, + }, + FileSystemSandboxEntry { + path: FileSystemPath::GlobPattern { + pattern: "**/*.env".to_string(), + }, + access: FileSystemAccessMode::None, + }, + ]), + } + ); + assert_eq!( + CoreFileSystemPermissions::from(permissions), + core_permissions + ); +} + +#[test] +fn additional_file_system_permissions_populates_entries_for_legacy_roots() { + let read_only_path = absolute_path("read-only"); + let read_write_path = absolute_path("read-write"); + let core_permissions = CoreFileSystemPermissions::from_read_write_roots( + Some(vec![read_only_path.clone()]), + Some(vec![read_write_path.clone()]), + ); + + let permissions = AdditionalFileSystemPermissions::from(core_permissions.clone()); + + assert_eq!( + permissions, + AdditionalFileSystemPermissions { + read: Some(vec![read_only_path.clone()]), + write: Some(vec![read_write_path.clone()]), + glob_scan_max_depth: None, + entries: Some(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: read_only_path, + }, + access: FileSystemAccessMode::Read, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: read_write_path, + }, + access: FileSystemAccessMode::Write, + }, + ]), + } + ); + assert_eq!( + CoreFileSystemPermissions::from(permissions), + core_permissions + ); +} + +#[test] +fn additional_file_system_permissions_rejects_zero_glob_scan_depth() { + serde_json::from_value::(json!({ + "read": null, + "write": null, + "globScanMaxDepth": 0, + "entries": [], + })) + .expect_err("zero glob scan depth should fail deserialization"); +} + +#[test] +fn permission_profile_file_system_permissions_preserves_glob_scan_depth() { + let core_permissions = CoreManagedFileSystemPermissions::Restricted { + entries: vec![CoreFileSystemSandboxEntry { + path: CoreFileSystemPath::GlobPattern { + pattern: "**/*.env".to_string(), + }, + access: CoreFileSystemAccessMode::None, + }], + glob_scan_max_depth: NonZeroUsize::new(2), + }; + + let permissions = PermissionProfileFileSystemPermissions::from(core_permissions.clone()); + + assert_eq!( + permissions, + PermissionProfileFileSystemPermissions::Restricted { + entries: vec![FileSystemSandboxEntry { + path: FileSystemPath::GlobPattern { + pattern: "**/*.env".to_string(), + }, + access: FileSystemAccessMode::None, + }], + glob_scan_max_depth: NonZeroUsize::new(2), + } + ); + assert_eq!( + CoreManagedFileSystemPermissions::from(permissions), + core_permissions + ); +} + +#[test] +fn permission_profile_file_system_permissions_rejects_zero_glob_scan_depth() { + serde_json::from_value::(json!({ + "type": "restricted", + "entries": [], + "globScanMaxDepth": 0, + })) + .expect_err("zero glob scan depth should fail deserialization"); +} + +#[test] +fn legacy_current_working_directory_special_path_deserializes_as_project_roots() { + let special_path = serde_json::from_value::(json!({ + "kind": "current_working_directory", + })) + .expect("legacy cwd special path should deserialize"); + + assert_eq!( + special_path, + FileSystemSpecialPath::ProjectRoots { subpath: None } + ); + assert_eq!( + serde_json::to_value(&special_path).expect("serialize special path"), + json!({ + "kind": "project_roots", + "subpath": null, + }) + ); +} + +#[test] +fn permissions_request_approval_response_uses_granted_permission_profile_without_macos() { + let read_only_path = if cfg!(windows) { + r"C:\tmp\read-only" + } else { + "/tmp/read-only" + }; + let read_write_path = if cfg!(windows) { + r"C:\tmp\read-write" + } else { + "/tmp/read-write" + }; + let response = serde_json::from_value::(json!({ + "permissions": { + "network": { + "enabled": true, + }, + "fileSystem": { + "read": [read_only_path], + "write": [read_write_path], + }, + }, + })) + .expect("permissions response should deserialize"); + + assert_eq!( + response.permissions, + GrantedPermissionProfile { + network: Some(AdditionalNetworkPermissions { + enabled: Some(true), + }), + file_system: Some(AdditionalFileSystemPermissions { + read: Some(vec![ + AbsolutePathBuf::try_from(PathBuf::from(read_only_path)) + .expect("path must be absolute"), + ]), + write: Some(vec![ + AbsolutePathBuf::try_from(PathBuf::from(read_write_path)) + .expect("path must be absolute"), + ]), + glob_scan_max_depth: None, + entries: None, + }), + } + ); + + assert_eq!( + CoreAdditionalPermissionProfile::from(response.permissions), + CoreAdditionalPermissionProfile { + network: Some(CoreNetworkPermissions { + enabled: Some(true), + }), + file_system: Some(CoreFileSystemPermissions::from_read_write_roots( + Some(vec![ + AbsolutePathBuf::try_from(PathBuf::from(read_only_path)) + .expect("path must be absolute"), + ]), + Some(vec![ + AbsolutePathBuf::try_from(PathBuf::from(read_write_path)) + .expect("path must be absolute"), + ]), + )), + } + ); +} + +#[test] +fn permissions_request_approval_response_defaults_scope_to_turn() { + let response = serde_json::from_value::(json!({ + "permissions": {}, + })) + .expect("response should deserialize"); + + assert_eq!(response.scope, PermissionGrantScope::Turn); + assert_eq!(response.strict_auto_review, None); +} + +#[test] +fn permissions_request_approval_response_accepts_strict_auto_review() { + let response = serde_json::from_value::(json!({ + "permissions": {}, + "strictAutoReview": true, + })) + .expect("response should deserialize"); + + assert_eq!(response.strict_auto_review, Some(true)); +} + +#[test] +fn fs_get_metadata_response_round_trips_minimal_fields() { + let response = FsGetMetadataResponse { + is_directory: false, + is_file: true, + is_symlink: false, + created_at_ms: 123, + modified_at_ms: 456, + }; + + let value = serde_json::to_value(&response).expect("serialize fs/getMetadata response"); + assert_eq!( + value, + json!({ + "isDirectory": false, + "isFile": true, + "isSymlink": false, + "createdAtMs": 123, + "modifiedAtMs": 456, + }) + ); + + let decoded = serde_json::from_value::(value) + .expect("deserialize fs/getMetadata response"); + assert_eq!(decoded, response); +} + +#[test] +fn fs_read_file_response_round_trips_base64_data() { + let response = FsReadFileResponse { + data_base64: "aGVsbG8=".to_string(), + }; + + let value = serde_json::to_value(&response).expect("serialize fs/readFile response"); + assert_eq!( + value, + json!({ + "dataBase64": "aGVsbG8=", + }) + ); + + let decoded = serde_json::from_value::(value) + .expect("deserialize fs/readFile response"); + assert_eq!(decoded, response); +} + +#[test] +fn fs_read_file_params_round_trip() { + let params = FsReadFileParams { + path: absolute_path("tmp/example.txt"), + }; + + let value = serde_json::to_value(¶ms).expect("serialize fs/readFile params"); + assert_eq!( + value, + json!({ + "path": absolute_path_string("tmp/example.txt"), + }) + ); + + let decoded = + serde_json::from_value::(value).expect("deserialize fs/readFile params"); + assert_eq!(decoded, params); +} + +#[test] +fn device_key_create_params_round_trip_uses_protection_policy() { + let params = DeviceKeyCreateParams { + protection_policy: None, + account_user_id: "account-user-1".to_string(), + client_id: "cli_123".to_string(), + }; + + let value = serde_json::to_value(¶ms).expect("serialize device/key/create params"); + assert_eq!( + value, + json!({ + "accountUserId": "account-user-1", + "clientId": "cli_123", + "protectionPolicy": null, + }) + ); + + let decoded = serde_json::from_value::(value) + .expect("deserialize device/key/create params"); + assert_eq!(decoded, params); + + let params = DeviceKeyCreateParams { + protection_policy: Some(DeviceKeyProtectionPolicy::AllowOsProtectedNonextractable), + account_user_id: "account-user-1".to_string(), + client_id: "cli_123".to_string(), + }; + let value = serde_json::to_value(¶ms) + .expect("serialize device/key/create params with protection policy"); + assert_eq!( + value, + json!({ + "accountUserId": "account-user-1", + "clientId": "cli_123", + "protectionPolicy": "allow_os_protected_nonextractable", + }) + ); +} + +#[test] +fn device_key_create_response_round_trips_protection_class() { + let response = DeviceKeyCreateResponse { + key_id: "dk_123".to_string(), + public_key_spki_der_base64: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE".to_string(), + algorithm: DeviceKeyAlgorithm::EcdsaP256Sha256, + protection_class: DeviceKeyProtectionClass::OsProtectedNonextractable, + }; + + let value = serde_json::to_value(&response).expect("serialize device/key/create response"); + assert_eq!( + value, + json!({ + "keyId": "dk_123", + "publicKeySpkiDerBase64": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE", + "algorithm": "ecdsa_p256_sha256", + "protectionClass": "os_protected_nonextractable", + }) + ); + + let decoded = serde_json::from_value::(value) + .expect("deserialize device/key/create response"); + assert_eq!(decoded, response); +} + +#[test] +fn device_key_sign_params_round_trip_uses_accepted_payload_enum() { + let params = DeviceKeySignParams { + key_id: "dk_123".to_string(), + payload: DeviceKeySignPayload::RemoteControlClientConnection { + nonce: "nonce-1".to_string(), + audience: RemoteControlClientConnectionAudience::RemoteControlClientWebsocket, + session_id: "wssess_123".to_string(), + target_origin: "https://chatgpt.com".to_string(), + target_path: "/api/codex/remote/control/client".to_string(), + account_user_id: "account-user-1".to_string(), + client_id: "cli_123".to_string(), + token_sha256_base64url: "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU".to_string(), + token_expires_at: 1_700_000_000, + scopes: vec!["remote_control_controller_websocket".to_string()], + }, + }; + + let value = serde_json::to_value(¶ms).expect("serialize device/key/sign params"); + assert_eq!( + value, + json!({ + "keyId": "dk_123", + "payload": { + "type": "remoteControlClientConnection", + "nonce": "nonce-1", + "audience": "remote_control_client_websocket", + "sessionId": "wssess_123", + "targetOrigin": "https://chatgpt.com", + "targetPath": "/api/codex/remote/control/client", + "accountUserId": "account-user-1", + "clientId": "cli_123", + "tokenSha256Base64url": "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU", + "tokenExpiresAt": 1_700_000_000, + "scopes": ["remote_control_controller_websocket"], + }, + }) + ); + + let decoded = serde_json::from_value::(value) + .expect("deserialize device/key/sign params"); + assert_eq!(decoded, params); +} + +#[test] +fn device_key_sign_params_round_trip_uses_enrollment_payload() { + let params = DeviceKeySignParams { + key_id: "dk_123".to_string(), + payload: DeviceKeySignPayload::RemoteControlClientEnrollment { + nonce: "nonce-1".to_string(), + audience: RemoteControlClientEnrollmentAudience::RemoteControlClientEnrollment, + challenge_id: "rch_123".to_string(), + target_origin: "https://chatgpt.com".to_string(), + target_path: "/wham/remote/control/client/enroll".to_string(), + account_user_id: "account-user-1".to_string(), + client_id: "cli_123".to_string(), + device_identity_sha256_base64url: "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU" + .to_string(), + challenge_expires_at: 1_700_000_000, + }, + }; + + let value = serde_json::to_value(¶ms) + .expect("serialize device/key/sign params with enrollment payload"); + assert_eq!( + value, + json!({ + "keyId": "dk_123", + "payload": { + "type": "remoteControlClientEnrollment", + "nonce": "nonce-1", + "audience": "remote_control_client_enrollment", + "challengeId": "rch_123", + "targetOrigin": "https://chatgpt.com", + "targetPath": "/wham/remote/control/client/enroll", + "accountUserId": "account-user-1", + "clientId": "cli_123", + "deviceIdentitySha256Base64url": "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU", + "challengeExpiresAt": 1_700_000_000, + }, + }) + ); + + let decoded = serde_json::from_value::(value) + .expect("deserialize device/key/sign params with enrollment payload"); + assert_eq!(decoded, params); +} + +#[test] +fn device_key_sign_response_returns_signed_payload_bytes() { + let response = DeviceKeySignResponse { + signature_der_base64: "MEUCIQD".to_string(), + signed_payload_base64: "eyJkb21haW4iOiJjb2RleA".to_string(), + algorithm: DeviceKeyAlgorithm::EcdsaP256Sha256, + }; + + let value = serde_json::to_value(&response).expect("serialize device/key/sign response"); + assert_eq!( + value, + json!({ + "signatureDerBase64": "MEUCIQD", + "signedPayloadBase64": "eyJkb21haW4iOiJjb2RleA", + "algorithm": "ecdsa_p256_sha256", + }) + ); + + let decoded = serde_json::from_value::(value) + .expect("deserialize device/key/sign response"); + assert_eq!(decoded, response); +} + +#[test] +fn fs_create_directory_params_round_trip_with_default_recursive() { + let params = FsCreateDirectoryParams { + path: absolute_path("tmp/example"), + recursive: None, + }; + + let value = serde_json::to_value(¶ms).expect("serialize fs/createDirectory params"); + assert_eq!( + value, + json!({ + "path": absolute_path_string("tmp/example"), + "recursive": null, + }) + ); + + let decoded = serde_json::from_value::(value) + .expect("deserialize fs/createDirectory params"); + assert_eq!(decoded, params); +} + +#[test] +fn fs_write_file_params_round_trip_with_base64_data() { + let params = FsWriteFileParams { + path: absolute_path("tmp/example.bin"), + data_base64: "AAE=".to_string(), + }; + + let value = serde_json::to_value(¶ms).expect("serialize fs/writeFile params"); + assert_eq!( + value, + json!({ + "path": absolute_path_string("tmp/example.bin"), + "dataBase64": "AAE=", + }) + ); + + let decoded = serde_json::from_value::(value) + .expect("deserialize fs/writeFile params"); + assert_eq!(decoded, params); +} + +#[test] +fn fs_copy_params_round_trip_with_recursive_directory_copy() { + let params = FsCopyParams { + source_path: absolute_path("tmp/source"), + destination_path: absolute_path("tmp/destination"), + recursive: true, + }; + + let value = serde_json::to_value(¶ms).expect("serialize fs/copy params"); + assert_eq!( + value, + json!({ + "sourcePath": absolute_path_string("tmp/source"), + "destinationPath": absolute_path_string("tmp/destination"), + "recursive": true, + }) + ); + + let decoded = + serde_json::from_value::(value).expect("deserialize fs/copy params"); + assert_eq!(decoded, params); +} + +#[test] +fn thread_shell_command_params_round_trip() { + let params = ThreadShellCommandParams { + thread_id: "thr_123".to_string(), + command: "printf 'hello world\\n'".to_string(), + }; + + let value = serde_json::to_value(¶ms).expect("serialize thread/shellCommand params"); + assert_eq!( + value, + json!({ + "threadId": "thr_123", + "command": "printf 'hello world\\n'", + }) + ); + + let decoded = serde_json::from_value::(value) + .expect("deserialize thread/shellCommand params"); + assert_eq!(decoded, params); +} + +#[test] +fn thread_shell_command_response_round_trip() { + let response = ThreadShellCommandResponse {}; + + let value = serde_json::to_value(&response).expect("serialize thread/shellCommand response"); + assert_eq!(value, json!({})); + + let decoded = serde_json::from_value::(value) + .expect("deserialize thread/shellCommand response"); + assert_eq!(decoded, response); +} + +#[test] +fn fs_changed_notification_round_trips() { + let notification = FsChangedNotification { + watch_id: "0195ec6b-1d6f-7c2e-8c7a-56f2c4a8b9d1".to_string(), + changed_paths: vec![ + absolute_path("tmp/repo/.git/HEAD"), + absolute_path("tmp/repo/.git/FETCH_HEAD"), + ], + }; + + let value = serde_json::to_value(¬ification).expect("serialize fs/changed notification"); + assert_eq!( + value, + json!({ + "watchId": "0195ec6b-1d6f-7c2e-8c7a-56f2c4a8b9d1", + "changedPaths": [ + absolute_path_string("tmp/repo/.git/HEAD"), + absolute_path_string("tmp/repo/.git/FETCH_HEAD"), + ], + }) + ); + + let decoded = serde_json::from_value::(value) + .expect("deserialize fs/changed notification"); + assert_eq!(decoded, notification); +} + +#[test] +fn command_exec_params_default_optional_streaming_flags() { + let params = serde_json::from_value::(json!({ + "command": ["ls", "-la"], + "timeoutMs": 1000, + "cwd": "/tmp" + })) + .expect("command/exec payload should deserialize"); + + assert_eq!( + params, + CommandExecParams { + command: vec!["ls".to_string(), "-la".to_string()], + process_id: None, + tty: false, + stream_stdin: false, + stream_stdout_stderr: false, + output_bytes_cap: None, + disable_output_cap: false, + disable_timeout: false, + timeout_ms: Some(1000), + cwd: Some(PathBuf::from("/tmp")), + env: None, + size: None, + sandbox_policy: None, + permission_profile: None, + } + ); +} + +#[test] +fn command_exec_params_round_trips_disable_timeout() { + let params = CommandExecParams { + command: vec!["sleep".to_string(), "30".to_string()], + process_id: Some("sleep-1".to_string()), + tty: false, + stream_stdin: false, + stream_stdout_stderr: false, + output_bytes_cap: None, + disable_output_cap: false, + disable_timeout: true, + timeout_ms: None, + cwd: None, + env: None, + size: None, + sandbox_policy: None, + permission_profile: None, + }; + + let value = serde_json::to_value(¶ms).expect("serialize command/exec params"); + assert_eq!( + value, + json!({ + "command": ["sleep", "30"], + "processId": "sleep-1", + "disableTimeout": true, + "timeoutMs": null, + "cwd": null, + "env": null, + "size": null, + "sandboxPolicy": null, + "permissionProfile": null, + "outputBytesCap": null, + }) + ); + + let decoded = + serde_json::from_value::(value).expect("deserialize round-trip"); + assert_eq!(decoded, params); +} + +#[test] +fn process_spawn_params_round_trips_without_sandbox_policy() { + let params = ProcessSpawnParams { + command: vec!["sleep".to_string(), "30".to_string()], + process_handle: "sleep-1".to_string(), + cwd: test_absolute_path(), + tty: false, + stream_stdin: false, + stream_stdout_stderr: false, + output_bytes_cap: None, + timeout_ms: None, + env: None, + size: None, + }; + + let value = serde_json::to_value(¶ms).expect("serialize process/spawn params"); + assert_eq!( + value, + json!({ + "command": ["sleep", "30"], + "processHandle": "sleep-1", + "cwd": absolute_path_string("readable"), + "env": null, + "size": null, + }) + ); + + let decoded = + serde_json::from_value::(value).expect("deserialize round-trip"); + assert_eq!(decoded, params); +} + +#[test] +fn process_spawn_params_distinguish_omitted_null_and_value_limits() { + let base = json!({ + "command": ["sleep", "30"], + "processHandle": "sleep-1", + "cwd": absolute_path_string("readable"), + }); + + let expected_omitted = ProcessSpawnParams { + command: vec!["sleep".to_string(), "30".to_string()], + process_handle: "sleep-1".to_string(), + cwd: test_absolute_path(), + tty: false, + stream_stdin: false, + stream_stdout_stderr: false, + output_bytes_cap: None, + timeout_ms: None, + env: None, + size: None, + }; + let decoded = + serde_json::from_value::(base).expect("deserialize omitted limits"); + assert_eq!(decoded, expected_omitted); + + let decoded = serde_json::from_value::(json!({ + "command": ["sleep", "30"], + "processHandle": "sleep-1", + "cwd": absolute_path_string("readable"), + "outputBytesCap": null, + "timeoutMs": null, + })) + .expect("deserialize disabled limits"); + assert_eq!( + decoded, + ProcessSpawnParams { + output_bytes_cap: Some(None), + timeout_ms: Some(None), + ..expected_omitted.clone() + } + ); + + let decoded = serde_json::from_value::(json!({ + "command": ["sleep", "30"], + "processHandle": "sleep-1", + "cwd": absolute_path_string("readable"), + "outputBytesCap": 123, + "timeoutMs": 456, + })) + .expect("deserialize explicit limits"); + assert_eq!( + decoded, + ProcessSpawnParams { + output_bytes_cap: Some(Some(123)), + timeout_ms: Some(Some(456)), + ..expected_omitted + } + ); +} + +#[test] +fn command_exec_params_round_trips_disable_output_cap() { + let params = CommandExecParams { + command: vec!["yes".to_string()], + process_id: Some("yes-1".to_string()), + tty: false, + stream_stdin: false, + stream_stdout_stderr: true, + output_bytes_cap: None, + disable_output_cap: true, + disable_timeout: false, + timeout_ms: None, + cwd: None, + env: None, + size: None, + sandbox_policy: None, + permission_profile: None, + }; + + let value = serde_json::to_value(¶ms).expect("serialize command/exec params"); + assert_eq!( + value, + json!({ + "command": ["yes"], + "processId": "yes-1", + "streamStdoutStderr": true, + "outputBytesCap": null, + "disableOutputCap": true, + "timeoutMs": null, + "cwd": null, + "env": null, + "size": null, + "sandboxPolicy": null, + "permissionProfile": null, + }) + ); + + let decoded = + serde_json::from_value::(value).expect("deserialize round-trip"); + assert_eq!(decoded, params); +} + +#[test] +fn command_exec_params_round_trips_env_overrides_and_unsets() { + let params = CommandExecParams { + command: vec!["printenv".to_string(), "FOO".to_string()], + process_id: Some("env-1".to_string()), + tty: false, + stream_stdin: false, + stream_stdout_stderr: false, + output_bytes_cap: None, + disable_output_cap: false, + disable_timeout: false, + timeout_ms: None, + cwd: None, + env: Some(HashMap::from([ + ("FOO".to_string(), Some("override".to_string())), + ("BAR".to_string(), Some("added".to_string())), + ("BAZ".to_string(), None), + ])), + size: None, + sandbox_policy: None, + permission_profile: None, + }; + + let value = serde_json::to_value(¶ms).expect("serialize command/exec params"); + assert_eq!( + value, + json!({ + "command": ["printenv", "FOO"], + "processId": "env-1", + "outputBytesCap": null, + "timeoutMs": null, + "cwd": null, + "env": { + "FOO": "override", + "BAR": "added", + "BAZ": null, + }, + "size": null, + "sandboxPolicy": null, + "permissionProfile": null, + }) + ); + + let decoded = + serde_json::from_value::(value).expect("deserialize round-trip"); + assert_eq!(decoded, params); +} + +#[test] +fn command_exec_write_round_trips_close_only_payload() { + let params = CommandExecWriteParams { + process_id: "proc-7".to_string(), + delta_base64: None, + close_stdin: true, + }; + + let value = serde_json::to_value(¶ms).expect("serialize command/exec/write params"); + assert_eq!( + value, + json!({ + "processId": "proc-7", + "deltaBase64": null, + "closeStdin": true, + }) + ); + + let decoded = + serde_json::from_value::(value).expect("deserialize round-trip"); + assert_eq!(decoded, params); +} + +#[test] +fn command_exec_terminate_round_trips() { + let params = CommandExecTerminateParams { + process_id: "proc-8".to_string(), + }; + + let value = serde_json::to_value(¶ms).expect("serialize command/exec/terminate params"); + assert_eq!( + value, + json!({ + "processId": "proc-8", + }) + ); + + let decoded = serde_json::from_value::(value) + .expect("deserialize round-trip"); + assert_eq!(decoded, params); +} + +#[test] +fn command_exec_params_round_trip_with_size() { + let params = CommandExecParams { + command: vec!["top".to_string()], + process_id: Some("pty-1".to_string()), + tty: true, + stream_stdin: false, + stream_stdout_stderr: false, + output_bytes_cap: None, + disable_output_cap: false, + disable_timeout: false, + timeout_ms: None, + cwd: None, + env: None, + size: Some(CommandExecTerminalSize { + rows: 40, + cols: 120, + }), + sandbox_policy: None, + permission_profile: None, + }; + + let value = serde_json::to_value(¶ms).expect("serialize command/exec params"); + assert_eq!( + value, + json!({ + "command": ["top"], + "processId": "pty-1", + "tty": true, + "outputBytesCap": null, + "timeoutMs": null, + "cwd": null, + "env": null, + "size": { + "rows": 40, + "cols": 120, + }, + "sandboxPolicy": null, + "permissionProfile": null, + }) + ); + + let decoded = + serde_json::from_value::(value).expect("deserialize round-trip"); + assert_eq!(decoded, params); +} + +#[test] +fn command_exec_resize_round_trips() { + let params = CommandExecResizeParams { + process_id: "proc-9".to_string(), + size: CommandExecTerminalSize { + rows: 50, + cols: 160, + }, + }; + + let value = serde_json::to_value(¶ms).expect("serialize command/exec/resize params"); + assert_eq!( + value, + json!({ + "processId": "proc-9", + "size": { + "rows": 50, + "cols": 160, + }, + }) + ); + + let decoded = + serde_json::from_value::(value).expect("deserialize round-trip"); + assert_eq!(decoded, params); +} + +#[test] +fn command_exec_output_delta_round_trips() { + let notification = CommandExecOutputDeltaNotification { + process_id: "proc-1".to_string(), + stream: CommandExecOutputStream::Stdout, + delta_base64: "AQI=".to_string(), + cap_reached: false, + }; + + let value = serde_json::to_value(¬ification) + .expect("serialize command/exec/outputDelta notification"); + assert_eq!( + value, + json!({ + "processId": "proc-1", + "stream": "stdout", + "deltaBase64": "AQI=", + "capReached": false, + }) + ); + + let decoded = serde_json::from_value::(value) + .expect("deserialize round-trip"); + assert_eq!(decoded, notification); +} + +#[test] +fn process_control_params_round_trip() { + let write = ProcessWriteStdinParams { + process_handle: "proc-7".to_string(), + delta_base64: None, + close_stdin: true, + }; + let value = serde_json::to_value(&write).expect("serialize process/writeStdin params"); + assert_eq!( + value, + json!({ + "processHandle": "proc-7", + "deltaBase64": null, + "closeStdin": true, + }) + ); + let decoded = serde_json::from_value::(value) + .expect("deserialize process/writeStdin params"); + assert_eq!(decoded, write); + + let resize = ProcessResizePtyParams { + process_handle: "proc-7".to_string(), + size: ProcessTerminalSize { + rows: 50, + cols: 160, + }, + }; + let value = serde_json::to_value(&resize).expect("serialize process/resizePty params"); + assert_eq!( + value, + json!({ + "processHandle": "proc-7", + "size": { + "rows": 50, + "cols": 160, + }, + }) + ); + let decoded = serde_json::from_value::(value) + .expect("deserialize process/resizePty params"); + assert_eq!(decoded, resize); + + let kill = ProcessKillParams { + process_handle: "proc-7".to_string(), + }; + let value = serde_json::to_value(&kill).expect("serialize process/kill params"); + assert_eq!( + value, + json!({ + "processHandle": "proc-7", + }) + ); + let decoded = + serde_json::from_value::(value).expect("deserialize process/kill"); + assert_eq!(decoded, kill); +} + +#[test] +fn process_notifications_round_trip() { + let delta = ProcessOutputDeltaNotification { + process_handle: "proc-1".to_string(), + stream: ProcessOutputStream::Stdout, + delta_base64: "AQI=".to_string(), + cap_reached: false, + }; + let value = serde_json::to_value(&delta).expect("serialize process/outputDelta"); + assert_eq!( + value, + json!({ + "processHandle": "proc-1", + "stream": "stdout", + "deltaBase64": "AQI=", + "capReached": false, + }) + ); + let decoded = serde_json::from_value::(value) + .expect("deserialize process/outputDelta"); + assert_eq!(decoded, delta); + + let exited = ProcessExitedNotification { + process_handle: "proc-1".to_string(), + exit_code: 0, + stdout: "out".to_string(), + stdout_cap_reached: false, + stderr: "err".to_string(), + stderr_cap_reached: true, + }; + let value = serde_json::to_value(&exited).expect("serialize process/exited"); + assert_eq!( + value, + json!({ + "processHandle": "proc-1", + "exitCode": 0, + "stdout": "out", + "stdoutCapReached": false, + "stderr": "err", + "stderrCapReached": true, + }) + ); + let decoded = serde_json::from_value::(value) + .expect("deserialize process/exited"); + assert_eq!(decoded, exited); +} + +#[test] +fn command_execution_output_delta_round_trips() { + let notification = CommandExecutionOutputDeltaNotification { + thread_id: "thread-1".to_string(), + turn_id: "turn-1".to_string(), + item_id: "item-1".to_string(), + delta: "\u{fffd}a\n".to_string(), + }; + + let value = serde_json::to_value(¬ification) + .expect("serialize item/commandExecution/outputDelta notification"); + assert_eq!( + value, + json!({ + "threadId": "thread-1", + "turnId": "turn-1", + "itemId": "item-1", + "delta": "\u{fffd}a\n", + }) + ); + + let decoded = serde_json::from_value::(value) + .expect("deserialize round-trip"); + assert_eq!(decoded, notification); +} + +#[test] +fn sandbox_policy_round_trips_external_sandbox_network_access() { + let v2_policy = SandboxPolicy::ExternalSandbox { + network_access: NetworkAccess::Enabled, + }; + + let core_policy = v2_policy.to_core(); + assert_eq!( + core_policy, + codex_protocol::protocol::SandboxPolicy::ExternalSandbox { + network_access: CoreNetworkAccess::Enabled, + } + ); + + let back_to_v2 = SandboxPolicy::from(core_policy); + assert_eq!(back_to_v2, v2_policy); +} + +#[test] +fn sandbox_policy_round_trips_read_only_network_access() { + let v2_policy = SandboxPolicy::ReadOnly { + network_access: true, + }; + + let core_policy = v2_policy.to_core(); + assert_eq!( + core_policy, + codex_protocol::protocol::SandboxPolicy::ReadOnly { + network_access: true, + } + ); + + let back_to_v2 = SandboxPolicy::from(core_policy); + assert_eq!(back_to_v2, v2_policy); +} + +#[test] +fn ask_for_approval_granular_round_trips_request_permissions_flag() { + let v2_policy = AskForApproval::Granular { + sandbox_approval: true, + rules: false, + skill_approval: false, + request_permissions: true, + mcp_elicitations: false, + }; + + let core_policy = v2_policy.to_core(); + assert_eq!( + core_policy, + CoreAskForApproval::Granular(CoreGranularApprovalConfig { + sandbox_approval: true, + rules: false, + skill_approval: false, + request_permissions: true, + mcp_elicitations: false, + }) + ); + + let back_to_v2 = AskForApproval::from(core_policy); + assert_eq!(back_to_v2, v2_policy); +} + +#[test] +fn ask_for_approval_granular_defaults_missing_optional_flags_to_false() { + let decoded = serde_json::from_value::(serde_json::json!({ + "granular": { + "sandbox_approval": true, + "rules": false, + "mcp_elicitations": true, + } + })) + .expect("granular approval policy should deserialize"); + + assert_eq!( + decoded, + AskForApproval::Granular { + sandbox_approval: true, + rules: false, + skill_approval: false, + request_permissions: false, + mcp_elicitations: true, + } + ); +} + +#[test] +fn ask_for_approval_granular_is_marked_experimental() { + let reason = + crate::experimental_api::ExperimentalApi::experimental_reason(&AskForApproval::Granular { + sandbox_approval: true, + rules: false, + skill_approval: false, + request_permissions: false, + mcp_elicitations: true, + }); + + assert_eq!(reason, Some("askForApproval.granular")); + assert_eq!( + crate::experimental_api::ExperimentalApi::experimental_reason(&AskForApproval::OnRequest,), + None + ); +} + +#[test] +fn profile_v2_granular_approval_policy_is_marked_experimental() { + let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&ProfileV2 { + model: None, + model_provider: None, + approval_policy: Some(AskForApproval::Granular { + sandbox_approval: true, + rules: false, + skill_approval: false, + request_permissions: true, + mcp_elicitations: false, + }), + approvals_reviewer: None, + service_tier: None, + model_reasoning_effort: None, + model_reasoning_summary: None, + model_verbosity: None, + web_search: None, + tools: None, + chatgpt_base_url: None, + additional: HashMap::new(), + }); + + assert_eq!(reason, Some("askForApproval.granular")); +} + +#[test] +fn config_granular_approval_policy_is_marked_experimental() { + let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&Config { + model: None, + review_model: None, + model_context_window: None, + model_auto_compact_token_limit: None, + model_provider: None, + approval_policy: Some(AskForApproval::Granular { + sandbox_approval: false, + rules: true, + skill_approval: false, + request_permissions: false, + mcp_elicitations: true, + }), + approvals_reviewer: None, + sandbox_mode: None, + sandbox_workspace_write: None, + forced_chatgpt_workspace_id: None, + forced_login_method: None, + web_search: None, + tools: None, + profile: None, + profiles: HashMap::new(), + instructions: None, + developer_instructions: None, + compact_prompt: None, + model_reasoning_effort: None, + model_reasoning_summary: None, + model_verbosity: None, + service_tier: None, + analytics: None, + apps: None, + additional: HashMap::new(), + }); + + assert_eq!(reason, Some("askForApproval.granular")); +} + +#[test] +fn config_approvals_reviewer_is_marked_experimental() { + let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&Config { + model: None, + review_model: None, + model_context_window: None, + model_auto_compact_token_limit: None, + model_provider: None, + approval_policy: None, + approvals_reviewer: Some(ApprovalsReviewer::AutoReview), + sandbox_mode: None, + sandbox_workspace_write: None, + forced_chatgpt_workspace_id: None, + forced_login_method: None, + web_search: None, + tools: None, + profile: None, + profiles: HashMap::new(), + instructions: None, + developer_instructions: None, + compact_prompt: None, + model_reasoning_effort: None, + model_reasoning_summary: None, + model_verbosity: None, + service_tier: None, + analytics: None, + apps: None, + additional: HashMap::new(), + }); + + assert_eq!(reason, Some("config/read.approvalsReviewer")); +} + +#[test] +fn config_nested_profile_granular_approval_policy_is_marked_experimental() { + let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&Config { + model: None, + review_model: None, + model_context_window: None, + model_auto_compact_token_limit: None, + model_provider: None, + approval_policy: None, + approvals_reviewer: None, + sandbox_mode: None, + sandbox_workspace_write: None, + forced_chatgpt_workspace_id: None, + forced_login_method: None, + web_search: None, + tools: None, + profile: None, + profiles: HashMap::from([( + "default".to_string(), + ProfileV2 { + model: None, + model_provider: None, + approval_policy: Some(AskForApproval::Granular { + sandbox_approval: true, + rules: false, + skill_approval: false, + request_permissions: false, + mcp_elicitations: true, + }), + approvals_reviewer: None, + service_tier: None, + model_reasoning_effort: None, + model_reasoning_summary: None, + model_verbosity: None, + web_search: None, + tools: None, + chatgpt_base_url: None, + additional: HashMap::new(), + }, + )]), + instructions: None, + developer_instructions: None, + compact_prompt: None, + model_reasoning_effort: None, + model_reasoning_summary: None, + model_verbosity: None, + service_tier: None, + analytics: None, + apps: None, + additional: HashMap::new(), + }); + + assert_eq!(reason, Some("askForApproval.granular")); +} + +#[test] +fn config_nested_profile_approvals_reviewer_is_marked_experimental() { + let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&Config { + model: None, + review_model: None, + model_context_window: None, + model_auto_compact_token_limit: None, + model_provider: None, + approval_policy: None, + approvals_reviewer: None, + sandbox_mode: None, + sandbox_workspace_write: None, + forced_chatgpt_workspace_id: None, + forced_login_method: None, + web_search: None, + tools: None, + profile: None, + profiles: HashMap::from([( + "default".to_string(), + ProfileV2 { + model: None, + model_provider: None, + approval_policy: None, + approvals_reviewer: Some(ApprovalsReviewer::AutoReview), + service_tier: None, + model_reasoning_effort: None, + model_reasoning_summary: None, + model_verbosity: None, + web_search: None, + tools: None, + chatgpt_base_url: None, + additional: HashMap::new(), + }, + )]), + instructions: None, + developer_instructions: None, + compact_prompt: None, + model_reasoning_effort: None, + model_reasoning_summary: None, + model_verbosity: None, + service_tier: None, + analytics: None, + apps: None, + additional: HashMap::new(), + }); + + assert_eq!(reason, Some("config/read.approvalsReviewer")); +} + +#[test] +fn config_requirements_granular_allowed_approval_policy_is_marked_experimental() { + let reason = + crate::experimental_api::ExperimentalApi::experimental_reason(&ConfigRequirements { + allowed_approval_policies: Some(vec![AskForApproval::Granular { + sandbox_approval: true, + rules: true, + skill_approval: false, + request_permissions: false, + mcp_elicitations: false, + }]), + allowed_approvals_reviewers: None, + allowed_sandbox_modes: None, + allowed_web_search_modes: None, + feature_requirements: None, + hooks: None, + enforce_residency: None, + network: None, + }); + + assert_eq!(reason, Some("askForApproval.granular")); +} + +#[test] +fn client_request_thread_start_granular_approval_policy_is_marked_experimental() { + let reason = crate::experimental_api::ExperimentalApi::experimental_reason( + &crate::ClientRequest::ThreadStart { + request_id: crate::RequestId::Integer(1), + params: ThreadStartParams { + approval_policy: Some(AskForApproval::Granular { + sandbox_approval: true, + rules: false, + skill_approval: false, + request_permissions: true, + mcp_elicitations: false, + }), + ..Default::default() + }, + }, + ); + + assert_eq!(reason, Some("askForApproval.granular")); +} + +#[test] +fn client_request_thread_resume_granular_approval_policy_is_marked_experimental() { + let reason = crate::experimental_api::ExperimentalApi::experimental_reason( + &crate::ClientRequest::ThreadResume { + request_id: crate::RequestId::Integer(2), + params: ThreadResumeParams { + thread_id: "thr_123".to_string(), + approval_policy: Some(AskForApproval::Granular { + sandbox_approval: false, + rules: true, + skill_approval: false, + request_permissions: false, + mcp_elicitations: true, + }), + ..Default::default() + }, + }, + ); + + assert_eq!(reason, Some("askForApproval.granular")); +} + +#[test] +fn client_request_thread_fork_granular_approval_policy_is_marked_experimental() { + let reason = crate::experimental_api::ExperimentalApi::experimental_reason( + &crate::ClientRequest::ThreadFork { + request_id: crate::RequestId::Integer(3), + params: ThreadForkParams { + thread_id: "thr_456".to_string(), + approval_policy: Some(AskForApproval::Granular { + sandbox_approval: true, + rules: false, + skill_approval: false, + request_permissions: false, + mcp_elicitations: true, + }), + ..Default::default() + }, + }, + ); + + assert_eq!(reason, Some("askForApproval.granular")); +} + +#[test] +fn client_request_turn_start_granular_approval_policy_is_marked_experimental() { + let reason = crate::experimental_api::ExperimentalApi::experimental_reason( + &crate::ClientRequest::TurnStart { + request_id: crate::RequestId::Integer(4), + params: TurnStartParams { + thread_id: "thr_123".to_string(), + input: Vec::new(), + approval_policy: Some(AskForApproval::Granular { + sandbox_approval: false, + rules: true, + skill_approval: false, + request_permissions: false, + mcp_elicitations: true, + }), + ..Default::default() + }, + }, + ); + + assert_eq!(reason, Some("askForApproval.granular")); +} + +#[test] +fn mcp_server_elicitation_response_round_trips_rmcp_result() { + let rmcp_result = rmcp::model::CreateElicitationResult { + action: rmcp::model::ElicitationAction::Accept, + content: Some(json!({ + "confirmed": true, + })), + }; + + let v2_response = McpServerElicitationRequestResponse::from(rmcp_result.clone()); + assert_eq!( + v2_response, + McpServerElicitationRequestResponse { + action: McpServerElicitationAction::Accept, + content: Some(json!({ + "confirmed": true, + })), + meta: None, + } + ); + assert_eq!( + rmcp::model::CreateElicitationResult::from(v2_response), + rmcp_result + ); +} + +#[test] +fn mcp_server_elicitation_request_from_core_url_request() { + let request = McpServerElicitationRequest::try_from(CoreElicitationRequest::Url { + meta: None, + message: "Finish sign-in".to_string(), + url: "https://example.com/complete".to_string(), + elicitation_id: "elicitation-123".to_string(), + }) + .expect("URL request should convert"); + + assert_eq!( + request, + McpServerElicitationRequest::Url { + meta: None, + message: "Finish sign-in".to_string(), + url: "https://example.com/complete".to_string(), + elicitation_id: "elicitation-123".to_string(), + } + ); +} + +#[test] +fn mcp_server_elicitation_request_from_core_form_request() { + let request = McpServerElicitationRequest::try_from(CoreElicitationRequest::Form { + meta: None, + message: "Allow this request?".to_string(), + requested_schema: json!({ + "type": "object", + "properties": { + "confirmed": { + "type": "boolean", + } + }, + "required": ["confirmed"], + }), + }) + .expect("form request should convert"); + + let expected_schema: McpElicitationSchema = serde_json::from_value(json!({ + "type": "object", + "properties": { + "confirmed": { + "type": "boolean", + } + }, + "required": ["confirmed"], + })) + .expect("expected schema should deserialize"); + + assert_eq!( + request, + McpServerElicitationRequest::Form { + meta: None, + message: "Allow this request?".to_string(), + requested_schema: expected_schema, + } + ); +} + +#[test] +fn mcp_elicitation_schema_matches_mcp_2025_11_25_primitives() { + let schema: McpElicitationSchema = serde_json::from_value(json!({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "email": { + "type": "string", + "title": "Email", + "description": "Work email address", + "format": "email", + "default": "dev@example.com", + }, + "count": { + "type": "integer", + "title": "Count", + "description": "How many items to create", + "minimum": 1, + "maximum": 5, + "default": 3, + }, + "confirmed": { + "type": "boolean", + "title": "Confirm", + "description": "Approve the pending action", + "default": true, + }, + "legacyChoice": { + "type": "string", + "title": "Action", + "description": "Legacy titled enum form", + "enum": ["allow", "deny"], + "enumNames": ["Allow", "Deny"], + "default": "allow", + }, + }, + "required": ["email", "confirmed"], + })) + .expect("schema should deserialize"); + + assert_eq!( + schema, + McpElicitationSchema { + schema_uri: Some("https://json-schema.org/draft/2020-12/schema".to_string()), + type_: McpElicitationObjectType::Object, + properties: BTreeMap::from([ + ( + "confirmed".to_string(), + McpElicitationPrimitiveSchema::Boolean(McpElicitationBooleanSchema { + type_: McpElicitationBooleanType::Boolean, + title: Some("Confirm".to_string()), + description: Some("Approve the pending action".to_string()), + default: Some(true), + }), + ), + ( + "count".to_string(), + McpElicitationPrimitiveSchema::Number(McpElicitationNumberSchema { + type_: McpElicitationNumberType::Integer, + title: Some("Count".to_string()), + description: Some("How many items to create".to_string()), + minimum: Some(1.0), + maximum: Some(5.0), + default: Some(3.0), + }), + ), + ( + "email".to_string(), + McpElicitationPrimitiveSchema::String(McpElicitationStringSchema { + type_: McpElicitationStringType::String, + title: Some("Email".to_string()), + description: Some("Work email address".to_string()), + min_length: None, + max_length: None, + format: Some(McpElicitationStringFormat::Email), + default: Some("dev@example.com".to_string()), + }), + ), + ( + "legacyChoice".to_string(), + McpElicitationPrimitiveSchema::Enum(McpElicitationEnumSchema::Legacy( + McpElicitationLegacyTitledEnumSchema { + type_: McpElicitationStringType::String, + title: Some("Action".to_string()), + description: Some("Legacy titled enum form".to_string()), + enum_: vec!["allow".to_string(), "deny".to_string()], + enum_names: Some(vec!["Allow".to_string(), "Deny".to_string(),]), + default: Some("allow".to_string()), + }, + )), + ), + ]), + required: Some(vec!["email".to_string(), "confirmed".to_string()]), + } + ); +} + +#[test] +fn mcp_server_elicitation_request_rejects_null_core_form_schema() { + let result = McpServerElicitationRequest::try_from(CoreElicitationRequest::Form { + meta: Some(json!({ + "persist": "session", + })), + message: "Allow this request?".to_string(), + requested_schema: JsonValue::Null, + }); + + assert!(result.is_err()); +} + +#[test] +fn mcp_server_elicitation_request_rejects_invalid_core_form_schema() { + let result = McpServerElicitationRequest::try_from(CoreElicitationRequest::Form { + meta: None, + message: "Allow this request?".to_string(), + requested_schema: json!({ + "type": "object", + "properties": { + "confirmed": { + "type": "object", + } + }, + }), + }); + + assert!(result.is_err()); +} + +#[test] +fn mcp_server_elicitation_response_serializes_nullable_content() { + let response = McpServerElicitationRequestResponse { + action: McpServerElicitationAction::Decline, + content: None, + meta: None, + }; + + assert_eq!( + serde_json::to_value(response).expect("response should serialize"), + json!({ + "action": "decline", + "content": null, + "_meta": null, + }) + ); +} + +#[test] +fn sandbox_policy_round_trips_workspace_write_access() { + let v2_policy = SandboxPolicy::WorkspaceWrite { + writable_roots: vec![], + network_access: true, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + }; + + let core_policy = v2_policy.to_core(); + assert_eq!( + core_policy, + codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { + writable_roots: vec![], + network_access: true, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + } + ); + + let back_to_v2 = SandboxPolicy::from(core_policy); + assert_eq!(back_to_v2, v2_policy); +} + +#[test] +fn sandbox_policy_deserializes_legacy_read_only_full_access_field() { + let policy = serde_json::from_value::(json!({ + "type": "readOnly", + "access": { + "type": "fullAccess" + }, + "networkAccess": true + })) + .expect("read-only policy should ignore legacy fullAccess field"); + assert_eq!( + policy, + SandboxPolicy::ReadOnly { + network_access: true + } + ); +} + +#[test] +fn sandbox_policy_deserializes_legacy_workspace_write_full_access_field() { + let writable_root = absolute_path("/workspace"); + let policy = serde_json::from_value::(json!({ + "type": "workspaceWrite", + "writableRoots": [writable_root], + "readOnlyAccess": { + "type": "fullAccess" + }, + "networkAccess": true, + "excludeTmpdirEnvVar": true, + "excludeSlashTmp": true + })) + .expect("workspace-write policy should ignore legacy fullAccess field"); + assert_eq!( + policy, + SandboxPolicy::WorkspaceWrite { + writable_roots: vec![absolute_path("/workspace")], + network_access: true, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, + } + ); +} + +#[test] +fn sandbox_policy_rejects_legacy_read_only_restricted_access_field() { + let err = serde_json::from_value::(json!({ + "type": "readOnly", + "access": { + "type": "restricted", + "includePlatformDefaults": false, + "readableRoots": [] + } + })) + .expect_err("read-only policy should reject removed restricted access field"); + assert!(err.to_string().contains("readOnly.access")); +} + +#[test] +fn sandbox_policy_rejects_legacy_workspace_write_restricted_read_access_field() { + let err = serde_json::from_value::(json!({ + "type": "workspaceWrite", + "writableRoots": [], + "readOnlyAccess": { + "type": "restricted", + "includePlatformDefaults": false, + "readableRoots": [] + }, + "networkAccess": false, + "excludeTmpdirEnvVar": false, + "excludeSlashTmp": false + })) + .expect_err("workspace-write policy should reject removed restricted readOnlyAccess field"); + assert!(err.to_string().contains("workspaceWrite.readOnlyAccess")); +} + +#[test] +fn automatic_approval_review_deserializes_aborted_status() { + let review: GuardianApprovalReview = serde_json::from_value(json!({ + "status": "aborted", + "riskLevel": null, + "userAuthorization": null, + "rationale": null + })) + .expect("aborted automatic review should deserialize"); + assert_eq!( + review, + GuardianApprovalReview { + status: GuardianApprovalReviewStatus::Aborted, + risk_level: None, + user_authorization: None, + rationale: None, + } + ); +} + +#[test] +fn guardian_approval_review_action_round_trips_command_shape() { + let value = json!({ + "type": "command", + "source": "shell", + "command": "rm -rf /tmp/example.sqlite", + "cwd": absolute_path_string("tmp"), + }); + let action: GuardianApprovalReviewAction = + serde_json::from_value(value.clone()).expect("guardian review action"); + + assert_eq!( + action, + GuardianApprovalReviewAction::Command { + source: GuardianCommandSource::Shell, + command: "rm -rf /tmp/example.sqlite".to_string(), + cwd: absolute_path("tmp"), + } + ); + assert_eq!( + serde_json::to_value(&action).expect("serialize guardian review action"), + value + ); +} + +#[test] +fn network_requirements_deserializes_legacy_fields() { + let requirements: NetworkRequirements = serde_json::from_value(json!({ + "allowedDomains": ["api.openai.com"], + "deniedDomains": ["blocked.example.com"], + "allowUnixSockets": ["/tmp/proxy.sock"] + })) + .expect("legacy network requirements should deserialize"); + + assert_eq!( + requirements, + NetworkRequirements { + enabled: None, + http_port: None, + socks_port: None, + allow_upstream_proxy: None, + dangerously_allow_non_loopback_proxy: None, + dangerously_allow_all_unix_sockets: None, + domains: None, + managed_allowed_domains_only: None, + allowed_domains: Some(vec!["api.openai.com".to_string()]), + denied_domains: Some(vec!["blocked.example.com".to_string()]), + unix_sockets: None, + allow_unix_sockets: Some(vec!["/tmp/proxy.sock".to_string()]), + allow_local_binding: None, + } + ); +} + +#[test] +fn network_requirements_serializes_canonical_and_legacy_fields() { + let requirements = NetworkRequirements { + enabled: Some(true), + http_port: Some(8080), + socks_port: Some(1080), + allow_upstream_proxy: Some(false), + dangerously_allow_non_loopback_proxy: Some(false), + dangerously_allow_all_unix_sockets: Some(true), + domains: Some(BTreeMap::from([ + ("api.openai.com".to_string(), NetworkDomainPermission::Allow), + ( + "blocked.example.com".to_string(), + NetworkDomainPermission::Deny, + ), + ])), + managed_allowed_domains_only: Some(true), + allowed_domains: Some(vec!["api.openai.com".to_string()]), + denied_domains: Some(vec!["blocked.example.com".to_string()]), + unix_sockets: Some(BTreeMap::from([ + ( + "/tmp/proxy.sock".to_string(), + NetworkUnixSocketPermission::Allow, + ), + ( + "/tmp/ignored.sock".to_string(), + NetworkUnixSocketPermission::None, + ), + ])), + allow_unix_sockets: Some(vec!["/tmp/proxy.sock".to_string()]), + allow_local_binding: Some(true), + }; + + assert_eq!( + serde_json::to_value(requirements).expect("network requirements should serialize"), + json!({ + "enabled": true, + "httpPort": 8080, + "socksPort": 1080, + "allowUpstreamProxy": false, + "dangerouslyAllowNonLoopbackProxy": false, + "dangerouslyAllowAllUnixSockets": true, + "domains": { + "api.openai.com": "allow", + "blocked.example.com": "deny" + }, + "managedAllowedDomainsOnly": true, + "allowedDomains": ["api.openai.com"], + "deniedDomains": ["blocked.example.com"], + "unixSockets": { + "/tmp/ignored.sock": "none", + "/tmp/proxy.sock": "allow" + }, + "allowUnixSockets": ["/tmp/proxy.sock"], + "allowLocalBinding": true + }) + ); +} + +#[test] +fn core_turn_item_into_thread_item_converts_supported_variants() { + let user_item = TurnItem::UserMessage(UserMessageItem { + id: "user-1".to_string(), + content: vec![ + CoreUserInput::Text { + text: "hello".to_string(), + text_elements: Vec::new(), + }, + CoreUserInput::Image { + image_url: "https://example.com/image.png".to_string(), + }, + CoreUserInput::LocalImage { + path: PathBuf::from("local/image.png"), + }, + CoreUserInput::Skill { + name: "skill-creator".to_string(), + path: PathBuf::from("/repo/.codex/skills/skill-creator/SKILL.md"), + }, + CoreUserInput::Mention { + name: "Demo App".to_string(), + path: "app://demo-app".to_string(), + }, + ], + }); + + assert_eq!( + ThreadItem::from(user_item), + ThreadItem::UserMessage { + id: "user-1".to_string(), + content: vec![ + UserInput::Text { + text: "hello".to_string(), + text_elements: Vec::new(), + }, + UserInput::Image { + url: "https://example.com/image.png".to_string(), + }, + UserInput::LocalImage { + path: PathBuf::from("local/image.png"), + }, + UserInput::Skill { + name: "skill-creator".to_string(), + path: PathBuf::from("/repo/.codex/skills/skill-creator/SKILL.md"), + }, + UserInput::Mention { + name: "Demo App".to_string(), + path: "app://demo-app".to_string(), + }, + ], + } + ); + + let agent_item = TurnItem::AgentMessage(AgentMessageItem { + id: "agent-1".to_string(), + content: vec![ + AgentMessageContent::Text { + text: "Hello ".to_string(), + }, + AgentMessageContent::Text { + text: "world".to_string(), + }, + ], + phase: None, + memory_citation: None, + }); + + assert_eq!( + ThreadItem::from(agent_item), + ThreadItem::AgentMessage { + id: "agent-1".to_string(), + text: "Hello world".to_string(), + phase: None, + memory_citation: None, + } + ); + + let agent_item_with_phase = TurnItem::AgentMessage(AgentMessageItem { + id: "agent-2".to_string(), + content: vec![AgentMessageContent::Text { + text: "final".to_string(), + }], + phase: Some(MessagePhase::FinalAnswer), + memory_citation: Some(CoreMemoryCitation { + entries: vec![CoreMemoryCitationEntry { + path: "MEMORY.md".to_string(), + line_start: 1, + line_end: 2, + note: "summary".to_string(), + }], + rollout_ids: vec!["rollout-1".to_string()], + }), + }); + + assert_eq!( + ThreadItem::from(agent_item_with_phase), + ThreadItem::AgentMessage { + id: "agent-2".to_string(), + text: "final".to_string(), + phase: Some(MessagePhase::FinalAnswer), + memory_citation: Some(MemoryCitation { + entries: vec![MemoryCitationEntry { + path: "MEMORY.md".to_string(), + line_start: 1, + line_end: 2, + note: "summary".to_string(), + }], + thread_ids: vec!["rollout-1".to_string()], + }), + } + ); + + let reasoning_item = TurnItem::Reasoning(ReasoningItem { + id: "reasoning-1".to_string(), + summary_text: vec!["line one".to_string(), "line two".to_string()], + raw_content: vec![], + }); + + assert_eq!( + ThreadItem::from(reasoning_item), + ThreadItem::Reasoning { + id: "reasoning-1".to_string(), + summary: vec!["line one".to_string(), "line two".to_string()], + content: vec![], + } + ); + + let search_item = TurnItem::WebSearch(WebSearchItem { + id: "search-1".to_string(), + query: "docs".to_string(), + action: CoreWebSearchAction::Search { + query: Some("docs".to_string()), + queries: None, + }, + }); + + assert_eq!( + ThreadItem::from(search_item), + ThreadItem::WebSearch { + id: "search-1".to_string(), + query: "docs".to_string(), + action: Some(WebSearchAction::Search { + query: Some("docs".to_string()), + queries: None, + }), + } + ); + + let image_view_item = TurnItem::ImageView(ImageViewItem { + id: "view-image-1".to_string(), + path: test_path_buf("/tmp/view-image.png").abs(), + }); + + assert_eq!( + ThreadItem::from(image_view_item), + ThreadItem::ImageView { + id: "view-image-1".to_string(), + path: test_path_buf("/tmp/view-image.png").abs(), + } + ); + + let file_change_item = TurnItem::FileChange(FileChangeItem { + id: "patch-1".to_string(), + changes: [( + PathBuf::from("README.md"), + codex_protocol::protocol::FileChange::Add { + content: "hello\n".to_string(), + }, + )] + .into_iter() + .collect(), + status: Some(codex_protocol::protocol::PatchApplyStatus::Completed), + auto_approved: None, + stdout: Some("Done!".to_string()), + stderr: Some(String::new()), + }); + + assert_eq!( + ThreadItem::from(file_change_item), + ThreadItem::FileChange { + id: "patch-1".to_string(), + changes: vec![FileUpdateChange { + path: "README.md".to_string(), + kind: PatchChangeKind::Add, + diff: "hello\n".to_string(), + }], + status: PatchApplyStatus::Completed, + } + ); + + let mcp_tool_call_item = TurnItem::McpToolCall(McpToolCallItem { + id: "mcp-1".to_string(), + server: "server".to_string(), + tool: "tool".to_string(), + arguments: json!({"arg": "value"}), + mcp_app_resource_uri: Some("app://connector".to_string()), + status: CoreMcpToolCallStatus::InProgress, + result: None, + error: None, + duration: None, + }); + + assert_eq!( + ThreadItem::from(mcp_tool_call_item), + ThreadItem::McpToolCall { + id: "mcp-1".to_string(), + server: "server".to_string(), + tool: "tool".to_string(), + status: McpToolCallStatus::InProgress, + arguments: json!({"arg": "value"}), + mcp_app_resource_uri: Some("app://connector".to_string()), + result: None, + error: None, + duration_ms: None, + } + ); + + let completed_mcp_tool_call_item = TurnItem::McpToolCall(McpToolCallItem { + id: "mcp-2".to_string(), + server: "server".to_string(), + tool: "tool".to_string(), + arguments: JsonValue::Null, + mcp_app_resource_uri: None, + status: CoreMcpToolCallStatus::Completed, + result: Some(CallToolResult { + content: vec![json!({"type": "text", "text": "ok"})], + structured_content: Some(json!({"ok": true})), + is_error: Some(false), + meta: Some(json!({"trace": "1"})), + }), + error: None, + duration: Some(Duration::from_millis(42)), + }); + + assert_eq!( + ThreadItem::from(completed_mcp_tool_call_item), + ThreadItem::McpToolCall { + id: "mcp-2".to_string(), + server: "server".to_string(), + tool: "tool".to_string(), + status: McpToolCallStatus::Completed, + arguments: JsonValue::Null, + mcp_app_resource_uri: None, + result: Some(Box::new(McpToolCallResult { + content: vec![json!({"type": "text", "text": "ok"})], + structured_content: Some(json!({"ok": true})), + meta: Some(json!({"trace": "1"})), + })), + error: None, + duration_ms: Some(42), + } + ); +} + +#[test] +fn skills_list_params_serialization_uses_force_reload() { + assert_eq!( + serde_json::to_value(SkillsListParams { + cwds: Vec::new(), + force_reload: false, + per_cwd_extra_user_roots: None, + }) + .unwrap(), + json!({ + "perCwdExtraUserRoots": null, + }), + ); + + assert_eq!( + serde_json::to_value(SkillsListParams { + cwds: vec![PathBuf::from("/repo")], + force_reload: true, + per_cwd_extra_user_roots: Some(vec![SkillsListExtraRootsForCwd { + cwd: PathBuf::from("/repo"), + extra_user_roots: vec![PathBuf::from("/shared/skills"), PathBuf::from("/tmp/x")], + }]), + }) + .unwrap(), + json!({ + "cwds": ["/repo"], + "forceReload": true, + "perCwdExtraUserRoots": [ + { + "cwd": "/repo", + "extraUserRoots": ["/shared/skills", "/tmp/x"], + } + ], + }), + ); +} + +#[test] +fn plugin_source_serializes_local_git_and_remote_variants() { + let local_path = if cfg!(windows) { + r"C:\plugins\linear" + } else { + "/plugins/linear" + }; + let local_path = AbsolutePathBuf::try_from(PathBuf::from(local_path)).unwrap(); + let local_path_json = local_path.as_path().display().to_string(); + + assert_eq!( + serde_json::to_value(PluginSource::Local { path: local_path }).unwrap(), + json!({ + "type": "local", + "path": local_path_json, + }), + ); + + assert_eq!( + serde_json::to_value(PluginSource::Git { + url: "https://github.com/openai/example.git".to_string(), + path: Some("plugins/example".to_string()), + ref_name: Some("main".to_string()), + sha: Some("abc123".to_string()), + }) + .unwrap(), + json!({ + "type": "git", + "url": "https://github.com/openai/example.git", + "path": "plugins/example", + "refName": "main", + "sha": "abc123", + }), + ); + + assert_eq!( + serde_json::to_value(PluginSource::Remote).unwrap(), + json!({ + "type": "remote", + }), + ); +} + +#[test] +fn marketplace_add_params_serialization_uses_optional_ref_name_and_sparse_paths() { + assert_eq!( + serde_json::to_value(MarketplaceAddParams { + source: "owner/repo".to_string(), + ref_name: None, + sparse_paths: None, + }) + .unwrap(), + json!({ + "source": "owner/repo", + "refName": null, + "sparsePaths": null, + }), + ); + + assert_eq!( + serde_json::to_value(MarketplaceAddParams { + source: "owner/repo".to_string(), + ref_name: Some("main".to_string()), + sparse_paths: Some(vec!["plugins/foo".to_string()]), + }) + .unwrap(), + json!({ + "source": "owner/repo", + "refName": "main", + "sparsePaths": ["plugins/foo"], + }), + ); +} + +#[test] +fn marketplace_upgrade_params_serialization_uses_optional_marketplace_name() { + assert_eq!( + serde_json::to_value(MarketplaceUpgradeParams { + marketplace_name: None, + }) + .unwrap(), + json!({ + "marketplaceName": null, + }), + ); + + assert_eq!( + serde_json::from_value::(json!({})).unwrap(), + MarketplaceUpgradeParams { + marketplace_name: None, + }, + ); + + assert_eq!( + serde_json::to_value(MarketplaceUpgradeParams { + marketplace_name: Some("debug".to_string()), + }) + .unwrap(), + json!({ + "marketplaceName": "debug", + }), + ); +} + +#[test] +fn plugin_marketplace_entry_serializes_remote_only_path_as_null() { + assert_eq!( + serde_json::to_value(PluginMarketplaceEntry { + name: "openai-curated".to_string(), + path: None, + interface: None, + plugins: Vec::new(), + }) + .unwrap(), + json!({ + "name": "openai-curated", + "path": null, + "interface": null, + "plugins": [], + }), + ); +} + +#[test] +fn plugin_interface_serializes_local_paths_and_remote_urls_separately() { + let composer_icon = if cfg!(windows) { + r"C:\plugins\linear\icon.png" + } else { + "/plugins/linear/icon.png" + }; + let composer_icon = AbsolutePathBuf::try_from(PathBuf::from(composer_icon)).unwrap(); + let composer_icon_json = composer_icon.as_path().display().to_string(); + + let interface = PluginInterface { + display_name: Some("Linear".to_string()), + short_description: None, + long_description: None, + developer_name: None, + category: Some("Productivity".to_string()), + capabilities: Vec::new(), + website_url: None, + privacy_policy_url: None, + terms_of_service_url: None, + default_prompt: None, + brand_color: None, + composer_icon: Some(composer_icon), + composer_icon_url: Some("https://example.com/linear/icon.png".to_string()), + logo: None, + logo_url: Some("https://example.com/linear/logo.png".to_string()), + screenshots: Vec::new(), + screenshot_urls: vec!["https://example.com/linear/screenshot.png".to_string()], + }; + + assert_eq!( + serde_json::to_value(interface).unwrap(), + json!({ + "displayName": "Linear", + "shortDescription": null, + "longDescription": null, + "developerName": null, + "category": "Productivity", + "capabilities": [], + "websiteUrl": null, + "privacyPolicyUrl": null, + "termsOfServiceUrl": null, + "defaultPrompt": null, + "brandColor": null, + "composerIcon": composer_icon_json, + "composerIconUrl": "https://example.com/linear/icon.png", + "logo": null, + "logoUrl": "https://example.com/linear/logo.png", + "screenshots": [], + "screenshotUrls": ["https://example.com/linear/screenshot.png"], + }), + ); +} + +#[test] +fn plugin_list_params_ignore_removed_force_remote_sync_field() { + assert_eq!( + serde_json::from_value::(json!({ + "cwds": null, + "forceRemoteSync": true, + })) + .unwrap(), + PluginListParams { cwds: None }, + ); +} + +#[test] +fn plugin_read_params_serialization_uses_install_source_fields() { + let marketplace_path = if cfg!(windows) { + r"C:\plugins\marketplace.json" + } else { + "/plugins/marketplace.json" + }; + let marketplace_path = AbsolutePathBuf::try_from(PathBuf::from(marketplace_path)).unwrap(); + let marketplace_path_json = marketplace_path.as_path().display().to_string(); + assert_eq!( + serde_json::to_value(PluginReadParams { + marketplace_path: Some(marketplace_path.clone()), + remote_marketplace_name: None, + plugin_name: "gmail".to_string(), + }) + .unwrap(), + json!({ + "marketplacePath": marketplace_path_json, + "remoteMarketplaceName": null, + "pluginName": "gmail", + }), + ); + + assert_eq!( + serde_json::from_value::(json!({ + "marketplacePath": marketplace_path_json, + "pluginName": "gmail", + "forceRemoteSync": true, + })) + .unwrap(), + PluginReadParams { + marketplace_path: Some(marketplace_path), + remote_marketplace_name: None, + plugin_name: "gmail".to_string(), + }, + ); + + assert_eq!( + serde_json::from_value::(json!({ + "remoteMarketplaceName": "openai-curated", + "pluginName": "gmail", + })) + .unwrap(), + PluginReadParams { + marketplace_path: None, + remote_marketplace_name: Some("openai-curated".to_string()), + plugin_name: "gmail".to_string(), + }, + ); +} + +#[test] +fn plugin_install_params_serialization_omits_force_remote_sync() { + let marketplace_path = if cfg!(windows) { + r"C:\plugins\marketplace.json" + } else { + "/plugins/marketplace.json" + }; + let marketplace_path = AbsolutePathBuf::try_from(PathBuf::from(marketplace_path)).unwrap(); + let marketplace_path_json = marketplace_path.as_path().display().to_string(); + assert_eq!( + serde_json::to_value(PluginInstallParams { + marketplace_path: Some(marketplace_path.clone()), + remote_marketplace_name: None, + plugin_name: "gmail".to_string(), + }) + .unwrap(), + json!({ + "marketplacePath": marketplace_path_json, + "remoteMarketplaceName": null, + "pluginName": "gmail", + }), + ); + + assert_eq!( + serde_json::from_value::(json!({ + "marketplacePath": marketplace_path_json, + "pluginName": "gmail", + "forceRemoteSync": true, + })) + .unwrap(), + PluginInstallParams { + marketplace_path: Some(marketplace_path), + remote_marketplace_name: None, + plugin_name: "gmail".to_string(), + }, + ); + + assert_eq!( + serde_json::from_value::(json!({ + "remoteMarketplaceName": "openai-curated", + "pluginName": "gmail", + "forceRemoteSync": true, + })) + .unwrap(), + PluginInstallParams { + marketplace_path: None, + remote_marketplace_name: Some("openai-curated".to_string()), + plugin_name: "gmail".to_string(), + }, + ); +} + +#[test] +fn plugin_skill_read_params_serialization_uses_remote_plugin_id() { + assert_eq!( + serde_json::to_value(PluginSkillReadParams { + remote_marketplace_name: "chatgpt-global".to_string(), + remote_plugin_id: "plugins~Plugin_00000000000000000000000000000000".to_string(), + skill_name: "plan-work".to_string(), + }) + .unwrap(), + json!({ + "remoteMarketplaceName": "chatgpt-global", + "remotePluginId": "plugins~Plugin_00000000000000000000000000000000", + "skillName": "plan-work", + }), + ); +} + +#[test] +fn plugin_share_params_and_response_serialization_use_camel_case_fields() { + let plugin_path = if cfg!(windows) { + r"C:\plugins\gmail" + } else { + "/plugins/gmail" + }; + let plugin_path = AbsolutePathBuf::try_from(PathBuf::from(plugin_path)).unwrap(); + let plugin_path_json = plugin_path.as_path().display().to_string(); + + assert_eq!( + serde_json::to_value(PluginShareSaveParams { + plugin_path: plugin_path.clone(), + remote_plugin_id: None, + }) + .unwrap(), + json!({ + "pluginPath": plugin_path_json, + "remotePluginId": null, + }), + ); + + assert_eq!( + serde_json::to_value(PluginShareSaveParams { + plugin_path, + remote_plugin_id: Some("plugins~Plugin_00000000000000000000000000000000".to_string(),), + }) + .unwrap(), + json!({ + "pluginPath": plugin_path_json, + "remotePluginId": "plugins~Plugin_00000000000000000000000000000000", + }), + ); + + assert_eq!( + serde_json::to_value(PluginShareSaveResponse { + remote_plugin_id: "plugins~Plugin_00000000000000000000000000000000".to_string(), + share_url: String::new(), + }) + .unwrap(), + json!({ + "remotePluginId": "plugins~Plugin_00000000000000000000000000000000", + "shareUrl": "", + }), + ); + + assert_eq!( + serde_json::from_value::(json!({})).unwrap(), + PluginShareListParams {}, + ); + + assert_eq!( + serde_json::to_value(PluginShareDeleteParams { + remote_plugin_id: "plugins~Plugin_00000000000000000000000000000000".to_string(), + }) + .unwrap(), + json!({ + "remotePluginId": "plugins~Plugin_00000000000000000000000000000000", + }), + ); +} + +#[test] +fn plugin_share_list_response_serializes_share_items() { + assert_eq!( + serde_json::to_value(PluginShareListResponse { + data: vec![PluginShareListItem { + plugin: PluginSummary { + id: "plugins~Plugin_00000000000000000000000000000000".to_string(), + name: "gmail".to_string(), + source: PluginSource::Remote, + installed: false, + enabled: false, + install_policy: PluginInstallPolicy::Available, + auth_policy: PluginAuthPolicy::OnUse, + availability: PluginAvailability::Available, + interface: None, + }, + share_url: "https://chatgpt.example/plugins/share/share-key-1".to_string(), + local_plugin_path: None, + }], + }) + .unwrap(), + json!({ + "data": [{ + "plugin": { + "id": "plugins~Plugin_00000000000000000000000000000000", + "name": "gmail", + "source": { "type": "remote" }, + "installed": false, + "enabled": false, + "installPolicy": "AVAILABLE", + "authPolicy": "ON_USE", + "availability": "AVAILABLE", + "interface": null, + }, + "shareUrl": "https://chatgpt.example/plugins/share/share-key-1", + "localPluginPath": null, + }], + }), + ); +} + +#[test] +fn plugin_summary_defaults_missing_availability_to_available() { + let summary: PluginSummary = serde_json::from_value(json!({ + "id": "plugins~Plugin_00000000000000000000000000000000", + "name": "gmail", + "source": { "type": "remote" }, + "installed": false, + "enabled": false, + "installPolicy": "AVAILABLE", + "authPolicy": "ON_USE", + "interface": null, + })) + .unwrap(); + + assert_eq!(summary.availability, PluginAvailability::Available); +} + +#[test] +fn plugin_availability_deserializes_enabled_alias() { + let availability: PluginAvailability = serde_json::from_value(json!("ENABLED")).unwrap(); + + assert_eq!(availability, PluginAvailability::Available); + assert_eq!( + serde_json::to_value(availability).unwrap(), + json!("AVAILABLE") + ); +} + +#[test] +fn plugin_uninstall_params_serialization_omits_force_remote_sync() { + assert_eq!( + serde_json::to_value(PluginUninstallParams { + plugin_id: "gmail@openai-curated".to_string(), + }) + .unwrap(), + json!({ + "pluginId": "gmail@openai-curated", + }), + ); + + assert_eq!( + serde_json::from_value::(json!({ + "pluginId": "gmail@openai-curated", + "forceRemoteSync": true, + })) + .unwrap(), + PluginUninstallParams { + plugin_id: "gmail@openai-curated".to_string(), + }, + ); + + assert_eq!( + serde_json::to_value(PluginUninstallParams { + plugin_id: "plugins~Plugin_gmail".to_string(), + }) + .unwrap(), + json!({ + "pluginId": "plugins~Plugin_gmail", + }), + ); + + assert_eq!( + serde_json::from_value::(json!({ + "pluginId": "plugins~Plugin_gmail", + "forceRemoteSync": true, + })) + .unwrap(), + PluginUninstallParams { + plugin_id: "plugins~Plugin_gmail".to_string(), + }, + ); +} + +#[test] +fn marketplace_remove_response_serializes_nullable_installed_root() { + let installed_root = if cfg!(windows) { + r"C:\marketplaces\debug" + } else { + "/tmp/marketplaces/debug" + }; + let installed_root = AbsolutePathBuf::try_from(PathBuf::from(installed_root)).unwrap(); + let installed_root_json = installed_root.as_path().display().to_string(); + assert_eq!( + serde_json::to_value(MarketplaceRemoveResponse { + marketplace_name: "debug".to_string(), + installed_root: Some(installed_root), + }) + .unwrap(), + json!({ + "marketplaceName": "debug", + "installedRoot": installed_root_json, + }), + ); + + assert_eq!( + serde_json::to_value(MarketplaceRemoveResponse { + marketplace_name: "debug".to_string(), + installed_root: None, + }) + .unwrap(), + json!({ + "marketplaceName": "debug", + "installedRoot": null, + }), + ); +} + +#[test] +fn marketplace_upgrade_response_serializes_camel_case_fields() { + let upgraded_root = if cfg!(windows) { + r"C:\marketplaces\debug" + } else { + "/tmp/marketplaces/debug" + }; + let upgraded_root = AbsolutePathBuf::try_from(PathBuf::from(upgraded_root)).unwrap(); + let upgraded_root_json = upgraded_root.as_path().display().to_string(); + + assert_eq!( + serde_json::to_value(MarketplaceUpgradeResponse { + selected_marketplaces: vec!["debug".to_string()], + upgraded_roots: vec![upgraded_root], + errors: vec![MarketplaceUpgradeErrorInfo { + marketplace_name: "broken".to_string(), + message: "failed to clone".to_string(), + }], + }) + .unwrap(), + json!({ + "selectedMarketplaces": ["debug"], + "upgradedRoots": [upgraded_root_json], + "errors": [{ + "marketplaceName": "broken", + "message": "failed to clone", + }], + }), + ); +} + +#[test] +fn codex_error_info_serializes_http_status_code_in_camel_case() { + let value = CodexErrorInfo::ResponseTooManyFailedAttempts { + http_status_code: Some(401), + }; + + assert_eq!( + serde_json::to_value(value).unwrap(), + json!({ + "responseTooManyFailedAttempts": { + "httpStatusCode": 401 + } + }) + ); +} + +#[test] +fn codex_error_info_serializes_cyber_policy_in_camel_case() { + assert_eq!( + serde_json::to_value(CodexErrorInfo::CyberPolicy).unwrap(), + json!("cyberPolicy") + ); +} + +#[test] +fn codex_error_info_serializes_active_turn_not_steerable_turn_kind_in_camel_case() { + let value = CodexErrorInfo::ActiveTurnNotSteerable { + turn_kind: NonSteerableTurnKind::Review, + }; + + assert_eq!( + serde_json::to_value(value).unwrap(), + json!({ + "activeTurnNotSteerable": { + "turnKind": "review" + } + }) + ); +} + +#[test] +fn dynamic_tool_response_serializes_content_items() { + let value = serde_json::to_value(DynamicToolCallResponse { + content_items: vec![DynamicToolCallOutputContentItem::InputText { + text: "dynamic-ok".to_string(), + }], + success: true, + }) + .unwrap(); + + assert_eq!( + value, + json!({ + "contentItems": [ + { + "type": "inputText", + "text": "dynamic-ok" + } + ], + "success": true, + }) + ); +} + +#[test] +fn dynamic_tool_response_serializes_text_and_image_content_items() { + let value = serde_json::to_value(DynamicToolCallResponse { + content_items: vec![ + DynamicToolCallOutputContentItem::InputText { + text: "dynamic-ok".to_string(), + }, + DynamicToolCallOutputContentItem::InputImage { + image_url: "data:image/png;base64,AAA".to_string(), + }, + ], + success: true, + }) + .unwrap(); + + assert_eq!( + value, + json!({ + "contentItems": [ + { + "type": "inputText", + "text": "dynamic-ok" + }, + { + "type": "inputImage", + "imageUrl": "data:image/png;base64,AAA" + } + ], + "success": true, + }) + ); +} + +#[test] +fn dynamic_tool_spec_deserializes_defer_loading() { + let value = json!({ + "name": "lookup_ticket", + "description": "Fetch a ticket", + "inputSchema": { + "type": "object", + "properties": { + "id": { "type": "string" } + } + }, + "deferLoading": true, + }); + + let actual: DynamicToolSpec = serde_json::from_value(value).expect("deserialize"); + + assert_eq!( + actual, + DynamicToolSpec { + namespace: None, + name: "lookup_ticket".to_string(), + description: "Fetch a ticket".to_string(), + input_schema: json!({ + "type": "object", + "properties": { + "id": { "type": "string" } + } + }), + defer_loading: true, + } + ); +} + +#[test] +fn dynamic_tool_spec_legacy_expose_to_context_inverts_to_defer_loading() { + let value = json!({ + "name": "lookup_ticket", + "description": "Fetch a ticket", + "inputSchema": { + "type": "object", + "properties": {} + }, + "exposeToContext": false, + }); + + let actual: DynamicToolSpec = serde_json::from_value(value).expect("deserialize"); + + assert!(actual.defer_loading); +} + +#[test] +fn thread_start_params_preserve_explicit_null_service_tier() { + let params: ThreadStartParams = + serde_json::from_value(json!({ "serviceTier": null })).expect("params should deserialize"); + assert_eq!(params.service_tier, Some(None)); + + let serialized = serde_json::to_value(¶ms).expect("params should serialize"); + assert_eq!( + serialized.get("serviceTier"), + Some(&serde_json::Value::Null) + ); + + let serialized_without_override = + serde_json::to_value(ThreadStartParams::default()).expect("params should serialize"); + assert_eq!(serialized_without_override.get("serviceTier"), None); +} + +#[test] +fn thread_lifecycle_responses_default_missing_compat_fields() { + let response = json!({ + "thread": { + "id": "thread-id", + "forkedFromId": null, + "preview": "", + "ephemeral": false, + "modelProvider": "openai", + "createdAt": 1, + "updatedAt": 1, + "status": { "type": "idle" }, + "path": null, + "cwd": absolute_path_string("tmp"), + "cliVersion": "0.0.0", + "source": "exec", + "agentNickname": null, + "agentRole": null, + "gitInfo": null, + "name": null, + "turns": [] + }, + "model": "gpt-5", + "modelProvider": "openai", + "serviceTier": null, + "cwd": absolute_path_string("tmp"), + "approvalPolicy": "on-failure", + "approvalsReviewer": "user", + "sandbox": { "type": "dangerFullAccess" }, + "reasoningEffort": null + }); + + let start: ThreadStartResponse = + serde_json::from_value(response.clone()).expect("thread/start response"); + let resume: ThreadResumeResponse = + serde_json::from_value(response.clone()).expect("thread/resume response"); + let fork: ThreadForkResponse = serde_json::from_value(response).expect("thread/fork response"); + + assert_eq!(start.instruction_sources, Vec::::new()); + assert_eq!(resume.instruction_sources, Vec::::new()); + assert_eq!(fork.instruction_sources, Vec::::new()); + assert_eq!(start.permission_profile, None); + assert_eq!(resume.permission_profile, None); + assert_eq!(fork.permission_profile, None); + assert_eq!(start.active_permission_profile, None); + assert_eq!(resume.active_permission_profile, None); + assert_eq!(fork.active_permission_profile, None); +} + +#[test] +fn turn_start_params_preserve_explicit_null_service_tier() { + let params: TurnStartParams = serde_json::from_value(json!({ + "threadId": "thread_123", + "input": [], + "serviceTier": null + })) + .expect("params should deserialize"); + assert_eq!(params.service_tier, Some(None)); + + let serialized = serde_json::to_value(¶ms).expect("params should serialize"); + assert_eq!( + serialized.get("serviceTier"), + Some(&serde_json::Value::Null) + ); + + let without_override = TurnStartParams { + thread_id: "thread_123".to_string(), + input: vec![], + responsesapi_client_metadata: None, + environments: None, + cwd: None, + approval_policy: None, + approvals_reviewer: None, + sandbox_policy: None, + permissions: None, + model: None, + service_tier: None, + effort: None, + summary: None, + output_schema: None, + collaboration_mode: None, + personality: None, + }; + let serialized_without_override = + serde_json::to_value(&without_override).expect("params should serialize"); + assert_eq!(serialized_without_override.get("serviceTier"), None); +} + +#[test] +fn turn_start_params_round_trip_environments() { + let cwd = test_absolute_path(); + let params: TurnStartParams = serde_json::from_value(json!({ + "threadId": "thread_123", + "input": [], + "environments": [ + { + "environmentId": "local", + "cwd": cwd + } + ], + })) + .expect("params should deserialize"); + + assert_eq!( + params.environments, + Some(vec![TurnEnvironmentParams { + environment_id: "local".to_string(), + cwd: cwd.clone(), + }]) + ); + assert_eq!( + crate::experimental_api::ExperimentalApi::experimental_reason(¶ms), + Some("turn/start.environments") + ); + + let serialized = serde_json::to_value(¶ms).expect("params should serialize"); + assert_eq!( + serialized.get("environments"), + Some(&json!([ + { + "environmentId": "local", + "cwd": cwd + } + ])) + ); +} + +#[test] +fn turn_start_params_preserve_empty_environments() { + let params: TurnStartParams = serde_json::from_value(json!({ + "threadId": "thread_123", + "input": [], + "environments": [], + })) + .expect("params should deserialize"); + + assert_eq!(params.environments, Some(Vec::new())); + assert_eq!( + crate::experimental_api::ExperimentalApi::experimental_reason(¶ms), + Some("turn/start.environments") + ); + + let serialized = serde_json::to_value(¶ms).expect("params should serialize"); + assert_eq!(serialized.get("environments"), Some(&json!([]))); +} + +#[test] +fn turn_start_params_treat_null_or_omitted_environments_as_default() { + let null_environments: TurnStartParams = serde_json::from_value(json!({ + "threadId": "thread_123", + "input": [], + "environments": null, + })) + .expect("params should deserialize"); + let omitted_environments: TurnStartParams = serde_json::from_value(json!({ + "threadId": "thread_123", + "input": [], + })) + .expect("params should deserialize"); + + assert_eq!(null_environments.environments, None); + assert_eq!(omitted_environments.environments, None); + assert_eq!( + crate::experimental_api::ExperimentalApi::experimental_reason(&null_environments), + None + ); + assert_eq!( + crate::experimental_api::ExperimentalApi::experimental_reason(&omitted_environments), + None + ); +} + +#[test] +fn turn_start_params_reject_relative_environment_cwd() { + let err = serde_json::from_value::(json!({ + "threadId": "thread_123", + "input": [], + "environments": [ + { + "environmentId": "local", + "cwd": "relative" + } + ], + })) + .expect_err("relative environment cwd should fail"); + + assert!( + err.to_string() + .contains("AbsolutePathBuf deserialized without a base path"), + "unexpected error: {err}" + ); +} diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs new file mode 100644 index 0000000000..ad125da4e6 --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs @@ -0,0 +1,1146 @@ +use super::ActivePermissionProfile; +use super::ApprovalsReviewer; +use super::AskForApproval; +use super::PermissionProfile; +use super::PermissionProfileSelectionParams; +use super::SandboxMode; +use super::SandboxPolicy; +use super::Thread; +use super::Turn; +use super::TurnEnvironmentParams; +use super::shared::v2_enum_from_core; +use codex_experimental_api_macros::ExperimentalApi; +use codex_protocol::config_types::Personality; +use codex_protocol::config_types::ServiceTier; +use codex_protocol::models::ResponseItem; +use codex_protocol::openai_models::ReasoningEffort; +use codex_protocol::protocol::ThreadGoalStatus as CoreThreadGoalStatus; +use codex_protocol::protocol::TokenUsage as CoreTokenUsage; +use codex_protocol::protocol::TokenUsageInfo as CoreTokenUsageInfo; +use codex_utils_absolute_path::AbsolutePathBuf; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use serde_json::Value as JsonValue; +use std::collections::HashMap; +use std::path::PathBuf; +use ts_rs::TS; + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase", export_to = "v2/")] +pub enum ThreadStartSource { + Startup, + Clear, +} + +#[derive(Serialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct DynamicToolSpec { + #[ts(optional)] + pub namespace: Option, + pub name: String, + pub description: String, + pub input_schema: JsonValue, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub defer_loading: bool, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct DynamicToolSpecDe { + namespace: Option, + name: String, + description: String, + input_schema: JsonValue, + defer_loading: Option, + expose_to_context: Option, +} + +impl<'de> Deserialize<'de> for DynamicToolSpec { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let DynamicToolSpecDe { + namespace, + name, + description, + input_schema, + defer_loading, + expose_to_context, + } = DynamicToolSpecDe::deserialize(deserializer)?; + + Ok(Self { + namespace, + name, + description, + input_schema, + defer_loading: defer_loading + .unwrap_or_else(|| expose_to_context.map(|visible| !visible).unwrap_or(false)), + }) + } +} + +// === Threads, Turns, and Items === +// Thread APIs +#[derive( + Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS, ExperimentalApi, +)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadStartParams { + #[ts(optional = nullable)] + pub model: Option, + #[ts(optional = nullable)] + pub model_provider: Option, + #[serde( + default, + deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option", + serialize_with = "crate::protocol::serde_helpers::serialize_double_option", + skip_serializing_if = "Option::is_none" + )] + #[ts(optional = nullable)] + pub service_tier: Option>, + #[ts(optional = nullable)] + pub cwd: Option, + #[experimental(nested)] + #[ts(optional = nullable)] + pub approval_policy: Option, + /// Override where approval requests are routed for review on this thread + /// and subsequent turns. + #[ts(optional = nullable)] + pub approvals_reviewer: Option, + #[ts(optional = nullable)] + pub sandbox: Option, + /// Named profile selection for this thread. Cannot be combined with + /// `sandbox`. Use bounded `modifications` for supported turn/thread + /// adjustments instead of replacing the full permissions profile. + #[experimental("thread/start.permissions")] + #[ts(optional = nullable)] + pub permissions: Option, + #[ts(optional = nullable)] + pub config: Option>, + #[ts(optional = nullable)] + pub service_name: Option, + #[ts(optional = nullable)] + pub base_instructions: Option, + #[ts(optional = nullable)] + pub developer_instructions: Option, + #[ts(optional = nullable)] + pub personality: Option, + #[ts(optional = nullable)] + pub ephemeral: Option, + #[ts(optional = nullable)] + pub session_start_source: Option, + /// Optional sticky environments for this thread. + /// + /// Omitted selects the default environment when environment access is + /// enabled. Empty disables environment access for turns that do not + /// provide a turn override. Non-empty selects the first environment as the + /// current turn environment. + #[experimental("thread/start.environments")] + #[ts(optional = nullable)] + pub environments: Option>, + #[experimental("thread/start.dynamicTools")] + #[ts(optional = nullable)] + pub dynamic_tools: Option>, + /// Test-only experimental field used to validate experimental gating and + /// schema filtering behavior in a stable way. + #[experimental("thread/start.mockExperimentalField")] + #[ts(optional = nullable)] + pub mock_experimental_field: Option, + /// If true, opt into emitting raw Responses API items on the event stream. + /// This is for internal use only (e.g. Codex Cloud). + #[experimental("thread/start.experimentalRawEvents")] + #[serde(default)] + pub experimental_raw_events: bool, + /// Deprecated and ignored by app-server. Kept only so older clients can + /// continue sending the field while rollout persistence always uses the + /// limited history policy. + #[experimental("thread/start.persistFullHistory")] + #[serde(default)] + pub persist_extended_history: bool, +} + +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct MockExperimentalMethodParams { + /// Test-only payload field. + #[ts(optional = nullable)] + pub value: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct MockExperimentalMethodResponse { + /// Echoes the input `value`. + pub echoed: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadStartResponse { + pub thread: Thread, + pub model: String, + pub model_provider: String, + pub service_tier: Option, + pub cwd: AbsolutePathBuf, + /// Instruction source files currently loaded for this thread. + #[serde(default)] + pub instruction_sources: Vec, + #[experimental(nested)] + pub approval_policy: AskForApproval, + /// Reviewer currently used for approval requests on this thread. + pub approvals_reviewer: ApprovalsReviewer, + /// Legacy sandbox policy retained for compatibility. Experimental clients + /// should prefer `permissionProfile` when they need exact runtime + /// permissions. + pub sandbox: SandboxPolicy, + /// Full active permissions for this thread. `activePermissionProfile` + /// carries display/provenance metadata for this runtime profile. + #[experimental("thread/start.permissionProfile")] + #[serde(default)] + pub permission_profile: Option, + /// Named or implicit built-in profile that produced the active + /// permissions, when known. + #[experimental("thread/start.activePermissionProfile")] + #[serde(default)] + pub active_permission_profile: Option, + pub reasoning_effort: Option, +} + +#[derive( + Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS, ExperimentalApi, +)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +/// There are three ways to resume a thread: +/// 1. By thread_id: load the thread from disk by thread_id and resume it. +/// 2. By history: instantiate the thread from memory and resume it. +/// 3. By path: load the thread from disk by path and resume it. +/// +/// The precedence is: history > path > thread_id. +/// If using history or path, the thread_id param will be ignored. +/// +/// Prefer using thread_id whenever possible. +pub struct ThreadResumeParams { + pub thread_id: String, + + /// [UNSTABLE] FOR CODEX CLOUD - DO NOT USE. + /// If specified, the thread will be resumed with the provided history + /// instead of loaded from disk. + #[experimental("thread/resume.history")] + #[ts(optional = nullable)] + pub history: Option>, + + /// [UNSTABLE] Specify the rollout path to resume from. + /// If specified, the thread_id param will be ignored. + #[experimental("thread/resume.path")] + #[ts(optional = nullable)] + pub path: Option, + + /// Configuration overrides for the resumed thread, if any. + #[ts(optional = nullable)] + pub model: Option, + #[ts(optional = nullable)] + pub model_provider: Option, + #[serde( + default, + deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option", + serialize_with = "crate::protocol::serde_helpers::serialize_double_option", + skip_serializing_if = "Option::is_none" + )] + #[ts(optional = nullable)] + pub service_tier: Option>, + #[ts(optional = nullable)] + pub cwd: Option, + #[experimental(nested)] + #[ts(optional = nullable)] + pub approval_policy: Option, + /// Override where approval requests are routed for review on this thread + /// and subsequent turns. + #[ts(optional = nullable)] + pub approvals_reviewer: Option, + #[ts(optional = nullable)] + pub sandbox: Option, + /// Named profile selection for the resumed thread. Cannot be combined + /// with `sandbox`. Use bounded `modifications` for supported thread + /// adjustments instead of replacing the full permissions profile. + #[experimental("thread/resume.permissions")] + #[ts(optional = nullable)] + pub permissions: Option, + #[ts(optional = nullable)] + pub config: Option>, + #[ts(optional = nullable)] + pub base_instructions: Option, + #[ts(optional = nullable)] + pub developer_instructions: Option, + #[ts(optional = nullable)] + pub personality: Option, + /// When true, return only thread metadata and live-resume state without + /// populating `thread.turns`. This is useful when the client plans to call + /// `thread/turns/list` immediately after resuming. + #[experimental("thread/resume.excludeTurns")] + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub exclude_turns: bool, + /// Deprecated and ignored by app-server. Kept only so older clients can + /// continue sending the field while rollout persistence always uses the + /// limited history policy. + #[experimental("thread/resume.persistFullHistory")] + #[serde(default)] + pub persist_extended_history: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadResumeResponse { + pub thread: Thread, + pub model: String, + pub model_provider: String, + pub service_tier: Option, + pub cwd: AbsolutePathBuf, + /// Instruction source files currently loaded for this thread. + #[serde(default)] + pub instruction_sources: Vec, + #[experimental(nested)] + pub approval_policy: AskForApproval, + /// Reviewer currently used for approval requests on this thread. + pub approvals_reviewer: ApprovalsReviewer, + /// Legacy sandbox policy retained for compatibility. Experimental clients + /// should prefer `permissionProfile` when they need exact runtime + /// permissions. + pub sandbox: SandboxPolicy, + /// Full active permissions for this thread. `activePermissionProfile` + /// carries display/provenance metadata for this runtime profile. + #[experimental("thread/resume.permissionProfile")] + #[serde(default)] + pub permission_profile: Option, + /// Named or implicit built-in profile that produced the active + /// permissions, when known. + #[experimental("thread/resume.activePermissionProfile")] + #[serde(default)] + pub active_permission_profile: Option, + pub reasoning_effort: Option, +} + +#[derive( + Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS, ExperimentalApi, +)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +/// There are two ways to fork a thread: +/// 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. +/// 2. By path: load the thread from disk by path and fork it into a new thread. +/// +/// If using path, the thread_id param will be ignored. +/// +/// Prefer using thread_id whenever possible. +pub struct ThreadForkParams { + pub thread_id: String, + + /// [UNSTABLE] Specify the rollout path to fork from. + /// If specified, the thread_id param will be ignored. + #[experimental("thread/fork.path")] + #[ts(optional = nullable)] + pub path: Option, + + /// Configuration overrides for the forked thread, if any. + #[ts(optional = nullable)] + pub model: Option, + #[ts(optional = nullable)] + pub model_provider: Option, + #[serde( + default, + deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option", + serialize_with = "crate::protocol::serde_helpers::serialize_double_option", + skip_serializing_if = "Option::is_none" + )] + #[ts(optional = nullable)] + pub service_tier: Option>, + #[ts(optional = nullable)] + pub cwd: Option, + #[experimental(nested)] + #[ts(optional = nullable)] + pub approval_policy: Option, + /// Override where approval requests are routed for review on this thread + /// and subsequent turns. + #[ts(optional = nullable)] + pub approvals_reviewer: Option, + #[ts(optional = nullable)] + pub sandbox: Option, + /// Named profile selection for the forked thread. Cannot be combined with + /// `sandbox`. Use bounded `modifications` for supported thread + /// adjustments instead of replacing the full permissions profile. + #[experimental("thread/fork.permissions")] + #[ts(optional = nullable)] + pub permissions: Option, + #[ts(optional = nullable)] + pub config: Option>, + #[ts(optional = nullable)] + pub base_instructions: Option, + #[ts(optional = nullable)] + pub developer_instructions: Option, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub ephemeral: bool, + /// When true, return only thread metadata and live fork state without + /// populating `thread.turns`. This is useful when the client plans to call + /// `thread/turns/list` immediately after forking. + #[experimental("thread/fork.excludeTurns")] + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub exclude_turns: bool, + /// Deprecated and ignored by app-server. Kept only so older clients can + /// continue sending the field while rollout persistence always uses the + /// limited history policy. + #[experimental("thread/fork.persistFullHistory")] + #[serde(default)] + pub persist_extended_history: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadForkResponse { + pub thread: Thread, + pub model: String, + pub model_provider: String, + pub service_tier: Option, + pub cwd: AbsolutePathBuf, + /// Instruction source files currently loaded for this thread. + #[serde(default)] + pub instruction_sources: Vec, + #[experimental(nested)] + pub approval_policy: AskForApproval, + /// Reviewer currently used for approval requests on this thread. + pub approvals_reviewer: ApprovalsReviewer, + /// Legacy sandbox policy retained for compatibility. Experimental clients + /// should prefer `permissionProfile` when they need exact runtime + /// permissions. + pub sandbox: SandboxPolicy, + /// Full active permissions for this thread. `activePermissionProfile` + /// carries display/provenance metadata for this runtime profile. + #[experimental("thread/fork.permissionProfile")] + #[serde(default)] + pub permission_profile: Option, + /// Named or implicit built-in profile that produced the active + /// permissions, when known. + #[experimental("thread/fork.activePermissionProfile")] + #[serde(default)] + pub active_permission_profile: Option, + pub reasoning_effort: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadArchiveParams { + pub thread_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadArchiveResponse {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadUnsubscribeParams { + pub thread_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadUnsubscribeResponse { + pub status: ThreadUnsubscribeStatus, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum ThreadUnsubscribeStatus { + NotLoaded, + NotSubscribed, + Unsubscribed, +} + +/// Parameters for `thread/increment_elicitation`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadIncrementElicitationParams { + /// Thread whose out-of-band elicitation counter should be incremented. + pub thread_id: String, +} + +/// Response for `thread/increment_elicitation`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadIncrementElicitationResponse { + /// Current out-of-band elicitation count after the increment. + pub count: u64, + /// Whether timeout accounting is paused after applying the increment. + pub paused: bool, +} + +/// Parameters for `thread/decrement_elicitation`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadDecrementElicitationParams { + /// Thread whose out-of-band elicitation counter should be decremented. + pub thread_id: String, +} + +/// Response for `thread/decrement_elicitation`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadDecrementElicitationResponse { + /// Current out-of-band elicitation count after the decrement. + pub count: u64, + /// Whether timeout accounting remains paused after applying the decrement. + pub paused: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadSetNameParams { + pub thread_id: String, + pub name: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadUnarchiveParams { + pub thread_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadSetNameResponse {} + +v2_enum_from_core! { + pub enum ThreadGoalStatus from CoreThreadGoalStatus { + Active, + Paused, + BudgetLimited, + Complete, + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadGoal { + pub thread_id: String, + pub objective: String, + pub status: ThreadGoalStatus, + #[ts(type = "number | null")] + pub token_budget: Option, + #[ts(type = "number")] + pub tokens_used: i64, + #[ts(type = "number")] + pub time_used_seconds: i64, + #[ts(type = "number")] + pub created_at: i64, + #[ts(type = "number")] + pub updated_at: i64, +} + +impl From for ThreadGoal { + fn from(value: codex_protocol::protocol::ThreadGoal) -> Self { + Self { + thread_id: value.thread_id.to_string(), + objective: value.objective, + status: value.status.into(), + token_budget: value.token_budget, + tokens_used: value.tokens_used, + time_used_seconds: value.time_used_seconds, + created_at: value.created_at, + updated_at: value.updated_at, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadGoalSetParams { + pub thread_id: String, + #[ts(optional = nullable)] + pub objective: Option, + #[ts(optional = nullable)] + pub status: Option, + #[serde( + default, + deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option", + serialize_with = "crate::protocol::serde_helpers::serialize_double_option", + skip_serializing_if = "Option::is_none" + )] + #[ts(optional = nullable, type = "number | null")] + pub token_budget: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadGoalSetResponse { + pub goal: ThreadGoal, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadGoalGetParams { + pub thread_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadGoalGetResponse { + pub goal: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadGoalClearParams { + pub thread_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadGoalClearResponse { + pub cleared: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadMetadataUpdateParams { + pub thread_id: String, + /// Patch the stored Git metadata for this thread. + /// Omit a field to leave it unchanged, set it to `null` to clear it, or + /// provide a string to replace the stored value. + #[ts(optional = nullable)] + pub git_info: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadMetadataGitInfoUpdateParams { + /// Omit to leave the stored commit unchanged, set to `null` to clear it, + /// or provide a non-empty string to replace it. + #[serde( + default, + skip_serializing_if = "Option::is_none", + serialize_with = "crate::protocol::serde_helpers::serialize_double_option", + deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option" + )] + #[ts(optional = nullable, type = "string | null")] + pub sha: Option>, + /// Omit to leave the stored branch unchanged, set to `null` to clear it, + /// or provide a non-empty string to replace it. + #[serde( + default, + skip_serializing_if = "Option::is_none", + serialize_with = "crate::protocol::serde_helpers::serialize_double_option", + deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option" + )] + #[ts(optional = nullable, type = "string | null")] + pub branch: Option>, + /// Omit to leave the stored origin URL unchanged, set to `null` to clear it, + /// or provide a non-empty string to replace it. + #[serde( + default, + skip_serializing_if = "Option::is_none", + serialize_with = "crate::protocol::serde_helpers::serialize_double_option", + deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option" + )] + #[ts(optional = nullable, type = "string | null")] + pub origin_url: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadMetadataUpdateResponse { + pub thread: Thread, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "lowercase")] +#[ts(rename_all = "lowercase")] +pub enum ThreadMemoryMode { + Enabled, + Disabled, +} + +impl ThreadMemoryMode { + pub fn as_str(self) -> &'static str { + match self { + Self::Enabled => "enabled", + Self::Disabled => "disabled", + } + } + + pub fn to_core(self) -> codex_protocol::protocol::ThreadMemoryMode { + match self { + Self::Enabled => codex_protocol::protocol::ThreadMemoryMode::Enabled, + Self::Disabled => codex_protocol::protocol::ThreadMemoryMode::Disabled, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadMemoryModeSetParams { + pub thread_id: String, + pub mode: ThreadMemoryMode, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadMemoryModeSetResponse {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct MemoryResetResponse {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadUnarchiveResponse { + pub thread: Thread, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadCompactStartParams { + pub thread_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadCompactStartResponse {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadShellCommandParams { + pub thread_id: String, + /// Shell command string evaluated by the thread's configured shell. + /// Unlike `command/exec`, this intentionally preserves shell syntax + /// such as pipes, redirects, and quoting. This runs unsandboxed with full + /// access rather than inheriting the thread sandbox policy. + pub command: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadShellCommandResponse {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadApproveGuardianDeniedActionParams { + pub thread_id: String, + /// Serialized `codex_protocol::protocol::GuardianAssessmentEvent`. + pub event: JsonValue, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadApproveGuardianDeniedActionResponse {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadBackgroundTerminalsCleanParams { + pub thread_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadBackgroundTerminalsCleanResponse {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRollbackParams { + pub thread_id: String, + /// The number of turns to drop from the end of the thread. Must be >= 1. + /// + /// This only modifies the thread's history and does not revert local file changes + /// that have been made by the agent. Clients are responsible for reverting these changes. + pub num_turns: u32, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadRollbackResponse { + /// The updated thread after applying the rollback, with `turns` populated. + /// + /// The ThreadItems stored in each Turn are lossy since we explicitly do not + /// persist all agent interactions, such as command executions. This is the same + /// behavior as `thread/resume`. + pub thread: Thread, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadListParams { + /// Opaque pagination cursor returned by a previous call. + #[ts(optional = nullable)] + pub cursor: Option, + /// Optional page size; defaults to a reasonable server-side value. + #[ts(optional = nullable)] + pub limit: Option, + /// Optional sort key; defaults to created_at. + #[ts(optional = nullable)] + pub sort_key: Option, + /// Optional sort direction; defaults to descending (newest first). + #[ts(optional = nullable)] + pub sort_direction: Option, + /// Optional provider filter; when set, only sessions recorded under these + /// providers are returned. When present but empty, includes all providers. + #[ts(optional = nullable)] + pub model_providers: Option>, + /// Optional source filter; when set, only sessions from these source kinds + /// are returned. When omitted or empty, defaults to interactive sources. + #[ts(optional = nullable)] + pub source_kinds: Option>, + /// Optional archived filter; when set to true, only archived threads are returned. + /// If false or null, only non-archived threads are returned. + #[ts(optional = nullable)] + pub archived: Option, + /// Optional cwd filter or filters; when set, only threads whose session cwd + /// exactly matches one of these paths are returned. + #[ts(optional = nullable, type = "string | Array | null")] + pub cwd: Option, + /// If true, return from the state DB without scanning JSONL rollouts to + /// repair thread metadata. Omitted or false preserves scan-and-repair + /// behavior. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub use_state_db_only: bool, + /// Optional substring filter for the extracted thread title. + #[ts(optional = nullable)] + pub search_term: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] +#[serde(untagged)] +pub enum ThreadListCwdFilter { + One(String), + Many(Vec), +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase", export_to = "v2/")] +pub enum ThreadSourceKind { + Cli, + #[serde(rename = "vscode")] + #[ts(rename = "vscode")] + VsCode, + Exec, + AppServer, + SubAgent, + SubAgentReview, + SubAgentCompact, + SubAgentThreadSpawn, + SubAgentOther, + Unknown, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/")] +pub enum ThreadSortKey { + CreatedAt, + UpdatedAt, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/")] +pub enum SortDirection { + Asc, + Desc, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadListResponse { + pub data: 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, + /// Opaque cursor to pass as `cursor` when reversing `sortDirection`. + /// This is only populated when the page contains at least one thread. + /// Use it with the opposite `sortDirection`; for timestamp sorts it anchors + /// at the start of the page timestamp so same-second updates are not skipped. + pub backwards_cursor: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadLoadedListParams { + /// Opaque pagination cursor returned by a previous call. + #[ts(optional = nullable)] + pub cursor: Option, + /// Optional page size; defaults to no limit. + #[ts(optional = nullable)] + pub limit: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadLoadedListResponse { + /// Thread ids for sessions currently loaded in memory. + pub data: 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(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type")] +#[ts(export_to = "v2/")] +pub enum ThreadStatus { + NotLoaded, + Idle, + SystemError, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + Active { + active_flags: Vec, + }, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum ThreadActiveFlag { + WaitingOnApproval, + WaitingOnUserInput, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadReadParams { + pub thread_id: String, + /// When true, include turns and their items from rollout history. + #[serde(default)] + pub include_turns: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadReadResponse { + pub thread: Thread, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadInjectItemsParams { + pub thread_id: String, + /// Raw Responses API items to append to the thread's model-visible history. + pub items: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadInjectItemsResponse {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadTurnsListParams { + pub thread_id: String, + /// Opaque cursor to pass to the next call to continue after the last turn. + #[ts(optional = nullable)] + pub cursor: Option, + /// Optional turn page size. + #[ts(optional = nullable)] + pub limit: Option, + /// Optional turn pagination direction; defaults to descending. + #[ts(optional = nullable)] + pub sort_direction: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadTurnsListResponse { + pub data: Vec, + /// Opaque cursor to pass to the next call to continue after the last turn. + /// if None, there are no more turns to return. + pub next_cursor: Option, + /// Opaque cursor to pass as `cursor` when reversing `sortDirection`. + /// This is only populated when the page contains at least one turn. + /// Use it with the opposite `sortDirection` to include the anchor turn again + /// and catch updates to that turn. + pub backwards_cursor: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadTokenUsageUpdatedNotification { + pub thread_id: String, + pub turn_id: String, + pub token_usage: ThreadTokenUsage, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadTokenUsage { + pub total: TokenUsageBreakdown, + pub last: TokenUsageBreakdown, + // TODO(aibrahim): make this not optional + #[ts(type = "number | null")] + pub model_context_window: Option, +} + +impl From for ThreadTokenUsage { + fn from(value: CoreTokenUsageInfo) -> Self { + Self { + total: value.total_token_usage.into(), + last: value.last_token_usage.into(), + model_context_window: value.model_context_window, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct TokenUsageBreakdown { + #[ts(type = "number")] + pub total_tokens: i64, + #[ts(type = "number")] + pub input_tokens: i64, + #[ts(type = "number")] + pub cached_input_tokens: i64, + #[ts(type = "number")] + pub output_tokens: i64, + #[ts(type = "number")] + pub reasoning_output_tokens: i64, +} + +impl From for TokenUsageBreakdown { + fn from(value: CoreTokenUsage) -> Self { + Self { + total_tokens: value.total_tokens, + input_tokens: value.input_tokens, + cached_input_tokens: value.cached_input_tokens, + output_tokens: value.output_tokens, + reasoning_output_tokens: value.reasoning_output_tokens, + } + } +} + +// Thread/Turn lifecycle notifications and item progress events +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadStartedNotification { + pub thread: Thread, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadStatusChangedNotification { + pub thread_id: String, + pub status: ThreadStatus, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadArchivedNotification { + pub thread_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadUnarchivedNotification { + pub thread_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadClosedNotification { + pub thread_id: String, +} +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadNameUpdatedNotification { + pub thread_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub thread_name: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadGoalUpdatedNotification { + pub thread_id: String, + pub turn_id: Option, + pub goal: ThreadGoal, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadGoalClearedNotification { + pub thread_id: String, +} + +/// Deprecated: Use `ContextCompaction` item type instead. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ContextCompactedNotification { + pub thread_id: String, + pub turn_id: String, +} diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs new file mode 100644 index 0000000000..07c21e3906 --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs @@ -0,0 +1,162 @@ +use super::CodexErrorInfo; +use super::ThreadItem; +use super::ThreadStatus; +use super::TurnStatus; +use codex_protocol::protocol::SessionSource as CoreSessionSource; +use codex_protocol::protocol::SubAgentSource as CoreSubAgentSource; +use codex_utils_absolute_path::AbsolutePathBuf; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use std::path::PathBuf; +use thiserror::Error; +use ts_rs::TS; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase", export_to = "v2/")] +#[derive(Default)] +pub enum SessionSource { + Cli, + #[serde(rename = "vscode")] + #[ts(rename = "vscode")] + #[default] + VsCode, + Exec, + AppServer, + Custom(String), + SubAgent(CoreSubAgentSource), + #[serde(other)] + Unknown, +} + +impl From for SessionSource { + fn from(value: CoreSessionSource) -> Self { + match value { + CoreSessionSource::Cli => SessionSource::Cli, + CoreSessionSource::VSCode => SessionSource::VsCode, + CoreSessionSource::Exec => SessionSource::Exec, + CoreSessionSource::Mcp => SessionSource::AppServer, + CoreSessionSource::Custom(source) => SessionSource::Custom(source), + // We do not want to render those at the app-server level. + CoreSessionSource::Internal(_) => SessionSource::Unknown, + CoreSessionSource::SubAgent(sub) => SessionSource::SubAgent(sub), + CoreSessionSource::Unknown => SessionSource::Unknown, + } + } +} + +impl From for CoreSessionSource { + fn from(value: SessionSource) -> Self { + match value { + SessionSource::Cli => CoreSessionSource::Cli, + SessionSource::VsCode => CoreSessionSource::VSCode, + SessionSource::Exec => CoreSessionSource::Exec, + SessionSource::AppServer => CoreSessionSource::Mcp, + SessionSource::Custom(source) => CoreSessionSource::Custom(source), + SessionSource::SubAgent(sub) => CoreSessionSource::SubAgent(sub), + SessionSource::Unknown => CoreSessionSource::Unknown, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct GitInfo { + pub sha: Option, + pub branch: Option, + pub origin_url: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct Thread { + pub id: String, + /// Source thread id when this thread was created by forking another thread. + pub forked_from_id: Option, + /// Usually the first user message in the thread, if available. + pub preview: String, + /// Whether the thread is ephemeral and should not be materialized on disk. + pub ephemeral: bool, + /// Model provider used for this thread (for example, 'openai'). + pub model_provider: String, + /// Unix timestamp (in seconds) when the thread was created. + #[ts(type = "number")] + pub created_at: i64, + /// Unix timestamp (in seconds) when the thread was last updated. + #[ts(type = "number")] + pub updated_at: i64, + /// Current runtime status for the thread. + pub status: ThreadStatus, + /// [UNSTABLE] Path to the thread on disk. + pub path: Option, + /// Working directory captured for the thread. + pub cwd: AbsolutePathBuf, + /// Version of the CLI that created the thread. + pub cli_version: String, + /// Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.). + pub source: SessionSource, + /// Optional random unique nickname assigned to an AgentControl-spawned sub-agent. + pub agent_nickname: Option, + /// Optional role (agent_role) assigned to an AgentControl-spawned sub-agent. + pub agent_role: Option, + /// Optional Git metadata captured when the thread was created. + pub git_info: Option, + /// Optional user-facing thread title. + pub name: Option, + /// Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` + /// (when `includeTurns` is true) responses. + /// For all other responses and notifications returning a Thread, + /// the turns field will be an empty list. + pub turns: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct Turn { + pub id: String, + /// Thread items currently included in this turn payload. + pub items: Vec, + /// Describes how much of `items` has been loaded for this turn. + #[serde(default)] + pub items_view: TurnItemsView, + pub status: TurnStatus, + /// Only populated when the Turn's status is failed. + pub error: Option, + /// Unix timestamp (in seconds) when the turn started. + #[ts(type = "number | null")] + pub started_at: Option, + /// Unix timestamp (in seconds) when the turn completed. + #[ts(type = "number | null")] + pub completed_at: Option, + /// Duration between turn start and completion in milliseconds, if known. + #[ts(type = "number | null")] + pub duration_ms: Option, +} + +#[derive(Default, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum TurnItemsView { + /// `items` was not loaded for this turn. The field is intentionally empty. + NotLoaded, + /// `items` contains only a display summary for this turn. + Summary, + /// `items` contains every ThreadItem available from persisted app-server history for this turn. + #[default] + Full, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, Error)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +#[error("{message}")] +pub struct TurnError { + pub message: String, + pub codex_error_info: Option, + #[serde(default)] + pub additional_details: Option, +} diff --git a/codex-rs/app-server-protocol/src/protocol/v2/turn.rs b/codex-rs/app-server-protocol/src/protocol/v2/turn.rs new file mode 100644 index 0000000000..38118ab15e --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/turn.rs @@ -0,0 +1,390 @@ +use super::ApprovalsReviewer; +use super::AskForApproval; +use super::PermissionProfileSelectionParams; +use super::SandboxPolicy; +use super::Turn; +use codex_experimental_api_macros::ExperimentalApi; +use codex_protocol::config_types::CollaborationMode; +use codex_protocol::config_types::Personality; +use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::config_types::ServiceTier; +use codex_protocol::openai_models::ReasoningEffort; +use codex_protocol::plan_tool::PlanItemArg as CorePlanItemArg; +use codex_protocol::plan_tool::StepStatus as CorePlanStepStatus; +use codex_protocol::user_input::ByteRange as CoreByteRange; +use codex_protocol::user_input::TextElement as CoreTextElement; +use codex_protocol::user_input::UserInput as CoreUserInput; +use codex_utils_absolute_path::AbsolutePathBuf; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use serde_json::Value as JsonValue; +use std::collections::HashMap; +use std::path::PathBuf; +use ts_rs::TS; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum TurnStatus { + Completed, + Interrupted, + Failed, + InProgress, +} + +// Turn APIs +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct TurnEnvironmentParams { + pub environment_id: String, + pub cwd: AbsolutePathBuf, +} + +#[derive( + Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS, ExperimentalApi, +)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct TurnStartParams { + pub thread_id: String, + pub input: Vec, + /// Optional turn-scoped Responses API client metadata. + #[experimental("turn/start.responsesapiClientMetadata")] + #[ts(optional = nullable)] + pub responsesapi_client_metadata: Option>, + /// Optional turn-scoped environments. + /// + /// Omitted uses the thread sticky environments. Empty disables + /// environment access for this turn. Non-empty selects the first + /// environment as the current turn environment for this turn. + #[experimental("turn/start.environments")] + #[ts(optional = nullable)] + pub environments: Option>, + /// Override the working directory for this turn and subsequent turns. + #[ts(optional = nullable)] + pub cwd: Option, + /// Override the approval policy for this turn and subsequent turns. + #[experimental(nested)] + #[ts(optional = nullable)] + pub approval_policy: Option, + /// Override where approval requests are routed for review on this turn and + /// subsequent turns. + #[ts(optional = nullable)] + pub approvals_reviewer: Option, + /// Override the sandbox policy for this turn and subsequent turns. + #[ts(optional = nullable)] + pub sandbox_policy: Option, + /// Select a named permissions profile for this turn and subsequent turns. + /// Cannot be combined with `sandboxPolicy`. Use bounded `modifications` + /// for supported turn adjustments instead of replacing the full + /// permissions profile. + #[experimental("turn/start.permissions")] + #[ts(optional = nullable)] + pub permissions: Option, + /// Override the model for this turn and subsequent turns. + #[ts(optional = nullable)] + pub model: Option, + /// Override the service tier for this turn and subsequent turns. + #[serde( + default, + deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option", + serialize_with = "crate::protocol::serde_helpers::serialize_double_option", + skip_serializing_if = "Option::is_none" + )] + #[ts(optional = nullable)] + pub service_tier: Option>, + /// Override the reasoning effort for this turn and subsequent turns. + #[ts(optional = nullable)] + pub effort: Option, + /// Override the reasoning summary for this turn and subsequent turns. + #[ts(optional = nullable)] + pub summary: Option, + /// Override the personality for this turn and subsequent turns. + #[ts(optional = nullable)] + pub personality: Option, + /// Optional JSON Schema used to constrain the final assistant message for + /// this turn. + #[ts(optional = nullable)] + pub output_schema: Option, + + /// EXPERIMENTAL - Set a pre-set collaboration mode. + /// Takes precedence over model, reasoning_effort, and developer instructions if set. + /// + /// For `collaboration_mode.settings.developer_instructions`, `null` means + /// "use the built-in instructions for the selected mode". + #[experimental("turn/start.collaborationMode")] + #[ts(optional = nullable)] + pub collaboration_mode: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct TurnStartResponse { + pub turn: Turn, +} + +#[derive( + Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS, ExperimentalApi, +)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct TurnSteerParams { + pub thread_id: String, + pub input: Vec, + /// Optional turn-scoped Responses API client metadata. + #[experimental("turn/steer.responsesapiClientMetadata")] + #[ts(optional = nullable)] + pub responsesapi_client_metadata: Option>, + /// Required active turn id precondition. The request fails when it does not + /// match the currently active turn. + pub expected_turn_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct TurnSteerResponse { + pub turn_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct TurnInterruptParams { + pub thread_id: String, + pub turn_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct TurnInterruptResponse {} + +// User input types +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ByteRange { + pub start: usize, + pub end: usize, +} + +impl From for ByteRange { + fn from(value: CoreByteRange) -> Self { + Self { + start: value.start, + end: value.end, + } + } +} + +impl From for CoreByteRange { + fn from(value: ByteRange) -> Self { + Self { + start: value.start, + end: value.end, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct TextElement { + /// Byte range in the parent `text` buffer that this element occupies. + pub byte_range: ByteRange, + /// Optional human-readable placeholder for the element, displayed in the UI. + placeholder: Option, +} + +impl TextElement { + pub fn new(byte_range: ByteRange, placeholder: Option) -> Self { + Self { + byte_range, + placeholder, + } + } + + pub fn set_placeholder(&mut self, placeholder: Option) { + self.placeholder = placeholder; + } + + pub fn placeholder(&self) -> Option<&str> { + self.placeholder.as_deref() + } +} + +impl From for TextElement { + fn from(value: CoreTextElement) -> Self { + Self::new( + value.byte_range.into(), + value._placeholder_for_conversion_only().map(str::to_string), + ) + } +} + +impl From for CoreTextElement { + fn from(value: TextElement) -> Self { + Self::new(value.byte_range.into(), value.placeholder) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type")] +#[ts(export_to = "v2/")] +pub enum UserInput { + Text { + text: String, + /// UI-defined spans within `text` used to render or persist special elements. + #[serde(default)] + text_elements: Vec, + }, + Image { + url: String, + }, + LocalImage { + path: PathBuf, + }, + Skill { + name: String, + path: PathBuf, + }, + Mention { + name: String, + path: String, + }, +} + +impl UserInput { + pub fn into_core(self) -> CoreUserInput { + match self { + UserInput::Text { + text, + text_elements, + } => CoreUserInput::Text { + text, + text_elements: text_elements.into_iter().map(Into::into).collect(), + }, + UserInput::Image { url } => CoreUserInput::Image { image_url: url }, + UserInput::LocalImage { path } => CoreUserInput::LocalImage { path }, + UserInput::Skill { name, path } => CoreUserInput::Skill { name, path }, + UserInput::Mention { name, path } => CoreUserInput::Mention { name, path }, + } + } +} + +impl From for UserInput { + fn from(value: CoreUserInput) -> Self { + match value { + CoreUserInput::Text { + text, + text_elements, + } => UserInput::Text { + text, + text_elements: text_elements.into_iter().map(Into::into).collect(), + }, + CoreUserInput::Image { image_url } => UserInput::Image { url: image_url }, + CoreUserInput::LocalImage { path } => UserInput::LocalImage { path }, + CoreUserInput::Skill { name, path } => UserInput::Skill { name, path }, + CoreUserInput::Mention { name, path } => UserInput::Mention { name, path }, + _ => unreachable!("unsupported user input variant"), + } + } +} + +impl UserInput { + pub fn text_char_count(&self) -> usize { + match self { + UserInput::Text { text, .. } => text.chars().count(), + UserInput::Image { .. } + | UserInput::LocalImage { .. } + | UserInput::Skill { .. } + | UserInput::Mention { .. } => 0, + } + } +} +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct TurnStartedNotification { + pub thread_id: String, + pub turn: Turn, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct Usage { + pub input_tokens: i32, + pub cached_input_tokens: i32, + pub output_tokens: i32, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct TurnCompletedNotification { + pub thread_id: String, + pub turn: Turn, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +/// Notification that the turn-level unified diff has changed. +/// Contains the latest aggregated diff across all file changes in the turn. +pub struct TurnDiffUpdatedNotification { + pub thread_id: String, + pub turn_id: String, + pub diff: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct TurnPlanUpdatedNotification { + pub thread_id: String, + pub turn_id: String, + pub explanation: Option, + pub plan: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct TurnPlanStep { + pub step: String, + pub status: TurnPlanStepStatus, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum TurnPlanStepStatus { + Pending, + InProgress, + Completed, +} + +impl From for TurnPlanStep { + fn from(value: CorePlanItemArg) -> Self { + Self { + step: value.step, + status: value.status.into(), + } + } +} + +impl From for TurnPlanStepStatus { + fn from(value: CorePlanStepStatus) -> Self { + match value { + CorePlanStepStatus::Pending => Self::Pending, + CorePlanStepStatus::InProgress => Self::InProgress, + CorePlanStepStatus::Completed => Self::Completed, + } + } +} diff --git a/codex-rs/app-server-protocol/src/protocol/v2/windows_sandbox.rs b/codex-rs/app-server-protocol/src/protocol/v2/windows_sandbox.rs new file mode 100644 index 0000000000..3e090c7bfd --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/windows_sandbox.rs @@ -0,0 +1,63 @@ +use codex_utils_absolute_path::AbsolutePathBuf; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use ts_rs::TS; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct WindowsWorldWritableWarningNotification { + pub sample_paths: Vec, + pub extra_count: usize, + pub failed_scan: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum WindowsSandboxSetupMode { + Elevated, + Unelevated, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum WindowsSandboxReadiness { + Ready, + NotConfigured, + UpdateRequired, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct WindowsSandboxSetupStartParams { + pub mode: WindowsSandboxSetupMode, + #[ts(optional = nullable)] + pub cwd: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct WindowsSandboxSetupStartResponse { + pub started: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct WindowsSandboxReadinessResponse { + pub status: WindowsSandboxReadiness, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct WindowsSandboxSetupCompletedNotification { + pub mode: WindowsSandboxSetupMode, + pub success: bool, + pub error: Option, +}