Skip to main content

hookwise/
decision.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4pub use crate::scope::ScopeLevel;
5
6/// The three possible permission states.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8#[serde(rename_all = "lowercase")]
9pub enum Decision {
10    Allow,
11    Deny,
12    Ask,
13}
14
15/// Which tier of the cascade produced this decision.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17pub enum DecisionTier {
18    /// Tier 0: deterministic path policy glob match
19    PathPolicy,
20    /// Tier 1: exact cache match (HashMap)
21    ExactCache,
22    /// Tier 2a: token-level Jaccard similarity
23    TokenJaccard,
24    /// Tier 2b: embedding HNSW similarity
25    EmbeddingSimilarity,
26    /// Tier 3: LLM supervisor evaluation
27    Supervisor,
28    /// Tier 4: human-in-the-loop
29    Human,
30    /// Sensitive path default (pre-cascade)
31    SensitivePath,
32    /// Explicit override (human-set, deterministic)
33    Override,
34    /// Default fallback when no cascade tier resolved
35    Default,
36}
37
38/// Metadata about how and why a decision was made.
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct DecisionMetadata {
41    /// Which cascade tier produced this decision.
42    pub tier: DecisionTier,
43
44    /// Confidence score from the deciding tier. 1.0 for deterministic tiers.
45    pub confidence: f64,
46
47    /// Human-readable reason for the decision.
48    pub reason: String,
49
50    /// For similarity tiers: the cache key of the matched entry.
51    pub matched_key: Option<CacheKey>,
52
53    /// For similarity tiers: the similarity score.
54    pub similarity_score: Option<f64>,
55}
56
57/// A unique key identifying a cached decision.
58/// The cache is keyed on (sanitized_input, tool, role).
59#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
60pub struct CacheKey {
61    /// The sanitized tool input.
62    pub sanitized_input: String,
63
64    /// The tool name (Bash, Write, Edit, Read, Glob, Grep, Task, etc.)
65    pub tool: String,
66
67    /// The role of the session that generated or matched this entry.
68    pub role: String,
69}
70
71impl std::fmt::Display for Decision {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        match self {
74            Decision::Allow => write!(f, "allow"),
75            Decision::Deny => write!(f, "deny"),
76            Decision::Ask => write!(f, "ask"),
77        }
78    }
79}
80
81impl Decision {
82    /// Returns the precedence rank (higher = more authoritative).
83    /// DENY > ASK > ALLOW
84    pub fn precedence(&self) -> u8 {
85        match self {
86            Decision::Deny => 3,
87            Decision::Ask => 2,
88            Decision::Allow => 1,
89        }
90    }
91}
92
93impl std::fmt::Display for ScopeLevel {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        match self {
96            ScopeLevel::Org => write!(f, "org"),
97            ScopeLevel::Project => write!(f, "project"),
98            ScopeLevel::User => write!(f, "user"),
99            ScopeLevel::Role => write!(f, "role"),
100        }
101    }
102}
103
104impl std::str::FromStr for ScopeLevel {
105    type Err = String;
106
107    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
108        match s.to_lowercase().as_str() {
109            "org" => Ok(ScopeLevel::Org),
110            "project" => Ok(ScopeLevel::Project),
111            "user" => Ok(ScopeLevel::User),
112            "role" => Ok(ScopeLevel::Role),
113            _ => Err(format!("unknown scope: {s}")),
114        }
115    }
116}
117
118/// A complete decision record, stored in JSONL and used in the cache.
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct DecisionRecord {
121    /// The cache key for this decision.
122    pub key: CacheKey,
123
124    /// The decision: allow, deny, or ask.
125    pub decision: Decision,
126
127    /// Metadata about how the decision was made.
128    pub metadata: DecisionMetadata,
129
130    /// When this decision was made.
131    pub timestamp: DateTime<Utc>,
132
133    /// Which scope this decision belongs to.
134    pub scope: ScopeLevel,
135
136    /// For Write/Edit tools: the file path.
137    pub file_path: Option<String>,
138
139    /// The session ID that triggered this decision (for audit trail).
140    pub session_id: String,
141}