Skip to content

Commit 4bb939f

Browse files
charley-oaicodex
andcommitted
guardian network allowlist review
Co-authored-by: Codex <noreply@openai.com>
1 parent 2717962 commit 4bb939f

5 files changed

Lines changed: 84 additions & 21 deletions

File tree

codex-rs/core/src/codex.rs

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10510,25 +10510,30 @@ mod tests {
1051010510
}
1051110511

1051210512
#[tokio::test]
10513-
async fn rejects_escalated_permissions_when_policy_not_on_request() {
10513+
async fn rejects_additional_permissions_when_policy_not_on_request() {
1051410514
use crate::exec::ExecParams;
10515+
use crate::features::Feature;
1051510516
use crate::protocol::AskForApproval;
1051610517
use crate::protocol::SandboxPolicy;
1051710518
use crate::sandboxing::SandboxPermissions;
1051810519
use crate::turn_diff_tracker::TurnDiffTracker;
1051910520
use std::collections::HashMap;
1052010521

10521-
let (session, mut turn_context_raw) = make_session_and_context().await;
10522+
let (mut session, mut turn_context_raw) = make_session_and_context().await;
1052210523
// Ensure policy is NOT OnRequest so the early rejection path triggers
1052310524
turn_context_raw
1052410525
.approval_policy
1052510526
.set(AskForApproval::OnFailure)
1052610527
.expect("test setup should allow updating approval policy");
10528+
session
10529+
.features
10530+
.enable(Feature::RequestPermissions)
10531+
.expect("test setup should allow enabling request permissions");
1052710532
let session = Arc::new(session);
1052810533
let mut turn_context = Arc::new(turn_context_raw);
1052910534

1053010535
let timeout_ms = 1000;
10531-
let sandbox_permissions = SandboxPermissions::RequireEscalated;
10536+
let sandbox_permissions = SandboxPermissions::WithAdditionalPermissions;
1053210537
let params = ExecParams {
1053310538
command: if cfg!(windows) {
1053410539
vec![
@@ -10596,8 +10601,8 @@ mod tests {
1059610601
};
1059710602

1059810603
let expected = format!(
10599-
"approval policy is {policy:?}; reject command — you should not ask for escalated permissions if the approval policy is {policy:?}",
10600-
policy = turn_context.approval_policy.value()
10604+
"approval policy is {policy:?}; reject command — you cannot request additional permissions unless the approval policy is OnRequest",
10605+
policy = turn_context.approval_policy.value(),
1060110606
);
1060210607

1060310608
pretty_assertions::assert_eq!(output, expected);
@@ -10656,7 +10661,7 @@ mod tests {
1065610661
assert!(exec_output.output.contains("hi"));
1065710662
}
1065810663
#[tokio::test]
10659-
async fn unified_exec_rejects_escalated_permissions_when_policy_not_on_request() {
10664+
async fn unified_exec_rejects_additional_permissions_when_policy_not_on_request() {
1066010665
use crate::protocol::AskForApproval;
1066110666
use crate::sandboxing::SandboxPermissions;
1066210667
use crate::turn_diff_tracker::TurnDiffTracker;
@@ -10670,6 +10675,52 @@ mod tests {
1067010675
let turn_context = Arc::new(turn_context_raw);
1067110676
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
1067210677

10678+
let handler = UnifiedExecHandler;
10679+
let resp = handler
10680+
.handle(ToolInvocation {
10681+
session: Arc::clone(&session),
10682+
turn: Arc::clone(&turn_context),
10683+
tracker: Arc::clone(&tracker),
10684+
call_id: "exec-call".to_string(),
10685+
tool_name: "exec_command".to_string(),
10686+
payload: ToolPayload::Function {
10687+
arguments: serde_json::json!({
10688+
"cmd": "echo hi",
10689+
"sandbox_permissions": SandboxPermissions::WithAdditionalPermissions,
10690+
"justification": "need additional sandbox permissions",
10691+
})
10692+
.to_string(),
10693+
},
10694+
})
10695+
.await;
10696+
10697+
let Err(FunctionCallError::RespondToModel(output)) = resp else {
10698+
panic!("expected error result");
10699+
};
10700+
10701+
let expected = format!(
10702+
"approval policy is {policy:?}; reject command — you cannot ask for additional sandbox permissions if the approval policy is {policy:?}",
10703+
policy = turn_context.approval_policy.value()
10704+
);
10705+
10706+
pretty_assertions::assert_eq!(output, expected);
10707+
}
10708+
10709+
#[tokio::test]
10710+
async fn unified_exec_rejects_escalated_permissions_when_policy_is_never() {
10711+
use crate::protocol::AskForApproval;
10712+
use crate::sandboxing::SandboxPermissions;
10713+
use crate::turn_diff_tracker::TurnDiffTracker;
10714+
10715+
let (session, mut turn_context_raw) = make_session_and_context().await;
10716+
turn_context_raw
10717+
.approval_policy
10718+
.set(AskForApproval::Never)
10719+
.expect("test setup should allow updating approval policy");
10720+
let session = Arc::new(session);
10721+
let turn_context = Arc::new(turn_context_raw);
10722+
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
10723+
1067310724
let handler = UnifiedExecHandler;
1067410725
let resp = handler
1067510726
.handle(ToolInvocation {
@@ -10682,7 +10733,7 @@ mod tests {
1068210733
arguments: serde_json::json!({
1068310734
"cmd": "echo hi",
1068410735
"sandbox_permissions": SandboxPermissions::RequireEscalated,
10685-
"justification": "need unsandboxed execution",
10736+
"justification": "need escalated permissions",
1068610737
})
1068710738
.to_string(),
1068810739
},
@@ -10694,7 +10745,7 @@ mod tests {
1069410745
};
1069510746

1069610747
let expected = format!(
10697-
"approval policy is {policy:?}; reject command — you cannot ask for escalated permissions if the approval policy is {policy:?}",
10748+
"approval policy is {policy:?}; reject command — you should not ask for escalated permissions if the approval policy is {policy:?}",
1069810749
policy = turn_context.approval_policy.value()
1069910750
);
1070010751

codex-rs/core/src/exec_policy.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,7 @@ pub fn render_decision_for_unmatched_command(
552552
// In restricted sandboxes (ReadOnly/WorkspaceWrite), do not prompt for
553553
// non‑escalated, non‑dangerous commands — let the sandbox enforce
554554
// restrictions (e.g., block network/write) without a user prompt.
555-
if sandbox_permissions.uses_additional_permissions() {
555+
if sandbox_permissions.requests_sandbox_override() {
556556
Decision::Prompt
557557
} else {
558558
Decision::Allow
@@ -567,7 +567,7 @@ pub fn render_decision_for_unmatched_command(
567567
Decision::Allow
568568
}
569569
SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. } => {
570-
if sandbox_permissions.uses_additional_permissions() {
570+
if sandbox_permissions.requests_sandbox_override() {
571571
Decision::Prompt
572572
} else {
573573
Decision::Allow
@@ -1592,7 +1592,8 @@ prefix_rule(pattern=["git"], decision="prompt")
15921592
}
15931593

15941594
#[tokio::test]
1595-
async fn exec_approval_requirement_rejects_unmatched_prompt_when_sandbox_rejection_enabled() {
1595+
async fn exec_approval_requirement_rejects_unmatched_sandbox_escalation_when_sandbox_rejection_enabled()
1596+
{
15961597
let command = vec!["madeup-cmd".to_string()];
15971598

15981599
let requirement = ExecPolicyManager::default()

codex-rs/core/src/tools/handlers/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ pub(super) fn normalize_and_validate_additional_permissions(
102102
SandboxPermissions::WithAdditionalPermissions
103103
);
104104

105+
if sandbox_permissions.requires_escalated_permissions()
106+
&& matches!(approval_policy, AskForApproval::Never)
107+
{
108+
return Err(format!(
109+
"approval policy is {approval_policy:?}; reject command — you should not ask for escalated permissions if the approval policy is {approval_policy:?}"
110+
));
111+
}
112+
105113
if !request_permission_enabled
106114
&& (uses_additional_permissions || additional_permissions.is_some())
107115
{

codex-rs/core/src/tools/handlers/shell.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ use crate::tools::runtimes::shell::ShellRuntimeBackend;
3333
use crate::tools::sandboxing::ToolCtx;
3434
use crate::tools::spec::ShellCommandBackendConfig;
3535
use codex_protocol::models::PermissionProfile;
36-
use codex_protocol::models::SandboxPermissions;
3736

3837
pub struct ShellHandler;
3938

codex-rs/core/src/tools/network_approval.rs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,12 @@ enum NetworkApprovalOutcome {
112112
DeniedByPolicy(String),
113113
}
114114

115-
fn allows_network_prompt(policy: AskForApproval) -> bool {
116-
!matches!(policy, AskForApproval::Never | AskForApproval::Guardian)
115+
/// Whether an allowlist miss may be reviewed instead of hard-denied.
116+
///
117+
/// Guardian uses the same inline decision path as user approvals, even though
118+
/// it is not an interactive prompt.
119+
fn allows_network_approval_flow(policy: AskForApproval) -> bool {
120+
!matches!(policy, AskForApproval::Never)
117121
}
118122

119123
impl PendingApprovalDecision {
@@ -316,7 +320,7 @@ impl NetworkApprovalService {
316320
.await;
317321
return NetworkDecision::deny(REASON_NOT_ALLOWED);
318322
};
319-
if !allows_network_prompt(turn_context.approval_policy.value()) {
323+
if !allows_network_approval_flow(turn_context.approval_policy.value()) {
320324
pending.set_decision(PendingApprovalDecision::Deny).await;
321325
let mut pending_approvals = self.pending_host_approvals.lock().await;
322326
pending_approvals.remove(&key);
@@ -666,12 +670,12 @@ mod tests {
666670
}
667671

668672
#[test]
669-
fn never_policy_disables_network_prompts() {
670-
assert!(!allows_network_prompt(AskForApproval::Never));
671-
assert!(allows_network_prompt(AskForApproval::OnRequest));
672-
assert!(allows_network_prompt(AskForApproval::OnFailure));
673-
assert!(!allows_network_prompt(AskForApproval::Guardian));
674-
assert!(allows_network_prompt(AskForApproval::UnlessTrusted));
673+
fn only_never_policy_disables_network_approval_flow() {
674+
assert!(!allows_network_approval_flow(AskForApproval::Never));
675+
assert!(allows_network_approval_flow(AskForApproval::OnRequest));
676+
assert!(allows_network_approval_flow(AskForApproval::OnFailure));
677+
assert!(allows_network_approval_flow(AskForApproval::Guardian));
678+
assert!(allows_network_approval_flow(AskForApproval::UnlessTrusted));
675679
}
676680

677681
fn denied_blocked_request(host: &str) -> BlockedRequest {

0 commit comments

Comments
 (0)