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:
Owen Lin
2026-04-10 16:21:02 -07:00
committed by GitHub
parent 7999b0f60f
commit a3be74143a
36 changed files with 577 additions and 172 deletions

View File

@@ -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",