Skip to main content

clash_policy/
lib.rs

1//! Clash policy language: parsing, IR, compilation, and evaluation.
2//!
3//! Extracted from `clash::policy` to break a circular dep with `clash-lsp`.
4//! Policies are authored in Starlark and compiled to a uniform trie IR.
5//! Evaluation is a single DFS pass — first match wins.
6
7pub mod compile;
8pub mod diff;
9pub mod error;
10pub mod format;
11pub mod ir;
12pub mod manifest_edit;
13pub mod match_tree;
14pub mod path;
15pub mod sandbox_edit;
16pub mod sandbox_types;
17pub mod test_eval;
18
19#[cfg(test)]
20mod proptests;
21
22pub use compile::compile_multi_level_to_tree;
23pub use error::{CompileError, PolicyError, PolicyParseError};
24pub use ir::{DecisionTrace, PolicyDecision, RuleMatch, RuleSkip};
25pub use match_tree::{CompiledPolicy, IncludeEntry, PolicyManifest};
26
27use std::fmt;
28
29use serde::{Deserialize, Serialize};
30
31// ---------------------------------------------------------------------------
32// PolicyLevel — inlined from clash::settings::discovery to avoid a circular dep
33// ---------------------------------------------------------------------------
34
35/// Policy level — where a policy file lives in the precedence hierarchy.
36///
37/// Higher-precedence levels override lower ones: Session > Project > User.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
39pub enum PolicyLevel {
40    /// User-level policy: `~/.clash/policy.star`
41    User = 0,
42    /// Project-level policy: `<project_root>/.clash/policy.star`
43    Project = 1,
44    /// Session-level policy: `/tmp/clash-<session_id>/policy.star`
45    /// Temporary rules that last only for the current Claude Code session.
46    Session = 2,
47}
48
49impl PolicyLevel {
50    /// All persistent levels in precedence order (highest first).
51    /// Session is excluded because it requires a session_id to resolve.
52    pub fn all_by_precedence() -> &'static [PolicyLevel] {
53        &[PolicyLevel::Project, PolicyLevel::User]
54    }
55
56    /// Display name for this level.
57    pub fn name(&self) -> &'static str {
58        match self {
59            PolicyLevel::User => "user",
60            PolicyLevel::Project => "project",
61            PolicyLevel::Session => "session",
62        }
63    }
64}
65
66impl fmt::Display for PolicyLevel {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        write!(f, "{}", self.name())
69    }
70}
71
72impl std::str::FromStr for PolicyLevel {
73    type Err = anyhow::Error;
74
75    fn from_str(s: &str) -> Result<Self, Self::Err> {
76        match s {
77            "user" => Ok(PolicyLevel::User),
78            "project" => Ok(PolicyLevel::Project),
79            "session" => Ok(PolicyLevel::Session),
80            _ => anyhow::bail!(
81                "unknown policy level: {s} (expected 'user', 'project', or 'session')"
82            ),
83        }
84    }
85}
86
87/// The effect a statement produces.
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
89#[serde(rename_all = "lowercase")]
90pub enum Effect {
91    /// Allow the action without prompting.
92    Allow,
93    /// Deny the action.
94    Deny,
95    /// Prompt the user for confirmation.
96    Ask,
97}
98
99impl fmt::Display for Effect {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        match self {
102            Effect::Allow => write!(f, "allow"),
103            Effect::Deny => write!(f, "deny"),
104            Effect::Ask => write!(f, "ask"),
105        }
106    }
107}
108
109impl std::str::FromStr for Effect {
110    type Err = String;
111
112    fn from_str(s: &str) -> Result<Self, Self::Err> {
113        match s {
114            "allow" => Ok(Effect::Allow),
115            "deny" => Ok(Effect::Deny),
116            "ask" => Ok(Effect::Ask),
117            _ => Err(format!("unknown effect: {s:?}")),
118        }
119    }
120}