Skip to main content

brainos_core/
security.rs

1//! Cross-cutting security primitives shared by audit, confirm, sandbox,
2//! and orchestrator. Lives here to keep the type single-sourced — every
3//! consumer crate already depends on `brain_core`, so promoting these
4//! to the leaf avoids the previous three-way duplication and the manual
5//! `convert_tier()` shims that came with it.
6
7use serde::{Deserialize, Serialize};
8
9/// Action tier determines confirmation requirement and timeout policy.
10///
11/// Mirrors VISION §6 (human in the loop): each tier maps to a distinct
12/// approval ceremony. `Read`/`Write`/`Execute` are auto-approved (with
13/// audit recording); `Destructive` and `External` block on explicit
14/// human approval.
15#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
16#[serde(rename_all = "snake_case")]
17pub enum ActionTier {
18    /// Read-only — never requires confirmation.
19    Read,
20    /// Write — implicit confirmation, user can undo.
21    Write,
22    /// Execute — sandboxed, reversible.
23    Execute,
24    /// Destructive — explicit approval required.
25    Destructive,
26    /// External — explicit approval + credential audit.
27    External,
28}
29
30impl ActionTier {
31    /// Whether this tier requires explicit confirmation before execution.
32    pub fn requires_confirmation(self) -> bool {
33        matches!(self, ActionTier::Destructive | ActionTier::External)
34    }
35
36    /// Default approval timeout for this tier.
37    ///
38    /// `External` is shorter than `Destructive` (60s vs 300s): a destructive
39    /// action may legitimately need a few minutes of consideration, but an
40    /// External call is almost always issued mid-chat where the user is
41    /// either watching or has already moved on. A 5-minute deadlock there
42    /// just makes the chat feel broken.
43    pub fn default_timeout(self) -> std::time::Duration {
44        match self {
45            ActionTier::Read => std::time::Duration::from_secs(30),
46            ActionTier::Write => std::time::Duration::from_secs(60),
47            ActionTier::Execute => std::time::Duration::from_secs(120),
48            ActionTier::Destructive => std::time::Duration::from_secs(300),
49            ActionTier::External => std::time::Duration::from_secs(60),
50        }
51    }
52}
53
54impl std::fmt::Display for ActionTier {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        let s = match self {
57            ActionTier::Read => "read",
58            ActionTier::Write => "write",
59            ActionTier::Execute => "execute",
60            ActionTier::Destructive => "destructive",
61            ActionTier::External => "external",
62        };
63        f.write_str(s)
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn requires_confirmation_only_destructive_external() {
73        assert!(!ActionTier::Read.requires_confirmation());
74        assert!(!ActionTier::Write.requires_confirmation());
75        assert!(!ActionTier::Execute.requires_confirmation());
76        assert!(ActionTier::Destructive.requires_confirmation());
77        assert!(ActionTier::External.requires_confirmation());
78    }
79
80    #[test]
81    fn default_timeout_increases_with_tier() {
82        assert!(ActionTier::Read.default_timeout() < ActionTier::Execute.default_timeout());
83        assert!(ActionTier::Execute.default_timeout() < ActionTier::Destructive.default_timeout());
84    }
85
86    #[test]
87    fn external_timeout_is_shorter_than_destructive() {
88        // External lives in chat; a 5-min deadlock there feels broken.
89        assert!(ActionTier::External.default_timeout() < ActionTier::Destructive.default_timeout());
90    }
91
92    #[test]
93    fn display_matches_serde_repr() {
94        assert_eq!(ActionTier::Destructive.to_string(), "destructive");
95        let json = serde_json::to_string(&ActionTier::Destructive).unwrap();
96        assert_eq!(json, "\"destructive\"");
97    }
98}