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 serde_with::serde_as; use ts_rs::TS; use crate::local_media::audio_mime_for_path; 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::permissions::RawFileSystemSandboxEntry; use crate::protocol::SandboxPolicy; use crate::user_input::UserInput; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_image::ImageProcessingError; use schemars::JsonSchema; use crate::ResponseItemId; use crate::mcp::CallToolResult; use codex_utils_path_uri::PathUri; mod executed_tool_calls; mod item_metadata; pub use crate::local_media::MAX_PROMPT_AUDIO_INPUT_BYTES; pub use crate::local_media::snapshot_local_user_input; pub use crate::permission_profile_snapshot::PermissionProfileSnapshot; pub use executed_tool_calls::ExecutedToolCall; pub use executed_tool_calls::ExecutedToolCallArguments; pub use executed_tool_calls::ExecutedToolCallTruncation; pub use executed_tool_calls::bound_executed_tool_calls_for_prompt; pub use executed_tool_calls::bound_executed_tool_calls_for_prompt_prioritizing_recent; pub use executed_tool_calls::executed_tool_call_metadata_bytes; pub use item_metadata::ContentItemKind; /// 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, Default, Eq, Hash, PartialEq, JsonSchema, TS)] pub struct FileSystemPermissions { #[schemars(with = "Vec")] #[ts(as = "Vec")] pub entries: Vec, pub glob_scan_max_depth: Option, } #[derive(Debug, Clone, Default, Eq, Hash, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct LegacyReadWriteRoots { #[serde(default, skip_serializing_if = "Option::is_none")] pub read: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub write: Option>, } impl FileSystemPermissions { pub fn is_empty(&self) -> bool { self.entries.is_empty() } pub fn from_read_write_roots( read: Option>, write: Option>, ) -> Self { Self::from_paths(read, write) } pub fn from_read_write_path_uris( read: Option>, write: Option>, ) -> Self { Self::from_paths(read, write) } fn from_paths

(read: Option>, write: Option>) -> Self where P: Into, { let mut entries = Vec::new(); if let Some(read) = read { entries.extend( read.into_iter().map(|path| { FileSystemSandboxEntry::new(path.into(), FileSystemAccessMode::Read) }), ); } if let Some(write) = write { entries.extend( write.into_iter().map(|path| { FileSystemSandboxEntry::new(path.into(), FileSystemAccessMode::Write) }), ); } Self { entries, glob_scan_max_depth: None, } } pub fn legacy_read_write_roots(&self) -> Option { self.as_legacy_permissions() } fn as_legacy_permissions(&self) -> Option { 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; }; let path = path.to_abs_path().ok()?; match entry.access { FileSystemAccessMode::Read => read.push(path), FileSystemAccessMode::Write => write.push(path), FileSystemAccessMode::Deny => return None, } } Some(LegacyReadWriteRoots { 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)] struct CanonicalFileSystemPermissions { #[serde(default, skip_serializing_if = "Vec::is_empty")] entries: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] glob_scan_max_depth: Option, } #[derive(Debug, Clone, Deserialize)] #[serde(untagged)] enum FileSystemPermissionsDe { Canonical(CanonicalFileSystemPermissions), Legacy(LegacyReadWriteRoots), } impl Serialize for FileSystemPermissions { fn serialize(&self, serializer: S) -> Result where S: Serializer, { if let Some(legacy) = self.as_legacy_permissions() { legacy.serialize(serializer) } else { CanonicalFileSystemPermissions { entries: self .entries .clone() .into_iter() .map(TryInto::try_into) .collect::>() .map_err(serde::ser::Error::custom)?, glob_scan_max_depth: self.glob_scan_max_depth, } .serialize(serializer) } } } impl<'de> Deserialize<'de> for FileSystemPermissions { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { match FileSystemPermissionsDe::deserialize(deserializer)? { FileSystemPermissionsDe::Canonical(CanonicalFileSystemPermissions { entries, glob_scan_max_depth, }) => Ok(Self { entries: entries .into_iter() .map(TryInto::try_into) .collect::>() .map_err(serde::de::Error::custom)?, glob_scan_max_depth, }), FileSystemPermissionsDe::Legacy(LegacyReadWriteRoots { 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, } 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, pub file_system: Option, } 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, JsonSchema, TS)] #[serde(tag = "type", rename_all = "snake_case")] #[ts(tag = "type")] pub enum ManagedFileSystemPermissions { /// Apply a managed filesystem sandbox from the listed entries. #[serde(rename_all = "snake_case")] #[ts(rename_all = "snake_case")] Restricted { #[schemars(with = "Vec")] #[ts(as = "Vec")] entries: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] glob_scan_max_depth: Option, }, /// Apply a managed sandbox that allows all filesystem access. Unrestricted, } #[derive(Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] enum SerializedManagedFileSystemPermissions { #[serde(rename_all = "snake_case")] Restricted { entries: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] glob_scan_max_depth: Option, }, Unrestricted, } impl Serialize for ManagedFileSystemPermissions { fn serialize(&self, serializer: S) -> Result where S: Serializer, { match self { Self::Restricted { entries, glob_scan_max_depth, } => SerializedManagedFileSystemPermissions::Restricted { entries: entries .clone() .into_iter() .map(TryInto::try_into) .collect::>() .map_err(serde::ser::Error::custom)?, glob_scan_max_depth: *glob_scan_max_depth, } .serialize(serializer), Self::Unrestricted => { SerializedManagedFileSystemPermissions::Unrestricted.serialize(serializer) } } } } impl<'de> Deserialize<'de> for ManagedFileSystemPermissions { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { Ok( match SerializedManagedFileSystemPermissions::deserialize(deserializer)? { SerializedManagedFileSystemPermissions::Restricted { entries, glob_scan_max_depth, } => Self::Restricted { entries: entries .into_iter() .map(TryInto::try_into) .collect::>() .map_err(serde::de::Error::custom)?, glob_scan_max_depth, }, SerializedManagedFileSystemPermissions::Unrestricted => Self::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 { /// Codex owns sandbox construction for this profile. #[serde(rename_all = "snake_case")] #[ts(rename_all = "snake_case")] Managed { file_system: ManagedFileSystemPermissions, 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 }, } /// 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.]` /// 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, } impl ActivePermissionProfile { pub fn new(id: impl Into) -> Self { Self { id: id.into(), extends: None, } } pub fn read_only() -> Self { Self::new(BUILT_IN_PERMISSION_PROFILE_READ_ONLY) } } impl Default for PermissionProfile { 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, } } /// Intersects managed filesystem permissions with read-only access and restricts network. /// /// Returns `None` when filesystem enforcement belongs to an external caller. pub fn intersect_with_read_only(&self) -> Option { let mut file_system = self.file_system_sandbox_policy(); match file_system.kind { FileSystemSandboxKind::Restricted => { for entry in &mut file_system.entries { entry.access = match entry.access { FileSystemAccessMode::Read | FileSystemAccessMode::Write => { FileSystemAccessMode::Read } FileSystemAccessMode::Deny => FileSystemAccessMode::Deny, }; } } FileSystemSandboxKind::Unrestricted => { file_system = FileSystemSandboxPolicy::read_only(); } FileSystemSandboxKind::ExternalSandbox => return None, } Some(Self::from_runtime_permissions( &file_system, 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 { 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 { #[serde(rename_all = "snake_case")] Managed { file_system: ManagedFileSystemPermissions, network: NetworkSandboxPolicy, }, Disabled, #[serde(rename_all = "snake_case")] External { network: NetworkSandboxPolicy, }, } impl From for PermissionProfile { fn from(value: TaggedPermissionProfile) -> 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 { network: Option, file_system: Option, } impl From for PermissionProfile { fn from(value: LegacyPermissionProfile) -> 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 { Tagged(TaggedPermissionProfile), Legacy(LegacyPermissionProfile), } impl<'de> Deserialize<'de> for PermissionProfile { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { Ok(match PermissionProfileDe::deserialize(deserializer)? { PermissionProfileDe::Tagged(tagged) => tagged.into(), PermissionProfileDe::Legacy(legacy) => legacy.into(), }) } } impl From 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::new( FileSystemPath::Special { value: FileSystemSpecialPath::Root, }, 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, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] phase: Option, }, 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, #[ts(as = "FunctionCallOutputBody")] #[schemars(with = "FunctionCallOutputBody")] output: FunctionCallOutputPayload, }, ToolSearchOutput { call_id: String, status: String, execution: String, #[ts(type = "unknown[]")] tools: Vec, }, } #[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, }, InputAudio { audio_url: String, }, 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 { 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, } /// Internal Responses API passthrough metadata copied into underlying chat messages. /// /// Responses API strongly types this payload. Do not modify it without first getting API /// approval and making the corresponding Responses API change. #[serde_as] #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)] pub struct InternalChatMessageMetadataPassthrough { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub turn_id: Option, /// Message creation time in fractional Unix seconds. #[serde(default, skip_serializing_if = "Option::is_none")] #[schemars(skip)] #[ts(skip)] pub create_time: Option, /// Harness-owned classifications aligned with the item's content entries. #[serde_as(deserialize_as = "serde_with::DefaultOnError")] #[serde(default, skip_serializing_if = "Option::is_none")] #[schemars(skip)] #[ts(skip)] pub content_item_kinds: Option>, /// Warehouse-only Responses metadata, not part of the public app-server protocol. #[serde(default, skip_deserializing, skip_serializing_if = "Option::is_none")] #[schemars(skip)] #[ts(skip)] pub executed_tool_calls: Option>, } impl InternalChatMessageMetadataPassthrough { pub(crate) fn set_turn_id_if_missing(metadata: &mut Option, turn_id: &str) { if turn_id.is_empty() || metadata .as_ref() .and_then(|metadata| metadata.turn_id.as_deref()) .is_some_and(|turn_id| !turn_id.is_empty()) { return; } metadata.get_or_insert_with(Self::default).turn_id = Some(turn_id.to_string()); } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ResponseItem { #[schemars(skip)] #[ts(skip)] AdditionalTools { #[serde(default, skip_serializing_if = "Option::is_none")] id: Option, role: String, tools: Vec, }, Message { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] id: Option, role: String, content: Vec, // 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, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] internal_chat_message_metadata_passthrough: Option, }, AgentMessage { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] id: Option, author: String, recipient: String, content: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] internal_chat_message_metadata_passthrough: Option, }, Reasoning { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] id: Option, summary: Vec, #[serde(default, skip_serializing_if = "should_serialize_reasoning_content")] #[ts(optional)] content: Option>, encrypted_content: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] internal_chat_message_metadata_passthrough: Option, }, LocalShellCall { /// Legacy id field retained for compatibility with older payloads. #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] id: Option, /// Set when using the Responses API. call_id: Option, status: LocalShellStatus, action: LocalShellAction, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] internal_chat_message_metadata_passthrough: Option, }, FunctionCall { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] id: Option, name: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] namespace: Option, // 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, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] encrypted_function_args: Option>, call_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] internal_chat_message_metadata_passthrough: Option, }, ToolSearchCall { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] id: Option, call_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] status: Option, execution: String, #[ts(type = "unknown")] arguments: serde_json::Value, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] internal_chat_message_metadata_passthrough: Option, }, // 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 { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] call_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] name: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] namespace: Option, #[ts(as = "FunctionCallOutputBody")] #[schemars(with = "FunctionCallOutputBody")] output: FunctionCallOutputPayload, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] internal_chat_message_metadata_passthrough: Option, }, CustomToolCall { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] status: Option, call_id: String, name: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] namespace: Option, input: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] internal_chat_message_metadata_passthrough: Option, }, // `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 { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] id: Option, call_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] name: Option, #[ts(as = "FunctionCallOutputBody")] #[schemars(with = "FunctionCallOutputBody")] output: FunctionCallOutputPayload, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] internal_chat_message_metadata_passthrough: Option, }, ToolSearchOutput { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] id: Option, call_id: Option, status: String, execution: String, #[ts(type = "unknown[]")] tools: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] internal_chat_message_metadata_passthrough: Option, }, // 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_if = "Option::is_none")] #[ts(optional)] id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] status: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] action: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] internal_chat_message_metadata_passthrough: Option, }, // 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 { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] id: Option, status: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] revised_prompt: Option, result: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] internal_chat_message_metadata_passthrough: Option, }, #[serde(alias = "compaction_summary")] Compaction { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] id: Option, encrypted_content: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] internal_chat_message_metadata_passthrough: Option, }, // Compaction triggers are request controls, not durable response items. CompactionTrigger {}, ContextCompaction { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] encrypted_content: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] internal_chat_message_metadata_passthrough: Option, }, #[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 Responses API item ID, if present. pub fn id(&self) -> Option<&ResponseItemId> { match self { Self::AdditionalTools { id, .. } | Self::Message { id, .. } | Self::AgentMessage { id, .. } | Self::LocalShellCall { id, .. } | Self::FunctionCall { id, .. } | Self::ToolSearchCall { id, .. } | Self::FunctionCallOutput { id, .. } | Self::CustomToolCall { id, .. } | Self::CustomToolCallOutput { id, .. } | Self::ToolSearchOutput { id, .. } | Self::WebSearchCall { id, .. } | Self::Reasoning { id, .. } | Self::ImageGenerationCall { id, .. } | Self::Compaction { id, .. } | Self::ContextCompaction { id, .. } => id.as_ref(), Self::CompactionTrigger { .. } | Self::Other => None, } } /// Sets or clears the Responses API item ID for variants that carry one. pub fn set_id(&mut self, new_id: Option) { match self { Self::AdditionalTools { id, .. } | Self::Message { id, .. } | Self::AgentMessage { id, .. } | Self::LocalShellCall { id, .. } | Self::FunctionCall { id, .. } | Self::ToolSearchCall { id, .. } | Self::FunctionCallOutput { id, .. } | Self::CustomToolCall { id, .. } | Self::CustomToolCallOutput { id, .. } | Self::ToolSearchOutput { id, .. } | Self::WebSearchCall { id, .. } | Self::Reasoning { id, .. } | Self::ImageGenerationCall { id, .. } | Self::Compaction { id, .. } | Self::ContextCompaction { id, .. } => *id = new_id, Self::CompactionTrigger { .. } | Self::Other => {} } } /// Returns the Responses API item ID prefix for variants that carry an ID. pub fn id_prefix(&self) -> Option<&'static str> { match self { Self::AdditionalTools { .. } => Some("at"), Self::Message { .. } => Some("msg"), Self::AgentMessage { .. } => Some("amsg"), Self::Reasoning { .. } => Some("rs"), Self::LocalShellCall { .. } => Some("lsh"), Self::FunctionCall { .. } => Some("fc"), Self::ToolSearchCall { .. } => Some("tsc"), Self::FunctionCallOutput { .. } => Some("fco"), Self::CustomToolCall { .. } => Some("ctc"), Self::CustomToolCallOutput { .. } => Some("ctco"), Self::ToolSearchOutput { .. } => Some("tso"), Self::WebSearchCall { .. } => Some("ws"), Self::ImageGenerationCall { .. } => Some("ig"), Self::Compaction { .. } | Self::ContextCompaction { .. } => Some("cmp"), Self::CompactionTrigger { .. } | Self::Other => None, } } /// Returns the non-empty turn ID stamped onto this item, if present. pub fn turn_id(&self) -> Option<&str> { self.internal_chat_message_metadata_passthrough() .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 set_turn_id_if_missing(&mut self, turn_id: &str) { let Some(metadata) = self.internal_chat_message_metadata_passthrough_mut() else { return; }; InternalChatMessageMetadataPassthrough::set_turn_id_if_missing(metadata, turn_id); } /// Stamps a harness-authored durable item without replacing its creation time. pub fn set_create_time_if_missing(&mut self, create_time: serde_json::Number) { let metadata = match self { Self::Message { role, internal_chat_message_metadata_passthrough: metadata, .. } if matches!(role.as_str(), "user" | "developer") => metadata, Self::AgentMessage { internal_chat_message_metadata_passthrough: metadata, .. } | Self::FunctionCallOutput { internal_chat_message_metadata_passthrough: metadata, .. } | Self::CustomToolCallOutput { internal_chat_message_metadata_passthrough: metadata, .. } | Self::ToolSearchOutput { internal_chat_message_metadata_passthrough: metadata, .. } => metadata, _ => return, }; metadata .get_or_insert_default() .create_time .get_or_insert(create_time); } /// Removes internal chat message metadata passthrough before sending to a provider that does /// not accept it. pub fn clear_internal_chat_message_metadata_passthrough(&mut self) { if let Some(metadata) = self.internal_chat_message_metadata_passthrough_mut() { *metadata = None; } } /// Removes content item classifications while preserving other passthrough metadata. pub fn clear_content_item_kinds(&mut self) { let Some(metadata) = self.internal_chat_message_metadata_passthrough_mut() else { return; }; let Some(metadata_value) = metadata else { return; }; metadata_value.content_item_kinds = None; if metadata_value == &InternalChatMessageMetadataPassthrough::default() { *metadata = None; } } fn internal_chat_message_metadata_passthrough( &self, ) -> Option<&InternalChatMessageMetadataPassthrough> { match self { Self::Message { internal_chat_message_metadata_passthrough: metadata, .. } | Self::AgentMessage { internal_chat_message_metadata_passthrough: metadata, .. } | Self::Reasoning { internal_chat_message_metadata_passthrough: metadata, .. } | Self::LocalShellCall { internal_chat_message_metadata_passthrough: metadata, .. } | Self::FunctionCall { internal_chat_message_metadata_passthrough: metadata, .. } | Self::ToolSearchCall { internal_chat_message_metadata_passthrough: metadata, .. } | Self::FunctionCallOutput { internal_chat_message_metadata_passthrough: metadata, .. } | Self::CustomToolCall { internal_chat_message_metadata_passthrough: metadata, .. } | Self::CustomToolCallOutput { internal_chat_message_metadata_passthrough: metadata, .. } | Self::ToolSearchOutput { internal_chat_message_metadata_passthrough: metadata, .. } | Self::WebSearchCall { internal_chat_message_metadata_passthrough: metadata, .. } | Self::ImageGenerationCall { internal_chat_message_metadata_passthrough: metadata, .. } | Self::Compaction { internal_chat_message_metadata_passthrough: metadata, .. } | Self::ContextCompaction { internal_chat_message_metadata_passthrough: metadata, .. } => metadata.as_ref(), Self::CompactionTrigger { .. } | Self::AdditionalTools { .. } | Self::Other => None, } } fn internal_chat_message_metadata_passthrough_mut( &mut self, ) -> Option<&mut Option> { match self { Self::Message { internal_chat_message_metadata_passthrough: metadata, .. } | Self::AgentMessage { internal_chat_message_metadata_passthrough: metadata, .. } | Self::Reasoning { internal_chat_message_metadata_passthrough: metadata, .. } | Self::LocalShellCall { internal_chat_message_metadata_passthrough: metadata, .. } | Self::FunctionCall { internal_chat_message_metadata_passthrough: metadata, .. } | Self::ToolSearchCall { internal_chat_message_metadata_passthrough: metadata, .. } | Self::FunctionCallOutput { internal_chat_message_metadata_passthrough: metadata, .. } | Self::CustomToolCall { internal_chat_message_metadata_passthrough: metadata, .. } | Self::CustomToolCallOutput { internal_chat_message_metadata_passthrough: metadata, .. } | Self::ToolSearchOutput { internal_chat_message_metadata_passthrough: metadata, .. } | Self::WebSearchCall { internal_chat_message_metadata_passthrough: metadata, .. } | Self::ImageGenerationCall { internal_chat_message_metadata_passthrough: metadata, .. } | Self::Compaction { internal_chat_message_metadata_passthrough: metadata, .. } | Self::ContextCompaction { internal_chat_message_metadata_passthrough: metadata, .. } => Some(metadata), Self::CompactionTrigger { .. } | Self::AdditionalTools { .. } | Self::Other => None, } } } pub const BASE_INSTRUCTIONS_DEFAULT: &str = include_str!("prompts/base_instructions/default.md"); /// Describes whether persisted base instructions were supplied by the user or generated for a model. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)] #[serde(tag = "type", rename_all = "snake_case")] #[ts(tag = "type")] pub enum BaseInstructionsProvenance { /// The instructions were explicitly configured and must survive model changes unchanged. Custom, /// The instructions were generated from this model's instruction template. Model { model: String }, } /// 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, /// Missing on rollouts written before base-instruction provenance was persisted. #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub provenance: Option, } impl Default for BaseInstructions { fn default() -> Self { Self { text: BASE_INSTRUCTIONS_DEFAULT.to_string(), provenance: None, } } } 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>) -> Option { 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::>() .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::>() .join(", "); format!("[{tokens}]") } fn should_serialize_reasoning_content(content: &Option>) -> bool { match content { Some(content) => !content .iter() .any(|c| matches!(c, ReasoningItemContent::ReasoningText { .. })), None => false, } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum LocalMediaKind { Audio, Image, } impl LocalMediaKind { fn name(self) -> &'static str { match self { Self::Audio => "audio", Self::Image => "image", } } } fn local_media_error_placeholder( path: &std::path::Path, error: impl std::fmt::Display, media_kind: LocalMediaKind, ) -> ContentItem { let media_name = media_kind.name(); let path = path.display(); ContentItem::InputText { text: format!("Codex could not read the local {media_name} at `{path}`: {error}"), } } pub const VIEW_IMAGE_TOOL_NAME: &str = "view_image"; const IMAGE_OPEN_TAG: &str = ""; const IMAGE_CLOSE_TAG: &str = ""; const LOCAL_IMAGE_OPEN_TAG_PREFIX: &str = ""; const LOCAL_IMAGE_CLOSE_TAG: &str = IMAGE_CLOSE_TAG; const AUDIO_OPEN_TAG: &str = ""; const LOCAL_AUDIO_OPEN_TAG_PREFIX: &str = "