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:
Owen Lin
2026-04-01 15:42:18 -07:00
committed by GitHub
parent f83f3fa2a6
commit 30f6786d62
30 changed files with 1869 additions and 616 deletions
+102 -6
View File
@@ -98,6 +98,47 @@ pub enum GuardianAssessmentStatus {
Aborted,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum GuardianCommandSource {
Shell,
UnifiedExec,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "snake_case")]
#[ts(tag = "type", rename_all = "snake_case")]
pub enum GuardianAssessmentAction {
Command {
source: GuardianCommandSource,
command: String,
cwd: PathBuf,
},
Execve {
source: GuardianCommandSource,
program: String,
argv: Vec<String>,
cwd: PathBuf,
},
ApplyPatch {
cwd: PathBuf,
files: Vec<PathBuf>,
},
NetworkAccess {
target: String,
host: String,
protocol: NetworkApprovalProtocol,
port: u16,
},
McpToolCall {
server: String,
tool_name: String,
connector_id: Option<String>,
connector_name: Option<String>,
tool_title: Option<String>,
},
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
pub struct NetworkPolicyAmendment {
pub host: String,
@@ -125,12 +166,8 @@ pub struct GuardianAssessmentEvent {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub rationale: Option<String>,
/// Canonical action payload that was reviewed. Included when available so
/// clients can render pending or resolved review state alongside the
/// reviewed request.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub action: Option<JsonValue>,
/// Canonical action payload that was reviewed.
pub action: GuardianAssessmentAction,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
@@ -303,3 +340,62 @@ pub struct ApplyPatchApprovalRequestEvent {
#[serde(skip_serializing_if = "Option::is_none")]
pub grant_root: Option<PathBuf>,
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn guardian_assessment_action_deserializes_command_shape() {
let action: GuardianAssessmentAction = serde_json::from_value(serde_json::json!({
"type": "command",
"source": "shell",
"command": "rm -rf /tmp/guardian",
"cwd": "/tmp",
}))
.expect("guardian action");
assert_eq!(
action,
GuardianAssessmentAction::Command {
source: GuardianCommandSource::Shell,
command: "rm -rf /tmp/guardian".to_string(),
cwd: PathBuf::from("/tmp"),
}
);
}
#[cfg(unix)]
#[test]
fn guardian_assessment_action_round_trips_execve_shape() {
let value = serde_json::json!({
"type": "execve",
"source": "shell",
"program": "/bin/rm",
"argv": ["/usr/bin/rm", "-f", "/tmp/file.sqlite"],
"cwd": "/tmp",
});
let action: GuardianAssessmentAction =
serde_json::from_value(value.clone()).expect("guardian action");
assert_eq!(
serde_json::to_value(&action).expect("serialize guardian action"),
value
);
assert_eq!(
action,
GuardianAssessmentAction::Execve {
source: GuardianCommandSource::Shell,
program: "/bin/rm".to_string(),
argv: vec![
"/usr/bin/rm".to_string(),
"-f".to_string(),
"/tmp/file.sqlite".to_string(),
],
cwd: PathBuf::from("/tmp"),
}
);
}
}
+2
View File
@@ -64,8 +64,10 @@ pub use crate::approvals::ApplyPatchApprovalRequestEvent;
pub use crate::approvals::ElicitationAction;
pub use crate::approvals::ExecApprovalRequestEvent;
pub use crate::approvals::ExecPolicyAmendment;
pub use crate::approvals::GuardianAssessmentAction;
pub use crate::approvals::GuardianAssessmentEvent;
pub use crate::approvals::GuardianAssessmentStatus;
pub use crate::approvals::GuardianCommandSource;
pub use crate::approvals::GuardianRiskLevel;
pub use crate::approvals::NetworkApprovalContext;
pub use crate::approvals::NetworkApprovalProtocol;