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    }
63
64    #[test]
65    fn test_dangerous_group_blocked_with_guarded() {
66        let config = SafetyConfig::new(false);
67        assert!(config.check_group(&ServiceGroup::Trade).is_err());
68        assert!(config.check_group(&ServiceGroup::Funding).is_err());
69    }
70
71    #[test]
72    fn test_dangerous_group_allowed_with_allow_dangerous() {
73        let config = SafetyConfig::new(true);
74        assert!(config.check_group(&ServiceGroup::Trade).is_ok());
75        assert!(config.check_group(&ServiceGroup::Funding).is_ok());
76    }
77
78    #[test]
79    fn test_operation_blocked_without_acknowledged() {
80        let config = SafetyConfig::new(false);
81        assert!(config.check_operation(&ServiceGroup::Trade, false).is_err());
82        assert!(config.check_operation(&ServiceGroup::Funding, false).is_err());
83    }
84
85    #[test]
86    fn test_operation_allowed_with_acknowledged() {
87        let config = SafetyConfig::new(false);
88        assert!(config.check_operation(&ServiceGroup::Trade, true).is_ok());
89        assert!(config.check_operation(&ServiceGroup::Funding, true).is_ok());
90    }
91
92    #[test]
93    fn test_safe_operation_no_acknowledgment_needed() {
94        let config = SafetyConfig::new(false);
95        assert!(config.check_operation(&ServiceGroup::Market, false).is_ok());
96        assert!(config.check_operation(&ServiceGroup::Account, false).is_ok());
97        assert!(config.check_operation(&ServiceGroup::Paper, false).is_ok());
98    }
99
100    #[test]
101    fn test_allow_dangerous_overrides_acknowledged() {
102        let config = SafetyConfig::new(true);
103        // Even without acknowledged, dangerous ops are allowed
104        assert!(config.check_operation(&ServiceGroup::Trade, false).is_ok());
105        assert!(config.check_operation(&ServiceGroup::Funding, false).is_ok());
106    }
107
108    #[test]
109    fn test_error_message_mentions_acknowledged() {
110        let config = SafetyConfig::new(false);
111        let err = config.check_operation(&ServiceGroup::Trade, false).unwrap_err();
112        assert!(err.contains("acknowledged"));
113        assert!(err.contains("true"));
114    }
115}