Merge ce54e4725b into sapling-pr-archive-bolinfest

This commit is contained in:
Michael Bolin
2026-02-26 17:11:23 -08:00
committed by GitHub
68 changed files with 1540 additions and 599 deletions
+1
View File
@@ -1791,6 +1791,7 @@ dependencies = [
"eventsource-stream",
"futures",
"http 1.4.0",
"iana-time-zone",
"image",
"indexmap 2.13.0",
"insta",
+1
View File
@@ -186,6 +186,7 @@ icu_locale_core = "2.1"
icu_provider = { version = "2.1", features = ["sync"] }
ignore = "0.4.23"
image = { version = "^0.25.9", default-features = false }
iana-time-zone = "0.1.64"
include_dir = "0.7.4"
indexmap = "2.12.0"
insta = "1.46.3"
@@ -560,7 +560,12 @@ impl CodexMessageProcessor {
Ok((review_request, hint))
}
pub async fn process_request(&mut self, connection_id: ConnectionId, request: ClientRequest) {
pub async fn process_request(
&mut self,
connection_id: ConnectionId,
request: ClientRequest,
client_name: Option<String>,
) {
let to_connection_request_id = |request_id| ConnectionRequestId {
connection_id,
request_id,
@@ -647,8 +652,12 @@ impl CodexMessageProcessor {
.await;
}
ClientRequest::TurnStart { request_id, params } => {
self.turn_start(to_connection_request_id(request_id), params)
.await;
self.turn_start(
to_connection_request_id(request_id),
params,
client_name.clone(),
)
.await;
}
ClientRequest::TurnSteer { request_id, params } => {
self.turn_steer(to_connection_request_id(request_id), params)
@@ -767,12 +776,20 @@ impl CodexMessageProcessor {
.await;
}
ClientRequest::SendUserMessage { request_id, params } => {
self.send_user_message(to_connection_request_id(request_id), params)
.await;
self.send_user_message(
to_connection_request_id(request_id),
params,
client_name.clone(),
)
.await;
}
ClientRequest::SendUserTurn { request_id, params } => {
self.send_user_turn(to_connection_request_id(request_id), params)
.await;
self.send_user_turn(
to_connection_request_id(request_id),
params,
client_name.clone(),
)
.await;
}
ClientRequest::InterruptConversation { request_id, params } => {
self.interrupt_conversation(to_connection_request_id(request_id), params)
@@ -5062,6 +5079,7 @@ impl CodexMessageProcessor {
&self,
request_id: ConnectionRequestId,
params: SendUserMessageParams,
client_name: Option<String>,
) {
let SendUserMessageParams {
conversation_id,
@@ -5080,6 +5098,10 @@ impl CodexMessageProcessor {
self.outgoing.send_error(request_id, error).await;
return;
};
if let Err(error) = Self::set_notify_client(conversation.as_ref(), client_name).await {
self.outgoing.send_error(request_id, error).await;
return;
}
let mapped_items: Vec<CoreInputItem> = items
.into_iter()
@@ -5110,7 +5132,12 @@ impl CodexMessageProcessor {
.await;
}
async fn send_user_turn(&self, request_id: ConnectionRequestId, params: SendUserTurnParams) {
async fn send_user_turn(
&self,
request_id: ConnectionRequestId,
params: SendUserTurnParams,
client_name: Option<String>,
) {
let SendUserTurnParams {
conversation_id,
items,
@@ -5136,6 +5163,10 @@ impl CodexMessageProcessor {
self.outgoing.send_error(request_id, error).await;
return;
};
if let Err(error) = Self::set_notify_client(conversation.as_ref(), client_name).await {
self.outgoing.send_error(request_id, error).await;
return;
}
let mapped_items: Vec<CoreInputItem> = items
.into_iter()
@@ -5160,7 +5191,7 @@ impl CodexMessageProcessor {
sandbox_policy,
model,
effort,
summary,
summary: Some(summary),
final_output_json_schema: output_schema,
collaboration_mode: None,
personality: None,
@@ -5607,7 +5638,12 @@ impl CodexMessageProcessor {
let _ = conversation.submit(Op::Interrupt).await;
}
async fn turn_start(&self, request_id: ConnectionRequestId, params: TurnStartParams) {
async fn turn_start(
&self,
request_id: ConnectionRequestId,
params: TurnStartParams,
client_name: Option<String>,
) {
if let Err(error) = Self::validate_v2_input_limit(&params.input) {
self.outgoing.send_error(request_id, error).await;
return;
@@ -5619,6 +5655,10 @@ impl CodexMessageProcessor {
return;
}
};
if let Err(error) = Self::set_notify_client(thread.as_ref(), client_name).await {
self.outgoing.send_error(request_id, error).await;
return;
}
let collaboration_modes_config = CollaborationModesConfig {
default_mode_request_user_input: thread.enabled(Feature::DefaultModeRequestUserInput),
@@ -5700,6 +5740,20 @@ impl CodexMessageProcessor {
}
}
async fn set_notify_client(
thread: &CodexThread,
client_name: Option<String>,
) -> Result<(), JSONRPCErrorError> {
match thread.set_notify_client(client_name).await {
Ok(()) => Ok(()),
Err(err) => Err(JSONRPCErrorError {
code: INTERNAL_ERROR_CODE,
message: format!("failed to set notify client: {err}"),
data: None,
}),
}
}
async fn turn_steer(&self, request_id: ConnectionRequestId, params: TurnSteerParams) {
let (_, thread) = match self.load_thread(&params.thread_id).await {
Ok(v) => v,
+3 -1
View File
@@ -140,6 +140,7 @@ pub(crate) struct ConnectionSessionState {
pub(crate) initialized: bool,
pub(crate) experimental_api_enabled: bool,
pub(crate) opted_out_notification_methods: HashSet<String>,
pub(crate) client_name: Option<String>,
}
pub(crate) struct MessageProcessorArgs {
@@ -329,6 +330,7 @@ impl MessageProcessor {
if let Ok(mut suffix) = USER_AGENT_SUFFIX.lock() {
*suffix = Some(user_agent_suffix);
}
session.client_name = Some(name.clone());
let user_agent = get_codex_user_agent();
let response = InitializeResponse { user_agent };
@@ -430,7 +432,7 @@ impl MessageProcessor {
// inline the full `CodexMessageProcessor::process_request` future, which
// can otherwise push worker-thread stack usage over the edge.
self.codex_message_processor
.process_request(connection_id, other)
.process_request(connection_id, other, session.client_name.clone())
.boxed()
.await;
}
@@ -620,6 +620,8 @@ fn append_rollout_turn_context(path: &Path, timestamp: &str, model: &str) -> std
item: RolloutItem::TurnContext(TurnContextItem {
turn_id: None,
cwd: PathBuf::from("/"),
current_date: None,
timezone: None,
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::DangerFullAccess,
network: None,
@@ -1,16 +1,24 @@
use anyhow::Result;
use app_test_support::McpProcess;
use app_test_support::create_final_assistant_message_sse_response;
use app_test_support::create_mock_responses_server_sequence_unchecked;
use app_test_support::to_response;
use codex_app_server_protocol::ClientInfo;
use codex_app_server_protocol::InitializeCapabilities;
use codex_app_server_protocol::InitializeResponse;
use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadStartResponse;
use codex_app_server_protocol::TurnStartParams;
use codex_app_server_protocol::TurnStartResponse;
use codex_app_server_protocol::UserInput as V2UserInput;
use core_test_support::fs_wait;
use pretty_assertions::assert_eq;
use serde_json::Value;
use std::path::Path;
use std::time::Duration;
use tempfile::TempDir;
use tokio::time::timeout;
@@ -178,11 +186,100 @@ async fn initialize_opt_out_notification_methods_filters_notifications() -> Resu
Ok(())
}
#[tokio::test]
async fn turn_start_notify_payload_includes_initialize_client_name() -> Result<()> {
let responses = vec![create_final_assistant_message_sse_response("Done")?];
let server = create_mock_responses_server_sequence_unchecked(responses).await;
let codex_home = TempDir::new()?;
let notify_script = codex_home.path().join("notify.py");
std::fs::write(
&notify_script,
r#"from pathlib import Path
import sys
Path(__file__).with_name("notify.json").write_text(sys.argv[-1], encoding="utf-8")
"#,
)?;
let notify_file = codex_home.path().join("notify.json");
let notify_script = notify_script
.to_str()
.expect("notify script path should be valid UTF-8");
create_config_toml_with_extra(
codex_home.path(),
&server.uri(),
"never",
&format!(
"notify = [\"python3\", {}]",
toml_basic_string(notify_script)
),
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(
DEFAULT_READ_TIMEOUT,
mcp.initialize_with_client_info(ClientInfo {
name: "xcode".to_string(),
title: Some("Xcode".to_string()),
version: "1.0.0".to_string(),
}),
)
.await??;
let thread_req = mcp
.send_thread_start_request(ThreadStartParams::default())
.await?;
let thread_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(thread_req)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response(thread_resp)?;
let turn_req = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id,
input: vec![V2UserInput::Text {
text: "Hello".to_string(),
text_elements: Vec::new(),
}],
..Default::default()
})
.await?;
let turn_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
)
.await??;
let _: TurnStartResponse = to_response(turn_resp)?;
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/completed"),
)
.await??;
fs_wait::wait_for_path_exists(&notify_file, Duration::from_secs(5)).await?;
let payload_raw = tokio::fs::read_to_string(&notify_file).await?;
let payload: Value = serde_json::from_str(&payload_raw)?;
assert_eq!(payload["client"], "xcode");
Ok(())
}
// Helper to create a config.toml pointing at the mock model server.
fn create_config_toml(
codex_home: &Path,
server_uri: &str,
approval_policy: &str,
) -> std::io::Result<()> {
create_config_toml_with_extra(codex_home, server_uri, approval_policy, "")
}
fn create_config_toml_with_extra(
codex_home: &Path,
server_uri: &str,
approval_policy: &str,
extra: &str,
) -> std::io::Result<()> {
let config_toml = codex_home.join("config.toml");
std::fs::write(
@@ -195,6 +292,8 @@ sandbox_mode = "read-only"
model_provider = "mock_provider"
{extra}
[model_providers.mock_provider]
name = "Mock provider for test"
base_url = "{server_uri}/v1"
@@ -205,3 +304,7 @@ stream_max_retries = 0
),
)
}
fn toml_basic_string(value: &str) -> String {
format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\""))
}
+1
View File
@@ -808,6 +808,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
stage_width = stage_width.max(stage.len());
rows.push((name, stage, enabled));
}
rows.sort_unstable_by_key(|(name, _, _)| *name);
for (name, stage, enabled) in rows {
println!("{name:<name_width$} {stage:<stage_width$} {enabled}");
+31
View File
@@ -2,6 +2,7 @@ use std::path::Path;
use anyhow::Result;
use predicates::str::contains;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
fn codex_command(codex_home: &Path) -> Result<assert_cmd::Command> {
@@ -58,3 +59,33 @@ async fn features_enable_under_development_feature_prints_warning() -> Result<()
Ok(())
}
#[tokio::test]
async fn features_list_is_sorted_alphabetically_by_feature_name() -> Result<()> {
let codex_home = TempDir::new()?;
let mut cmd = codex_command(codex_home.path())?;
let output = cmd
.args(["features", "list"])
.assert()
.success()
.get_output()
.stdout
.clone();
let stdout = String::from_utf8(output)?;
let actual_names = stdout
.lines()
.map(|line| {
line.split_once(" ")
.map(|(name, _)| name.trim_end().to_string())
.expect("feature list output should contain aligned columns")
})
.collect::<Vec<_>>();
let mut expected_names = actual_names.clone();
expected_names.sort();
assert_eq!(actual_names, expected_names);
Ok(())
}
+1
View File
@@ -61,6 +61,7 @@ env-flags = { workspace = true }
eventsource-stream = { workspace = true }
futures = { workspace = true }
http = { workspace = true }
iana-time-zone = { workspace = true }
indexmap = { workspace = true }
keyring = { workspace = true, features = ["crypto-rust"] }
libc = { workspace = true }
+21
View File
@@ -1179,6 +1179,18 @@
},
"type": "object"
},
"RealtimeAudioToml": {
"additionalProperties": false,
"properties": {
"microphone": {
"type": "string"
},
"speaker": {
"type": "string"
}
},
"type": "object"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
@@ -1540,6 +1552,15 @@
"default": null,
"description": "Settings for app-specific controls."
},
"audio": {
"allOf": [
{
"$ref": "#/definitions/RealtimeAudioToml"
}
],
"default": null,
"description": "Machine-local realtime audio device preferences used by realtime voice."
},
"background_terminal_max_timeout": {
"description": "Maximum poll window for background terminal output (`write_stdin`), in milliseconds. Default: `300000` (5 minutes).",
"format": "uint64",
+100 -2
View File
@@ -53,6 +53,8 @@ use crate::util::error_or_panic;
use crate::ws_version_from_features;
use async_channel::Receiver;
use async_channel::Sender;
use chrono::Local;
use chrono::Utc;
use codex_hooks::HookEvent;
use codex_hooks::HookEventAfterAgent;
use codex_hooks::HookPayload;
@@ -441,6 +443,7 @@ impl Codex {
thread_name: None,
original_config_do_not_use: Arc::clone(&config),
metrics_service_name,
notify_client: None,
session_source,
dynamic_tools,
persist_extended_history,
@@ -528,6 +531,18 @@ impl Codex {
self.session.steer_input(input, expected_turn_id).await
}
pub(crate) async fn set_notify_client(
&self,
notify_client: Option<String>,
) -> ConstraintResult<()> {
self.session
.update_settings(SessionSettingsUpdate {
notify_client,
..Default::default()
})
.await
}
pub(crate) async fn agent_status(&self) -> AgentStatus {
self.agent_status.borrow().clone()
}
@@ -595,6 +610,9 @@ pub(crate) struct TurnContext {
/// the model as well as sandbox policies are resolved against this path
/// instead of `std::env::current_dir()`.
pub(crate) cwd: PathBuf,
pub(crate) current_date: Option<String>,
pub(crate) timezone: Option<String>,
pub(crate) notify_client: Option<String>,
pub(crate) developer_instructions: Option<String>,
pub(crate) compact_prompt: Option<String>,
pub(crate) user_instructions: Option<String>,
@@ -679,6 +697,9 @@ impl TurnContext {
reasoning_summary: self.reasoning_summary,
session_source: self.session_source.clone(),
cwd: self.cwd.clone(),
current_date: self.current_date.clone(),
timezone: self.timezone.clone(),
notify_client: self.notify_client.clone(),
developer_instructions: self.developer_instructions.clone(),
compact_prompt: self.compact_prompt.clone(),
user_instructions: self.user_instructions.clone(),
@@ -719,6 +740,8 @@ impl TurnContext {
TurnContextItem {
turn_id: Some(self.sub_id.clone()),
cwd: self.cwd.clone(),
current_date: self.current_date.clone(),
timezone: self.timezone.clone(),
approval_policy: self.approval_policy.value(),
sandbox_policy: self.sandbox_policy.get().clone(),
network: self.turn_context_network_item(),
@@ -748,6 +771,16 @@ impl TurnContext {
}
}
fn local_time_context() -> (String, String) {
match iana_time_zone::get_timezone() {
Ok(timezone) => (Local::now().format("%Y-%m-%d").to_string(), timezone),
Err(_) => (
Utc::now().format("%Y-%m-%d").to_string(),
"Etc/UTC".to_string(),
),
}
}
#[derive(Clone)]
pub(crate) struct SessionConfiguration {
/// Provider identifier ("openai", "openrouter", ...).
@@ -794,6 +827,7 @@ pub(crate) struct SessionConfiguration {
original_config_do_not_use: Arc<Config>,
/// Optional service name tag for session metrics.
metrics_service_name: Option<String>,
notify_client: Option<String>,
/// Source of the session (cli, vscode, exec, mcp, ...)
session_source: SessionSource,
dynamic_tools: Vec<DynamicToolSpec>,
@@ -841,6 +875,9 @@ impl SessionConfiguration {
if let Some(cwd) = updates.cwd.clone() {
next_configuration.cwd = cwd;
}
if let Some(notify_client) = updates.notify_client.clone() {
next_configuration.notify_client = Some(notify_client);
}
Ok(next_configuration)
}
}
@@ -855,6 +892,7 @@ pub(crate) struct SessionSettingsUpdate {
pub(crate) reasoning_summary: Option<ReasoningSummaryConfig>,
pub(crate) final_output_json_schema: Option<Option<Value>>,
pub(crate) personality: Option<Personality>,
pub(crate) notify_client: Option<String>,
}
impl Session {
@@ -1017,6 +1055,7 @@ impl Session {
.features
.enabled(Feature::UseLinuxSandboxBwrap),
));
let (current_date, timezone) = local_time_context();
TurnContext {
sub_id,
config: per_turn_config.clone(),
@@ -1028,6 +1067,9 @@ impl Session {
reasoning_summary,
session_source,
cwd,
current_date: Some(current_date),
timezone: Some(timezone),
notify_client: session_configuration.notify_client.clone(),
developer_instructions: session_configuration.developer_instructions.clone(),
compact_prompt: session_configuration.compact_prompt.clone(),
user_instructions: session_configuration.user_instructions.clone(),
@@ -3924,9 +3966,10 @@ mod handlers {
sandbox_policy: Some(sandbox_policy),
windows_sandbox_level: None,
collaboration_mode,
reasoning_summary: Some(summary),
reasoning_summary: summary,
final_output_json_schema: Some(final_output_json_schema),
personality,
notify_client: None,
},
)
}
@@ -4676,6 +4719,9 @@ async fn spawn_review_thread(
tools_config,
features: parent_turn_context.features.clone(),
ghost_snapshot: parent_turn_context.ghost_snapshot.clone(),
current_date: parent_turn_context.current_date.clone(),
timezone: parent_turn_context.timezone.clone(),
notify_client: parent_turn_context.notify_client.clone(),
developer_instructions: None,
user_instructions: None,
compact_prompt: parent_turn_context.compact_prompt.clone(),
@@ -5061,6 +5107,7 @@ pub(crate) async fn run_turn(
.dispatch(HookPayload {
session_id: sess.conversation_id,
cwd: turn_context.cwd.clone(),
client: turn_context.notify_client.clone(),
triggered_at: chrono::Utc::now(),
hook_event: HookEvent::AfterAgent {
event: HookEventAfterAgent {
@@ -7259,6 +7306,8 @@ mod tests {
let previous_context_item = TurnContextItem {
turn_id: Some(turn_context.sub_id.clone()),
cwd: turn_context.cwd.clone(),
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy.get().clone(),
network: None,
@@ -7296,6 +7345,8 @@ mod tests {
let mut previous_context_item = TurnContextItem {
turn_id: Some(turn_context.sub_id.clone()),
cwd: turn_context.cwd.clone(),
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy.get().clone(),
network: None,
@@ -7490,7 +7541,10 @@ mod tests {
.record_context_updates_and_set_reference_context_item(&turn_context, None)
.await;
let history_after_second_seed = session.clone_history().await;
assert_eq!(expected, history_after_second_seed.raw_items());
assert_eq!(
history_after_seed.raw_items(),
history_after_second_seed.raw_items()
);
}
#[tokio::test]
@@ -7658,6 +7712,8 @@ mod tests {
let previous_context_item = TurnContextItem {
turn_id: Some(turn_context.sub_id.clone()),
cwd: turn_context.cwd.clone(),
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy.get().clone(),
network: None,
@@ -7861,6 +7917,7 @@ mod tests {
thread_name: None,
original_config_do_not_use: Arc::clone(&config),
metrics_service_name: None,
notify_client: None,
session_source: SessionSource::Exec,
dynamic_tools: Vec::new(),
persist_extended_history: false,
@@ -7953,6 +8010,7 @@ mod tests {
thread_name: None,
original_config_do_not_use: Arc::clone(&config),
metrics_service_name: None,
notify_client: None,
session_source: SessionSource::Exec,
dynamic_tools: Vec::new(),
persist_extended_history: false,
@@ -8264,6 +8322,7 @@ mod tests {
thread_name: None,
original_config_do_not_use: Arc::clone(&config),
metrics_service_name: None,
notify_client: None,
session_source: SessionSource::Exec,
dynamic_tools: Vec::new(),
persist_extended_history: false,
@@ -8317,6 +8376,7 @@ mod tests {
thread_name: None,
original_config_do_not_use: Arc::clone(&config),
metrics_service_name: None,
notify_client: None,
session_source: SessionSource::Exec,
dynamic_tools: Vec::new(),
persist_extended_history: false,
@@ -8398,6 +8458,7 @@ mod tests {
thread_name: None,
original_config_do_not_use: Arc::clone(&config),
metrics_service_name: None,
notify_client: None,
session_source: SessionSource::Exec,
dynamic_tools: Vec::new(),
persist_extended_history: false,
@@ -8557,6 +8618,7 @@ mod tests {
thread_name: None,
original_config_do_not_use: Arc::clone(&config),
metrics_service_name: None,
notify_client: None,
session_source: SessionSource::Exec,
dynamic_tools,
persist_extended_history: false,
@@ -8823,6 +8885,42 @@ mod tests {
assert!(environment_update.contains("<denied>blocked.example.com</denied>"));
}
#[tokio::test]
async fn build_settings_update_items_emits_environment_item_for_time_changes() {
let (session, previous_context) = make_session_and_context().await;
let previous_context = Arc::new(previous_context);
let mut current_context = previous_context
.with_model(
previous_context.model_info.slug.clone(),
&session.services.models_manager,
)
.await;
current_context.current_date = Some("2026-02-27".to_string());
current_context.timezone = Some("Europe/Berlin".to_string());
let reference_context_item = previous_context.to_turn_context_item();
let update_items = session.build_settings_update_items(
Some(&reference_context_item),
None,
&current_context,
);
let environment_update = update_items
.iter()
.find_map(|item| match item {
ResponseItem::Message { role, content, .. } if role == "user" => {
let [ContentItem::InputText { text }] = content.as_slice() else {
return None;
};
text.contains("<environment_context>").then_some(text)
}
_ => None,
})
.expect("environment update item should be emitted");
assert!(environment_update.contains("<current_date>2026-02-27</current_date>"));
assert!(environment_update.contains("<timezone>Europe/Berlin</timezone>"));
}
#[tokio::test]
async fn record_context_updates_and_set_reference_context_item_injects_full_context_when_baseline_missing()
{
+5
View File
@@ -1,6 +1,7 @@
use crate::agent::AgentStatus;
use crate::codex::Codex;
use crate::codex::SteerInputError;
use crate::config::ConstraintResult;
use crate::error::Result as CodexResult;
use crate::features::Feature;
use crate::file_watcher::WatchRegistration;
@@ -67,6 +68,10 @@ impl CodexThread {
self.codex.steer_input(input, expected_turn_id).await
}
pub async fn set_notify_client(&self, notify_client: Option<String>) -> ConstraintResult<()> {
self.codex.set_notify_client(notify_client).await
}
/// Use sparingly: this is intended to be removed soon.
pub async fn submit_with_id(&self, sub: Submission) -> CodexResult<()> {
self.codex.submit_with_id(sub).await
+68
View File
@@ -839,6 +839,30 @@ impl ConfigEditsBuilder {
self
}
pub fn set_realtime_microphone(mut self, microphone: Option<&str>) -> Self {
let segments = vec!["audio".to_string(), "microphone".to_string()];
match microphone {
Some(microphone) => self.edits.push(ConfigEdit::SetPath {
segments,
value: value(microphone),
}),
None => self.edits.push(ConfigEdit::ClearPath { segments }),
}
self
}
pub fn set_realtime_speaker(mut self, speaker: Option<&str>) -> Self {
let segments = vec!["audio".to_string(), "speaker".to_string()];
match speaker {
Some(speaker) => self.edits.push(ConfigEdit::SetPath {
segments,
value: value(speaker),
}),
None => self.edits.push(ConfigEdit::ClearPath { segments }),
}
self
}
pub fn clear_legacy_windows_sandbox_keys(mut self) -> Self {
for key in [
"experimental_windows_sandbox",
@@ -1804,6 +1828,50 @@ model_reasoning_effort = "high"
assert_eq!(notice, Some(true));
}
#[test]
fn blocking_builder_set_realtime_audio_persists_and_clears() {
let tmp = tempdir().expect("tmpdir");
let codex_home = tmp.path();
ConfigEditsBuilder::new(codex_home)
.set_realtime_microphone(Some("USB Mic"))
.set_realtime_speaker(Some("Desk Speakers"))
.apply_blocking()
.expect("persist realtime audio");
let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
let config: TomlValue = toml::from_str(&raw).expect("parse config");
let realtime_audio = config
.get("audio")
.and_then(TomlValue::as_table)
.expect("audio table should exist");
assert_eq!(
realtime_audio.get("microphone").and_then(TomlValue::as_str),
Some("USB Mic")
);
assert_eq!(
realtime_audio.get("speaker").and_then(TomlValue::as_str),
Some("Desk Speakers")
);
ConfigEditsBuilder::new(codex_home)
.set_realtime_microphone(None)
.apply_blocking()
.expect("clear realtime microphone");
let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
let config: TomlValue = toml::from_str(&raw).expect("parse config");
let realtime_audio = config
.get("audio")
.and_then(TomlValue::as_table)
.expect("audio table should exist");
assert_eq!(realtime_audio.get("microphone"), None);
assert_eq!(
realtime_audio.get("speaker").and_then(TomlValue::as_str),
Some("Desk Speakers")
);
}
#[test]
fn replace_mcp_servers_blocking_clears_table_when_empty() {
let tmp = tempdir().expect("tmpdir");
+63
View File
@@ -426,6 +426,9 @@ pub struct Config {
/// Base URL for requests to ChatGPT (as opposed to the OpenAI API).
pub chatgpt_base_url: String,
/// Machine-local realtime audio device preferences used by realtime voice.
pub realtime_audio: RealtimeAudioConfig,
/// Experimental / do not use. Overrides only the realtime conversation
/// websocket transport base URL (the `Op::RealtimeConversation` `/ws`
/// connection) without changing normal provider HTTP requests.
@@ -1175,6 +1178,10 @@ pub struct ConfigToml {
/// Base URL for requests to ChatGPT (as opposed to the OpenAI API).
pub chatgpt_base_url: Option<String>,
/// Machine-local realtime audio device preferences used by realtime voice.
#[serde(default)]
pub audio: Option<RealtimeAudioToml>,
/// Experimental / do not use. Overrides only the realtime conversation
/// websocket transport base URL (the `Op::RealtimeConversation` `/ws`
/// connection) without changing normal provider HTTP requests.
@@ -1306,6 +1313,19 @@ impl ProjectConfig {
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RealtimeAudioConfig {
pub microphone: Option<String>,
pub speaker: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct RealtimeAudioToml {
pub microphone: Option<String>,
pub speaker: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct ToolsToml {
@@ -2146,6 +2166,12 @@ impl Config {
.chatgpt_base_url
.or(cfg.chatgpt_base_url)
.unwrap_or("https://chatgpt.com/backend-api/".to_string()),
realtime_audio: cfg
.audio
.map_or_else(RealtimeAudioConfig::default, |audio| RealtimeAudioConfig {
microphone: audio.microphone,
speaker: audio.speaker,
}),
experimental_realtime_ws_base_url: cfg.experimental_realtime_ws_base_url,
experimental_realtime_ws_backend_prompt: cfg.experimental_realtime_ws_backend_prompt,
forced_chatgpt_workspace_id,
@@ -4766,6 +4792,7 @@ model_verbosity = "high"
model_verbosity: None,
personality: Some(Personality::Pragmatic),
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
realtime_audio: RealtimeAudioConfig::default(),
experimental_realtime_ws_base_url: None,
experimental_realtime_ws_backend_prompt: None,
base_instructions: None,
@@ -4892,6 +4919,7 @@ model_verbosity = "high"
model_verbosity: None,
personality: Some(Personality::Pragmatic),
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
realtime_audio: RealtimeAudioConfig::default(),
experimental_realtime_ws_base_url: None,
experimental_realtime_ws_backend_prompt: None,
base_instructions: None,
@@ -5016,6 +5044,7 @@ model_verbosity = "high"
model_verbosity: None,
personality: Some(Personality::Pragmatic),
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
realtime_audio: RealtimeAudioConfig::default(),
experimental_realtime_ws_base_url: None,
experimental_realtime_ws_backend_prompt: None,
base_instructions: None,
@@ -5126,6 +5155,7 @@ model_verbosity = "high"
model_verbosity: Some(Verbosity::High),
personality: Some(Personality::Pragmatic),
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
realtime_audio: RealtimeAudioConfig::default(),
experimental_realtime_ws_base_url: None,
experimental_realtime_ws_backend_prompt: None,
base_instructions: None,
@@ -5970,6 +6000,39 @@ experimental_realtime_ws_backend_prompt = "prompt from config"
);
Ok(())
}
#[test]
fn realtime_audio_loads_from_config_toml() -> std::io::Result<()> {
let cfg: ConfigToml = toml::from_str(
r#"
[audio]
microphone = "USB Mic"
speaker = "Desk Speakers"
"#,
)
.expect("TOML deserialization should succeed");
let realtime_audio = cfg
.audio
.as_ref()
.expect("realtime audio config should be present");
assert_eq!(realtime_audio.microphone.as_deref(), Some("USB Mic"));
assert_eq!(realtime_audio.speaker.as_deref(), Some("Desk Speakers"));
let codex_home = TempDir::new()?;
let config = Config::load_from_base_config_with_overrides(
cfg,
ConfigOverrides::default(),
codex_home.path().to_path_buf(),
)?;
assert_eq!(config.realtime_audio.microphone.as_deref(), Some("USB Mic"));
assert_eq!(
config.realtime_audio.speaker.as_deref(),
Some("Desk Speakers")
);
Ok(())
}
}
#[cfg(test)]
+136 -19
View File
@@ -13,6 +13,8 @@ use std::path::PathBuf;
pub(crate) struct EnvironmentContext {
pub cwd: Option<PathBuf>,
pub shell: Shell,
pub current_date: Option<String>,
pub timezone: Option<String>,
pub network: Option<NetworkContext>,
pub subagents: Option<String>,
}
@@ -27,12 +29,16 @@ impl EnvironmentContext {
pub fn new(
cwd: Option<PathBuf>,
shell: Shell,
current_date: Option<String>,
timezone: Option<String>,
network: Option<NetworkContext>,
subagents: Option<String>,
) -> Self {
Self {
cwd,
shell,
current_date,
timezone,
network,
subagents,
}
@@ -44,11 +50,17 @@ impl EnvironmentContext {
pub fn equals_except_shell(&self, other: &EnvironmentContext) -> bool {
let EnvironmentContext {
cwd,
current_date,
timezone,
network,
subagents,
shell: _,
} = other;
self.cwd == *cwd && self.network == *network && self.subagents == *subagents
self.cwd == *cwd
&& self.current_date == *current_date
&& self.timezone == *timezone
&& self.network == *network
&& self.subagents == *subagents
}
pub fn diff_from_turn_context_item(
@@ -63,18 +75,22 @@ impl EnvironmentContext {
} else {
None
};
let current_date = after.current_date.clone();
let timezone = after.timezone.clone();
let network = if before_network != after_network {
after_network
} else {
before_network
};
EnvironmentContext::new(cwd, shell.clone(), network, None)
EnvironmentContext::new(cwd, shell.clone(), current_date, timezone, network, None)
}
pub fn from_turn_context(turn_context: &TurnContext, shell: &Shell) -> Self {
Self::new(
Some(turn_context.cwd.clone()),
shell.clone(),
turn_context.current_date.clone(),
turn_context.timezone.clone(),
Self::network_from_turn_context(turn_context),
None,
)
@@ -84,6 +100,8 @@ impl EnvironmentContext {
Self::new(
Some(turn_context_item.cwd.clone()),
shell.clone(),
turn_context_item.current_date.clone(),
turn_context_item.timezone.clone(),
Self::network_from_turn_context_item(turn_context_item),
None,
)
@@ -143,6 +161,12 @@ impl EnvironmentContext {
let shell_name = self.shell.name();
lines.push(format!(" <shell>{shell_name}</shell>"));
if let Some(current_date) = self.current_date {
lines.push(format!(" <current_date>{current_date}</current_date>"));
}
if let Some(timezone) = self.timezone {
lines.push(format!(" <timezone>{timezone}</timezone>"));
}
match self.network {
Some(ref network) => {
lines.push(" <network enabled=\"true\">".to_string());
@@ -193,12 +217,21 @@ mod tests {
#[test]
fn serialize_workspace_write_environment_context() {
let cwd = test_path_buf("/repo");
let context = EnvironmentContext::new(Some(cwd.clone()), fake_shell(), None, None);
let context = EnvironmentContext::new(
Some(cwd.clone()),
fake_shell(),
Some("2026-02-26".to_string()),
Some("America/Los_Angeles".to_string()),
None,
None,
);
let expected = format!(
r#"<environment_context>
<cwd>{cwd}</cwd>
<shell>bash</shell>
<current_date>2026-02-26</current_date>
<timezone>America/Los_Angeles</timezone>
</environment_context>"#,
cwd = cwd.display(),
);
@@ -215,6 +248,8 @@ mod tests {
let context = EnvironmentContext::new(
Some(test_path_buf("/repo")),
fake_shell(),
Some("2026-02-26".to_string()),
Some("America/Los_Angeles".to_string()),
Some(network),
None,
);
@@ -223,6 +258,8 @@ mod tests {
r#"<environment_context>
<cwd>{}</cwd>
<shell>bash</shell>
<current_date>2026-02-26</current_date>
<timezone>America/Los_Angeles</timezone>
<network enabled="true">
<allowed>api.example.com</allowed>
<allowed>*.openai.com</allowed>
@@ -237,10 +274,19 @@ mod tests {
#[test]
fn serialize_read_only_environment_context() {
let context = EnvironmentContext::new(None, fake_shell(), None, None);
let context = EnvironmentContext::new(
None,
fake_shell(),
Some("2026-02-26".to_string()),
Some("America/Los_Angeles".to_string()),
None,
None,
);
let expected = r#"<environment_context>
<shell>bash</shell>
<current_date>2026-02-26</current_date>
<timezone>America/Los_Angeles</timezone>
</environment_context>"#;
assert_eq!(context.serialize_to_xml(), expected);
@@ -248,10 +294,19 @@ mod tests {
#[test]
fn serialize_external_sandbox_environment_context() {
let context = EnvironmentContext::new(None, fake_shell(), None, None);
let context = EnvironmentContext::new(
None,
fake_shell(),
Some("2026-02-26".to_string()),
Some("America/Los_Angeles".to_string()),
None,
None,
);
let expected = r#"<environment_context>
<shell>bash</shell>
<current_date>2026-02-26</current_date>
<timezone>America/Los_Angeles</timezone>
</environment_context>"#;
assert_eq!(context.serialize_to_xml(), expected);
@@ -259,10 +314,19 @@ mod tests {
#[test]
fn serialize_external_sandbox_with_restricted_network_environment_context() {
let context = EnvironmentContext::new(None, fake_shell(), None, None);
let context = EnvironmentContext::new(
None,
fake_shell(),
Some("2026-02-26".to_string()),
Some("America/Los_Angeles".to_string()),
None,
None,
);
let expected = r#"<environment_context>
<shell>bash</shell>
<current_date>2026-02-26</current_date>
<timezone>America/Los_Angeles</timezone>
</environment_context>"#;
assert_eq!(context.serialize_to_xml(), expected);
@@ -270,10 +334,19 @@ mod tests {
#[test]
fn serialize_full_access_environment_context() {
let context = EnvironmentContext::new(None, fake_shell(), None, None);
let context = EnvironmentContext::new(
None,
fake_shell(),
Some("2026-02-26".to_string()),
Some("America/Los_Angeles".to_string()),
None,
None,
);
let expected = r#"<environment_context>
<shell>bash</shell>
<current_date>2026-02-26</current_date>
<timezone>America/Los_Angeles</timezone>
</environment_context>"#;
assert_eq!(context.serialize_to_xml(), expected);
@@ -281,29 +354,65 @@ mod tests {
#[test]
fn equals_except_shell_compares_cwd() {
let context1 =
EnvironmentContext::new(Some(PathBuf::from("/repo")), fake_shell(), None, None);
let context2 =
EnvironmentContext::new(Some(PathBuf::from("/repo")), fake_shell(), None, None);
let context1 = EnvironmentContext::new(
Some(PathBuf::from("/repo")),
fake_shell(),
None,
None,
None,
None,
);
let context2 = EnvironmentContext::new(
Some(PathBuf::from("/repo")),
fake_shell(),
None,
None,
None,
None,
);
assert!(context1.equals_except_shell(&context2));
}
#[test]
fn equals_except_shell_ignores_sandbox_policy() {
let context1 =
EnvironmentContext::new(Some(PathBuf::from("/repo")), fake_shell(), None, None);
let context2 =
EnvironmentContext::new(Some(PathBuf::from("/repo")), fake_shell(), None, None);
let context1 = EnvironmentContext::new(
Some(PathBuf::from("/repo")),
fake_shell(),
None,
None,
None,
None,
);
let context2 = EnvironmentContext::new(
Some(PathBuf::from("/repo")),
fake_shell(),
None,
None,
None,
None,
);
assert!(context1.equals_except_shell(&context2));
}
#[test]
fn equals_except_shell_compares_cwd_differences() {
let context1 =
EnvironmentContext::new(Some(PathBuf::from("/repo1")), fake_shell(), None, None);
let context2 =
EnvironmentContext::new(Some(PathBuf::from("/repo2")), fake_shell(), None, None);
let context1 = EnvironmentContext::new(
Some(PathBuf::from("/repo1")),
fake_shell(),
None,
None,
None,
None,
);
let context2 = EnvironmentContext::new(
Some(PathBuf::from("/repo2")),
fake_shell(),
None,
None,
None,
None,
);
assert!(!context1.equals_except_shell(&context2));
}
@@ -319,6 +428,8 @@ mod tests {
},
None,
None,
None,
None,
);
let context2 = EnvironmentContext::new(
Some(PathBuf::from("/repo")),
@@ -329,6 +440,8 @@ mod tests {
},
None,
None,
None,
None,
);
assert!(context1.equals_except_shell(&context2));
@@ -339,6 +452,8 @@ mod tests {
let context = EnvironmentContext::new(
Some(test_path_buf("/repo")),
fake_shell(),
Some("2026-02-26".to_string()),
Some("America/Los_Angeles".to_string()),
None,
Some("- agent-1: atlas\n- agent-2".to_string()),
);
@@ -347,6 +462,8 @@ mod tests {
r#"<environment_context>
<cwd>{}</cwd>
<shell>bash</shell>
<current_date>2026-02-26</current_date>
<timezone>America/Los_Angeles</timezone>
<subagents>
- agent-1: atlas
- agent-2
+2
View File
@@ -1386,6 +1386,8 @@ mod tests {
item: RolloutItem::TurnContext(TurnContextItem {
turn_id: Some("turn-1".to_string()),
cwd: latest_cwd.clone(),
current_date: None,
timezone: None,
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
network: None,
+1
View File
@@ -379,6 +379,7 @@ async fn dispatch_after_tool_use_hook(
.dispatch(HookPayload {
session_id: session.conversation_id,
cwd: turn.cwd.clone(),
client: turn.notify_client.clone(),
triggered_at: chrono::Utc::now(),
hook_event: HookEvent::AfterToolUse {
event: HookEventAfterToolUse {
@@ -799,327 +799,5 @@ fn join_program_and_argv(program: &AbsolutePathBuf, argv: &[String]) -> Vec<Stri
}
#[cfg(test)]
mod tests {
use super::CoreShellActionProvider;
#[cfg(target_os = "macos")]
use super::CoreShellCommandExecutor;
use super::ParsedShellCommand;
use super::extract_shell_script;
use super::join_program_and_argv;
use super::map_exec_result;
#[cfg(target_os = "macos")]
use crate::config::Constrained;
#[cfg(target_os = "macos")]
use crate::config::Permissions;
#[cfg(target_os = "macos")]
use crate::config::types::ShellEnvironmentPolicy;
use crate::exec::SandboxType;
#[cfg(target_os = "macos")]
use crate::protocol::AskForApproval;
use crate::protocol::ReadOnlyAccess;
use crate::protocol::SandboxPolicy;
#[cfg(target_os = "macos")]
use crate::sandboxing::SandboxPermissions;
#[cfg(target_os = "macos")]
use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
#[cfg(target_os = "macos")]
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::MacOsPreferencesPermission;
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
use codex_protocol::models::PermissionProfile;
use codex_shell_escalation::EscalationExecution;
use codex_shell_escalation::EscalationPermissions;
use codex_shell_escalation::ExecResult;
use codex_shell_escalation::Permissions as EscalatedPermissions;
#[cfg(target_os = "macos")]
use codex_shell_escalation::ShellCommandExecutor;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
#[cfg(target_os = "macos")]
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;
#[test]
fn extract_shell_script_preserves_login_flag() {
assert_eq!(
extract_shell_script(&["/bin/zsh".into(), "-lc".into(), "echo hi".into()]).unwrap(),
ParsedShellCommand {
script: "echo hi".to_string(),
login: true,
}
);
assert_eq!(
extract_shell_script(&["/bin/zsh".into(), "-c".into(), "echo hi".into()]).unwrap(),
ParsedShellCommand {
script: "echo hi".to_string(),
login: false,
}
);
}
#[test]
fn extract_shell_script_supports_wrapped_command_prefixes() {
assert_eq!(
extract_shell_script(&[
"/usr/bin/env".into(),
"CODEX_EXECVE_WRAPPER=1".into(),
"/bin/zsh".into(),
"-lc".into(),
"echo hello".into()
])
.unwrap(),
ParsedShellCommand {
script: "echo hello".to_string(),
login: true,
}
);
assert_eq!(
extract_shell_script(&[
"sandbox-exec".into(),
"-p".into(),
"sandbox_policy".into(),
"/bin/zsh".into(),
"-c".into(),
"pwd".into(),
])
.unwrap(),
ParsedShellCommand {
script: "pwd".to_string(),
login: false,
}
);
}
#[test]
fn extract_shell_script_rejects_unsupported_shell_invocation() {
let err = extract_shell_script(&[
"sandbox-exec".into(),
"-fc".into(),
"echo not supported".into(),
])
.unwrap_err();
assert!(matches!(err, super::ToolError::Rejected(_)));
assert_eq!(
match err {
super::ToolError::Rejected(reason) => reason,
_ => "".to_string(),
},
"unexpected shell command format for zsh-fork execution"
);
}
#[test]
fn join_program_and_argv_replaces_original_argv_zero() {
assert_eq!(
join_program_and_argv(
&AbsolutePathBuf::from_absolute_path("/tmp/tool").unwrap(),
&["./tool".into(), "--flag".into(), "value".into()],
),
vec!["/tmp/tool", "--flag", "value"]
);
assert_eq!(
join_program_and_argv(
&AbsolutePathBuf::from_absolute_path("/tmp/tool").unwrap(),
&["./tool".into()]
),
vec!["/tmp/tool"]
);
}
#[test]
fn map_exec_result_preserves_stdout_and_stderr() {
let out = map_exec_result(
SandboxType::None,
ExecResult {
exit_code: 0,
stdout: "out".to_string(),
stderr: "err".to_string(),
output: "outerr".to_string(),
duration: Duration::from_millis(1),
timed_out: false,
},
)
.unwrap();
assert_eq!(out.stdout.text, "out");
assert_eq!(out.stderr.text, "err");
assert_eq!(out.aggregated_output.text, "outerr");
}
#[test]
fn shell_request_escalation_execution_is_explicit() {
let requested_permissions = PermissionProfile {
file_system: Some(FileSystemPermissions {
read: None,
write: Some(vec![PathBuf::from("./output")]),
}),
..Default::default()
};
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![
AbsolutePathBuf::from_absolute_path("/tmp/original/output").unwrap(),
],
read_only_access: ReadOnlyAccess::FullAccess,
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
};
let macos_seatbelt_profile_extensions = MacOsSeatbeltProfileExtensions {
macos_preferences: MacOsPreferencesPermission::ReadWrite,
..Default::default()
};
assert_eq!(
CoreShellActionProvider::shell_request_escalation_execution(
crate::sandboxing::SandboxPermissions::UseDefault,
&sandbox_policy,
None,
Some(&macos_seatbelt_profile_extensions),
),
EscalationExecution::TurnDefault,
);
assert_eq!(
CoreShellActionProvider::shell_request_escalation_execution(
crate::sandboxing::SandboxPermissions::RequireEscalated,
&sandbox_policy,
None,
Some(&macos_seatbelt_profile_extensions),
),
EscalationExecution::Unsandboxed,
);
assert_eq!(
CoreShellActionProvider::shell_request_escalation_execution(
crate::sandboxing::SandboxPermissions::WithAdditionalPermissions,
&sandbox_policy,
Some(&requested_permissions),
Some(&macos_seatbelt_profile_extensions),
),
EscalationExecution::Permissions(EscalationPermissions::Permissions(
EscalatedPermissions {
sandbox_policy,
macos_seatbelt_profile_extensions: Some(macos_seatbelt_profile_extensions),
},
)),
);
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn prepare_escalated_exec_turn_default_preserves_macos_seatbelt_extensions() {
let cwd = AbsolutePathBuf::from_absolute_path(std::env::temp_dir()).unwrap();
let executor = CoreShellCommandExecutor {
command: vec!["echo".to_string(), "ok".to_string()],
cwd: cwd.to_path_buf(),
env: HashMap::new(),
network: None,
sandbox: SandboxType::None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::UseDefault,
justification: None,
arg0: None,
sandbox_policy_cwd: cwd.to_path_buf(),
macos_seatbelt_profile_extensions: Some(MacOsSeatbeltProfileExtensions {
macos_preferences: MacOsPreferencesPermission::ReadWrite,
..Default::default()
}),
codex_linux_sandbox_exe: None,
use_linux_sandbox_bwrap: false,
};
let prepared = executor
.prepare_escalated_exec(
&AbsolutePathBuf::from_absolute_path("/bin/echo").unwrap(),
&["echo".to_string(), "ok".to_string()],
&cwd,
HashMap::new(),
EscalationExecution::TurnDefault,
)
.await
.unwrap();
assert_eq!(
prepared.command.first().map(String::as_str),
Some(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
);
assert_eq!(prepared.command.get(1).map(String::as_str), Some("-p"));
assert!(
prepared
.command
.get(2)
.is_some_and(|policy| policy.contains("(allow user-preference-write)")),
"expected seatbelt policy to include macOS extension profile: {:?}",
prepared.command
);
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn prepare_escalated_exec_permissions_preserve_macos_seatbelt_extensions() {
let cwd = AbsolutePathBuf::from_absolute_path(std::env::temp_dir()).unwrap();
let executor = CoreShellCommandExecutor {
command: vec!["echo".to_string(), "ok".to_string()],
cwd: cwd.to_path_buf(),
env: HashMap::new(),
network: None,
sandbox: SandboxType::None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::UseDefault,
justification: None,
arg0: None,
sandbox_policy_cwd: cwd.to_path_buf(),
macos_seatbelt_profile_extensions: None,
codex_linux_sandbox_exe: None,
use_linux_sandbox_bwrap: false,
};
let permissions = Permissions {
approval_policy: Constrained::allow_any(AskForApproval::Never),
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
network: None,
allow_login_shell: true,
shell_environment_policy: ShellEnvironmentPolicy::default(),
windows_sandbox_mode: None,
macos_seatbelt_profile_extensions: Some(MacOsSeatbeltProfileExtensions {
macos_preferences: MacOsPreferencesPermission::ReadWrite,
..Default::default()
}),
};
let prepared = executor
.prepare_escalated_exec(
&AbsolutePathBuf::from_absolute_path("/bin/echo").unwrap(),
&["echo".to_string(), "ok".to_string()],
&cwd,
HashMap::new(),
EscalationExecution::Permissions(EscalationPermissions::Permissions(
EscalatedPermissions {
sandbox_policy: permissions.sandbox_policy.get().clone(),
macos_seatbelt_profile_extensions: permissions
.macos_seatbelt_profile_extensions
.clone(),
},
)),
)
.await
.unwrap();
assert_eq!(
prepared.command.first().map(String::as_str),
Some(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
);
assert_eq!(prepared.command.get(1).map(String::as_str), Some("-p"));
assert!(
prepared
.command
.get(2)
.is_some_and(|policy| policy.contains("(allow user-preference-write)")),
"expected seatbelt policy to include macOS extension profile: {:?}",
prepared.command
);
}
}
#[path = "unix_escalation_tests.rs"]
mod tests;
@@ -0,0 +1,320 @@
use super::CoreShellActionProvider;
#[cfg(target_os = "macos")]
use super::CoreShellCommandExecutor;
use super::ParsedShellCommand;
use super::extract_shell_script;
use super::join_program_and_argv;
use super::map_exec_result;
#[cfg(target_os = "macos")]
use crate::config::Constrained;
#[cfg(target_os = "macos")]
use crate::config::Permissions;
#[cfg(target_os = "macos")]
use crate::config::types::ShellEnvironmentPolicy;
use crate::exec::SandboxType;
#[cfg(target_os = "macos")]
use crate::protocol::AskForApproval;
use crate::protocol::ReadOnlyAccess;
use crate::protocol::SandboxPolicy;
#[cfg(target_os = "macos")]
use crate::sandboxing::SandboxPermissions;
#[cfg(target_os = "macos")]
use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
#[cfg(target_os = "macos")]
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::MacOsPreferencesPermission;
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
use codex_protocol::models::PermissionProfile;
use codex_shell_escalation::EscalationExecution;
use codex_shell_escalation::EscalationPermissions;
use codex_shell_escalation::ExecResult;
use codex_shell_escalation::Permissions as EscalatedPermissions;
#[cfg(target_os = "macos")]
use codex_shell_escalation::ShellCommandExecutor;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
#[cfg(target_os = "macos")]
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;
#[test]
fn extract_shell_script_preserves_login_flag() {
assert_eq!(
extract_shell_script(&["/bin/zsh".into(), "-lc".into(), "echo hi".into()]).unwrap(),
ParsedShellCommand {
script: "echo hi".to_string(),
login: true,
}
);
assert_eq!(
extract_shell_script(&["/bin/zsh".into(), "-c".into(), "echo hi".into()]).unwrap(),
ParsedShellCommand {
script: "echo hi".to_string(),
login: false,
}
);
}
#[test]
fn extract_shell_script_supports_wrapped_command_prefixes() {
assert_eq!(
extract_shell_script(&[
"/usr/bin/env".into(),
"CODEX_EXECVE_WRAPPER=1".into(),
"/bin/zsh".into(),
"-lc".into(),
"echo hello".into()
])
.unwrap(),
ParsedShellCommand {
script: "echo hello".to_string(),
login: true,
}
);
assert_eq!(
extract_shell_script(&[
"sandbox-exec".into(),
"-p".into(),
"sandbox_policy".into(),
"/bin/zsh".into(),
"-c".into(),
"pwd".into(),
])
.unwrap(),
ParsedShellCommand {
script: "pwd".to_string(),
login: false,
}
);
}
#[test]
fn extract_shell_script_rejects_unsupported_shell_invocation() {
let err = extract_shell_script(&[
"sandbox-exec".into(),
"-fc".into(),
"echo not supported".into(),
])
.unwrap_err();
assert!(matches!(err, super::ToolError::Rejected(_)));
assert_eq!(
match err {
super::ToolError::Rejected(reason) => reason,
_ => "".to_string(),
},
"unexpected shell command format for zsh-fork execution"
);
}
#[test]
fn join_program_and_argv_replaces_original_argv_zero() {
assert_eq!(
join_program_and_argv(
&AbsolutePathBuf::from_absolute_path("/tmp/tool").unwrap(),
&["./tool".into(), "--flag".into(), "value".into()],
),
vec!["/tmp/tool", "--flag", "value"]
);
assert_eq!(
join_program_and_argv(
&AbsolutePathBuf::from_absolute_path("/tmp/tool").unwrap(),
&["./tool".into()]
),
vec!["/tmp/tool"]
);
}
#[test]
fn map_exec_result_preserves_stdout_and_stderr() {
let out = map_exec_result(
SandboxType::None,
ExecResult {
exit_code: 0,
stdout: "out".to_string(),
stderr: "err".to_string(),
output: "outerr".to_string(),
duration: Duration::from_millis(1),
timed_out: false,
},
)
.unwrap();
assert_eq!(out.stdout.text, "out");
assert_eq!(out.stderr.text, "err");
assert_eq!(out.aggregated_output.text, "outerr");
}
#[test]
fn shell_request_escalation_execution_is_explicit() {
let requested_permissions = PermissionProfile {
file_system: Some(FileSystemPermissions {
read: None,
write: Some(vec![PathBuf::from("./output")]),
}),
..Default::default()
};
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![AbsolutePathBuf::from_absolute_path("/tmp/original/output").unwrap()],
read_only_access: ReadOnlyAccess::FullAccess,
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
};
let macos_seatbelt_profile_extensions = MacOsSeatbeltProfileExtensions {
macos_preferences: MacOsPreferencesPermission::ReadWrite,
..Default::default()
};
assert_eq!(
CoreShellActionProvider::shell_request_escalation_execution(
crate::sandboxing::SandboxPermissions::UseDefault,
&sandbox_policy,
None,
Some(&macos_seatbelt_profile_extensions),
),
EscalationExecution::TurnDefault,
);
assert_eq!(
CoreShellActionProvider::shell_request_escalation_execution(
crate::sandboxing::SandboxPermissions::RequireEscalated,
&sandbox_policy,
None,
Some(&macos_seatbelt_profile_extensions),
),
EscalationExecution::Unsandboxed,
);
assert_eq!(
CoreShellActionProvider::shell_request_escalation_execution(
crate::sandboxing::SandboxPermissions::WithAdditionalPermissions,
&sandbox_policy,
Some(&requested_permissions),
Some(&macos_seatbelt_profile_extensions),
),
EscalationExecution::Permissions(EscalationPermissions::Permissions(
EscalatedPermissions {
sandbox_policy,
macos_seatbelt_profile_extensions: Some(macos_seatbelt_profile_extensions),
},
)),
);
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn prepare_escalated_exec_turn_default_preserves_macos_seatbelt_extensions() {
let cwd = AbsolutePathBuf::from_absolute_path(std::env::temp_dir()).unwrap();
let executor = CoreShellCommandExecutor {
command: vec!["echo".to_string(), "ok".to_string()],
cwd: cwd.to_path_buf(),
env: HashMap::new(),
network: None,
sandbox: SandboxType::None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::UseDefault,
justification: None,
arg0: None,
sandbox_policy_cwd: cwd.to_path_buf(),
macos_seatbelt_profile_extensions: Some(MacOsSeatbeltProfileExtensions {
macos_preferences: MacOsPreferencesPermission::ReadWrite,
..Default::default()
}),
codex_linux_sandbox_exe: None,
use_linux_sandbox_bwrap: false,
};
let prepared = executor
.prepare_escalated_exec(
&AbsolutePathBuf::from_absolute_path("/bin/echo").unwrap(),
&["echo".to_string(), "ok".to_string()],
&cwd,
HashMap::new(),
EscalationExecution::TurnDefault,
)
.await
.unwrap();
assert_eq!(
prepared.command.first().map(String::as_str),
Some(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
);
assert_eq!(prepared.command.get(1).map(String::as_str), Some("-p"));
assert!(
prepared
.command
.get(2)
.is_some_and(|policy| policy.contains("(allow user-preference-write)")),
"expected seatbelt policy to include macOS extension profile: {:?}",
prepared.command
);
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn prepare_escalated_exec_permissions_preserve_macos_seatbelt_extensions() {
let cwd = AbsolutePathBuf::from_absolute_path(std::env::temp_dir()).unwrap();
let executor = CoreShellCommandExecutor {
command: vec!["echo".to_string(), "ok".to_string()],
cwd: cwd.to_path_buf(),
env: HashMap::new(),
network: None,
sandbox: SandboxType::None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
sandbox_permissions: SandboxPermissions::UseDefault,
justification: None,
arg0: None,
sandbox_policy_cwd: cwd.to_path_buf(),
macos_seatbelt_profile_extensions: None,
codex_linux_sandbox_exe: None,
use_linux_sandbox_bwrap: false,
};
let permissions = Permissions {
approval_policy: Constrained::allow_any(AskForApproval::Never),
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
network: None,
allow_login_shell: true,
shell_environment_policy: ShellEnvironmentPolicy::default(),
windows_sandbox_mode: None,
macos_seatbelt_profile_extensions: Some(MacOsSeatbeltProfileExtensions {
macos_preferences: MacOsPreferencesPermission::ReadWrite,
..Default::default()
}),
};
let prepared = executor
.prepare_escalated_exec(
&AbsolutePathBuf::from_absolute_path("/bin/echo").unwrap(),
&["echo".to_string(), "ok".to_string()],
&cwd,
HashMap::new(),
EscalationExecution::Permissions(EscalationPermissions::Permissions(
EscalatedPermissions {
sandbox_policy: permissions.sandbox_policy.get().clone(),
macos_seatbelt_profile_extensions: permissions
.macos_seatbelt_profile_extensions
.clone(),
},
)),
)
.await
.unwrap();
assert_eq!(
prepared.command.first().map(String::as_str),
Some(MACOS_PATH_TO_SEATBELT_EXECUTABLE)
);
assert_eq!(prepared.command.get(1).map(String::as_str), Some("-p"));
assert!(
prepared
.command
.get(2)
.is_some_and(|policy| policy.contains("(allow user-preference-write)")),
"expected seatbelt policy to include macOS extension profile: {:?}",
prepared.command
);
}
+68
View File
@@ -12,6 +12,7 @@ use futures::SinkExt;
use futures::StreamExt;
use serde_json::Value;
use tokio::net::TcpListener;
use tokio::sync::Notify;
use tokio::sync::oneshot;
use tokio_tungstenite::accept_hdr_async_with_config;
use tokio_tungstenite::tungstenite::Message;
@@ -335,6 +336,7 @@ pub struct WebSocketTestServer {
uri: String,
connections: Arc<Mutex<Vec<Vec<WebSocketRequest>>>>,
handshakes: Arc<Mutex<Vec<WebSocketHandshake>>>,
request_log_updated: Arc<Notify>,
shutdown: oneshot::Sender<()>,
task: tokio::task::JoinHandle<()>,
}
@@ -356,6 +358,26 @@ impl WebSocketTestServer {
connections.first().cloned().unwrap_or_default()
}
pub async fn wait_for_request(
&self,
connection_index: usize,
request_index: usize,
) -> WebSocketRequest {
loop {
if let Some(request) = self
.connections
.lock()
.unwrap()
.get(connection_index)
.and_then(|connection| connection.get(request_index))
.cloned()
{
return request;
}
self.request_log_updated.notified().await;
}
}
pub fn handshakes(&self) -> Vec<WebSocketHandshake> {
self.handshakes.lock().unwrap().clone()
}
@@ -1069,6 +1091,7 @@ pub async fn start_websocket_server(connections: Vec<Vec<Vec<Value>>>) -> WebSoc
pub async fn start_websocket_server_with_headers(
connections: Vec<WebSocketConnectionConfig>,
) -> WebSocketTestServer {
let start = std::time::Instant::now();
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("bind websocket server");
@@ -1076,8 +1099,10 @@ pub async fn start_websocket_server_with_headers(
let uri = format!("ws://{addr}");
let connections_log = Arc::new(Mutex::new(Vec::new()));
let handshakes_log = Arc::new(Mutex::new(Vec::new()));
let request_log_updated = Arc::new(Notify::new());
let requests = Arc::clone(&connections_log);
let handshakes = Arc::clone(&handshakes_log);
let request_log = Arc::clone(&request_log_updated);
let connections = Arc::new(Mutex::new(VecDeque::from(connections)));
let (shutdown_tx, mut shutdown_rx) = oneshot::channel();
@@ -1159,9 +1184,51 @@ pub async fn start_websocket_server_with_headers(
let mut log = requests.lock().unwrap();
if let Some(connection_log) = log.get_mut(connection_index) {
connection_log.push(WebSocketRequest { body });
let request_index = connection_log.len() - 1;
let request = &connection_log[request_index];
let request_body = request.body_json();
eprintln!(
"[ws test server +{}ms] connection={} received request={} type={:?} role={:?} text={:?} data={:?}",
start.elapsed().as_millis(),
connection_index,
request_index,
request_body.get("type").and_then(Value::as_str),
request_body
.get("item")
.and_then(|item| item.get("role"))
.and_then(Value::as_str),
request_body
.get("item")
.and_then(|item| item.get("content"))
.and_then(Value::as_array)
.and_then(|content| content.first())
.and_then(|content| content.get("text"))
.and_then(Value::as_str),
request_body
.get("item")
.and_then(|item| item.get("content"))
.and_then(Value::as_array)
.and_then(|content| content.first())
.and_then(|content| content.get("data"))
.and_then(Value::as_str),
);
}
request_log.notify_waiters();
}
eprintln!(
"[ws test server +{}ms] connection={} sending batch_size={} event_types={:?} audio_data={:?}",
start.elapsed().as_millis(),
connection_index,
request_events.len(),
request_events
.iter()
.map(|event| event.get("type").and_then(Value::as_str))
.collect::<Vec<_>>(),
request_events
.iter()
.find_map(|event| event.get("delta").and_then(Value::as_str)),
);
for event in &request_events {
let Ok(payload) = serde_json::to_string(event) else {
continue;
@@ -1184,6 +1251,7 @@ pub async fn start_websocket_server_with_headers(
uri,
connections: connections_log,
handshakes: handshakes_log,
request_log_updated,
shutdown: shutdown_tx,
task,
}
+1 -2
View File
@@ -11,7 +11,6 @@ use codex_core::ThreadManager;
use codex_core::built_in_model_providers;
use codex_core::config::Config;
use codex_core::features::Feature;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
@@ -300,7 +299,7 @@ impl TestCodex {
sandbox_policy,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
+7 -8
View File
@@ -12,7 +12,6 @@ use std::sync::atomic::AtomicI32;
use std::sync::atomic::Ordering;
use codex_core::features::Feature;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
@@ -312,7 +311,7 @@ async fn apply_patch_cli_move_without_content_change_has_no_turn_diff(
sandbox_policy: SandboxPolicy::DangerFullAccess,
model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -907,7 +906,7 @@ async fn apply_patch_shell_command_heredoc_with_cd_emits_turn_diff() -> Result<(
sandbox_policy: SandboxPolicy::DangerFullAccess,
model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -987,7 +986,7 @@ async fn apply_patch_shell_command_failure_propagates_error_and_skips_diff() ->
sandbox_policy: SandboxPolicy::DangerFullAccess,
model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -1137,7 +1136,7 @@ async fn apply_patch_emits_turn_diff_event_with_unified_diff(
sandbox_policy: SandboxPolicy::DangerFullAccess,
model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -1200,7 +1199,7 @@ async fn apply_patch_turn_diff_for_rename_with_content_change(
sandbox_policy: SandboxPolicy::DangerFullAccess,
model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -1271,7 +1270,7 @@ async fn apply_patch_aggregates_diff_across_multiple_tool_calls() -> Result<()>
sandbox_policy: SandboxPolicy::DangerFullAccess,
model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -1342,7 +1341,7 @@ async fn apply_patch_aggregates_diff_preserves_success_after_failure() -> Result
sandbox_policy: SandboxPolicy::DangerFullAccess,
model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
+1 -2
View File
@@ -13,7 +13,6 @@ use codex_core::sandboxing::SandboxPermissions;
use codex_protocol::approvals::NetworkApprovalProtocol;
use codex_protocol::approvals::NetworkPolicyAmendment;
use codex_protocol::approvals::NetworkPolicyRuleAction;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
@@ -551,7 +550,7 @@ async fn submit_turn(
sandbox_policy,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
+74 -3
View File
@@ -981,9 +981,11 @@ async fn user_turn_collaboration_mode_overrides_model_and_effort() -> anyhow::Re
sandbox_policy: config.permissions.sandbox_policy.get().clone(),
model: session_configured.model.clone(),
effort: Some(ReasoningEffort::Low),
summary: config
.model_reasoning_summary
.unwrap_or(ReasoningSummary::Auto),
summary: Some(
config
.model_reasoning_summary
.unwrap_or(ReasoningSummary::Auto),
),
collaboration_mode: Some(collaboration_mode),
final_output_json_schema: None,
personality: None,
@@ -1049,6 +1051,75 @@ async fn configured_reasoning_summary_is_sent() -> anyhow::Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn user_turn_explicit_reasoning_summary_overrides_model_catalog_default() -> anyhow::Result<()>
{
skip_if_no_network!(Ok(()));
let server = MockServer::start().await;
let resp_mock = mount_sse_once(
&server,
sse(vec![ev_response_created("resp1"), ev_completed("resp1")]),
)
.await;
let mut model_catalog: ModelsResponse =
serde_json::from_str(include_str!("../../models.json")).expect("valid models.json");
let model = model_catalog
.models
.iter_mut()
.find(|model| model.slug == "gpt-5.1")
.expect("gpt-5.1 exists in bundled models.json");
model.supports_reasoning_summaries = true;
model.default_reasoning_summary = ReasoningSummary::Detailed;
let TestCodex {
codex,
config,
session_configured,
..
} = test_codex()
.with_model("gpt-5.1")
.with_config(move |config| {
config.model_catalog = Some(model_catalog);
})
.build(&server)
.await?;
codex
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
cwd: config.cwd.clone(),
approval_policy: config.permissions.approval_policy.value(),
sandbox_policy: config.permissions.sandbox_policy.get().clone(),
model: session_configured.model,
effort: None,
summary: Some(ReasoningSummary::Concise),
collaboration_mode: None,
final_output_json_schema: None,
personality: None,
})
.await
.unwrap();
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
let request_body = resp_mock.single_request().body_json();
pretty_assertions::assert_eq!(
request_body
.get("reasoning")
.and_then(|reasoning| reasoning.get("summary"))
.and_then(|value| value.as_str()),
Some("concise")
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn reasoning_summary_is_omitted_when_disabled() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
@@ -169,10 +169,11 @@ async fn collaboration_instructions_added_on_user_turn() -> Result<()> {
sandbox_policy: test.config.permissions.sandbox_policy.get().clone(),
model: test.session_configured.model.clone(),
effort: None,
summary: test
.config
.model_reasoning_summary
.unwrap_or(codex_protocol::config_types::ReasoningSummary::Auto),
summary: Some(
test.config
.model_reasoning_summary
.unwrap_or(codex_protocol::config_types::ReasoningSummary::Auto),
),
collaboration_mode: Some(collaboration_mode),
final_output_json_schema: None,
personality: None,
@@ -278,10 +279,11 @@ async fn user_turn_overrides_collaboration_instructions_after_override() -> Resu
sandbox_policy: test.config.permissions.sandbox_policy.get().clone(),
model: test.session_configured.model.clone(),
effort: None,
summary: test
.config
.model_reasoning_summary
.unwrap_or(codex_protocol::config_types::ReasoningSummary::Auto),
summary: Some(
test.config
.model_reasoning_summary
.unwrap_or(codex_protocol::config_types::ReasoningSummary::Auto),
),
collaboration_mode: Some(turn_mode),
final_output_json_schema: None,
personality: None,
+7 -8
View File
@@ -5,7 +5,6 @@ use codex_core::built_in_model_providers;
use codex_core::compact::SUMMARIZATION_PROMPT;
use codex_core::compact::SUMMARY_PREFIX;
use codex_core::config::Config;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::items::TurnItem;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelsResponse;
@@ -1659,7 +1658,7 @@ async fn auto_compact_runs_after_resume_when_token_usage_is_over_limit() {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: resumed.session_configured.model.clone(),
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -1748,7 +1747,7 @@ async fn pre_sampling_compact_runs_on_switch_to_smaller_context_model() {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: previous_model.to_string(),
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -1771,7 +1770,7 @@ async fn pre_sampling_compact_runs_on_switch_to_smaller_context_model() {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: next_model.to_string(),
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -1880,7 +1879,7 @@ async fn pre_sampling_compact_runs_after_resume_and_switch_to_smaller_model() {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: previous_model.to_string(),
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -1927,7 +1926,7 @@ async fn pre_sampling_compact_runs_after_resume_and_switch_to_smaller_model() {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: next_model.to_string(),
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -3128,7 +3127,7 @@ async fn snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: previous_model.to_string(),
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -3151,7 +3150,7 @@ async fn snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: next_model.to_string(),
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
+2 -3
View File
@@ -4,7 +4,6 @@ use anyhow::Result;
use codex_core::features::Feature;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::Settings;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
@@ -55,7 +54,7 @@ async fn submit_user_turn(
sandbox_policy,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode,
personality: None,
})
@@ -134,7 +133,7 @@ async fn execpolicy_blocks_shell_invocation() -> Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
+2 -3
View File
@@ -1,5 +1,4 @@
use anyhow::Context;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::AskForApproval;
@@ -126,7 +125,7 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -208,7 +207,7 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()>
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
+5 -5
View File
@@ -377,7 +377,7 @@ async fn plan_mode_emits_plan_item_from_proposed_plan_block() -> anyhow::Result<
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
model: session_configured.model.clone(),
effort: None,
summary: codex_protocol::config_types::ReasoningSummary::Auto,
summary: None,
collaboration_mode: Some(collaboration_mode),
personality: None,
})
@@ -452,7 +452,7 @@ async fn plan_mode_strips_plan_from_agent_messages() -> anyhow::Result<()> {
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
model: session_configured.model.clone(),
effort: None,
summary: codex_protocol::config_types::ReasoningSummary::Auto,
summary: None,
collaboration_mode: Some(collaboration_mode),
personality: None,
})
@@ -559,7 +559,7 @@ async fn plan_mode_streaming_citations_are_stripped_across_added_deltas_and_done
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
model: session_configured.model.clone(),
effort: None,
summary: codex_protocol::config_types::ReasoningSummary::Auto,
summary: None,
collaboration_mode: Some(collaboration_mode),
personality: None,
})
@@ -744,7 +744,7 @@ async fn plan_mode_streaming_proposed_plan_tag_split_across_added_and_delta_is_p
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
model: session_configured.model.clone(),
effort: None,
summary: codex_protocol::config_types::ReasoningSummary::Auto,
summary: None,
collaboration_mode: Some(collaboration_mode),
personality: None,
})
@@ -856,7 +856,7 @@ async fn plan_mode_handles_missing_plan_close_tag() -> anyhow::Result<()> {
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
model: session_configured.model.clone(),
effort: None,
summary: codex_protocol::config_types::ReasoningSummary::Auto,
summary: None,
collaboration_mode: Some(collaboration_mode),
personality: None,
})
+1 -2
View File
@@ -1,6 +1,5 @@
#![cfg(not(target_os = "windows"))]
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
@@ -84,7 +83,7 @@ async fn codex_returns_json_result(model: String) -> anyhow::Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
+1 -2
View File
@@ -7,7 +7,6 @@ use std::time::Duration;
use anyhow::Result;
use codex_core::config::ProjectConfig;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::TrustLevel;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
@@ -65,7 +64,7 @@ async fn submit_skill_turn(test: &TestCodex, skill_path: PathBuf, prompt: &str)
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
+8 -8
View File
@@ -58,7 +58,7 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<(
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -91,7 +91,7 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<(
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: next_model.to_string(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -146,7 +146,7 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -179,7 +179,7 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: next_model.to_string(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -294,7 +294,7 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result<
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: image_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -313,7 +313,7 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result<
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: text_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -471,7 +471,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: large_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -526,7 +526,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: smaller_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -6,7 +6,6 @@ use std::sync::Arc;
use anyhow::Result;
use codex_core::config::types::Personality;
use codex_core::features::Feature;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
@@ -122,7 +121,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> {
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -144,7 +143,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> {
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: Some(Personality::Friendly),
})
@@ -221,7 +220,7 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -243,7 +242,7 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -348,7 +347,7 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: resumed.session_configured.model.clone(),
effort: resumed.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: Some(Personality::Friendly),
})
@@ -99,7 +99,7 @@ async fn renews_cache_ttl_on_matching_models_etag() -> Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: test.session_configured.model.clone(),
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -4,7 +4,6 @@ use std::sync::Arc;
use anyhow::Result;
use codex_core::CodexAuth;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
@@ -106,7 +105,7 @@ async fn refresh_models_on_models_etag_mismatch_and_avoid_duplicate_models_fetch
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
+13 -13
View File
@@ -97,7 +97,7 @@ async fn user_turn_personality_none_does_not_add_update_message() -> anyhow::Res
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -143,7 +143,7 @@ async fn config_personality_some_sets_instructions_template() -> anyhow::Result<
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -196,7 +196,7 @@ async fn config_personality_none_sends_no_personality() -> anyhow::Result<()> {
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -255,7 +255,7 @@ async fn default_personality_is_pragmatic_without_config_toml() -> anyhow::Resul
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -302,7 +302,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()>
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -336,7 +336,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()>
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -398,7 +398,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -432,7 +432,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -504,7 +504,7 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()>
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -538,7 +538,7 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()>
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -647,7 +647,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: remote_slug.to_string(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: Some(Personality::Friendly),
})
@@ -758,7 +758,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: remote_slug.to_string(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -792,7 +792,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: remote_slug.to_string(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
+67 -32
View File
@@ -44,14 +44,32 @@ fn text_user_input_parts(texts: Vec<String>) -> serde_json::Value {
})
}
fn default_env_context_str(cwd: &str, shell: &Shell) -> String {
fn assert_default_env_context(text: &str, cwd: &str, shell: &Shell) {
let shell_name = shell.name();
format!(
r#"<environment_context>
<cwd>{cwd}</cwd>
<shell>{shell_name}</shell>
</environment_context>"#
)
assert!(
text.starts_with(ENVIRONMENT_CONTEXT_OPEN_TAG),
"expected environment context fragment: {text}"
);
assert!(
text.contains(&format!("<cwd>{cwd}</cwd>")),
"expected cwd in environment context: {text}"
);
assert!(
text.contains(&format!("<shell>{shell_name}</shell>")),
"expected shell in environment context: {text}"
);
assert!(
text.contains("<current_date>") && text.contains("</current_date>"),
"expected current_date in environment context: {text}"
);
assert!(
text.contains("<timezone>") && text.contains("</timezone>"),
"expected timezone in environment context: {text}"
);
assert!(
text.ends_with("</environment_context>"),
"expected closing environment_context tag: {text}"
);
}
fn assert_tool_names(body: &serde_json::Value, expected_names: &[&str]) {
@@ -318,10 +336,13 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests
let shell = default_user_shell();
let cwd_str = config.cwd.to_string_lossy();
let expected_env_text = default_env_context_str(&cwd_str, &shell);
let env_text = input1[1]["content"][1]["text"]
.as_str()
.expect("environment context text");
assert_default_env_context(env_text, &cwd_str, &shell);
assert_eq!(
input1[1]["content"][1]["text"].as_str(),
Some(expected_env_text.as_str()),
input1[1]["content"][1]["type"].as_str(),
Some("input_text"),
"expected environment context bundled after UI message in cached contextual message"
);
assert_eq!(input1[2], text_user_input("hello 1".to_string()));
@@ -523,6 +544,18 @@ async fn override_before_first_turn_emits_environment_context() -> anyhow::Resul
!env_texts.is_empty(),
"expected environment context to be emitted: {env_texts:?}"
);
assert!(
env_texts
.iter()
.any(|text| text.contains("<current_date>") && text.contains("</current_date>")),
"expected current_date in environment context: {env_texts:?}"
);
assert!(
env_texts
.iter()
.any(|text| text.contains("<timezone>") && text.contains("</timezone>")),
"expected timezone in environment context: {env_texts:?}"
);
let env_count = input
.iter()
@@ -646,7 +679,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
sandbox_policy: new_policy.clone(),
model: "o3".to_string(),
effort: Some(ReasoningEffort::High),
summary: ReasoningSummary::Detailed,
summary: Some(ReasoningSummary::Detailed),
collaboration_mode: None,
final_output_json_schema: None,
personality: None,
@@ -672,21 +705,6 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
"role": "user",
"content": [ { "type": "input_text", "text": "hello 2" } ]
});
let shell = default_user_shell();
let expected_env_text_2 = format!(
r#"<environment_context>
<cwd>{}</cwd>
<shell>{}</shell>
</environment_context>"#,
new_cwd.path().display(),
shell.name()
);
let expected_env_msg_2 = serde_json::json!({
"type": "message",
"role": "user",
"content": [ { "type": "input_text", "text": expected_env_text_2 } ]
});
let expected_permissions_msg = body1["input"][0].clone();
let body1_input = body1["input"].as_array().expect("input array");
let expected_settings_update_msg = body2["input"][body1_input.len()].clone();
@@ -704,6 +722,14 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
}),
"expected model switch section after model override: {expected_settings_update_msg:?}"
);
let expected_env_msg_2 = body2["input"][body1_input.len() + 1].clone();
assert_eq!(expected_env_msg_2["role"].as_str(), Some("user"));
let env_text = expected_env_msg_2["content"][0]["text"]
.as_str()
.expect("environment context text");
let shell = default_user_shell();
let expected_cwd = new_cwd.path().display().to_string();
assert_default_env_context(env_text, &expected_cwd, &shell);
let mut expected_body2 = body1_input.to_vec();
expected_body2.push(expected_settings_update_msg);
expected_body2.push(expected_env_msg_2);
@@ -761,7 +787,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
sandbox_policy: default_sandbox_policy.clone(),
model: default_model.clone(),
effort: default_effort,
summary: default_summary.unwrap_or(ReasoningSummary::Auto),
summary: Some(default_summary.unwrap_or(ReasoningSummary::Auto)),
collaboration_mode: None,
final_output_json_schema: None,
personality: None,
@@ -780,7 +806,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
sandbox_policy: default_sandbox_policy.clone(),
model: default_model.clone(),
effort: default_effort,
summary: default_summary.unwrap_or(ReasoningSummary::Auto),
summary: Some(default_summary.unwrap_or(ReasoningSummary::Auto)),
collaboration_mode: None,
final_output_json_schema: None,
personality: None,
@@ -798,13 +824,18 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
let shell = default_user_shell();
let default_cwd_lossy = default_cwd.to_string_lossy();
let expected_env_text_1 = expected_ui_msg["content"][1]["text"]
.as_str()
.expect("cached environment context text")
.to_string();
assert_default_env_context(&expected_env_text_1, &default_cwd_lossy, &shell);
let expected_contextual_user_msg_1 = text_user_input_parts(vec![
expected_ui_msg["content"][0]["text"]
.as_str()
.expect("cached user instructions text")
.to_string(),
default_env_context_str(&default_cwd_lossy, &shell),
expected_env_text_1,
]);
let expected_user_message_1 = text_user_input("hello 1".to_string());
@@ -875,7 +906,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
sandbox_policy: default_sandbox_policy.clone(),
model: default_model,
effort: default_effort,
summary: default_summary.unwrap_or(ReasoningSummary::Auto),
summary: Some(default_summary.unwrap_or(ReasoningSummary::Auto)),
collaboration_mode: None,
final_output_json_schema: None,
personality: None,
@@ -894,7 +925,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: "o3".to_string(),
effort: Some(ReasoningEffort::High),
summary: ReasoningSummary::Detailed,
summary: Some(ReasoningSummary::Detailed),
collaboration_mode: None,
final_output_json_schema: None,
personality: None,
@@ -911,7 +942,11 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
let expected_ui_msg = body1["input"][1].clone();
let shell = default_user_shell();
let expected_env_text_1 = default_env_context_str(&default_cwd.to_string_lossy(), &shell);
let expected_env_text_1 = expected_ui_msg["content"][1]["text"]
.as_str()
.expect("cached environment context text")
.to_string();
assert_default_env_context(&expected_env_text_1, &default_cwd.to_string_lossy(), &shell);
let expected_contextual_user_msg_1 = text_user_input_parts(vec![
expected_ui_msg["content"][0]["text"]
.as_str()
@@ -715,6 +715,7 @@ async fn inbound_realtime_text_ignores_user_role_and_still_forwards_audio() -> R
async fn delegated_turn_user_role_echo_does_not_redelegate_and_still_forwards_audio() -> Result<()>
{
skip_if_no_network!(Ok(()));
let start = std::time::Instant::now();
let (gate_completed_tx, gate_completed_rx) = oneshot::channel();
let first_chunks = vec![
@@ -806,18 +807,45 @@ async fn delegated_turn_user_role_echo_does_not_redelegate_and_still_forwards_au
_ => None,
})
.await;
eprintln!(
"[realtime test +{}ms] saw trigger text={:?}",
start.elapsed().as_millis(),
"delegate now"
);
let audio_out = tokio::time::timeout(
Duration::from_millis(500),
wait_for_event_match(&test.codex, |msg| match msg {
EventMsg::RealtimeConversationRealtime(RealtimeConversationRealtimeEvent {
payload: RealtimeEvent::AudioOut(frame),
}) => Some(frame.clone()),
_ => None,
}),
)
.await
.expect("timed out waiting for realtime audio after echoed user-role message");
let mirrored_request = realtime_server.wait_for_request(0, 1).await;
let mirrored_request_body = mirrored_request.body_json();
eprintln!(
"[realtime test +{}ms] saw mirrored request type={:?} role={:?} text={:?} data={:?}",
start.elapsed().as_millis(),
mirrored_request_body["type"].as_str(),
mirrored_request_body["item"]["role"].as_str(),
mirrored_request_body["item"]["content"][0]["text"].as_str(),
mirrored_request_body["item"]["content"][0]["data"].as_str(),
);
assert_eq!(
mirrored_request_body["type"].as_str(),
Some("conversation.item.create")
);
assert_eq!(
mirrored_request_body["item"]["content"][0]["text"].as_str(),
Some("assistant says hi")
);
let audio_out = wait_for_event_match(&test.codex, |msg| match msg {
EventMsg::RealtimeConversationRealtime(RealtimeConversationRealtimeEvent {
payload: RealtimeEvent::AudioOut(frame),
}) => Some(frame.clone()),
_ => None,
})
.await;
eprintln!(
"[realtime test +{}ms] saw audio out data={} sample_rate={} num_channels={}",
start.elapsed().as_millis(),
audio_out.data,
audio_out.sample_rate,
audio_out.num_channels
);
assert_eq!(audio_out.data, "AQID");
let completion = completions
@@ -828,6 +856,10 @@ async fn delegated_turn_user_role_echo_does_not_redelegate_and_still_forwards_au
completion
.await
.expect("delegated turn request did not complete");
eprintln!(
"[realtime test +{}ms] delegated completion resolved",
start.elapsed().as_millis()
);
wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_))
})
+14 -8
View File
@@ -139,6 +139,7 @@ async fn remote_models_long_model_slug_is_sent_with_high_reasoning() -> Result<(
},
];
remote_model.supports_reasoning_summaries = true;
remote_model.default_reasoning_summary = ReasoningSummary::Detailed;
mount_models_once(
&server,
ModelsResponse {
@@ -175,9 +176,7 @@ async fn remote_models_long_model_slug_is_sent_with_high_reasoning() -> Result<(
sandbox_policy: config.permissions.sandbox_policy.get().clone(),
model: requested_model.to_string(),
effort: None,
summary: config
.model_reasoning_summary
.unwrap_or(ReasoningSummary::Auto),
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -191,8 +190,13 @@ async fn remote_models_long_model_slug_is_sent_with_high_reasoning() -> Result<(
.get("reasoning")
.and_then(|reasoning| reasoning.get("effort"))
.and_then(|value| value.as_str());
let reasoning_summary = body
.get("reasoning")
.and_then(|reasoning| reasoning.get("summary"))
.and_then(|value| value.as_str());
assert_eq!(body["model"].as_str(), Some(requested_model));
assert_eq!(reasoning_effort, Some("high"));
assert_eq!(reasoning_summary, Some("detailed"));
Ok(())
}
@@ -229,9 +233,11 @@ async fn namespaced_model_slug_uses_catalog_metadata_without_fallback_warning()
sandbox_policy: config.permissions.sandbox_policy.get().clone(),
model: requested_model.to_string(),
effort: None,
summary: config
.model_reasoning_summary
.unwrap_or(ReasoningSummary::Auto),
summary: Some(
config
.model_reasoning_summary
.unwrap_or(ReasoningSummary::Auto),
),
collaboration_mode: None,
personality: None,
})
@@ -384,7 +390,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: REMOTE_MODEL_SLUG.to_string(),
effort: None,
summary: ReasoningSummary::Auto,
summary: Some(ReasoningSummary::Auto),
collaboration_mode: None,
personality: None,
})
@@ -596,7 +602,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: model.to_string(),
effort: None,
summary: ReasoningSummary::Auto,
summary: Some(ReasoningSummary::Auto),
collaboration_mode: None,
personality: None,
})
@@ -4,7 +4,6 @@ use anyhow::Result;
use codex_core::config::Constrained;
use codex_core::features::Feature;
use codex_core::sandboxing::SandboxPermissions;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
@@ -111,7 +110,7 @@ async fn submit_turn(
sandbox_policy,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -5,7 +5,6 @@ use std::collections::HashMap;
use codex_core::features::Feature;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::Settings;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
@@ -138,7 +137,7 @@ async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Resul
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: Some(CollaborationMode {
mode,
settings: Settings {
@@ -254,7 +253,7 @@ where
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: Some(collaboration_mode),
personality: None,
})
@@ -28,6 +28,8 @@ fn resume_history(
let turn_ctx = TurnContextItem {
turn_id: Some(turn_id.clone()),
cwd: config.cwd.clone(),
current_date: None,
timezone: None,
approval_policy: config.permissions.approval_policy.value(),
sandbox_policy: config.permissions.sandbox_policy.get().clone(),
network: None,
+6 -6
View File
@@ -128,7 +128,7 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> {
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -295,7 +295,7 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> {
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -493,7 +493,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: text_only_model_slug.to_string(),
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -605,7 +605,7 @@ async fn stdio_server_propagates_whitelisted_env_vars() -> anyhow::Result<()> {
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -764,7 +764,7 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> {
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -983,7 +983,7 @@ async fn streamable_http_with_oauth_round_trip_impl() -> anyhow::Result<()> {
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -1,5 +1,4 @@
use anyhow::Result;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::AskForApproval;
@@ -49,7 +48,7 @@ async fn openai_model_header_mismatch_emits_warning_event_and_warning_item() ->
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: REQUESTED_MODEL.to_string(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -146,7 +145,7 @@ async fn response_model_field_mismatch_emits_warning_when_header_matches_request
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: REQUESTED_MODEL.to_string(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -230,7 +229,7 @@ async fn openai_model_header_mismatch_only_emits_one_warning_per_turn() -> Resul
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: REQUESTED_MODEL.to_string(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -278,7 +277,7 @@ async fn openai_model_header_casing_only_mismatch_does_not_warn() -> Result<()>
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: REQUESTED_MODEL.to_string(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
+4 -5
View File
@@ -1,6 +1,5 @@
use anyhow::Result;
use codex_core::features::Feature;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ExecCommandBeginEvent;
@@ -162,7 +161,7 @@ async fn run_snapshot_command_with_options(
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -248,7 +247,7 @@ async fn run_shell_command_snapshot_with_options(
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -317,7 +316,7 @@ async fn run_tool_turn_on_harness(
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -535,7 +534,7 @@ async fn shell_command_snapshot_still_intercepts_apply_patch() -> Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
+1 -2
View File
@@ -4,7 +4,6 @@
use anyhow::Result;
use codex_core::config::Config;
use codex_core::features::Feature;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
@@ -59,7 +58,7 @@ async fn submit_turn_with_policies(
sandbox_policy,
model: test.session_configured.model.clone(),
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
+1 -1
View File
@@ -77,7 +77,7 @@ async fn user_turn_includes_skill_instructions() -> Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: codex_protocol::config_types::ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
+5 -6
View File
@@ -4,7 +4,6 @@ use std::fs;
use assert_matches::assert_matches;
use codex_core::features::Feature;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::plan_tool::StepStatus;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
@@ -89,7 +88,7 @@ async fn shell_tool_executes_command_and_streams_output() -> anyhow::Result<()>
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -158,7 +157,7 @@ async fn update_plan_tool_emits_plan_update_event() -> anyhow::Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -237,7 +236,7 @@ async fn update_plan_tool_rejects_malformed_payload() -> anyhow::Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -328,7 +327,7 @@ async fn apply_patch_tool_executes_and_emits_patch_events() -> anyhow::Result<()
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -427,7 +426,7 @@ async fn apply_patch_reports_parse_diagnostics() -> anyhow::Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -5,7 +5,6 @@ use std::fs;
use std::time::Duration;
use std::time::Instant;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
@@ -46,7 +45,7 @@ async fn run_turn(test: &TestCodex, prompt: &str) -> anyhow::Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -361,7 +360,7 @@ async fn shell_tools_start_before_response_completed_when_stream_delayed() -> an
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
+1 -2
View File
@@ -5,7 +5,6 @@ use anyhow::Context;
use anyhow::Result;
use codex_core::config::types::McpServerConfig;
use codex_core::config::types::McpServerTransportConfig;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
@@ -488,7 +487,7 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> {
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
+26 -27
View File
@@ -6,7 +6,6 @@ use std::sync::OnceLock;
use anyhow::Context;
use anyhow::Result;
use codex_core::features::Feature;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ExecCommandSource;
@@ -209,7 +208,7 @@ async fn unified_exec_intercepts_apply_patch_exec_command() -> Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -338,7 +337,7 @@ async fn unified_exec_emits_exec_command_begin_event() -> Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -416,7 +415,7 @@ async fn unified_exec_resolves_relative_workdir() -> Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -497,7 +496,7 @@ async fn unified_exec_respects_workdir_override() -> Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -590,7 +589,7 @@ async fn unified_exec_emits_exec_command_end_event() -> Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -665,7 +664,7 @@ async fn unified_exec_emits_output_delta_for_exec_command() -> Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -741,7 +740,7 @@ async fn unified_exec_full_lifecycle_with_background_end_event() -> Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -871,7 +870,7 @@ async fn unified_exec_emits_terminal_interaction_for_write_stdin() -> Result<()>
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -1008,7 +1007,7 @@ async fn unified_exec_terminal_interaction_captures_delayed_output() -> Result<(
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -1168,7 +1167,7 @@ async fn unified_exec_emits_one_begin_and_one_end_event() -> Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -1266,7 +1265,7 @@ async fn exec_command_reports_chunk_and_exit_metadata() -> Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -1384,7 +1383,7 @@ async fn unified_exec_defaults_to_pipe() -> Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -1474,7 +1473,7 @@ async fn unified_exec_can_enable_tty() -> Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -1555,7 +1554,7 @@ async fn unified_exec_respects_early_exit_notifications() -> Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -1686,7 +1685,7 @@ async fn write_stdin_returns_exit_metadata_and_clears_session() -> Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -1854,7 +1853,7 @@ async fn unified_exec_emits_end_event_when_session_dies_via_stdin() -> Result<()
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -1931,7 +1930,7 @@ async fn unified_exec_keeps_long_running_session_after_turn_end() -> Result<()>
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -2019,7 +2018,7 @@ async fn unified_exec_interrupt_terminates_long_running_session() -> Result<()>
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -2116,7 +2115,7 @@ async fn unified_exec_reuses_session_via_stdin() -> Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -2251,7 +2250,7 @@ PY
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -2365,7 +2364,7 @@ async fn unified_exec_timeout_and_followup_poll() -> Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -2461,7 +2460,7 @@ PY
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -2543,7 +2542,7 @@ async fn unified_exec_runs_under_sandbox() -> Result<()> {
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -2647,7 +2646,7 @@ async fn unified_exec_python_prompt_under_seatbelt() -> Result<()> {
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -2742,7 +2741,7 @@ async fn unified_exec_runs_on_all_platforms() -> Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -2877,7 +2876,7 @@ async fn unified_exec_prunes_exited_sessions_first() -> Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
+1 -2
View File
@@ -1,6 +1,5 @@
use anyhow::Context;
use codex_core::features::Feature;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ExecCommandEndEvent;
@@ -178,7 +177,7 @@ async fn user_shell_command_does_not_replace_active_turn() -> anyhow::Result<()>
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: fixture.session_configured.model.clone(),
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
+8 -8
View File
@@ -112,7 +112,7 @@ async fn user_turn_with_local_image_attaches_image() -> anyhow::Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -214,7 +214,7 @@ async fn view_image_tool_attaches_local_image() -> anyhow::Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -344,7 +344,7 @@ console.log(out.output?.body?.text ?? "");
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -447,7 +447,7 @@ async fn view_image_tool_errors_when_path_is_directory() -> anyhow::Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -522,7 +522,7 @@ async fn view_image_tool_placeholder_for_non_image_files() -> anyhow::Result<()>
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -614,7 +614,7 @@ async fn view_image_tool_errors_when_file_missing() -> anyhow::Result<()> {
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -737,7 +737,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: model_slug.to_string(),
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -813,7 +813,7 @@ async fn replaces_invalid_local_image_after_bad_request() -> anyhow::Result<()>
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
@@ -1,6 +1,5 @@
use anyhow::Result;
use codex_core::features::Feature;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
@@ -161,7 +160,7 @@ async fn websocket_fallback_hides_first_websocket_retry_stream_error() -> Result
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_configured.model.clone(),
effort: None,
summary: ReasoningSummary::Auto,
summary: None,
collaboration_mode: None,
personality: None,
})
+1 -3
View File
@@ -377,7 +377,6 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
let default_approval_policy = config.permissions.approval_policy.value();
let default_sandbox_policy = config.permissions.sandbox_policy.get();
let default_effort = config.model_reasoning_effort;
let default_summary = config.model_reasoning_summary;
// When --yolo (dangerously_bypass_approvals_and_sandbox) is set, also skip the git repo check
// since the user is explicitly running in an externally sandboxed environment.
@@ -560,8 +559,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
sandbox_policy: default_sandbox_policy.clone(),
model: default_model,
effort: default_effort,
summary: default_summary
.unwrap_or(codex_protocol::config_types::ReasoningSummary::Auto),
summary: None,
final_output_json_schema: output_schema,
collaboration_mode: None,
personality: None,
+2
View File
@@ -104,6 +104,7 @@ mod tests {
HookPayload {
session_id: ThreadId::new(),
cwd: PathBuf::from(CWD),
client: None,
triggered_at: Utc
.with_ymd_and_hms(2025, 1, 1, 0, 0, 0)
.single()
@@ -172,6 +173,7 @@ mod tests {
HookPayload {
session_id: ThreadId::new(),
cwd: PathBuf::from(CWD),
client: None,
triggered_at: Utc
.with_ymd_and_hms(2025, 1, 1, 0, 0, 0)
.single()
+4
View File
@@ -65,6 +65,8 @@ impl Hook {
pub struct HookPayload {
pub session_id: ThreadId,
pub cwd: PathBuf,
#[serde(skip_serializing_if = "Option::is_none")]
pub client: Option<String>,
#[serde(serialize_with = "serialize_triggered_at")]
pub triggered_at: DateTime<Utc>,
pub hook_event: HookEvent,
@@ -181,6 +183,7 @@ mod tests {
let payload = HookPayload {
session_id,
cwd: PathBuf::from("tmp"),
client: None,
triggered_at: Utc
.with_ymd_and_hms(2025, 1, 1, 0, 0, 0)
.single()
@@ -218,6 +221,7 @@ mod tests {
let payload = HookPayload {
session_id,
cwd: PathBuf::from("tmp"),
client: None,
triggered_at: Utc
.with_ymd_and_hms(2025, 1, 1, 0, 0, 0)
.single()
+27 -15
View File
@@ -1,4 +1,3 @@
use std::path::Path;
use std::process::Stdio;
use std::sync::Arc;
@@ -19,6 +18,8 @@ enum UserNotification {
thread_id: String,
turn_id: String,
cwd: String,
#[serde(skip_serializing_if = "Option::is_none")]
client: Option<String>,
/// Messages that the user sent to the agent to initiate the turn.
input_messages: Vec<String>,
@@ -28,13 +29,14 @@ enum UserNotification {
},
}
pub fn legacy_notify_json(hook_event: &HookEvent, cwd: &Path) -> Result<String, serde_json::Error> {
match hook_event {
pub fn legacy_notify_json(payload: &HookPayload) -> Result<String, serde_json::Error> {
match &payload.hook_event {
HookEvent::AfterAgent { event } => {
serde_json::to_string(&UserNotification::AgentTurnComplete {
thread_id: event.thread_id.to_string(),
turn_id: event.turn_id.clone(),
cwd: cwd.display().to_string(),
cwd: payload.cwd.display().to_string(),
client: payload.client.clone(),
input_messages: event.input_messages.clone(),
last_assistant_message: event.last_assistant_message.clone(),
})
@@ -56,7 +58,7 @@ pub fn notify_hook(argv: Vec<String>) -> Hook {
Some(command) => command,
None => return HookResult::Success,
};
if let Ok(notify_payload) = legacy_notify_json(&payload.hook_event, &payload.cwd) {
if let Ok(notify_payload) = legacy_notify_json(payload) {
command.arg(notify_payload);
}
@@ -91,6 +93,7 @@ mod tests {
"thread-id": "b5f6c1c2-1111-2222-3333-444455556666",
"turn-id": "12345",
"cwd": "/Users/example/project",
"client": "codex-tui",
"input-messages": ["Rename `foo` to `bar` and update the callsites."],
"last-assistant-message": "Rename complete and verified `cargo build` succeeds.",
})
@@ -102,6 +105,7 @@ mod tests {
thread_id: "b5f6c1c2-1111-2222-3333-444455556666".to_string(),
turn_id: "12345".to_string(),
cwd: "/Users/example/project".to_string(),
client: Some("codex-tui".to_string()),
input_messages: vec!["Rename `foo` to `bar` and update the callsites.".to_string()],
last_assistant_message: Some(
"Rename complete and verified `cargo build` succeeds.".to_string(),
@@ -115,19 +119,27 @@ mod tests {
#[test]
fn legacy_notify_json_matches_historical_wire_shape() -> Result<()> {
let hook_event = HookEvent::AfterAgent {
event: crate::HookEventAfterAgent {
thread_id: ThreadId::from_string("b5f6c1c2-1111-2222-3333-444455556666")
.expect("valid thread id"),
turn_id: "12345".to_string(),
input_messages: vec!["Rename `foo` to `bar` and update the callsites.".to_string()],
last_assistant_message: Some(
"Rename complete and verified `cargo build` succeeds.".to_string(),
),
let payload = HookPayload {
session_id: ThreadId::new(),
cwd: std::path::Path::new("/Users/example/project").to_path_buf(),
client: Some("codex-tui".to_string()),
triggered_at: chrono::Utc::now(),
hook_event: HookEvent::AfterAgent {
event: crate::HookEventAfterAgent {
thread_id: ThreadId::from_string("b5f6c1c2-1111-2222-3333-444455556666")
.expect("valid thread id"),
turn_id: "12345".to_string(),
input_messages: vec![
"Rename `foo` to `bar` and update the callsites.".to_string(),
],
last_assistant_message: Some(
"Rename complete and verified `cargo build` succeeds.".to_string(),
),
},
},
};
let serialized = legacy_notify_json(&hook_event, Path::new("/Users/example/project"))?;
let serialized = legacy_notify_json(&payload)?;
let actual: Value = serde_json::from_str(&serialized)?;
assert_eq!(actual, expected_notification_json());
+11 -1
View File
@@ -185,7 +185,11 @@ pub enum Op {
effort: Option<ReasoningEffortConfig>,
/// Will only be honored if the model is configured to use reasoning.
summary: ReasoningSummaryConfig,
///
/// When omitted, the session keeps the current setting (which allows core to
/// fall back to the selected model's default on new sessions).
#[serde(default, skip_serializing_if = "Option::is_none")]
summary: Option<ReasoningSummaryConfig>,
// The JSON schema to use for the final assistant message
final_output_json_schema: Option<Value>,
@@ -2129,6 +2133,10 @@ pub struct TurnContextItem {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub turn_id: Option<String>,
pub cwd: PathBuf,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_date: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timezone: Option<String>,
pub approval_policy: AskForApproval,
pub sandbox_policy: SandboxPolicy,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -3357,6 +3365,8 @@ mod tests {
let item = TurnContextItem {
turn_id: None,
cwd: PathBuf::from("/tmp"),
current_date: None,
timezone: None,
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::DangerFullAccess,
network: Some(TurnContextNetworkItem {
+4
View File
@@ -252,6 +252,8 @@ mod tests {
&RolloutItem::TurnContext(TurnContextItem {
turn_id: Some("turn-1".to_string()),
cwd: PathBuf::from("/parent/workspace"),
current_date: None,
timezone: None,
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::DangerFullAccess,
network: None,
@@ -286,6 +288,8 @@ mod tests {
&RolloutItem::TurnContext(TurnContextItem {
turn_id: Some("turn-1".to_string()),
cwd: PathBuf::from("/fallback/workspace"),
current_date: None,
timezone: None,
approval_policy: AskForApproval::OnRequest,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
network: None,
+122
View File
@@ -0,0 +1,122 @@
use codex_core::config::Config;
use cpal::traits::DeviceTrait;
use cpal::traits::HostTrait;
use tracing::warn;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum AudioDeviceKind {
Input,
Output,
}
impl AudioDeviceKind {
fn noun(self) -> &'static str {
match self {
Self::Input => "input",
Self::Output => "output",
}
}
fn configured_name(self, config: &Config) -> Option<&str> {
match self {
Self::Input => config.realtime_audio.microphone.as_deref(),
Self::Output => config.realtime_audio.speaker.as_deref(),
}
}
}
pub(crate) fn select_configured_input_device_and_config(
config: &Config,
) -> Result<(cpal::Device, cpal::SupportedStreamConfig), String> {
select_device_and_config(AudioDeviceKind::Input, config)
}
pub(crate) fn select_configured_output_device_and_config(
config: &Config,
) -> Result<(cpal::Device, cpal::SupportedStreamConfig), String> {
select_device_and_config(AudioDeviceKind::Output, config)
}
fn select_device_and_config(
kind: AudioDeviceKind,
config: &Config,
) -> Result<(cpal::Device, cpal::SupportedStreamConfig), String> {
let host = cpal::default_host();
let configured_name = kind.configured_name(config);
let selected = configured_name
.and_then(|name| find_device_by_name(&host, kind, name))
.or_else(|| {
let default_device = default_device(&host, kind);
if let Some(name) = configured_name
&& default_device.is_some()
{
warn!(
"configured {} audio device `{name}` was unavailable; falling back to system default",
kind.noun()
);
}
default_device
})
.ok_or_else(|| missing_device_error(kind, configured_name))?;
let stream_config = default_config(&selected, kind)?;
Ok((selected, stream_config))
}
fn find_device_by_name(
host: &cpal::Host,
kind: AudioDeviceKind,
name: &str,
) -> Option<cpal::Device> {
let devices = devices(host, kind).ok()?;
devices
.into_iter()
.find(|device| device.name().ok().as_deref() == Some(name))
}
fn devices(host: &cpal::Host, kind: AudioDeviceKind) -> Result<Vec<cpal::Device>, String> {
match kind {
AudioDeviceKind::Input => host
.input_devices()
.map(|devices| devices.collect())
.map_err(|err| format!("failed to enumerate input audio devices: {err}")),
AudioDeviceKind::Output => host
.output_devices()
.map(|devices| devices.collect())
.map_err(|err| format!("failed to enumerate output audio devices: {err}")),
}
}
fn default_device(host: &cpal::Host, kind: AudioDeviceKind) -> Option<cpal::Device> {
match kind {
AudioDeviceKind::Input => host.default_input_device(),
AudioDeviceKind::Output => host.default_output_device(),
}
}
fn default_config(
device: &cpal::Device,
kind: AudioDeviceKind,
) -> Result<cpal::SupportedStreamConfig, String> {
match kind {
AudioDeviceKind::Input => device
.default_input_config()
.map_err(|err| format!("failed to get default input config: {err}")),
AudioDeviceKind::Output => device
.default_output_config()
.map_err(|err| format!("failed to get default output config: {err}")),
}
}
fn missing_device_error(kind: AudioDeviceKind, configured_name: Option<&str>) -> String {
match (kind, configured_name) {
(AudioDeviceKind::Input, Some(name)) => format!(
"configured input audio device `{name}` was unavailable and no default input audio device was found"
),
(AudioDeviceKind::Output, Some(name)) => format!(
"configured output audio device `{name}` was unavailable and no default output audio device was found"
),
(AudioDeviceKind::Input, None) => "no input audio device available".to_string(),
(AudioDeviceKind::Output, None) => "no output audio device available".to_string(),
}
}
+1 -4
View File
@@ -4139,10 +4139,7 @@ impl ChatWidget {
sandbox_policy: self.config.permissions.sandbox_policy.get().clone(),
model: effective_mode.model().to_string(),
effort: effective_mode.reasoning_effort(),
summary: self
.config
.model_reasoning_summary
.unwrap_or(codex_protocol::config_types::ReasoningSummary::Auto),
summary: None,
final_output_json_schema: None,
collaboration_mode,
personality,
+15
View File
@@ -13,6 +13,17 @@ use tokio::sync::mpsc::unbounded_channel;
use crate::app_event::AppEvent;
use crate::app_event_sender::AppEventSender;
const TUI_NOTIFY_CLIENT: &str = "codex-tui";
async fn initialize_notify_client(thread: &CodexThread) {
if let Err(err) = thread
.set_notify_client(Some(TUI_NOTIFY_CLIENT.to_string()))
.await
{
tracing::error!("failed to set notify client: {err}");
}
}
/// Spawn the agent bootstrapper and op forwarding loop, returning the
/// `UnboundedSender<Op>` used by the UI to submit operations.
pub(crate) fn spawn_agent(
@@ -42,6 +53,7 @@ pub(crate) fn spawn_agent(
return;
}
};
initialize_notify_client(thread.as_ref()).await;
// Forward the captured `SessionConfigured` event so it can be rendered in the UI.
let ev = codex_protocol::protocol::Event {
@@ -87,6 +99,8 @@ pub(crate) fn spawn_agent_from_existing(
let app_event_tx_clone = app_event_tx;
tokio::spawn(async move {
initialize_notify_client(thread.as_ref()).await;
// Forward the captured `SessionConfigured` event so it can be rendered in the UI.
let ev = codex_protocol::protocol::Event {
id: "".to_string(),
@@ -123,6 +137,7 @@ pub(crate) fn spawn_op_forwarder(thread: std::sync::Arc<CodexThread>) -> Unbound
let (codex_op_tx, mut codex_op_rx) = unbounded_channel::<Op>();
tokio::spawn(async move {
initialize_notify_client(thread.as_ref()).await;
while let Some(op) = codex_op_rx.recv().await {
if let Err(e) = thread.submit(op).await {
tracing::error!("failed to submit op: {e}");
+6 -3
View File
@@ -207,7 +207,7 @@ impl ChatWidget {
{
if self.realtime_conversation.audio_player.is_none() {
self.realtime_conversation.audio_player =
crate::voice::RealtimeAudioPlayer::start().ok();
crate::voice::RealtimeAudioPlayer::start(&self.config).ok();
}
if let Some(player) = &self.realtime_conversation.audio_player
&& let Err(err) = player.enqueue_frame(frame)
@@ -231,7 +231,10 @@ impl ChatWidget {
self.realtime_conversation.meter_placeholder_id = Some(placeholder_id.clone());
self.request_redraw();
let capture = match crate::voice::VoiceCapture::start_realtime(self.app_event_tx.clone()) {
let capture = match crate::voice::VoiceCapture::start_realtime(
&self.config,
self.app_event_tx.clone(),
) {
Ok(capture) => capture,
Err(err) => {
self.remove_transcription_placeholder(&placeholder_id);
@@ -250,7 +253,7 @@ impl ChatWidget {
self.realtime_conversation.capture = Some(capture);
if self.realtime_conversation.audio_player.is_none() {
self.realtime_conversation.audio_player =
crate::voice::RealtimeAudioPlayer::start().ok();
crate::voice::RealtimeAudioPlayer::start(&self.config).ok();
}
std::thread::spawn(move || {
+3 -2
View File
@@ -751,8 +751,8 @@ async fn enter_with_only_remote_images_submits_user_turn() {
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
let items = match next_submit_op(&mut op_rx) {
Op::UserTurn { items, .. } => items,
let (items, summary) = match next_submit_op(&mut op_rx) {
Op::UserTurn { items, summary, .. } => (items, summary),
other => panic!("expected Op::UserTurn, got {other:?}"),
};
assert_eq!(
@@ -761,6 +761,7 @@ async fn enter_with_only_remote_images_submits_user_turn() {
image_url: remote_url.clone(),
}]
);
assert_eq!(summary, None);
assert!(chat.remote_image_urls().is_empty());
let mut user_cell = None;
+7 -2
View File
@@ -63,6 +63,8 @@ mod app_backtrack;
mod app_event;
mod app_event_sender;
mod ascii_animation;
#[cfg(all(not(target_os = "linux"), feature = "voice-input"))]
mod audio_device;
mod bottom_pane;
mod chatwidget;
mod cli;
@@ -123,6 +125,7 @@ mod voice;
mod voice {
use crate::app_event::AppEvent;
use crate::app_event_sender::AppEventSender;
use codex_core::config::Config;
use codex_protocol::protocol::RealtimeAudioFrame;
use std::sync::Arc;
use std::sync::Mutex;
@@ -146,7 +149,7 @@ mod voice {
Err("voice input is unavailable in this build".to_string())
}
pub fn start_realtime(_tx: AppEventSender) -> Result<Self, String> {
pub fn start_realtime(_config: &Config, _tx: AppEventSender) -> Result<Self, String> {
Err("voice input is unavailable in this build".to_string())
}
@@ -186,7 +189,7 @@ mod voice {
}
impl RealtimeAudioPlayer {
pub(crate) fn start() -> Result<Self, String> {
pub(crate) fn start(_config: &Config) -> Result<Self, String> {
Err("voice output is unavailable in this build".to_string())
}
@@ -1269,6 +1272,8 @@ mod tests {
TurnContextItem {
turn_id: None,
cwd,
current_date: None,
timezone: None,
approval_policy: config.permissions.approval_policy.value(),
sandbox_policy: config.permissions.sandbox_policy.get().clone(),
network: None,
+14 -12
View File
@@ -51,7 +51,7 @@ pub struct VoiceCapture {
impl VoiceCapture {
pub fn start() -> Result<Self, String> {
let (device, config) = select_input_device_and_config()?;
let (device, config) = select_default_input_device_and_config()?;
let sample_rate = config.sample_rate().0;
let channels = config.channels();
@@ -74,8 +74,8 @@ impl VoiceCapture {
})
}
pub fn start_realtime(tx: AppEventSender) -> Result<Self, String> {
let (device, config) = select_input_device_and_config()?;
pub fn start_realtime(config: &Config, tx: AppEventSender) -> Result<Self, String> {
let (device, config) = select_realtime_input_device_and_config(config)?;
let sample_rate = config.sample_rate().0;
let channels = config.channels();
@@ -262,7 +262,8 @@ pub fn transcribe_async(
// Voice input helpers
// -------------------------
fn select_input_device_and_config() -> Result<(cpal::Device, cpal::SupportedStreamConfig), String> {
fn select_default_input_device_and_config()
-> Result<(cpal::Device, cpal::SupportedStreamConfig), String> {
let host = cpal::default_host();
let device = host
.default_input_device()
@@ -273,6 +274,12 @@ fn select_input_device_and_config() -> Result<(cpal::Device, cpal::SupportedStre
Ok((device, config))
}
fn select_realtime_input_device_and_config(
config: &Config,
) -> Result<(cpal::Device, cpal::SupportedStreamConfig), String> {
crate::audio_device::select_configured_input_device_and_config(config)
}
fn build_input_stream(
device: &cpal::Device,
config: &cpal::SupportedStreamConfig,
@@ -466,14 +473,9 @@ pub(crate) struct RealtimeAudioPlayer {
}
impl RealtimeAudioPlayer {
pub(crate) fn start() -> Result<Self, String> {
let host = cpal::default_host();
let device = host
.default_output_device()
.ok_or_else(|| "no output audio device available".to_string())?;
let config = device
.default_output_config()
.map_err(|e| format!("failed to get default output config: {e}"))?;
pub(crate) fn start(config: &Config) -> Result<Self, String> {
let (device, config) =
crate::audio_device::select_configured_output_device_and_config(config)?;
let output_sample_rate = config.sample_rate().0;
let output_channels = config.channels();
let queue = Arc::new(Mutex::new(VecDeque::new()));
+2
View File
@@ -24,6 +24,8 @@ Codex can run a notification hook when the agent finishes a turn. See the config
- https://developers.openai.com/codex/config-reference
When Codex knows which client started the turn, the legacy notify JSON payload also includes a top-level `client` field. The TUI reports `codex-tui`, and the app server reports the `clientInfo.name` value from `initialize`.
## JSON Schema
The generated JSON Schema for `config.toml` lives at `codex-rs/core/config.schema.json`.