#![allow(missing_docs)]
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use async_trait::async_trait;
use std::io::Write;
use tokio::sync::Mutex;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum PermissionMode {
Default,
AcceptEdits,
Plan,
BypassPermissions,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Effort {
Low,
Medium,
High,
Max,
}
impl std::fmt::Display for Effort {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Effort::Low => write!(f, "low"),
Effort::Medium => write!(f, "medium"),
Effort::High => write!(f, "high"),
Effort::Max => write!(f, "max"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RateLimitStatus {
Allowed,
AllowedWarning,
Rejected,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum RateLimitType {
#[serde(rename = "five_hour")]
FiveHour,
#[serde(rename = "seven_day")]
SevenDay,
#[serde(rename = "seven_day_opus")]
SevenDayOpus,
#[serde(rename = "seven_day_sonnet")]
SevenDaySonnet,
#[serde(rename = "overage")]
Overage,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RateLimitInfo {
pub status: RateLimitStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resets_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rate_limit_type: Option<RateLimitType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub utilization: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub overage_status: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub overage_resets_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub overage_disabled_reason: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub raw: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AssistantMessageError {
AuthenticationFailed,
BillingError,
RateLimit,
InvalidRequest,
ServerError,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SdkBeta {
#[serde(rename = "context-1m-2025-08-07")]
Context1M,
}
impl std::fmt::Display for SdkBeta {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SdkBeta::Context1M => write!(f, "context-1m-2025-08-07"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ToolsConfig {
List(Vec<String>),
Preset(ToolsPreset),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolsPreset {
#[serde(rename = "type")]
pub preset_type: String,
pub preset: String,
}
impl ToolsConfig {
pub fn list(tools: Vec<String>) -> Self {
ToolsConfig::List(tools)
}
pub fn none() -> Self {
ToolsConfig::List(vec![])
}
pub fn claude_code_preset() -> Self {
ToolsConfig::Preset(ToolsPreset {
preset_type: "preset".to_string(),
preset: "claude_code".to_string(),
})
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SandboxNetworkConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_unix_sockets: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_all_unix_sockets: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_local_binding: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub http_proxy_port: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub socks_proxy_port: Option<u16>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SandboxIgnoreViolations {
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub network: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SandboxSettings {
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_allow_bash_if_sandboxed: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub excluded_commands: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_unsandboxed_commands: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub network: Option<SandboxNetworkConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore_violations: Option<SandboxIgnoreViolations>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_weaker_nested_sandbox: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum SdkPluginConfig {
Local {
path: String,
},
}
impl Default for PermissionMode {
fn default() -> Self {
Self::Default
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ControlProtocolFormat {
Legacy,
Control,
Auto,
}
impl Default for ControlProtocolFormat {
fn default() -> Self {
Self::Legacy
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum McpConnectionStatus {
Connected,
Failed,
NeedsAuth,
Pending,
Disabled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpToolAnnotations {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub read_only: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub destructive: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub open_world: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpToolInfo {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub annotations: Option<McpToolAnnotations>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpServerInfo {
pub name: String,
pub version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpServerStatus {
pub name: String,
pub status: McpConnectionStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub server_info: Option<McpServerInfo>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<McpToolInfo>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ThinkingConfig {
Adaptive,
Enabled {
budget_tokens: i32,
},
Disabled,
}
#[derive(Clone)]
pub enum McpServerConfig {
Stdio {
command: String,
args: Option<Vec<String>>,
env: Option<HashMap<String, String>>,
},
Sse {
url: String,
headers: Option<HashMap<String, String>>,
},
Http {
url: String,
headers: Option<HashMap<String, String>>,
},
Sdk {
name: String,
instance: Arc<dyn std::any::Any + Send + Sync>,
},
}
impl std::fmt::Debug for McpServerConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Stdio { command, args, env } => f
.debug_struct("Stdio")
.field("command", command)
.field("args", args)
.field("env", env)
.finish(),
Self::Sse { url, headers } => f
.debug_struct("Sse")
.field("url", url)
.field("headers", headers)
.finish(),
Self::Http { url, headers } => f
.debug_struct("Http")
.field("url", url)
.field("headers", headers)
.finish(),
Self::Sdk { name, .. } => f
.debug_struct("Sdk")
.field("name", name)
.field("instance", &"<Arc<dyn Any>>")
.finish(),
}
}
}
impl Serialize for McpServerConfig {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(None)?;
match self {
Self::Stdio { command, args, env } => {
map.serialize_entry("type", "stdio")?;
map.serialize_entry("command", command)?;
if let Some(args) = args {
map.serialize_entry("args", args)?;
}
if let Some(env) = env {
map.serialize_entry("env", env)?;
}
}
Self::Sse { url, headers } => {
map.serialize_entry("type", "sse")?;
map.serialize_entry("url", url)?;
if let Some(headers) = headers {
map.serialize_entry("headers", headers)?;
}
}
Self::Http { url, headers } => {
map.serialize_entry("type", "http")?;
map.serialize_entry("url", url)?;
if let Some(headers) = headers {
map.serialize_entry("headers", headers)?;
}
}
Self::Sdk { name, .. } => {
map.serialize_entry("type", "sdk")?;
map.serialize_entry("name", name)?;
}
}
map.end()
}
}
impl<'de> Deserialize<'de> for McpServerConfig {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
enum McpServerConfigHelper {
Stdio {
command: String,
#[serde(skip_serializing_if = "Option::is_none")]
args: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
env: Option<HashMap<String, String>>,
},
Sse {
url: String,
#[serde(skip_serializing_if = "Option::is_none")]
headers: Option<HashMap<String, String>>,
},
Http {
url: String,
#[serde(skip_serializing_if = "Option::is_none")]
headers: Option<HashMap<String, String>>,
},
}
let helper = McpServerConfigHelper::deserialize(deserializer)?;
Ok(match helper {
McpServerConfigHelper::Stdio { command, args, env } => {
McpServerConfig::Stdio { command, args, env }
}
McpServerConfigHelper::Sse { url, headers } => {
McpServerConfig::Sse { url, headers }
}
McpServerConfigHelper::Http { url, headers } => {
McpServerConfig::Http { url, headers }
}
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum PermissionUpdateDestination {
UserSettings,
ProjectSettings,
LocalSettings,
Session,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum PermissionBehavior {
Allow,
Deny,
Ask,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionRuleValue {
pub tool_name: String,
pub rule_content: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum PermissionUpdateType {
AddRules,
ReplaceRules,
RemoveRules,
SetMode,
AddDirectories,
RemoveDirectories,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PermissionUpdate {
#[serde(rename = "type")]
pub update_type: PermissionUpdateType,
#[serde(skip_serializing_if = "Option::is_none")]
pub rules: Option<Vec<PermissionRuleValue>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub behavior: Option<PermissionBehavior>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mode: Option<PermissionMode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub directories: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub destination: Option<PermissionUpdateDestination>,
}
#[derive(Debug, Clone)]
pub struct ToolPermissionContext {
pub signal: Option<Arc<dyn std::any::Any + Send + Sync>>,
pub suggestions: Vec<PermissionUpdate>,
}
#[derive(Debug, Clone)]
pub struct PermissionResultAllow {
pub updated_input: Option<serde_json::Value>,
pub updated_permissions: Option<Vec<PermissionUpdate>>,
}
#[derive(Debug, Clone)]
pub struct PermissionResultDeny {
pub message: String,
pub interrupt: bool,
}
#[derive(Debug, Clone)]
pub enum PermissionResult {
Allow(PermissionResultAllow),
Deny(PermissionResultDeny),
}
#[async_trait]
pub trait CanUseTool: Send + Sync {
async fn can_use_tool(
&self,
tool_name: &str,
input: &serde_json::Value,
context: &ToolPermissionContext,
) -> PermissionResult;
}
#[derive(Debug, Clone)]
pub struct HookContext {
pub signal: Option<Arc<dyn std::any::Any + Send + Sync>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BaseHookInput {
pub session_id: String,
pub transcript_path: String,
pub cwd: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_mode: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreToolUseHookInput {
pub session_id: String,
pub transcript_path: String,
pub cwd: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_mode: Option<String>,
pub tool_name: String,
pub tool_input: serde_json::Value,
pub tool_use_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostToolUseHookInput {
pub session_id: String,
pub transcript_path: String,
pub cwd: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_mode: Option<String>,
pub tool_name: String,
pub tool_input: serde_json::Value,
pub tool_response: serde_json::Value,
pub tool_use_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserPromptSubmitHookInput {
pub session_id: String,
pub transcript_path: String,
pub cwd: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_mode: Option<String>,
pub prompt: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StopHookInput {
pub session_id: String,
pub transcript_path: String,
pub cwd: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_mode: Option<String>,
pub stop_hook_active: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubagentStopHookInput {
pub session_id: String,
pub transcript_path: String,
pub cwd: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_mode: Option<String>,
pub stop_hook_active: bool,
pub agent_id: String,
pub agent_transcript_path: String,
pub agent_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreCompactHookInput {
pub session_id: String,
pub transcript_path: String,
pub cwd: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_mode: Option<String>,
pub trigger: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_instructions: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostToolUseFailureHookInput {
pub session_id: String,
pub transcript_path: String,
pub cwd: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_mode: Option<String>,
pub tool_name: String,
pub tool_input: serde_json::Value,
pub tool_use_id: String,
pub error: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_interrupt: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotificationHookInput {
pub session_id: String,
pub transcript_path: String,
pub cwd: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_mode: Option<String>,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
pub notification_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubagentStartHookInput {
pub session_id: String,
pub transcript_path: String,
pub cwd: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_mode: Option<String>,
pub agent_id: String,
pub agent_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionRequestHookInput {
pub session_id: String,
pub transcript_path: String,
pub cwd: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_mode: Option<String>,
pub tool_name: String,
pub tool_input: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_suggestions: Option<Vec<serde_json::Value>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "hook_event_name")]
pub enum HookInput {
#[serde(rename = "PreToolUse")]
PreToolUse(PreToolUseHookInput),
#[serde(rename = "PostToolUse")]
PostToolUse(PostToolUseHookInput),
#[serde(rename = "PostToolUseFailure")]
PostToolUseFailure(PostToolUseFailureHookInput),
#[serde(rename = "UserPromptSubmit")]
UserPromptSubmit(UserPromptSubmitHookInput),
#[serde(rename = "Stop")]
Stop(StopHookInput),
#[serde(rename = "SubagentStop")]
SubagentStop(SubagentStopHookInput),
#[serde(rename = "PreCompact")]
PreCompact(PreCompactHookInput),
#[serde(rename = "Notification")]
Notification(NotificationHookInput),
#[serde(rename = "SubagentStart")]
SubagentStart(SubagentStartHookInput),
#[serde(rename = "PermissionRequest")]
PermissionRequest(PermissionRequestHookInput),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AsyncHookJSONOutput {
#[serde(rename = "async")]
pub async_: bool,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "asyncTimeout")]
pub async_timeout: Option<u32>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SyncHookJSONOutput {
#[serde(rename = "continue", skip_serializing_if = "Option::is_none")]
pub continue_: Option<bool>,
#[serde(rename = "suppressOutput", skip_serializing_if = "Option::is_none")]
pub suppress_output: Option<bool>,
#[serde(rename = "stopReason", skip_serializing_if = "Option::is_none")]
pub stop_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub decision: Option<String>, #[serde(rename = "systemMessage", skip_serializing_if = "Option::is_none")]
pub system_message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(rename = "hookSpecificOutput", skip_serializing_if = "Option::is_none")]
pub hook_specific_output: Option<HookSpecificOutput>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum HookJSONOutput {
Async(AsyncHookJSONOutput),
Sync(SyncHookJSONOutput),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreToolUseHookSpecificOutput {
#[serde(rename = "permissionDecision", skip_serializing_if = "Option::is_none")]
pub permission_decision: Option<String>,
#[serde(rename = "permissionDecisionReason", skip_serializing_if = "Option::is_none")]
pub permission_decision_reason: Option<String>,
#[serde(rename = "updatedInput", skip_serializing_if = "Option::is_none")]
pub updated_input: Option<serde_json::Value>,
#[serde(rename = "additionalContext", skip_serializing_if = "Option::is_none")]
pub additional_context: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostToolUseHookSpecificOutput {
#[serde(rename = "additionalContext", skip_serializing_if = "Option::is_none")]
pub additional_context: Option<String>,
#[serde(rename = "updatedMCPToolOutput", skip_serializing_if = "Option::is_none")]
pub updated_mcp_tool_output: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserPromptSubmitHookSpecificOutput {
#[serde(rename = "additionalContext", skip_serializing_if = "Option::is_none")]
pub additional_context: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionStartHookSpecificOutput {
#[serde(rename = "additionalContext", skip_serializing_if = "Option::is_none")]
pub additional_context: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostToolUseFailureHookSpecificOutput {
#[serde(rename = "additionalContext", skip_serializing_if = "Option::is_none")]
pub additional_context: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotificationHookSpecificOutput {
#[serde(rename = "additionalContext", skip_serializing_if = "Option::is_none")]
pub additional_context: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubagentStartHookSpecificOutput {
#[serde(rename = "additionalContext", skip_serializing_if = "Option::is_none")]
pub additional_context: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionRequestHookSpecificOutput {
pub decision: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "hookEventName")]
pub enum HookSpecificOutput {
#[serde(rename = "PreToolUse")]
PreToolUse(PreToolUseHookSpecificOutput),
#[serde(rename = "PostToolUse")]
PostToolUse(PostToolUseHookSpecificOutput),
#[serde(rename = "PostToolUseFailure")]
PostToolUseFailure(PostToolUseFailureHookSpecificOutput),
#[serde(rename = "UserPromptSubmit")]
UserPromptSubmit(UserPromptSubmitHookSpecificOutput),
#[serde(rename = "SessionStart")]
SessionStart(SessionStartHookSpecificOutput),
#[serde(rename = "Notification")]
Notification(NotificationHookSpecificOutput),
#[serde(rename = "SubagentStart")]
SubagentStart(SubagentStartHookSpecificOutput),
#[serde(rename = "PermissionRequest")]
PermissionRequest(PermissionRequestHookSpecificOutput),
}
#[async_trait]
pub trait HookCallback: Send + Sync {
async fn execute(
&self,
input: &HookInput,
tool_use_id: Option<&str>,
context: &HookContext,
) -> Result<HookJSONOutput, crate::errors::SdkError>;
}
#[deprecated(
since = "0.3.0",
note = "Use the new HookCallback trait with HookInput/HookJSONOutput instead"
)]
#[allow(dead_code)]
#[async_trait]
pub trait HookCallbackLegacy: Send + Sync {
async fn execute_legacy(
&self,
input: &serde_json::Value,
tool_use_id: Option<&str>,
context: &HookContext,
) -> serde_json::Value;
}
#[derive(Clone)]
pub struct HookMatcher {
pub matcher: Option<serde_json::Value>,
pub hooks: Vec<Arc<dyn HookCallback>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SettingSource {
User,
Project,
Local,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentDefinition {
pub description: String,
pub prompt: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub skills: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub memory: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", rename = "mcpServers")]
pub mcp_servers: Option<Vec<serde_json::Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SystemPrompt {
String(String),
Preset {
#[serde(rename = "type")]
preset_type: String, preset: String, #[serde(skip_serializing_if = "Option::is_none")]
append: Option<String>,
},
}
#[derive(Clone, Default)]
pub struct ClaudeCodeOptions {
pub system_prompt_v2: Option<SystemPrompt>,
#[deprecated(since = "0.1.12", note = "Use system_prompt_v2 instead")]
pub system_prompt: Option<String>,
#[deprecated(since = "0.1.12", note = "Use system_prompt_v2 instead")]
pub append_system_prompt: Option<String>,
pub allowed_tools: Vec<String>,
pub disallowed_tools: Vec<String>,
pub permission_mode: PermissionMode,
pub mcp_servers: HashMap<String, McpServerConfig>,
pub mcp_tools: Vec<String>,
pub max_turns: Option<i32>,
pub max_thinking_tokens: Option<i32>,
pub max_output_tokens: Option<u32>,
pub model: Option<String>,
pub cwd: Option<PathBuf>,
pub continue_conversation: bool,
pub resume: Option<String>,
pub permission_prompt_tool_name: Option<String>,
pub settings: Option<String>,
pub add_dirs: Vec<PathBuf>,
pub extra_args: HashMap<String, Option<String>>,
pub env: HashMap<String, String>,
pub debug_stderr: Option<Arc<Mutex<dyn Write + Send + Sync>>>,
pub include_partial_messages: bool,
pub can_use_tool: Option<Arc<dyn CanUseTool>>,
pub hooks: Option<HashMap<String, Vec<HookMatcher>>>,
pub control_protocol_format: ControlProtocolFormat,
pub setting_sources: Option<Vec<SettingSource>>,
pub fork_session: bool,
pub agents: Option<HashMap<String, AgentDefinition>>,
pub cli_channel_buffer_size: Option<usize>,
pub tools: Option<ToolsConfig>,
pub betas: Vec<SdkBeta>,
pub max_budget_usd: Option<f64>,
pub fallback_model: Option<String>,
pub output_format: Option<serde_json::Value>,
pub enable_file_checkpointing: bool,
pub sandbox: Option<SandboxSettings>,
pub plugins: Vec<SdkPluginConfig>,
pub user: Option<String>,
pub stderr_callback: Option<Arc<dyn Fn(&str) + Send + Sync>>,
pub auto_download_cli: bool,
pub effort: Option<Effort>,
pub thinking: Option<ThinkingConfig>,
}
impl std::fmt::Debug for ClaudeCodeOptions {
#[allow(deprecated)]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ClaudeCodeOptions")
.field("system_prompt", &self.system_prompt)
.field("append_system_prompt", &self.append_system_prompt)
.field("allowed_tools", &self.allowed_tools)
.field("disallowed_tools", &self.disallowed_tools)
.field("permission_mode", &self.permission_mode)
.field("mcp_servers", &self.mcp_servers)
.field("mcp_tools", &self.mcp_tools)
.field("max_turns", &self.max_turns)
.field("max_thinking_tokens", &self.max_thinking_tokens)
.field("max_output_tokens", &self.max_output_tokens)
.field("model", &self.model)
.field("cwd", &self.cwd)
.field("continue_conversation", &self.continue_conversation)
.field("resume", &self.resume)
.field("permission_prompt_tool_name", &self.permission_prompt_tool_name)
.field("settings", &self.settings)
.field("add_dirs", &self.add_dirs)
.field("extra_args", &self.extra_args)
.field("env", &self.env)
.field("debug_stderr", &self.debug_stderr.is_some())
.field("include_partial_messages", &self.include_partial_messages)
.field("can_use_tool", &self.can_use_tool.is_some())
.field("hooks", &self.hooks.is_some())
.field("control_protocol_format", &self.control_protocol_format)
.field("effort", &self.effort)
.field("thinking", &self.thinking)
.finish()
}
}
impl ClaudeCodeOptions {
pub fn builder() -> ClaudeCodeOptionsBuilder {
ClaudeCodeOptionsBuilder::default()
}
}
#[derive(Debug, Default)]
pub struct ClaudeCodeOptionsBuilder {
options: ClaudeCodeOptions,
}
impl ClaudeCodeOptionsBuilder {
#[allow(deprecated)]
pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
self.options.system_prompt = Some(prompt.into());
self
}
#[allow(deprecated)]
pub fn append_system_prompt(mut self, prompt: impl Into<String>) -> Self {
self.options.append_system_prompt = Some(prompt.into());
self
}
pub fn allowed_tools(mut self, tools: Vec<String>) -> Self {
self.options.allowed_tools = tools;
self
}
pub fn allow_tool(mut self, tool: impl Into<String>) -> Self {
self.options.allowed_tools.push(tool.into());
self
}
pub fn disallowed_tools(mut self, tools: Vec<String>) -> Self {
self.options.disallowed_tools = tools;
self
}
pub fn disallow_tool(mut self, tool: impl Into<String>) -> Self {
self.options.disallowed_tools.push(tool.into());
self
}
pub fn permission_mode(mut self, mode: PermissionMode) -> Self {
self.options.permission_mode = mode;
self
}
pub fn add_mcp_server(mut self, name: impl Into<String>, config: McpServerConfig) -> Self {
self.options.mcp_servers.insert(name.into(), config);
self
}
pub fn mcp_servers(mut self, servers: HashMap<String, McpServerConfig>) -> Self {
self.options.mcp_servers = servers;
self
}
pub fn mcp_tools(mut self, tools: Vec<String>) -> Self {
self.options.mcp_tools = tools;
self
}
pub fn max_turns(mut self, turns: i32) -> Self {
self.options.max_turns = Some(turns);
self
}
pub fn max_thinking_tokens(mut self, tokens: i32) -> Self {
self.options.max_thinking_tokens = Some(tokens);
self
}
pub fn max_output_tokens(mut self, tokens: u32) -> Self {
self.options.max_output_tokens = Some(tokens.clamp(1, 32000));
self
}
pub fn model(mut self, model: impl Into<String>) -> Self {
self.options.model = Some(model.into());
self
}
pub fn cwd(mut self, path: impl Into<PathBuf>) -> Self {
self.options.cwd = Some(path.into());
self
}
pub fn continue_conversation(mut self, enable: bool) -> Self {
self.options.continue_conversation = enable;
self
}
pub fn resume(mut self, id: impl Into<String>) -> Self {
self.options.resume = Some(id.into());
self
}
pub fn permission_prompt_tool_name(mut self, name: impl Into<String>) -> Self {
self.options.permission_prompt_tool_name = Some(name.into());
self
}
pub fn settings(mut self, settings: impl Into<String>) -> Self {
self.options.settings = Some(settings.into());
self
}
pub fn add_dirs(mut self, dirs: Vec<PathBuf>) -> Self {
self.options.add_dirs = dirs;
self
}
pub fn add_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.options.add_dirs.push(dir.into());
self
}
pub fn extra_args(mut self, args: HashMap<String, Option<String>>) -> Self {
self.options.extra_args = args;
self
}
pub fn add_extra_arg(mut self, key: impl Into<String>, value: Option<String>) -> Self {
self.options.extra_args.insert(key.into(), value);
self
}
pub fn control_protocol_format(mut self, format: ControlProtocolFormat) -> Self {
self.options.control_protocol_format = format;
self
}
pub fn include_partial_messages(mut self, include: bool) -> Self {
self.options.include_partial_messages = include;
self
}
pub fn fork_session(mut self, fork: bool) -> Self {
self.options.fork_session = fork;
self
}
pub fn setting_sources(mut self, sources: Vec<SettingSource>) -> Self {
self.options.setting_sources = Some(sources);
self
}
pub fn agents(mut self, agents: HashMap<String, AgentDefinition>) -> Self {
self.options.agents = Some(agents);
self
}
pub fn cli_channel_buffer_size(mut self, size: usize) -> Self {
self.options.cli_channel_buffer_size = Some(size);
self
}
pub fn tools(mut self, config: ToolsConfig) -> Self {
self.options.tools = Some(config);
self
}
pub fn betas(mut self, betas: Vec<SdkBeta>) -> Self {
self.options.betas = betas;
self
}
pub fn add_beta(mut self, beta: SdkBeta) -> Self {
self.options.betas.push(beta);
self
}
pub fn max_budget_usd(mut self, budget: f64) -> Self {
self.options.max_budget_usd = Some(budget);
self
}
pub fn fallback_model(mut self, model: impl Into<String>) -> Self {
self.options.fallback_model = Some(model.into());
self
}
pub fn output_format(mut self, format: serde_json::Value) -> Self {
self.options.output_format = Some(format);
self
}
pub fn enable_file_checkpointing(mut self, enable: bool) -> Self {
self.options.enable_file_checkpointing = enable;
self
}
pub fn sandbox(mut self, settings: SandboxSettings) -> Self {
self.options.sandbox = Some(settings);
self
}
pub fn plugins(mut self, plugins: Vec<SdkPluginConfig>) -> Self {
self.options.plugins = plugins;
self
}
pub fn add_plugin(mut self, plugin: SdkPluginConfig) -> Self {
self.options.plugins.push(plugin);
self
}
pub fn user(mut self, user: impl Into<String>) -> Self {
self.options.user = Some(user.into());
self
}
pub fn stderr_callback(mut self, callback: Arc<dyn Fn(&str) + Send + Sync>) -> Self {
self.options.stderr_callback = Some(callback);
self
}
pub fn auto_download_cli(mut self, enable: bool) -> Self {
self.options.auto_download_cli = enable;
self
}
pub fn effort(mut self, effort: Effort) -> Self {
self.options.effort = Some(effort);
self
}
pub fn thinking(mut self, config: ThinkingConfig) -> Self {
self.options.thinking = Some(config);
self
}
pub fn build(self) -> ClaudeCodeOptions {
self.options
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct TaskUsage {
#[serde(default)]
pub total_tokens: u64,
#[serde(default)]
pub tool_uses: u64,
#[serde(default)]
pub duration_ms: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TaskStatus {
Completed,
Failed,
Stopped,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TaskStartedMessage {
pub task_id: String,
pub description: String,
pub uuid: String,
pub session_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_use_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub task_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TaskProgressMessage {
pub task_id: String,
pub description: String,
#[serde(default)]
pub usage: TaskUsage,
pub uuid: String,
pub session_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_use_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_tool_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TaskNotificationMessage {
pub task_id: String,
pub status: TaskStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_file: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
pub uuid: String,
pub session_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_use_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub usage: Option<TaskUsage>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum Message {
User {
message: UserMessage,
},
Assistant {
message: AssistantMessage,
},
System {
subtype: String,
data: serde_json::Value,
},
Result {
subtype: String,
duration_ms: i64,
duration_api_ms: i64,
is_error: bool,
num_turns: i32,
session_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
total_cost_usd: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
usage: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
result: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", alias = "structuredOutput")]
structured_output: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
stop_reason: Option<String>,
},
#[serde(rename = "stream_event")]
StreamEvent {
uuid: String,
session_id: String,
event: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
parent_tool_use_id: Option<String>,
},
#[serde(rename = "rate_limit")]
RateLimit {
rate_limit_info: RateLimitInfo,
uuid: String,
session_id: String,
},
#[serde(skip)]
Unknown {
msg_type: String,
raw: serde_json::Value,
},
}
impl Message {
pub fn as_task_started(&self) -> Option<TaskStartedMessage> {
if let Message::System { subtype, data } = self {
if subtype == "task_started" {
return serde_json::from_value(data.clone()).ok();
}
}
None
}
pub fn as_task_progress(&self) -> Option<TaskProgressMessage> {
if let Message::System { subtype, data } = self {
if subtype == "task_progress" {
return serde_json::from_value(data.clone()).ok();
}
}
None
}
pub fn as_task_notification(&self) -> Option<TaskNotificationMessage> {
if let Message::System { subtype, data } = self {
if subtype == "task_notification" {
return serde_json::from_value(data.clone()).ok();
}
}
None
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct UserMessage {
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AssistantMessage {
pub content: Vec<ContentBlock>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub usage: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<AssistantMessageError>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_tool_use_id: Option<String>,
}
pub use Message::Result as ResultMessage;
pub use Message::System as SystemMessage;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum ContentBlock {
Text(TextContent),
Thinking(ThinkingContent),
ToolUse(ToolUseContent),
ToolResult(ToolResultContent),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TextContent {
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ThinkingContent {
pub thinking: String,
pub signature: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ToolUseContent {
pub id: String,
pub name: String,
pub input: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ToolResultContent {
pub tool_use_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<ContentValue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_error: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum ContentValue {
Text(String),
Structured(Vec<serde_json::Value>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserContent {
pub role: String,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssistantContent {
pub role: String,
pub content: Vec<ContentBlock>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SDKControlInterruptRequest {
pub subtype: String, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SDKControlPermissionRequest {
pub subtype: String, #[serde(alias = "toolName")]
pub tool_name: String,
pub input: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none", alias = "permissionSuggestions")]
pub permission_suggestions: Option<Vec<PermissionUpdate>>,
#[serde(skip_serializing_if = "Option::is_none", alias = "blockedPath")]
pub blocked_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SDKControlInitializeRequest {
pub subtype: String, #[serde(skip_serializing_if = "Option::is_none")]
pub hooks: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SDKControlSetPermissionModeRequest {
pub subtype: String, pub mode: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SDKControlSetModelRequest {
pub subtype: String, #[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SDKHookCallbackRequest {
pub subtype: String, #[serde(alias = "callbackId")]
pub callback_id: String,
pub input: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none", alias = "toolUseId")]
pub tool_use_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SDKControlMcpMessageRequest {
pub subtype: String, #[serde(rename = "server_name", alias = "mcpServerName", alias = "mcp_server_name")]
pub mcp_server_name: String,
pub message: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SDKControlRewindFilesRequest {
pub subtype: String,
#[serde(alias = "userMessageId")]
pub user_message_id: String,
}
impl SDKControlRewindFilesRequest {
pub fn new(user_message_id: impl Into<String>) -> Self {
Self {
subtype: "rewind_files".to_string(),
user_message_id: user_message_id.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum SDKControlRequest {
#[serde(rename = "interrupt")]
Interrupt(SDKControlInterruptRequest),
#[serde(rename = "can_use_tool")]
CanUseTool(SDKControlPermissionRequest),
#[serde(rename = "initialize")]
Initialize(SDKControlInitializeRequest),
#[serde(rename = "set_permission_mode")]
SetPermissionMode(SDKControlSetPermissionModeRequest),
#[serde(rename = "set_model")]
SetModel(SDKControlSetModelRequest),
#[serde(rename = "hook_callback")]
HookCallback(SDKHookCallbackRequest),
#[serde(rename = "mcp_message")]
McpMessage(SDKControlMcpMessageRequest),
#[serde(rename = "rewind_files")]
RewindFiles(SDKControlRewindFilesRequest),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ControlRequest {
Interrupt {
request_id: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ControlResponse {
InterruptAck {
request_id: String,
success: bool,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_permission_mode_serialization() {
let mode = PermissionMode::AcceptEdits;
let json = serde_json::to_string(&mode).unwrap();
assert_eq!(json, r#""acceptEdits""#);
let deserialized: PermissionMode = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, mode);
let plan_mode = PermissionMode::Plan;
let plan_json = serde_json::to_string(&plan_mode).unwrap();
assert_eq!(plan_json, r#""plan""#);
let plan_deserialized: PermissionMode = serde_json::from_str(&plan_json).unwrap();
assert_eq!(plan_deserialized, plan_mode);
}
#[test]
fn test_message_serialization() {
let msg = Message::User {
message: UserMessage {
content: "Hello".to_string(),
},
};
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains(r#""type":"user""#));
assert!(json.contains(r#""content":"Hello""#));
let deserialized: Message = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, msg);
}
#[test]
#[allow(deprecated)]
fn test_options_builder() {
let options = ClaudeCodeOptions::builder()
.system_prompt("Test prompt")
.model("claude-3-opus")
.permission_mode(PermissionMode::AcceptEdits)
.allow_tool("read")
.allow_tool("write")
.max_turns(10)
.build();
assert_eq!(options.system_prompt, Some("Test prompt".to_string()));
assert_eq!(options.model, Some("claude-3-opus".to_string()));
assert_eq!(options.permission_mode, PermissionMode::AcceptEdits);
assert_eq!(options.allowed_tools, vec!["read", "write"]);
assert_eq!(options.max_turns, Some(10));
}
#[test]
fn test_extra_args() {
let mut extra_args = HashMap::new();
extra_args.insert("custom-flag".to_string(), Some("value".to_string()));
extra_args.insert("boolean-flag".to_string(), None);
let options = ClaudeCodeOptions::builder()
.extra_args(extra_args.clone())
.add_extra_arg("another-flag", Some("another-value".to_string()))
.build();
assert_eq!(options.extra_args.len(), 3);
assert_eq!(options.extra_args.get("custom-flag"), Some(&Some("value".to_string())));
assert_eq!(options.extra_args.get("boolean-flag"), Some(&None));
assert_eq!(options.extra_args.get("another-flag"), Some(&Some("another-value".to_string())));
}
#[test]
fn test_thinking_content_serialization() {
let thinking = ThinkingContent {
thinking: "Let me think about this...".to_string(),
signature: "sig123".to_string(),
};
let json = serde_json::to_string(&thinking).unwrap();
assert!(json.contains(r#""thinking":"Let me think about this...""#));
assert!(json.contains(r#""signature":"sig123""#));
let deserialized: ThinkingContent = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.thinking, thinking.thinking);
assert_eq!(deserialized.signature, thinking.signature);
}
#[test]
fn test_tools_config_list_serialization() {
let tools = ToolsConfig::List(vec!["Read".to_string(), "Write".to_string(), "Bash".to_string()]);
let json = serde_json::to_string(&tools).unwrap();
assert!(json.contains("Read"));
assert!(json.contains("Write"));
assert!(json.contains("Bash"));
let deserialized: ToolsConfig = serde_json::from_str(&json).unwrap();
match deserialized {
ToolsConfig::List(list) => {
assert_eq!(list.len(), 3);
assert!(list.contains(&"Read".to_string()));
}
_ => panic!("Expected List variant"),
}
}
#[test]
fn test_tools_config_preset_serialization() {
let preset = ToolsConfig::claude_code_preset();
let json = serde_json::to_string(&preset).unwrap();
assert!(json.contains("preset"));
assert!(json.contains("claude_code"));
let custom_preset = ToolsConfig::Preset(ToolsPreset {
preset_type: "preset".to_string(),
preset: "custom".to_string(),
});
let json = serde_json::to_string(&custom_preset).unwrap();
assert!(json.contains("custom"));
let deserialized: ToolsConfig = serde_json::from_str(&json).unwrap();
match deserialized {
ToolsConfig::Preset(p) => assert_eq!(p.preset, "custom"),
_ => panic!("Expected Preset variant"),
}
}
#[test]
fn test_tools_config_helper_methods() {
let tools = ToolsConfig::list(vec!["Read".to_string(), "Write".to_string()]);
match tools {
ToolsConfig::List(list) => assert_eq!(list.len(), 2),
_ => panic!("Expected List variant"),
}
let empty = ToolsConfig::none();
match empty {
ToolsConfig::List(list) => assert!(list.is_empty()),
_ => panic!("Expected empty List variant"),
}
let preset = ToolsConfig::claude_code_preset();
match preset {
ToolsConfig::Preset(p) => {
assert_eq!(p.preset_type, "preset");
assert_eq!(p.preset, "claude_code");
}
_ => panic!("Expected Preset variant"),
}
}
#[test]
fn test_sdk_beta_serialization() {
let beta = SdkBeta::Context1M;
let json = serde_json::to_string(&beta).unwrap();
assert_eq!(json, r#""context-1m-2025-08-07""#);
let display = format!("{}", beta);
assert_eq!(display, "context-1m-2025-08-07");
let deserialized: SdkBeta = serde_json::from_str(r#""context-1m-2025-08-07""#).unwrap();
assert!(matches!(deserialized, SdkBeta::Context1M));
}
#[test]
fn test_sandbox_settings_serialization() {
let sandbox = SandboxSettings {
enabled: Some(true),
auto_allow_bash_if_sandboxed: Some(true),
excluded_commands: Some(vec!["git".to_string(), "docker".to_string()]),
allow_unsandboxed_commands: Some(false),
network: Some(SandboxNetworkConfig {
allow_unix_sockets: Some(vec!["/tmp/ssh-agent.sock".to_string()]),
allow_all_unix_sockets: Some(false),
allow_local_binding: Some(true),
http_proxy_port: Some(8080),
socks_proxy_port: Some(1080),
}),
ignore_violations: Some(SandboxIgnoreViolations {
file: Some(vec!["/tmp".to_string(), "/var/log".to_string()]),
network: Some(vec!["localhost".to_string()]),
}),
enable_weaker_nested_sandbox: Some(false),
};
let json = serde_json::to_string(&sandbox).unwrap();
assert!(json.contains("enabled"));
assert!(json.contains("autoAllowBashIfSandboxed")); assert!(json.contains("excludedCommands"));
assert!(json.contains("httpProxyPort"));
assert!(json.contains("8080"));
let deserialized: SandboxSettings = serde_json::from_str(&json).unwrap();
assert!(deserialized.enabled.unwrap());
assert!(deserialized.network.is_some());
assert_eq!(deserialized.network.as_ref().unwrap().http_proxy_port, Some(8080));
}
#[test]
fn test_sandbox_network_config() {
let config = SandboxNetworkConfig {
allow_unix_sockets: Some(vec!["/run/user/1000/keyring/ssh".to_string()]),
allow_all_unix_sockets: Some(false),
allow_local_binding: Some(true),
http_proxy_port: Some(3128),
socks_proxy_port: Some(1080),
};
let json = serde_json::to_string(&config).unwrap();
let deserialized: SandboxNetworkConfig = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.http_proxy_port, Some(3128));
assert_eq!(deserialized.socks_proxy_port, Some(1080));
assert_eq!(deserialized.allow_local_binding, Some(true));
}
#[test]
fn test_sandbox_ignore_violations() {
let violations = SandboxIgnoreViolations {
file: Some(vec!["/tmp".to_string(), "/var/cache".to_string()]),
network: Some(vec!["127.0.0.1".to_string(), "localhost".to_string()]),
};
let json = serde_json::to_string(&violations).unwrap();
assert!(json.contains("file"));
assert!(json.contains("/tmp"));
let deserialized: SandboxIgnoreViolations = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.file.as_ref().unwrap().len(), 2);
assert_eq!(deserialized.network.as_ref().unwrap().len(), 2);
}
#[test]
fn test_sandbox_settings_default() {
let sandbox = SandboxSettings::default();
assert!(sandbox.enabled.is_none());
assert!(sandbox.network.is_none());
assert!(sandbox.ignore_violations.is_none());
}
#[test]
fn test_sdk_plugin_config_serialization() {
let plugin = SdkPluginConfig::Local {
path: "/path/to/plugin".to_string()
};
let json = serde_json::to_string(&plugin).unwrap();
assert!(json.contains("local")); assert!(json.contains("/path/to/plugin"));
let deserialized: SdkPluginConfig = serde_json::from_str(&json).unwrap();
match deserialized {
SdkPluginConfig::Local { path } => {
assert_eq!(path, "/path/to/plugin");
}
}
}
#[test]
fn test_sdk_control_rewind_files_request() {
let request = SDKControlRewindFilesRequest {
subtype: "rewind_files".to_string(),
user_message_id: "msg_12345".to_string(),
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("user_message_id"));
assert!(json.contains("msg_12345"));
assert!(json.contains("subtype"));
assert!(json.contains("rewind_files"));
let deserialized: SDKControlRewindFilesRequest = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.user_message_id, "msg_12345");
assert_eq!(deserialized.subtype, "rewind_files");
}
#[test]
fn test_options_builder_with_new_fields() {
let options = ClaudeCodeOptions::builder()
.tools(ToolsConfig::claude_code_preset())
.add_beta(SdkBeta::Context1M)
.max_budget_usd(10.0)
.fallback_model("claude-3-haiku")
.output_format(serde_json::json!({"type": "object"}))
.enable_file_checkpointing(true)
.sandbox(SandboxSettings::default())
.add_plugin(SdkPluginConfig::Local { path: "/plugin".to_string() })
.auto_download_cli(true)
.build();
assert!(options.tools.is_some());
match options.tools.as_ref().unwrap() {
ToolsConfig::Preset(preset) => assert_eq!(preset.preset, "claude_code"),
_ => panic!("Expected Preset variant"),
}
assert_eq!(options.betas.len(), 1);
assert!(matches!(options.betas[0], SdkBeta::Context1M));
assert_eq!(options.max_budget_usd, Some(10.0));
assert_eq!(options.fallback_model, Some("claude-3-haiku".to_string()));
assert!(options.output_format.is_some());
assert!(options.enable_file_checkpointing);
assert!(options.sandbox.is_some());
assert_eq!(options.plugins.len(), 1);
assert!(options.auto_download_cli);
}
#[test]
fn test_options_builder_with_tools_list() {
let options = ClaudeCodeOptions::builder()
.tools(ToolsConfig::List(vec!["Read".to_string(), "Bash".to_string()]))
.build();
match options.tools.as_ref().unwrap() {
ToolsConfig::List(list) => {
assert_eq!(list.len(), 2);
assert!(list.contains(&"Read".to_string()));
assert!(list.contains(&"Bash".to_string()));
}
_ => panic!("Expected List variant"),
}
}
#[test]
fn test_options_builder_multiple_betas() {
let options = ClaudeCodeOptions::builder()
.add_beta(SdkBeta::Context1M)
.betas(vec![SdkBeta::Context1M])
.build();
assert_eq!(options.betas.len(), 1);
}
#[test]
fn test_options_builder_add_beta_accumulates() {
let options = ClaudeCodeOptions::builder()
.add_beta(SdkBeta::Context1M)
.add_beta(SdkBeta::Context1M)
.build();
assert_eq!(options.betas.len(), 2);
}
#[test]
fn test_options_builder_multiple_plugins() {
let options = ClaudeCodeOptions::builder()
.add_plugin(SdkPluginConfig::Local { path: "/plugin1".to_string() })
.add_plugin(SdkPluginConfig::Local { path: "/plugin2".to_string() })
.plugins(vec![SdkPluginConfig::Local { path: "/plugin3".to_string() }])
.build();
assert_eq!(options.plugins.len(), 1);
}
#[test]
fn test_options_builder_add_plugin_accumulates() {
let options = ClaudeCodeOptions::builder()
.add_plugin(SdkPluginConfig::Local { path: "/plugin1".to_string() })
.add_plugin(SdkPluginConfig::Local { path: "/plugin2".to_string() })
.add_plugin(SdkPluginConfig::Local { path: "/plugin3".to_string() })
.build();
assert_eq!(options.plugins.len(), 3);
}
#[test]
fn test_message_result_with_structured_output() {
let json = r#"{
"type": "result",
"subtype": "success",
"cost_usd": 0.05,
"duration_ms": 1500,
"duration_api_ms": 1200,
"is_error": false,
"num_turns": 3,
"session_id": "session_123",
"structured_output": {"answer": 42}
}"#;
let msg: Message = serde_json::from_str(json).unwrap();
match msg {
Message::Result {
structured_output,
..
} => {
assert!(structured_output.is_some());
let output = structured_output.unwrap();
assert_eq!(output["answer"], 42);
}
_ => panic!("Expected Result message"),
}
}
#[test]
fn test_message_result_with_structured_output_camel_case() {
let json = r#"{
"type": "result",
"subtype": "success",
"cost_usd": 0.05,
"duration_ms": 1500,
"duration_api_ms": 1200,
"is_error": false,
"num_turns": 3,
"session_id": "session_123",
"structuredOutput": {"name": "test", "value": true}
}"#;
let msg: Message = serde_json::from_str(json).unwrap();
match msg {
Message::Result {
structured_output,
..
} => {
assert!(structured_output.is_some());
let output = structured_output.unwrap();
assert_eq!(output["name"], "test");
assert_eq!(output["value"], true);
}
_ => panic!("Expected Result message"),
}
}
#[test]
fn test_default_options_new_fields() {
let options = ClaudeCodeOptions::default();
assert!(options.tools.is_none());
assert!(options.betas.is_empty());
assert!(options.max_budget_usd.is_none());
assert!(options.fallback_model.is_none());
assert!(options.output_format.is_none());
assert!(!options.enable_file_checkpointing);
assert!(options.sandbox.is_none());
assert!(options.plugins.is_empty());
assert!(options.user.is_none());
assert!(!options.auto_download_cli);
}
}