This commit is contained in:
viyatb-oai
2025-12-18 00:53:18 -08:00
1483 changed files with 138470 additions and 44142 deletions
+6 -5
View File
@@ -1,7 +1,8 @@
[package]
edition = "2024"
name = "codex-protocol"
version = { workspace = true }
version.workspace = true
edition.workspace = true
license.workspace = true
[lib]
name = "codex_protocol"
@@ -12,18 +13,17 @@ workspace = true
[dependencies]
codex-git = { workspace = true }
base64 = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-image = { workspace = true }
icu_decimal = { workspace = true }
icu_locale_core = { workspace = true }
icu_provider = { workspace = true, features = ["sync"] }
mcp-types = { workspace = true }
mime_guess = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
serde_with = { workspace = true, features = ["macros", "base64"] }
schemars = { workspace = true }
strum = { workspace = true }
strum_macros = { workspace = true }
sys-locale = { workspace = true }
@@ -37,6 +37,7 @@ uuid = { workspace = true, features = ["serde", "v7", "v4"] }
[dev-dependencies]
anyhow = { workspace = true }
pretty_assertions = { workspace = true }
tempfile = { workspace = true }
[package.metadata.cargo-shear]
+53 -21
View File
@@ -3,6 +3,7 @@ use std::path::PathBuf;
use crate::parse_command::ParsedCommand;
use crate::protocol::FileChange;
use mcp_types::RequestId;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
@@ -12,27 +13,31 @@ fn is_false(value: &bool) -> bool {
!*value
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum SandboxRiskLevel {
Low,
Medium,
High,
}
/// Proposed execpolicy change to allow commands starting with this prefix.
///
/// The `command` tokens form the prefix that would be added as an execpolicy
/// `prefix_rule(..., decision="allow")`, letting the agent bypass approval for
/// commands that start with this token sequence.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
pub struct SandboxCommandAssessment {
pub description: String,
pub risk_level: SandboxRiskLevel,
#[serde(transparent)]
#[ts(type = "Array<string>")]
pub struct ExecPolicyAmendment {
pub command: Vec<String>,
}
impl SandboxRiskLevel {
pub fn as_str(&self) -> &'static str {
match self {
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
}
impl ExecPolicyAmendment {
pub fn new(command: Vec<String>) -> Self {
Self { command }
}
pub fn command(&self) -> &[String] {
&self.command
}
}
impl From<Vec<String>> for ExecPolicyAmendment {
fn from(command: Vec<String>) -> Self {
Self { command }
}
}
@@ -40,6 +45,10 @@ impl SandboxRiskLevel {
pub struct ExecApprovalRequestEvent {
/// Identifier for the associated exec call, if available.
pub call_id: String,
/// Turn ID that this command belongs to.
/// Uses `#[serde(default)]` for backwards compatibility.
#[serde(default)]
pub turn_id: String,
/// The command to be executed.
pub command: Vec<String>,
/// The command's working directory.
@@ -47,18 +56,41 @@ pub struct ExecApprovalRequestEvent {
/// Optional human-readable reason for the approval (e.g. retry without sandbox).
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
/// Optional model-provided risk assessment describing the blocked command.
#[serde(skip_serializing_if = "Option::is_none")]
pub risk: Option<SandboxCommandAssessment>,
/// Proposed execpolicy amendment that can be applied to allow future runs.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
pub parsed_cmd: Vec<ParsedCommand>,
#[serde(default, skip_serializing_if = "is_false")]
pub network_preflight_only: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ElicitationRequestEvent {
pub server_name: String,
pub id: RequestId,
pub message: String,
// TODO: MCP servers can request we fill out a schema for the elicitation. We don't support
// this yet.
// pub requested_schema: ElicitRequestParamsRequestedSchema,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "lowercase")]
pub enum ElicitationAction {
Accept,
Decline,
Cancel,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ApplyPatchApprovalRequestEvent {
/// Responses API call id for the associated patch apply call, if available.
pub call_id: String,
/// Turn ID that this patch belongs to.
/// Uses `#[serde(default)]` for backwards compatibility with older senders.
#[serde(default)]
pub turn_id: String,
pub changes: HashMap<PathBuf, FileChange>,
/// Optional explanatory reason (e.g. request for extra write access).
#[serde(skip_serializing_if = "Option::is_none")]
+10 -28
View File
@@ -2,36 +2,8 @@ use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum_macros::Display;
use strum_macros::EnumIter;
use ts_rs::TS;
/// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning
#[derive(
Debug,
Serialize,
Deserialize,
Default,
Clone,
Copy,
PartialEq,
Eq,
Display,
JsonSchema,
TS,
EnumIter,
Hash,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum ReasoningEffort {
None,
Minimal,
Low,
#[default]
Medium,
High,
}
/// A summary of the reasoning performed by the model. This can be useful for
/// debugging and understanding the model's reasoning process.
/// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries
@@ -98,3 +70,13 @@ pub enum ForcedLoginMethod {
Chatgpt,
Api,
}
/// Represents the trust level for a project directory.
/// This determines the approval policy and sandbox mode applied.
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum TrustLevel {
Trusted,
Untrusted,
}
+4
View File
@@ -11,6 +11,8 @@ use serde::Serialize;
use ts_rs::TS;
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
#[serde(tag = "type")]
#[ts(tag = "type")]
pub enum TurnItem {
UserMessage(UserMessageItem),
AgentMessage(AgentMessageItem),
@@ -25,6 +27,8 @@ pub struct UserMessageItem {
}
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
#[serde(tag = "type")]
#[ts(tag = "type")]
pub enum AgentMessageContent {
Text { text: String },
}
+1
View File
@@ -8,6 +8,7 @@ pub mod items;
pub mod message_history;
pub mod models;
pub mod num_format;
pub mod openai_models;
pub mod parse_command;
pub mod plan_tool;
pub mod protocol;
+211 -45
View File
@@ -1,6 +1,5 @@
use std::collections::HashMap;
use base64::Engine;
use codex_utils_image::load_and_resize_to_fit;
use mcp_types::CallToolResult;
use mcp_types::ContentBlock;
@@ -15,6 +14,25 @@ use codex_git::GhostCommit;
use codex_utils_image::error::ImageProcessingError;
use schemars::JsonSchema;
/// Controls whether a command should use the session sandbox or bypass it.
#[derive(
Debug, Clone, Copy, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS,
)]
#[serde(rename_all = "snake_case")]
pub enum SandboxPermissions {
/// Run with the configured sandbox
#[default]
UseDefault,
/// Request to run outside the sandbox
RequireEscalated,
}
impl SandboxPermissions {
pub fn requires_escalated_permissions(self) -> bool {
matches!(self, SandboxPermissions::RequireEscalated)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ResponseInputItem {
@@ -132,6 +150,10 @@ pub enum ResponseItem {
GhostSnapshot {
ghost_commit: GhostCommit,
},
#[serde(alias = "compaction_summary")]
Compaction {
encrypted_content: String,
},
#[serde(other)]
Other,
}
@@ -158,6 +180,29 @@ fn local_image_error_placeholder(
}
}
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 format `{}`.",
path.display(),
mime
),
}
}
impl From<ResponseInputItem> for ResponseItem {
fn from(item: ResponseInputItem) -> Self {
match item {
@@ -214,8 +259,24 @@ pub struct LocalShellExecAction {
#[serde(tag = "type", rename_all = "snake_case")]
pub enum WebSearchAction {
Search {
query: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
query: Option<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,
}
@@ -239,52 +300,37 @@ impl From<Vec<UserInput>> for ResponseInputItem {
role: "user".to_string(),
content: items
.into_iter()
.map(|c| match c {
UserInput::Text { text } => ContentItem::InputText { text },
UserInput::Image { image_url } => ContentItem::InputImage { image_url },
.filter_map(|c| match c {
UserInput::Text { text } => Some(ContentItem::InputText { text }),
UserInput::Image { image_url } => Some(ContentItem::InputImage { image_url }),
UserInput::LocalImage { path } => match load_and_resize_to_fit(&path) {
Ok(image) => ContentItem::InputImage {
Ok(image) => Some(ContentItem::InputImage {
image_url: image.into_data_url(),
},
}),
Err(err) => {
tracing::warn!("Failed to resize image {}: {}", path.display(), err);
if matches!(&err, ImageProcessingError::Read { .. }) {
local_image_error_placeholder(&path, &err)
Some(local_image_error_placeholder(&path, &err))
} else if err.is_invalid_image() {
Some(invalid_image_error_placeholder(&path, &err))
} else {
match std::fs::read(&path) {
Ok(bytes) => {
let Some(mime_guess) = mime_guess::from_path(&path).first()
else {
return local_image_error_placeholder(
&path,
"unsupported MIME type (unknown)",
);
};
let mime = mime_guess.essence_str().to_owned();
if !mime.starts_with("image/") {
return local_image_error_placeholder(
&path,
format!("unsupported MIME type `{mime}`"),
);
}
let encoded =
base64::engine::general_purpose::STANDARD.encode(bytes);
ContentItem::InputImage {
image_url: format!("data:{mime};base64,{encoded}"),
}
}
Err(read_err) => {
tracing::warn!(
"Skipping image {} – could not read file: {}",
path.display(),
read_err
);
local_image_error_placeholder(&path, &read_err)
}
let Some(mime_guess) = mime_guess::from_path(&path).first() else {
return Some(local_image_error_placeholder(
&path,
"unsupported MIME type (unknown)",
));
};
let mime = mime_guess.essence_str().to_owned();
if !mime.starts_with("image/") {
return Some(local_image_error_placeholder(
&path,
format!("unsupported MIME type `{mime}`"),
));
}
Some(unsupported_image_error_placeholder(&path, &mime))
}
}
},
UserInput::Skill { .. } => None, // Skill bodies are injected later in core
})
.collect::<Vec<ContentItem>>(),
}
@@ -301,8 +347,9 @@ pub struct ShellToolCallParams {
/// This is the maximum time in milliseconds that the command is allowed to run.
#[serde(alias = "timeout")]
pub timeout_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub with_escalated_permissions: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub sandbox_permissions: Option<SandboxPermissions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub justification: Option<String>,
}
@@ -314,11 +361,15 @@ 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(skip_serializing_if = "Option::is_none")]
pub with_escalated_permissions: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub sandbox_permissions: Option<SandboxPermissions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub justification: Option<String>,
}
@@ -365,6 +416,7 @@ impl Serialize for FunctionCallOutputPayload {
where
S: Serializer,
{
tracing::debug!("Function call output payload: {:?}", self);
if let Some(items) = &self.content_items {
items.serialize(serializer)
} else {
@@ -452,7 +504,7 @@ fn convert_content_blocks_to_items(
) -> Option<Vec<FunctionCallOutputContentItem>> {
let mut saw_image = false;
let mut items = Vec::with_capacity(blocks.len());
tracing::warn!("Blocks: {:?}", blocks);
for block in blocks {
match block {
ContentBlock::TextContent(text) => {
@@ -503,6 +555,7 @@ mod tests {
use anyhow::Result;
use mcp_types::ImageContent;
use mcp_types::TextContent;
use pretty_assertions::assert_eq;
use tempfile::tempdir;
#[test]
@@ -616,6 +669,87 @@ mod tests {
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(),
}
);
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"
}
}"#,
WebSearchAction::Search {
query: Some("weather seattle".into()),
},
Some("completed".into()),
),
(
r#"{
"type": "web_search_call",
"status": "open",
"action": {
"type": "open_page",
"url": "https://example.com"
}
}"#,
WebSearchAction::OpenPage {
url: Some("https://example.com".into()),
},
Some("open".into()),
),
(
r#"{
"type": "web_search_call",
"status": "in_progress",
"action": {
"type": "find_in_page",
"url": "https://example.com/docs",
"pattern": "installation"
}
}"#,
WebSearchAction::FindInPage {
url: Some("https://example.com/docs".into()),
pattern: Some("installation".into()),
},
Some("in_progress".into()),
),
];
for (json_literal, expected_action, expected_status) in cases {
let parsed: ResponseItem = serde_json::from_str(json_literal)?;
let expected = ResponseItem::WebSearchCall {
id: None,
status: expected_status.clone(),
action: expected_action.clone(),
};
assert_eq!(parsed, expected);
let serialized = serde_json::to_value(&parsed)?;
let original_value: serde_json::Value = serde_json::from_str(json_literal)?;
assert_eq!(serialized, original_value);
}
Ok(())
}
#[test]
fn deserialize_shell_tool_call_params() -> Result<()> {
let json = r#"{
@@ -630,7 +764,7 @@ mod tests {
command: vec!["ls".to_string(), "-l".to_string()],
workdir: Some("/tmp".to_string()),
timeout_ms: Some(1000),
with_escalated_permissions: None,
sandbox_permissions: None,
justification: None,
},
params
@@ -703,4 +837,36 @@ mod tests {
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(),
}]);
match item {
ResponseInputItem::Message { content, .. } => {
assert_eq!(content.len(), 1);
let expected = format!(
"Codex cannot attach image at `{}`: unsupported image format `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(())
}
}
+258
View File
@@ -0,0 +1,258 @@
use std::collections::HashMap;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::IntoEnumIterator;
use strum_macros::Display;
use strum_macros::EnumIter;
use ts_rs::TS;
use crate::config_types::Verbosity;
/// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning
#[derive(
Debug,
Serialize,
Deserialize,
Default,
Clone,
Copy,
PartialEq,
Eq,
Display,
JsonSchema,
TS,
EnumIter,
Hash,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum ReasoningEffort {
None,
Minimal,
Low,
#[default]
Medium,
High,
XHigh,
}
/// A reasoning effort option that can be surfaced for a model.
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)]
pub struct ReasoningEffortPreset {
/// Effort level that the model supports.
pub effort: ReasoningEffort,
/// Short human description shown next to the effort in UIs.
pub description: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)]
pub struct ModelUpgrade {
pub id: String,
pub reasoning_effort_mapping: Option<HashMap<ReasoningEffort, ReasoningEffort>>,
pub migration_config_key: String,
}
/// Metadata describing a Codex-supported model.
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)]
pub struct ModelPreset {
/// Stable identifier for the preset.
pub id: String,
/// Model slug (e.g., "gpt-5").
pub model: String,
/// Display name shown in UIs.
pub display_name: String,
/// Short human description shown in UIs.
pub description: String,
/// Reasoning effort applied when none is explicitly chosen.
pub default_reasoning_effort: ReasoningEffort,
/// Supported reasoning effort options.
pub supported_reasoning_efforts: Vec<ReasoningEffortPreset>,
/// Whether this is the default model for new users.
pub is_default: bool,
/// recommended upgrade model
pub upgrade: Option<ModelUpgrade>,
/// Whether this preset should appear in the picker UI.
pub show_in_picker: bool,
}
/// Visibility of a model in the picker or APIs.
#[derive(
Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, TS, JsonSchema, EnumIter, Display,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum ModelVisibility {
List,
Hide,
None,
}
/// Shell execution capability for a model.
#[derive(
Debug,
Serialize,
Deserialize,
Clone,
Copy,
PartialEq,
Eq,
TS,
JsonSchema,
EnumIter,
Display,
Hash,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum ConfigShellToolType {
Default,
Local,
UnifiedExec,
Disabled,
ShellCommand,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, TS, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ApplyPatchToolType {
Freeform,
Function,
}
#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Default, Hash, TS, JsonSchema, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ReasoningSummaryFormat {
#[default]
None,
Experimental,
}
/// Server-provided truncation policy metadata for a model.
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, TS, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum TruncationMode {
Bytes,
Tokens,
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, TS, JsonSchema)]
pub struct TruncationPolicyConfig {
pub mode: TruncationMode,
pub limit: i64,
}
impl TruncationPolicyConfig {
pub const fn bytes(limit: i64) -> Self {
Self {
mode: TruncationMode::Bytes,
limit,
}
}
pub const fn tokens(limit: i64) -> Self {
Self {
mode: TruncationMode::Tokens,
limit,
}
}
}
/// Semantic version triple encoded as an array in JSON (e.g. [0, 62, 0]).
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, TS, JsonSchema)]
pub struct ClientVersion(pub i32, pub i32, pub i32);
/// Model metadata returned by the Codex backend `/models` endpoint.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
pub struct ModelInfo {
pub slug: String,
pub display_name: String,
pub description: Option<String>,
pub default_reasoning_level: ReasoningEffort,
pub supported_reasoning_levels: Vec<ReasoningEffortPreset>,
pub shell_type: ConfigShellToolType,
pub visibility: ModelVisibility,
pub minimal_client_version: ClientVersion,
pub supported_in_api: bool,
pub priority: i32,
pub upgrade: Option<String>,
pub base_instructions: Option<String>,
pub supports_reasoning_summaries: bool,
pub support_verbosity: bool,
pub default_verbosity: Option<Verbosity>,
pub apply_patch_tool_type: Option<ApplyPatchToolType>,
pub truncation_policy: TruncationPolicyConfig,
pub supports_parallel_tool_calls: bool,
pub context_window: Option<i64>,
pub reasoning_summary_format: ReasoningSummaryFormat,
pub experimental_supported_tools: Vec<String>,
}
/// Response wrapper for `/models`.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema, Default)]
pub struct ModelsResponse {
pub models: Vec<ModelInfo>,
#[serde(default)]
pub etag: String,
}
// convert ModelInfo to ModelPreset
impl From<ModelInfo> for ModelPreset {
fn from(info: ModelInfo) -> Self {
ModelPreset {
id: info.slug.clone(),
model: info.slug.clone(),
display_name: info.display_name,
description: info.description.unwrap_or_default(),
default_reasoning_effort: info.default_reasoning_level,
supported_reasoning_efforts: info.supported_reasoning_levels.clone(),
is_default: false, // default is the highest priority available model
upgrade: info.upgrade.as_ref().map(|upgrade_slug| ModelUpgrade {
id: upgrade_slug.clone(),
reasoning_effort_mapping: reasoning_effort_mapping_from_presets(
&info.supported_reasoning_levels,
),
migration_config_key: info.slug.clone(),
}),
show_in_picker: info.visibility == ModelVisibility::List,
}
}
}
fn reasoning_effort_mapping_from_presets(
presets: &[ReasoningEffortPreset],
) -> Option<HashMap<ReasoningEffort, ReasoningEffort>> {
if presets.is_empty() {
return None;
}
// Map every canonical effort to the closest supported effort for the new model.
let supported: Vec<ReasoningEffort> = presets.iter().map(|p| p.effort).collect();
let mut map = HashMap::new();
for effort in ReasoningEffort::iter() {
let nearest = nearest_effort(effort, &supported);
map.insert(effort, nearest);
}
Some(map)
}
fn effort_rank(effort: ReasoningEffort) -> i32 {
match effort {
ReasoningEffort::None => 0,
ReasoningEffort::Minimal => 1,
ReasoningEffort::Low => 2,
ReasoningEffort::Medium => 3,
ReasoningEffort::High => 4,
ReasoningEffort::XHigh => 5,
}
}
fn nearest_effort(target: ReasoningEffort, supported: &[ReasoningEffort]) -> ReasoningEffort {
let target_rank = effort_rank(target);
supported
.iter()
.copied()
.min_by_key(|candidate| (effort_rank(*candidate) - target_rank).abs())
.unwrap_or(target)
}
+435 -30
View File
@@ -11,7 +11,7 @@ use std::str::FromStr;
use std::time::Duration;
use crate::ConversationId;
use crate::config_types::ReasoningEffort as ReasoningEffortConfig;
use crate::approvals::ElicitationRequestEvent;
use crate::config_types::ReasoningSummary as ReasoningSummaryConfig;
use crate::custom_prompts::CustomPrompt;
use crate::items::TurnItem;
@@ -19,10 +19,13 @@ use crate::message_history::HistoryEntry;
use crate::models::ContentItem;
use crate::models::ResponseItem;
use crate::num_format::format_with_separators;
use crate::openai_models::ReasoningEffort as ReasoningEffortConfig;
use crate::parse_command::ParsedCommand;
use crate::plan_tool::UpdatePlanArgs;
use crate::user_input::UserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
use mcp_types::CallToolResult;
use mcp_types::RequestId;
use mcp_types::Resource as McpResource;
use mcp_types::ResourceTemplate as McpResourceTemplate;
use mcp_types::Tool as McpTool;
@@ -32,12 +35,13 @@ use serde::Serialize;
use serde_json::Value;
use serde_with::serde_as;
use strum_macros::Display;
use tracing::error;
use ts_rs::TS;
pub use crate::approvals::ApplyPatchApprovalRequestEvent;
pub use crate::approvals::ElicitationAction;
pub use crate::approvals::ExecApprovalRequestEvent;
pub use crate::approvals::SandboxCommandAssessment;
pub use crate::approvals::SandboxRiskLevel;
pub use crate::approvals::ExecPolicyAmendment;
/// Open/close tags for special user-input blocks. Used across crates to avoid
/// duplicated hardcoded strings.
@@ -153,6 +157,16 @@ pub enum Op {
decision: ReviewDecision,
},
/// Resolve an MCP elicitation request.
ResolveElicitation {
/// Name of the MCP server that issued the request.
server_name: String,
/// Request identifier from the MCP server.
request_id: RequestId,
/// User's decision for the request.
decision: ElicitationAction,
},
/// Cache a network approval decision for this session.
NetworkApprovalCache {
/// The host that was approved.
@@ -180,6 +194,19 @@ pub enum Op {
/// Request the list of available custom prompts.
ListCustomPrompts,
/// Request the list of skills for the provided `cwd` values or the session default.
ListSkills {
/// Working directories to scope repo skills discovery.
///
/// When empty, the session default working directory is used.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
cwds: Vec<PathBuf>,
/// When true, recompute skills even if a cached result exists.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
force_reload: bool,
},
/// Request the agent to summarize the current conversation context.
/// The agent will use its existing context (either conversation history or previous response id)
/// to generate a summary which will be returned as an AgentMessage event.
@@ -203,6 +230,9 @@ pub enum Op {
/// The raw command string after '!'
command: String,
},
/// Request the list of available models.
ListModels,
}
/// Determines the conditions under which the user is consulted to approve
@@ -249,7 +279,7 @@ pub enum AskForApproval {
/// Determines execution restrictions for model shell commands.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display, JsonSchema, TS)]
#[strum(serialize_all = "kebab-case")]
#[serde(tag = "mode", rename_all = "kebab-case")]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum SandboxPolicy {
/// No restrictions whatsoever. Use with caution.
#[serde(rename = "danger-full-access")]
@@ -266,7 +296,7 @@ pub enum SandboxPolicy {
/// 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<PathBuf>,
writable_roots: Vec<AbsolutePathBuf>,
/// When set to `true`, outbound network access is allowed. `false` by
/// default.
@@ -288,15 +318,15 @@ pub enum SandboxPolicy {
/// A writable root path accompanied by a list of subpaths that should remain
/// read‑only even when the root is writable. This is primarily used to ensure
/// top‑level VCS metadata directories (e.g. `.git`) under a writable root are
/// not modified by the agent.
/// that folders containing files that could be modified to escalate the
/// privileges of the agent (e.g. `.codex`, `.git`, notably `.git/hooks`) under
/// a writable root are not modified by the agent.
#[derive(Debug, Clone, PartialEq, Eq, JsonSchema)]
pub struct WritableRoot {
/// Absolute path, by construction.
pub root: PathBuf,
pub root: AbsolutePathBuf,
/// Also absolute paths, by construction.
pub read_only_subpaths: Vec<PathBuf>,
/// By construction, these subpaths are all under `root`.
pub read_only_subpaths: Vec<AbsolutePathBuf>,
}
impl WritableRoot {
@@ -378,16 +408,30 @@ impl SandboxPolicy {
network_access: _,
} => {
// Start from explicitly configured writable roots.
let mut roots: Vec<PathBuf> = writable_roots.clone();
let mut roots: Vec<AbsolutePathBuf> = writable_roots.clone();
// Always include defaults: cwd, /tmp (if present on Unix), and
// on macOS, the per-user TMPDIR unless explicitly excluded.
roots.push(cwd.to_path_buf());
// TODO(mbolin): cwd param should be AbsolutePathBuf.
let cwd_absolute = AbsolutePathBuf::from_absolute_path(cwd);
match cwd_absolute {
Ok(cwd) => {
roots.push(cwd);
}
Err(e) => {
error!(
"Ignoring invalid cwd {:?} for sandbox writable root: {}",
cwd, e
);
}
}
// Include /tmp on Unix unless explicitly excluded.
if cfg!(unix) && !exclude_slash_tmp {
let slash_tmp = PathBuf::from("/tmp");
if slash_tmp.is_dir() {
#[allow(clippy::expect_used)]
let slash_tmp =
AbsolutePathBuf::from_absolute_path("/tmp").expect("/tmp is absolute");
if slash_tmp.as_path().is_dir() {
roots.push(slash_tmp);
}
}
@@ -404,7 +448,16 @@ impl SandboxPolicy {
&& let Some(tmpdir) = std::env::var_os("TMPDIR")
&& !tmpdir.is_empty()
{
roots.push(PathBuf::from(tmpdir));
match AbsolutePathBuf::from_absolute_path(PathBuf::from(&tmpdir)) {
Ok(tmpdir_path) => {
roots.push(tmpdir_path);
}
Err(e) => {
error!(
"Ignoring invalid TMPDIR value {tmpdir:?} for sandbox writable root: {e}",
);
}
}
}
// For each root, compute subpaths that should remain read-only.
@@ -412,10 +465,20 @@ impl SandboxPolicy {
.into_iter()
.map(|writable_root| {
let mut subpaths = Vec::new();
let top_level_git = writable_root.join(".git");
if top_level_git.is_dir() {
#[allow(clippy::expect_used)]
let top_level_git = writable_root
.join(".git")
.expect(".git is a valid relative path");
if top_level_git.as_path().is_dir() {
subpaths.push(top_level_git);
}
#[allow(clippy::expect_used)]
let top_level_codex = writable_root
.join(".codex")
.expect(".codex is a valid relative path");
if top_level_codex.as_path().is_dir() {
subpaths.push(top_level_codex);
}
WritableRoot {
root: writable_root,
read_only_subpaths: subpaths,
@@ -440,6 +503,7 @@ pub struct Event {
/// NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen.
#[derive(Debug, Clone, Deserialize, Serialize, Display, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "snake_case")]
#[ts(tag = "type")]
#[strum(serialize_all = "snake_case")]
pub enum EventMsg {
/// Error while executing a submission
@@ -449,6 +513,9 @@ pub enum EventMsg {
/// indicates the task continued but the user should still be notified.
Warning(WarningEvent),
/// Conversation history was compacted (either automatically or manually).
ContextCompacted(ContextCompactedEvent),
/// Agent has started a task
TaskStarted(TaskStartedEvent),
@@ -485,6 +552,12 @@ pub enum EventMsg {
/// Ack the client's configure message.
SessionConfigured(SessionConfiguredEvent),
/// Incremental MCP startup progress updates.
McpStartupUpdate(McpStartupUpdateEvent),
/// Aggregate MCP startup completion summary.
McpStartupComplete(McpStartupCompleteEvent),
McpToolCallBegin(McpToolCallBeginEvent),
McpToolCallEnd(McpToolCallEndEvent),
@@ -499,6 +572,9 @@ pub enum EventMsg {
/// Incremental chunk of output from a running command.
ExecCommandOutputDelta(ExecCommandOutputDeltaEvent),
/// Terminal interaction for an in-progress command (stdin sent and stdout observed).
TerminalInteraction(TerminalInteractionEvent),
ExecCommandEnd(ExecCommandEndEvent),
/// Notification that the agent attached a local image via the view_image tool.
@@ -506,6 +582,8 @@ pub enum EventMsg {
ExecApprovalRequest(ExecApprovalRequestEvent),
ElicitationRequest(ElicitationRequestEvent),
ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent),
/// Notification advising the user that something they are using has been
@@ -540,6 +618,12 @@ pub enum EventMsg {
/// List of custom prompts available to the agent.
ListCustomPromptsResponse(ListCustomPromptsResponseEvent),
/// List of skills available to the agent.
ListSkillsResponse(ListSkillsResponseEvent),
/// Notification that skill data may have been updated and clients may want to reload.
SkillsUpdateAvailable,
PlanUpdate(UpdatePlanArgs),
TurnAborted(TurnAbortedEvent),
@@ -563,6 +647,35 @@ pub enum EventMsg {
ReasoningRawContentDelta(ReasoningRawContentDeltaEvent),
}
/// Codex errors that we expose to clients.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
pub enum CodexErrorInfo {
ContextWindowExceeded,
UsageLimitExceeded,
HttpConnectionFailed {
http_status_code: Option<u16>,
},
/// Failed to connect to the response SSE stream.
ResponseStreamConnectionFailed {
http_status_code: Option<u16>,
},
InternalServerError,
Unauthorized,
BadRequest,
SandboxError,
/// The response SSE stream disconnected in the middle of a turnbefore completion.
ResponseStreamDisconnected {
http_status_code: Option<u16>,
},
/// Reached the retry limit for responses.
ResponseTooManyFailedAttempts {
http_status_code: Option<u16>,
},
Other,
}
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
pub struct RawResponseItemEvent {
pub item: ResponseItem,
@@ -625,6 +738,9 @@ pub struct ReasoningContentDeltaEvent {
pub turn_id: String,
pub item_id: String,
pub delta: String,
// load with default value so it's backward compatible with the old format.
#[serde(default)]
pub summary_index: i64,
}
impl HasLegacyEvent for ReasoningContentDeltaEvent {
@@ -641,6 +757,9 @@ pub struct ReasoningRawContentDeltaEvent {
pub turn_id: String,
pub item_id: String,
pub delta: String,
// load with default value so it's backward compatible with the old format.
#[serde(default)]
pub content_index: i64,
}
impl HasLegacyEvent for ReasoningRawContentDeltaEvent {
@@ -681,6 +800,8 @@ pub struct ExitedReviewModeEvent {
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ErrorEvent {
pub message: String,
#[serde(default)]
pub codex_error_info: Option<CodexErrorInfo>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
@@ -688,6 +809,9 @@ pub struct WarningEvent {
pub message: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ContextCompactedEvent;
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct TaskCompleteEvent {
pub last_agent_message: Option<String>,
@@ -785,6 +909,8 @@ pub struct TokenCountEvent {
pub struct RateLimitSnapshot {
pub primary: Option<RateLimitWindow>,
pub secondary: Option<RateLimitWindow>,
pub credits: Option<CreditsSnapshot>,
pub plan_type: Option<crate::account::PlanType>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
@@ -799,6 +925,13 @@ pub struct RateLimitWindow {
pub resets_at: Option<i64>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
pub struct CreditsSnapshot {
pub has_credits: bool,
pub unlimited: bool,
pub balance: Option<String>,
}
// Includes prompts, tools and space to call compact.
const BASELINE_TOKENS: i64 = 12000;
@@ -931,7 +1064,13 @@ pub struct AgentReasoningRawContentDeltaEvent {
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct AgentReasoningSectionBreakEvent {}
pub struct AgentReasoningSectionBreakEvent {
// load with default value so it's backward compatible with the old format.
#[serde(default)]
pub item_id: String,
#[serde(default)]
pub summary_index: i64,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct AgentReasoningDeltaEvent {
@@ -1066,6 +1205,29 @@ pub enum SubAgentSource {
Other(String),
}
impl fmt::Display for SessionSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SessionSource::Cli => f.write_str("cli"),
SessionSource::VSCode => f.write_str("vscode"),
SessionSource::Exec => f.write_str("exec"),
SessionSource::Mcp => f.write_str("mcp"),
SessionSource::SubAgent(sub_source) => write!(f, "subagent_{sub_source}"),
SessionSource::Unknown => f.write_str("unknown"),
}
}
}
impl fmt::Display for SubAgentSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SubAgentSource::Review => f.write_str("review"),
SubAgentSource::Compact => f.write_str("compact"),
SubAgentSource::Other(other) => f.write_str(other),
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
pub struct SessionMeta {
pub id: ConversationId,
@@ -1115,6 +1277,8 @@ pub enum RolloutItem {
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
pub struct CompactedItem {
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub replacement_history: Option<Vec<ResponseItem>>,
}
impl From<CompactedItem> for ResponseItem {
@@ -1160,11 +1324,47 @@ pub struct GitInfo {
pub repository_url: Option<String>,
}
/// Review request sent to the review session.
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum ReviewDelivery {
Inline,
Detached,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "camelCase")]
#[ts(tag = "type")]
pub enum ReviewTarget {
/// Review the working tree: staged, unstaged, and untracked files.
UncommittedChanges,
/// Review changes between the current branch and the given base branch.
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
BaseBranch { branch: String },
/// Review the changes introduced by a specific commit.
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
Commit {
sha: String,
/// Optional human-readable label (e.g., commit subject) for UIs.
title: Option<String>,
},
/// Arbitrary instructions provided by the user.
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
Custom { instructions: String },
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
/// Review request sent to the review session.
pub struct ReviewRequest {
pub prompt: String,
pub user_facing_hint: String,
pub target: ReviewTarget,
#[serde(skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub user_facing_hint: Option<String>,
}
/// Structured review result produced by a child review session.
@@ -1211,25 +1411,68 @@ pub struct ReviewLineRange {
pub end: u32,
}
#[derive(Debug, Clone, Copy, Display, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum ExecCommandSource {
Agent,
UserShell,
UnifiedExecStartup,
UnifiedExecInteraction,
}
impl Default for ExecCommandSource {
fn default() -> Self {
Self::Agent
}
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ExecCommandBeginEvent {
/// Identifier so this can be paired with the ExecCommandEnd event.
pub call_id: String,
/// Identifier for the underlying PTY process (when available).
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub process_id: Option<String>,
/// Turn ID that this command belongs to.
pub turn_id: String,
/// The command to be executed.
pub command: Vec<String>,
/// The command's working directory if not the default cwd for the agent.
pub cwd: PathBuf,
pub parsed_cmd: Vec<ParsedCommand>,
/// True when this exec was initiated directly by the user (e.g. bang command),
/// not by the agent/model. Defaults to false for backwards compatibility.
/// Where the command originated. Defaults to Agent for backward compatibility.
#[serde(default)]
pub is_user_shell_command: bool,
pub source: ExecCommandSource,
/// Raw input sent to a unified exec session (if this is an interaction event).
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub interaction_input: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ExecCommandEndEvent {
/// Identifier for the ExecCommandBegin that finished.
pub call_id: String,
/// Identifier for the underlying PTY process (when available).
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub process_id: Option<String>,
/// Turn ID that this command belongs to.
pub turn_id: String,
/// The command that was executed.
pub command: Vec<String>,
/// The command's working directory if not the default cwd for the agent.
pub cwd: PathBuf,
pub parsed_cmd: Vec<ParsedCommand>,
/// Where the command originated. Defaults to Agent for backward compatibility.
#[serde(default)]
pub source: ExecCommandSource,
/// Raw input sent to a unified exec session (if this is an interaction event).
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub interaction_input: Option<String>,
/// Captured stdout
pub stdout: String,
/// Captured stderr
@@ -1275,6 +1518,17 @@ pub struct ExecCommandOutputDeltaEvent {
pub chunk: Vec<u8>,
}
#[serde_as]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
pub struct TerminalInteractionEvent {
/// Identifier for the ExecCommandBegin that produced this chunk.
pub call_id: String,
/// Process id associated with the running command.
pub process_id: String,
/// Stdin sent to the running session.
pub stdin: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct BackgroundEventEvent {
pub message: String,
@@ -1305,6 +1559,8 @@ pub struct UndoCompletedEvent {
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct StreamErrorEvent {
pub message: String,
#[serde(default)]
pub codex_error_info: Option<CodexErrorInfo>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
@@ -1316,6 +1572,10 @@ pub struct StreamInfoEvent {
pub struct PatchApplyBeginEvent {
/// Identifier so this can be paired with the PatchApplyEnd event.
pub call_id: String,
/// Turn ID that this patch belongs to.
/// Uses `#[serde(default)]` for backwards compatibility.
#[serde(default)]
pub turn_id: String,
/// If true, there was no ApplyPatchApprovalRequest for this patch.
pub auto_approved: bool,
/// The changes to be applied.
@@ -1326,12 +1586,19 @@ pub struct PatchApplyBeginEvent {
pub struct PatchApplyEndEvent {
/// Identifier for the PatchApplyBegin that finished.
pub call_id: String,
/// Turn ID that this patch belongs to.
/// Uses `#[serde(default)]` for backwards compatibility.
#[serde(default)]
pub turn_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,
/// The changes that were applied (mirrors PatchApplyBeginEvent::changes).
#[serde(default)]
pub changes: HashMap<PathBuf, FileChange>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
@@ -1360,6 +1627,37 @@ pub struct McpListToolsResponseEvent {
pub auth_statuses: std::collections::HashMap<String, McpAuthStatus>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct McpStartupUpdateEvent {
/// Server name being started.
pub server: String,
/// Current startup status.
pub status: McpStartupStatus,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[serde(rename_all = "snake_case", tag = "state")]
#[ts(rename_all = "snake_case", tag = "state")]
pub enum McpStartupStatus {
Starting,
Ready,
Failed { error: String },
Cancelled,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, Default)]
pub struct McpStartupCompleteEvent {
pub ready: Vec<String>,
pub failed: Vec<McpStartupFailure>,
pub cancelled: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct McpStartupFailure {
pub server: String,
pub error: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
@@ -1388,7 +1686,43 @@ pub struct ListCustomPromptsResponseEvent {
pub custom_prompts: Vec<CustomPrompt>,
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, TS)]
/// Response payload for `Op::ListSkills`.
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ListSkillsResponseEvent {
pub skills: Vec<SkillsListEntry>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
pub enum SkillScope {
User,
Repo,
Public,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct SkillMetadata {
pub name: String,
pub description: String,
pub path: PathBuf,
pub scope: SkillScope,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct SkillErrorInfo {
pub path: PathBuf,
pub message: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct SkillsListEntry {
pub cwd: PathBuf,
pub skills: Vec<SkillMetadata>,
pub errors: Vec<SkillErrorInfo>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct SessionConfiguredEvent {
/// Name left as session_id instead of conversation_id for backwards compatibility.
pub session_id: ConversationId,
@@ -1396,6 +1730,18 @@ pub struct SessionConfiguredEvent {
/// Tell the client what model is being queried.
pub model: String,
pub model_provider_id: String,
/// When to escalate for approval for execution
pub approval_policy: AskForApproval,
/// How to sandbox commands executed in the system
pub sandbox_policy: SandboxPolicy,
/// Working directory that should be treated as the *root* of the
/// session.
pub cwd: PathBuf,
/// The effort the model is putting into reasoning about the user's request.
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_effort: Option<ReasoningEffortConfig>,
@@ -1415,14 +1761,18 @@ pub struct SessionConfiguredEvent {
}
/// User's decision in response to an ExecApprovalRequest.
#[derive(
Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Display, JsonSchema, TS,
)]
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq, Display, JsonSchema, TS)]
#[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 apply the proposed execpolicy
/// amendment so future matching commands are permitted.
ApprovedExecpolicyAmendment {
proposed_execpolicy_amendment: ExecPolicyAmendment,
},
/// 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.
@@ -1439,7 +1789,8 @@ pub enum ReviewDecision {
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[serde(tag = "type", rename_all = "snake_case")]
#[ts(tag = "type")]
pub enum FileChange {
Add {
content: String,
@@ -1480,6 +1831,7 @@ mod tests {
use crate::items::UserMessageItem;
use crate::items::WebSearchItem;
use anyhow::Result;
use pretty_assertions::assert_eq;
use serde_json::json;
use tempfile::NamedTempFile;
@@ -1524,6 +1876,10 @@ mod tests {
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
session_id: conversation_id,
model: "codex-mini-latest".to_string(),
model_provider_id: "openai".to_string(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::ReadOnly,
cwd: PathBuf::from("/home/user/project"),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
history_entry_count: 0,
@@ -1538,6 +1894,12 @@ mod tests {
"type": "session_configured",
"session_id": "67e55044-10b1-426f-9247-bb680e5fe0c8",
"model": "codex-mini-latest",
"model_provider_id": "openai",
"approval_policy": "never",
"sandbox_policy": {
"type": "read-only"
},
"cwd": "/home/user/project",
"reasoning_effort": "medium",
"history_log_id": 0,
"history_entry_count": 0,
@@ -1565,4 +1927,47 @@ mod tests {
assert_eq!(deserialized, event);
Ok(())
}
#[test]
fn serialize_mcp_startup_update_event() -> Result<()> {
let event = Event {
id: "init".to_string(),
msg: EventMsg::McpStartupUpdate(McpStartupUpdateEvent {
server: "srv".to_string(),
status: McpStartupStatus::Failed {
error: "boom".to_string(),
},
}),
};
let value = serde_json::to_value(&event)?;
assert_eq!(value["msg"]["type"], "mcp_startup_update");
assert_eq!(value["msg"]["server"], "srv");
assert_eq!(value["msg"]["status"]["state"], "failed");
assert_eq!(value["msg"]["status"]["error"], "boom");
Ok(())
}
#[test]
fn serialize_mcp_startup_complete_event() -> Result<()> {
let event = Event {
id: "init".to_string(),
msg: EventMsg::McpStartupComplete(McpStartupCompleteEvent {
ready: vec!["a".to_string()],
failed: vec![McpStartupFailure {
server: "b".to_string(),
error: "bad".to_string(),
}],
cancelled: vec!["c".to_string()],
}),
};
let value = serde_json::to_value(&event)?;
assert_eq!(value["msg"]["type"], "mcp_startup_complete");
assert_eq!(value["msg"]["ready"][0], "a");
assert_eq!(value["msg"]["failed"][0]["server"], "b");
assert_eq!(value["msg"]["failed"][0]["error"], "bad");
assert_eq!(value["msg"]["cancelled"][0], "c");
Ok(())
}
}
+6
View File
@@ -21,4 +21,10 @@ pub enum UserInput {
LocalImage {
path: std::path::PathBuf,
},
/// Skill selected by the user (name + path to SKILL.md).
Skill {
name: String,
path: std::path::PathBuf,
},
}