Skip to content

Commit 79de526

Browse files
committed
feedback
1 parent 6a02ed1 commit 79de526

File tree

2 files changed

+105
-109
lines changed

2 files changed

+105
-109
lines changed

codex-rs/core/src/codex.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2689,6 +2689,20 @@ async fn handle_container_exec_with_params(
26892689
sub_id: String,
26902690
call_id: String,
26912691
) -> ResponseInputItem {
2692+
if params.with_escalated_permissions.unwrap_or(false)
2693+
&& !matches!(turn_context.approval_policy, AskForApproval::OnRequest)
2694+
{
2695+
return ResponseInputItem::FunctionCallOutput {
2696+
call_id,
2697+
output: FunctionCallOutputPayload {
2698+
content: format!(
2699+
"approval policy is {policy:?}; reject command — you should not ask for escalated permissions if the approval policy is {policy:?}",
2700+
policy = turn_context.approval_policy
2701+
),
2702+
success: None,
2703+
},
2704+
};
2705+
}
26922706
// check if this was a patch, and apply it if so
26932707
let apply_patch_exec = match maybe_parse_apply_patch_verified(&params.command, &params.cwd) {
26942708
MaybeApplyPatchVerified::Body(changes) => {
@@ -3650,4 +3664,94 @@ mod tests {
36503664

36513665
(rollout_items, live_history.contents())
36523666
}
3667+
3668+
#[tokio::test]
3669+
async fn rejects_escalated_permissions_when_policy_not_on_request() {
3670+
use crate::exec::ExecParams;
3671+
use crate::protocol::AskForApproval;
3672+
use crate::protocol::SandboxPolicy;
3673+
use crate::turn_diff_tracker::TurnDiffTracker;
3674+
use std::collections::HashMap;
3675+
3676+
let (session, mut turn_context) = make_session_and_context();
3677+
// Ensure policy is NOT OnRequest so the early rejection path triggers
3678+
turn_context.approval_policy = AskForApproval::OnFailure;
3679+
3680+
let params = ExecParams {
3681+
command: vec![
3682+
"/bin/sh".to_string(),
3683+
"-c".to_string(),
3684+
"echo hi".to_string(),
3685+
],
3686+
cwd: turn_context.cwd.clone(),
3687+
timeout_ms: Some(1000),
3688+
env: HashMap::new(),
3689+
with_escalated_permissions: Some(true),
3690+
justification: Some("test".to_string()),
3691+
};
3692+
3693+
let mut turn_diff_tracker = TurnDiffTracker::new();
3694+
3695+
let sub_id = "test-sub".to_string();
3696+
let call_id = "test-call".to_string();
3697+
3698+
let resp = handle_container_exec_with_params(
3699+
params,
3700+
&session,
3701+
&turn_context,
3702+
&mut turn_diff_tracker,
3703+
sub_id,
3704+
call_id,
3705+
)
3706+
.await;
3707+
3708+
let ResponseInputItem::FunctionCallOutput { output, .. } = resp else {
3709+
panic!("expected FunctionCallOutput");
3710+
};
3711+
3712+
let expected = format!(
3713+
"approval policy is {policy:?}; reject command — you should not ask for escalated permissions if the approval policy is {policy:?}",
3714+
policy = turn_context.approval_policy
3715+
);
3716+
3717+
pretty_assertions::assert_eq!(output.content, expected);
3718+
3719+
// Now retry the same command WITHOUT escalated permissions; should succeed.
3720+
// Force DangerFullAccess to avoid platform sandbox dependencies in tests.
3721+
turn_context.sandbox_policy = SandboxPolicy::DangerFullAccess;
3722+
3723+
let params2 = ExecParams {
3724+
command: vec![
3725+
"/bin/sh".to_string(),
3726+
"-c".to_string(),
3727+
"echo hi".to_string(),
3728+
],
3729+
cwd: turn_context.cwd.clone(),
3730+
timeout_ms: Some(1000),
3731+
env: HashMap::new(),
3732+
with_escalated_permissions: Some(false),
3733+
justification: Some("test".to_string()),
3734+
};
3735+
3736+
let resp2 = handle_container_exec_with_params(
3737+
params2,
3738+
&session,
3739+
&turn_context,
3740+
&mut turn_diff_tracker,
3741+
"test-sub".to_string(),
3742+
"test-call-2".to_string(),
3743+
)
3744+
.await;
3745+
3746+
let ResponseInputItem::FunctionCallOutput { output, .. } = resp2 else {
3747+
panic!("expected FunctionCallOutput on retry");
3748+
};
3749+
3750+
// Parse the structured exec output and assert success without new structs
3751+
let v: serde_json::Value =
3752+
serde_json::from_str(&output.content).expect("valid exec output json");
3753+
assert_eq!(v["metadata"]["exit_code"].as_i64(), Some(0));
3754+
assert!(v["output"].as_str().unwrap_or("").contains("hi"));
3755+
assert_eq!(output.success, Some(true));
3756+
}
36533757
}

codex-rs/core/src/safety.rs

Lines changed: 1 addition & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ use std::path::Component;
33
use std::path::Path;
44
use std::path::PathBuf;
55

6-
use AskForApproval::*;
76
use codex_apply_patch::ApplyPatchAction;
87
use codex_apply_patch::ApplyPatchFileChange;
98

@@ -99,18 +98,7 @@ pub fn assess_command_safety(
9998
// would probably be fine to run the command in a sandbox, but when
10099
// `approved.contains(command)` is `true`, the user may have approved it for
101100
// the session _because_ they know it needs to run outside a sandbox.
102-
let command_is_trusted = is_known_safe_command(command) || approved.contains(command);
103-
104-
// reject function calls when the model asks for escalated permissions when it should not have to
105-
if let Some(decision) = reject_forbidden_escalation(
106-
approval_policy,
107-
with_escalated_permissions,
108-
command_is_trusted,
109-
) {
110-
return decision;
111-
}
112-
113-
if command_is_trusted {
101+
if is_known_safe_command(command) || approved.contains(command) {
114102
return SafetyCheck::AutoApprove {
115103
sandbox_type: SandboxType::None,
116104
};
@@ -127,12 +115,6 @@ pub(crate) fn assess_safety_for_untrusted_command(
127115
use AskForApproval::*;
128116
use SandboxPolicy::*;
129117

130-
if let Some(decision) =
131-
reject_forbidden_escalation(approval_policy, with_escalated_permissions, false)
132-
{
133-
return decision;
134-
}
135-
136118
match (approval_policy, sandbox_policy) {
137119
(UnlessTrusted, _) => {
138120
// Even though the user may have opted into DangerFullAccess,
@@ -194,38 +176,6 @@ pub fn get_platform_sandbox() -> Option<SandboxType> {
194176
}
195177
}
196178

197-
/// Forbidden escalation is when the model asks for escalated permissions when it should not have to
198-
/// Rules:
199-
/// The model shouldn't ask for escalated permissions if the command is trusted
200-
/// The model shouldn't ask for escalated permissions if the approval policy is Never
201-
/// The model shouldn't ask for escalated permissions if the approval policy is OnFailure and it hasn't failed
202-
fn reject_forbidden_escalation(
203-
approval_policy: AskForApproval,
204-
with_escalated_permissions: bool,
205-
command_is_trusted: bool,
206-
) -> Option<SafetyCheck> {
207-
if !with_escalated_permissions {
208-
return None;
209-
}
210-
211-
let reason = match approval_policy {
212-
Never => Some(
213-
"auto-rejected. You should not ask for escalated permissions if the approval policy is Never".to_string(),
214-
),
215-
OnFailure => Some(
216-
"auto-rejected. You should not ask for escalated permissions if the approval policy is OnFailure and it hasn't failed"
217-
.to_string(),
218-
),
219-
UnlessTrusted if command_is_trusted => Some(
220-
"auto-rejected. The command is already trusted under the UnlessTrusted approval policy. You do not need to ask for escalated permissions"
221-
.to_string(),
222-
),
223-
OnRequest | UnlessTrusted => None,
224-
}?;
225-
226-
Some(SafetyCheck::Reject { reason })
227-
}
228-
229179
fn is_write_patch_constrained_to_writable_paths(
230180
action: &ApplyPatchAction,
231181
sandbox_policy: &SandboxPolicy,
@@ -397,62 +347,4 @@ mod tests {
397347
};
398348
assert_eq!(safety_check, expected);
399349
}
400-
401-
#[test]
402-
fn test_escalation_rejected_when_policy_is_never() {
403-
let command = vec!["git".to_string(), "status".to_string()];
404-
let approval_policy = AskForApproval::Never;
405-
let sandbox_policy = SandboxPolicy::ReadOnly;
406-
let approved = HashSet::new();
407-
408-
let safety_check =
409-
assess_command_safety(&command, approval_policy, &sandbox_policy, &approved, true);
410-
411-
assert_eq!(
412-
safety_check,
413-
SafetyCheck::Reject {
414-
reason: "auto-rejected. You should not ask for escalated permissions if the approval policy is Never"
415-
.to_string(),
416-
}
417-
);
418-
}
419-
420-
#[test]
421-
fn test_escalation_rejected_for_on_failure_policy() {
422-
let command = vec!["git".to_string(), "status".to_string()];
423-
let approval_policy = AskForApproval::OnFailure;
424-
let sandbox_policy = SandboxPolicy::ReadOnly;
425-
let approved = HashSet::new();
426-
427-
let safety_check =
428-
assess_command_safety(&command, approval_policy, &sandbox_policy, &approved, true);
429-
430-
assert_eq!(
431-
safety_check,
432-
SafetyCheck::Reject {
433-
reason:
434-
"auto-rejected. You should not ask for escalated permissions if the approval policy is OnFailure and it hasn't failed"
435-
.to_string(),
436-
}
437-
);
438-
}
439-
440-
#[test]
441-
fn test_escalation_rejected_when_trusted_under_unless_trusted() {
442-
let command = vec!["just".to_string(), "fmt".to_string()];
443-
let approval_policy = AskForApproval::UnlessTrusted;
444-
let sandbox_policy = SandboxPolicy::ReadOnly;
445-
let approved = HashSet::from([command.clone()]);
446-
447-
let safety_check =
448-
assess_command_safety(&command, approval_policy, &sandbox_policy, &approved, true);
449-
450-
assert_eq!(
451-
safety_check,
452-
SafetyCheck::Reject {
453-
reason: "auto-rejected. The command is already trusted under the UnlessTrusted approval policy. You do not need to ask for escalated permissions"
454-
.to_string(),
455-
}
456-
);
457-
}
458350
}

0 commit comments

Comments
 (0)