Skip to main content

aios_core/
policy_engine.rs

1//! 策略引擎 — 校验 LLM 返回的意图是否合法
2//!
3//! 职责:
4//! 1. 检查风险等级是否允许自动执行 (引擎配置 + 后端能力双重)
5//! 2. 检查推荐的 action 是否在白名单内
6//! 3. 检查目标 app 是否可操作(必须在本窗口的上下文中出现过)
7//! 4. 输出经过滤的可执行动作列表
8
9use std::collections::BTreeSet;
10
11use aios_spec::{
12    ActionType, ActionUrgency, AuthorizedAction, CapabilityLevel, DenialReason, Intent,
13    IntentBatch, RiskLevel, SanitizedEventType, StructuredContext,
14};
15use tracing::debug;
16
17/// 策略校验结果
18#[derive(Debug, Clone)]
19pub struct PolicyDecision {
20    /// 原始意图 ID
21    pub intent_id: String,
22    /// 原始意图是否通过校验
23    pub approved: bool,
24    /// 被拒绝的原因 (如有)
25    pub rejection_reason: Option<DenialReason>,
26    /// 在意图层通过但被动作层规则拦截的具体原因序列
27    /// (同一意图内可有多条;顺序对应被丢弃的 SuggestedAction 顺序)
28    pub action_denials: Vec<DenialReason>,
29    /// 通过校验的动作列表 (可能少于原始列表)
30    pub approved_actions: Vec<AuthorizedAction>,
31}
32
33/// 策略引擎配置
34#[derive(Debug, Clone)]
35pub struct PolicyConfig {
36    /// 允许自动执行的最大风险等级
37    pub max_auto_risk: RiskLevel,
38    /// 禁止的 action 类型 (按 Debug 名称子串匹配)
39    pub blocked_actions: Vec<String>,
40    /// 单次最多执行的动作数
41    pub max_actions_per_batch: usize,
42    /// 置信度下限——低于此值的意图直接拒绝
43    pub min_confidence: f32,
44}
45
46impl Default for PolicyConfig {
47    fn default() -> Self {
48        Self {
49            max_auto_risk: RiskLevel::Low,
50            blocked_actions: vec![],
51            max_actions_per_batch: 5,
52            min_confidence: 0.3,
53        }
54    }
55}
56
57/// 策略引擎
58pub struct PolicyEngine {
59    config: PolicyConfig,
60}
61
62impl PolicyEngine {
63    pub fn new(config: PolicyConfig) -> Self {
64        Self { config }
65    }
66
67    /// 校验整个 IntentBatch, 返回每个意图的决策。
68    ///
69    /// 不检查后端能力等级,也不进行 target-not-in-context 检查——
70    /// 用于未指定后端 / 未携带窗口上下文的场景或向后兼容。
71    pub fn evaluate_batch(&self, batch: &IntentBatch) -> Vec<PolicyDecision> {
72        batch
73            .intents
74            .iter()
75            .map(|intent| self.evaluate_intent(intent, batch.generated_at_ms, None, None))
76            .collect()
77    }
78
79    /// 校验整个 IntentBatch,同时执行后端能力等级检查。
80    pub fn evaluate_batch_with_capability(
81        &self,
82        batch: &IntentBatch,
83        capability: &CapabilityLevel,
84    ) -> Vec<PolicyDecision> {
85        batch
86            .intents
87            .iter()
88            .map(|intent| {
89                self.evaluate_intent(intent, batch.generated_at_ms, Some(capability), None)
90            })
91            .collect()
92    }
93
94    /// 完整的校验入口:风险 + 能力 + 上下文。
95    ///
96    /// 当上下文 (`ctx`) 提供时,引擎会拒绝任何 `target` 指向未在本窗口
97    /// 出现过的 package/path 的动作。这是封堵 LLM 凭空指定目标的关键门。
98    pub fn evaluate_batch_with_context(
99        &self,
100        batch: &IntentBatch,
101        capability: &CapabilityLevel,
102        ctx: &StructuredContext,
103    ) -> Vec<PolicyDecision> {
104        let known = KnownTargets::from_context(ctx);
105        batch
106            .intents
107            .iter()
108            .map(|intent| {
109                self.evaluate_intent(
110                    intent,
111                    batch.generated_at_ms,
112                    Some(capability),
113                    Some(&known),
114                )
115            })
116            .collect()
117    }
118
119    fn evaluate_intent(
120        &self,
121        intent: &Intent,
122        authorized_at_ms: i64,
123        capability: Option<&CapabilityLevel>,
124        known: Option<&KnownTargets>,
125    ) -> PolicyDecision {
126        // 1. 后端能力等级检查 — 先于通用策略
127        if let Some(cap) = capability {
128            if !cap.allows_risk(intent.risk_level) {
129                return rejected(
130                    intent,
131                    DenialReason::RiskExceedsCapability,
132                    "risk exceeds backend capability",
133                );
134            }
135        }
136
137        // 2. 通用风险等级检查
138        if intent.risk_level as u8 > self.config.max_auto_risk as u8 {
139            return rejected(
140                intent,
141                DenialReason::RiskExceedsConfig,
142                "risk exceeds engine config",
143            );
144        }
145
146        // 3. 置信度下限
147        if intent.confidence < self.config.min_confidence {
148            return rejected(
149                intent,
150                DenialReason::ConfidenceTooLow,
151                "confidence below floor",
152            );
153        }
154
155        // 4. 过滤动作
156        let mut approved_actions: Vec<AuthorizedAction> = Vec::new();
157        let mut action_denials: Vec<DenialReason> = Vec::new();
158
159        for action in &intent.suggested_actions {
160            if approved_actions.len() >= self.config.max_actions_per_batch {
161                debug!(
162                    intent_id = %intent.intent_id,
163                    reason = ?DenialReason::BatchActionCapExceeded,
164                    "policy denial"
165                );
166                action_denials.push(DenialReason::BatchActionCapExceeded);
167                continue;
168            }
169
170            let action_name = format!("{:?}", action.action_type);
171            if self
172                .config
173                .blocked_actions
174                .iter()
175                .any(|blocked| action_name.contains(blocked))
176            {
177                debug!(
178                    intent_id = %intent.intent_id,
179                    reason = ?DenialReason::ActionTypeBlocked,
180                    action = %action_name,
181                    "policy denial"
182                );
183                action_denials.push(DenialReason::ActionTypeBlocked);
184                continue;
185            }
186
187            if matches!(action.urgency, ActionUrgency::Deferred) {
188                debug!(
189                    intent_id = %intent.intent_id,
190                    reason = ?DenialReason::ActionUrgencyDeferred,
191                    "policy denial"
192                );
193                action_denials.push(DenialReason::ActionUrgencyDeferred);
194                continue;
195            }
196
197            if let Some(cap) = capability {
198                if !cap.allows_action(&action.action_type) {
199                    debug!(
200                        intent_id = %intent.intent_id,
201                        reason = ?DenialReason::ActionCapabilityDenied,
202                        action = %action_name,
203                        "policy denial"
204                    );
205                    action_denials.push(DenialReason::ActionCapabilityDenied);
206                    continue;
207                }
208            }
209
210            if let Some(k) = known {
211                if let Some(reason) = check_target(&action.action_type, action.target.as_deref(), k)
212                {
213                    debug!(
214                        intent_id = %intent.intent_id,
215                        reason = ?reason,
216                        target = ?action.target,
217                        "policy denial"
218                    );
219                    action_denials.push(reason);
220                    continue;
221                }
222            }
223
224            approved_actions.push(AuthorizedAction {
225                intent_id: intent.intent_id.clone(),
226                action: action.clone(),
227                authorized_at_ms,
228            });
229        }
230
231        PolicyDecision {
232            intent_id: intent.intent_id.clone(),
233            approved: true,
234            rejection_reason: None,
235            action_denials,
236            approved_actions,
237        }
238    }
239}
240
241impl Default for PolicyEngine {
242    fn default() -> Self {
243        Self::new(PolicyConfig::default())
244    }
245}
246
247fn rejected(intent: &Intent, reason: DenialReason, log_msg: &'static str) -> PolicyDecision {
248    debug!(
249        intent_id = %intent.intent_id,
250        reason = ?reason,
251        "policy denial: {log_msg}"
252    );
253    PolicyDecision {
254        intent_id: intent.intent_id.clone(),
255        approved: false,
256        rejection_reason: Some(reason),
257        action_denials: vec![],
258        approved_actions: vec![],
259    }
260}
261
262/// 检查 (action_type, target) 是否通过 target 校验。返回 Some(reason)
263/// 即拒绝。规则:
264///
265/// - `NoOp` — 不关心 target。
266/// - `PreWarmProcess` — executor 强制要求 target;None 视为 hallucinated。
267/// - `KeepAlive` / `ReleaseMemory` / `PrefetchFile` — None 是合法的"系统/窗口
268///   范围"语义;如果给了 Some(target) 则必须在 KnownTargets 中。
269///
270/// 这一切都是为了拦截 LLM 凭空指定 package:宁可拒绝一条可疑动作,
271/// 也不让从未在窗口里出现过的实体被执行器接受。
272fn check_target(
273    action: &ActionType,
274    target: Option<&str>,
275    known: &KnownTargets,
276) -> Option<DenialReason> {
277    match action {
278        ActionType::NoOp => None,
279        ActionType::PreWarmProcess => match target {
280            Some(t) if !t.is_empty() && (known.packages.contains(t) || known.files.contains(t)) => {
281                None
282            },
283            _ => Some(DenialReason::TargetNotInContext),
284        },
285        ActionType::KeepAlive | ActionType::ReleaseMemory | ActionType::PrefetchFile => {
286            match target {
287                None => None,
288                Some("") => Some(DenialReason::TargetNotInContext),
289                Some(t) if known.packages.contains(t) || known.files.contains(t) => None,
290                Some(_) => Some(DenialReason::TargetNotInContext),
291            }
292        },
293    }
294}
295
296/// 一次窗口上下文中"已知"的可操作实体。
297///
298/// 由 [`StructuredContext`] 派生:所有出现在 sanitized 事件里的 package
299/// 名以及 ContextSummary 汇总里的 foreground/notified apps。当前不收集
300/// 具体文件路径(脱敏阶段已经丢弃),但保留 `files` 字段以便未来扩展。
301struct KnownTargets {
302    packages: BTreeSet<String>,
303    files: BTreeSet<String>,
304}
305
306impl KnownTargets {
307    fn from_context(ctx: &StructuredContext) -> Self {
308        let mut packages: BTreeSet<String> = BTreeSet::new();
309        for pkg in &ctx.summary.foreground_apps {
310            packages.insert(pkg.clone());
311        }
312        for pkg in &ctx.summary.notified_apps {
313            packages.insert(pkg.clone());
314        }
315        for event in &ctx.events {
316            if let Some(pkg) = event.app_package.as_ref() {
317                packages.insert(pkg.clone());
318            }
319            match &event.event_type {
320                SanitizedEventType::AppTransition { package_name, .. } => {
321                    packages.insert(package_name.clone());
322                },
323                SanitizedEventType::Notification { source_package, .. } => {
324                    packages.insert(source_package.clone());
325                },
326                SanitizedEventType::ProcessResource {
327                    package_name: Some(p),
328                    ..
329                } => {
330                    packages.insert(p.clone());
331                },
332                _ => {},
333            }
334        }
335        Self {
336            packages,
337            files: BTreeSet::new(),
338        }
339    }
340}