mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
fix(guardian): make GuardianAssessmentEvent.action strongly typed (#16448)
## Description Previously the `action` field on `EventMsg::GuardianAssessment`, which describes what Guardian is reviewing, was typed as an arbitrary JSON blob. This PR cleans it up and defines a sum type representing all the various actions that Guardian can review. This is a breaking change (on purpose), which is fine because: - the Codex app / VSCE does not actually use `action` at the moment - the TUI code that consumes `action` is updated in this PR as well - rollout files that serialized old `EventMsg::GuardianAssessment` will just silently drop these guardian events - the contract is defined as unstable, so other clients have a fair warning :) This will make things much easier for followup Guardian work. ## Why The old guardian review payloads worked, but they pushed too much shape knowledge into downstream consumers. The TUI had custom JSON parsing logic for commands, patches, network requests, and MCP calls, and the app-server protocol was effectively just passing through an opaque blob. Typing this at the protocol boundary makes the contract clearer.
This commit is contained in:
@@ -223,6 +223,7 @@ fn guardian_auto_approval_review_notification(
|
||||
risk_level: assessment.risk_level.map(Into::into),
|
||||
rationale: assessment.rationale.clone(),
|
||||
};
|
||||
let action = assessment.action.clone().into();
|
||||
match assessment.status {
|
||||
codex_protocol::protocol::GuardianAssessmentStatus::InProgress => {
|
||||
ServerNotification::ItemGuardianApprovalReviewStarted(
|
||||
@@ -231,7 +232,7 @@ fn guardian_auto_approval_review_notification(
|
||||
turn_id,
|
||||
target_item_id: assessment.id.clone(),
|
||||
review,
|
||||
action: assessment.action.clone(),
|
||||
action,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -244,7 +245,7 @@ fn guardian_auto_approval_review_notification(
|
||||
turn_id,
|
||||
target_item_id: assessment.id.clone(),
|
||||
review,
|
||||
action: assessment.action.clone(),
|
||||
action,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -2904,10 +2905,11 @@ mod tests {
|
||||
#[test]
|
||||
fn guardian_assessment_started_uses_event_turn_id_fallback() {
|
||||
let conversation_id = ThreadId::new();
|
||||
let action = json!({
|
||||
"tool": "shell",
|
||||
"command": "rm -rf /tmp/example.sqlite",
|
||||
});
|
||||
let action = codex_protocol::protocol::GuardianAssessmentAction::Command {
|
||||
source: codex_protocol::protocol::GuardianCommandSource::Shell,
|
||||
command: "rm -rf /tmp/example.sqlite".to_string(),
|
||||
cwd: "/tmp".into(),
|
||||
};
|
||||
let notification = guardian_auto_approval_review_notification(
|
||||
&conversation_id,
|
||||
"turn-from-event",
|
||||
@@ -2918,7 +2920,7 @@ mod tests {
|
||||
risk_score: None,
|
||||
risk_level: None,
|
||||
rationale: None,
|
||||
action: Some(action.clone()),
|
||||
action: action.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -2934,7 +2936,7 @@ mod tests {
|
||||
assert_eq!(payload.review.risk_score, None);
|
||||
assert_eq!(payload.review.risk_level, None);
|
||||
assert_eq!(payload.review.rationale, None);
|
||||
assert_eq!(payload.action, Some(action));
|
||||
assert_eq!(payload.action, action.into());
|
||||
}
|
||||
other => panic!("unexpected notification: {other:?}"),
|
||||
}
|
||||
@@ -2943,10 +2945,11 @@ mod tests {
|
||||
#[test]
|
||||
fn guardian_assessment_completed_emits_review_payload() {
|
||||
let conversation_id = ThreadId::new();
|
||||
let action = json!({
|
||||
"tool": "shell",
|
||||
"command": "rm -rf /tmp/example.sqlite",
|
||||
});
|
||||
let action = codex_protocol::protocol::GuardianAssessmentAction::Command {
|
||||
source: codex_protocol::protocol::GuardianCommandSource::Shell,
|
||||
command: "rm -rf /tmp/example.sqlite".to_string(),
|
||||
cwd: "/tmp".into(),
|
||||
};
|
||||
let notification = guardian_auto_approval_review_notification(
|
||||
&conversation_id,
|
||||
"turn-from-event",
|
||||
@@ -2957,7 +2960,7 @@ mod tests {
|
||||
risk_score: Some(91),
|
||||
risk_level: Some(codex_protocol::protocol::GuardianRiskLevel::High),
|
||||
rationale: Some("too risky".to_string()),
|
||||
action: Some(action.clone()),
|
||||
action: action.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -2973,7 +2976,7 @@ mod tests {
|
||||
Some(codex_app_server_protocol::GuardianRiskLevel::High)
|
||||
);
|
||||
assert_eq!(payload.review.rationale.as_deref(), Some("too risky"));
|
||||
assert_eq!(payload.action, Some(action));
|
||||
assert_eq!(payload.action, action.into());
|
||||
}
|
||||
other => panic!("unexpected notification: {other:?}"),
|
||||
}
|
||||
@@ -2982,10 +2985,12 @@ mod tests {
|
||||
#[test]
|
||||
fn guardian_assessment_aborted_emits_completed_review_payload() {
|
||||
let conversation_id = ThreadId::new();
|
||||
let action = json!({
|
||||
"tool": "network_access",
|
||||
"target": "api.openai.com:443",
|
||||
});
|
||||
let action = codex_protocol::protocol::GuardianAssessmentAction::NetworkAccess {
|
||||
target: "api.openai.com:443".to_string(),
|
||||
host: "api.openai.com".to_string(),
|
||||
protocol: codex_protocol::protocol::NetworkApprovalProtocol::Https,
|
||||
port: 443,
|
||||
};
|
||||
let notification = guardian_auto_approval_review_notification(
|
||||
&conversation_id,
|
||||
"turn-from-event",
|
||||
@@ -2996,7 +3001,7 @@ mod tests {
|
||||
risk_score: None,
|
||||
risk_level: None,
|
||||
rationale: None,
|
||||
action: Some(action.clone()),
|
||||
action: action.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -3009,7 +3014,7 @@ mod tests {
|
||||
assert_eq!(payload.review.risk_score, None);
|
||||
assert_eq!(payload.review.risk_level, None);
|
||||
assert_eq!(payload.review.rationale, None);
|
||||
assert_eq!(payload.action, Some(action));
|
||||
assert_eq!(payload.action, action.into());
|
||||
}
|
||||
other => panic!("unexpected notification: {other:?}"),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user