Codex Handbook
protocol/src/models.rs 3476 lines
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(())    }}