Refactor execpolicy fallback evaluation (#7544)

## Refactor of the `execpolicy` crate

To illustrate why we need this refactor, consider an agent attempting to
run `apple | rm -rf ./`. Suppose `apple` is allowed by `execpolicy`.
Before this PR, `execpolicy` would consider `apple` and `pear` and only
render one rule match: `Allow`. We would skip any heuristics checks on
`rm -rf ./` and immediately approve `apple | rm -rf ./` to run.

To fix this, we now thread a `fallback` evaluation function into
`execpolicy` that runs when no `execpolicy` rules match a given command.
In our example, we would run `fallback` on `rm -rf ./` and prevent
`apple | rm -rf ./` from being run without approval.
This commit is contained in:
zhao-oai authored and GitHub committed 2025-12-03 23:39:48 -08:00
1 parent e925a380dc
commit 3d35cb4619
27 files changed
+538 -257

No files matched your search

@@ -17,6 +17,7 @@ use crate::render::highlight::highlight_bash_to_lines;
use crate::render::renderable::ColumnRenderable;
use crate::render::renderable::Renderable;
use codex_core::protocol::ElicitationAction;
use codex_core::protocol::ExecPolicyAmendment;
use codex_core::protocol::FileChange;
use codex_core::protocol::Op;
use codex_core::protocol::ReviewDecision;
@@ -43,7 +44,7 @@ pub(crate) enum ApprovalRequest {
command: Vec<String>,
reason: Option<String>,
risk: Option<SandboxCommandAssessment>,
allow_prefix: Option<Vec<String>>,
proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
},
ApplyPatch {
id: String,
@@ -105,8 +106,11 @@ impl ApprovalOverlay {
header: Box<dyn Renderable>,
) -> (Vec<ApprovalOption>, SelectionViewParams) {
let (options, title) = match &variant {
ApprovalVariant::Exec { allow_prefix, .. } => (
exec_options(allow_prefix.clone()),
ApprovalVariant::Exec {
proposed_execpolicy_amendment,
..
} => (
exec_options(proposed_execpolicy_amendment.clone()),
"Would you like to run the following command?".to_string(),
),
ApprovalVariant::ApplyPatch { .. } => (
@@ -337,7 +341,7 @@ impl From<ApprovalRequest> for ApprovalRequestState {
command,
reason,
risk,
allow_prefix,
proposed_execpolicy_amendment,
} => {
let reason = reason.filter(|item| !item.is_empty());
let has_reason = reason.is_some();
@@ -360,7 +364,7 @@ impl From<ApprovalRequest> for ApprovalRequestState {
variant: ApprovalVariant::Exec {
id,
command,
allow_prefix,
proposed_execpolicy_amendment,
},
header: Box::new(Paragraph::new(header).wrap(Wrap { trim: false })),
}
@@ -437,7 +441,7 @@ enum ApprovalVariant {
Exec {
id: String,
command: Vec<String>,
allow_prefix: Option<Vec<String>>,
proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
},
ApplyPatch {
id: String,
@@ -470,7 +474,7 @@ impl ApprovalOption {
}
}
fn exec_options(allow_prefix: Option<Vec<String>>) -> Vec<ApprovalOption> {
fn exec_options(proposed_execpolicy_amendment: Option<ExecPolicyAmendment>) -> Vec<ApprovalOption> {
vec![
ApprovalOption {
label: "Yes, proceed".to_string(),
@@ -486,10 +490,10 @@ fn exec_options(allow_prefix: Option<Vec<String>>) -> Vec<ApprovalOption> {
},
]
.into_iter()
.chain(allow_prefix.map(|prefix| ApprovalOption {
.chain(proposed_execpolicy_amendment.map(|prefix| ApprovalOption {
label: "Yes, and don't ask again for commands with this prefix".to_string(),
decision: ApprovalDecision::Review(ReviewDecision::ApprovedAllowPrefix {
allow_prefix: prefix,
decision: ApprovalDecision::Review(ReviewDecision::ApprovedExecpolicyAmendment {
proposed_execpolicy_amendment: prefix,
}),
display_shortcut: None,
additional_shortcuts: vec![key_hint::plain(KeyCode::Char('p'))],
@@ -556,7 +560,7 @@ mod tests {
command: vec!["echo".to_string(), "hi".to_string()],
reason: Some("reason".to_string()),
risk: None,
allow_prefix: None,
proposed_execpolicy_amendment: None,
}
}
@@ -590,7 +594,7 @@ mod tests {
}
#[test]
fn exec_prefix_option_emits_allow_prefix() {
fn exec_prefix_option_emits_execpolicy_amendment() {
let (tx, mut rx) = unbounded_channel::<AppEvent>();
let tx = AppEventSender::new(tx);
let mut view = ApprovalOverlay::new(
@@ -599,7 +603,9 @@ mod tests {
command: vec!["echo".to_string()],
reason: None,
risk: None,
allow_prefix: Some(vec!["echo".to_string()]),
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![
"echo".to_string(),
])),
},
tx,
);
@@ -609,8 +615,10 @@ mod tests {
if let AppEvent::CodexOp(Op::ExecApproval { decision, .. }) = ev {
assert_eq!(
decision,
ReviewDecision::ApprovedAllowPrefix {
allow_prefix: vec!["echo".to_string()]
ReviewDecision::ApprovedExecpolicyAmendment {
proposed_execpolicy_amendment: ExecPolicyAmendment::new(vec![
"echo".to_string()
])
}
);
saw_op = true;
@@ -619,7 +627,7 @@ mod tests {
}
assert!(
saw_op,
"expected approval decision to emit an op with allow prefix"
"expected approval decision to emit an op with command prefix"
);
}
@@ -633,7 +641,7 @@ mod tests {
command,
reason: None,
risk: None,
allow_prefix: None,
proposed_execpolicy_amendment: None,
};
let view = ApprovalOverlay::new(exec_request, tx);
+1 -1
View File
@@ -570,7 +570,7 @@ mod tests {
command: vec!["echo".into(), "ok".into()],
reason: None,
risk: None,
allow_prefix: None,
proposed_execpolicy_amendment: None,
}
}
+1 -1
View File
@@ -1074,7 +1074,7 @@ impl ChatWidget {
command: ev.command,
reason: ev.reason,
risk: ev.risk,
allow_prefix: ev.allow_prefix,
proposed_execpolicy_amendment: ev.proposed_execpolicy_amendment,
};
self.bottom_pane.push_approval_request(request);
self.request_redraw();
+18 -6
View File
@@ -23,6 +23,7 @@ use codex_core::protocol::ExecApprovalRequestEvent;
use codex_core::protocol::ExecCommandBeginEvent;
use codex_core::protocol::ExecCommandEndEvent;
use codex_core::protocol::ExecCommandSource;
use codex_core::protocol::ExecPolicyAmendment;
use codex_core::protocol::ExitedReviewModeEvent;
use codex_core::protocol::FileChange;
use codex_core::protocol::Op;
@@ -688,7 +689,7 @@ fn exec_approval_emits_proposed_command_and_decision_history() {
"this is a test reason such as one that would be produced by the model".into(),
),
risk: None,
allow_prefix: None,
proposed_execpolicy_amendment: None,
parsed_cmd: vec![],
};
chat.handle_codex_event(Event {
@@ -733,7 +734,7 @@ fn exec_approval_decision_truncates_multiline_and_long_commands() {
"this is a test reason such as one that would be produced by the model".into(),
),
risk: None,
allow_prefix: None,
proposed_execpolicy_amendment: None,
parsed_cmd: vec![],
};
chat.handle_codex_event(Event {
@@ -784,7 +785,7 @@ fn exec_approval_decision_truncates_multiline_and_long_commands() {
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
reason: None,
risk: None,
allow_prefix: None,
proposed_execpolicy_amendment: None,
parsed_cmd: vec![],
};
chat.handle_codex_event(Event {
@@ -1993,7 +1994,11 @@ fn approval_modal_exec_snapshot() {
"this is a test reason such as one that would be produced by the model".into(),
),
risk: None,
allow_prefix: Some(vec!["echo".into(), "hello".into(), "world".into()]),
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![
"echo".into(),
"hello".into(),
"world".into(),
])),
parsed_cmd: vec![],
};
chat.handle_codex_event(Event {
@@ -2040,7 +2045,11 @@ fn approval_modal_exec_without_reason_snapshot() {
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
reason: None,
risk: None,
allow_prefix: Some(vec!["echo".into(), "hello".into(), "world".into()]),
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![
"echo".into(),
"hello".into(),
"world".into(),
])),
parsed_cmd: vec![],
};
chat.handle_codex_event(Event {
@@ -2254,7 +2263,10 @@ fn status_widget_and_approval_modal_snapshot() {
"this is a test reason such as one that would be produced by the model".into(),
),
risk: None,
allow_prefix: Some(vec!["echo".into(), "hello world".into()]),
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![
"echo".into(),
"hello world".into(),
])),
parsed_cmd: vec![],
};
chat.handle_codex_event(Event {
+2 -2
View File
@@ -409,7 +409,7 @@ pub fn new_approval_decision_cell(
],
)
}
ApprovedAllowPrefix { .. } => {
ApprovedExecpolicyAmendment { .. } => {
let snippet = Span::from(exec_snippet(&command)).dim();
(
"✔ ".green(),
@@ -418,7 +418,7 @@ pub fn new_approval_decision_cell(
"approved".bold(),
" codex to run ".into(),
snippet,
" and added its prefix to your allow list".bold(),
" and applied the execpolicy amendment".bold(),
],
)
}