Skip to main content

indodax_cli/mcp/
safety.rs

1use super::service::ServiceGroup;
2
3/// Configuration for safety/guarding of dangerous MCP operations.
4#[derive(Debug, Clone)]
5pub struct SafetyConfig {
6    /// If true, dangerous operations (trade, funding) are allowed without
7    /// the `acknowledged` parameter being true.
8    pub allow_dangerous: bool,
9}
10
11impl SafetyConfig {
12    /// Create a new safety configuration.
13    ///
14    /// `allow_dangerous` should be true only when the user explicitly opts in
15    /// (e.g., via `--allow-dangerous` CLI flag).
16    pub fn new(allow_dangerous: bool) -> Self {
17        Self { allow_dangerous }
18    }
19
20    /// Check whether an operation in the given service group is allowed.
21    ///
22    /// Returns `Ok(())` if allowed, or `Err(message)` with guidance.
23    pub fn check_group(&self, group: &ServiceGroup) -> Result<(), String> {
24        if group.is_dangerous() && !self.allow_dangerous {
25            return Err(format!(
26                "[DANGEROUS: requires acknowledged=true] The '{}' service group contains dangerous operations. \
27                 Provide `acknowledged: true` in your tool call arguments, or start the MCP server with \
28                 `--allow-dangerous` to disable this guard.",
29                group
30            ));
31        }
32        Ok(())
33    }
34
35    /// Check whether a dangerous operation is allowed with the given acknowledged flag.
36    ///
37    /// Must provide `acknowledged: true` for dangerous groups unless `allow_dangerous` is set.
38    pub fn check_operation(&self, group: &ServiceGroup, acknowledged: bool) -> Result<(), String> {
39        if group.is_dangerous() && !self.allow_dangerous && !acknowledged {
40            return Err(format!(
41                "[DANGEROUS: requires acknowledged=true] This operation belongs to the '{}' group. \
42                 Set `acknowledged` to `true` to confirm you want to proceed, or start the MCP server \
43                 with `--allow-dangerous` to disable this check entirely.",
44                group
45            ));
46        }
47        Ok(())
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54
55    #[test]
56    fn test_safe_group_allowed_with_guarded() {
57        let config = SafetyConfig::new(false);
58        assert!(config.check_group(&ServiceGroup::Market).is_ok());
59        assert!(config.check_group(&ServiceGroup::Account).is_ok());
60        assert!(config.check_group(&ServiceGroup::Paper).is_ok());
61        assert!(config.check_group(&ServiceGroup::Auth).is_ok());
62        assert!(config.check_group(&ServiceGroup::Alert).is_ok());
63    }
64
65    #[test]
66    fn test_dangerous_group_blocked_with_guarded() {
67        let config = SafetyConfig::new(false);
68        assert!(config.check_group(&ServiceGroup::Trade).is_err());
69        assert!(config.check_group(&ServiceGroup::Funding).is_err());
70    }
71
72    #[test]
73    fn test_dangerous_group_allowed_with_allow_dangerous() {
74        let config = SafetyConfig::new(true);
75        assert!(config.check_group(&ServiceGroup::Trade).is_ok());
76        assert!(config.check_group(&ServiceGroup::Funding).is_ok());
77    }
78
79    #[test]
80    fn test_operation_blocked_without_acknowledged() {
81        let config = SafetyConfig::new(false);
82        assert!(config.check_operation(&ServiceGroup::Trade, false).is_err());
83        assert!(config.check_operation(&ServiceGroup::Funding, false).is_err());
84    }
85
86    #[test]
87    fn test_operation_allowed_with_acknowledged() {
88        let config = SafetyConfig::new(false);
89        assert!(config.check_operation(&ServiceGroup::Trade, true).is_ok());
90        assert!(config.check_operation(&ServiceGroup::Funding, true).is_ok());
91    }
92
93    #[test]
94    fn test_safe_operation_no_acknowledgment_needed() {
95        let config = SafetyConfig::new(false);
96        assert!(config.check_operation(&ServiceGroup::Market, false).is_ok());
97        assert!(config.check_operation(&ServiceGroup::Account, false).is_ok());
98        assert!(config.check_operation(&ServiceGroup::Paper, false).is_ok());
99    }
100
101    #[test]
102    fn test_allow_dangerous_overrides_acknowledged() {
103        let config = SafetyConfig::new(true);
104        // Even without acknowledged, dangerous ops are allowed
105        assert!(config.check_operation(&ServiceGroup::Trade, false).is_ok());
106        assert!(config.check_operation(&ServiceGroup::Funding, false).is_ok());
107    }
108
109    #[test]
110    fn test_error_message_mentions_acknowledged() {
111        let config = SafetyConfig::new(false);
112        let err = config.check_operation(&ServiceGroup::Trade, false).unwrap_err();
113        assert!(err.contains("acknowledged"));
114        assert!(err.contains("true"));
115    }
116}