Skip to main content

lsp_max_protocol/
intent.rs

1use serde::{Deserialize, Serialize};
2
3pub const INTENT_DECLARE: &str = "max/intent.declare";
4pub const INTENT_VALIDATE: &str = "max/intent.validate";
5pub const INTENT_REVOKE: &str = "max/intent.revoke";
6pub const INTENT_LIST: &str = "max/intent.list";
7
8/// Declared action categories for intent pre-flight.
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
10pub enum IntentKind {
11    /// Agent intends to write/mutate a file
12    FileWrite { uri: String },
13    /// Agent intends to run a shell command
14    ShellExec { command: String },
15    /// Agent intends to call an LSP method
16    LspCall { method: String },
17    /// Agent intends to promote a CANDIDATE to ADMITTED
18    AdmissionPromotion { method: String },
19    /// Agent intends to push to git remote
20    GitPush { branch: String },
21    /// Agent intends to create a receipt artifact
22    ReceiptGeneration { receipt_id: String },
23    /// Custom intent for extension methods
24    Custom {
25        kind: String,
26        payload: serde_json::Value,
27    },
28}
29
30/// Pre-flight validation outcome for a declared intent.
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
32pub enum IntentOutcome {
33    /// Intent is clear — action may proceed
34    Cleared,
35    /// Intent is blocked — action must not proceed
36    Blocked { reason: String },
37    /// Intent needs clarification before proceeding
38    ClarificationRequired { question: String },
39    /// Intent is deferred — retry after condition is met
40    Deferred { condition: String },
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct IntentDeclareParams {
45    /// Unique intent ID (client-generated)
46    pub intent_id: String,
47    /// The declared action
48    pub kind: IntentKind,
49    /// Human-readable description of why this action is being taken
50    pub rationale: String,
51    /// Optional: related document context (serialised URI string)
52    pub context_uri: Option<String>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct IntentDeclareResult {
57    pub intent_id: String,
58    pub outcome: IntentOutcome,
59    /// CANDIDATE | ADMITTED | REFUSED
60    pub law_status: String,
61    pub gate_open: bool,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct IntentValidateParams {
66    pub intent_id: String,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct IntentValidateResult {
71    pub intent_id: String,
72    pub valid: bool,
73    pub outcome: IntentOutcome,
74    pub violations: Vec<String>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct IntentRevokeParams {
79    pub intent_id: String,
80    pub reason: String,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct IntentRevokeResult {
85    pub intent_id: String,
86    pub status: String,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct IntentListParams {
91    /// "Cleared" | "Blocked" | "ClarificationRequired"
92    pub filter_outcome: Option<String>,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct IntentListResult {
97    pub intents: Vec<IntentSummary>,
98    pub total: usize,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct IntentSummary {
103    pub intent_id: String,
104    pub kind_label: String,
105    pub outcome: String,
106    pub law_status: String,
107    pub gate_open: bool,
108}
109
110/// In-process intent registry for the current session.
111pub struct IntentRegistry {
112    intents: std::collections::HashMap<String, (IntentDeclareParams, IntentDeclareResult)>,
113}
114
115impl IntentRegistry {
116    pub fn new() -> Self {
117        Self {
118            intents: std::collections::HashMap::new(),
119        }
120    }
121
122    pub fn declare(&mut self, params: IntentDeclareParams) -> IntentDeclareResult {
123        let outcome = self.validate_intent(&params);
124        let gate_open = matches!(outcome, IntentOutcome::Cleared);
125        let law_status = if gate_open {
126            "CANDIDATE".into()
127        } else {
128            "REFUSED".into()
129        };
130        let result = IntentDeclareResult {
131            intent_id: params.intent_id.clone(),
132            outcome: outcome.clone(),
133            law_status,
134            gate_open,
135        };
136        self.intents
137            .insert(params.intent_id.clone(), (params, result.clone()));
138        result
139    }
140
141    fn validate_intent(&self, params: &IntentDeclareParams) -> IntentOutcome {
142        match &params.kind {
143            IntentKind::FileWrite { uri }
144                if uri.contains("tower-lsp") || uri.contains("tower_lsp") =>
145            {
146                IntentOutcome::Blocked {
147                    reason: "LawViolation: uri contains forbidden tower-lsp reference".into(),
148                }
149            }
150            IntentKind::ShellExec { command } if command.contains("--no-verify") => {
151                IntentOutcome::Blocked {
152                    reason: "LawViolation: --no-verify bypasses law compliance hooks".into(),
153                }
154            }
155            IntentKind::AdmissionPromotion { method } if method.is_empty() => {
156                IntentOutcome::ClarificationRequired {
157                    question: "Which method is being promoted?".into(),
158                }
159            }
160            _ => IntentOutcome::Cleared,
161        }
162    }
163
164    pub fn revoke(&mut self, id: &str) -> Option<IntentRevokeResult> {
165        self.intents.remove(id).map(|_| IntentRevokeResult {
166            intent_id: id.to_string(),
167            status: "REVOKED".into(),
168        })
169    }
170
171    pub fn list(&self, filter: Option<&str>) -> IntentListResult {
172        let intents: Vec<IntentSummary> = self
173            .intents
174            .values()
175            .filter(|(_, r)| filter.map(|f| r.outcome.label() == f).unwrap_or(true))
176            .map(|(p, r)| IntentSummary {
177                intent_id: p.intent_id.clone(),
178                kind_label: p.kind.label(),
179                outcome: r.outcome.label().to_string(),
180                law_status: r.law_status.clone(),
181                gate_open: r.gate_open,
182            })
183            .collect();
184        let total = intents.len();
185        IntentListResult { intents, total }
186    }
187}
188
189impl Default for IntentRegistry {
190    fn default() -> Self {
191        Self::new()
192    }
193}
194
195impl IntentKind {
196    pub fn label(&self) -> String {
197        match self {
198            Self::FileWrite { uri } => format!("FileWrite:{uri}"),
199            Self::ShellExec { command } => format!("ShellExec:{command}"),
200            Self::LspCall { method } => format!("LspCall:{method}"),
201            Self::AdmissionPromotion { method } => format!("AdmissionPromotion:{method}"),
202            Self::GitPush { branch } => format!("GitPush:{branch}"),
203            Self::ReceiptGeneration { receipt_id } => {
204                format!("ReceiptGeneration:{receipt_id}")
205            }
206            Self::Custom { kind, .. } => format!("Custom:{kind}"),
207        }
208    }
209}
210
211impl IntentOutcome {
212    pub fn label(&self) -> &str {
213        match self {
214            Self::Cleared => "Cleared",
215            Self::Blocked { .. } => "Blocked",
216            Self::ClarificationRequired { .. } => "ClarificationRequired",
217            Self::Deferred { .. } => "Deferred",
218        }
219    }
220}