mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
## What changed - Let approval extensions choose between cached approval, synchronous review, and a user prompt for tool and permission requests, while core enforces mandatory Guardian and fresh-review requirements. - Carry an optional `review_reason` on Guardian assessment events and use it to trigger strict-review notifications, retaining the fallback for older events. - Require synchronous review when cached evidence has an unusable encrypted parent compaction. ## Testing Add regression coverage for manual prompts and cached approvals with non-UTF-8 working directories. Update approval contributors in existing tests to use the decision API. GitOrigin-RevId: a4e66416070bdaa881daa41429c49021629f44c1
146 lines
4.6 KiB
Rust
146 lines
4.6 KiB
Rust
use std::collections::VecDeque;
|
|
|
|
use codex_protocol::approvals::GuardianAssessmentAction;
|
|
use codex_protocol::approvals::GuardianAssessmentEvent;
|
|
use codex_protocol::approvals::GuardianAssessmentStatus;
|
|
|
|
const MAX_RECENT_DENIALS: usize = 10;
|
|
|
|
#[derive(Debug, Default)]
|
|
pub(crate) struct RecentAutoReviewDenials {
|
|
entries: VecDeque<GuardianAssessmentEvent>,
|
|
}
|
|
|
|
impl RecentAutoReviewDenials {
|
|
pub(crate) fn push(&mut self, event: GuardianAssessmentEvent) {
|
|
if event.status != GuardianAssessmentStatus::Denied {
|
|
return;
|
|
}
|
|
|
|
self.entries.retain(|entry| entry.id != event.id);
|
|
self.entries.push_front(event);
|
|
self.entries.truncate(MAX_RECENT_DENIALS);
|
|
}
|
|
|
|
pub(crate) fn is_empty(&self) -> bool {
|
|
self.entries.is_empty()
|
|
}
|
|
|
|
pub(crate) fn entries(&self) -> impl Iterator<Item = &GuardianAssessmentEvent> {
|
|
self.entries.iter()
|
|
}
|
|
|
|
pub(crate) fn take(&mut self, id: &str) -> Option<GuardianAssessmentEvent> {
|
|
let idx = self.entries.iter().position(|entry| entry.id == id)?;
|
|
self.entries.remove(idx)
|
|
}
|
|
}
|
|
|
|
pub(crate) fn action_summary(action: &GuardianAssessmentAction) -> String {
|
|
match action {
|
|
GuardianAssessmentAction::Command { command, .. } => command.clone(),
|
|
GuardianAssessmentAction::Execve { program, argv, .. } => {
|
|
let command = if argv.is_empty() {
|
|
vec![program.clone()]
|
|
} else {
|
|
argv.clone()
|
|
};
|
|
shlex::try_join(command.iter().map(String::as_str))
|
|
.unwrap_or_else(|_| command.join(" "))
|
|
}
|
|
GuardianAssessmentAction::WriteStdin {
|
|
process_id, stdin, ..
|
|
} => {
|
|
let stdin = crate::text_formatting::truncate_text(
|
|
&format!("{stdin:?}"),
|
|
/*max_graphemes*/ 80,
|
|
);
|
|
format!("send input to terminal {process_id}: {stdin}")
|
|
}
|
|
GuardianAssessmentAction::ApplyPatch { files, .. } => {
|
|
if files.len() == 1 {
|
|
format!("apply_patch touching {}", files[0].render_for_ui())
|
|
} else {
|
|
format!("apply_patch touching {} files", files.len())
|
|
}
|
|
}
|
|
GuardianAssessmentAction::NetworkAccess { target, .. } => {
|
|
format!("network access to {target}")
|
|
}
|
|
GuardianAssessmentAction::McpToolCall {
|
|
server,
|
|
tool_name,
|
|
connector_name,
|
|
..
|
|
} => {
|
|
let label = connector_name.as_deref().unwrap_or(server.as_str());
|
|
format!("MCP {tool_name} on {label}")
|
|
}
|
|
GuardianAssessmentAction::RequestPermissions { reason, .. } => reason
|
|
.as_deref()
|
|
.map(|reason| format!("permission request: {reason}"))
|
|
.unwrap_or_else(|| "permission request".to_string()),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use codex_protocol::approvals::GuardianCommandSource;
|
|
use codex_utils_absolute_path::test_support::PathBufExt;
|
|
use codex_utils_absolute_path::test_support::test_path_buf;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
use super::*;
|
|
|
|
fn denied_event(id: usize) -> GuardianAssessmentEvent {
|
|
GuardianAssessmentEvent {
|
|
review_reason: None,
|
|
id: format!("review-{id}"),
|
|
target_item_id: None,
|
|
plugin_id: None,
|
|
script_path: None,
|
|
turn_id: "turn-1".to_string(),
|
|
started_at_ms: 0,
|
|
completed_at_ms: Some(1),
|
|
status: GuardianAssessmentStatus::Denied,
|
|
risk_level: None,
|
|
user_authorization: None,
|
|
rationale: Some(format!("rationale {id}")),
|
|
decision_source: None,
|
|
action: GuardianAssessmentAction::Command {
|
|
source: GuardianCommandSource::Shell,
|
|
command: format!("rm -rf /tmp/test-{id}"),
|
|
cwd: test_path_buf("/tmp").abs().into(),
|
|
},
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn keeps_only_ten_most_recent_denials() {
|
|
let mut denials = RecentAutoReviewDenials::default();
|
|
for id in 0..12 {
|
|
denials.push(denied_event(id));
|
|
}
|
|
|
|
let ids = denials
|
|
.entries()
|
|
.map(|entry| entry.id.as_str())
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(
|
|
ids,
|
|
vec![
|
|
"review-11",
|
|
"review-10",
|
|
"review-9",
|
|
"review-8",
|
|
"review-7",
|
|
"review-6",
|
|
"review-5",
|
|
"review-4",
|
|
"review-3",
|
|
"review-2",
|
|
]
|
|
);
|
|
}
|
|
}
|