use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PermissionDecision {
Permit,
Reject,
Prompt,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PermissionDecisionResult {
pub decision: PermissionDecision,
pub reason: String,
pub tool_name: String,
pub description: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
#[serde(default)]
pub reviewable: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PermissionGateRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
pub tool_call_id: String,
pub tool_name: String,
pub description: String,
pub input: Value,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PermissionGateResponse {
pub allow: bool,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ExecPolicyAmendment {
pub command: Vec<String>,
}
impl ExecPolicyAmendment {
pub fn new(command: Vec<String>) -> Self {
Self { command }
}
pub fn command(&self) -> &[String] {
&self.command
}
}
impl From<Vec<String>> for ExecPolicyAmendment {
fn from(command: Vec<String>) -> Self {
Self { command }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NetworkApprovalProtocol {
Http,
Https,
Socks5Tcp,
Socks5Udp,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NetworkApprovalContext {
pub host: String,
pub protocol: NetworkApprovalProtocol,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NetworkPolicyRuleAction {
Allow,
Deny,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NetworkPolicyAmendment {
pub host: String,
pub action: NetworkPolicyRuleAction,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PendingApprovalContext {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
pub tool_name: String,
#[serde(default)]
pub command: Option<String>,
#[serde(default)]
pub network: Option<NetworkApprovalContext>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub payload_digest: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum GuardianRiskLevel {
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum GuardianUserAuthorization {
Unknown,
Low,
Medium,
High,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum GuardianAssessmentOutcome {
Allow,
Deny,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GuardianAssessmentStatus {
InProgress,
Approved,
Denied,
TimedOut,
Aborted,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GuardianAssessmentDecisionSource {
Agent,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GuardianCommandSource {
Shell,
UnifiedExec,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum GuardianAssessmentAction {
Command {
source: GuardianCommandSource,
command: String,
cwd: String,
},
Execve {
source: GuardianCommandSource,
program: String,
argv: Vec<String>,
cwd: String,
},
WriteStdin {
approval_id: String,
process_id: String,
stdin: String,
cwd: String,
},
ApplyPatch {
cwd: String,
files: Vec<String>,
},
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>,
},
RequestPermissions {
reason: Option<String>,
permissions: Value,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GuardianAssessmentEvent {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_item_id: Option<String>,
#[serde(default)]
pub turn_id: String,
#[serde(default)]
pub started_at_ms: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub completed_at_ms: Option<i64>,
pub status: GuardianAssessmentStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub risk_level: Option<GuardianRiskLevel>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub user_authorization: Option<GuardianUserAuthorization>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rationale: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub decision_source: Option<GuardianAssessmentDecisionSource>,
pub action: GuardianAssessmentAction,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExecApprovalKind {
#[default]
Command,
WriteStdin,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReviewDecision {
Approved,
ApprovedForSession,
ApprovedExecpolicyAmendment {
proposed_execpolicy_amendment: ExecPolicyAmendment,
},
NetworkPolicyAmendment {
network_policy_amendment: NetworkPolicyAmendment,
},
Abort,
}
impl ReviewDecision {
pub const fn is_approved(&self) -> bool {
matches!(
self,
Self::Approved
| Self::ApprovedForSession
| Self::ApprovedExecpolicyAmendment { .. }
| Self::NetworkPolicyAmendment { .. }
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExecApprovalRequestEvent {
#[serde(default)]
pub kind: ExecApprovalKind,
pub call_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub approval_id: Option<String>,
#[serde(default)]
pub turn_id: String,
pub started_at_ms: i64,
pub command: Vec<String>,
pub cwd: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub network_approval_context: Option<NetworkApprovalContext>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub proposed_network_policy_amendments: Option<Vec<NetworkPolicyAmendment>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub available_decisions: Option<Vec<ReviewDecision>>,
}
impl ExecApprovalRequestEvent {
pub fn effective_approval_id(&self) -> String {
self.approval_id
.clone()
.unwrap_or_else(|| self.call_id.clone())
}
pub fn effective_available_decisions(&self) -> Vec<ReviewDecision> {
self.available_decisions.clone().unwrap_or_else(|| {
let mut out = vec![ReviewDecision::Approved];
if let Some(prefix) = &self.proposed_execpolicy_amendment {
out.push(ReviewDecision::ApprovedExecpolicyAmendment {
proposed_execpolicy_amendment: prefix.clone(),
});
}
out.push(ReviewDecision::Abort);
out
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(
tag = "mode",
rename_all = "snake_case",
rename_all_fields = "camelCase"
)]
pub enum ElicitationRequest {
Form {
message: String,
requested_schema: Value,
},
Url {
message: String,
url: String,
elicitation_id: String,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ElicitationRequestEvent {
pub turn_id: Option<String>,
pub server_name: String,
pub id: String,
pub request: ElicitationRequest,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ElicitationAction {
Accept,
Decline,
Cancel,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exec_policy_amendment_is_transparent_array() {
let amendment = ExecPolicyAmendment::new(vec!["cargo".into(), "check".into()]);
assert_eq!(
serde_json::to_string(&amendment).unwrap(),
r#"["cargo","check"]"#
);
assert_eq!(
amendment.command(),
&["cargo".to_string(), "check".to_string()]
);
}
#[test]
fn approval_decisions_keep_codex_wire_tags() {
let value = serde_json::to_value(ReviewDecision::ApprovedForSession).unwrap();
assert_eq!(value, serde_json::json!("approved_for_session"));
let value = serde_json::to_value(NetworkApprovalProtocol::Https).unwrap();
assert_eq!(value, serde_json::json!("https"));
}
#[test]
fn approval_id_falls_back_to_call_id() {
let request = ExecApprovalRequestEvent {
kind: ExecApprovalKind::Command,
call_id: "call-1".into(),
approval_id: None,
turn_id: String::new(),
started_at_ms: 0,
command: vec!["echo".into()],
cwd: "/tmp".into(),
reason: None,
network_approval_context: None,
proposed_execpolicy_amendment: None,
proposed_network_policy_amendments: None,
available_decisions: None,
};
assert_eq!(request.effective_approval_id(), "call-1");
}
#[test]
fn san_permission_gate_contract_round_trips() {
let request = PermissionGateRequest {
request_id: Some("req-1".into()),
tool_call_id: "call-1".into(),
tool_name: "Bash".into(),
description: "run command".into(),
input: serde_json::json!({"command":"git status"}),
};
let value = serde_json::to_value(&request).unwrap();
assert_eq!(value["toolCallId"], "call-1");
assert_eq!(
serde_json::from_value::<PermissionDecision>(serde_json::json!("prompt")).unwrap(),
PermissionDecision::Prompt
);
}
}