mirror of
https://github.com/openai/codex.git
synced 2026-11-02 19:43:24 +00:00
Merge ce54e4725b into sapling-pr-archive-bolinfest
This commit is contained in:
Generated
+1
@@ -1791,6 +1791,7 @@ dependencies = [
|
||||
"eventsource-stream",
|
||||
"futures",
|
||||
"http 1.4.0",
|
||||
"iana-time-zone",
|
||||
"image",
|
||||
"indexmap 2.13.0",
|
||||
"insta",
|
||||
|
||||
@@ -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(¶ms.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(¶ms.thread_id).await {
|
||||
Ok(v) => v,
|
||||
|
||||
@@ -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(
|
||||
¬ify_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(¬ify_file, Duration::from_secs(5)).await?;
|
||||
let payload_raw = tokio::fs::read_to_string(¬ify_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('"', "\\\""))
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
@@ -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,
|
||||
¤t_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()
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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(_))
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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 || {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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()));
|
||||
|
||||
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user