mirror of
https://github.com/openai/codex.git
synced 2026-05-04 21:32:21 +03:00
fix(guardian, app-server): introduce guardian review ids (#17298)
## Description This PR introduces `review_id` as the stable identifier for guardian reviews and exposes it in app-server `item/autoApprovalReview/started` and `item/autoApprovalReview/completed` events. Internally, guardian rejection state is now keyed by `review_id` instead of the reviewed tool item ID. `target_item_id` is still included when a review maps to a concrete thread item, but it is no longer overloaded as the review lifecycle identifier. ## Motivation We'd like to give users the ability to preempt a guardian review while it's running (approve or decline). However, we can't implement the API that allows the user to override a running guardian review because we didn't have a unique `review_id` per guardian review. Using `target_item_id` is not correct since: - with execve reviews, there can be multiple execve calls (and therefore guardian reviews) per shell command - with network policy reviews, there is no target item ID The PR that actually implements user overrides will use `review_id` as the stable identifier.
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
//! - The projection is presentation-specific. Core protocol events stay generic, while the
|
||||
//! app-server protocol decides how to surface those events as `ThreadItem`s for clients.
|
||||
use crate::protocol::common::ServerNotification;
|
||||
use crate::protocol::v2::AutoReviewDecisionSource;
|
||||
use crate::protocol::v2::CommandAction;
|
||||
use crate::protocol::v2::CommandExecutionSource;
|
||||
use crate::protocol::v2::CommandExecutionStatus;
|
||||
@@ -142,12 +143,13 @@ pub fn build_item_from_guardian_event(
|
||||
) -> Option<ThreadItem> {
|
||||
match &assessment.action {
|
||||
GuardianAssessmentAction::Command { command, cwd, .. } => {
|
||||
let id = assessment.target_item_id.as_ref()?;
|
||||
let command = command.clone();
|
||||
let command_actions = vec![CommandAction::Unknown {
|
||||
command: command.clone(),
|
||||
}];
|
||||
Some(ThreadItem::CommandExecution {
|
||||
id: assessment.id.clone(),
|
||||
id: id.clone(),
|
||||
command,
|
||||
cwd: cwd.clone(),
|
||||
process_id: None,
|
||||
@@ -162,6 +164,7 @@ pub fn build_item_from_guardian_event(
|
||||
GuardianAssessmentAction::Execve {
|
||||
program, argv, cwd, ..
|
||||
} => {
|
||||
let id = assessment.target_item_id.as_ref()?;
|
||||
let argv = if argv.is_empty() {
|
||||
vec![program.clone()]
|
||||
} else {
|
||||
@@ -179,7 +182,7 @@ pub fn build_item_from_guardian_event(
|
||||
parsed_cmd.into_iter().map(CommandAction::from).collect()
|
||||
};
|
||||
Some(ThreadItem::CommandExecution {
|
||||
id: assessment.id.clone(),
|
||||
id: id.clone(),
|
||||
command,
|
||||
cwd: cwd.clone(),
|
||||
process_id: None,
|
||||
@@ -202,9 +205,6 @@ pub fn guardian_auto_approval_review_notification(
|
||||
event_turn_id: &str,
|
||||
assessment: &GuardianAssessmentEvent,
|
||||
) -> ServerNotification {
|
||||
// TODO(ccunningham): Attach guardian review state to the reviewed tool
|
||||
// item's lifecycle instead of sending standalone review notifications so
|
||||
// the app-server API can persist and replay review state via `thread/read`.
|
||||
let turn_id = if assessment.turn_id.is_empty() {
|
||||
event_turn_id.to_string()
|
||||
} else {
|
||||
@@ -236,7 +236,8 @@ pub fn guardian_auto_approval_review_notification(
|
||||
ItemGuardianApprovalReviewStartedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id,
|
||||
target_item_id: assessment.id.clone(),
|
||||
review_id: assessment.id.clone(),
|
||||
target_item_id: assessment.target_item_id.clone(),
|
||||
review,
|
||||
action,
|
||||
},
|
||||
@@ -249,7 +250,12 @@ pub fn guardian_auto_approval_review_notification(
|
||||
ItemGuardianApprovalReviewCompletedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id,
|
||||
target_item_id: assessment.id.clone(),
|
||||
review_id: assessment.id.clone(),
|
||||
target_item_id: assessment.target_item_id.clone(),
|
||||
decision_source: assessment
|
||||
.decision_source
|
||||
.map(AutoReviewDecisionSource::from)
|
||||
.unwrap_or(AutoReviewDecisionSource::Agent),
|
||||
review,
|
||||
action,
|
||||
},
|
||||
|
||||
@@ -2088,12 +2088,14 @@ mod tests {
|
||||
local_images: Vec::new(),
|
||||
}),
|
||||
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||
id: "guardian-exec".into(),
|
||||
id: "review-guardian-exec".into(),
|
||||
target_item_id: Some("guardian-exec".into()),
|
||||
turn_id: "turn-1".into(),
|
||||
status: GuardianAssessmentStatus::InProgress,
|
||||
risk_level: None,
|
||||
user_authorization: None,
|
||||
rationale: None,
|
||||
decision_source: None,
|
||||
action: serde_json::from_value(serde_json::json!({
|
||||
"type": "command",
|
||||
"source": "shell",
|
||||
@@ -2103,12 +2105,16 @@ mod tests {
|
||||
.expect("guardian action"),
|
||||
}),
|
||||
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||
id: "guardian-exec".into(),
|
||||
id: "review-guardian-exec".into(),
|
||||
target_item_id: Some("guardian-exec".into()),
|
||||
turn_id: "turn-1".into(),
|
||||
status: GuardianAssessmentStatus::Denied,
|
||||
risk_level: Some(codex_protocol::protocol::GuardianRiskLevel::High),
|
||||
user_authorization: Some(codex_protocol::protocol::GuardianUserAuthorization::Low),
|
||||
rationale: Some("Would delete user data.".into()),
|
||||
decision_source: Some(
|
||||
codex_protocol::protocol::GuardianAssessmentDecisionSource::Agent,
|
||||
),
|
||||
action: serde_json::from_value(serde_json::json!({
|
||||
"type": "command",
|
||||
"source": "shell",
|
||||
@@ -2161,12 +2167,14 @@ mod tests {
|
||||
local_images: Vec::new(),
|
||||
}),
|
||||
EventMsg::GuardianAssessment(GuardianAssessmentEvent {
|
||||
id: "guardian-execve".into(),
|
||||
id: "review-guardian-execve".into(),
|
||||
target_item_id: Some("guardian-execve".into()),
|
||||
turn_id: "turn-1".into(),
|
||||
status: GuardianAssessmentStatus::InProgress,
|
||||
risk_level: None,
|
||||
user_authorization: None,
|
||||
rationale: None,
|
||||
decision_source: None,
|
||||
action: serde_json::from_value(serde_json::json!({
|
||||
"type": "execve",
|
||||
"source": "shell",
|
||||
|
||||
@@ -9,6 +9,7 @@ use codex_protocol::account::PlanType;
|
||||
use codex_protocol::approvals::ElicitationRequest as CoreElicitationRequest;
|
||||
use codex_protocol::approvals::ExecPolicyAmendment as CoreExecPolicyAmendment;
|
||||
use codex_protocol::approvals::GuardianAssessmentAction as CoreGuardianAssessmentAction;
|
||||
use codex_protocol::approvals::GuardianAssessmentDecisionSource as CoreGuardianAssessmentDecisionSource;
|
||||
use codex_protocol::approvals::GuardianCommandSource as CoreGuardianCommandSource;
|
||||
use codex_protocol::approvals::NetworkApprovalContext as CoreNetworkApprovalContext;
|
||||
use codex_protocol::approvals::NetworkApprovalProtocol as CoreNetworkApprovalProtocol;
|
||||
@@ -4565,6 +4566,22 @@ pub enum GuardianApprovalReviewStatus {
|
||||
Aborted,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
/// [UNSTABLE] Source that produced a terminal guardian approval review decision.
|
||||
pub enum AutoReviewDecisionSource {
|
||||
Agent,
|
||||
}
|
||||
|
||||
impl From<CoreGuardianAssessmentDecisionSource> for AutoReviewDecisionSource {
|
||||
fn from(value: CoreGuardianAssessmentDecisionSource) -> Self {
|
||||
match value {
|
||||
CoreGuardianAssessmentDecisionSource::Agent => Self::Agent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -5321,14 +5338,23 @@ pub struct ItemStartedNotification {
|
||||
#[ts(export_to = "v2/")]
|
||||
/// [UNSTABLE] Temporary notification payload for guardian automatic approval
|
||||
/// review. This shape is expected to change soon.
|
||||
///
|
||||
/// TODO(ccunningham): Attach guardian review state to the reviewed tool item's
|
||||
/// lifecycle instead of sending separate standalone review notifications so the
|
||||
/// app-server API can persist and replay review state via `thread/read`.
|
||||
pub struct ItemGuardianApprovalReviewStartedNotification {
|
||||
pub thread_id: String,
|
||||
pub turn_id: String,
|
||||
pub target_item_id: String,
|
||||
/// Stable identifier for this review.
|
||||
pub review_id: String,
|
||||
/// Identifier for the reviewed item or tool call when one exists.
|
||||
///
|
||||
/// In most cases, one review maps to one target item. The exceptions are
|
||||
/// - execve reviews, where a single command may contain multiple execve
|
||||
/// calls to review (only possible when using the shell_zsh_fork feature)
|
||||
/// - network policy reviews, where there is no target item
|
||||
///
|
||||
/// A network call is triggered by a CommandExecution item, so having a
|
||||
/// target_item_id set to the CommandExecution item would be misleading
|
||||
/// because the review is about the network call, not the command execution.
|
||||
/// Therefore, target_item_id is set to None for network policy reviews.
|
||||
pub target_item_id: Option<String>,
|
||||
pub review: GuardianApprovalReview,
|
||||
pub action: GuardianApprovalReviewAction,
|
||||
}
|
||||
@@ -5338,14 +5364,24 @@ pub struct ItemGuardianApprovalReviewStartedNotification {
|
||||
#[ts(export_to = "v2/")]
|
||||
/// [UNSTABLE] Temporary notification payload for guardian automatic approval
|
||||
/// review. This shape is expected to change soon.
|
||||
///
|
||||
/// TODO(ccunningham): Attach guardian review state to the reviewed tool item's
|
||||
/// lifecycle instead of sending separate standalone review notifications so the
|
||||
/// app-server API can persist and replay review state via `thread/read`.
|
||||
pub struct ItemGuardianApprovalReviewCompletedNotification {
|
||||
pub thread_id: String,
|
||||
pub turn_id: String,
|
||||
pub target_item_id: String,
|
||||
/// Stable identifier for this review.
|
||||
pub review_id: String,
|
||||
/// Identifier for the reviewed item or tool call when one exists.
|
||||
///
|
||||
/// In most cases, one review maps to one target item. The exceptions are
|
||||
/// - execve reviews, where a single command may contain multiple execve
|
||||
/// calls to review (only possible when using the shell_zsh_fork feature)
|
||||
/// - network policy reviews, where there is no target item
|
||||
///
|
||||
/// A network call is triggered by a CommandExecution item, so having a
|
||||
/// target_item_id set to the CommandExecution item would be misleading
|
||||
/// because the review is about the network call, not the command execution.
|
||||
/// Therefore, target_item_id is set to None for network policy reviews.
|
||||
pub target_item_id: Option<String>,
|
||||
pub decision_source: AutoReviewDecisionSource,
|
||||
pub review: GuardianApprovalReview,
|
||||
pub action: GuardianApprovalReviewAction,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user