Skip to main content

zeph_config/memory/
consent_gate.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Write-time memory-consent gate configuration (issue #6490, `MemGhost`).
5//!
6//! See [`crate::memory::root::MemoryConfig::consent_gate`].
7
8use serde::{Deserialize, Serialize};
9
10fn default_confirm_threshold() -> String {
11    "external_untrusted".to_owned()
12}
13
14fn default_disclose_threshold() -> String {
15    "local_untrusted".to_owned()
16}
17
18/// Configuration for the write-time memory-consent gate, nested under `[memory.consent_gate]`
19/// in TOML (issue #6490).
20///
21/// Gates memory writes derived from untrusted content (tool output, web scrapes, MCP
22/// responses) behind either an interactive confirmation (`memory_save` tool path) or a
23/// visible in-turn disclosure note (autonomous background tool-output writes, which must
24/// never block on `Channel::confirm` per the non-blocking contract — see spec-039).
25///
26/// `confirm_threshold`/`disclose_threshold` accept the `snake_case` serialization of
27/// `zeph_sanitizer::ContentTrustLevel` (`"trusted"`, `"local_untrusted"`,
28/// `"external_untrusted"`). `zeph-config` cannot depend on `zeph-sanitizer` (the dependency
29/// runs the other way), so these are plain strings parsed by callers via
30/// `ContentTrustLevel::from_str_opt`.
31///
32/// # Example (TOML)
33///
34/// ```toml
35/// [memory.consent_gate]
36/// enabled = true
37/// confirm_threshold = "external_untrusted"
38/// disclose_threshold = "local_untrusted"
39/// audit_all = true
40/// ```
41#[derive(Debug, Clone, Deserialize, Serialize)]
42#[serde(default)]
43pub struct ConsentGateConfig {
44    /// Master switch. Default: `true`.
45    pub enabled: bool,
46    /// Minimum trust tier (inclusive) that requires interactive confirmation via
47    /// `Channel::confirm` on the `memory_save` tool path. Default: `"external_untrusted"`.
48    #[serde(default = "default_confirm_threshold")]
49    pub confirm_threshold: String,
50    /// Minimum trust tier (inclusive) that requires a visible in-turn disclosure note on
51    /// autonomous background tool-output memory writes. Default: `"local_untrusted"`.
52    #[serde(default = "default_disclose_threshold")]
53    pub disclose_threshold: String,
54    /// When `true`, every memory write is recorded in the audit log with source
55    /// attribution, regardless of trust tier. Default: `true`.
56    pub audit_all: bool,
57}
58
59impl Default for ConsentGateConfig {
60    fn default() -> Self {
61        Self {
62            enabled: true,
63            confirm_threshold: default_confirm_threshold(),
64            disclose_threshold: default_disclose_threshold(),
65            audit_all: true,
66        }
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn defaults_match_spec() {
76        let cfg = ConsentGateConfig::default();
77        assert!(cfg.enabled);
78        assert_eq!(cfg.confirm_threshold, "external_untrusted");
79        assert_eq!(cfg.disclose_threshold, "local_untrusted");
80        assert!(cfg.audit_all);
81    }
82
83    #[test]
84    fn deserializes_from_empty_table() {
85        let cfg: ConsentGateConfig = toml::from_str("").unwrap();
86        assert!(cfg.enabled);
87        assert_eq!(cfg.confirm_threshold, "external_untrusted");
88        assert_eq!(cfg.disclose_threshold, "local_untrusted");
89    }
90
91    #[test]
92    fn deserializes_explicit_values() {
93        let cfg: ConsentGateConfig = toml::from_str(
94            "enabled = false\nconfirm_threshold = \"local_untrusted\"\naudit_all = false\n",
95        )
96        .unwrap();
97        assert!(!cfg.enabled);
98        assert_eq!(cfg.confirm_threshold, "local_untrusted");
99        assert!(!cfg.audit_all);
100    }
101}