Skip to main content

lsp_max/
gate.rs

1//! Validation gate logic for verifying server authorization, state checks, and compliance keys.
2
3// ---------------------------------------------------------------------------
4// Composable gate algebra (BreedGate pattern from dteam)
5// ---------------------------------------------------------------------------
6
7/// A named, composable law gate — a single intuitionistic proof obligation.
8///
9/// Each `LawGate` is a predicate over the server registry. `accept_gates`
10/// is their conjunction: a proof-tree root that is `true` iff every obligation
11/// is satisfied. Function-pointer representation makes gates stack-allocable,
12/// inspectable by name, and zero-heap.
13#[derive(Clone, Copy)]
14pub struct LawGate {
15    /// Human-readable name for this gate obligation (stable identifier).
16    pub name: &'static str,
17    /// The predicate to evaluate against the current server registry.
18    pub check: fn(&crate::ServerRegistry) -> bool,
19}
20
21impl std::fmt::Debug for LawGate {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        f.debug_struct("LawGate").field("name", &self.name).finish()
24    }
25}
26
27impl LawGate {
28    /// Evaluate this gate against `registry`.
29    pub fn eval(&self, registry: &crate::ServerRegistry) -> bool {
30        (self.check)(registry)
31    }
32}
33
34/// Returns `true` iff every gate in `gates` passes against `registry`.
35///
36/// Short-circuits on the first failing gate. The name of the failing gate
37/// can be found by iterating manually when diagnostics are needed.
38pub fn accept_gates(registry: &crate::ServerRegistry, gates: &[LawGate]) -> bool {
39    gates.iter().all(|g| g.eval(registry))
40}
41
42/// Default gate pack for law-state transition validation.
43///
44/// These are the baseline proof obligations every server must satisfy before
45/// a law-state transition is admitted. Additional gates can be composed by
46/// concatenating slices.
47pub const DEFAULT_GATES: &[LawGate] = &[
48    LawGate {
49        name: "not-uninitialized",
50        check: |r| r.current_state != crate::service::State::Uninitialized,
51    },
52    LawGate {
53        name: "receipt-present",
54        check: |r| !r.receipts.is_empty(),
55    },
56    // The "clean slate" gate closes the most dangerous fake-completion loophole:
57    // an LLM that emits zero diagnostics while having no receipts is claiming
58    // completion without evidence. A clean diagnostic surface is only lawful
59    // when at least one receipt exists to back the claim.
60    LawGate {
61        name: "clean-slate-requires-receipt",
62        check: |r| !r.diagnostics.is_empty() || !r.receipts.is_empty(),
63    },
64];
65
66// ---------------------------------------------------------------------------
67// Legacy per-ID gate dispatch
68// ---------------------------------------------------------------------------
69
70/// Evaluates a security gate check given its unique identifier, the current server state,
71/// and the workspace root path.
72///
73/// Returns `true` if the gate permits transition, `false` otherwise.
74pub fn run_gate_logic(
75    gate_id: &str,
76    current_state: crate::service::State,
77    root_path: std::path::PathBuf,
78) -> bool {
79    match gate_id {
80        "some-gate" => true,
81        "gate-state-check" => current_state != crate::service::State::Uninitialized,
82        "gate-receipt-check" | "gate-auth-check" => {
83            let spec = crate::diagnostics::law_table::law_table()
84                .iter()
85                .find(|s| s.gate_id == gate_id);
86            let Some(spec) = spec else { return false };
87            let path = root_path.join(spec.receipt_file);
88            if path.exists() {
89                std::fs::read_to_string(&path)
90                    .map(|c| c.trim() == spec.receipt_token)
91                    .unwrap_or(false)
92            } else {
93                false
94            }
95        }
96        "gate-powl-conformance" => {
97            // POWL conformance gate — admitted when a receipt exists for the declared model.
98            // Full wasm4pm integration routes through control_plane::powl_conformance.
99            let spec = crate::diagnostics::law_table::law_table()
100                .iter()
101                .find(|s| s.gate_id == gate_id);
102            match spec {
103                Some(spec) => root_path.join(spec.receipt_file).exists(),
104                None => false, // No law table entry — UNKNOWN, conservative refusal
105            }
106        }
107        _ => {
108            let output = std::process::Command::new("cargo")
109                .arg("check")
110                .current_dir(root_path)
111                .output();
112            match output {
113                Ok(out) => {
114                    if !out.status.success() {
115                        eprintln!("cargo check failed!");
116                        eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));
117                        eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr));
118                    }
119                    out.status.success()
120                }
121                Err(e) => {
122                    eprintln!("failed to execute cargo check: {:?}", e);
123                    false
124                }
125            }
126        }
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use crate::service::State;
134    use std::collections::HashMap;
135
136    fn minimal_registry(state: State) -> crate::ServerRegistry {
137        crate::ServerRegistry {
138            client_capabilities: None,
139            server_capabilities: None,
140            diagnostics: HashMap::new(),
141            repair_plans: HashMap::new(),
142            gates: HashMap::new(),
143            receipts: HashMap::new(),
144            snapshots: HashMap::new(),
145            cleared_diagnostics: std::collections::HashSet::new(),
146            current_state: state,
147            document_versions: HashMap::new(),
148            root_path: std::path::PathBuf::from("/tmp"),
149            action_seq: 0,
150            conformance_delta_log: std::collections::VecDeque::new(),
151        }
152    }
153
154    #[test]
155    fn law_gate_eval_returns_true_when_predicate_passes() {
156        let gate = LawGate {
157            name: "always-true",
158            check: |_| true,
159        };
160        let reg = minimal_registry(State::Initialized);
161        assert!(gate.eval(&reg));
162    }
163
164    #[test]
165    fn law_gate_eval_returns_false_when_predicate_fails() {
166        let gate = LawGate {
167            name: "always-false",
168            check: |_| false,
169        };
170        let reg = minimal_registry(State::Initialized);
171        assert!(!gate.eval(&reg));
172    }
173
174    #[test]
175    fn law_gate_debug_shows_name() {
176        let gate = LawGate {
177            name: "my-gate",
178            check: |_| true,
179        };
180        assert!(format!("{gate:?}").contains("my-gate"));
181    }
182
183    #[test]
184    fn accept_gates_empty_slice_always_passes() {
185        let reg = minimal_registry(State::Uninitialized);
186        assert!(accept_gates(&reg, &[]));
187    }
188
189    #[test]
190    fn accept_gates_all_pass() {
191        let gates = [
192            LawGate {
193                name: "g1",
194                check: |_| true,
195            },
196            LawGate {
197                name: "g2",
198                check: |_| true,
199            },
200        ];
201        let reg = minimal_registry(State::Initialized);
202        assert!(accept_gates(&reg, &gates));
203    }
204
205    #[test]
206    fn accept_gates_fails_on_first_false_gate() {
207        let gates = [
208            LawGate {
209                name: "fail",
210                check: |_| false,
211            },
212            LawGate {
213                name: "pass",
214                check: |_| true,
215            },
216        ];
217        let reg = minimal_registry(State::Initialized);
218        assert!(!accept_gates(&reg, &gates));
219    }
220
221    #[test]
222    fn default_gates_block_uninitialized_state() {
223        let reg = minimal_registry(State::Uninitialized);
224        assert!(!accept_gates(&reg, DEFAULT_GATES));
225    }
226
227    #[test]
228    fn run_gate_logic_some_gate_always_returns_true() {
229        let result = run_gate_logic(
230            "some-gate",
231            State::Uninitialized,
232            std::path::PathBuf::from("/tmp"),
233        );
234        assert!(result);
235    }
236
237    #[test]
238    fn run_gate_logic_state_check_passes_when_initialized() {
239        let result = run_gate_logic(
240            "gate-state-check",
241            State::Initialized,
242            std::path::PathBuf::from("/tmp"),
243        );
244        assert!(result);
245    }
246
247    #[test]
248    fn run_gate_logic_state_check_fails_when_uninitialized() {
249        let result = run_gate_logic(
250            "gate-state-check",
251            State::Uninitialized,
252            std::path::PathBuf::from("/tmp"),
253        );
254        assert!(!result);
255    }
256}