1use serde::{Deserialize, Serialize};
2use time::OffsetDateTime;
3
4use crate::events::{ThreadId, TurnId};
5
6#[derive(
7 Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash,
8)]
9#[serde(rename_all = "snake_case")]
10pub enum PolicyMode {
11 #[default]
12 Default,
13 #[serde(alias = "accept_edits", alias = "accept-edits")]
14 AcceptAll,
15 Plan,
16 Bypass,
17}
18
19#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
20#[serde(transparent)]
21pub struct AutoApproveSet {
22 pub tools: Vec<String>,
23}
24
25impl AutoApproveSet {
26 pub fn empty() -> Self {
27 Self::default()
28 }
29
30 pub fn all() -> Self {
31 Self {
32 tools: vec!["*".to_string()],
33 }
34 }
35
36 pub fn accept_all() -> Self {
37 Self {
38 tools: vec![
39 "fs.write".to_string(),
40 "fs.edit".to_string(),
41 "fs.multi_edit".to_string(),
42 "apply_patch".to_string(),
43 "write_file".to_string(),
44 "edit".to_string(),
45 "multi_edit".to_string(),
46 "process.spawn".to_string(),
47 "shell".to_string(),
48 "exec_command".to_string(),
49 "write_stdin".to_string(),
50 "vcs/select".to_string(),
51 "vcs/snapshot/create".to_string(),
52 "vcs/restore".to_string(),
53 "vcs/lines/switch".to_string(),
54 "vcs/sync".to_string(),
55 ],
56 }
57 }
58
59 pub fn contains_tool(&self, tool_name: &str) -> bool {
60 self.tools
61 .iter()
62 .any(|tool| tool == "*" || tool == tool_name)
63 }
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
67pub struct PolicyModeConfig {
68 pub auto_approve: AutoApproveSet,
69 pub denied_tools: Vec<String>,
70 pub allow_writes: bool,
71 pub allow_process: bool,
72 pub allow_network: bool,
73 pub requires_user_to_exit: bool,
74}
75
76impl PolicyModeConfig {
77 pub fn for_mode(mode: PolicyMode) -> Self {
78 match mode {
79 PolicyMode::Default => Self::default_mode(),
80 PolicyMode::AcceptAll => Self::accept_all(),
81 PolicyMode::Plan => Self::plan(),
82 PolicyMode::Bypass => Self::bypass(),
83 }
84 }
85
86 pub fn default_mode() -> Self {
87 Self {
88 auto_approve: AutoApproveSet::empty(),
89 denied_tools: Vec::new(),
90 allow_writes: true,
91 allow_process: true,
92 allow_network: true,
93 requires_user_to_exit: false,
94 }
95 }
96
97 pub fn accept_all() -> Self {
98 Self {
99 auto_approve: AutoApproveSet::accept_all(),
100 denied_tools: Vec::new(),
101 allow_writes: true,
102 allow_process: true,
103 allow_network: true,
104 requires_user_to_exit: false,
105 }
106 }
107
108 pub fn plan() -> Self {
109 Self {
110 auto_approve: AutoApproveSet::empty(),
111 denied_tools: Vec::new(),
112 allow_writes: false,
113 allow_process: false,
114 allow_network: true,
115 requires_user_to_exit: false,
116 }
117 }
118
119 pub fn bypass() -> Self {
120 Self {
121 auto_approve: AutoApproveSet::all(),
122 denied_tools: Vec::new(),
123 allow_writes: true,
124 allow_process: true,
125 allow_network: true,
126 requires_user_to_exit: false,
127 }
128 }
129}
130
131impl Default for PolicyModeConfig {
132 fn default() -> Self {
133 Self::default_mode()
134 }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
138#[serde(rename_all = "snake_case", tag = "decision", content = "details")]
139pub enum PolicyDecision {
140 Allowed,
141 RequiresApproval { reason: Option<String> },
142 AutoApproved { matched_rule: Option<String> },
143 Denied { reason: String },
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct PolicyDecisionRecorded {
148 pub thread_id: ThreadId,
149 pub turn_id: TurnId,
150 pub tool_id: String,
151 pub tool_name: String,
152 pub mode: PolicyMode,
153 pub decision: PolicyDecision,
154 #[serde(with = "time::serde::rfc3339")]
155 pub timestamp: OffsetDateTime,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct PolicyBypassActive {
160 pub thread_id: ThreadId,
161 pub turn_id: TurnId,
162 pub tool_id: String,
163 pub tool_name: String,
164 #[serde(with = "time::serde::rfc3339")]
165 pub timestamp: OffsetDateTime,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct PolicyModeChanged {
170 pub thread_id: ThreadId,
171 pub turn_id: Option<TurnId>,
172 pub previous_mode: PolicyMode,
173 pub new_mode: PolicyMode,
174 pub reason: Option<String>,
175 #[serde(with = "time::serde::rfc3339")]
176 pub timestamp: OffsetDateTime,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct PolicyExitPlanRequested {
181 pub thread_id: ThreadId,
182 pub turn_id: TurnId,
183 pub request_id: String,
184 pub target_mode: PolicyMode,
185 pub plan_summary: Option<String>,
186 #[serde(default, skip_serializing_if = "Vec::is_empty")]
187 pub next_steps: Vec<String>,
188 #[serde(with = "time::serde::rfc3339")]
189 pub timestamp: OffsetDateTime,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct PolicyExitPlanResolved {
194 pub thread_id: ThreadId,
195 pub turn_id: TurnId,
196 pub request_id: String,
197 pub approved: bool,
198 pub target_mode: PolicyMode,
199 pub resolved_mode: PolicyMode,
200 #[serde(with = "time::serde::rfc3339")]
201 pub timestamp: OffsetDateTime,
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn policy_mode_serde_round_trips_as_snake_case() {
210 let serialized = serde_json::to_string(&PolicyMode::AcceptAll).unwrap();
211 assert_eq!(serialized, "\"accept_all\"");
212
213 let round_trip: PolicyMode = serde_json::from_str(&serialized).unwrap();
214 assert_eq!(round_trip, PolicyMode::AcceptAll);
215
216 let legacy: PolicyMode = serde_json::from_str("\"accept_edits\"").unwrap();
217 assert_eq!(legacy, PolicyMode::AcceptAll);
218 }
219
220 #[test]
221 fn policy_mode_config_serde_round_trips() {
222 let config = PolicyModeConfig {
223 auto_approve: AutoApproveSet {
224 tools: vec!["fs.write".to_string(), "network".to_string()],
225 },
226 denied_tools: vec!["process.spawn".to_string()],
227 allow_writes: true,
228 allow_process: false,
229 allow_network: true,
230 requires_user_to_exit: false,
231 };
232
233 let serialized = serde_json::to_string(&config).unwrap();
234 let round_trip: PolicyModeConfig = serde_json::from_str(&serialized).unwrap();
235
236 assert_eq!(round_trip, config);
237 assert!(round_trip.auto_approve.contains_tool("fs.write"));
238 assert!(!round_trip.auto_approve.contains_tool("fs.read"));
239 }
240
241 #[test]
242 fn policy_decision_serde_round_trips() {
243 let decision = PolicyDecision::RequiresApproval {
244 reason: Some("write access".to_string()),
245 };
246
247 let serialized = serde_json::to_string(&decision).unwrap();
248 let round_trip: PolicyDecision = serde_json::from_str(&serialized).unwrap();
249
250 assert_eq!(round_trip, decision);
251 }
252
253 #[test]
254 fn policy_event_payloads_round_trip() {
255 let event = PolicyExitPlanRequested {
256 thread_id: "thread-a".to_string(),
257 turn_id: "turn-a".to_string(),
258 request_id: "exit-1".to_string(),
259 target_mode: PolicyMode::Default,
260 plan_summary: Some("Implement the approved edits.".to_string()),
261 next_steps: vec!["edit files".to_string(), "run tests".to_string()],
262 timestamp: OffsetDateTime::UNIX_EPOCH,
263 };
264
265 let serialized = serde_json::to_string(&event).unwrap();
266 let round_trip: PolicyExitPlanRequested = serde_json::from_str(&serialized).unwrap();
267
268 assert_eq!(round_trip.request_id, "exit-1");
269 assert_eq!(round_trip.target_mode, PolicyMode::Default);
270 assert_eq!(
271 round_trip.plan_summary.as_deref(),
272 Some("Implement the approved edits.")
273 );
274 assert_eq!(round_trip.next_steps, ["edit files", "run tests"]);
275 }
276
277 #[test]
278 fn policy_mode_default_presets_match_contract() {
279 let default = PolicyModeConfig::for_mode(PolicyMode::Default);
280 assert_eq!(default.auto_approve, AutoApproveSet::empty());
281 assert!(default.allow_writes);
282 assert!(default.allow_process);
283 assert!(default.allow_network);
284 assert!(!default.requires_user_to_exit);
285
286 let accept_all = PolicyModeConfig::for_mode(PolicyMode::AcceptAll);
287 assert!(accept_all.auto_approve.contains_tool("fs.write"));
288 assert!(accept_all.auto_approve.contains_tool("fs.edit"));
289 assert!(accept_all.auto_approve.contains_tool("fs.multi_edit"));
290 assert!(accept_all.auto_approve.contains_tool("apply_patch"));
291 assert!(accept_all.auto_approve.contains_tool("write_file"));
292 assert!(accept_all.auto_approve.contains_tool("edit"));
293 assert!(accept_all.auto_approve.contains_tool("multi_edit"));
294 assert!(accept_all.auto_approve.contains_tool("process.spawn"));
295 assert!(accept_all.auto_approve.contains_tool("shell"));
296 assert!(accept_all.auto_approve.contains_tool("exec_command"));
297 assert!(accept_all.auto_approve.contains_tool("write_stdin"));
298 assert!(accept_all.allow_writes);
299 assert!(accept_all.allow_process);
300 assert!(accept_all.allow_network);
301
302 let plan = PolicyModeConfig::for_mode(PolicyMode::Plan);
303 assert_eq!(plan.auto_approve, AutoApproveSet::empty());
304 assert!(!plan.allow_writes);
305 assert!(!plan.allow_process);
306 assert!(plan.allow_network);
307 assert!(!plan.requires_user_to_exit);
308
309 let bypass = PolicyModeConfig::for_mode(PolicyMode::Bypass);
310 assert!(bypass.auto_approve.contains_tool("any.tool"));
311 assert!(bypass.allow_writes);
312 assert!(bypass.allow_process);
313 assert!(bypass.allow_network);
314 assert!(!bypass.requires_user_to_exit);
315 }
316}