Codex Handbook
tui/src/auto_review_denials.rs 133 lines
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::ApplyPatch { files, .. } => {            if files.len() == 1 {                format!("apply_patch touching {}", files[0].display())            } 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 {            id: format!("review-{id}"),            target_item_id: 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(),            },        }    }    #[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",            ]        );    }}