use std::collections::HashMap;use std::io;use std::num::NonZeroUsize;use std::path::Path;use codex_utils_image::PromptImageMode;use codex_utils_image::data_url_from_bytes;use codex_utils_image::load_for_prompt_bytes;use serde::Deserialize;use serde::Deserializer;use serde::Serialize;use serde::ser::Serializer;use ts_rs::TS;use crate::permissions::FileSystemAccessMode;use crate::permissions::FileSystemPath;use crate::permissions::FileSystemSandboxEntry;use crate::permissions::FileSystemSandboxKind;use crate::permissions::FileSystemSandboxPolicy;use crate::permissions::FileSystemSpecialPath;use crate::permissions::NetworkSandboxPolicy;use crate::protocol::SandboxPolicy;use crate::user_input::UserInput;use codex_utils_absolute_path::AbsolutePathBuf;use codex_utils_image::ImageProcessingError;use codex_utils_path_uri::PathUri;use schemars::JsonSchema;use crate::mcp::CallToolResult;/// Controls the per-command sandbox override requested by a shell-like tool call.#[derive( Debug, Clone, Copy, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS,)]#[serde(rename_all = "snake_case")]pub enum SandboxPermissions { /// Run with the turn's configured sandbox policy unchanged. #[default] UseDefault, /// Request to run outside the sandbox. RequireEscalated, /// Request to stay in the sandbox while widening permissions for this /// command only. WithAdditionalPermissions,}impl SandboxPermissions { /// True if SandboxPermissions requires full unsandboxed execution (i.e. RequireEscalated) pub fn requires_escalated_permissions(self) -> bool { matches!(self, SandboxPermissions::RequireEscalated) } /// True if SandboxPermissions requests any explicit per-command override /// beyond `UseDefault`. pub fn requests_sandbox_override(self) -> bool { !matches!(self, SandboxPermissions::UseDefault) } /// True if SandboxPermissions uses the sandboxed per-command permission /// widening flow. pub fn uses_additional_permissions(self) -> bool { matches!(self, SandboxPermissions::WithAdditionalPermissions) }}#[derive(Debug, Clone, Eq, Hash, PartialEq, JsonSchema, TS)]pub struct FileSystemPermissions<PathType = AbsolutePathBuf> { pub entries: Vec<FileSystemSandboxEntry<PathType>>, pub glob_scan_max_depth: Option<NonZeroUsize>,}impl From<FileSystemPermissions<AbsolutePathBuf>> for FileSystemPermissions<PathUri> { fn from(value: FileSystemPermissions<AbsolutePathBuf>) -> Self { FileSystemPermissions { entries: value .entries .into_iter() .map(FileSystemSandboxEntry::<PathUri>::from) .collect(), glob_scan_max_depth: value.glob_scan_max_depth, } }}impl TryFrom<FileSystemPermissions<PathUri>> for FileSystemPermissions<AbsolutePathBuf> { type Error = io::Error; fn try_from(value: FileSystemPermissions<PathUri>) -> Result<Self, Self::Error> { Ok(FileSystemPermissions { entries: value .entries .into_iter() .map(FileSystemSandboxEntry::<AbsolutePathBuf>::try_from) .collect::<io::Result<_>>()?, glob_scan_max_depth: value.glob_scan_max_depth, }) }}impl<PathType> Default for FileSystemPermissions<PathType> { fn default() -> Self { Self { entries: Vec::new(), glob_scan_max_depth: None, } }}pub type LegacyReadWriteRoots<PathType = AbsolutePathBuf> = (Option<Vec<PathType>>, Option<Vec<PathType>>);impl<PathType> FileSystemPermissions<PathType> { pub fn is_empty(&self) -> bool { self.entries.is_empty() } pub fn from_read_write_roots( read: Option<Vec<PathType>>, write: Option<Vec<PathType>>, ) -> Self { let mut entries = Vec::new(); if let Some(read) = read { entries.extend(read.into_iter().map(|path| FileSystemSandboxEntry { path: FileSystemPath::Path { path }, access: FileSystemAccessMode::Read, })); } if let Some(write) = write { entries.extend(write.into_iter().map(|path| FileSystemSandboxEntry { path: FileSystemPath::Path { path }, access: FileSystemAccessMode::Write, })); } Self { entries, glob_scan_max_depth: None, } } pub fn explicit_path_entries(&self) -> impl Iterator<Item = (&PathType, FileSystemAccessMode)> { self.entries.iter().filter_map(|entry| match &entry.path { FileSystemPath::Path { path } => Some((path, entry.access)), FileSystemPath::GlobPattern { .. } | FileSystemPath::Special { .. } => None, }) } pub fn legacy_read_write_roots(&self) -> Option<LegacyReadWriteRoots<PathType>> where PathType: Clone, { self.as_legacy_permissions() .map(|legacy| (legacy.read, legacy.write)) } fn as_legacy_permissions(&self) -> Option<LegacyFileSystemPermissions<PathType>> where PathType: Clone, { if self.glob_scan_max_depth.is_some() { return None; } let mut read = Vec::new(); let mut write = Vec::new(); for entry in &self.entries { let FileSystemPath::Path { path } = &entry.path else { return None; }; match entry.access { FileSystemAccessMode::Read => read.push(path.clone()), FileSystemAccessMode::Write => write.push(path.clone()), FileSystemAccessMode::Deny => return None, } } Some(LegacyFileSystemPermissions { read: (!read.is_empty()).then_some(read), write: (!write.is_empty()).then_some(write), }) }}#[derive(Debug, Clone, Default, Eq, Hash, PartialEq, Serialize, Deserialize)]#[serde(deny_unknown_fields)]#[serde(bound(deserialize = "PathType: Deserialize<'de>"))]struct LegacyFileSystemPermissions<PathType = AbsolutePathBuf> { #[serde(default, skip_serializing_if = "Option::is_none")] read: Option<Vec<PathType>>, #[serde(default, skip_serializing_if = "Option::is_none")] write: Option<Vec<PathType>>,}#[derive(Debug, Clone, Default, Eq, Hash, PartialEq, Serialize, Deserialize)]#[serde(deny_unknown_fields)]#[serde(bound(deserialize = "PathType: Deserialize<'de>"))]struct CanonicalFileSystemPermissions<PathType = AbsolutePathBuf> { #[serde(default, skip_serializing_if = "Vec::is_empty")] entries: Vec<FileSystemSandboxEntry<PathType>>, #[serde(default, skip_serializing_if = "Option::is_none")] glob_scan_max_depth: Option<NonZeroUsize>,}#[derive(Debug, Clone, Deserialize)]#[serde(untagged)]enum FileSystemPermissionsDe<PathType = AbsolutePathBuf> { Canonical(CanonicalFileSystemPermissions<PathType>), Legacy(LegacyFileSystemPermissions<PathType>),}impl<PathType> Serialize for FileSystemPermissions<PathType>where PathType: Clone + Serialize,{ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer, { if let Some(legacy) = self.as_legacy_permissions() { legacy.serialize(serializer) } else { CanonicalFileSystemPermissions { entries: self.entries.clone(), glob_scan_max_depth: self.glob_scan_max_depth, } .serialize(serializer) } }}impl<'de, PathType> Deserialize<'de> for FileSystemPermissions<PathType>where PathType: Deserialize<'de>,{ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { match FileSystemPermissionsDe::deserialize(deserializer)? { FileSystemPermissionsDe::Canonical(CanonicalFileSystemPermissions { entries, glob_scan_max_depth, }) => Ok(Self { entries, glob_scan_max_depth, }), FileSystemPermissionsDe::Legacy(LegacyFileSystemPermissions { read, write }) => { Ok(Self::from_read_write_roots(read, write)) } } }}#[derive(Debug, Clone, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS)]pub struct NetworkPermissions { pub enabled: Option<bool>,}impl NetworkPermissions { pub fn is_empty(&self) -> bool { self.enabled.is_none() }}/// Partial permission overlay used for per-command requests and approved/// session/turn grants.#[derive(Debug, Clone, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS)]pub struct AdditionalPermissionProfile { pub network: Option<NetworkPermissions>, pub file_system: Option<FileSystemPermissions>,}impl AdditionalPermissionProfile { pub fn is_empty(&self) -> bool { self.network.is_none() && self.file_system.is_none() }}#[derive( Debug, Clone, Copy, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS,)]#[serde(rename_all = "snake_case")]pub enum SandboxEnforcement { /// Codex owns sandbox construction for this profile. #[default] Managed, /// No outer filesystem sandbox should be applied. Disabled, /// Filesystem isolation is enforced by an external caller. External,}impl SandboxEnforcement { pub fn from_legacy_sandbox_policy(sandbox_policy: &SandboxPolicy) -> Self { match sandbox_policy { SandboxPolicy::DangerFullAccess => Self::Disabled, SandboxPolicy::ExternalSandbox { .. } => Self::External, SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. } => Self::Managed, } }}/// Filesystem permissions for profiles where Codex owns sandbox construction.#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema, TS)]#[serde(tag = "type", rename_all = "snake_case")]#[ts(tag = "type")]pub enum ManagedFileSystemPermissions<PathType = AbsolutePathBuf> { /// Apply a managed filesystem sandbox from the listed entries. #[serde(rename_all = "snake_case")] #[ts(rename_all = "snake_case")] Restricted { entries: Vec<FileSystemSandboxEntry<PathType>>, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] glob_scan_max_depth: Option<NonZeroUsize>, }, /// Apply a managed sandbox that allows all filesystem access. Unrestricted,}impl From<ManagedFileSystemPermissions<AbsolutePathBuf>> for ManagedFileSystemPermissions<PathUri> { fn from(value: ManagedFileSystemPermissions<AbsolutePathBuf>) -> Self { match value { ManagedFileSystemPermissions::Restricted { entries, glob_scan_max_depth, } => ManagedFileSystemPermissions::Restricted { entries: entries .into_iter() .map(FileSystemSandboxEntry::<PathUri>::from) .collect(), glob_scan_max_depth, }, ManagedFileSystemPermissions::Unrestricted => { ManagedFileSystemPermissions::Unrestricted } } }}impl TryFrom<ManagedFileSystemPermissions<PathUri>> for ManagedFileSystemPermissions<AbsolutePathBuf>{ type Error = io::Error; fn try_from(value: ManagedFileSystemPermissions<PathUri>) -> Result<Self, Self::Error> { Ok(match value { ManagedFileSystemPermissions::Restricted { entries, glob_scan_max_depth, } => ManagedFileSystemPermissions::Restricted { entries: entries .into_iter() .map(FileSystemSandboxEntry::<AbsolutePathBuf>::try_from) .collect::<io::Result<_>>()?, glob_scan_max_depth, }, ManagedFileSystemPermissions::Unrestricted => { ManagedFileSystemPermissions::Unrestricted } }) }}impl ManagedFileSystemPermissions { fn from_sandbox_policy(file_system_sandbox_policy: &FileSystemSandboxPolicy) -> Self { match file_system_sandbox_policy.kind { FileSystemSandboxKind::Restricted => Self::Restricted { entries: file_system_sandbox_policy.entries.clone(), glob_scan_max_depth: file_system_sandbox_policy .glob_scan_max_depth .and_then(NonZeroUsize::new), }, FileSystemSandboxKind::Unrestricted => Self::Unrestricted, FileSystemSandboxKind::ExternalSandbox => unreachable!( "external filesystem policies are represented by PermissionProfile::External" ), } } pub fn to_sandbox_policy(&self) -> FileSystemSandboxPolicy { match self { Self::Restricted { entries, glob_scan_max_depth, } => FileSystemSandboxPolicy { kind: FileSystemSandboxKind::Restricted, glob_scan_max_depth: glob_scan_max_depth.map(usize::from), entries: entries.clone(), }, Self::Unrestricted => FileSystemSandboxPolicy::unrestricted(), } }}/// Reserved identifier for the built-in read-only permission profile.pub const BUILT_IN_PERMISSION_PROFILE_READ_ONLY: &str = ":read-only";/// Reserved identifier for the built-in workspace-write permission profile.pub const BUILT_IN_PERMISSION_PROFILE_WORKSPACE: &str = ":workspace";/// Reserved identifier for the built-in full-access permission profile.pub const BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS: &str = ":danger-full-access";/// Canonical active runtime permissions for a conversation, turn, or command.#[derive(Debug, Clone, Eq, PartialEq, Serialize, JsonSchema, TS)]#[serde(tag = "type", rename_all = "snake_case")]#[ts(tag = "type")]pub enum PermissionProfile<PathType = AbsolutePathBuf> { /// Codex owns sandbox construction for this profile. #[serde(rename_all = "snake_case")] #[ts(rename_all = "snake_case")] Managed { file_system: ManagedFileSystemPermissions<PathType>, network: NetworkSandboxPolicy, }, /// Do not apply an outer sandbox. Disabled, /// Filesystem isolation is enforced by an external caller. #[serde(rename_all = "snake_case")] #[ts(rename_all = "snake_case")] External { network: NetworkSandboxPolicy },}impl From<PermissionProfile<AbsolutePathBuf>> for PermissionProfile<PathUri> { fn from(value: PermissionProfile<AbsolutePathBuf>) -> Self { match value { PermissionProfile::Managed { file_system, network, } => PermissionProfile::Managed { file_system: file_system.into(), network, }, PermissionProfile::Disabled => PermissionProfile::Disabled, PermissionProfile::External { network } => PermissionProfile::External { network }, } }}impl TryFrom<PermissionProfile<PathUri>> for PermissionProfile<AbsolutePathBuf> { type Error = io::Error; fn try_from(value: PermissionProfile<PathUri>) -> Result<Self, Self::Error> { Ok(match value { PermissionProfile::Managed { file_system, network, } => PermissionProfile::Managed { file_system: file_system.try_into()?, network, }, PermissionProfile::Disabled => PermissionProfile::Disabled, PermissionProfile::External { network } => PermissionProfile::External { network }, }) }}/// Metadata for the named or implicit built-in permissions profile that/// produced the active `PermissionProfile`.////// The runtime must honor `PermissionProfile`; this sidecar exists so clients/// can display stable profile identity without trying to reverse-engineer a/// name from the compiled permissions.#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, JsonSchema, TS)]pub struct ActivePermissionProfile { /// Profile identifier from `default_permissions` or the implicit built-in /// default, such as `:workspace` or a user-defined `[permissions.<id>]` /// profile. pub id: String, /// Optional parent profile identifier from the selected permissions /// profile's `extends` setting. #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub extends: Option<String>,}impl ActivePermissionProfile { pub fn new(id: impl Into<String>) -> Self { Self { id: id.into(), extends: None, } } pub fn read_only() -> Self { Self::new(BUILT_IN_PERMISSION_PROFILE_READ_ONLY) }}impl<PathType> Default for PermissionProfile<PathType> { fn default() -> Self { Self::Managed { file_system: ManagedFileSystemPermissions::Restricted { entries: Vec::new(), glob_scan_max_depth: None, }, network: NetworkSandboxPolicy::Restricted, } }}impl PermissionProfile { /// Managed read-only filesystem access with restricted network access. pub fn read_only() -> Self { let file_system = FileSystemSandboxPolicy::read_only(); Self::Managed { file_system: ManagedFileSystemPermissions::from_sandbox_policy(&file_system), network: NetworkSandboxPolicy::Restricted, } } /// Managed workspace-write filesystem access with restricted network /// access. /// /// The returned profile contains symbolic `:workspace_roots` entries that /// must be resolved against the active permission root before enforcement. pub fn workspace_write() -> Self { Self::workspace_write_with( &[], NetworkSandboxPolicy::Restricted, /*exclude_tmpdir_env_var*/ false, /*exclude_slash_tmp*/ false, ) } /// Managed workspace-write filesystem access with the legacy /// `sandbox_workspace_write` knobs applied directly to the profile. /// /// The returned profile contains symbolic `:workspace_roots` entries that /// must be resolved against the active permission root before enforcement. pub fn workspace_write_with( writable_roots: &[AbsolutePathBuf], network: NetworkSandboxPolicy, exclude_tmpdir_env_var: bool, exclude_slash_tmp: bool, ) -> Self { let file_system = FileSystemSandboxPolicy::workspace_write( writable_roots, exclude_tmpdir_env_var, exclude_slash_tmp, ); Self::Managed { file_system: ManagedFileSystemPermissions::from_sandbox_policy(&file_system), network, } } pub fn materialize_project_roots_with_workspace_roots( self, workspace_roots: &[AbsolutePathBuf], ) -> Self { match self { Self::Managed { file_system, network, } => { let file_system = file_system .to_sandbox_policy() .materialize_project_roots_with_workspace_roots(workspace_roots); Self::Managed { file_system: ManagedFileSystemPermissions::from_sandbox_policy(&file_system), network, } } Self::Disabled => Self::Disabled, Self::External { network } => Self::External { network }, } } pub fn from_runtime_permissions( file_system_sandbox_policy: &FileSystemSandboxPolicy, network_sandbox_policy: NetworkSandboxPolicy, ) -> Self { let enforcement = match file_system_sandbox_policy.kind { FileSystemSandboxKind::Restricted | FileSystemSandboxKind::Unrestricted => { SandboxEnforcement::Managed } FileSystemSandboxKind::ExternalSandbox => SandboxEnforcement::External, }; Self::from_runtime_permissions_with_enforcement( enforcement, file_system_sandbox_policy, network_sandbox_policy, ) } pub fn from_runtime_permissions_with_enforcement( enforcement: SandboxEnforcement, file_system_sandbox_policy: &FileSystemSandboxPolicy, network_sandbox_policy: NetworkSandboxPolicy, ) -> Self { match file_system_sandbox_policy.kind { FileSystemSandboxKind::ExternalSandbox => Self::External { network: network_sandbox_policy, }, FileSystemSandboxKind::Unrestricted if enforcement == SandboxEnforcement::Disabled => { Self::Disabled } FileSystemSandboxKind::Restricted | FileSystemSandboxKind::Unrestricted => { Self::Managed { file_system: ManagedFileSystemPermissions::from_sandbox_policy( file_system_sandbox_policy, ), network: network_sandbox_policy, } } } } pub fn from_legacy_sandbox_policy(sandbox_policy: &SandboxPolicy) -> Self { Self::from_runtime_permissions_with_enforcement( SandboxEnforcement::from_legacy_sandbox_policy(sandbox_policy), &FileSystemSandboxPolicy::from(sandbox_policy), NetworkSandboxPolicy::from(sandbox_policy), ) } pub fn from_legacy_sandbox_policy_for_cwd(sandbox_policy: &SandboxPolicy, cwd: &Path) -> Self { Self::from_runtime_permissions_with_enforcement( SandboxEnforcement::from_legacy_sandbox_policy(sandbox_policy), &FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(sandbox_policy, cwd), NetworkSandboxPolicy::from(sandbox_policy), ) } pub fn enforcement(&self) -> SandboxEnforcement { match self { Self::Managed { .. } => SandboxEnforcement::Managed, Self::Disabled => SandboxEnforcement::Disabled, Self::External { .. } => SandboxEnforcement::External, } } pub fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy { match self { Self::Managed { file_system, .. } => file_system.to_sandbox_policy(), Self::Disabled => FileSystemSandboxPolicy::unrestricted(), Self::External { .. } => FileSystemSandboxPolicy::external_sandbox(), } } pub fn network_sandbox_policy(&self) -> NetworkSandboxPolicy { match self { Self::Managed { network, .. } | Self::External { network } => *network, Self::Disabled => NetworkSandboxPolicy::Enabled, } } pub fn to_legacy_sandbox_policy(&self, cwd: &Path) -> io::Result<SandboxPolicy> { match self { Self::Managed { file_system, network, } => file_system .to_sandbox_policy() .to_legacy_sandbox_policy(*network, cwd), Self::Disabled => Ok(SandboxPolicy::DangerFullAccess), Self::External { network } => Ok(SandboxPolicy::ExternalSandbox { network_access: if network.is_enabled() { crate::protocol::NetworkAccess::Enabled } else { crate::protocol::NetworkAccess::Restricted }, }), } } pub fn to_runtime_permissions(&self) -> (FileSystemSandboxPolicy, NetworkSandboxPolicy) { ( self.file_system_sandbox_policy(), self.network_sandbox_policy(), ) }}#[derive(Debug, Clone, Deserialize)]#[serde(tag = "type", rename_all = "snake_case")]enum TaggedPermissionProfile<PathType = AbsolutePathBuf> { #[serde(rename_all = "snake_case")] Managed { file_system: ManagedFileSystemPermissions<PathType>, network: NetworkSandboxPolicy, }, Disabled, #[serde(rename_all = "snake_case")] External { network: NetworkSandboxPolicy, },}impl<PathType> From<TaggedPermissionProfile<PathType>> for PermissionProfile<PathType> { fn from(value: TaggedPermissionProfile<PathType>) -> Self { match value { TaggedPermissionProfile::Managed { file_system, network, } => Self::Managed { file_system, network, }, TaggedPermissionProfile::Disabled => Self::Disabled, TaggedPermissionProfile::External { network } => Self::External { network }, } }}/// Pre-tagged shape written to rollout files before `PermissionProfile`/// represented enforcement explicitly.#[derive(Debug, Clone, Default, Deserialize)]#[serde(deny_unknown_fields)]struct LegacyPermissionProfile<PathType = AbsolutePathBuf> { network: Option<NetworkPermissions>, file_system: Option<FileSystemPermissions<PathType>>,}impl<PathType> From<LegacyPermissionProfile<PathType>> for PermissionProfile<PathType> { fn from(value: LegacyPermissionProfile<PathType>) -> Self { let file_system = value.file_system.map_or_else( || ManagedFileSystemPermissions::Restricted { entries: Vec::new(), glob_scan_max_depth: None, }, |permissions| ManagedFileSystemPermissions::Restricted { entries: permissions.entries, glob_scan_max_depth: permissions.glob_scan_max_depth, }, ); let network_sandbox_policy = if value .network .as_ref() .and_then(|network| network.enabled) .unwrap_or(false) { NetworkSandboxPolicy::Enabled } else { NetworkSandboxPolicy::Restricted }; Self::Managed { file_system, network: network_sandbox_policy, } }}#[derive(Debug, Clone, Deserialize)]#[serde(untagged)]enum PermissionProfileDe<PathType = AbsolutePathBuf> { Tagged(TaggedPermissionProfile<PathType>), Legacy(LegacyPermissionProfile<PathType>),}impl<'de, PathType> Deserialize<'de> for PermissionProfile<PathType>where PathType: Deserialize<'de>,{ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { Ok(match PermissionProfileDe::deserialize(deserializer)? { PermissionProfileDe::Tagged(tagged) => tagged.into(), PermissionProfileDe::Legacy(legacy) => legacy.into(), }) }}impl From<NetworkSandboxPolicy> for NetworkPermissions { fn from(value: NetworkSandboxPolicy) -> Self { Self { enabled: Some(value.is_enabled()), } }}impl From<&FileSystemSandboxPolicy> for FileSystemPermissions { fn from(value: &FileSystemSandboxPolicy) -> Self { let entries = match value.kind { FileSystemSandboxKind::Restricted => value.entries.clone(), FileSystemSandboxKind::Unrestricted | FileSystemSandboxKind::ExternalSandbox => { vec![FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Write, }] } }; Self { entries, glob_scan_max_depth: value.glob_scan_max_depth.and_then(NonZeroUsize::new), } }}impl From<&FileSystemPermissions> for FileSystemSandboxPolicy { fn from(value: &FileSystemPermissions) -> Self { let mut policy = FileSystemSandboxPolicy::restricted(value.entries.clone()); policy.glob_scan_max_depth = value.glob_scan_max_depth.map(usize::from); policy }}#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]#[serde(tag = "type", rename_all = "snake_case")]pub enum ResponseInputItem { Message { role: String, content: Vec<ContentItem>, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] phase: Option<MessagePhase>, }, FunctionCallOutput { call_id: String, #[ts(as = "FunctionCallOutputBody")] #[schemars(with = "FunctionCallOutputBody")] output: FunctionCallOutputPayload, }, McpToolCallOutput { call_id: String, output: CallToolResult, }, CustomToolCallOutput { call_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] name: Option<String>, #[ts(as = "FunctionCallOutputBody")] #[schemars(with = "FunctionCallOutputBody")] output: FunctionCallOutputPayload, }, ToolSearchOutput { call_id: String, status: String, execution: String, #[ts(type = "unknown[]")] tools: Vec<serde_json::Value>, },}#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]#[serde(tag = "type", rename_all = "snake_case")]pub enum ContentItem { InputText { text: String, }, InputImage { image_url: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] detail: Option<ImageDetail>, }, OutputText { text: String, },}#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]#[serde(tag = "type", rename_all = "snake_case")]pub enum AgentMessageInputContent { InputText { text: String }, EncryptedContent { encrypted_content: String },}/// Returns the locally readable text when an agent message is entirely plaintext.pub fn plaintext_agent_message_content(content: &[AgentMessageInputContent]) -> Option<String> { let mut text_parts = Vec::with_capacity(content.len()); for part in content { match part { AgentMessageInputContent::InputText { text } => text_parts.push(text.as_str()), AgentMessageInputContent::EncryptedContent { .. } => return None, } } let text = text_parts.join("\n"); (!text.trim().is_empty()).then_some(text)}#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "lowercase")]pub enum ImageDetail { Auto, Low, High, Original,}pub const DEFAULT_IMAGE_DETAIL: ImageDetail = ImageDetail::High;#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]/// Classifies an assistant message as interim commentary or final answer text.////// Providers do not emit this consistently, so callers must treat `None` as/// "phase unknown" and keep compatibility behavior for legacy models.pub enum MessagePhase { /// Mid-turn assistant text (for example preamble/progress narration). /// /// Additional tool calls or assistant output may follow before turn /// completion. Commentary, /// The assistant's terminal answer text for the current turn. FinalAnswer,}#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]pub struct ResponseItemMetadata { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub turn_id: Option<String>,}#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]#[serde(tag = "type", rename_all = "snake_case")]pub enum ResponseItem { Message { #[serde(default, skip_serializing)] #[ts(skip)] id: Option<String>, role: String, content: Vec<ContentItem>, // Optional output-message phase (for example: "commentary", "final_answer"). // Availability varies by provider/model, so downstream consumers must // preserve fallback behavior when this is absent. #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] phase: Option<MessagePhase>, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] metadata: Option<ResponseItemMetadata>, }, AgentMessage { author: String, recipient: String, content: Vec<AgentMessageInputContent>, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] metadata: Option<ResponseItemMetadata>, }, Reasoning { #[serde(default, skip_serializing)] #[ts(skip)] #[schemars(skip)] id: String, summary: Vec<ReasoningItemReasoningSummary>, #[serde(default, skip_serializing_if = "should_serialize_reasoning_content")] #[ts(optional)] content: Option<Vec<ReasoningItemContent>>, encrypted_content: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] metadata: Option<ResponseItemMetadata>, }, LocalShellCall { /// Legacy id field retained for compatibility with older payloads. #[serde(default, skip_serializing)] #[ts(skip)] id: Option<String>, /// Set when using the Responses API. call_id: Option<String>, status: LocalShellStatus, action: LocalShellAction, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] metadata: Option<ResponseItemMetadata>, }, FunctionCall { #[serde(default, skip_serializing)] #[ts(skip)] id: Option<String>, name: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] namespace: Option<String>, // The Responses API returns the function call arguments as a *string* that contains // JSON, not as an already‑parsed object. We keep it as a raw string here and let // Session::handle_function_call parse it into a Value. arguments: String, call_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] metadata: Option<ResponseItemMetadata>, }, ToolSearchCall { #[serde(default, skip_serializing)] #[ts(skip)] id: Option<String>, call_id: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] status: Option<String>, execution: String, #[ts(type = "unknown")] arguments: serde_json::Value, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] metadata: Option<ResponseItemMetadata>, }, // NOTE: The `output` field for `function_call_output` uses a dedicated payload type with // custom serialization. On the wire it is either: // - a plain string (`content`) // - an array of structured content items (`content_items`) // We keep this behavior centralized in `FunctionCallOutputPayload`. FunctionCallOutput { call_id: String, #[ts(as = "FunctionCallOutputBody")] #[schemars(with = "FunctionCallOutputBody")] output: FunctionCallOutputPayload, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] metadata: Option<ResponseItemMetadata>, }, CustomToolCall { #[serde(default, skip_serializing)] #[ts(skip)] id: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] status: Option<String>, call_id: String, name: String, input: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] metadata: Option<ResponseItemMetadata>, }, // `custom_tool_call_output.output` uses the same wire encoding as // `function_call_output.output` so freeform tools can return either plain // text or structured content items. CustomToolCallOutput { call_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] name: Option<String>, #[ts(as = "FunctionCallOutputBody")] #[schemars(with = "FunctionCallOutputBody")] output: FunctionCallOutputPayload, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] metadata: Option<ResponseItemMetadata>, }, ToolSearchOutput { call_id: Option<String>, status: String, execution: String, #[ts(type = "unknown[]")] tools: Vec<serde_json::Value>, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] metadata: Option<ResponseItemMetadata>, }, // Emitted by the Responses API when the agent triggers a web search. // Example payload (from SSE `response.output_item.done`): // { // "id":"ws_...", // "type":"web_search_call", // "status":"completed", // "action": {"type":"search","query":"weather: San Francisco, CA"} // } WebSearchCall { #[serde(default, skip_serializing)] #[ts(skip)] id: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] status: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] action: Option<WebSearchAction>, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] metadata: Option<ResponseItemMetadata>, }, // Emitted by the Responses API when the agent triggers image generation. // Example payload: // { // "id":"ig_123", // "type":"image_generation_call", // "status":"completed", // "revised_prompt":"A gray tabby cat hugging an otter...", // "result":"..." // } ImageGenerationCall { id: String, status: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] revised_prompt: Option<String>, result: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] metadata: Option<ResponseItemMetadata>, }, #[serde(alias = "compaction_summary")] Compaction { encrypted_content: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] metadata: Option<ResponseItemMetadata>, }, CompactionTrigger { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] metadata: Option<ResponseItemMetadata>, }, ContextCompaction { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] encrypted_content: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] metadata: Option<ResponseItemMetadata>, }, #[serde(other)] Other,}impl ResponseItem { /// Returns whether this item is an ordinary user-role message. pub fn is_user_message(&self) -> bool { matches!(self, Self::Message { role, .. } if role == "user") } /// Returns the non-empty turn ID stamped onto this item, if present. pub fn turn_id(&self) -> Option<&str> { self.metadata() .and_then(|metadata| metadata.turn_id.as_deref()) .filter(|turn_id| !turn_id.is_empty()) } /// Stamps the item with `turn_id` unless it already has a non-empty turn ID. pub fn stamp_turn_id_if_missing(&mut self, turn_id: &str) { if turn_id.is_empty() || self.turn_id().is_some() { return; } let Some(metadata) = self.metadata_mut() else { return; }; metadata .get_or_insert_with(ResponseItemMetadata::default) .turn_id = Some(turn_id.to_string()); } /// Removes Responses API item metadata before sending to a provider that does not accept it. pub fn clear_metadata(&mut self) { if let Some(metadata) = self.metadata_mut() { *metadata = None; } } fn metadata(&self) -> Option<&ResponseItemMetadata> { match self { Self::Message { metadata, .. } | Self::AgentMessage { metadata, .. } | Self::Reasoning { metadata, .. } | Self::LocalShellCall { metadata, .. } | Self::FunctionCall { metadata, .. } | Self::ToolSearchCall { metadata, .. } | Self::FunctionCallOutput { metadata, .. } | Self::CustomToolCall { metadata, .. } | Self::CustomToolCallOutput { metadata, .. } | Self::ToolSearchOutput { metadata, .. } | Self::WebSearchCall { metadata, .. } | Self::ImageGenerationCall { metadata, .. } | Self::Compaction { metadata, .. } | Self::CompactionTrigger { metadata } | Self::ContextCompaction { metadata, .. } => metadata.as_ref(), Self::Other => None, } } fn metadata_mut(&mut self) -> Option<&mut Option<ResponseItemMetadata>> { match self { Self::Message { metadata, .. } | Self::AgentMessage { metadata, .. } | Self::Reasoning { metadata, .. } | Self::LocalShellCall { metadata, .. } | Self::FunctionCall { metadata, .. } | Self::ToolSearchCall { metadata, .. } | Self::FunctionCallOutput { metadata, .. } | Self::CustomToolCall { metadata, .. } | Self::CustomToolCallOutput { metadata, .. } | Self::ToolSearchOutput { metadata, .. } | Self::WebSearchCall { metadata, .. } | Self::ImageGenerationCall { metadata, .. } | Self::Compaction { metadata, .. } | Self::CompactionTrigger { metadata } | Self::ContextCompaction { metadata, .. } => Some(metadata), Self::Other => None, } }}pub const BASE_INSTRUCTIONS_DEFAULT: &str = include_str!("prompts/base_instructions/default.md");/// Base instructions for the model in a thread. Corresponds to the `instructions` field in the ResponsesAPI.#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]#[serde(rename = "base_instructions", rename_all = "snake_case")]pub struct BaseInstructions { pub text: String,}impl Default for BaseInstructions { fn default() -> Self { Self { text: BASE_INSTRUCTIONS_DEFAULT.to_string(), } }}const MAX_RENDERED_PREFIXES: usize = 100;const MAX_ALLOW_PREFIX_TEXT_BYTES: usize = 5000;const TRUNCATED_MARKER: &str = "...\n[Some commands were truncated]";pub fn format_allow_prefixes(prefixes: Vec<Vec<String>>) -> Option<String> { let mut truncated = false; if prefixes.len() > MAX_RENDERED_PREFIXES { truncated = true; } let mut prefixes = prefixes; prefixes.sort_by(|a, b| { a.len() .cmp(&b.len()) .then_with(|| prefix_combined_str_len(a).cmp(&prefix_combined_str_len(b))) .then_with(|| a.cmp(b)) }); let full_text = prefixes .into_iter() .take(MAX_RENDERED_PREFIXES) .map(|prefix| format!("- {}", render_command_prefix(&prefix))) .collect::<Vec<_>>() .join("\n"); // truncate to last UTF8 char let mut output = full_text; let byte_idx = output .char_indices() .nth(MAX_ALLOW_PREFIX_TEXT_BYTES) .map(|(i, _)| i); if let Some(byte_idx) = byte_idx { truncated = true; output = output[..byte_idx].to_string(); } if truncated { Some(format!("{output}{TRUNCATED_MARKER}")) } else { Some(output) }}fn prefix_combined_str_len(prefix: &[String]) -> usize { prefix.iter().map(String::len).sum()}fn render_command_prefix(prefix: &[String]) -> String { let tokens = prefix .iter() .map(|token| serde_json::to_string(token).unwrap_or_else(|_| format!("{token:?}"))) .collect::<Vec<_>>() .join(", "); format!("[{tokens}]")}fn should_serialize_reasoning_content(content: &Option<Vec<ReasoningItemContent>>) -> bool { match content { Some(content) => !content .iter() .any(|c| matches!(c, ReasoningItemContent::ReasoningText { .. })), None => false, }}fn local_image_error_placeholder( path: &std::path::Path, error: impl std::fmt::Display,) -> ContentItem { ContentItem::InputText { text: format!( "Codex could not read the local image at `{}`: {}", path.display(), error ), }}pub const VIEW_IMAGE_TOOL_NAME: &str = "view_image";const IMAGE_OPEN_TAG: &str = "<image>";const IMAGE_CLOSE_TAG: &str = "</image>";const LOCAL_IMAGE_OPEN_TAG_PREFIX: &str = "<image name=";const LOCAL_IMAGE_OPEN_TAG_SUFFIX: &str = ">";const LOCAL_IMAGE_CLOSE_TAG: &str = IMAGE_CLOSE_TAG;pub fn image_open_tag_text() -> String { IMAGE_OPEN_TAG.to_string()}pub fn image_close_tag_text() -> String { IMAGE_CLOSE_TAG.to_string()}pub fn local_image_label_text(label_number: usize) -> String { format!("[Image #{label_number}]")}pub fn local_image_open_tag_text_with_path(label_number: usize, path: &std::path::Path) -> String { let label = local_image_label_text(label_number); let path = path.display(); format!("{LOCAL_IMAGE_OPEN_TAG_PREFIX}{label} path=\"{path}\"{LOCAL_IMAGE_OPEN_TAG_SUFFIX}")}pub fn is_local_image_open_tag_text(text: &str) -> bool { text.strip_prefix(LOCAL_IMAGE_OPEN_TAG_PREFIX) .is_some_and(|rest| rest.ends_with(LOCAL_IMAGE_OPEN_TAG_SUFFIX))}pub fn is_local_image_close_tag_text(text: &str) -> bool { is_image_close_tag_text(text)}pub fn is_image_open_tag_text(text: &str) -> bool { text == IMAGE_OPEN_TAG}pub fn is_image_close_tag_text(text: &str) -> bool { text == IMAGE_CLOSE_TAG}fn invalid_image_error_placeholder( path: &std::path::Path, error: impl std::fmt::Display,) -> ContentItem { ContentItem::InputText { text: format!( "Image located at `{}` is invalid: {}", path.display(), error ), }}fn unsupported_image_error_placeholder(path: &std::path::Path, mime: &str) -> ContentItem { ContentItem::InputText { text: format!( "Codex cannot attach image at `{}`: unsupported image `{}`.", path.display(), mime ), }}pub fn local_image_content_items_with_label_number( path: &std::path::Path, file_bytes: Vec<u8>, label_number: Option<usize>, detail: ImageDetail,) -> Vec<ContentItem> { let mode = match detail { ImageDetail::Original => PromptImageMode::Original, ImageDetail::Auto | ImageDetail::Low | ImageDetail::High => PromptImageMode::ResizeToFit, }; match load_for_prompt_bytes(path, file_bytes, mode) { Ok(image) => local_image_content_items(path, image.into_data_url(), label_number, detail), Err(err) => match &err { ImageProcessingError::Read { .. } | ImageProcessingError::Encode { .. } | ImageProcessingError::InvalidDataUrl { .. } | ImageProcessingError::ImageTooLarge { .. } => { vec![local_image_error_placeholder(path, &err)] } ImageProcessingError::Decode { .. } if err.is_invalid_image() => { vec![invalid_image_error_placeholder(path, &err)] } ImageProcessingError::Decode { .. } => { vec![local_image_error_placeholder(path, &err)] } ImageProcessingError::UnsupportedImageFormat { mime } => { vec![unsupported_image_error_placeholder(path, mime)] } }, }}#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum LocalImagePreparation { Process, Defer,}fn local_image_content_items( path: &std::path::Path, image_url: String, label_number: Option<usize>, detail: ImageDetail,) -> Vec<ContentItem> { let mut items = Vec::with_capacity(3); if let Some(label_number) = label_number { items.push(ContentItem::InputText { text: local_image_open_tag_text_with_path(label_number, path), }); } items.push(ContentItem::InputImage { image_url, detail: Some(detail), }); if label_number.is_some() { items.push(ContentItem::InputText { text: LOCAL_IMAGE_CLOSE_TAG.to_string(), }); } items}impl From<ResponseInputItem> for ResponseItem { fn from(item: ResponseInputItem) -> Self { match item { ResponseInputItem::Message { role, content, phase, } => Self::Message { role, content, id: None, phase, metadata: None, }, ResponseInputItem::FunctionCallOutput { call_id, output } => Self::FunctionCallOutput { call_id, output, metadata: None, }, ResponseInputItem::McpToolCallOutput { call_id, output } => { let output = output.into_function_call_output_payload(); Self::FunctionCallOutput { call_id, output, metadata: None, } } ResponseInputItem::CustomToolCallOutput { call_id, name, output, } => Self::CustomToolCallOutput { call_id, name, output, metadata: None, }, ResponseInputItem::ToolSearchOutput { call_id, status, execution, tools, } => Self::ToolSearchOutput { call_id: Some(call_id), status, execution, tools, metadata: None, }, } }}#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]#[serde(rename_all = "snake_case")]pub enum LocalShellStatus { Completed, InProgress, Incomplete,}#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]#[serde(tag = "type", rename_all = "snake_case")]pub enum LocalShellAction { Exec(LocalShellExecAction),}#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]pub struct LocalShellExecAction { pub command: Vec<String>, pub timeout_ms: Option<u64>, pub working_directory: Option<String>, pub env: Option<HashMap<String, String>>, pub user: Option<String>,}#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]#[serde(tag = "type", rename_all = "snake_case")]#[schemars(rename = "ResponsesApiWebSearchAction")]pub enum WebSearchAction { Search { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] query: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] queries: Option<Vec<String>>, }, OpenPage { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] url: Option<String>, }, FindInPage { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] url: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pattern: Option<String>, }, #[serde(other)] Other,}#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]#[serde(tag = "type", rename_all = "snake_case")]pub enum ReasoningItemReasoningSummary { SummaryText { text: String },}#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]#[serde(tag = "type", rename_all = "snake_case")]pub enum ReasoningItemContent { ReasoningText { text: String }, Text { text: String },}impl From<Vec<UserInput>> for ResponseInputItem { fn from(items: Vec<UserInput>) -> Self { Self::from_user_input(items, LocalImagePreparation::Process) }}impl ResponseInputItem { pub fn from_user_input( items: Vec<UserInput>, local_image_preparation: LocalImagePreparation, ) -> Self { let mut image_index = 0; Self::Message { role: "user".to_string(), content: items .into_iter() .flat_map(|c| match c { UserInput::Text { text, .. } => vec![ContentItem::InputText { text }], UserInput::Image { image_url, detail, .. } => { image_index += 1; let detail = detail.unwrap_or(DEFAULT_IMAGE_DETAIL); vec![ContentItem::InputImage { image_url, detail: Some(detail), }] } UserInput::LocalImage { path, detail, .. } => { image_index += 1; let detail = detail.unwrap_or(DEFAULT_IMAGE_DETAIL); match std::fs::read(&path) { Ok(file_bytes) => match local_image_preparation { LocalImagePreparation::Process => { local_image_content_items_with_label_number( &path, file_bytes, Some(image_index), detail, ) } LocalImagePreparation::Defer => local_image_content_items( &path, data_url_from_bytes("application/octet-stream", &file_bytes), Some(image_index), detail, ), }, Err(err) => vec![local_image_error_placeholder(&path, err)], } } UserInput::Skill { .. } | UserInput::Mention { .. } => Vec::new(), // Tool bodies are injected later in core }) .collect::<Vec<ContentItem>>(), phase: None, } }}#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]pub struct SearchToolCallParams { pub query: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub limit: Option<usize>,}/// If the `name` of a `ResponseItem::FunctionCall` is `shell_command`, the/// `arguments` field should deserialize to this struct.#[derive(Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]pub struct ShellCommandToolCallParams { pub command: String, pub workdir: Option<String>, /// Whether to run the shell with login shell semantics #[serde(skip_serializing_if = "Option::is_none")] pub login: Option<bool>, /// This is the maximum time in milliseconds that the command is allowed to run. #[serde(alias = "timeout")] pub timeout_ms: Option<u64>, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub sandbox_permissions: Option<SandboxPermissions>, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub prefix_rule: Option<Vec<String>>, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub additional_permissions: Option<AdditionalPermissionProfile>, #[serde(skip_serializing_if = "Option::is_none")] pub justification: Option<String>,}/// Responses API compatible content items that can be returned by a tool call./// This is a subset of ContentItem with the types we support as function call outputs.#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]#[serde(tag = "type", rename_all = "snake_case")]pub enum FunctionCallOutputContentItem { // Do not rename, these are serialized and used directly in the responses API. InputText { text: String, }, // Do not rename, these are serialized and used directly in the responses API. InputImage { image_url: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] detail: Option<ImageDetail>, }, EncryptedContent { encrypted_content: String, },}/// Converts structured function-call output content into plain text for/// human-readable surfaces.////// This conversion is intentionally lossy:/// - only `input_text` items are included/// - image items are ignored////// We use this helper where callers still need a string representation (for/// example telemetry previews or legacy string-only output paths) while keeping/// the original multimodal `content_items` as the authoritative payload sent to/// the model.pub fn function_call_output_content_items_to_text( content_items: &[FunctionCallOutputContentItem],) -> Option<String> { let text_segments = content_items .iter() .filter_map(|item| match item { FunctionCallOutputContentItem::InputText { text } if !text.trim().is_empty() => { Some(text.as_str()) } FunctionCallOutputContentItem::InputText { .. } | FunctionCallOutputContentItem::InputImage { .. } | FunctionCallOutputContentItem::EncryptedContent { .. } => None, }) .collect::<Vec<_>>(); if text_segments.is_empty() { None } else { Some(text_segments.join("\n")) }}impl From<crate::dynamic_tools::DynamicToolCallOutputContentItem> for FunctionCallOutputContentItem{ fn from(item: crate::dynamic_tools::DynamicToolCallOutputContentItem) -> Self { match item { crate::dynamic_tools::DynamicToolCallOutputContentItem::InputText { text } => { Self::InputText { text } } crate::dynamic_tools::DynamicToolCallOutputContentItem::InputImage { image_url } => { Self::InputImage { image_url, detail: Some(DEFAULT_IMAGE_DETAIL), } } } }}/// The payload we send back to OpenAI when reporting a tool call result.////// `body` serializes directly as the wire value for `function_call_output.output`./// `success` remains internal metadata for downstream handling.#[derive(Debug, Default, Clone, PartialEq, JsonSchema, TS)]pub struct FunctionCallOutputPayload { pub body: FunctionCallOutputBody, pub success: Option<bool>,}#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]#[serde(untagged)]pub enum FunctionCallOutputBody { Text(String), ContentItems(Vec<FunctionCallOutputContentItem>),}impl FunctionCallOutputBody { /// Best-effort conversion of a function-call output body to plain text for /// human-readable surfaces. /// /// This conversion is intentionally lossy when the body contains content /// items: image entries are dropped and text entries are joined with /// newlines. pub fn to_text(&self) -> Option<String> { match self { Self::Text(content) => Some(content.clone()), Self::ContentItems(items) => function_call_output_content_items_to_text(items), } }}impl Default for FunctionCallOutputBody { fn default() -> Self { Self::Text(String::new()) }}impl FunctionCallOutputPayload { pub fn from_text(content: String) -> Self { Self { body: FunctionCallOutputBody::Text(content), success: None, } } pub fn from_content_items(content_items: Vec<FunctionCallOutputContentItem>) -> Self { Self { body: FunctionCallOutputBody::ContentItems(content_items), success: None, } } pub fn text_content(&self) -> Option<&str> { match &self.body { FunctionCallOutputBody::Text(content) => Some(content), FunctionCallOutputBody::ContentItems(_) => None, } } pub fn text_content_mut(&mut self) -> Option<&mut String> { match &mut self.body { FunctionCallOutputBody::Text(content) => Some(content), FunctionCallOutputBody::ContentItems(_) => None, } } pub fn content_items(&self) -> Option<&[FunctionCallOutputContentItem]> { match &self.body { FunctionCallOutputBody::Text(_) => None, FunctionCallOutputBody::ContentItems(items) => Some(items), } } pub fn content_items_mut(&mut self) -> Option<&mut Vec<FunctionCallOutputContentItem>> { match &mut self.body { FunctionCallOutputBody::Text(_) => None, FunctionCallOutputBody::ContentItems(items) => Some(items), } }}// `function_call_output.output` is encoded as either:// - an array of structured content items// - a plain stringimpl Serialize for FunctionCallOutputPayload { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer, { match &self.body { FunctionCallOutputBody::Text(content) => serializer.serialize_str(content), FunctionCallOutputBody::ContentItems(items) => items.serialize(serializer), } }}impl<'de> Deserialize<'de> for FunctionCallOutputPayload { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { let body = FunctionCallOutputBody::deserialize(deserializer)?; Ok(FunctionCallOutputPayload { body, success: None, }) }}impl CallToolResult { pub fn from_result(result: Result<Self, String>) -> Self { match result { Ok(result) => result, Err(error) => Self::from_error_text(error), } } pub fn from_error_text(text: String) -> Self { Self { content: vec![serde_json::json!({ "type": "text", "text": text, })], structured_content: None, is_error: Some(true), meta: None, } } pub fn success(&self) -> bool { self.is_error != Some(true) } pub fn as_function_call_output_payload(&self) -> FunctionCallOutputPayload { if let Some(structured_content) = &self.structured_content && !structured_content.is_null() { match serde_json::to_string(structured_content) { Ok(serialized_structured_content) => { return FunctionCallOutputPayload { body: FunctionCallOutputBody::Text(serialized_structured_content), success: Some(self.success()), }; } Err(err) => { return FunctionCallOutputPayload { body: FunctionCallOutputBody::Text(err.to_string()), success: Some(false), }; } } } let serialized_content = match serde_json::to_string(&self.content) { Ok(serialized_content) => serialized_content, Err(err) => { return FunctionCallOutputPayload { body: FunctionCallOutputBody::Text(err.to_string()), success: Some(false), }; } }; let content_items = convert_mcp_content_to_items(&self.content); let body = match content_items { Some(content_items) => FunctionCallOutputBody::ContentItems(content_items), None => FunctionCallOutputBody::Text(serialized_content), }; FunctionCallOutputPayload { body, success: Some(self.success()), } } pub fn into_function_call_output_payload(self) -> FunctionCallOutputPayload { self.as_function_call_output_payload() }}fn convert_mcp_content_to_items( contents: &[serde_json::Value],) -> Option<Vec<FunctionCallOutputContentItem>> { const CODEX_IMAGE_DETAIL_META_KEY: &str = "codex/imageDetail"; #[derive(serde::Deserialize)] #[serde(tag = "type")] enum McpContent { #[serde(rename = "text")] Text { text: String }, #[serde(rename = "image")] Image { data: String, #[serde(rename = "mimeType", alias = "mime_type")] mime_type: Option<String>, #[serde(rename = "_meta", default)] meta: Option<serde_json::Value>, }, #[serde(other)] Unknown, } let mut saw_image = false; let mut items = Vec::with_capacity(contents.len()); for content in contents { let item = match serde_json::from_value::<McpContent>(content.clone()) { Ok(McpContent::Text { text }) => FunctionCallOutputContentItem::InputText { text }, Ok(McpContent::Image { data, mime_type, meta, }) => { saw_image = true; let image_url = if data.starts_with("data:") { data } else { let mime_type = mime_type.unwrap_or_else(|| "application/octet-stream".into()); format!("data:{mime_type};base64,{data}") }; FunctionCallOutputContentItem::InputImage { image_url, detail: meta .as_ref() .and_then(serde_json::Value::as_object) .and_then(|meta| meta.get(CODEX_IMAGE_DETAIL_META_KEY)) .and_then(serde_json::Value::as_str) .and_then(|detail| match detail { "auto" => Some(ImageDetail::Auto), "low" => Some(ImageDetail::Low), "high" => Some(ImageDetail::High), "original" => Some(ImageDetail::Original), _ => None, }) .or(Some(DEFAULT_IMAGE_DETAIL)), } } Ok(McpContent::Unknown) | Err(_) => FunctionCallOutputContentItem::InputText { text: serde_json::to_string(content).unwrap_or_else(|_| "<content>".to_string()), }, }; items.push(item); } if saw_image { Some(items) } else { None }}// Implement Display so callers can treat the payload like a plain string when logging or doing// trivial substring checks in tests (existing tests call `.contains()` on the output). For// `ContentItems`, Display emits a JSON representation.impl std::fmt::Display for FunctionCallOutputPayload { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match &self.body { FunctionCallOutputBody::Text(content) => f.write_str(content), FunctionCallOutputBody::ContentItems(items) => { let content = serde_json::to_string(items).unwrap_or_default(); f.write_str(content.as_str()) } } }}// (Moved event mapping logic into codex-core to avoid coupling protocol to UI-facing events.)#[cfg(test)]mod tests { use super::*; use anyhow::Result; use codex_execpolicy::Policy; use pretty_assertions::assert_eq; use std::path::PathBuf; use tempfile::tempdir; // A tiny valid PNG (1x1) so image conversion tests don't depend on cross-crate // file paths, which break under Bazel sandboxing. const TINY_PNG_BYTES: &[u8] = &[ 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, 0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 11, 73, 68, 65, 84, 120, 156, 99, 96, 0, 2, 0, 0, 5, 0, 1, 122, 94, 171, 63, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130, ]; #[test] fn plaintext_agent_message_content_rejects_mixed_encrypted_content() { let content = vec![ AgentMessageInputContent::InputText { text: "Message Type: MESSAGE\nPayload:\n".to_string(), }, AgentMessageInputContent::EncryptedContent { encrypted_content: "encrypted-payload".to_string(), }, ]; assert_eq!(plaintext_agent_message_content(&content), None); } #[test] fn response_input_message_conversion_preserves_phase() { let item = ResponseItem::from(ResponseInputItem::Message { role: "assistant".to_string(), content: vec![ContentItem::OutputText { text: "still working".to_string(), }], phase: Some(MessagePhase::Commentary), }); assert_eq!( item, ResponseItem::Message { id: None, role: "assistant".to_string(), content: vec![ContentItem::OutputText { text: "still working".to_string(), }], phase: Some(MessagePhase::Commentary), metadata: None, } ); } #[test] fn response_item_metadata_round_trips_and_stamps_turn_ids() -> Result<()> { let mut item = response_item_with_metadata(Some(response_item_metadata("turn-1"))); let round_trip: ResponseItem = serde_json::from_value(serde_json::to_value(&item)?)?; assert_eq!(round_trip, item); let unknown_metadata: ResponseItem = serde_json::from_value(serde_json::json!({ "type": "message", "role": "user", "content": [{"type": "input_text", "text": "hello"}], "metadata": { "turn_id": "turn-1", "other": "ignored", }, }))?; assert_eq!(unknown_metadata, item); item.stamp_turn_id_if_missing("turn-2"); assert_eq!(item.turn_id(), Some("turn-1")); let mut empty_turn_id = response_item_with_metadata(Some(response_item_metadata(""))); empty_turn_id.stamp_turn_id_if_missing("turn-1"); assert_eq!(empty_turn_id.turn_id(), Some("turn-1")); let mut missing_turn_id = response_item_with_metadata(/*metadata*/ None); missing_turn_id.stamp_turn_id_if_missing(""); missing_turn_id.stamp_turn_id_if_missing("turn-1"); assert_eq!(missing_turn_id.turn_id(), Some("turn-1")); let mut other = ResponseItem::Other; other.stamp_turn_id_if_missing("turn-1"); assert_eq!(other.turn_id(), None); Ok(()) } fn response_item_with_metadata(metadata: Option<ResponseItemMetadata>) -> ResponseItem { ResponseItem::Message { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: "hello".to_string(), }], phase: None, metadata, } } fn response_item_metadata(turn_id: &str) -> ResponseItemMetadata { ResponseItemMetadata { turn_id: Some(turn_id.to_string()), } } #[test] fn image_detail_roundtrips_all_wire_values() -> Result<()> { assert_eq!( serde_json::from_str::<ImageDetail>("\"auto\"")?, ImageDetail::Auto ); assert_eq!( serde_json::from_str::<ImageDetail>("\"low\"")?, ImageDetail::Low ); assert_eq!(serde_json::to_string(&ImageDetail::Auto)?, "\"auto\""); assert_eq!(serde_json::to_string(&ImageDetail::Low)?, "\"low\""); let content_item: ContentItem = serde_json::from_value(serde_json::json!({ "type": "input_image", "image_url": "data:image/png;base64,abc", "detail": "auto", }))?; assert_eq!( content_item, ContentItem::InputImage { image_url: "data:image/png;base64,abc".to_string(), detail: Some(ImageDetail::Auto), } ); Ok(()) } #[test] fn sandbox_permissions_helpers_match_documented_semantics() { let cases = [ (SandboxPermissions::UseDefault, false, false, false), (SandboxPermissions::RequireEscalated, true, true, false), ( SandboxPermissions::WithAdditionalPermissions, false, true, true, ), ]; for ( sandbox_permissions, requires_escalated_permissions, requests_sandbox_override, uses_additional_permissions, ) in cases { assert_eq!( sandbox_permissions.requires_escalated_permissions(), requires_escalated_permissions ); assert_eq!( sandbox_permissions.requests_sandbox_override(), requests_sandbox_override ); assert_eq!( sandbox_permissions.uses_additional_permissions(), uses_additional_permissions ); } } #[test] fn convert_mcp_content_to_items_preserves_data_urls() { let contents = vec![serde_json::json!({ "type": "image", "data": "data:image/png;base64,Zm9v", "mimeType": "image/png", })]; let items = convert_mcp_content_to_items(&contents).expect("expected image items"); assert_eq!( items, vec![FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,Zm9v".to_string(), detail: Some(DEFAULT_IMAGE_DETAIL), }] ); } #[test] fn response_item_parses_image_generation_call() { let item = serde_json::from_value::<ResponseItem>(serde_json::json!({ "id": "ig_123", "type": "image_generation_call", "status": "completed", "revised_prompt": "A small blue square", "result": "Zm9v", })) .expect("image generation item should deserialize"); assert_eq!( item, ResponseItem::ImageGenerationCall { id: "ig_123".to_string(), status: "completed".to_string(), revised_prompt: Some("A small blue square".to_string()), result: "Zm9v".to_string(), metadata: None, } ); } #[test] fn response_item_parses_image_generation_call_without_revised_prompt() { let item = serde_json::from_value::<ResponseItem>(serde_json::json!({ "id": "ig_123", "type": "image_generation_call", "status": "completed", "result": "Zm9v", })) .expect("image generation item should deserialize"); assert_eq!( item, ResponseItem::ImageGenerationCall { id: "ig_123".to_string(), status: "completed".to_string(), revised_prompt: None, result: "Zm9v".to_string(), metadata: None, } ); } #[test] fn additional_permission_profile_is_empty_when_all_fields_are_none() { assert_eq!(AdditionalPermissionProfile::default().is_empty(), true); } #[test] fn additional_permission_profile_is_not_empty_when_field_is_present_but_nested_empty() { let permission_profile = AdditionalPermissionProfile { network: Some(NetworkPermissions { enabled: None }), file_system: None, }; assert_eq!(permission_profile.is_empty(), false); } #[test] fn permission_profile_round_trip_preserves_glob_scan_max_depth() { let mut file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern: "**/*.env".to_string(), }, access: FileSystemAccessMode::Deny, }]); file_system_sandbox_policy.glob_scan_max_depth = Some(2); let permission_profile = PermissionProfile::from_runtime_permissions( &file_system_sandbox_policy, NetworkSandboxPolicy::Restricted, ); assert_eq!( permission_profile.file_system_sandbox_policy(), file_system_sandbox_policy ); } #[test] fn permission_profile_deserializes_legacy_rollout_shape() -> Result<()> { let legacy = serde_json::json!({ "network": { "enabled": true, }, "file_system": { "entries": [{ "path": { "type": "special", "value": { "kind": "root", }, }, "access": "write", }], "glob_scan_max_depth": 2, }, }); let permission_profile: PermissionProfile = serde_json::from_value(legacy)?; assert_eq!( permission_profile, PermissionProfile::Managed { file_system: ManagedFileSystemPermissions::Restricted { entries: vec![FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Write, }], glob_scan_max_depth: NonZeroUsize::new(2), }, network: NetworkSandboxPolicy::Enabled, } ); Ok(()) } #[test] fn permission_profile_presets_match_legacy_defaults() { assert_eq!( PermissionProfile::read_only(), PermissionProfile::from_legacy_sandbox_policy(&SandboxPolicy::new_read_only_policy()) ); assert_eq!( PermissionProfile::workspace_write(), PermissionProfile::from_legacy_sandbox_policy( &SandboxPolicy::new_workspace_write_policy() ) ); } #[test] fn permission_profile_round_trip_preserves_disabled_sandbox() -> Result<()> { let cwd = tempdir()?; let permission_profile = PermissionProfile::from_legacy_sandbox_policy(&SandboxPolicy::DangerFullAccess); assert_eq!(permission_profile, PermissionProfile::Disabled); assert_eq!( permission_profile.to_legacy_sandbox_policy(cwd.path())?, SandboxPolicy::DangerFullAccess ); assert_eq!( permission_profile.to_runtime_permissions(), ( FileSystemSandboxPolicy::unrestricted(), NetworkSandboxPolicy::Enabled ) ); Ok(()) } #[test] fn disabled_permission_profile_ignores_runtime_network_policy() { let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( SandboxEnforcement::Disabled, &FileSystemSandboxPolicy::unrestricted(), NetworkSandboxPolicy::Restricted, ); assert_eq!(permission_profile, PermissionProfile::Disabled); } #[test] fn permission_profile_from_runtime_permissions_preserves_external_sandbox() { let permission_profile = PermissionProfile::from_runtime_permissions( &FileSystemSandboxPolicy::external_sandbox(), NetworkSandboxPolicy::Restricted, ); assert_eq!( permission_profile, PermissionProfile::External { network: NetworkSandboxPolicy::Restricted, } ); assert_eq!( PermissionProfile::from_runtime_permissions_with_enforcement( SandboxEnforcement::Managed, &FileSystemSandboxPolicy::external_sandbox(), NetworkSandboxPolicy::Restricted, ), permission_profile, ); } #[test] fn permission_profile_from_runtime_permissions_preserves_unrestricted_managed_network() { let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( SandboxEnforcement::External, &FileSystemSandboxPolicy::unrestricted(), NetworkSandboxPolicy::Restricted, ); assert_eq!( permission_profile, PermissionProfile::Managed { file_system: ManagedFileSystemPermissions::Unrestricted, network: NetworkSandboxPolicy::Restricted, }, "the legacy ExternalSandbox projection must not hide a split unrestricted filesystem policy" ); assert_eq!( permission_profile.to_runtime_permissions(), ( FileSystemSandboxPolicy::unrestricted(), NetworkSandboxPolicy::Restricted, ) ); } #[test] fn permission_profile_round_trip_preserves_external_sandbox() -> Result<()> { let cwd = tempdir()?; let sandbox_policy = SandboxPolicy::ExternalSandbox { network_access: crate::protocol::NetworkAccess::Restricted, }; let permission_profile = PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy); assert_eq!( permission_profile, PermissionProfile::External { network: NetworkSandboxPolicy::Restricted, } ); assert_eq!( permission_profile.to_legacy_sandbox_policy(cwd.path())?, sandbox_policy ); assert_eq!( permission_profile.to_runtime_permissions(), ( FileSystemSandboxPolicy::external_sandbox(), NetworkSandboxPolicy::Restricted ) ); Ok(()) } #[test] fn file_system_permissions_with_glob_scan_depth_uses_canonical_json() -> Result<()> { let path = AbsolutePathBuf::try_from(PathBuf::from(if cfg!(windows) { r"C:\tmp\allowed" } else { "/tmp/allowed" })) .expect("absolute path"); let file_system_permissions = FileSystemPermissions { entries: vec![FileSystemSandboxEntry { path: FileSystemPath::Path { path }, access: FileSystemAccessMode::Read, }], glob_scan_max_depth: NonZeroUsize::new(2), }; let serialized = serde_json::to_value(&file_system_permissions)?; assert_eq!(serialized.get("read"), None); assert_eq!(serialized.get("write"), None); assert_eq!( serialized.get("glob_scan_max_depth"), Some(&serde_json::json!(2)) ); assert!(serialized.get("entries").is_some()); assert_eq!( serde_json::from_value::<FileSystemPermissions>(serialized)?, file_system_permissions ); Ok(()) } #[test] fn file_system_permissions_rejects_zero_glob_scan_depth() { serde_json::from_value::<FileSystemPermissions>(serde_json::json!({ "entries": [], "glob_scan_max_depth": 0, })) .expect_err("zero glob scan depth should fail deserialization"); } #[test] fn convert_mcp_content_to_items_builds_data_urls_when_missing_prefix() { let contents = vec![serde_json::json!({ "type": "image", "data": "Zm9v", "mimeType": "image/png", })]; let items = convert_mcp_content_to_items(&contents).expect("expected image items"); assert_eq!( items, vec![FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,Zm9v".to_string(), detail: Some(DEFAULT_IMAGE_DETAIL), }] ); } #[test] fn convert_mcp_content_to_items_returns_none_without_images() { let contents = vec![serde_json::json!({ "type": "text", "text": "hello", })]; assert_eq!(convert_mcp_content_to_items(&contents), None); } #[test] fn function_call_output_content_items_to_text_joins_text_segments() { let content_items = vec![ FunctionCallOutputContentItem::InputText { text: "line 1".to_string(), }, FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,AAA".to_string(), detail: Some(DEFAULT_IMAGE_DETAIL), }, FunctionCallOutputContentItem::InputText { text: "line 2".to_string(), }, ]; let text = function_call_output_content_items_to_text(&content_items); assert_eq!(text, Some("line 1\nline 2".to_string())); } #[test] fn function_call_output_content_items_to_text_ignores_blank_text_and_images() { let content_items = vec![ FunctionCallOutputContentItem::InputText { text: " ".to_string(), }, FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,AAA".to_string(), detail: Some(DEFAULT_IMAGE_DETAIL), }, FunctionCallOutputContentItem::EncryptedContent { encrypted_content: "enc_opaque".to_string(), }, ]; let text = function_call_output_content_items_to_text(&content_items); assert_eq!(text, None); } #[test] fn function_call_output_body_to_text_returns_plain_text_content() { let body = FunctionCallOutputBody::Text("ok".to_string()); let text = body.to_text(); assert_eq!(text, Some("ok".to_string())); } #[test] fn function_call_output_body_to_text_uses_content_item_fallback() { let body = FunctionCallOutputBody::ContentItems(vec![ FunctionCallOutputContentItem::InputText { text: "line 1".to_string(), }, FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,AAA".to_string(), detail: Some(DEFAULT_IMAGE_DETAIL), }, ]); let text = body.to_text(); assert_eq!(text, Some("line 1".to_string())); } #[test] fn function_call_deserializes_optional_namespace() { let item: ResponseItem = serde_json::from_value(serde_json::json!({ "type": "function_call", "name": "mcp__codex_apps__gmail_get_recent_emails", "namespace": "mcp__codex_apps__gmail", "arguments": "{\"top_k\":5}", "call_id": "call-1", })) .expect("function_call should deserialize"); assert_eq!( item, ResponseItem::FunctionCall { id: None, name: "mcp__codex_apps__gmail_get_recent_emails".to_string(), namespace: Some("mcp__codex_apps__gmail".to_string()), arguments: "{\"top_k\":5}".to_string(), call_id: "call-1".to_string(), metadata: None, } ); } #[test] fn render_command_prefix_list_sorts_by_len_then_total_len_then_alphabetical() { let prefixes = vec![ vec!["b".to_string(), "zz".to_string()], vec!["aa".to_string()], vec!["b".to_string()], vec!["a".to_string(), "b".to_string(), "c".to_string()], vec!["a".to_string()], vec!["b".to_string(), "a".to_string()], ]; let output = format_allow_prefixes(prefixes).expect("rendered list"); assert_eq!( output, r#"- ["a"]- ["b"]- ["aa"]- ["b", "a"]- ["b", "zz"]- ["a", "b", "c"]"# .to_string(), ); } #[test] fn render_command_prefix_list_limits_output_to_max_prefixes() { let prefixes = (0..(MAX_RENDERED_PREFIXES + 5)) .map(|i| vec![format!("{i:03}")]) .collect::<Vec<_>>(); let output = format_allow_prefixes(prefixes).expect("rendered list"); assert_eq!(output.ends_with(TRUNCATED_MARKER), true); eprintln!("output: {output}"); assert_eq!(output.lines().count(), MAX_RENDERED_PREFIXES + 1); } #[test] fn format_allow_prefixes_limits_output() { let mut exec_policy = Policy::empty(); for i in 0..200 { exec_policy .add_prefix_rule( &[format!("tool-{i:03}"), "x".repeat(500)], codex_execpolicy::Decision::Allow, ) .expect("add rule"); } let output = format_allow_prefixes(exec_policy.get_allowed_prefixes()).expect("formatted prefixes"); assert!( output.len() <= MAX_ALLOW_PREFIX_TEXT_BYTES + TRUNCATED_MARKER.len(), "output length exceeds expected limit: {output}", ); } #[test] fn serializes_success_as_plain_string() -> Result<()> { let item = ResponseInputItem::FunctionCallOutput { call_id: "call1".into(), output: FunctionCallOutputPayload::from_text("ok".into()), }; let json = serde_json::to_string(&item)?; let v: serde_json::Value = serde_json::from_str(&json)?; // Success case -> output should be a plain string assert_eq!(v.get("output").unwrap().as_str().unwrap(), "ok"); Ok(()) } #[test] fn serializes_failure_as_string() -> Result<()> { let item = ResponseInputItem::FunctionCallOutput { call_id: "call1".into(), output: FunctionCallOutputPayload { body: FunctionCallOutputBody::Text("bad".into()), success: Some(false), }, }; let json = serde_json::to_string(&item)?; let v: serde_json::Value = serde_json::from_str(&json)?; assert_eq!(v.get("output").unwrap().as_str().unwrap(), "bad"); Ok(()) } #[test] fn serializes_image_outputs_as_array() -> Result<()> { let call_tool_result = CallToolResult { content: vec![ serde_json::json!({"type":"text","text":"caption"}), serde_json::json!({"type":"image","data":"BASE64","mimeType":"image/png"}), ], structured_content: None, is_error: Some(false), meta: None, }; let payload = call_tool_result.into_function_call_output_payload(); assert_eq!(payload.success, Some(true)); let Some(items) = payload.content_items() else { panic!("expected content items"); }; let items = items.to_vec(); assert_eq!( items, vec![ FunctionCallOutputContentItem::InputText { text: "caption".into(), }, FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,BASE64".into(), detail: Some(DEFAULT_IMAGE_DETAIL), }, ] ); let item = ResponseInputItem::FunctionCallOutput { call_id: "call1".into(), output: payload, }; let json = serde_json::to_string(&item)?; let v: serde_json::Value = serde_json::from_str(&json)?; let output = v.get("output").expect("output field"); assert!(output.is_array(), "expected array output"); Ok(()) } #[test] fn serializes_custom_tool_image_outputs_as_array() -> Result<()> { let item = ResponseInputItem::CustomToolCallOutput { call_id: "call1".into(), name: None, output: FunctionCallOutputPayload::from_content_items(vec![ FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,BASE64".into(), detail: Some(DEFAULT_IMAGE_DETAIL), }, ]), }; let json = serde_json::to_string(&item)?; let v: serde_json::Value = serde_json::from_str(&json)?; let output = v.get("output").expect("output field"); assert!(output.is_array(), "expected array output"); Ok(()) } #[test] fn serializes_encrypted_function_output_content_as_array() -> Result<()> { let item = ResponseInputItem::FunctionCallOutput { call_id: "call1".into(), output: FunctionCallOutputPayload::from_content_items(vec![ FunctionCallOutputContentItem::EncryptedContent { encrypted_content: "enc_opaque".into(), }, ]), }; let json = serde_json::to_value(&item)?; assert_eq!( json, serde_json::json!({ "type": "function_call_output", "call_id": "call1", "output": [ { "type": "encrypted_content", "encrypted_content": "enc_opaque", } ], }) ); Ok(()) } #[test] fn preserves_existing_image_data_urls() -> Result<()> { let call_tool_result = CallToolResult { content: vec![serde_json::json!({ "type": "image", "data": "data:image/png;base64,BASE64", "mimeType": "image/png" })], structured_content: None, is_error: Some(false), meta: None, }; let payload = call_tool_result.into_function_call_output_payload(); let Some(items) = payload.content_items() else { panic!("expected content items"); }; let items = items.to_vec(); assert_eq!( items, vec![FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,BASE64".into(), detail: Some(DEFAULT_IMAGE_DETAIL), }] ); Ok(()) } #[test] fn preserves_original_detail_metadata_on_mcp_images() -> Result<()> { let call_tool_result = CallToolResult { content: vec![serde_json::json!({ "type": "image", "data": "BASE64", "mimeType": "image/png", "_meta": { "codex/imageDetail": "original", }, })], structured_content: None, is_error: Some(false), meta: None, }; let payload = call_tool_result.into_function_call_output_payload(); let Some(items) = payload.content_items() else { panic!("expected content items"); }; let items = items.to_vec(); assert_eq!( items, vec![FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,BASE64".into(), detail: Some(ImageDetail::Original), }] ); Ok(()) } #[test] fn preserves_standard_detail_metadata_on_mcp_images() -> Result<()> { let call_tool_result = CallToolResult { content: vec![serde_json::json!({ "type": "image", "data": "BASE64", "mimeType": "image/png", "_meta": { "codex/imageDetail": "high", }, })], structured_content: None, is_error: Some(false), meta: None, }; let payload = call_tool_result.into_function_call_output_payload(); let Some(items) = payload.content_items() else { panic!("expected content items"); }; let items = items.to_vec(); assert_eq!( items, vec![FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,BASE64".into(), detail: Some(ImageDetail::High), }] ); Ok(()) } #[test] fn deserializes_array_payload_into_items() -> Result<()> { let json = r#"[ {"type": "input_text", "text": "note"}, {"type": "input_image", "image_url": "data:image/png;base64,XYZ"} ]"#; let payload: FunctionCallOutputPayload = serde_json::from_str(json)?; assert_eq!(payload.success, None); let expected_items = vec![ FunctionCallOutputContentItem::InputText { text: "note".into(), }, FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,XYZ".into(), detail: None, }, ]; assert_eq!( payload.body, FunctionCallOutputBody::ContentItems(expected_items.clone()) ); assert_eq!( serde_json::to_string(&payload)?, serde_json::to_string(&expected_items)? ); Ok(()) } #[test] fn deserializes_encrypted_array_payload_into_items() -> Result<()> { let json = r#"[ {"type": "encrypted_content", "encrypted_content": "enc_opaque"} ]"#; let payload: FunctionCallOutputPayload = serde_json::from_str(json)?; let expected_items = vec![FunctionCallOutputContentItem::EncryptedContent { encrypted_content: "enc_opaque".into(), }]; assert_eq!(payload.success, None); assert_eq!( payload.body, FunctionCallOutputBody::ContentItems(expected_items.clone()) ); assert_eq!( serde_json::to_string(&payload)?, serde_json::to_string(&expected_items)? ); Ok(()) } #[test] fn deserializes_compaction_alias() -> Result<()> { let json = r#"{"type":"compaction_summary","encrypted_content":"abc"}"#; let item: ResponseItem = serde_json::from_str(json)?; assert_eq!( item, ResponseItem::Compaction { encrypted_content: "abc".into(), metadata: None, } ); Ok(()) } #[test] fn deserializes_context_compaction() -> Result<()> { let json = r#"{"type":"context_compaction","encrypted_content":"abc"}"#; let item: ResponseItem = serde_json::from_str(json)?; assert_eq!( item, ResponseItem::ContextCompaction { encrypted_content: Some("abc".into()), metadata: None, } ); Ok(()) } #[test] fn serializes_compaction_trigger_without_payload() -> Result<()> { let item = ResponseItem::CompactionTrigger { metadata: None }; assert_eq!( serde_json::to_value(item)?, serde_json::json!({ "type": "compaction_trigger", }) ); Ok(()) } #[test] fn serializes_stamped_compaction_trigger_metadata() -> Result<()> { let mut item = ResponseItem::CompactionTrigger { metadata: None }; item.stamp_turn_id_if_missing("turn-1"); assert_eq!( serde_json::to_value(item)?, serde_json::json!({ "type": "compaction_trigger", "metadata": { "turn_id": "turn-1", }, }) ); Ok(()) } #[test] fn deserializes_compaction_trigger_without_payload() -> Result<()> { let json = r#"{"type":"compaction_trigger"}"#; let item: ResponseItem = serde_json::from_str(json)?; assert_eq!(item, ResponseItem::CompactionTrigger { metadata: None }); Ok(()) } #[test] fn deserializes_legacy_ghost_snapshot_as_other() -> Result<()> { let json = r#"{ "type":"ghost_snapshot", "ghost_commit":{ "id":"ghost-1", "parent":null, "preexisting_untracked_files":[], "preexisting_untracked_dirs":[] } }"#; let item: ResponseItem = serde_json::from_str(json)?; assert_eq!(item, ResponseItem::Other); Ok(()) } #[test] fn roundtrips_web_search_call_actions() -> Result<()> { let cases = vec![ ( r#"{ "type": "web_search_call", "status": "completed", "action": { "type": "search", "query": "weather seattle", "queries": ["weather seattle", "seattle weather now"] } }"#, None, Some(WebSearchAction::Search { query: Some("weather seattle".into()), queries: Some(vec!["weather seattle".into(), "seattle weather now".into()]), }), Some("completed".into()), true, ), ( r#"{ "type": "web_search_call", "status": "open", "action": { "type": "open_page", "url": "https://example.com" } }"#, None, Some(WebSearchAction::OpenPage { url: Some("https://example.com".into()), }), Some("open".into()), true, ), ( r#"{ "type": "web_search_call", "status": "in_progress", "action": { "type": "find_in_page", "url": "https://example.com/docs", "pattern": "installation" } }"#, None, Some(WebSearchAction::FindInPage { url: Some("https://example.com/docs".into()), pattern: Some("installation".into()), }), Some("in_progress".into()), true, ), ( r#"{ "type": "web_search_call", "status": "in_progress", "id": "ws_partial" }"#, Some("ws_partial".into()), None, Some("in_progress".into()), false, ), ]; for (json_literal, expected_id, expected_action, expected_status, expect_roundtrip) in cases { let parsed: ResponseItem = serde_json::from_str(json_literal)?; let expected = ResponseItem::WebSearchCall { id: expected_id.clone(), status: expected_status.clone(), action: expected_action.clone(), metadata: None, }; assert_eq!(parsed, expected); let serialized = serde_json::to_value(&parsed)?; let mut expected_serialized: serde_json::Value = serde_json::from_str(json_literal)?; if !expect_roundtrip && let Some(obj) = expected_serialized.as_object_mut() { obj.remove("id"); } assert_eq!(serialized, expected_serialized); } Ok(()) } #[test] fn serializes_image_user_input_without_tags() -> Result<()> { let image_url = "data:image/png;base64,abc".to_string(); let item = ResponseInputItem::from(vec![UserInput::Image { image_url: image_url.clone(), detail: None, }]); match item { ResponseInputItem::Message { content, .. } => { let expected = vec![ContentItem::InputImage { image_url, detail: Some(DEFAULT_IMAGE_DETAIL), }]; assert_eq!(content, expected); } other => panic!("expected message response but got {other:?}"), } Ok(()) } #[test] fn image_user_input_preserves_requested_detail() -> Result<()> { let image_url = "data:image/png;base64,abc".to_string(); let item = ResponseInputItem::from(vec![UserInput::Image { image_url: image_url.clone(), detail: Some(ImageDetail::Original), }]); match item { ResponseInputItem::Message { content, .. } => { assert_eq!( content.first(), Some(&ContentItem::InputImage { image_url, detail: Some(ImageDetail::Original), }) ); } other => panic!("expected message response but got {other:?}"), } Ok(()) } #[test] fn tool_search_call_roundtrips() -> Result<()> { let parsed: ResponseItem = serde_json::from_str( r#"{ "type": "tool_search_call", "call_id": "search-1", "execution": "client", "arguments": { "query": "calendar create", "limit": 1 } }"#, )?; assert_eq!( parsed, ResponseItem::ToolSearchCall { id: None, call_id: Some("search-1".to_string()), status: None, execution: "client".to_string(), arguments: serde_json::json!({ "query": "calendar create", "limit": 1, }), metadata: None, } ); assert_eq!( serde_json::to_value(&parsed)?, serde_json::json!({ "type": "tool_search_call", "call_id": "search-1", "execution": "client", "arguments": { "query": "calendar create", "limit": 1, } }) ); Ok(()) } #[test] fn tool_search_output_roundtrips() -> Result<()> { let input = ResponseInputItem::ToolSearchOutput { call_id: "search-1".to_string(), status: "completed".to_string(), execution: "client".to_string(), tools: vec![serde_json::json!({ "type": "function", "name": "mcp__codex_apps__calendar_create_event", "description": "Create a calendar event.", "defer_loading": true, "parameters": { "type": "object", "properties": { "title": {"type": "string"} }, "required": ["title"], "additionalProperties": false, } })], }; assert_eq!( ResponseItem::from(input.clone()), ResponseItem::ToolSearchOutput { call_id: Some("search-1".to_string()), status: "completed".to_string(), execution: "client".to_string(), tools: vec![serde_json::json!({ "type": "function", "name": "mcp__codex_apps__calendar_create_event", "description": "Create a calendar event.", "defer_loading": true, "parameters": { "type": "object", "properties": { "title": {"type": "string"} }, "required": ["title"], "additionalProperties": false, } })], metadata: None, } ); assert_eq!( serde_json::to_value(input)?, serde_json::json!({ "type": "tool_search_output", "call_id": "search-1", "status": "completed", "execution": "client", "tools": [{ "type": "function", "name": "mcp__codex_apps__calendar_create_event", "description": "Create a calendar event.", "defer_loading": true, "parameters": { "type": "object", "properties": { "title": {"type": "string"} }, "required": ["title"], "additionalProperties": false, } }] }) ); Ok(()) } #[test] fn tool_search_server_items_allow_null_call_id() -> Result<()> { let parsed_call: ResponseItem = serde_json::from_str( r#"{ "type": "tool_search_call", "execution": "server", "call_id": null, "status": "completed", "arguments": { "paths": ["crm"] } }"#, )?; assert_eq!( parsed_call, ResponseItem::ToolSearchCall { id: None, call_id: None, status: Some("completed".to_string()), execution: "server".to_string(), arguments: serde_json::json!({ "paths": ["crm"], }), metadata: None, } ); let parsed_output: ResponseItem = serde_json::from_str( r#"{ "type": "tool_search_output", "execution": "server", "call_id": null, "status": "completed", "tools": [] }"#, )?; assert_eq!( parsed_output, ResponseItem::ToolSearchOutput { call_id: None, status: "completed".to_string(), execution: "server".to_string(), tools: vec![], metadata: None, } ); Ok(()) } #[test] fn mixed_remote_and_local_images_share_label_sequence() -> Result<()> { let image_url = "data:image/png;base64,abc".to_string(); let dir = tempdir()?; let local_path = dir.path().join("local.png"); std::fs::write(&local_path, TINY_PNG_BYTES)?; let item = ResponseInputItem::from(vec![ UserInput::Image { image_url: image_url.clone(), detail: None, }, UserInput::LocalImage { path: local_path.clone(), detail: None, }, ]); match item { ResponseInputItem::Message { content, .. } => { assert_eq!( content.first(), Some(&ContentItem::InputImage { image_url, detail: Some(DEFAULT_IMAGE_DETAIL), }) ); assert_eq!( content.get(1), Some(&ContentItem::InputText { text: local_image_open_tag_text_with_path( /*label_number*/ 2, &local_path ), }) ); assert!(matches!( content.get(2), Some(ContentItem::InputImage { .. }) )); assert_eq!( content.get(3), Some(&ContentItem::InputText { text: image_close_tag_text(), }) ); } other => panic!("expected message response but got {other:?}"), } Ok(()) } #[test] fn local_image_open_tag_preserves_path() { assert_eq!( local_image_open_tag_text_with_path( /*label_number*/ 1, std::path::Path::new(r#"/tmp/a&"<b>.png"#), ), r#"<image name=[Image #1] path="/tmp/a&"<b>.png">"# ); } #[test] fn local_image_user_input_preserves_requested_detail() -> Result<()> { let dir = tempdir()?; let local_path = dir.path().join("local.png"); std::fs::write(&local_path, TINY_PNG_BYTES)?; let item = ResponseInputItem::from(vec![UserInput::LocalImage { path: local_path, detail: Some(ImageDetail::Original), }]); match item { ResponseInputItem::Message { content, .. } => { assert!(matches!( content.get(1), Some(ContentItem::InputImage { detail: Some(ImageDetail::Original), .. }) )); } other => panic!("expected message response but got {other:?}"), } Ok(()) } #[test] fn local_image_read_error_adds_placeholder() -> Result<()> { let dir = tempdir()?; let missing_path = dir.path().join("missing-image.png"); let item = ResponseInputItem::from(vec![UserInput::LocalImage { path: missing_path.clone(), detail: None, }]); match item { ResponseInputItem::Message { content, .. } => { assert_eq!(content.len(), 1); match &content[0] { ContentItem::InputText { text } => { let display_path = missing_path.display().to_string(); assert!( text.contains(&display_path), "placeholder should mention missing path: {text}" ); assert!( text.contains("could not read"), "placeholder should mention read issue: {text}" ); } other => panic!("expected placeholder text but found {other:?}"), } } other => panic!("expected message response but got {other:?}"), } Ok(()) } #[test] fn local_image_non_image_adds_placeholder() -> Result<()> { let dir = tempdir()?; let json_path = dir.path().join("example.json"); std::fs::write(&json_path, br#"{"hello":"world"}"#)?; let item = ResponseInputItem::from(vec![UserInput::LocalImage { path: json_path.clone(), detail: None, }]); match item { ResponseInputItem::Message { content, .. } => { assert_eq!(content.len(), 1); match &content[0] { ContentItem::InputText { text } => { assert!( text.contains("unsupported image `application/json`"), "placeholder should mention unsupported image MIME: {text}" ); assert!( text.contains(&json_path.display().to_string()), "placeholder should mention path: {text}" ); } other => panic!("expected placeholder text but found {other:?}"), } } other => panic!("expected message response but got {other:?}"), } Ok(()) } #[test] fn local_image_unsupported_image_format_adds_placeholder() -> Result<()> { let dir = tempdir()?; let svg_path = dir.path().join("example.svg"); std::fs::write( &svg_path, br#"<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"></svg>"#, )?; let item = ResponseInputItem::from(vec![UserInput::LocalImage { path: svg_path.clone(), detail: None, }]); match item { ResponseInputItem::Message { content, .. } => { assert_eq!(content.len(), 1); let expected = format!( "Codex cannot attach image at `{}`: unsupported image `image/svg+xml`.", svg_path.display() ); match &content[0] { ContentItem::InputText { text } => assert_eq!(text, &expected), other => panic!("expected placeholder text but found {other:?}"), } } other => panic!("expected message response but got {other:?}"), } Ok(()) }}