mirror of
https://github.com/openai/codex.git
synced 2026-10-25 18:28:38 +00:00
Merge origin/main into etraut/alarm
This commit is contained in:
+374
-160
@@ -1,5 +1,4 @@
|
||||
use super::*;
|
||||
use crate::CodexAuth;
|
||||
use crate::alarms::AlarmDelivery;
|
||||
use crate::alarms::AlarmsState;
|
||||
use crate::config::ConfigBuilder;
|
||||
@@ -12,15 +11,19 @@ use crate::config_loader::NetworkDomainPermissionsToml;
|
||||
use crate::config_loader::RequirementSource;
|
||||
use crate::config_loader::Sourced;
|
||||
use crate::exec::ExecCapturePolicy;
|
||||
use crate::exec::ExecToolCallOutput;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::mcp_connection_manager::ToolInfo;
|
||||
use crate::models_manager::model_info;
|
||||
use crate::shell::default_user_shell;
|
||||
use crate::tools::format_exec_output_str;
|
||||
|
||||
use codex_features::Features;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_mcp::mcp_connection_manager::ToolInfo;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_models_manager::bundled_models_response;
|
||||
use codex_models_manager::model_info;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
use codex_protocol::models::FunctionCallOutputBody;
|
||||
use codex_protocol::models::FunctionCallOutputPayload;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
@@ -35,20 +38,6 @@ use codex_protocol::request_permissions::PermissionGrantScope;
|
||||
use codex_protocol::request_permissions::RequestPermissionProfile;
|
||||
use tracing::Span;
|
||||
|
||||
use crate::protocol::CompactedItem;
|
||||
use crate::protocol::CreditsSnapshot;
|
||||
use crate::protocol::InitialHistory;
|
||||
use crate::protocol::NetworkApprovalProtocol;
|
||||
use crate::protocol::RateLimitSnapshot;
|
||||
use crate::protocol::RateLimitWindow;
|
||||
use crate::protocol::ResumedHistory;
|
||||
use crate::protocol::RolloutItem;
|
||||
use crate::protocol::TokenCountEvent;
|
||||
use crate::protocol::TokenUsage;
|
||||
use crate::protocol::TokenUsageInfo;
|
||||
use crate::protocol::TurnCompleteEvent;
|
||||
use crate::protocol::TurnStartedEvent;
|
||||
use crate::protocol::UserMessageEvent;
|
||||
use crate::rollout::policy::EventPersistenceMode;
|
||||
use crate::rollout::recorder::RolloutRecorder;
|
||||
use crate::rollout::recorder::RolloutRecorderParams;
|
||||
@@ -56,7 +45,6 @@ use crate::state::TaskKind;
|
||||
use crate::tasks::SessionTask;
|
||||
use crate::tasks::SessionTaskContext;
|
||||
use crate::tools::ToolRouter;
|
||||
use crate::tools::context::FunctionToolOutput;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::handlers::ShellHandler;
|
||||
@@ -78,11 +66,28 @@ use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::DeveloperInstructions;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::openai_models::ModelsResponse;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::CompactedItem;
|
||||
use codex_protocol::protocol::ConversationAudioParams;
|
||||
use codex_protocol::protocol::CreditsSnapshot;
|
||||
use codex_protocol::protocol::GranularApprovalConfig;
|
||||
use codex_protocol::protocol::InitialHistory;
|
||||
use codex_protocol::protocol::InterAgentCommunication;
|
||||
use codex_protocol::protocol::NetworkApprovalProtocol;
|
||||
use codex_protocol::protocol::RateLimitSnapshot;
|
||||
use codex_protocol::protocol::RateLimitWindow;
|
||||
use codex_protocol::protocol::RealtimeAudioFrame;
|
||||
use codex_protocol::protocol::ResumedHistory;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::Submission;
|
||||
use codex_protocol::protocol::ThreadRolledBackEvent;
|
||||
use codex_protocol::protocol::TokenCountEvent;
|
||||
use codex_protocol::protocol::TokenUsage;
|
||||
use codex_protocol::protocol::TokenUsageInfo;
|
||||
use codex_protocol::protocol::TurnAbortedEvent;
|
||||
use codex_protocol::protocol::TurnCompleteEvent;
|
||||
use codex_protocol::protocol::TurnStartedEvent;
|
||||
use codex_protocol::protocol::UserMessageEvent;
|
||||
use codex_protocol::protocol::W3cTraceContext;
|
||||
use core_test_support::PathBufExt;
|
||||
use core_test_support::context_snapshot;
|
||||
@@ -116,12 +121,6 @@ use std::time::Duration as StdDuration;
|
||||
#[path = "codex_tests_guardian.rs"]
|
||||
mod guardian_tests;
|
||||
|
||||
use codex_protocol::models::function_call_output_content_items_to_text;
|
||||
|
||||
fn expect_text_tool_output(output: &FunctionToolOutput) -> String {
|
||||
function_call_output_content_items_to_text(&output.body).unwrap_or_default()
|
||||
}
|
||||
|
||||
struct InstructionsTestCase {
|
||||
slug: &'static str,
|
||||
expects_apply_patch_instructions: bool,
|
||||
@@ -240,7 +239,7 @@ async fn interrupting_regular_turn_waiting_on_startup_prewarm_emits_turn_aborted
|
||||
.expect("channel open");
|
||||
assert!(matches!(
|
||||
second.msg,
|
||||
EventMsg::TurnAborted(crate::protocol::TurnAbortedEvent {
|
||||
EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
turn_id: Some(turn_id),
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
}) if turn_id == tc.sub_id
|
||||
@@ -252,9 +251,7 @@ fn test_model_client_session() -> crate::client::ModelClientSession {
|
||||
/*auth_manager*/ None,
|
||||
ThreadId::try_from("00000000-0000-4000-8000-000000000001")
|
||||
.expect("test thread id should be valid"),
|
||||
crate::model_provider_info::ModelProviderInfo::create_openai_provider(
|
||||
/* base_url */ /*base_url*/ None,
|
||||
),
|
||||
ModelProviderInfo::create_openai_provider(/* base_url */ /*base_url*/ None),
|
||||
codex_protocol::protocol::SessionSource::Exec,
|
||||
/*model_verbosity*/ None,
|
||||
/*enable_request_compression*/ false,
|
||||
@@ -281,6 +278,23 @@ fn developer_input_texts(items: &[ResponseItem]) -> Vec<&str> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn user_input_texts(items: &[ResponseItem]) -> Vec<&str> {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
ResponseItem::Message { role, content, .. } if role == "user" => {
|
||||
Some(content.as_slice())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.flat_map(|content| content.iter())
|
||||
.filter_map(|item| match item {
|
||||
ContentItem::InputText { text } => Some(text.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn test_tool_runtime(session: Arc<Session>, turn_context: Arc<TurnContext>) -> ToolCallRuntime {
|
||||
let router = Arc::new(ToolRouter::from_config(
|
||||
&turn_context.tools_config,
|
||||
@@ -529,8 +543,8 @@ async fn start_managed_network_proxy_ignores_invalid_execpolicy_network_rules()
|
||||
async fn get_base_instructions_no_user_content() {
|
||||
let prompt_with_apply_patch_instructions =
|
||||
include_str!("../prompt_with_apply_patch_instructions.md");
|
||||
let models_response: ModelsResponse =
|
||||
serde_json::from_str(include_str!("../models.json")).expect("valid models.json");
|
||||
let models_response = bundled_models_response()
|
||||
.unwrap_or_else(|err| panic!("bundled models.json should parse: {err}"));
|
||||
let model_info_for_slug = |slug: &str, config: &Config| {
|
||||
let model = models_response
|
||||
.models
|
||||
@@ -538,7 +552,7 @@ async fn get_base_instructions_no_user_content() {
|
||||
.find(|candidate| candidate.slug == slug)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| panic!("model slug {slug} is missing from models.json"));
|
||||
model_info::with_config_overrides(model, config)
|
||||
model_info::with_config_overrides(model, &config.to_models_manager_config())
|
||||
};
|
||||
let test_cases = vec![
|
||||
InstructionsTestCase {
|
||||
@@ -603,7 +617,7 @@ async fn reload_user_config_layer_updates_effective_apps_config() {
|
||||
.and_then(|table| table.get("apps"))
|
||||
.cloned()
|
||||
.expect("apps table");
|
||||
let apps = crate::config::types::AppsConfigToml::deserialize(apps_toml)
|
||||
let apps = codex_config::types::AppsConfigToml::deserialize(apps_toml)
|
||||
.expect("deserialize apps config");
|
||||
let app = apps
|
||||
.apps
|
||||
@@ -1786,7 +1800,10 @@ async fn set_rate_limits_retains_previous_credits() {
|
||||
let config = build_test_config(codex_home.path()).await;
|
||||
let config = Arc::new(config);
|
||||
let model = ModelsManager::get_model_offline_for_tests(config.model.as_deref());
|
||||
let model_info = ModelsManager::construct_model_info_offline_for_tests(model.as_str(), &config);
|
||||
let model_info = ModelsManager::construct_model_info_offline_for_tests(
|
||||
model.as_str(),
|
||||
&config.to_models_manager_config(),
|
||||
);
|
||||
let reasoning_effort = config.model_reasoning_effort;
|
||||
let collaboration_mode = CollaborationMode {
|
||||
mode: ModeKind::Default,
|
||||
@@ -1821,6 +1838,7 @@ async fn set_rate_limits_retains_previous_credits() {
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
app_server_client_version: None,
|
||||
session_source: SessionSource::Exec,
|
||||
dynamic_tools: Vec::new(),
|
||||
persist_extended_history: false,
|
||||
@@ -1884,7 +1902,10 @@ async fn set_rate_limits_updates_plan_type_when_present() {
|
||||
let config = build_test_config(codex_home.path()).await;
|
||||
let config = Arc::new(config);
|
||||
let model = ModelsManager::get_model_offline_for_tests(config.model.as_deref());
|
||||
let model_info = ModelsManager::construct_model_info_offline_for_tests(model.as_str(), &config);
|
||||
let model_info = ModelsManager::construct_model_info_offline_for_tests(
|
||||
model.as_str(),
|
||||
&config.to_models_manager_config(),
|
||||
);
|
||||
let reasoning_effort = config.model_reasoning_effort;
|
||||
let collaboration_mode = CollaborationMode {
|
||||
mode: ModeKind::Default,
|
||||
@@ -1919,6 +1940,7 @@ async fn set_rate_limits_updates_plan_type_when_present() {
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
app_server_client_version: None,
|
||||
session_source: SessionSource::Exec,
|
||||
dynamic_tools: Vec::new(),
|
||||
persist_extended_history: false,
|
||||
@@ -2034,7 +2056,10 @@ async fn turn_context_with_model_updates_model_fields() {
|
||||
let expected_model_info = session
|
||||
.services
|
||||
.models_manager
|
||||
.get_model_info("gpt-5.1", updated.config.as_ref())
|
||||
.get_model_info(
|
||||
"gpt-5.1",
|
||||
&updated.config.as_ref().to_models_manager_config(),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(updated.config.model.as_deref(), Some("gpt-5.1"));
|
||||
@@ -2122,9 +2147,7 @@ fn success_flag_true_with_no_error_and_content_used() {
|
||||
assert_eq!(expected, got);
|
||||
}
|
||||
|
||||
async fn wait_for_thread_rolled_back(
|
||||
rx: &async_channel::Receiver<Event>,
|
||||
) -> crate::protocol::ThreadRolledBackEvent {
|
||||
async fn wait_for_thread_rolled_back(rx: &async_channel::Receiver<Event>) -> ThreadRolledBackEvent {
|
||||
let deadline = StdDuration::from_secs(2);
|
||||
let start = std::time::Instant::now();
|
||||
loop {
|
||||
@@ -2195,7 +2218,7 @@ fn text_block(s: &str) -> serde_json::Value {
|
||||
}
|
||||
|
||||
async fn build_test_config(codex_home: &Path) -> Config {
|
||||
ConfigBuilder::default()
|
||||
ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.to_path_buf())
|
||||
.build()
|
||||
.await
|
||||
@@ -2227,7 +2250,10 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
|
||||
let config = build_test_config(codex_home.path()).await;
|
||||
let config = Arc::new(config);
|
||||
let model = ModelsManager::get_model_offline_for_tests(config.model.as_deref());
|
||||
let model_info = ModelsManager::construct_model_info_offline_for_tests(model.as_str(), &config);
|
||||
let model_info = ModelsManager::construct_model_info_offline_for_tests(
|
||||
model.as_str(),
|
||||
&config.to_models_manager_config(),
|
||||
);
|
||||
let reasoning_effort = config.model_reasoning_effort;
|
||||
let collaboration_mode = CollaborationMode {
|
||||
mode: ModeKind::Default,
|
||||
@@ -2263,6 +2289,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
app_server_client_version: None,
|
||||
session_source: SessionSource::Exec,
|
||||
dynamic_tools: Vec::new(),
|
||||
persist_extended_history: false,
|
||||
@@ -2491,7 +2518,10 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
|
||||
CollaborationModesConfig::default(),
|
||||
));
|
||||
let model = ModelsManager::get_model_offline_for_tests(config.model.as_deref());
|
||||
let model_info = ModelsManager::construct_model_info_offline_for_tests(model.as_str(), &config);
|
||||
let model_info = ModelsManager::construct_model_info_offline_for_tests(
|
||||
model.as_str(),
|
||||
&config.to_models_manager_config(),
|
||||
);
|
||||
let collaboration_mode = CollaborationMode {
|
||||
mode: ModeKind::Default,
|
||||
settings: Settings {
|
||||
@@ -2525,6 +2555,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
app_server_client_version: None,
|
||||
session_source: SessionSource::Exec,
|
||||
dynamic_tools: Vec::new(),
|
||||
persist_extended_history: false,
|
||||
@@ -2587,7 +2618,10 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
let exec_policy = Arc::new(ExecPolicyManager::default());
|
||||
let (agent_status_tx, _agent_status_rx) = watch::channel(AgentStatus::PendingInit);
|
||||
let model = ModelsManager::get_model_offline_for_tests(config.model.as_deref());
|
||||
let model_info = ModelsManager::construct_model_info_offline_for_tests(model.as_str(), &config);
|
||||
let model_info = ModelsManager::construct_model_info_offline_for_tests(
|
||||
model.as_str(),
|
||||
&config.to_models_manager_config(),
|
||||
);
|
||||
let reasoning_effort = config.model_reasoning_effort;
|
||||
let collaboration_mode = CollaborationMode {
|
||||
mode: ModeKind::Default,
|
||||
@@ -2622,6 +2656,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
app_server_client_version: None,
|
||||
session_source: SessionSource::Exec,
|
||||
dynamic_tools: Vec::new(),
|
||||
persist_extended_history: false,
|
||||
@@ -2631,7 +2666,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
let per_turn_config = Session::build_per_turn_config(&session_configuration);
|
||||
let model_info = ModelsManager::construct_model_info_offline_for_tests(
|
||||
session_configuration.collaboration_mode.model(),
|
||||
&per_turn_config,
|
||||
&per_turn_config.to_models_manager_config(),
|
||||
);
|
||||
let session_telemetry = session_telemetry(
|
||||
conversation_id,
|
||||
@@ -2656,11 +2691,9 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
|
||||
let skills_watcher = Arc::new(SkillsWatcher::noop());
|
||||
let services = SessionServices {
|
||||
mcp_connection_manager: Arc::new(RwLock::new(
|
||||
McpConnectionManager::new_mcp_connection_manager_for_tests(
|
||||
&config.permissions.approval_policy,
|
||||
),
|
||||
)),
|
||||
mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized(
|
||||
&config.permissions.approval_policy,
|
||||
))),
|
||||
mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()),
|
||||
unified_exec_manager: UnifiedExecProcessManager::new(
|
||||
config.background_terminal_max_timeout,
|
||||
@@ -2795,15 +2828,13 @@ async fn request_permissions_emits_event_when_granular_policy_allows_requests()
|
||||
Arc::get_mut(&mut turn_context)
|
||||
.expect("single turn context ref")
|
||||
.approval_policy
|
||||
.set(crate::protocol::AskForApproval::Granular(
|
||||
crate::protocol::GranularApprovalConfig {
|
||||
sandbox_approval: true,
|
||||
rules: true,
|
||||
skill_approval: true,
|
||||
request_permissions: true,
|
||||
mcp_elicitations: true,
|
||||
},
|
||||
))
|
||||
.set(AskForApproval::Granular(GranularApprovalConfig {
|
||||
sandbox_approval: true,
|
||||
rules: true,
|
||||
skill_approval: true,
|
||||
request_permissions: true,
|
||||
mcp_elicitations: true,
|
||||
}))
|
||||
.expect("test setup should allow updating approval policy");
|
||||
|
||||
let session = Arc::new(session);
|
||||
@@ -2870,15 +2901,13 @@ async fn request_permissions_is_auto_denied_when_granular_policy_blocks_tool_req
|
||||
Arc::get_mut(&mut turn_context)
|
||||
.expect("single turn context ref")
|
||||
.approval_policy
|
||||
.set(crate::protocol::AskForApproval::Granular(
|
||||
crate::protocol::GranularApprovalConfig {
|
||||
sandbox_approval: true,
|
||||
rules: true,
|
||||
skill_approval: true,
|
||||
request_permissions: false,
|
||||
mcp_elicitations: true,
|
||||
},
|
||||
))
|
||||
.set(AskForApproval::Granular(GranularApprovalConfig {
|
||||
sandbox_approval: true,
|
||||
rules: true,
|
||||
skill_approval: true,
|
||||
request_permissions: false,
|
||||
mcp_elicitations: true,
|
||||
}))
|
||||
.expect("test setup should allow updating approval policy");
|
||||
|
||||
let session = Arc::new(session);
|
||||
@@ -3101,7 +3130,7 @@ async fn user_turn_updates_approvals_reviewer() {
|
||||
}],
|
||||
cwd: config.cwd.to_path_buf(),
|
||||
approval_policy: config.permissions.approval_policy.value(),
|
||||
approvals_reviewer: Some(crate::config::types::ApprovalsReviewer::GuardianSubagent),
|
||||
approvals_reviewer: Some(codex_config::types::ApprovalsReviewer::GuardianSubagent),
|
||||
sandbox_policy: config.permissions.sandbox_policy.get().clone(),
|
||||
model: turn_context.model_info.slug.clone(),
|
||||
effort: config.model_reasoning_effort,
|
||||
@@ -3117,7 +3146,7 @@ async fn user_turn_updates_approvals_reviewer() {
|
||||
let state = session.state.lock().await;
|
||||
assert_eq!(
|
||||
state.session_configuration.approvals_reviewer,
|
||||
crate::config::types::ApprovalsReviewer::GuardianSubagent
|
||||
codex_config::types::ApprovalsReviewer::GuardianSubagent
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3127,7 +3156,6 @@ async fn spawn_task_turn_span_inherits_dispatch_trace_context() {
|
||||
captured_trace: Arc<std::sync::Mutex<Option<W3cTraceContext>>>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl SessionTask for TraceCaptureTask {
|
||||
fn kind(&self) -> TaskKind {
|
||||
TaskKind::Regular
|
||||
@@ -3433,7 +3461,10 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx(
|
||||
let exec_policy = Arc::new(ExecPolicyManager::default());
|
||||
let (agent_status_tx, _agent_status_rx) = watch::channel(AgentStatus::PendingInit);
|
||||
let model = ModelsManager::get_model_offline_for_tests(config.model.as_deref());
|
||||
let model_info = ModelsManager::construct_model_info_offline_for_tests(model.as_str(), &config);
|
||||
let model_info = ModelsManager::construct_model_info_offline_for_tests(
|
||||
model.as_str(),
|
||||
&config.to_models_manager_config(),
|
||||
);
|
||||
let reasoning_effort = config.model_reasoning_effort;
|
||||
let collaboration_mode = CollaborationMode {
|
||||
mode: ModeKind::Default,
|
||||
@@ -3468,6 +3499,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx(
|
||||
original_config_do_not_use: Arc::clone(&config),
|
||||
metrics_service_name: None,
|
||||
app_server_client_name: None,
|
||||
app_server_client_version: None,
|
||||
session_source: SessionSource::Exec,
|
||||
dynamic_tools,
|
||||
persist_extended_history: false,
|
||||
@@ -3477,7 +3509,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx(
|
||||
let per_turn_config = Session::build_per_turn_config(&session_configuration);
|
||||
let model_info = ModelsManager::construct_model_info_offline_for_tests(
|
||||
session_configuration.collaboration_mode.model(),
|
||||
&per_turn_config,
|
||||
&per_turn_config.to_models_manager_config(),
|
||||
);
|
||||
let session_telemetry = session_telemetry(
|
||||
conversation_id,
|
||||
@@ -3502,11 +3534,9 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx(
|
||||
|
||||
let skills_watcher = Arc::new(SkillsWatcher::noop());
|
||||
let services = SessionServices {
|
||||
mcp_connection_manager: Arc::new(RwLock::new(
|
||||
McpConnectionManager::new_mcp_connection_manager_for_tests(
|
||||
&config.permissions.approval_policy,
|
||||
),
|
||||
)),
|
||||
mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized(
|
||||
&config.permissions.approval_policy,
|
||||
))),
|
||||
mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()),
|
||||
unified_exec_manager: UnifiedExecProcessManager::new(
|
||||
config.background_terminal_max_timeout,
|
||||
@@ -3829,17 +3859,9 @@ async fn build_settings_update_items_emits_environment_item_for_network_changes(
|
||||
.build_settings_update_items(Some(&reference_context_item), ¤t_context)
|
||||
.await;
|
||||
|
||||
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,
|
||||
})
|
||||
let environment_update = user_input_texts(&update_items)
|
||||
.into_iter()
|
||||
.find(|text| text.contains("<environment_context>"))
|
||||
.expect("environment update item should be emitted");
|
||||
assert!(environment_update.contains("<network enabled=\"true\">"));
|
||||
assert!(environment_update.contains("<allowed>api.example.com</allowed>"));
|
||||
@@ -3864,22 +3886,43 @@ async fn build_settings_update_items_emits_environment_item_for_time_changes() {
|
||||
.build_settings_update_items(Some(&reference_context_item), ¤t_context)
|
||||
.await;
|
||||
|
||||
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,
|
||||
})
|
||||
let environment_update = user_input_texts(&update_items)
|
||||
.into_iter()
|
||||
.find(|text| text.contains("<environment_context>"))
|
||||
.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 build_settings_update_items_omits_environment_item_when_disabled() {
|
||||
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;
|
||||
let mut config = (*current_context.config).clone();
|
||||
config.include_environment_context = false;
|
||||
current_context.config = Arc::new(config);
|
||||
current_context.current_date = Some("2026-02-27".to_string());
|
||||
|
||||
let reference_context_item = previous_context.to_turn_context_item();
|
||||
let update_items = session
|
||||
.build_settings_update_items(Some(&reference_context_item), ¤t_context)
|
||||
.await;
|
||||
|
||||
let user_texts = user_input_texts(&update_items);
|
||||
assert!(
|
||||
!user_texts
|
||||
.iter()
|
||||
.any(|text| text.contains("<environment_context>")),
|
||||
"did not expect environment context updates when disabled, got {user_texts:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_settings_update_items_emits_realtime_start_when_session_becomes_live() {
|
||||
let (session, previous_context) = make_session_and_context().await;
|
||||
@@ -4449,7 +4492,6 @@ struct NeverEndingTask {
|
||||
listen_to_cancellation_token: bool,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl SessionTask for NeverEndingTask {
|
||||
fn kind(&self) -> TaskKind {
|
||||
self.kind
|
||||
@@ -4857,6 +4899,214 @@ async fn queued_response_items_for_next_turn_move_into_next_active_turn() {
|
||||
assert_eq!(sess.get_pending_input().await, vec![queued_item]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn queue_only_mailbox_mail_waits_for_next_turn_after_answer_boundary() {
|
||||
let (sess, tc, _rx) = make_session_and_context_with_rx().await;
|
||||
let communication = InterAgentCommunication::new(
|
||||
AgentPath::try_from("/root/worker").expect("worker path should parse"),
|
||||
AgentPath::root(),
|
||||
Vec::new(),
|
||||
"late queue-only update".to_string(),
|
||||
/*trigger_turn*/ false,
|
||||
);
|
||||
sess.spawn_task(
|
||||
Arc::clone(&tc),
|
||||
Vec::new(),
|
||||
NeverEndingTask {
|
||||
kind: TaskKind::Regular,
|
||||
listen_to_cancellation_token: true,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
sess.defer_mailbox_delivery_to_next_turn(&tc.sub_id).await;
|
||||
sess.enqueue_mailbox_communication(communication.clone());
|
||||
|
||||
assert!(
|
||||
!sess.has_pending_input().await,
|
||||
"queue-only mailbox mail should stay buffered once the current turn emitted its answer"
|
||||
);
|
||||
assert_eq!(sess.get_pending_input().await, Vec::new());
|
||||
|
||||
sess.abort_all_tasks(TurnAbortReason::Replaced).await;
|
||||
|
||||
assert_eq!(
|
||||
sess.get_pending_input().await,
|
||||
vec![communication.to_response_input_item()],
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn trigger_turn_mailbox_mail_waits_for_next_turn_after_answer_boundary() {
|
||||
let (sess, tc, _rx) = make_session_and_context_with_rx().await;
|
||||
sess.spawn_task(
|
||||
Arc::clone(&tc),
|
||||
Vec::new(),
|
||||
NeverEndingTask {
|
||||
kind: TaskKind::Regular,
|
||||
listen_to_cancellation_token: true,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
sess.defer_mailbox_delivery_to_next_turn(&tc.sub_id).await;
|
||||
sess.enqueue_mailbox_communication(InterAgentCommunication::new(
|
||||
AgentPath::try_from("/root/worker").expect("worker path should parse"),
|
||||
AgentPath::root(),
|
||||
Vec::new(),
|
||||
"late trigger update".to_string(),
|
||||
/*trigger_turn*/ true,
|
||||
));
|
||||
|
||||
assert!(
|
||||
!sess.has_pending_input().await,
|
||||
"trigger-turn mailbox mail should not extend the current turn after its answer boundary"
|
||||
);
|
||||
|
||||
sess.abort_all_tasks(TurnAbortReason::Replaced).await;
|
||||
|
||||
assert!(sess.has_trigger_turn_mailbox_items().await);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn steered_input_reopens_mailbox_delivery_for_current_turn() {
|
||||
let (sess, tc, _rx) = make_session_and_context_with_rx().await;
|
||||
let communication = InterAgentCommunication::new(
|
||||
AgentPath::try_from("/root/worker").expect("worker path should parse"),
|
||||
AgentPath::root(),
|
||||
Vec::new(),
|
||||
"queued child update".to_string(),
|
||||
/*trigger_turn*/ false,
|
||||
);
|
||||
sess.spawn_task(
|
||||
Arc::clone(&tc),
|
||||
Vec::new(),
|
||||
NeverEndingTask {
|
||||
kind: TaskKind::Regular,
|
||||
listen_to_cancellation_token: true,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
sess.defer_mailbox_delivery_to_next_turn(&tc.sub_id).await;
|
||||
sess.enqueue_mailbox_communication(communication.clone());
|
||||
sess.steer_input(
|
||||
vec![UserInput::Text {
|
||||
text: "follow up".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
Some(&tc.sub_id),
|
||||
)
|
||||
.await
|
||||
.expect("steered input should be accepted");
|
||||
|
||||
assert_eq!(
|
||||
sess.get_pending_input().await,
|
||||
vec![
|
||||
ResponseInputItem::from(vec![UserInput::Text {
|
||||
text: "follow up".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}]),
|
||||
communication.to_response_input_item(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stale_defer_mailbox_delivery_does_not_override_steered_input() {
|
||||
let (sess, tc, _rx) = make_session_and_context_with_rx().await;
|
||||
let communication = InterAgentCommunication::new(
|
||||
AgentPath::try_from("/root/worker").expect("worker path should parse"),
|
||||
AgentPath::root(),
|
||||
Vec::new(),
|
||||
"queued child update".to_string(),
|
||||
/*trigger_turn*/ false,
|
||||
);
|
||||
sess.spawn_task(
|
||||
Arc::clone(&tc),
|
||||
Vec::new(),
|
||||
NeverEndingTask {
|
||||
kind: TaskKind::Regular,
|
||||
listen_to_cancellation_token: true,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
sess.defer_mailbox_delivery_to_next_turn(&tc.sub_id).await;
|
||||
sess.enqueue_mailbox_communication(communication.clone());
|
||||
sess.steer_input(
|
||||
vec![UserInput::Text {
|
||||
text: "follow up".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
Some(&tc.sub_id),
|
||||
)
|
||||
.await
|
||||
.expect("steered input should be accepted");
|
||||
|
||||
sess.defer_mailbox_delivery_to_next_turn(&tc.sub_id).await;
|
||||
|
||||
assert_eq!(
|
||||
sess.get_pending_input().await,
|
||||
vec![
|
||||
ResponseInputItem::from(vec![UserInput::Text {
|
||||
text: "follow up".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}]),
|
||||
communication.to_response_input_item(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tool_calls_reopen_mailbox_delivery_for_current_turn() {
|
||||
let (sess, tc, _rx) = make_session_and_context_with_rx().await;
|
||||
let communication = InterAgentCommunication::new(
|
||||
AgentPath::try_from("/root/worker").expect("worker path should parse"),
|
||||
AgentPath::root(),
|
||||
Vec::new(),
|
||||
"queued child update".to_string(),
|
||||
/*trigger_turn*/ false,
|
||||
);
|
||||
sess.spawn_task(
|
||||
Arc::clone(&tc),
|
||||
Vec::new(),
|
||||
NeverEndingTask {
|
||||
kind: TaskKind::Regular,
|
||||
listen_to_cancellation_token: true,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
sess.defer_mailbox_delivery_to_next_turn(&tc.sub_id).await;
|
||||
sess.enqueue_mailbox_communication(communication.clone());
|
||||
|
||||
let item = ResponseItem::FunctionCall {
|
||||
id: None,
|
||||
name: "test_tool".to_string(),
|
||||
namespace: None,
|
||||
arguments: "{}".to_string(),
|
||||
call_id: "call-1".to_string(),
|
||||
};
|
||||
let mut ctx = HandleOutputCtx {
|
||||
sess: Arc::clone(&sess),
|
||||
turn_context: Arc::clone(&tc),
|
||||
tool_runtime: test_tool_runtime(Arc::clone(&sess), Arc::clone(&tc)),
|
||||
cancellation_token: CancellationToken::new(),
|
||||
};
|
||||
|
||||
let output = handle_output_item_done(&mut ctx, item, /*previously_active_item*/ None)
|
||||
.await
|
||||
.expect("tool call should be handled");
|
||||
|
||||
assert!(output.needs_follow_up);
|
||||
assert!(output.tool_future.is_some());
|
||||
assert_eq!(
|
||||
sess.get_pending_input().await,
|
||||
vec![communication.to_response_input_item()],
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn abort_review_task_emits_exited_then_aborted_and_records_history() {
|
||||
let (sess, tc, rx) = make_session_and_context_with_rx().await;
|
||||
@@ -5160,10 +5410,12 @@ async fn sample_rollout(
|
||||
#[tokio::test]
|
||||
async fn rejects_escalated_permissions_when_policy_not_on_request() {
|
||||
use crate::exec::ExecParams;
|
||||
use crate::protocol::AskForApproval;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use crate::exec_policy::ExecApprovalRequest;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::tools::sandboxing::ExecApprovalRequirement;
|
||||
use crate::turn_diff_tracker::TurnDiffTracker;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use std::collections::HashMap;
|
||||
|
||||
let (session, mut turn_context_raw) = make_session_and_context().await;
|
||||
@@ -5206,23 +5458,6 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
|
||||
arg0: None,
|
||||
};
|
||||
|
||||
let params2 = ExecParams {
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
command: params.command.clone(),
|
||||
cwd: params.cwd.clone(),
|
||||
expiration: timeout_ms.into(),
|
||||
capture_policy: ExecCapturePolicy::ShellTool,
|
||||
env: HashMap::new(),
|
||||
network: None,
|
||||
windows_sandbox_level: turn_context.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: turn_context
|
||||
.config
|
||||
.permissions
|
||||
.windows_sandbox_private_desktop,
|
||||
justification: params.justification.clone(),
|
||||
arg0: None,
|
||||
};
|
||||
|
||||
let turn_diff_tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
|
||||
|
||||
let tool_name = "shell";
|
||||
@@ -5260,9 +5495,11 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
|
||||
);
|
||||
|
||||
pretty_assertions::assert_eq!(output, expected);
|
||||
pretty_assertions::assert_eq!(session.granted_turn_permissions().await, None);
|
||||
|
||||
// Now retry the same command WITHOUT escalated permissions; should succeed.
|
||||
// Force DangerFullAccess to avoid platform sandbox dependencies in tests.
|
||||
// The rejection should not poison the non-escalated path for the same
|
||||
// command. Force DangerFullAccess so this check stays focused on approval
|
||||
// policy rather than platform-specific sandbox behavior.
|
||||
let turn_context_mut = Arc::get_mut(&mut turn_context).expect("unique turn context Arc");
|
||||
turn_context_mut
|
||||
.sandbox_policy
|
||||
@@ -5273,51 +5510,28 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
|
||||
turn_context_mut.network_sandbox_policy =
|
||||
NetworkSandboxPolicy::from(turn_context_mut.sandbox_policy.get());
|
||||
|
||||
let resp2 = handler
|
||||
.handle(ToolInvocation {
|
||||
session: Arc::clone(&session),
|
||||
turn: Arc::clone(&turn_context),
|
||||
tracker: Arc::clone(&turn_diff_tracker),
|
||||
call_id: "test-call-2".to_string(),
|
||||
tool_name: tool_name.to_string(),
|
||||
tool_namespace: None,
|
||||
payload: ToolPayload::Function {
|
||||
arguments: serde_json::json!({
|
||||
"command": params2.command.clone(),
|
||||
"workdir": Some(turn_context.cwd.to_string_lossy().to_string()),
|
||||
"timeout_ms": params2.expiration.timeout_ms(),
|
||||
"sandbox_permissions": params2.sandbox_permissions,
|
||||
"justification": params2.justification.clone(),
|
||||
})
|
||||
.to_string(),
|
||||
},
|
||||
let exec_approval_requirement = session
|
||||
.services
|
||||
.exec_policy
|
||||
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
|
||||
command: ¶ms.command,
|
||||
approval_policy: turn_context.approval_policy.value(),
|
||||
sandbox_policy: turn_context.sandbox_policy.get(),
|
||||
file_system_sandbox_policy: &turn_context.file_system_sandbox_policy,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
})
|
||||
.await;
|
||||
|
||||
let output = expect_text_tool_output(&resp2.expect("expected Ok result"));
|
||||
|
||||
#[derive(Deserialize, PartialEq, Eq, Debug)]
|
||||
struct ResponseExecMetadata {
|
||||
exit_code: i32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ResponseExecOutput {
|
||||
output: String,
|
||||
metadata: ResponseExecMetadata,
|
||||
}
|
||||
|
||||
let exec_output: ResponseExecOutput =
|
||||
serde_json::from_str(&output).expect("valid exec output json");
|
||||
|
||||
pretty_assertions::assert_eq!(exec_output.metadata, ResponseExecMetadata { exit_code: 0 });
|
||||
assert!(exec_output.output.contains("hi"));
|
||||
assert!(matches!(
|
||||
exec_approval_requirement,
|
||||
ExecApprovalRequirement::Skip { .. }
|
||||
));
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn unified_exec_rejects_escalated_permissions_when_policy_not_on_request() {
|
||||
use crate::protocol::AskForApproval;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::turn_diff_tracker::TurnDiffTracker;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
|
||||
let (session, mut turn_context_raw) = make_session_and_context().await;
|
||||
turn_context_raw
|
||||
|
||||
Reference in New Issue
Block a user