Merge 2d03a18cb1 into sapling-pr-archive-bolinfest

This commit is contained in:
Michael Bolin
2026-04-28 17:07:34 -07:00
committed by GitHub
19 changed files with 756 additions and 1631 deletions
+15 -13
View File
@@ -5,8 +5,9 @@ use anyhow::Result;
use codex_core::config::Config;
use codex_core::config::Constrained;
use codex_features::Feature;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::SandboxPolicy;
use crate::test_codex::TestCodex;
use crate::test_codex::test_codex;
@@ -22,7 +23,7 @@ impl ZshForkRuntime {
&self,
config: &mut Config,
approval_policy: AskForApproval,
sandbox_policy: SandboxPolicy,
permission_profile: PermissionProfile,
) {
config
.features
@@ -37,18 +38,19 @@ impl ZshForkRuntime {
config.permissions.allow_login_shell = false;
config.permissions.approval_policy = Constrained::allow_any(approval_policy);
config
.set_legacy_sandbox_policy(sandbox_policy)
.expect("set sandbox policy");
.permissions
.set_permission_profile(permission_profile)
.expect("set permission profile");
}
}
pub fn restrictive_workspace_write_policy() -> SandboxPolicy {
SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
}
pub fn restrictive_workspace_write_profile() -> PermissionProfile {
PermissionProfile::workspace_write_with(
&[],
NetworkSandboxPolicy::Restricted,
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ true,
)
}
pub fn zsh_fork_runtime(test_name: &str) -> Result<Option<ZshForkRuntime>> {
@@ -78,7 +80,7 @@ pub async fn build_zsh_fork_test<F>(
server: &wiremock::MockServer,
runtime: ZshForkRuntime,
approval_policy: AskForApproval,
sandbox_policy: SandboxPolicy,
permission_profile: PermissionProfile,
pre_build_hook: F,
) -> Result<TestCodex>
where
@@ -87,7 +89,7 @@ where
let mut builder = test_codex()
.with_pre_build_hook(pre_build_hook)
.with_config(move |config| {
runtime.apply_to_config(config, approval_policy, sandbox_policy);
runtime.apply_to_config(config, approval_policy, permission_profile);
});
builder.build(server).await
}
+25 -194
View File
@@ -13,11 +13,9 @@ use std::sync::atomic::Ordering;
use std::time::Duration;
use codex_features::Feature;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::user_input::UserInput;
#[cfg(target_os = "linux")]
use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0;
use core_test_support::assert_regex_match;
@@ -59,6 +57,15 @@ async fn apply_patch_harness_with(
Box::pin(TestCodexHarness::with_remote_aware_builder(builder)).await
}
fn restrictive_workspace_write_profile() -> PermissionProfile {
PermissionProfile::workspace_write_with(
&[],
NetworkSandboxPolicy::Restricted,
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ true,
)
}
pub async fn mount_apply_patch(
harness: &TestCodexHarness,
call_id: &str,
@@ -354,28 +361,7 @@ async fn apply_patch_cli_move_without_content_change_has_no_turn_diff(
let call_id = "apply-move-no-change";
mount_apply_patch(&harness, call_id, patch, "ok", model_output).await;
let model = test.session_configured.model.clone();
codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "rename without content change".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: harness.cwd().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
harness.submit("rename without content change").await?;
let mut saw_turn_diff = false;
wait_for_event(&codex, |event| match event {
@@ -641,16 +627,10 @@ async fn apply_patch_cli_rejects_path_traversal_outside_workspace(
let call_id = "apply-path-traversal";
mount_apply_patch(&harness, call_id, patch, "fail", model_output).await;
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
harness
.submit_with_policy(
.submit_with_permission_profile(
"attempt to escape workspace via apply_patch",
sandbox_policy,
restrictive_workspace_write_profile(),
)
.await?;
@@ -696,14 +676,11 @@ async fn apply_patch_cli_rejects_move_path_traversal_outside_workspace(
let call_id = "apply-move-traversal";
mount_apply_patch(&harness, call_id, patch, "fail", model_output).await;
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
harness
.submit_with_policy("attempt move traversal via apply_patch", sandbox_policy)
.submit_with_permission_profile(
"attempt move traversal via apply_patch",
restrictive_workspace_write_profile(),
)
.await?;
let out = harness.apply_patch_output(call_id, model_output).await;
@@ -992,27 +969,7 @@ async fn apply_patch_custom_tool_streaming_emits_updated_changes() -> Result<()>
)
.await;
codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "create streamed file".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: harness.cwd().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: test.session_configured.model.clone(),
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
harness.submit("create streamed file").await?;
let mut updates = Vec::new();
wait_for_event(&codex, |event| match event {
@@ -1090,28 +1047,7 @@ async fn apply_patch_shell_command_heredoc_with_cd_emits_turn_diff() -> Result<(
];
mount_sse_sequence(harness.server(), bodies).await;
let model = test.session_configured.model.clone();
codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "apply via shell heredoc with cd".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: harness.cwd().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
harness.submit("apply via shell heredoc with cd").await?;
let mut saw_turn_diff = None;
let mut saw_patch_begin = false;
@@ -1176,28 +1112,7 @@ async fn apply_patch_shell_command_failure_propagates_error_and_skips_diff() ->
];
mount_sse_sequence(harness.server(), bodies).await;
let model = test.session_configured.model.clone();
codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "apply patch via shell".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: harness.cwd().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
harness.submit("apply patch via shell").await?;
let mut saw_turn_diff = false;
wait_for_event(&codex, |event| match event {
@@ -1333,28 +1248,7 @@ async fn apply_patch_emits_turn_diff_event_with_unified_diff(
let patch = format!("*** Begin Patch\n*** Add File: {file}\n+hello\n*** End Patch\n");
mount_apply_patch(&harness, call_id, patch.as_str(), "ok", model_output).await;
let model = test.session_configured.model.clone();
codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "emit diff".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: harness.cwd().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
harness.submit("emit diff").await?;
let mut saw_turn_diff = None;
wait_for_event(&codex, |event| match event {
@@ -1402,28 +1296,7 @@ async fn apply_patch_turn_diff_for_rename_with_content_change(
let patch = "*** Begin Patch\n*** Update File: old.txt\n*** Move to: new.txt\n@@\n-old\n+new\n*** End Patch";
mount_apply_patch(&harness, call_id, patch, "ok", model_output).await;
let model = test.session_configured.model.clone();
codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "rename with change".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: harness.cwd().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
harness.submit("rename with change").await?;
let mut last_diff: Option<String> = None;
wait_for_event(&codex, |event| match event {
@@ -1480,28 +1353,7 @@ async fn apply_patch_aggregates_diff_across_multiple_tool_calls() -> Result<()>
]);
mount_sse_sequence(harness.server(), vec![s1, s2, s3]).await;
let model = test.session_configured.model.clone();
codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "aggregate diffs".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: harness.cwd().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
harness.submit("aggregate diffs").await?;
let mut last_diff: Option<String> = None;
wait_for_event(&codex, |event| match event {
@@ -1558,28 +1410,7 @@ async fn apply_patch_aggregates_diff_preserves_success_after_failure() -> Result
];
mount_sse_sequence(harness.server(), responses).await;
let model = test.session_configured.model.clone();
codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "apply patch twice with failure".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: harness.cwd().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
harness.submit("apply patch twice with failure").await?;
let mut last_diff: Option<String> = None;
wait_for_event_with_timeout(
+28 -10
View File
@@ -32,10 +32,11 @@ use core_test_support::responses::start_mock_server;
use core_test_support::skip_if_no_network;
use core_test_support::test_codex::TestCodex;
use core_test_support::test_codex::test_codex;
use core_test_support::test_codex::turn_permission_fields;
use core_test_support::wait_for_event;
use core_test_support::wait_for_event_with_timeout;
use core_test_support::zsh_fork::build_zsh_fork_test;
use core_test_support::zsh_fork::restrictive_workspace_write_policy;
use core_test_support::zsh_fork::restrictive_workspace_write_profile;
use core_test_support::zsh_fork::zsh_fork_runtime;
use pretty_assertions::assert_eq;
use regex_lite::Regex;
@@ -2318,7 +2319,7 @@ async fn matched_prefix_rule_runs_unsandboxed_under_zsh_fork() -> Result<()> {
};
let approval_policy = AskForApproval::Never;
let sandbox_policy = restrictive_workspace_write_policy();
let permission_profile = restrictive_workspace_write_profile();
let outside_dir = tempfile::tempdir_in(std::env::current_dir()?)?;
let outside_path = outside_dir
.path()
@@ -2332,7 +2333,7 @@ async fn matched_prefix_rule_runs_unsandboxed_under_zsh_fork() -> Result<()> {
&server,
runtime,
approval_policy,
sandbox_policy.clone(),
permission_profile.clone(),
move |home| {
let _ = fs::remove_file(&outside_path_for_hook);
let rules_dir = home.join("rules");
@@ -2367,13 +2368,30 @@ async fn matched_prefix_rule_runs_unsandboxed_under_zsh_fork() -> Result<()> {
)
.await;
submit_turn(
&test,
"run allowed touch under zsh fork",
approval_policy,
sandbox_policy,
)
.await?;
let session_model = test.session_configured.model.clone();
let (sandbox_policy, permission_profile) =
turn_permission_fields(permission_profile, test.cwd.path());
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "run allowed touch under zsh fork".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd.path().to_path_buf(),
approval_policy,
approvals_reviewer: Some(ApprovalsReviewer::User),
sandbox_policy,
permission_profile,
model: session_model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
wait_for_completion_without_approval(&test).await;
+61 -134
View File
@@ -8,6 +8,7 @@ use codex_model_provider_info::ModelProviderInfo;
use codex_model_provider_info::built_in_model_providers;
use codex_models_manager::bundled_models_response;
use codex_protocol::items::TurnItem;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::protocol::AskForApproval;
@@ -17,7 +18,6 @@ use codex_protocol::protocol::ItemStartedEvent;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::RolloutItem;
use codex_protocol::protocol::RolloutLine;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::WarningEvent;
use codex_protocol::user_input::UserInput;
use core_test_support::context_snapshot;
@@ -28,6 +28,7 @@ use core_test_support::responses::ev_reasoning_item;
use core_test_support::responses::mount_models_once;
use core_test_support::skip_if_no_network;
use core_test_support::test_codex::test_codex;
use core_test_support::test_codex::turn_permission_fields;
use core_test_support::wait_for_event;
use core_test_support::wait_for_event_match;
use std::path::PathBuf;
@@ -71,6 +72,30 @@ const PRETURN_CONTEXT_DIFF_CWD: &str = "/tmp/PRETURN_CONTEXT_DIFF_CWD";
pub(super) const COMPACT_WARNING_MESSAGE: &str = "Heads up: Long threads and multiple compactions can cause the model to be less accurate. Start a new thread when possible to keep threads small and targeted.";
fn disabled_permission_user_turn(text: impl Into<String>, cwd: PathBuf, model: String) -> Op {
let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, cwd.as_path());
Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: text.into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd,
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy,
permission_profile,
model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
}
}
fn auto_summary(summary: &str) -> String {
summary.to_string()
}
@@ -1675,25 +1700,11 @@ async fn auto_compact_runs_after_resume_when_token_usage_is_over_limit() {
resumed
.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: follow_up_user.into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: resumed.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: resumed.session_configured.model.clone(),
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.submit(disabled_permission_user_turn(
follow_up_user,
resumed.cwd.path().to_path_buf(),
resumed.session_configured.model.clone(),
))
.await
.unwrap();
@@ -1768,25 +1779,11 @@ async fn pre_sampling_compact_runs_on_switch_to_smaller_context_model() {
let test = builder.build(&server).await.expect("build test codex");
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "before switch".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: previous_model.to_string(),
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.submit(disabled_permission_user_turn(
"before switch",
test.cwd.path().to_path_buf(),
previous_model.to_string(),
))
.await
.expect("submit first user turn");
wait_for_event(&test.codex, |event| {
@@ -1795,25 +1792,11 @@ async fn pre_sampling_compact_runs_on_switch_to_smaller_context_model() {
.await;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "after switch".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: next_model.to_string(),
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.submit(disabled_permission_user_turn(
"after switch",
test.cwd.path().to_path_buf(),
next_model.to_string(),
))
.await
.expect("submit second user turn");
assert_compaction_uses_turn_lifecycle_id(&test.codex).await;
@@ -1908,25 +1891,11 @@ async fn pre_sampling_compact_runs_after_resume_and_switch_to_smaller_model() {
initial
.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "before resume".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: initial.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: previous_model.to_string(),
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.submit(disabled_permission_user_turn(
"before resume",
initial.cwd.path().to_path_buf(),
previous_model.to_string(),
))
.await
.expect("submit pre-resume turn");
wait_for_event(&initial.codex, |event| {
@@ -1959,25 +1928,11 @@ async fn pre_sampling_compact_runs_after_resume_and_switch_to_smaller_model() {
resumed
.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "after resume".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: resumed.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: next_model.to_string(),
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.submit(disabled_permission_user_turn(
"after resume",
resumed.cwd.path().to_path_buf(),
next_model.to_string(),
))
.await
.expect("submit resumed user turn");
assert_compaction_uses_turn_lifecycle_id(&resumed.codex).await;
@@ -3195,25 +3150,11 @@ async fn snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch
.expect("build codex");
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "BEFORE_SWITCH_USER".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: previous_model.to_string(),
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.submit(disabled_permission_user_turn(
"BEFORE_SWITCH_USER",
test.cwd.path().to_path_buf(),
previous_model.to_string(),
))
.await
.expect("submit first user turn");
wait_for_event(&test.codex, |event| {
@@ -3222,25 +3163,11 @@ async fn snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch
.await;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "AFTER_SWITCH_USER".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: next_model.to_string(),
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.submit(disabled_permission_user_turn(
"AFTER_SWITCH_USER",
test.cwd.path().to_path_buf(),
next_model.to_string(),
))
.await
.expect("submit second user turn");
wait_for_event(&test.codex, |event| {
+14 -9
View File
@@ -5,10 +5,10 @@ use codex_features::Feature;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::user_input::UserInput;
use core_test_support::responses::ev_assistant_message;
use core_test_support::responses::ev_completed;
@@ -18,6 +18,7 @@ use core_test_support::responses::mount_sse_once;
use core_test_support::responses::sse;
use core_test_support::responses::start_mock_server;
use core_test_support::test_codex::test_codex;
use core_test_support::test_codex::turn_permission_fields;
use core_test_support::wait_for_event;
use serde_json::Value;
use serde_json::json;
@@ -38,10 +39,12 @@ async fn submit_user_turn(
test: &core_test_support::test_codex::TestCodex,
prompt: &str,
approval_policy: AskForApproval,
sandbox_policy: SandboxPolicy,
permission_profile: PermissionProfile,
collaboration_mode: Option<CollaborationMode>,
) -> Result<()> {
let session_model = test.session_configured.model.clone();
let (sandbox_policy, permission_profile) =
turn_permission_fields(permission_profile, test.config.cwd.as_path());
test.codex
.submit(Op::UserTurn {
environments: None,
@@ -54,7 +57,7 @@ async fn submit_user_turn(
approval_policy,
approvals_reviewer: None,
sandbox_policy,
permission_profile: None,
permission_profile,
model: session_model,
effort: None,
summary: None,
@@ -125,6 +128,8 @@ async fn execpolicy_blocks_shell_invocation() -> Result<()> {
.await;
let session_model = test.session_configured.model.clone();
let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path());
test.codex
.submit(Op::UserTurn {
environments: None,
@@ -136,8 +141,8 @@ async fn execpolicy_blocks_shell_invocation() -> Result<()> {
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
sandbox_policy,
permission_profile,
model: session_model,
effort: None,
summary: None,
@@ -208,7 +213,7 @@ async fn shell_command_empty_script_with_collaboration_mode_does_not_panic() ->
&test,
"run an empty shell command",
AskForApproval::OnRequest,
SandboxPolicy::DangerFullAccess,
PermissionProfile::Disabled,
Some(collaboration_mode),
)
.await?;
@@ -267,7 +272,7 @@ async fn unified_exec_empty_script_with_collaboration_mode_does_not_panic() -> R
&test,
"run empty unified exec command",
AskForApproval::OnRequest,
SandboxPolicy::DangerFullAccess,
PermissionProfile::Disabled,
Some(collaboration_mode),
)
.await?;
@@ -322,7 +327,7 @@ async fn shell_command_whitespace_script_with_collaboration_mode_does_not_panic(
&test,
"run whitespace shell command",
AskForApproval::OnRequest,
SandboxPolicy::DangerFullAccess,
PermissionProfile::Disabled,
Some(collaboration_mode),
)
.await?;
@@ -381,7 +386,7 @@ async fn unified_exec_whitespace_script_with_collaboration_mode_does_not_panic()
&test,
"run whitespace unified exec command",
AskForApproval::OnRequest,
SandboxPolicy::DangerFullAccess,
PermissionProfile::Disabled,
Some(collaboration_mode),
)
.await?;
+39 -29
View File
@@ -7,13 +7,14 @@ use codex_core::config::Constrained;
use codex_features::Feature;
use codex_protocol::items::parse_hook_prompt_fragment;
use codex_protocol::models::ContentItem;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::ResponseItem;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::RolloutItem;
use codex_protocol::protocol::RolloutLine;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::user_input::UserInput;
use core_test_support::managed_network_requirements_loader;
use core_test_support::responses::ev_apply_patch_function_call;
@@ -48,6 +49,24 @@ const BLOCKED_PROMPT_CONTEXT: &str = "Remember the blocked lighthouse note.";
const PERMISSION_REQUEST_HOOK_MATCHER: &str = "^Bash$";
const PERMISSION_REQUEST_ALLOW_REASON: &str = "should not be used for allow";
fn restrictive_workspace_write_profile() -> PermissionProfile {
PermissionProfile::workspace_write_with(
&[],
NetworkSandboxPolicy::Restricted,
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ true,
)
}
fn network_workspace_write_profile() -> PermissionProfile {
PermissionProfile::workspace_write_with(
&[],
NetworkSandboxPolicy::Enabled,
/*exclude_tmpdir_env_var*/ false,
/*exclude_slash_tmp*/ false,
)
}
fn write_stop_hook(home: &Path, block_prompts: &[&str]) -> Result<()> {
let script_path = home.join("stop_hook.py");
let log_path = home.join("stop_hook_log.jsonl");
@@ -1322,10 +1341,10 @@ async fn permission_request_hook_allows_shell_command_without_user_approval() ->
fs::write(&marker, "seed").context("create permission request marker")?;
test.submit_turn_with_policies(
test.submit_turn_with_approval_and_permission_profile(
"run the shell command after hook approval",
AskForApproval::OnRequest,
codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
PermissionProfile::Disabled,
)
.await?;
@@ -1407,15 +1426,10 @@ async fn permission_request_hook_allows_apply_patch_with_write_alias() -> Result
let test = builder.build(&server).await?;
let target_path = test.workspace_path(&patch_path);
test.submit_turn_with_policies(
test.submit_turn_with_approval_and_permission_profile(
"apply the patch after hook approval",
AskForApproval::OnRequest,
SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
},
restrictive_workspace_write_profile(),
)
.await?;
@@ -1494,10 +1508,10 @@ async fn permission_request_hook_sees_raw_exec_command_input() -> Result<()> {
fs::write(&marker, "seed").context("create exec command permission request marker")?;
test.submit_turn_with_policies(
test.submit_turn_with_approval_and_permission_profile(
"run the exec command after hook approval",
AskForApproval::OnRequest,
codex_protocol::protocol::SandboxPolicy::new_read_only_policy(),
PermissionProfile::read_only(),
)
.await?;
@@ -1558,13 +1572,8 @@ allow_local_binding = true
.await;
let approval_policy = AskForApproval::OnFailure;
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: true,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
};
let sandbox_policy_for_config = sandbox_policy.clone();
let permission_profile = network_workspace_write_profile();
let permission_profile_for_config = permission_profile.clone();
let test = test_codex()
.with_home(Arc::clone(&home))
.with_pre_build_hook(|home| {
@@ -1580,8 +1589,9 @@ allow_local_binding = true
.expect("test config should allow feature update");
config.permissions.approval_policy = Constrained::allow_any(approval_policy);
config
.set_legacy_sandbox_policy(sandbox_policy_for_config)
.expect("set sandbox policy");
.permissions
.set_permission_profile(permission_profile_for_config)
.expect("set permission profile");
})
.build(&server)
.await?;
@@ -1598,10 +1608,10 @@ allow_local_binding = true
.as_ref()
.expect("expected runtime managed network proxy addresses");
test.submit_turn_with_policies(
test.submit_turn_with_approval_and_permission_profile(
"run the shell command after network hook approval",
approval_policy,
sandbox_policy,
permission_profile,
)
.await?;
@@ -1695,10 +1705,10 @@ async fn permission_request_hook_sees_retry_context_after_sandbox_denial() -> Re
let marker_path = test.workspace_path(marker);
let _ = fs::remove_file(&marker_path);
test.submit_turn_with_policies(
test.submit_turn_with_approval_and_permission_profile(
"retry the shell command after sandbox denial",
AskForApproval::OnFailure,
codex_protocol::protocol::SandboxPolicy::new_read_only_policy(),
PermissionProfile::read_only(),
)
.await?;
@@ -1769,9 +1779,9 @@ async fn pre_tool_use_blocks_shell_command_before_execution() -> Result<()> {
fs::remove_file(&marker).context("remove leftover pre tool use marker")?;
}
test.submit_turn_with_policy(
test.submit_turn_with_permission_profile(
"run the blocked shell command",
codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
PermissionProfile::Disabled,
)
.await?;
@@ -2013,9 +2023,9 @@ async fn pre_tool_use_blocks_shell_when_defined_in_config_toml() -> Result<()> {
fs::remove_file(&marker).context("remove leftover config.toml marker")?;
}
test.submit_turn_with_policy(
test.submit_turn_with_permission_profile(
"run the blocked shell command from config toml",
codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
PermissionProfile::Disabled,
)
.await?;
+57 -95
View File
@@ -6,7 +6,9 @@ use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::items::AgentMessageContent;
use codex_protocol::items::TurnItem;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::WebSearchAction;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ItemCompletedEvent;
use codex_protocol::protocol::ItemStartedEvent;
@@ -33,12 +35,42 @@ use core_test_support::responses::start_mock_server;
use core_test_support::skip_if_no_network;
use core_test_support::test_codex::TestCodex;
use core_test_support::test_codex::test_codex;
use core_test_support::test_codex::turn_permission_fields;
use core_test_support::wait_for_event;
use core_test_support::wait_for_event_match;
use pretty_assertions::assert_eq;
use std::path::Path;
use std::path::PathBuf;
fn disabled_plan_turn(
text: &str,
model: String,
collaboration_mode: CollaborationMode,
) -> anyhow::Result<Op> {
let cwd = std::env::current_dir()?;
let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, cwd.as_path());
Ok(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: text.into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd,
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy,
permission_profile,
model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: Some(collaboration_mode),
personality: None,
})
}
fn image_generation_artifact_path(codex_home: &Path, session_id: &str, call_id: &str) -> PathBuf {
fn sanitize(value: &str) -> String {
let mut sanitized: String = value
@@ -529,25 +561,11 @@ async fn plan_mode_emits_plan_item_from_proposed_plan_block() -> anyhow::Result<
};
codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "please plan".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: std::env::current_dir()?,
approval_policy: codex_protocol::protocol::AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_configured.model.clone(),
effort: None,
summary: None,
service_tier: None,
collaboration_mode: Some(collaboration_mode),
personality: None,
})
.submit(disabled_plan_turn(
"please plan",
session_configured.model.clone(),
collaboration_mode,
)?)
.await?;
let plan_delta = wait_for_event_match(&codex, |ev| match ev {
@@ -608,25 +626,11 @@ async fn plan_mode_strips_plan_from_agent_messages() -> anyhow::Result<()> {
};
codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "please plan".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: std::env::current_dir()?,
approval_policy: codex_protocol::protocol::AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_configured.model.clone(),
effort: None,
summary: None,
service_tier: None,
collaboration_mode: Some(collaboration_mode),
personality: None,
})
.submit(disabled_plan_turn(
"please plan",
session_configured.model.clone(),
collaboration_mode,
)?)
.await?;
let mut agent_deltas = Vec::new();
@@ -719,25 +723,11 @@ async fn plan_mode_streaming_citations_are_stripped_across_added_deltas_and_done
};
codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "please plan with citations".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: std::env::current_dir()?,
approval_policy: codex_protocol::protocol::AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_configured.model.clone(),
effort: None,
summary: None,
service_tier: None,
collaboration_mode: Some(collaboration_mode),
personality: None,
})
.submit(disabled_plan_turn(
"please plan with citations",
session_configured.model.clone(),
collaboration_mode,
)?)
.await?;
let mut agent_started = None;
@@ -908,25 +898,11 @@ async fn plan_mode_streaming_proposed_plan_tag_split_across_added_and_delta_is_p
};
codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "please plan".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: std::env::current_dir()?,
approval_policy: codex_protocol::protocol::AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_configured.model.clone(),
effort: None,
summary: None,
service_tier: None,
collaboration_mode: Some(collaboration_mode),
personality: None,
})
.submit(disabled_plan_turn(
"please plan",
session_configured.model.clone(),
collaboration_mode,
)?)
.await?;
let mut agent_started = None;
@@ -1024,25 +1000,11 @@ async fn plan_mode_handles_missing_plan_close_tag() -> anyhow::Result<()> {
};
codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "please plan".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: std::env::current_dir()?,
approval_policy: codex_protocol::protocol::AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_configured.model.clone(),
effort: None,
summary: None,
service_tier: None,
collaboration_mode: Some(collaboration_mode),
personality: None,
})
.submit(disabled_plan_turn(
"please plan",
session_configured.model.clone(),
collaboration_mode,
)?)
.await?;
let mut plan_delta = None;
+94 -225
View File
@@ -5,6 +5,7 @@ use codex_login::CodexAuth;
use codex_models_manager::manager::RefreshStrategy;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ConfigShellToolType;
use codex_protocol::openai_models::InputModality;
use codex_protocol::openai_models::ModelInfo;
@@ -17,7 +18,6 @@ use codex_protocol::openai_models::default_input_modalities;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::user_input::UserInput;
use core_test_support::responses::ev_completed_with_tokens;
use core_test_support::responses::ev_image_generation_call;
@@ -29,13 +29,36 @@ use core_test_support::responses::sse;
use core_test_support::responses::sse_completed;
use core_test_support::responses::start_mock_server;
use core_test_support::skip_if_no_network;
use core_test_support::test_codex::TestCodex;
use core_test_support::test_codex::test_codex;
use core_test_support::test_codex::turn_permission_fields;
use core_test_support::wait_for_event;
use pretty_assertions::assert_eq;
use std::path::Path;
use std::path::PathBuf;
use wiremock::MockServer;
fn read_only_user_turn(test: &TestCodex, items: Vec<UserInput>, model: String) -> Op {
let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::read_only(), test.cwd_path());
Op::UserTurn {
environments: None,
items,
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy,
permission_profile,
model,
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
}
}
fn image_generation_artifact_path(codex_home: &Path, session_id: &str, call_id: &str) -> PathBuf {
fn sanitize(value: &str) -> String {
let mut sanitized: String = value
@@ -120,25 +143,14 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<(
let next_model = "gpt-5.4";
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
.submit(read_only_user_turn(
&test,
vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
test.session_configured.model.clone(),
))
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -160,25 +172,14 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<(
.await?;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
.submit(read_only_user_turn(
&test,
vec![UserInput::Text {
text: "switch models".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: next_model.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
next_model.to_string(),
))
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -222,25 +223,14 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul
let next_model = "exp-codex-personality";
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
.submit(read_only_user_turn(
&test,
vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
test.session_configured.model.clone(),
))
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -262,25 +252,14 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul
.await?;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
.submit(read_only_user_turn(
&test,
vec![UserInput::Text {
text: "switch model and personality".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: next_model.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
next_model.to_string(),
))
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -401,9 +380,9 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result<
.to_string();
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![
.submit(read_only_user_turn(
&test,
vec![
UserInput::Image {
image_url: image_url.clone(),
},
@@ -412,42 +391,20 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result<
text_elements: Vec::new(),
},
],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: image_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
image_model_slug.to_string(),
))
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
.submit(read_only_user_turn(
&test,
vec![UserInput::Text {
text: "second turn".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: text_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
text_model_slug.to_string(),
))
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -539,48 +496,26 @@ async fn generated_image_is_replayed_for_image_capable_models() -> Result<()> {
.await;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
.submit(read_only_user_turn(
&test,
vec![UserInput::Text {
text: "generate a lobster".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: image_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
service_tier: None,
summary: None,
collaboration_mode: None,
personality: None,
})
image_model_slug.to_string(),
))
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
.submit(read_only_user_turn(
&test,
vec![UserInput::Text {
text: "describe the generated image".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: image_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
service_tier: None,
summary: None,
collaboration_mode: None,
personality: None,
})
image_model_slug.to_string(),
))
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -675,48 +610,26 @@ async fn model_change_from_generated_image_to_text_preserves_prior_generated_ima
.await;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
.submit(read_only_user_turn(
&test,
vec![UserInput::Text {
text: "generate a lobster".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: image_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
service_tier: None,
summary: None,
collaboration_mode: None,
personality: None,
})
image_model_slug.to_string(),
))
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
.submit(read_only_user_turn(
&test,
vec![UserInput::Text {
text: "describe the generated image".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: text_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
service_tier: None,
summary: None,
collaboration_mode: None,
personality: None,
})
text_model_slug.to_string(),
))
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -813,25 +726,14 @@ async fn thread_rollback_after_generated_image_drops_entire_image_turn_history()
.await;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
.submit(read_only_user_turn(
&test,
vec![UserInput::Text {
text: "generate a lobster".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: image_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
service_tier: None,
summary: None,
collaboration_mode: None,
personality: None,
})
image_model_slug.to_string(),
))
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -844,25 +746,14 @@ async fn thread_rollback_after_generated_image_drops_entire_image_turn_history()
.await;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
.submit(read_only_user_turn(
&test,
vec![UserInput::Text {
text: "after rollback".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: image_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
service_tier: None,
summary: None,
collaboration_mode: None,
personality: None,
})
image_model_slug.to_string(),
))
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -1003,25 +894,14 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<
);
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
.submit(read_only_user_turn(
&test,
vec![UserInput::Text {
text: "use larger model".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: large_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
large_model_slug.to_string(),
))
.await?;
let large_window_event = wait_for_event(&test.codex, |event| {
@@ -1065,25 +945,14 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<
.await?;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
.submit(read_only_user_turn(
&test,
vec![UserInput::Text {
text: "switch to smaller model".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: smaller_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
smaller_model_slug.to_string(),
))
.await?;
let smaller_turn_started_event = wait_for_event(&test.codex, |event| {
@@ -5,10 +5,11 @@ use codex_core::config::Constrained;
use codex_core::context::ContextualUserFragment;
use codex_core::context::PermissionsInstructions;
use codex_core::load_exec_policy;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::user_input::UserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
use core_test_support::responses::ResponsesRequest;
@@ -540,20 +541,19 @@ async fn permissions_message_includes_writable_roots() -> Result<()> {
.await;
let writable = TempDir::new()?;
let writable_root = AbsolutePathBuf::try_from(writable.path())?;
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![writable_root],
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
};
let sandbox_policy_for_config = sandbox_policy.clone();
let permission_profile = PermissionProfile::workspace_write_with(
&[writable_root],
NetworkSandboxPolicy::Restricted,
/*exclude_tmpdir_env_var*/ false,
/*exclude_slash_tmp*/ false,
);
let mut builder = test_codex().with_config(move |config| {
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
config
.permissions
.set_legacy_sandbox_policy(sandbox_policy_for_config, config.cwd.as_path())
.expect("test sandbox policy should be allowed");
.set_permission_profile(permission_profile)
.expect("test permission profile should be allowed");
config.config_layer_stack = ConfigLayerStack::default();
});
let test = builder.build(&server).await?;
@@ -574,6 +574,7 @@ async fn permissions_message_includes_writable_roots() -> Result<()> {
let permissions = permissions_texts(&req.single_request());
let normalize_line_endings = |s: &str| s.replace("\r\n", "\n");
let exec_policy = load_exec_policy(&test.config.config_layer_stack).await?;
let sandbox_policy = test.config.legacy_sandbox_policy();
let expected = PermissionsInstructions::from_policy(
&sandbox_policy,
AskForApproval::OnRequest,
+122 -248
View File
@@ -3,6 +3,7 @@ use codex_features::Feature;
use codex_models_manager::manager::RefreshStrategy;
use codex_models_manager::manager::SharedModelsManager;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ConfigShellToolType;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelInstructionsVariables;
@@ -16,7 +17,6 @@ use codex_protocol::openai_models::default_input_modalities;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::user_input::UserInput;
use core_test_support::load_default_config_for_test;
use core_test_support::responses::mount_models_once;
@@ -25,7 +25,9 @@ use core_test_support::responses::mount_sse_sequence;
use core_test_support::responses::sse_completed;
use core_test_support::responses::start_mock_server;
use core_test_support::skip_if_no_network;
use core_test_support::test_codex::TestCodex;
use core_test_support::test_codex::test_codex;
use core_test_support::test_codex::turn_permission_fields;
use core_test_support::wait_for_event;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
@@ -39,6 +41,46 @@ const LOCAL_FRIENDLY_TEMPLATE: &str =
"You optimize for team morale and being a supportive teammate as much as code quality.";
const LOCAL_PRAGMATIC_TEMPLATE: &str = "You are a deeply pragmatic, effective software engineer.";
fn read_only_text_turn(
test: &TestCodex,
text: &str,
model: String,
approval_policy: AskForApproval,
) -> Op {
let personality = None;
read_only_text_turn_with_personality(test, text, model, approval_policy, personality)
}
fn read_only_text_turn_with_personality(
test: &TestCodex,
text: &str,
model: String,
approval_policy: AskForApproval,
personality: Option<Personality>,
) -> Op {
let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::read_only(), test.cwd_path());
Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: text.into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy,
approvals_reviewer: None,
sandbox_policy,
permission_profile,
model,
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality,
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn personality_does_not_mutate_base_instructions_without_template() {
let codex_home = TempDir::new().expect("create temp dir");
@@ -94,25 +136,12 @@ async fn user_turn_personality_none_does_not_add_update_message() -> anyhow::Res
let test = builder.build(&server).await?;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.submit(read_only_text_turn(
&test,
"hello",
test.session_configured.model.clone(),
test.config.permissions.approval_policy.value(),
))
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -147,25 +176,12 @@ async fn config_personality_some_sets_instructions_template() -> anyhow::Result<
let test = builder.build(&server).await?;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.submit(read_only_text_turn(
&test,
"hello",
test.session_configured.model.clone(),
test.config.permissions.approval_policy.value(),
))
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -207,25 +223,12 @@ async fn config_personality_none_sends_no_personality() -> anyhow::Result<()> {
let test = builder.build(&server).await?;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.submit(read_only_text_turn(
&test,
"hello",
test.session_configured.model.clone(),
test.config.permissions.approval_policy.value(),
))
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -273,25 +276,12 @@ async fn default_personality_is_pragmatic_without_config_toml() -> anyhow::Resul
let test = builder.build(&server).await?;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.submit(read_only_text_turn(
&test,
"hello",
test.session_configured.model.clone(),
test.config.permissions.approval_policy.value(),
))
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -327,25 +317,12 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()>
let test = builder.build(&server).await?;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.submit(read_only_text_turn(
&test,
"hello",
test.session_configured.model.clone(),
test.config.permissions.approval_policy.value(),
))
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -368,25 +345,12 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()>
.await?;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.submit(read_only_text_turn(
&test,
"hello",
test.session_configured.model.clone(),
test.config.permissions.approval_policy.value(),
))
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -437,25 +401,12 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho
let test = builder.build(&server).await?;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.submit(read_only_text_turn(
&test,
"hello",
test.session_configured.model.clone(),
test.config.permissions.approval_policy.value(),
))
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -478,25 +429,12 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho
.await?;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.submit(read_only_text_turn(
&test,
"hello",
test.session_configured.model.clone(),
test.config.permissions.approval_policy.value(),
))
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -560,25 +498,12 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()>
let test = builder.build(&server).await?;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.submit(read_only_text_turn(
&test,
"hello",
test.session_configured.model.clone(),
test.config.permissions.approval_policy.value(),
))
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -601,25 +526,12 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()>
.await?;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.submit(read_only_text_turn(
&test,
"hello",
test.session_configured.model.clone(),
test.config.permissions.approval_policy.value(),
))
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -722,25 +634,13 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow
wait_for_model_available(&test.thread_manager.get_models_manager(), remote_slug).await;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: remote_slug.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: Some(Personality::Friendly),
})
.submit(read_only_text_turn_with_personality(
&test,
"hello",
remote_slug.to_string(),
AskForApproval::Never,
Some(Personality::Friendly),
))
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -845,25 +745,12 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
wait_for_model_available(&test.thread_manager.get_models_manager(), remote_slug).await;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: remote_slug.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.submit(read_only_text_turn(
&test,
"hello",
remote_slug.to_string(),
AskForApproval::Never,
))
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -886,25 +773,12 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
.await?;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: remote_slug.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.submit(read_only_text_turn(
&test,
"hello",
remote_slug.to_string(),
AskForApproval::Never,
))
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
+29 -20
View File
@@ -9,12 +9,13 @@ use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::Settings;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::user_input::UserInput;
use core_test_support::TempDirExt;
use core_test_support::responses::ev_completed;
@@ -25,6 +26,7 @@ use core_test_support::responses::start_mock_server;
use core_test_support::skip_if_no_network;
use core_test_support::test_codex::TestCodex;
use core_test_support::test_codex::test_codex;
use core_test_support::test_codex::turn_permission_fields;
use core_test_support::wait_for_event;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
@@ -405,7 +407,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an
)
.await;
let TestCodex { codex, .. } = test_codex()
let TestCodex { codex, config, .. } = test_codex()
.with_config(|config| {
config.user_instructions = Some("be consistent and helpful".to_string());
config
@@ -431,19 +433,22 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
let writable = TempDir::new().unwrap();
let new_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![writable.path().try_into().unwrap()],
network_access: true,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let permission_profile = PermissionProfile::workspace_write_with(
&[writable.abs()],
NetworkSandboxPolicy::Enabled,
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ true,
);
let sandbox_policy = permission_profile
.to_legacy_sandbox_policy(config.cwd.as_path())
.expect("workspace profile should have legacy projection");
codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: Some(AskForApproval::Never),
approvals_reviewer: None,
sandbox_policy: Some(new_policy.clone()),
permission_profile: None,
sandbox_policy: Some(sandbox_policy),
permission_profile: Some(permission_profile),
windows_sandbox_level: None,
model: None,
effort: Some(Some(ReasoningEffort::High)),
@@ -709,12 +714,14 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
// Second turn using per-turn overrides via UserTurn
let new_cwd = TempDir::new().unwrap();
let writable = TempDir::new().unwrap();
let new_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![writable.abs()],
network_access: true,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let permission_profile = PermissionProfile::workspace_write_with(
&[writable.abs()],
NetworkSandboxPolicy::Enabled,
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ true,
);
let (sandbox_policy, permission_profile) =
turn_permission_fields(permission_profile, new_cwd.path());
codex
.submit(Op::UserTurn {
environments: None,
@@ -725,8 +732,8 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
cwd: new_cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: new_policy.clone(),
permission_profile: None,
sandbox_policy,
permission_profile,
model: "o3".to_string(),
effort: Some(ReasoningEffort::High),
summary: Some(ReasoningSummary::Detailed),
@@ -983,6 +990,8 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
.await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, default_cwd.as_path());
codex
.submit(Op::UserTurn {
environments: None,
@@ -993,8 +1002,8 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
cwd: default_cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
sandbox_policy,
permission_profile,
model: "o3".to_string(),
effort: Some(ReasoningEffort::High),
summary: Some(ReasoningSummary::Detailed),
+14 -7
View File
@@ -8,6 +8,7 @@ use codex_models_manager::bundled_models_response;
use codex_models_manager::manager::RefreshStrategy;
use codex_models_manager::manager::SharedModelsManager;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ConfigShellToolType;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelPreset;
@@ -21,7 +22,6 @@ use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ExecCommandSource;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::user_input::UserInput;
use core_test_support::load_default_config_for_test;
use core_test_support::responses::ev_assistant_message;
@@ -37,6 +37,7 @@ use core_test_support::skip_if_no_network;
use core_test_support::skip_if_sandbox;
use core_test_support::test_codex::TestCodex;
use core_test_support::test_codex::test_codex;
use core_test_support::test_codex::turn_permission_fields;
use core_test_support::wait_for_event;
use core_test_support::wait_for_event_match;
use pretty_assertions::assert_eq;
@@ -627,6 +628,9 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> {
];
mount_sse_sequence(&server, responses).await;
let cwd_path = cwd.path().to_path_buf();
let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, cwd_path.as_path());
codex
.submit(Op::UserTurn {
items: vec![UserInput::Text {
@@ -634,11 +638,11 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> {
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
cwd: cwd_path,
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
sandbox_policy,
permission_profile,
model: REMOTE_MODEL_SLUG.to_string(),
effort: None,
summary: Some(ReasoningSummary::Auto),
@@ -855,6 +859,9 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
})
.await?;
let cwd_path = cwd.path().to_path_buf();
let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, cwd_path.as_path());
codex
.submit(Op::UserTurn {
items: vec![UserInput::Text {
@@ -862,11 +869,11 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
cwd: cwd_path,
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
sandbox_policy,
permission_profile,
model: model.to_string(),
effort: None,
summary: Some(ReasoningSummary::Auto),
@@ -6,11 +6,12 @@ use codex_core::config::Constrained;
use codex_features::Feature;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::request_permissions::PermissionGrantScope;
use codex_protocol::request_permissions::RequestPermissionProfile;
use codex_protocol::request_permissions::RequestPermissionsResponse;
@@ -28,6 +29,7 @@ use core_test_support::skip_if_no_network;
use core_test_support::skip_if_sandbox;
use core_test_support::test_codex::TestCodex;
use core_test_support::test_codex::test_codex;
use core_test_support::test_codex::turn_permission_fields;
use core_test_support::wait_for_event;
use pretty_assertions::assert_eq;
use regex_lite::Regex;
@@ -70,13 +72,13 @@ fn build_add_file_patch(patch_path: &Path, content: &str) -> String {
)
}
fn workspace_write_excluding_tmp() -> SandboxPolicy {
SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
}
fn workspace_write_excluding_tmp() -> PermissionProfile {
PermissionProfile::workspace_write_with(
&[],
NetworkSandboxPolicy::Restricted,
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ true,
)
}
fn requested_directory_write_permissions(path: &Path) -> RequestPermissionProfile {
@@ -133,10 +135,12 @@ async fn submit_turn(
test: &TestCodex,
prompt: &str,
approval_policy: AskForApproval,
sandbox_policy: SandboxPolicy,
permission_profile: PermissionProfile,
approvals_reviewer: Option<ApprovalsReviewer>,
) -> Result<()> {
let session_model = test.session_configured.model.clone();
let (sandbox_policy, permission_profile) =
turn_permission_fields(permission_profile, test.config.cwd.as_path());
test.codex
.submit(Op::UserTurn {
environments: None,
@@ -149,7 +153,7 @@ async fn submit_turn(
approval_policy,
approvals_reviewer,
sandbox_policy,
permission_profile: None,
permission_profile,
model: session_model,
effort: None,
summary: None,
@@ -199,14 +203,15 @@ async fn approved_folder_write_request_permissions_unblocks_later_exec_without_s
let server = start_mock_server().await;
let approval_policy = AskForApproval::OnRequest;
let sandbox_policy = workspace_write_excluding_tmp();
let sandbox_policy_for_config = sandbox_policy.clone();
let permission_profile = workspace_write_excluding_tmp();
let permission_profile_for_config = permission_profile.clone();
let mut builder = test_codex().with_config(move |config| {
config.permissions.approval_policy = Constrained::allow_any(approval_policy);
config
.set_legacy_sandbox_policy(sandbox_policy_for_config)
.expect("set sandbox policy");
.permissions
.set_permission_profile(permission_profile_for_config)
.expect("set permission profile");
config
.features
.enable(Feature::ExecPermissionApprovals)
@@ -258,7 +263,7 @@ async fn approved_folder_write_request_permissions_unblocks_later_exec_without_s
&test,
"write outside the workspace",
approval_policy,
sandbox_policy,
permission_profile,
/*approvals_reviewer*/ None,
)
.await?;
@@ -331,14 +336,15 @@ async fn approved_folder_write_request_permissions_unblocks_later_apply_patch()
async fn apply_patch_after_request_permissions(strict_auto_review: bool) -> Result<()> {
let server = start_mock_server().await;
let approval_policy = AskForApproval::OnRequest;
let sandbox_policy = workspace_write_excluding_tmp();
let sandbox_policy_for_config = sandbox_policy.clone();
let permission_profile = workspace_write_excluding_tmp();
let permission_profile_for_config = permission_profile.clone();
let mut builder = test_codex().with_config(move |config| {
config.permissions.approval_policy = Constrained::allow_any(approval_policy);
config
.set_legacy_sandbox_policy(sandbox_policy_for_config)
.expect("set sandbox policy");
.permissions
.set_permission_profile(permission_profile_for_config)
.expect("set permission profile");
config
.features
.enable(Feature::ExecPermissionApprovals)
@@ -415,7 +421,7 @@ async fn apply_patch_after_request_permissions(strict_auto_review: bool) -> Resu
&test,
"patch outside the workspace",
approval_policy,
sandbox_policy,
permission_profile,
strict_auto_review.then_some(ApprovalsReviewer::User),
)
.await?;
@@ -4,10 +4,10 @@
use anyhow::Result;
use anyhow::anyhow;
use codex_features::Feature;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::user_input::UserInput;
use core_test_support::responses::ResponseMock;
use core_test_support::responses::ResponsesRequest;
@@ -21,6 +21,7 @@ use core_test_support::responses::start_mock_server;
use core_test_support::skip_if_no_network;
use core_test_support::test_codex::TestCodex;
use core_test_support::test_codex::test_codex;
use core_test_support::test_codex::turn_permission_fields;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::time::Duration;
@@ -125,6 +126,9 @@ async fn responses_api_parent_and_subagent_requests_include_identity_headers() -
async fn submit_turn_with_timeout(test: &TestCodex, prompt: &str) -> Result<()> {
let session_model = test.session_configured.model.clone();
let cwd = test.config.cwd.to_path_buf();
let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::workspace_write(), cwd.as_path());
test.codex
.submit(Op::UserTurn {
environments: None,
@@ -133,16 +137,11 @@ async fn submit_turn_with_timeout(test: &TestCodex, prompt: &str) -> Result<()>
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.config.cwd.to_path_buf(),
cwd,
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
},
permission_profile: None,
sandbox_policy,
permission_profile,
model: session_model,
effort: None,
summary: None,
+72 -250
View File
@@ -29,6 +29,7 @@ use codex_mcp::MCP_SANDBOX_STATE_META_CAPABILITY;
use codex_models_manager::manager::RefreshStrategy;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ConfigShellToolType;
use codex_protocol::openai_models::InputModality;
use codex_protocol::openai_models::ModelInfo;
@@ -41,7 +42,6 @@ use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::McpInvocation;
use codex_protocol::protocol::McpToolCallBeginEvent;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::user_input::UserInput;
use codex_utils_cargo_bin::cargo_bin;
use core_test_support::assert_regex_match;
@@ -53,6 +53,7 @@ use core_test_support::skip_if_no_network;
use core_test_support::stdio_server_bin;
use core_test_support::test_codex::TestCodex;
use core_test_support::test_codex::test_codex;
use core_test_support::test_codex::turn_permission_fields;
use core_test_support::wait_for_event;
use core_test_support::wait_for_event_with_timeout;
use reqwest::Client;
@@ -92,6 +93,39 @@ fn assert_wall_time_header(output: &str) {
assert_eq!(marker, "Output:");
}
fn read_only_user_turn(fixture: &TestCodex, text: impl Into<String>) -> Op {
read_only_user_turn_with_model(fixture, text, fixture.session_configured.model.clone())
}
fn read_only_user_turn_with_model(
fixture: &TestCodex,
text: impl Into<String>,
model: String,
) -> Op {
let cwd = fixture.cwd.path().to_path_buf();
let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::read_only(), cwd.as_path());
Op::UserTurn {
items: vec![UserInput::Text {
text: text.into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd,
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy,
permission_profile,
model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
environments: None,
}
}
#[derive(Debug, PartialEq, Eq)]
enum McpCallEvent {
Begin(String),
@@ -321,28 +355,9 @@ async fn call_cwd_tool(
)
.await;
let session_model = fixture.session_configured.model.clone();
fixture
.codex
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "call the rmcp cwd tool".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: fixture.config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: session_model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
environments: None,
})
.submit(read_only_user_turn(fixture, "call the rmcp cwd tool"))
.await?;
wait_for_event(&fixture.codex, |ev| {
@@ -455,29 +470,9 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> {
})
.build_remote_aware(&server)
.await?;
let session_model = fixture.session_configured.model.clone();
fixture
.codex
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "call the rmcp echo tool".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: fixture.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: session_model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
environments: None,
})
.submit(read_only_user_turn(&fixture, "call the rmcp echo tool"))
.await?;
let begin_event = wait_for_event(&fixture.codex, |ev| {
@@ -797,9 +792,11 @@ async fn stdio_mcp_tool_call_includes_sandbox_state_meta() -> anyhow::Result<()>
sleep(Duration::from_millis(200)).await;
}
let sandbox_policy = SandboxPolicy::new_read_only_policy();
fixture
.submit_turn_with_policy("call the rmcp sandbox_meta tool", sandbox_policy.clone())
.submit_turn_with_permission_profile(
"call the rmcp sandbox_meta tool",
PermissionProfile::read_only(),
)
.await?;
let request = call_mock.single_request();
@@ -824,6 +821,8 @@ async fn stdio_mcp_tool_call_includes_sandbox_state_meta() -> anyhow::Result<()>
let sandbox_meta = meta
.get(MCP_SANDBOX_STATE_META_CAPABILITY)
.expect("sandbox state metadata should be present");
let (sandbox_policy, _) =
turn_permission_fields(PermissionProfile::read_only(), fixture.config.cwd.as_path());
let expected_sandbox_policy = serde_json::to_value(&sandbox_policy)?;
assert_eq!(
sandbox_meta.get("sandboxPolicy"),
@@ -888,29 +887,12 @@ async fn stdio_mcp_parallel_tool_calls_default_false_runs_serially() -> anyhow::
})
.build_remote_aware(&server)
.await?;
let session_model = fixture.session_configured.model.clone();
fixture
.codex
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "call the rmcp sync tool twice".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: fixture.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: session_model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
environments: None,
})
.submit(read_only_user_turn(
&fixture,
"call the rmcp sync tool twice",
))
.await?;
let mut call_events = Vec::new();
@@ -1022,29 +1004,12 @@ async fn stdio_mcp_parallel_tool_calls_opt_in_runs_concurrently() -> anyhow::Res
})
.build_remote_aware(&server)
.await?;
let session_model = fixture.session_configured.model.clone();
fixture
.codex
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "call the rmcp sync tool twice".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: fixture.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: session_model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
environments: None,
})
.submit(read_only_user_turn(
&fixture,
"call the rmcp sync tool twice",
))
.await?;
wait_for_event(&fixture.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -1121,31 +1086,11 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> {
})
.build_remote_aware(&server)
.await?;
let session_model = fixture.session_configured.model.clone();
wait_for_mcp_tool(&fixture, &tool_name).await?;
fixture
.codex
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "call the rmcp image tool".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: fixture.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: session_model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
environments: None,
})
.submit(read_only_user_turn(&fixture, "call the rmcp image tool"))
.await?;
// Wait for tool begin/end and final completion.
@@ -1274,31 +1219,14 @@ async fn stdio_image_responses_preserve_original_detail_metadata() -> anyhow::Re
})
.build_remote_aware(&server)
.await?;
let session_model = fixture.session_configured.model.clone();
wait_for_mcp_tool(&fixture, &tool_name).await?;
fixture
.codex
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "call the rmcp image_scenario tool".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: fixture.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: session_model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
environments: None,
})
.submit(read_only_user_turn(
&fixture,
"call the rmcp image_scenario tool",
))
.await?;
wait_for_event(&fixture.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -1435,25 +1363,11 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re
fixture
.codex
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "call the rmcp image tool".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: fixture.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: text_only_model_slug.to_string(),
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
environments: None,
})
.submit(read_only_user_turn_with_model(
&fixture,
"call the rmcp image tool",
text_only_model_slug.to_string(),
))
.await?;
wait_for_event(&fixture.codex, |ev| {
@@ -1541,29 +1455,9 @@ async fn stdio_server_propagates_whitelisted_env_vars() -> anyhow::Result<()> {
})
.build_remote_aware(&server)
.await?;
let session_model = fixture.session_configured.model.clone();
fixture
.codex
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "call the rmcp echo tool".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: fixture.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: session_model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
environments: None,
})
.submit(read_only_user_turn(&fixture, "call the rmcp echo tool"))
.await?;
let begin_event = wait_for_event(&fixture.codex, |ev| {
@@ -1680,28 +1574,9 @@ async fn stdio_server_propagates_explicit_local_env_var_source() -> anyhow::Resu
.build_remote_aware(&server)
.await?;
let session_model = fixture.session_configured.model.clone();
fixture
.codex
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "call the rmcp echo tool".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: fixture.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: session_model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
environments: None,
})
.submit(read_only_user_turn(&fixture, "call the rmcp echo tool"))
.await?;
wait_for_event(&fixture.codex, |ev| {
@@ -1791,28 +1666,9 @@ async fn remote_stdio_env_var_source_does_not_copy_local_env() -> anyhow::Result
.build_remote_aware(&server)
.await?;
let session_model = fixture.session_configured.model.clone();
fixture
.codex
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "call the rmcp echo tool".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: fixture.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: session_model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
environments: None,
})
.submit(read_only_user_turn(&fixture, "call the rmcp echo tool"))
.await?;
wait_for_event(&fixture.codex, |ev| {
@@ -1992,30 +1848,13 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> {
})
.build_remote_aware(&server)
.await?;
let session_model = fixture.session_configured.model.clone();
// Phase 4: submit the user turn that should trigger the MCP tool call.
fixture
.codex
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "call the rmcp streamable http echo tool".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: fixture.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: session_model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
environments: None,
})
.submit(read_only_user_turn(
&fixture,
"call the rmcp streamable http echo tool",
))
.await?;
// Phase 5: assert Codex begins the expected tool invocation.
@@ -2196,8 +2035,6 @@ async fn streamable_http_with_oauth_round_trip_impl() -> anyhow::Result<()> {
})
.build_remote_aware(&server)
.await?;
let session_model = fixture.session_configured.model.clone();
// Phase 5: wait for MCP discovery to publish the expected tool before the
// turn is submitted, which keeps failures tied to server startup/discovery.
wait_for_mcp_tool(&fixture, &tool_name).await?;
@@ -2205,25 +2042,10 @@ async fn streamable_http_with_oauth_round_trip_impl() -> anyhow::Result<()> {
// Phase 6: submit the user turn that should invoke the OAuth-backed tool.
fixture
.codex
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "call the rmcp streamable http oauth echo tool".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: fixture.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
model: session_model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
environments: None,
})
.submit(read_only_user_turn(
&fixture,
"call the rmcp streamable http oauth echo tool",
))
.await?;
// Phase 7: assert Codex begins the expected tool invocation.
@@ -1,5 +1,6 @@
use anyhow::Result;
use codex_protocol::models::ContentItem;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::CodexErrorInfo;
@@ -7,7 +8,6 @@ use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ModelRerouteReason;
use codex_protocol::protocol::ModelVerification;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::user_input::UserInput;
use core_test_support::responses::ev_assistant_message;
use core_test_support::responses::ev_function_call;
@@ -20,7 +20,9 @@ use core_test_support::responses::sse_completed;
use core_test_support::responses::sse_response;
use core_test_support::responses::start_mock_server;
use core_test_support::skip_if_no_network;
use core_test_support::test_codex::TestCodex;
use core_test_support::test_codex::test_codex;
use core_test_support::test_codex::turn_permission_fields;
use core_test_support::wait_for_event;
use pretty_assertions::assert_eq;
use wiremock::ResponseTemplate;
@@ -32,6 +34,30 @@ const TRUSTED_ACCESS_FOR_CYBER_VERIFICATION: &str = "trusted_access_for_cyber";
const CYBER_POLICY_MESSAGE: &str =
"This request has been flagged for potentially high-risk cyber activity.";
fn disabled_text_turn(test: &TestCodex, text: &str) -> Op {
let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, test.cwd_path());
Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: text.to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy,
permission_profile,
model: REQUESTED_MODEL.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn openai_model_header_mismatch_emits_warning_event_and_warning_item() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -45,25 +71,7 @@ async fn openai_model_header_mismatch_emits_warning_event_and_warning_item() ->
let test = builder.build(&server).await?;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "trigger safety check".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: REQUESTED_MODEL.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.submit(disabled_text_turn(&test, "trigger safety check"))
.await?;
let reroute = wait_for_event(&test.codex, |event| {
@@ -141,25 +149,7 @@ async fn cyber_policy_response_emits_typed_error_without_retry() -> Result<()> {
let test = builder.build(&server).await?;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "trigger cyber policy error".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: REQUESTED_MODEL.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.submit(disabled_text_turn(&test, "trigger cyber policy error"))
.await?;
let error = wait_for_event(&test.codex, |event| matches!(event, EventMsg::Error(_))).await;
@@ -198,25 +188,7 @@ async fn response_model_field_mismatch_emits_warning_when_header_matches_request
let test = builder.build(&server).await?;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "trigger response model check".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: REQUESTED_MODEL.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.submit(disabled_text_turn(&test, "trigger response model check"))
.await?;
let reroute = wait_for_event(&test.codex, |event| {
@@ -286,25 +258,7 @@ async fn openai_model_header_mismatch_only_emits_one_warning_per_turn() -> Resul
let test = builder.build(&server).await?;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "trigger follow-up turn".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: REQUESTED_MODEL.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.submit(disabled_text_turn(&test, "trigger follow-up turn"))
.await?;
let mut warning_count = 0;
@@ -338,25 +292,7 @@ async fn openai_model_header_casing_only_mismatch_does_not_warn() -> Result<()>
let test = builder.build(&server).await?;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "trigger casing check".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: REQUESTED_MODEL.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.submit(disabled_text_turn(&test, "trigger casing check"))
.await?;
let mut reroute_count = 0;
@@ -399,25 +335,7 @@ async fn model_verification_emits_structured_event_without_reroute_or_warning()
let test = builder.build(&server).await?;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "trigger model verification".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: REQUESTED_MODEL.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.submit(disabled_text_turn(&test, "trigger model verification"))
.await?;
let mut verification_count = 0;
@@ -493,25 +411,10 @@ async fn model_verification_only_emits_once_per_turn() -> Result<()> {
let test = builder.build(&server).await?;
test.codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
text: "trigger follow-up model verification".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: REQUESTED_MODEL.to_string(),
effort: test.config.model_reasoning_effort,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.submit(disabled_text_turn(
&test,
"trigger follow-up model verification",
))
.await?;
let mut verification_count = 0;
+13 -10
View File
@@ -2,21 +2,22 @@
#![cfg(unix)]
use anyhow::Result;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ExecApprovalRequestEvent;
use codex_protocol::protocol::GranularApprovalConfig;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::user_input::UserInput;
use core_test_support::responses::mount_function_call_agent_response;
use core_test_support::responses::start_mock_server;
use core_test_support::skip_if_no_network;
use core_test_support::test_codex::TestCodex;
use core_test_support::test_codex::turn_permission_fields;
use core_test_support::wait_for_event;
use core_test_support::wait_for_event_match;
use core_test_support::zsh_fork::build_zsh_fork_test;
use core_test_support::zsh_fork::restrictive_workspace_write_policy;
use core_test_support::zsh_fork::restrictive_workspace_write_profile;
use core_test_support::zsh_fork::zsh_fork_runtime;
use std::fs;
use std::path::Path;
@@ -40,8 +41,10 @@ async fn submit_turn_with_policies(
test: &TestCodex,
prompt: &str,
approval_policy: AskForApproval,
sandbox_policy: SandboxPolicy,
permission_profile: PermissionProfile,
) -> Result<()> {
let (sandbox_policy, permission_profile) =
turn_permission_fields(permission_profile, test.cwd_path());
test.codex
.submit(Op::UserTurn {
environments: None,
@@ -54,7 +57,7 @@ async fn submit_turn_with_policies(
approval_policy,
approvals_reviewer: None,
sandbox_policy,
permission_profile: None,
permission_profile,
model: test.session_configured.model.clone(),
effort: None,
summary: None,
@@ -144,7 +147,7 @@ async fn shell_zsh_fork_skill_scripts_ignore_declared_permissions() -> Result<()
request_permissions: true,
mcp_elicitations: true,
});
let workspace_write_policy = restrictive_workspace_write_policy();
let workspace_write_profile = restrictive_workspace_write_profile();
let outside_dir = tempfile::tempdir_in(std::env::current_dir()?)?;
let allowed_dir = outside_dir.path().join("allowed-output");
fs::create_dir_all(&allowed_dir)?;
@@ -165,7 +168,7 @@ async fn shell_zsh_fork_skill_scripts_ignore_declared_permissions() -> Result<()
&server,
runtime,
approval_policy,
workspace_write_policy.clone(),
workspace_write_profile.clone(),
move |home| {
let _ = fs::remove_file(&allowed_path_for_hook);
write_skill_with_shell_script_contents(
@@ -190,7 +193,7 @@ async fn shell_zsh_fork_skill_scripts_ignore_declared_permissions() -> Result<()
&test,
"use $mbolin-test-skill",
approval_policy,
workspace_write_policy,
workspace_write_profile,
)
.await?;
@@ -235,13 +238,13 @@ async fn shell_zsh_fork_still_enforces_workspace_write_sandbox() -> Result<()> {
let server = start_mock_server().await;
let tool_call_id = "zsh-fork-workspace-write-deny";
let outside_path = "/tmp/codex-zsh-fork-workspace-write-deny.txt";
let workspace_write_policy = restrictive_workspace_write_policy();
let workspace_write_profile = restrictive_workspace_write_profile();
let _ = fs::remove_file(outside_path);
let test = build_zsh_fork_test(
&server,
runtime,
AskForApproval::Never,
workspace_write_policy.clone(),
workspace_write_profile.clone(),
move |_| {
let _ = fs::remove_file(outside_path);
},
@@ -258,7 +261,7 @@ async fn shell_zsh_fork_still_enforces_workspace_write_sandbox() -> Result<()> {
&test,
"write outside workspace with zsh fork",
AskForApproval::Never,
workspace_write_policy,
workspace_write_profile,
)
.await?;
+7 -11
View File
@@ -9,7 +9,6 @@ use std::time::Instant;
use anyhow::Context;
use anyhow::Result;
use codex_config::Constrained;
use codex_config::types::McpServerConfig;
use codex_config::types::McpServerTransportConfig;
use codex_core::sandboxing::SandboxPermissions;
@@ -21,7 +20,6 @@ use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::TurnEnvironmentSelection;
use core_test_support::assert_regex_match;
use core_test_support::responses::ev_assistant_message;
@@ -547,14 +545,9 @@ async fn shell_enforces_glob_deny_read_policy() -> Result<()> {
skip_if_sandbox!(Ok(()));
let server = start_mock_server().await;
let read_only_policy = SandboxPolicy::new_read_only_policy();
let read_only_policy_for_config = read_only_policy.clone();
let mut builder = test_codex()
.with_model("gpt-5.4")
.with_config(move |config| {
config
.set_legacy_sandbox_policy(read_only_policy_for_config)
.expect("set sandbox policy");
let mut file_system_sandbox_policy = FileSystemSandboxPolicy::default();
file_system_sandbox_policy
.entries
@@ -564,11 +557,13 @@ async fn shell_enforces_glob_deny_read_policy() -> Result<()> {
},
access: FileSystemAccessMode::None,
});
config.permissions.permission_profile =
Constrained::allow_any(PermissionProfile::from_runtime_permissions(
config
.permissions
.set_permission_profile(PermissionProfile::from_runtime_permissions(
&file_system_sandbox_policy,
NetworkSandboxPolicy::Restricted,
));
))
.expect("set permission profile");
});
let fixture = builder.build(&server).await?;
@@ -608,8 +603,9 @@ async fn shell_enforces_glob_deny_read_policy() -> Result<()> {
];
let mock = mount_sse_sequence(&server, responses).await;
let permission_profile = fixture.session_configured.permission_profile.clone();
fixture
.submit_turn_with_policy("read the fixture files", read_only_policy)
.submit_turn_with_permission_profile("read the fixture files", permission_profile)
.await?;
let output_text = mock
+84 -203
View File
@@ -6,6 +6,7 @@ use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use codex_exec_server::CreateDirectoryOptions;
use codex_login::CodexAuth;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ConfigShellToolType;
use codex_protocol::openai_models::InputModality;
use codex_protocol::openai_models::ModelInfo;
@@ -17,7 +18,6 @@ use codex_protocol::openai_models::TruncationPolicyConfig;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::user_input::UserInput;
use core_test_support::responses;
use core_test_support::responses::ev_assistant_message;
@@ -30,6 +30,7 @@ use core_test_support::responses::start_mock_server;
use core_test_support::skip_if_no_network;
use core_test_support::test_codex::TestCodex;
use core_test_support::test_codex::test_codex;
use core_test_support::test_codex::turn_permission_fields;
use core_test_support::wait_for_event_with_timeout;
use image::DynamicImage;
use image::GenericImageView;
@@ -50,6 +51,27 @@ use wiremock::matchers::body_string_contains;
const VIEW_IMAGE_TURN_COMPLETE_TIMEOUT: Duration = Duration::from_secs(30);
fn disabled_user_turn(test: &TestCodex, items: Vec<UserInput>, model: String) -> Op {
let (sandbox_policy, permission_profile) =
turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path());
Op::UserTurn {
environments: None,
items,
final_output_json_schema: None,
cwd: test.config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy,
permission_profile,
model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
}
}
fn image_messages(body: &Value) -> Vec<&Value> {
body.get("input")
.and_then(Value::as_array)
@@ -137,7 +159,6 @@ async fn assert_user_turn_local_image_resizes_to(
let test = builder.build_remote_aware(&server).await?;
let TestCodex {
codex,
config,
session_configured,
..
} = &test;
@@ -158,24 +179,13 @@ async fn assert_user_turn_local_image_resizes_to(
let session_model = session_configured.model.clone();
codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::LocalImage {
.submit(disabled_user_turn(
&test,
vec![UserInput::LocalImage {
path: abs_path.clone(),
}],
final_output_json_schema: None,
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
session_model,
))
.await?;
wait_for_event_with_timeout(
@@ -279,25 +289,14 @@ async fn view_image_tool_attaches_local_image() -> anyhow::Result<()> {
let session_model = session_configured.model.clone();
codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
.submit(disabled_user_turn(
&test,
vec![UserInput::Text {
text: "please add the screenshot".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
session_model,
))
.await?;
let mut tool_event = None;
@@ -376,7 +375,6 @@ async fn view_image_tool_can_preserve_original_resolution_when_requested_on_gpt5
let test = builder.build_remote_aware(&server).await?;
let TestCodex {
codex,
config,
session_configured,
..
} = &test;
@@ -412,25 +410,14 @@ async fn view_image_tool_can_preserve_original_resolution_when_requested_on_gpt5
let session_model = session_configured.model.clone();
codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
.submit(disabled_user_turn(
&test,
vec![UserInput::Text {
text: "please add the original screenshot".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
service_tier: None,
summary: None,
collaboration_mode: None,
personality: None,
})
session_model,
))
.await?;
wait_for_event_with_timeout(
@@ -479,7 +466,6 @@ async fn view_image_tool_errors_clearly_for_unsupported_detail_values() -> anyho
let test = builder.build_remote_aware(&server).await?;
let TestCodex {
codex,
config,
session_configured,
..
} = &test;
@@ -513,25 +499,14 @@ async fn view_image_tool_errors_clearly_for_unsupported_detail_values() -> anyho
let session_model = session_configured.model.clone();
codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
.submit(disabled_user_turn(
&test,
vec![UserInput::Text {
text: "please attach the image at low detail".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
service_tier: None,
summary: None,
collaboration_mode: None,
personality: None,
})
session_model,
))
.await?;
wait_for_event_with_timeout(
@@ -569,7 +544,6 @@ async fn view_image_tool_treats_null_detail_as_omitted() -> anyhow::Result<()> {
let test = builder.build_remote_aware(&server).await?;
let TestCodex {
codex,
config,
session_configured,
..
} = &test;
@@ -605,25 +579,14 @@ async fn view_image_tool_treats_null_detail_as_omitted() -> anyhow::Result<()> {
let session_model = session_configured.model.clone();
codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
.submit(disabled_user_turn(
&test,
vec![UserInput::Text {
text: "please attach the image with a null detail".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
service_tier: None,
summary: None,
collaboration_mode: None,
personality: None,
})
session_model,
))
.await?;
wait_for_event_with_timeout(
@@ -671,7 +634,6 @@ async fn view_image_tool_resizes_when_model_lacks_original_detail_support() -> a
let test = builder.build_remote_aware(&server).await?;
let TestCodex {
codex,
config,
session_configured,
..
} = &test;
@@ -707,25 +669,14 @@ async fn view_image_tool_resizes_when_model_lacks_original_detail_support() -> a
let session_model = session_configured.model.clone();
codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
.submit(disabled_user_turn(
&test,
vec![UserInput::Text {
text: "please add the screenshot".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
service_tier: None,
summary: None,
collaboration_mode: None,
personality: None,
})
session_model,
))
.await?;
wait_for_event_with_timeout(
@@ -777,7 +728,6 @@ async fn view_image_tool_does_not_force_original_resolution_with_capability_only
let test = builder.build_remote_aware(&server).await?;
let TestCodex {
codex,
config,
session_configured,
..
} = &test;
@@ -813,25 +763,14 @@ async fn view_image_tool_does_not_force_original_resolution_with_capability_only
let session_model = session_configured.model.clone();
codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
.submit(disabled_user_turn(
&test,
vec![UserInput::Text {
text: "please add the screenshot".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
service_tier: None,
summary: None,
collaboration_mode: None,
personality: None,
})
session_model,
))
.await?;
wait_for_event_with_timeout(
@@ -880,7 +819,6 @@ async fn view_image_tool_errors_when_path_is_directory() -> anyhow::Result<()> {
let test = builder.build_remote_aware(&server).await?;
let TestCodex {
codex,
config,
session_configured,
..
} = &test;
@@ -907,25 +845,14 @@ async fn view_image_tool_errors_when_path_is_directory() -> anyhow::Result<()> {
let session_model = session_configured.model.clone();
codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
.submit(disabled_user_turn(
&test,
vec![UserInput::Text {
text: "please attach the folder".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
session_model,
))
.await?;
wait_for_event_with_timeout(
@@ -962,7 +889,6 @@ async fn view_image_tool_errors_for_non_image_files() -> anyhow::Result<()> {
let test = builder.build_remote_aware(&server).await?;
let TestCodex {
codex,
config,
session_configured,
..
} = &test;
@@ -990,25 +916,14 @@ async fn view_image_tool_errors_for_non_image_files() -> anyhow::Result<()> {
let session_model = session_configured.model.clone();
codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
.submit(disabled_user_turn(
&test,
vec![UserInput::Text {
text: "please use the view_image tool to read the json file".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
session_model,
))
.await?;
wait_for_event_with_timeout(
@@ -1078,25 +993,14 @@ async fn view_image_tool_errors_when_file_missing() -> anyhow::Result<()> {
let session_model = session_configured.model.clone();
codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
.submit(disabled_user_turn(
&test,
vec![UserInput::Text {
text: "please attach the missing image".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
session_model,
))
.await?;
wait_for_event_with_timeout(
@@ -1189,7 +1093,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an
config.model = Some(model_slug.to_string());
});
let test = builder.build_remote_aware(&server).await?;
let TestCodex { codex, config, .. } = &test;
let TestCodex { codex, .. } = &test;
let rel_path = "assets/example.png";
write_workspace_png(
@@ -1217,25 +1121,14 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an
let mock = responses::mount_sse_once(&server, second_response).await;
codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::Text {
.submit(disabled_user_turn(
&test,
vec![UserInput::Text {
text: "please attach the image".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: model_slug.to_string(),
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
model_slug.to_string(),
))
.await?;
wait_for_event_with_timeout(
@@ -1289,7 +1182,6 @@ async fn replaces_invalid_local_image_after_bad_request() -> anyhow::Result<()>
let test = builder.build_remote_aware(&server).await?;
let TestCodex {
codex,
config,
session_configured,
..
} = &test;
@@ -1300,24 +1192,13 @@ async fn replaces_invalid_local_image_after_bad_request() -> anyhow::Result<()>
let session_model = session_configured.model.clone();
codex
.submit(Op::UserTurn {
environments: None,
items: vec![UserInput::LocalImage {
.submit(disabled_user_turn(
&test,
vec![UserInput::LocalImage {
path: abs_path.clone(),
}],
final_output_json_schema: None,
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
model: session_model,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
session_model,
))
.await?;
wait_for_event_with_timeout(