mirror of
https://github.com/openai/codex.git
synced 2026-05-23 04:24:21 +00:00
Compare commits
3 Commits
jif/rename
...
dh--rules-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9662f2e47 | ||
|
|
f47cbee1d7 | ||
|
|
414cad772c |
@@ -1216,6 +1216,25 @@ impl ConfigBuilder {
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Update the effective cwd while keeping the implicit runtime workspace
|
||||
/// root attached to it. Explicit runtime roots remain stable.
|
||||
pub fn set_cwd_retargeting_implicit_workspace_root(&mut self, cwd: AbsolutePathBuf) {
|
||||
let previous_cwd = std::mem::replace(&mut self.cwd, cwd.clone());
|
||||
if self.workspace_roots_explicit || !self.workspace_roots.contains(&previous_cwd) {
|
||||
return;
|
||||
}
|
||||
|
||||
let previous_workspace_roots = std::mem::take(&mut self.workspace_roots);
|
||||
self.workspace_roots.push(cwd);
|
||||
for root in previous_workspace_roots {
|
||||
if root != previous_cwd && !self.workspace_roots.contains(&root) {
|
||||
self.workspace_roots.push(root);
|
||||
}
|
||||
}
|
||||
self.permissions
|
||||
.set_workspace_roots(self.workspace_roots.clone());
|
||||
}
|
||||
|
||||
pub fn legacy_sandbox_policy(&self) -> SandboxPolicy {
|
||||
self.permissions.legacy_sandbox_policy(self.cwd.as_path())
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ pub(crate) struct EnvironmentContext {
|
||||
pub(crate) timezone: Option<String>,
|
||||
pub(crate) network: Option<NetworkContext>,
|
||||
pub(crate) subagents: Option<String>,
|
||||
pub(crate) approved_command_prefixes: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -130,6 +131,7 @@ impl EnvironmentContext {
|
||||
timezone,
|
||||
network,
|
||||
subagents,
|
||||
approved_command_prefixes: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +148,7 @@ impl EnvironmentContext {
|
||||
timezone,
|
||||
network,
|
||||
subagents,
|
||||
approved_command_prefixes: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,6 +161,7 @@ impl EnvironmentContext {
|
||||
&& self.timezone == other.timezone
|
||||
&& self.network == other.network
|
||||
&& self.subagents == other.subagents
|
||||
&& self.approved_command_prefixes == other.approved_command_prefixes
|
||||
}
|
||||
|
||||
pub(crate) fn diff_from_turn_context_item(
|
||||
@@ -232,6 +236,11 @@ impl EnvironmentContext {
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_approved_command_prefixes(mut self, prefixes: Option<String>) -> Self {
|
||||
self.approved_command_prefixes = prefixes.filter(|prefixes| !prefixes.is_empty());
|
||||
self
|
||||
}
|
||||
|
||||
fn network_from_turn_context(turn_context: &TurnContext) -> Option<NetworkContext> {
|
||||
let network = turn_context
|
||||
.config
|
||||
@@ -318,6 +327,11 @@ impl ContextualUserFragment for EnvironmentContext {
|
||||
lines.extend(subagents.lines().map(|line| format!(" {line}")));
|
||||
lines.push(" </subagents>".to_string());
|
||||
}
|
||||
if let Some(prefixes) = &self.approved_command_prefixes {
|
||||
lines.push(" <approved_command_prefixes>".to_string());
|
||||
lines.extend(prefixes.lines().map(|line| format!(" {line}")));
|
||||
lines.push(" </approved_command_prefixes>".to_string());
|
||||
}
|
||||
format!("\n{}\n", lines.join("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,6 +211,37 @@ fn serialize_environment_context_with_subagents() {
|
||||
assert_eq!(context.render(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_environment_context_with_approved_command_prefixes() {
|
||||
let context = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: test_path_buf("/repo").abs(),
|
||||
shell: fake_shell_name(),
|
||||
}],
|
||||
Some("2026-02-26".to_string()),
|
||||
Some("America/Los_Angeles".to_string()),
|
||||
/*network*/ None,
|
||||
/*subagents*/ None,
|
||||
)
|
||||
.with_approved_command_prefixes(Some("- [\"git\", \"pull\"]".to_string()));
|
||||
|
||||
let expected = format!(
|
||||
r#"<environment_context>
|
||||
<cwd>{}</cwd>
|
||||
<shell>bash</shell>
|
||||
<current_date>2026-02-26</current_date>
|
||||
<timezone>America/Los_Angeles</timezone>
|
||||
<approved_command_prefixes>
|
||||
- ["git", "pull"]
|
||||
</approved_command_prefixes>
|
||||
</environment_context>"#,
|
||||
test_path_buf("/repo").display()
|
||||
);
|
||||
|
||||
assert_eq!(context.render(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_environment_context_with_multiple_selected_environments() {
|
||||
let local_cwd = test_path_buf("/repo/local");
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use super::ContextualUserFragment;
|
||||
use codex_execpolicy::Policy;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::SandboxMode;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::models::format_allow_prefixes;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::GranularApprovalConfig;
|
||||
@@ -43,10 +41,9 @@ static SANDBOX_MODE_READ_ONLY_TEMPLATE: LazyLock<Template> = LazyLock::new(|| {
|
||||
.unwrap_or_else(|err| panic!("read-only sandbox template must parse: {err}"))
|
||||
});
|
||||
|
||||
struct PermissionsPromptConfig<'a> {
|
||||
struct PermissionsPromptConfig {
|
||||
approval_policy: AskForApproval,
|
||||
approvals_reviewer: ApprovalsReviewer,
|
||||
exec_policy: &'a Policy,
|
||||
exec_permission_approvals_enabled: bool,
|
||||
request_permissions_tool_enabled: bool,
|
||||
}
|
||||
@@ -63,7 +60,6 @@ impl PermissionsInstructions {
|
||||
permission_profile: &PermissionProfile,
|
||||
approval_policy: AskForApproval,
|
||||
approvals_reviewer: ApprovalsReviewer,
|
||||
exec_policy: &Policy,
|
||||
cwd: &Path,
|
||||
exec_permission_approvals_enabled: bool,
|
||||
request_permissions_tool_enabled: bool,
|
||||
@@ -76,7 +72,6 @@ impl PermissionsInstructions {
|
||||
PermissionsPromptConfig {
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
exec_policy,
|
||||
exec_permission_approvals_enabled,
|
||||
request_permissions_tool_enabled,
|
||||
},
|
||||
@@ -87,7 +82,7 @@ impl PermissionsInstructions {
|
||||
fn from_permissions_with_network(
|
||||
sandbox_mode: SandboxMode,
|
||||
network_access: NetworkAccess,
|
||||
config: PermissionsPromptConfig<'_>,
|
||||
config: PermissionsPromptConfig,
|
||||
writable_roots: Option<Vec<WritableRoot>>,
|
||||
) -> Self {
|
||||
let mut text = String::new();
|
||||
@@ -97,7 +92,6 @@ impl PermissionsInstructions {
|
||||
&approval_text(
|
||||
config.approval_policy,
|
||||
config.approvals_reviewer,
|
||||
config.exec_policy,
|
||||
config.exec_permission_approvals_enabled,
|
||||
config.request_permissions_tool_enabled,
|
||||
),
|
||||
@@ -164,7 +158,6 @@ fn append_section(text: &mut String, section: &str) {
|
||||
fn approval_text(
|
||||
approval_policy: AskForApproval,
|
||||
approvals_reviewer: ApprovalsReviewer,
|
||||
exec_policy: &Policy,
|
||||
exec_permission_approvals_enabled: bool,
|
||||
request_permissions_tool_enabled: bool,
|
||||
) -> String {
|
||||
@@ -185,11 +178,6 @@ fn approval_text(
|
||||
if request_permissions_tool_enabled {
|
||||
sections.push(request_permissions_tool_prompt_section().to_string());
|
||||
}
|
||||
if let Some(prefixes) = approved_command_prefixes_text(exec_policy) {
|
||||
sections.push(format!(
|
||||
"## Approved command prefixes\nThe following prefix rules have already been approved: {prefixes}"
|
||||
));
|
||||
}
|
||||
sections.join("\n\n")
|
||||
};
|
||||
let text = match approval_policy {
|
||||
@@ -201,7 +189,6 @@ fn approval_text(
|
||||
AskForApproval::OnRequest => on_request_instructions(),
|
||||
AskForApproval::Granular(granular_config) => granular_instructions(
|
||||
granular_config,
|
||||
exec_policy,
|
||||
exec_permission_approvals_enabled,
|
||||
request_permissions_tool_enabled,
|
||||
),
|
||||
@@ -246,11 +233,6 @@ fn writable_roots_text(writable_roots: Option<Vec<WritableRoot>>) -> Option<Stri
|
||||
})
|
||||
}
|
||||
|
||||
fn approved_command_prefixes_text(exec_policy: &Policy) -> Option<String> {
|
||||
format_allow_prefixes(exec_policy.get_allowed_prefixes())
|
||||
.filter(|prefixes| !prefixes.is_empty())
|
||||
}
|
||||
|
||||
fn granular_prompt_intro_text() -> &'static str {
|
||||
"# Approval Requests\n\nApproval policy is `granular`. Categories set to `false` are automatically rejected instead of prompting the user."
|
||||
}
|
||||
@@ -261,7 +243,6 @@ fn request_permissions_tool_prompt_section() -> &'static str {
|
||||
|
||||
fn granular_instructions(
|
||||
granular_config: GranularApprovalConfig,
|
||||
exec_policy: &Policy,
|
||||
exec_permission_approvals_enabled: bool,
|
||||
request_permissions_tool_enabled: bool,
|
||||
) -> String {
|
||||
@@ -322,12 +303,6 @@ fn granular_instructions(
|
||||
sections.push(request_permissions_tool_prompt_section().to_string());
|
||||
}
|
||||
|
||||
if let Some(prefixes) = approved_command_prefixes_text(exec_policy) {
|
||||
sections.push(format!(
|
||||
"## Approved command prefixes\nThe following prefix rules have already been approved: {prefixes}"
|
||||
));
|
||||
}
|
||||
|
||||
sections.join("\n\n")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use super::*;
|
||||
use codex_execpolicy::Decision;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
@@ -35,7 +34,6 @@ fn builds_permissions_with_network_access_override() {
|
||||
PermissionsPromptConfig {
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
exec_policy: &Policy::empty(),
|
||||
exec_permission_approvals_enabled: false,
|
||||
request_permissions_tool_enabled: false,
|
||||
},
|
||||
@@ -72,7 +70,6 @@ fn builds_permissions_from_profile() {
|
||||
&permission_profile,
|
||||
AskForApproval::UnlessTrusted,
|
||||
ApprovalsReviewer::User,
|
||||
&Policy::empty(),
|
||||
&cwd,
|
||||
/*exec_permission_approvals_enabled*/ false,
|
||||
/*request_permissions_tool_enabled*/ false,
|
||||
@@ -84,28 +81,16 @@ fn builds_permissions_from_profile() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn includes_request_rule_instructions_for_on_request() {
|
||||
let mut exec_policy = Policy::empty();
|
||||
exec_policy
|
||||
.add_prefix_rule(&["git".to_string(), "pull".to_string()], Decision::Allow)
|
||||
.expect("add rule");
|
||||
let instructions = PermissionsInstructions::from_permissions_with_network(
|
||||
SandboxMode::WorkspaceWrite,
|
||||
NetworkAccess::Enabled,
|
||||
PermissionsPromptConfig {
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
exec_policy: &exec_policy,
|
||||
exec_permission_approvals_enabled: false,
|
||||
request_permissions_tool_enabled: false,
|
||||
},
|
||||
/*writable_roots*/ None,
|
||||
fn on_request_approval_text_matches_prompt() {
|
||||
assert_eq!(
|
||||
approval_text(
|
||||
AskForApproval::OnRequest,
|
||||
ApprovalsReviewer::User,
|
||||
/*exec_permission_approvals_enabled*/ false,
|
||||
/*request_permissions_tool_enabled*/ false,
|
||||
),
|
||||
APPROVAL_POLICY_ON_REQUEST_RULE.to_string(),
|
||||
);
|
||||
|
||||
let text = instructions.body();
|
||||
assert!(text.contains("prefix_rule"));
|
||||
assert!(text.contains("Approved command prefixes"));
|
||||
assert!(text.contains(r#"["git", "pull"]"#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -116,7 +101,6 @@ fn includes_request_permissions_tool_instructions_for_unless_trusted_when_enable
|
||||
PermissionsPromptConfig {
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
exec_policy: &Policy::empty(),
|
||||
exec_permission_approvals_enabled: false,
|
||||
request_permissions_tool_enabled: true,
|
||||
},
|
||||
@@ -136,7 +120,6 @@ fn includes_request_permissions_tool_instructions_for_on_failure_when_enabled()
|
||||
PermissionsPromptConfig {
|
||||
approval_policy: AskForApproval::OnFailure,
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
exec_policy: &Policy::empty(),
|
||||
exec_permission_approvals_enabled: false,
|
||||
request_permissions_tool_enabled: true,
|
||||
},
|
||||
@@ -156,7 +139,6 @@ fn includes_request_permission_rule_instructions_for_on_request_when_enabled() {
|
||||
PermissionsPromptConfig {
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
exec_policy: &Policy::empty(),
|
||||
exec_permission_approvals_enabled: true,
|
||||
request_permissions_tool_enabled: false,
|
||||
},
|
||||
@@ -176,7 +158,6 @@ fn includes_request_permissions_tool_instructions_for_on_request_when_tool_is_en
|
||||
PermissionsPromptConfig {
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
exec_policy: &Policy::empty(),
|
||||
exec_permission_approvals_enabled: false,
|
||||
request_permissions_tool_enabled: true,
|
||||
},
|
||||
@@ -196,7 +177,6 @@ fn on_request_includes_tool_guidance_alongside_inline_permission_guidance_when_b
|
||||
PermissionsPromptConfig {
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
exec_policy: &Policy::empty(),
|
||||
exec_permission_approvals_enabled: true,
|
||||
request_permissions_tool_enabled: true,
|
||||
},
|
||||
@@ -213,7 +193,6 @@ fn auto_review_approvals_append_auto_review_specific_guidance() {
|
||||
let text = approval_text(
|
||||
AskForApproval::OnRequest,
|
||||
ApprovalsReviewer::AutoReview,
|
||||
&Policy::empty(),
|
||||
/*exec_permission_approvals_enabled*/ false,
|
||||
/*request_permissions_tool_enabled*/ false,
|
||||
);
|
||||
@@ -228,7 +207,6 @@ fn auto_review_approvals_omit_auto_review_specific_guidance_when_approval_is_nev
|
||||
let text = approval_text(
|
||||
AskForApproval::Never,
|
||||
ApprovalsReviewer::AutoReview,
|
||||
&Policy::empty(),
|
||||
/*exec_permission_approvals_enabled*/ false,
|
||||
/*request_permissions_tool_enabled*/ false,
|
||||
);
|
||||
@@ -280,7 +258,6 @@ fn granular_policy_lists_prompted_and_rejected_categories_separately() {
|
||||
mcp_elicitations: false,
|
||||
}),
|
||||
ApprovalsReviewer::User,
|
||||
&Policy::empty(),
|
||||
/*exec_permission_approvals_enabled*/ true,
|
||||
/*request_permissions_tool_enabled*/ false,
|
||||
);
|
||||
@@ -317,7 +294,6 @@ fn granular_policy_includes_command_permission_instructions_when_sandbox_approva
|
||||
mcp_elicitations: true,
|
||||
}),
|
||||
ApprovalsReviewer::User,
|
||||
&Policy::empty(),
|
||||
/*exec_permission_approvals_enabled*/ true,
|
||||
/*request_permissions_tool_enabled*/ false,
|
||||
);
|
||||
@@ -349,7 +325,6 @@ fn granular_policy_omits_shell_permission_instructions_when_inline_requests_are_
|
||||
mcp_elicitations: true,
|
||||
}),
|
||||
ApprovalsReviewer::User,
|
||||
&Policy::empty(),
|
||||
/*exec_permission_approvals_enabled*/ false,
|
||||
/*request_permissions_tool_enabled*/ false,
|
||||
);
|
||||
@@ -381,7 +356,6 @@ fn granular_policy_includes_request_permissions_tool_only_when_that_prompt_can_s
|
||||
mcp_elicitations: true,
|
||||
}),
|
||||
ApprovalsReviewer::User,
|
||||
&Policy::empty(),
|
||||
/*exec_permission_approvals_enabled*/ true,
|
||||
/*request_permissions_tool_enabled*/ true,
|
||||
);
|
||||
@@ -396,7 +370,6 @@ fn granular_policy_includes_request_permissions_tool_only_when_that_prompt_can_s
|
||||
mcp_elicitations: true,
|
||||
}),
|
||||
ApprovalsReviewer::User,
|
||||
&Policy::empty(),
|
||||
/*exec_permission_approvals_enabled*/ true,
|
||||
/*request_permissions_tool_enabled*/ true,
|
||||
);
|
||||
@@ -414,7 +387,6 @@ fn granular_policy_lists_request_permissions_category_without_tool_section_when_
|
||||
mcp_elicitations: false,
|
||||
}),
|
||||
ApprovalsReviewer::User,
|
||||
&Policy::empty(),
|
||||
/*exec_permission_approvals_enabled*/ true,
|
||||
/*request_permissions_tool_enabled*/ false,
|
||||
);
|
||||
|
||||
@@ -10,7 +10,6 @@ use crate::context::RealtimeStartWithInstructions;
|
||||
use crate::session::PreviousTurnSettings;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::shell::Shell;
|
||||
use codex_execpolicy::Policy;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::models::ContentItem;
|
||||
@@ -42,7 +41,6 @@ fn build_environment_update_item(
|
||||
fn build_permissions_update_item(
|
||||
previous: Option<&TurnContextItem>,
|
||||
next: &TurnContext,
|
||||
exec_policy: &Policy,
|
||||
) -> Option<String> {
|
||||
if !next.config.include_permissions_instructions {
|
||||
return None;
|
||||
@@ -60,7 +58,6 @@ fn build_permissions_update_item(
|
||||
&next.permission_profile,
|
||||
next.approval_policy.value(),
|
||||
next.config.approvals_reviewer,
|
||||
exec_policy,
|
||||
#[allow(deprecated)]
|
||||
&next.cwd,
|
||||
next.features.enabled(Feature::ExecPermissionApprovals),
|
||||
@@ -211,7 +208,6 @@ pub(crate) fn build_settings_update_items(
|
||||
previous_turn_settings: Option<&PreviousTurnSettings>,
|
||||
next: &TurnContext,
|
||||
shell: &Shell,
|
||||
exec_policy: &Policy,
|
||||
personality_feature_enabled: bool,
|
||||
) -> Vec<ResponseItem> {
|
||||
// TODO(ccunningham): build_settings_update_items still does not cover every
|
||||
@@ -223,7 +219,7 @@ pub(crate) fn build_settings_update_items(
|
||||
// Keep model-switch instructions first so model-specific guidance is read before
|
||||
// any other context diffs on this turn.
|
||||
build_model_instructions_update_item(previous_turn_settings, next),
|
||||
build_permissions_update_item(previous, next, exec_policy),
|
||||
build_permissions_update_item(previous, next),
|
||||
build_collaboration_mode_update_item(previous, next),
|
||||
build_realtime_update_item(previous, previous_turn_settings, next),
|
||||
build_personality_update_item(previous, next, personality_feature_enabled),
|
||||
|
||||
@@ -66,6 +66,22 @@ fn test_writable_roots_constraint() {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_permission_profile_allows_absolute_patch_inside_cwd() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let cwd = tmp.path().abs();
|
||||
let inside_path = cwd.join("inside.txt");
|
||||
let action = ApplyPatchAction::new_add_for_test(&inside_path, "".to_string());
|
||||
let permission_profile = PermissionProfile::workspace_write();
|
||||
let file_system_sandbox_policy = permission_profile.file_system_sandbox_policy();
|
||||
|
||||
assert!(is_write_patch_constrained_to_writable_paths(
|
||||
&action,
|
||||
&file_system_sandbox_policy,
|
||||
&cwd,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_sandbox_auto_approves_in_on_request() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
@@ -1565,13 +1565,11 @@ impl Session {
|
||||
state.previous_turn_settings()
|
||||
};
|
||||
let shell = self.user_shell();
|
||||
let exec_policy = self.services.exec_policy.current();
|
||||
crate::context_manager::updates::build_settings_update_items(
|
||||
reference_context_item,
|
||||
previous_turn_settings.as_ref(),
|
||||
current_context,
|
||||
shell.as_ref(),
|
||||
exec_policy.as_ref(),
|
||||
self.features.enabled(Feature::Personality),
|
||||
)
|
||||
}
|
||||
@@ -2667,7 +2665,6 @@ impl Session {
|
||||
&turn_context.permission_profile,
|
||||
turn_context.approval_policy.value(),
|
||||
turn_context.config.approvals_reviewer,
|
||||
self.services.exec_policy.current().as_ref(),
|
||||
#[allow(deprecated)]
|
||||
&turn_context.cwd,
|
||||
turn_context
|
||||
@@ -2807,9 +2804,16 @@ impl Session {
|
||||
.agent_control
|
||||
.format_environment_context_subagents(self.conversation_id)
|
||||
.await;
|
||||
let approved_command_prefixes = if turn_context.config.include_permissions_instructions
|
||||
{
|
||||
format_allow_prefixes(self.services.exec_policy.current().get_allowed_prefixes())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
contextual_user_sections.push(
|
||||
crate::context::EnvironmentContext::from_turn_context(turn_context, shell.as_ref())
|
||||
.with_subagents(subagents)
|
||||
.with_approved_command_prefixes(approved_command_prefixes)
|
||||
.render(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -270,7 +270,7 @@ pub(crate) fn apply_spawn_agent_runtime_overrides(
|
||||
config.codex_linux_sandbox_exe = turn.codex_linux_sandbox_exe.clone();
|
||||
#[allow(deprecated)]
|
||||
let turn_cwd = turn.cwd.clone();
|
||||
config.cwd = turn_cwd;
|
||||
config.set_cwd_retargeting_implicit_workspace_root(turn_cwd);
|
||||
config
|
||||
.permissions
|
||||
.set_permission_profile(turn.permission_profile())
|
||||
|
||||
@@ -3982,7 +3982,7 @@ async fn build_agent_spawn_config_uses_turn_context_values() {
|
||||
expected.codex_linux_sandbox_exe = turn.codex_linux_sandbox_exe.clone();
|
||||
#[allow(deprecated)]
|
||||
{
|
||||
expected.cwd = turn.cwd.clone();
|
||||
expected.set_cwd_retargeting_implicit_workspace_root(turn.cwd.clone());
|
||||
}
|
||||
expected
|
||||
.permissions
|
||||
@@ -3996,6 +3996,24 @@ async fn build_agent_spawn_config_uses_turn_context_values() {
|
||||
assert_eq!(config, expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_agent_spawn_config_retargets_implicit_workspace_root_with_turn_cwd() {
|
||||
let (_session, mut turn) = make_session_and_context().await;
|
||||
let base_instructions = BaseInstructions {
|
||||
text: "base".to_string(),
|
||||
};
|
||||
let turn_cwd = tempfile::tempdir().expect("temp dir").abs();
|
||||
#[allow(deprecated)]
|
||||
{
|
||||
turn.cwd = turn_cwd.clone();
|
||||
}
|
||||
|
||||
let config = build_agent_spawn_config(&base_instructions, &turn).expect("spawn config");
|
||||
|
||||
assert_eq!(config.cwd, turn_cwd);
|
||||
assert_eq!(config.workspace_roots, vec![turn_cwd]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_agent_spawn_config_preserves_base_user_instructions() {
|
||||
let (_session, mut turn) = make_session_and_context().await;
|
||||
|
||||
@@ -553,7 +553,7 @@ impl TestCodexBuilder {
|
||||
} else {
|
||||
load_default_config_for_test(home).await
|
||||
};
|
||||
config.cwd = cwd_override;
|
||||
config.set_cwd_retargeting_implicit_workspace_root(cwd_override);
|
||||
config.model_provider = model_provider;
|
||||
if let Ok(path) = codex_utils_cargo_bin::cargo_bin("codex") {
|
||||
config.codex_self_exe = Some(path);
|
||||
|
||||
@@ -2159,6 +2159,109 @@ async fn approving_apply_patch_for_session_skips_future_prompts_for_same_file()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
#[cfg(unix)]
|
||||
async fn same_cwd_apply_patch_with_auto_review_skips_guardian_review() -> Result<()> {
|
||||
let server = start_mock_server().await;
|
||||
let approval_policy = AskForApproval::OnRequest;
|
||||
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![],
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
};
|
||||
let permission_profile = restrictive_workspace_write_profile();
|
||||
let permission_profile_for_config = permission_profile.clone();
|
||||
|
||||
let mut builder = test_codex()
|
||||
.with_model("gpt-5.4")
|
||||
.with_config(move |config| {
|
||||
config.permissions.approval_policy = Constrained::allow_any(approval_policy);
|
||||
config
|
||||
.permissions
|
||||
.set_permission_profile(permission_profile_for_config)
|
||||
.expect("set permission profile");
|
||||
config.approvals_reviewer = ApprovalsReviewer::AutoReview;
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
assert_eq!(test.config.workspace_roots, vec![test.config.cwd.clone()]);
|
||||
|
||||
let path = test
|
||||
.config
|
||||
.cwd
|
||||
.join("apply_patch_auto_review_inside_workspace.txt")
|
||||
.into_path_buf();
|
||||
let patch_path = path.display().to_string();
|
||||
let _ = fs::remove_file(&path);
|
||||
let patch = build_add_file_patch(&patch_path, "same-cwd-auto-review");
|
||||
let call_id = "apply_patch_auto_review_inside_workspace";
|
||||
|
||||
let _ = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_apply_patch_custom_tool_call(call_id, &patch),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
let results_mock = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_completed("resp-2"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let session_model = test.session_configured.model.clone();
|
||||
test.codex
|
||||
.submit(Op::UserTurn {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "apply same-cwd patch without guardian review".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: test.config.cwd.to_path_buf(),
|
||||
approval_policy,
|
||||
approvals_reviewer: Some(ApprovalsReviewer::AutoReview),
|
||||
sandbox_policy,
|
||||
permission_profile: Some(permission_profile),
|
||||
model: session_model,
|
||||
effort: None,
|
||||
summary: None,
|
||||
service_tier: None,
|
||||
collaboration_mode: None,
|
||||
personality: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let event = wait_for_event(&test.codex, |event| {
|
||||
matches!(
|
||||
event,
|
||||
EventMsg::GuardianAssessment(_) | EventMsg::TurnComplete(_)
|
||||
)
|
||||
})
|
||||
.await;
|
||||
match event {
|
||||
EventMsg::TurnComplete(_) => {}
|
||||
EventMsg::GuardianAssessment(event) => {
|
||||
panic!("unexpected guardian review for same-cwd apply_patch: {event:?}")
|
||||
}
|
||||
other => panic!("unexpected event: {other:?}"),
|
||||
}
|
||||
|
||||
let output_request = results_mock.single_request();
|
||||
let output_item = output_request.custom_tool_call_output(call_id);
|
||||
let result = parse_result(&output_item);
|
||||
assert_eq!(result.exit_code, Some(0));
|
||||
assert!(fs::read_to_string(&path)?.contains("same-cwd-auto-review"));
|
||||
let _ = fs::remove_file(path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
#[cfg(unix)]
|
||||
async fn approving_execpolicy_amendment_persists_policy_and_skips_future_prompts() -> Result<()> {
|
||||
|
||||
@@ -4,14 +4,17 @@ use codex_core::ForkSnapshot;
|
||||
use codex_core::config::Constrained;
|
||||
use codex_core::context::ContextualUserFragment;
|
||||
use codex_core::context::PermissionsInstructions;
|
||||
use codex_core::load_exec_policy;
|
||||
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
|
||||
use codex_exec_server::REMOTE_ENVIRONMENT_ID;
|
||||
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::TurnEnvironmentSelection;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use core_test_support::get_remote_test_env;
|
||||
use core_test_support::responses::ResponsesRequest;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
@@ -22,7 +25,9 @@ use core_test_support::skip_if_no_network;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn permissions_texts(request: &ResponsesRequest) -> Vec<String> {
|
||||
@@ -33,6 +38,14 @@ fn permissions_texts(request: &ResponsesRequest) -> Vec<String> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn environment_context_texts(request: &ResponsesRequest) -> Vec<String> {
|
||||
request
|
||||
.message_input_texts("user")
|
||||
.into_iter()
|
||||
.filter(|text| text.contains("<environment_context>"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn permissions_message_sent_once_on_start() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
@@ -67,6 +80,167 @@ async fn permissions_message_sent_once_on_start() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn approved_command_prefixes_are_sent_in_initial_environment_context_only() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let req1 = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
|
||||
)
|
||||
.await;
|
||||
let req2 = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut builder = test_codex().with_config(move |config| {
|
||||
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let rules_dir = config.codex_home.join("rules");
|
||||
fs::create_dir_all(&rules_dir).expect("create rules dir");
|
||||
fs::write(
|
||||
rules_dir.join("default.rules"),
|
||||
r#"prefix_rule(pattern=["git", "pull"], decision="allow")"#,
|
||||
)
|
||||
.expect("write policy");
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
let rollout_path = test
|
||||
.session_configured
|
||||
.rollout_path
|
||||
.clone()
|
||||
.expect("rollout path");
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello 1".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserInput {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "hello 2".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
let first_environment_contexts = environment_context_texts(&req1.single_request());
|
||||
assert_eq!(first_environment_contexts.len(), 1);
|
||||
assert!(
|
||||
first_environment_contexts[0].contains("<approved_command_prefixes>"),
|
||||
"expected approved prefixes in environment context: {first_environment_contexts:?}"
|
||||
);
|
||||
assert!(first_environment_contexts[0].contains(r#"["git", "pull"]"#));
|
||||
|
||||
let first_permissions = permissions_texts(&req1.single_request());
|
||||
assert_eq!(first_permissions.len(), 1);
|
||||
assert!(
|
||||
!first_permissions[0].contains("Approved command prefixes"),
|
||||
"did not expect approved prefixes in permissions message: {first_permissions:?}"
|
||||
);
|
||||
|
||||
let second_environment_contexts = environment_context_texts(&req2.single_request());
|
||||
assert_eq!(second_environment_contexts, first_environment_contexts);
|
||||
|
||||
let rollout_text = fs::read_to_string(rollout_path)?;
|
||||
for line in rollout_text.lines() {
|
||||
let item: Value = serde_json::from_str(line)?;
|
||||
if item.get("type").and_then(Value::as_str) == Some("turn_context") {
|
||||
let text = item.to_string();
|
||||
assert!(
|
||||
!text.contains("approved_command_prefixes"),
|
||||
"turn_context rollout item should not include approved prefixes: {text}"
|
||||
);
|
||||
assert!(
|
||||
!text.contains(r#"["git","pull"]"#),
|
||||
"turn_context rollout item should not include approved prefix list: {text}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn approved_command_prefixes_render_with_multiple_environments() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
let Some(_remote_env) = get_remote_test_env() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let req = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut builder = test_codex().with_config(move |config| {
|
||||
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let rules_dir = config.codex_home.join("rules");
|
||||
fs::create_dir_all(&rules_dir).expect("create rules dir");
|
||||
fs::write(
|
||||
rules_dir.join("default.rules"),
|
||||
r#"prefix_rule(pattern=["cargo", "test"], decision="allow")"#,
|
||||
)
|
||||
.expect("write policy");
|
||||
});
|
||||
let test = builder.build_with_remote_and_local_env(&server).await?;
|
||||
let local_cwd =
|
||||
AbsolutePathBuf::from_absolute_path(test.cwd.path()).expect("test cwd is absolute");
|
||||
let remote_cwd = AbsolutePathBuf::from_absolute_path(test.cwd.path().join("remote"))
|
||||
.expect("remote cwd is absolute");
|
||||
|
||||
test.submit_turn_with_environments(
|
||||
"hello multiple environments",
|
||||
Some(vec![
|
||||
TurnEnvironmentSelection {
|
||||
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd: local_cwd.clone(),
|
||||
},
|
||||
TurnEnvironmentSelection {
|
||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
cwd: remote_cwd.clone(),
|
||||
},
|
||||
]),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let environment_contexts = environment_context_texts(&req.single_request());
|
||||
assert_eq!(environment_contexts.len(), 1);
|
||||
let environment_context = &environment_contexts[0];
|
||||
assert!(environment_context.contains("<environments>"));
|
||||
assert!(environment_context.contains(&format!(r#"<environment id="{LOCAL_ENVIRONMENT_ID}">"#)));
|
||||
assert!(environment_context.contains(&format!("<cwd>{}</cwd>", local_cwd.to_string_lossy())));
|
||||
assert!(
|
||||
environment_context.contains(&format!(r#"<environment id="{REMOTE_ENVIRONMENT_ID}">"#))
|
||||
);
|
||||
assert!(environment_context.contains(&format!("<cwd>{}</cwd>", remote_cwd.to_string_lossy())));
|
||||
assert!(environment_context.contains("<approved_command_prefixes>"));
|
||||
assert!(environment_context.contains(r#"["cargo", "test"]"#));
|
||||
assert!(
|
||||
!environment_context.contains("\n <cwd>"),
|
||||
"multiple environment context should not render a legacy top-level cwd: {environment_context}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn permissions_message_added_on_override_change() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
@@ -218,6 +392,13 @@ async fn permissions_message_omitted_when_disabled() -> Result<()> {
|
||||
let mut builder = test_codex().with_config(move |config| {
|
||||
config.include_permissions_instructions = false;
|
||||
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let rules_dir = config.codex_home.join("rules");
|
||||
fs::create_dir_all(&rules_dir).expect("create rules dir");
|
||||
fs::write(
|
||||
rules_dir.join("default.rules"),
|
||||
r#"prefix_rule(pattern=["git", "pull"], decision="allow")"#,
|
||||
)
|
||||
.expect("write policy");
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
@@ -272,6 +453,12 @@ async fn permissions_message_omitted_when_disabled() -> Result<()> {
|
||||
permissions_texts(&req2.single_request()),
|
||||
Vec::<String>::new()
|
||||
);
|
||||
let environment_contexts = environment_context_texts(&req1.single_request());
|
||||
assert_eq!(environment_contexts.len(), 1);
|
||||
assert!(
|
||||
!environment_contexts[0].contains("<approved_command_prefixes>"),
|
||||
"did not expect approved prefixes when include_permissions_instructions = false: {environment_contexts:?}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -578,13 +765,11 @@ 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 permission_profile = test.config.permissions.effective_permission_profile();
|
||||
let expected = PermissionsInstructions::from_permission_profile(
|
||||
&permission_profile,
|
||||
AskForApproval::OnRequest,
|
||||
test.config.approvals_reviewer,
|
||||
&exec_policy,
|
||||
test.config.cwd.as_path(),
|
||||
/*exec_permission_approvals_enabled*/ false,
|
||||
/*request_permissions_tool_enabled*/ false,
|
||||
|
||||
Reference in New Issue
Block a user