1use std::path::{Path, PathBuf};
9
10#[derive(Debug, Clone)]
14pub struct LogConfig {
15 pub log_persistence: String,
16 pub log_persistence_path: String,
17 pub log_persistence_max_entries: usize,
18 pub log_tool_io: String,
19 pub log_tool_io_truncate_bytes: usize,
20 pub log_tool_io_denylist: Vec<String>,
21}
22
23impl Default for LogConfig {
24 fn default() -> Self {
25 Self {
26 log_persistence: "rolling".into(),
27 log_persistence_path: String::new(),
28 log_persistence_max_entries: 10_000,
29 log_tool_io: "redacted".into(),
30 log_tool_io_truncate_bytes: 2048,
31 log_tool_io_denylist: Vec::new(),
32 }
33 }
34}
35
36const DEFAULT_LOG_REL_PATH: &str = "state/runtime-trace.jsonl";
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum StoragePolicy {
41 None,
43 Rolling,
45 Full,
47}
48
49impl StoragePolicy {
50 pub fn from_raw(raw: &str) -> Self {
51 match raw.trim().to_ascii_lowercase().as_str() {
52 "rolling" => Self::Rolling,
53 "full" => Self::Full,
54 _ => Self::None,
55 }
56 }
57
58 pub fn is_enabled(self) -> bool {
59 !matches!(self, Self::None)
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum ToolIoPolicy {
66 Off,
68 Redacted,
70 Full,
72}
73
74impl ToolIoPolicy {
75 pub fn from_raw(raw: &str) -> Self {
76 match raw.trim().to_ascii_lowercase().as_str() {
77 "off" => Self::Off,
78 "full" => Self::Full,
79 _ => Self::Redacted,
80 }
81 }
82
83 pub fn captures_io(self) -> bool {
84 !matches!(self, Self::Off)
85 }
86}
87
88#[derive(Debug, Clone)]
90pub struct ResolvedPolicy {
91 pub storage: StoragePolicy,
92 pub path: PathBuf,
93 pub max_entries: usize,
94 pub tool_io: ToolIoPolicy,
95 pub tool_io_truncate_bytes: usize,
96 pub tool_io_denylist: Vec<String>,
97}
98
99impl ResolvedPolicy {
100 pub fn from_config(config: &LogConfig, workspace_dir: &Path) -> Self {
101 Self {
102 storage: StoragePolicy::from_raw(&config.log_persistence),
103 path: resolve_path(&config.log_persistence_path, workspace_dir),
104 max_entries: config.log_persistence_max_entries.max(1),
105 tool_io: ToolIoPolicy::from_raw(&config.log_tool_io),
106 tool_io_truncate_bytes: config.log_tool_io_truncate_bytes,
107 tool_io_denylist: config.log_tool_io_denylist.clone(),
108 }
109 }
110
111 pub fn is_tool_denylisted(&self, tool: &str) -> bool {
112 self.tool_io_denylist.iter().any(|t| t == tool)
113 }
114}
115
116fn resolve_path(raw: &str, workspace_dir: &Path) -> PathBuf {
117 let raw = raw.trim();
118 if raw.is_empty() {
119 return workspace_dir.join(DEFAULT_LOG_REL_PATH);
120 }
121 let p = PathBuf::from(raw);
122 if p.is_absolute() {
123 p
124 } else {
125 workspace_dir.join(p)
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 fn make_config() -> LogConfig {
134 LogConfig::default()
135 }
136
137 #[test]
138 fn storage_policy_parses_known() {
139 assert_eq!(StoragePolicy::from_raw("none"), StoragePolicy::None);
140 assert_eq!(StoragePolicy::from_raw("rolling"), StoragePolicy::Rolling);
141 assert_eq!(StoragePolicy::from_raw("full"), StoragePolicy::Full);
142 assert_eq!(StoragePolicy::from_raw("xyz"), StoragePolicy::None);
143 }
144
145 #[test]
146 fn tool_io_policy_defaults_to_redacted() {
147 assert_eq!(ToolIoPolicy::from_raw(""), ToolIoPolicy::Redacted);
148 assert_eq!(ToolIoPolicy::from_raw("redacted"), ToolIoPolicy::Redacted);
149 assert_eq!(ToolIoPolicy::from_raw("off"), ToolIoPolicy::Off);
150 assert_eq!(ToolIoPolicy::from_raw("full"), ToolIoPolicy::Full);
151 }
152
153 #[test]
154 fn resolved_policy_uses_workspace_default_when_path_empty() {
155 let mut c = make_config();
156 c.log_persistence_path = String::new();
157 let tmp = tempfile::tempdir().unwrap();
158 let p = ResolvedPolicy::from_config(&c, tmp.path());
159 assert_eq!(p.path, tmp.path().join(DEFAULT_LOG_REL_PATH));
160 }
161
162 #[test]
163 fn resolved_policy_respects_denylist() {
164 let mut c = make_config();
165 c.log_tool_io_denylist = vec!["memory_recall_personal".to_string()];
166 let p = ResolvedPolicy::from_config(&c, std::path::Path::new("/"));
167 assert!(p.is_tool_denylisted("memory_recall_personal"));
168 assert!(!p.is_tool_denylisted("shell"));
169 }
170}