//! Defines the protocol for a Codex session between a client and an agent. //! //! Uses a SQ (Submission Queue) / EQ (Event Queue) pattern to asynchronously communicate //! between user and agent. use std::collections::HashMap; use std::fmt; use std::path::Path; use std::path::PathBuf; use std::str::FromStr; // Added for FinalOutput Display implementation use mcp_types::CallToolResult; use serde::Deserialize; use serde::Serialize; use strum_macros::Display; use uuid::Uuid; use crate::config_types::ReasoningEffort as ReasoningEffortConfig; use crate::config_types::ReasoningSummary as ReasoningSummaryConfig; use crate::message_history::HistoryEntry; use crate::model_provider_info::ModelProviderInfo; /// Submission Queue Entry - requests from user #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Submission { /// Unique id for this Submission to correlate with Events pub id: String, /// Payload pub op: Op, } /// Submission operation #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] #[serde(tag = "type", rename_all = "snake_case")] #[allow(clippy::large_enum_variant)] #[non_exhaustive] pub enum Op { /// Configure the model session. ConfigureSession { /// Provider identifier ("openai", "openrouter", ...). provider: ModelProviderInfo, /// If not specified, server will use its default model. model: String, model_reasoning_effort: ReasoningEffortConfig, model_reasoning_summary: ReasoningSummaryConfig, /// Model instructions that are appended to the base instructions. user_instructions: Option, /// Base instructions override. base_instructions: Option, /// When to escalate for approval for execution approval_policy: AskForApproval, /// How to sandbox commands executed in the system sandbox_policy: SandboxPolicy, /// Disable server-side response storage (send full context each request) #[serde(default)] disable_response_storage: bool, /// Optional external notifier command tokens. Present only when the /// client wants the agent to spawn a program after each completed /// turn. #[serde(skip_serializing_if = "Option::is_none")] #[serde(default)] notify: Option>, /// Working directory that should be treated as the *root* of the /// session. All relative paths supplied by the model as well as the /// execution sandbox are resolved against this directory **instead** /// of the process-wide current working directory. CLI front-ends are /// expected to expand this to an absolute path before sending the /// `ConfigureSession` operation so that the business-logic layer can /// operate deterministically. cwd: std::path::PathBuf, /// Path to a rollout file to resume from. #[serde(skip_serializing_if = "Option::is_none")] resume_path: Option, }, /// Abort current task. /// This server sends no corresponding Event Interrupt, /// Input from the user UserInput { /// User input items, see `InputItem` items: Vec, }, /// Approve a command execution ExecApproval { /// The id of the submission we are approving id: String, /// The user's decision in response to the request. decision: ReviewDecision, }, /// Approve a code patch PatchApproval { /// The id of the submission we are approving id: String, /// The user's decision in response to the request. decision: ReviewDecision, }, /// Append an entry to the persistent cross-session message history. /// /// Note the entry is not guaranteed to be logged if the user has /// history disabled, it matches the list of "sensitive" patterns, etc. AddToHistory { /// The message text to be stored. text: String, }, /// Request a single history entry identified by `log_id` + `offset`. GetHistoryEntryRequest { offset: usize, log_id: u64 }, /// Request to shut down codex instance. Shutdown, } /// Determines the conditions under which the user is consulted to approve /// running the command proposed by Codex. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, Display)] #[serde(rename_all = "kebab-case")] #[strum(serialize_all = "kebab-case")] pub enum AskForApproval { /// Under this policy, only "known safe" commands—as determined by /// `is_safe_command()`—that **only read files** are auto‑approved. /// Everything else will ask the user to approve. #[default] #[serde(rename = "untrusted")] #[strum(serialize = "untrusted")] UnlessTrusted, /// *All* commands are auto‑approved, but they are expected to run inside a /// sandbox where network access is disabled and writes are confined to a /// specific set of paths. If the command fails, it will be escalated to /// the user to approve execution without a sandbox. OnFailure, /// Never ask the user to approve commands. Failures are immediately returned /// to the model, and never escalated to the user for approval. Never, } /// Determines execution restrictions for model shell commands. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "mode", rename_all = "kebab-case")] pub enum SandboxPolicy { /// No restrictions whatsoever. Use with caution. #[serde(rename = "danger-full-access")] DangerFullAccess, /// Read-only access to the entire file-system. #[serde(rename = "read-only")] ReadOnly, /// Same as `ReadOnly` but additionally grants write access to the current /// working directory ("workspace"). #[serde(rename = "workspace-write")] WorkspaceWrite { /// Additional folders (beyond cwd and possibly TMPDIR) that should be /// writable from within the sandbox. #[serde(default, skip_serializing_if = "Vec::is_empty")] writable_roots: Vec, /// When set to `true`, outbound network access is allowed. `false` by /// default. #[serde(default)] network_access: bool, }, } impl FromStr for SandboxPolicy { type Err = serde_json::Error; fn from_str(s: &str) -> Result { serde_json::from_str(s) } } impl SandboxPolicy { /// Returns a policy with read-only disk access and no network. pub fn new_read_only_policy() -> Self { SandboxPolicy::ReadOnly } /// Returns a policy that can read the entire disk, but can only write to /// the current working directory and the per-user tmp dir on macOS. It does /// not allow network access. pub fn new_workspace_write_policy() -> Self { SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: false, } } /// Always returns `true` for now, as we do not yet support restricting read /// access. pub fn has_full_disk_read_access(&self) -> bool { true } pub fn has_full_disk_write_access(&self) -> bool { match self { SandboxPolicy::DangerFullAccess => true, SandboxPolicy::ReadOnly => false, SandboxPolicy::WorkspaceWrite { .. } => false, } } pub fn has_full_network_access(&self) -> bool { match self { SandboxPolicy::DangerFullAccess => true, SandboxPolicy::ReadOnly => false, SandboxPolicy::WorkspaceWrite { network_access, .. } => *network_access, } } /// Returns the list of writable roots that should be passed down to the /// Landlock rules installer, tailored to the current working directory. pub fn get_writable_roots_with_cwd(&self, cwd: &Path) -> Vec { match self { SandboxPolicy::DangerFullAccess => Vec::new(), SandboxPolicy::ReadOnly => Vec::new(), SandboxPolicy::WorkspaceWrite { writable_roots, .. } => { let mut roots = writable_roots.clone(); roots.push(cwd.to_path_buf()); // Also include the per-user tmp dir on macOS. // Note this is added dynamically rather than storing it in // writable_roots because writable_roots contains only static // values deserialized from the config file. if cfg!(target_os = "macos") { if let Some(tmpdir) = std::env::var_os("TMPDIR") { roots.push(PathBuf::from(tmpdir)); } } roots } } } } /// User input #[non_exhaustive] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] #[serde(tag = "type", rename_all = "snake_case")] pub enum InputItem { Text { text: String, }, /// Pre‑encoded data: URI image. Image { image_url: String, }, /// Local image path provided by the user. This will be converted to an /// `Image` variant (base64 data URL) during request serialization. LocalImage { path: std::path::PathBuf, }, } /// Event Queue Entry - events from agent #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Event { /// Submission `id` that this event is correlated with. pub id: String, /// Payload pub msg: EventMsg, } /// Response event from the agent #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum EventMsg { /// Error while executing a submission Error(ErrorEvent), /// Agent has started a task TaskStarted, /// Agent has completed all actions TaskComplete(TaskCompleteEvent), /// Token count event, sent periodically to report the number of tokens /// used in the current session. TokenCount(TokenUsage), /// Agent text output message AgentMessage(AgentMessageEvent), /// Agent text output delta message AgentMessageDelta(AgentMessageDeltaEvent), /// Reasoning event from agent. AgentReasoning(AgentReasoningEvent), /// Agent reasoning delta event from agent. AgentReasoningDelta(AgentReasoningDeltaEvent), /// Ack the client's configure message. SessionConfigured(SessionConfiguredEvent), McpToolCallBegin(McpToolCallBeginEvent), McpToolCallEnd(McpToolCallEndEvent), /// Notification that the server is about to execute a command. ExecCommandBegin(ExecCommandBeginEvent), ExecCommandEnd(ExecCommandEndEvent), ExecApprovalRequest(ExecApprovalRequestEvent), ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent), BackgroundEvent(BackgroundEventEvent), /// Notification that the agent is about to apply a code patch. Mirrors /// `ExecCommandBegin` so front‑ends can show progress indicators. PatchApplyBegin(PatchApplyBeginEvent), /// Notification that a patch application has finished. PatchApplyEnd(PatchApplyEndEvent), /// Response to GetHistoryEntryRequest. GetHistoryEntryResponse(GetHistoryEntryResponseEvent), /// Notification that the agent is shutting down. ShutdownComplete, } // Individual event payload types matching each `EventMsg` variant. #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ErrorEvent { pub message: String, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct TaskCompleteEvent { pub last_agent_message: Option, } #[derive(Debug, Clone, Deserialize, Serialize, Default)] pub struct TokenUsage { pub input_tokens: u64, pub cached_input_tokens: Option, pub output_tokens: u64, pub reasoning_output_tokens: Option, pub total_tokens: u64, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct FinalOutput { pub token_usage: TokenUsage, } impl From for FinalOutput { fn from(token_usage: TokenUsage) -> Self { Self { token_usage } } } impl fmt::Display for FinalOutput { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let u = &self.token_usage; write!( f, "Token usage: total={} input={}{} output={}{}", u.total_tokens, u.input_tokens, u.cached_input_tokens .map(|c| format!(" (cached {c})")) .unwrap_or_default(), u.output_tokens, u.reasoning_output_tokens .map(|r| format!(" (reasoning {r})")) .unwrap_or_default() ) } } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct AgentMessageEvent { pub message: String, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct AgentMessageDeltaEvent { pub delta: String, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct AgentReasoningEvent { pub text: String, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct AgentReasoningDeltaEvent { pub delta: String, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct McpToolCallBeginEvent { /// Identifier so this can be paired with the McpToolCallEnd event. pub call_id: String, /// Name of the MCP server as defined in the config. pub server: String, /// Name of the tool as given by the MCP server. pub tool: String, /// Arguments to the tool call. pub arguments: Option, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct McpToolCallEndEvent { /// Identifier for the corresponding McpToolCallBegin that finished. pub call_id: String, /// Result of the tool call. Note this could be an error. pub result: Result, } impl McpToolCallEndEvent { pub fn is_success(&self) -> bool { match &self.result { Ok(result) => !result.is_error.unwrap_or(false), Err(_) => false, } } } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ExecCommandBeginEvent { /// Identifier so this can be paired with the ExecCommandEnd event. pub call_id: String, /// The command to be executed. pub command: Vec, /// The command's working directory if not the default cwd for the agent. pub cwd: PathBuf, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ExecCommandEndEvent { /// Identifier for the ExecCommandBegin that finished. pub call_id: String, /// Captured stdout pub stdout: String, /// Captured stderr pub stderr: String, /// The command's exit code. pub exit_code: i32, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ExecApprovalRequestEvent { /// Identifier for the associated exec call, if available. pub call_id: String, /// The command to be executed. pub command: Vec, /// The command's working directory. pub cwd: PathBuf, /// Optional human-readable reason for the approval (e.g. retry without sandbox). #[serde(skip_serializing_if = "Option::is_none")] pub reason: Option, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ApplyPatchApprovalRequestEvent { /// Responses API call id for the associated patch apply call, if available. pub call_id: String, pub changes: HashMap, /// Optional explanatory reason (e.g. request for extra write access). #[serde(skip_serializing_if = "Option::is_none")] pub reason: Option, /// When set, the agent is asking the user to allow writes under this root for the remainder of the session. #[serde(skip_serializing_if = "Option::is_none")] pub grant_root: Option, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct BackgroundEventEvent { pub message: String, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct PatchApplyBeginEvent { /// Identifier so this can be paired with the PatchApplyEnd event. pub call_id: String, /// If true, there was no ApplyPatchApprovalRequest for this patch. pub auto_approved: bool, /// The changes to be applied. pub changes: HashMap, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct PatchApplyEndEvent { /// Identifier for the PatchApplyBegin that finished. pub call_id: String, /// Captured stdout (summary printed by apply_patch). pub stdout: String, /// Captured stderr (parser errors, IO failures, etc.). pub stderr: String, /// Whether the patch was applied successfully. pub success: bool, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct GetHistoryEntryResponseEvent { pub offset: usize, pub log_id: u64, /// The entry at the requested offset, if available and parseable. #[serde(skip_serializing_if = "Option::is_none")] pub entry: Option, } #[derive(Debug, Default, Clone, Deserialize, Serialize)] pub struct SessionConfiguredEvent { /// Unique id for this session. pub session_id: Uuid, /// Tell the client what model is being queried. pub model: String, /// Identifier of the history log file (inode on Unix, 0 otherwise). pub history_log_id: u64, /// Current number of entries in the history log. pub history_entry_count: usize, } /// User's decision in response to an ExecApprovalRequest. #[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum ReviewDecision { /// User has approved this command and the agent should execute it. Approved, /// User has approved this command and wants to automatically approve any /// future identical instances (`command` and `cwd` match exactly) for the /// remainder of the session. ApprovedForSession, /// User has denied this command and the agent should not execute it, but /// it should continue the session and try something else. #[default] Denied, /// User has denied this command and the agent should not do anything until /// the user's next command. Abort, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum FileChange { Add { content: String, }, Delete, Update { unified_diff: String, move_path: Option, }, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Chunk { /// 1-based line index of the first line in the original file pub orig_index: u32, pub deleted_lines: Vec, pub inserted_lines: Vec, } #[cfg(test)] mod tests { #![allow(clippy::unwrap_used)] use super::*; /// Serialize Event to verify that its JSON representation has the expected /// amount of nesting. #[test] fn serialize_event() { let session_id: Uuid = uuid::uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8"); let event = Event { id: "1234".to_string(), msg: EventMsg::SessionConfigured(SessionConfiguredEvent { session_id, model: "codex-mini-latest".to_string(), history_log_id: 0, history_entry_count: 0, }), }; let serialized = serde_json::to_string(&event).unwrap(); assert_eq!( serialized, r#"{"id":"1234","msg":{"type":"session_configured","session_id":"67e55044-10b1-426f-9247-bb680e5fe0c8","model":"codex-mini-latest","history_log_id":0,"history_entry_count":0}}"# ); } }