Skip to main content

vtcode_config/
safety.rs

1//! Safety / audit configuration.
2//!
3//! This module wires the `vtcode_safety::audit_log` sinks into the top-level
4//! VT Code configuration. The struct is purely additive — existing configs
5//! that don't declare a `[safety.audit]` table get the defaults from
6//! [`ToolAuditConfig::default`].
7
8use std::path::PathBuf;
9use vtcode_commons::VtCodePaths;
10
11use serde::{Deserialize, Serialize};
12
13/// Configuration block for the persistent tool-call audit log.
14///
15/// ```toml
16/// [safety.audit]
17/// enabled = true
18/// path = "<state>/audit/tools.jsonl"
19/// max_size_bytes = 32 * 1024 * 1024
20/// max_files = 4
21/// ```
22#[derive(Debug, Clone, Default, Serialize, Deserialize)]
23pub struct SafetyConfig {
24    /// Audit log configuration. `None` ⇒ audit disabled, equivalent to a
25    /// disabled [`ToolAuditConfig`].
26    #[serde(default)]
27    audit: Option<ToolAuditConfig>,
28}
29
30/// Subset of `[safety.audit]` controlling the audit log sink.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct ToolAuditConfig {
33    /// When true, the runloop persists audit entries. Default `false`.
34    #[serde(default)]
35    enabled: bool,
36    /// Path of the JSONL file. Tilde expansion is performed at load time.
37    #[serde(default = "default_audit_path")]
38    path: PathBuf,
39    /// Rotation threshold in bytes. Default 32 MiB.
40    #[serde(default = "default_max_size_bytes")]
41    max_size_bytes: u64,
42    /// Maximum number of rotated files to keep (current file + N-1 archives).
43    /// Default `4`.
44    #[serde(default = "default_max_files")]
45    max_files: usize,
46}
47
48impl Default for ToolAuditConfig {
49    fn default() -> Self {
50        Self {
51            enabled: false,
52            path: default_audit_path(),
53            max_size_bytes: default_max_size_bytes(),
54            max_files: default_max_files(),
55        }
56    }
57}
58
59fn default_audit_path() -> PathBuf {
60    VtCodePaths::resolve()
61        .and_then(|paths| paths.state_path("audit/tools.jsonl"))
62        .unwrap_or_default()
63}
64
65fn default_max_size_bytes() -> u64 {
66    32 * 1024 * 1024
67}
68
69fn default_max_files() -> usize {
70    4
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn defaults_disable_audit() {
79        let cfg = ToolAuditConfig::default();
80        assert!(!cfg.enabled);
81        assert_eq!(cfg.max_files, 4);
82        assert_eq!(cfg.max_size_bytes, 32 * 1024 * 1024);
83    }
84
85    #[test]
86    fn safety_config_defaults_to_no_audit() {
87        let cfg = SafetyConfig::default();
88        assert!(cfg.audit.is_none());
89    }
90
91    #[test]
92    fn parses_minimal_toml() {
93        let parsed: ToolAuditConfig = toml::from_str(
94            r#"
95            enabled = true
96            path = "/tmp/audit.jsonl"
97            "#,
98        )
99        .expect("minimal toml");
100        assert!(parsed.enabled);
101        assert_eq!(parsed.path, PathBuf::from("/tmp/audit.jsonl"));
102        // Defaults apply to omitted fields.
103        assert_eq!(parsed.max_files, 4);
104        assert_eq!(parsed.max_size_bytes, 32 * 1024 * 1024);
105    }
106}