Files
AstroResearch/src/agent/runtime/hardline.rs
T
fmq 698d007f39 feat: Agent 安全纵深防御、Checkpoint 快照、会话 Rewind/Branch、自进化
Skill、流式执行优化与系统架构全面升级

  本次提交对标 Claude Code 与 Hermes-Agent 的工程细节,在安全、可靠性、
  会话管理、自我进化四个维度进行了系统性加固,变更总量 48 文件 / +12680 -2292 行。

  ═══════ 安全纵深防御 ═══════

  1. Hardline 硬阻止层 (src/agent/runtime/hardline.rs, +534 行)
     - 不可绕过的危险命令拦截(关重启、磁盘擦除、Fork 炸弹、rm -rf /、kill -1)
     - 反规避标准化管线: ANSI 序列剥离 → Unicode NFKC → shell 反斜杠还原 → 空字面量清理
     - 在 PermissionChecker 之前执行,YOLO/Bypass 模式下同样生效
     - 集成到 executor Phase 2,被拒绝工具直接注入错误结果

  2. Permission 优先级裁决器 (src/agent/runtime/permission.rs, +200 行)
     - 7 层正式优先级规则 (P0 Deny → P7 Allow),带冲突日志
     - explain() 方法支持审计追溯
     - Hook PermissionRequired 与 Checker 结果的正确叠加逻辑

  ═══════ Checkpoint 文件快照系统 ═══════

  3. git2 原生快照 (src/agent/runtime/checkpoint.rs, +920 行)
     - 基于 git2 bare repo,内容寻址自动去重
     - 文件变更操作前自动触发 (file_write/file_edit/run_bash)
     - 每目录每 turn 最多一次快照,防止同一轮重复
     - 支持 list/diff/restore API + pre-rollback 安全快照
     - 旧快照自动 prune(保留最近 N 个)+ 按目录隔离 ref
     - 排除规则自动过滤 node_modules/target/.git/*.pdf 等
     - 集成到 executor: 文件操作前 ckpt.ensure_checkpoint()

  ═══════ 错误恢复系统大升级 ═══════

  4. 21 种 FailoverReason 分类 (src/agent/runtime/error_recovery.rs, +1200 行)
     - 参考 Hermes-Agent error_classifier.py
     - 8 步分类管线: provider-specific → HTTP status → text pattern → error body → fallback
     - is_retryable / should_compress / should_failover / is_permanent 方法
     - Context Overflow 自动修复: 从错误消息提取 token 限制,自动下调预算
     - RecoveryStep::AdjustMaxTokens 实现 (参考 Claude Code 自动修复)
     - 向后兼容 ErrorKind 别名

  ═══════ 会话 Rewind / Branch / Retry 体系 ═══════

  5. 完整 undo 栈 (src/agent/runtime/session.rs, +800 行 + 2 迁移脚本)
     - Rewind (软删除): active=0 标记,审计 trail 保留,LLM 不可见
     - Restore (撤销回退): 冲突检测——回退后有新消息则拒绝,引导使用 Branch
     - Branch: 分叉会话,复制所有 active=1 消息到新会话
     - Retry: 硬删除最后一轮对话,返回原消息文本供前端重提交
     - 数据库: agent_messages.active 列 + agent_sessions.rewind_count + parent_session_id
     - API: 4 个新端点 (/branch, /retry, /rewind, /rewind/restore)
     - load_history_for_agent 全面使用 active=1 过滤

  ═══════ Hooks 系统模块化重构 ═══════

  6. 单文件 → 7 模块体系 (src/agent/hooks/)
     hooks.rs (994 行) 拆分为:
     - mod.rs    — 入口 + HookRegistry + SessionHookManager
     - types.rs  — 类型定义 (Context, TaggedContext, PermissionRequestAction 等)
     - traits.rs — AgentHook + AsyncAgentHook + 15 种生命周期事件
     - matcher.rs — 工具名/参数匹配 + session 作用域过滤
     - dispatch.rs — 并行调度引擎 (run_pre/post_tool_use 等)
     - registry.rs — 注册/注销/查询
     - builtins.rs — CancellationHook + MetricsHook + AuditLogHook + ContextDeduplicator

     关键改进:
     - run_pre_tool_use 并行执行所有匹配 hooks,聚合 Block/MutateInput/Continue
     - TaggedContext 带完整来源标记的上下文注入 (hook_name + event)
     - ContextDeduplicator 单 dispatch cycle 内内容哈希去重
     - AsyncAgentHook 支持 fire-and-forget 异步 hooks

  ═══════ Executor 并发执行升级 ═══════

  7. 三阶段管道重写 (src/agent/runtime/executor.rs, +600 行)
     - Phase 1: 死循环检测 + 参数解析 (不变)
     - Phase 2: Hardline 预检查 (新增) → PermissionChecker (改进)
     - Phase 3: ToolPartitioner 分区 → 逐批次执行 (重写)
       - 并行批次内 FuturesUnordered 并发
       - 串行批次确保非并发安全工具独占执行
       - Checkpoint 预触发集成
     - Hook 上下文注入: system-reminder 格式 + ContextDeduplicator 去重
     - Hook 阻塞错误详细记录

  ═══════ 流式执行真正的流式调度 ═══════

  8. StreamingExecutor 重写 (src/agent/runtime/streaming_executor.rs, ~400 行变更)
     - on_tool_use 中对并发安全工具立即 tokio::spawn,不等待 flush
     - executing_non_concurrent 标志阻塞后继工具直到独占工具完成
     - JoinHandle 管理替代自定义 cancel channel
     - completed_queue 按流顺序 yield
     - Sibling Abort 通过 broadcast channel + tokio::select! 竞速
     - ToolContext 实现 Clone (支持 per-task 上下文复制)

  ═══════ 自改进 Skill 系统 ═══════

  9. PatternDetector + SkillCreator + Curator (src/agent/skills/, +1500 行)
     - PatternDetector: 扫描 agent_messages 表,检测跨 session 重复工具调用模式
     - SkillCreator: 将高置信度模式自动生成 SKILL.md (YAML frontmatter + 工作流步骤)
     - SelfImprovePipeline: 一站式 模式检测 → 创建 → 质量审查
     - Curator: 分析 skill 使用统计,标记 stale/deprecated,建议清理
     - Skill frontmatter 新增 pinned 字段 (禁止 Curator 自动清理)

  ═══════ 基础设施优化 ═══════

  10. 系统提示词缓存 (src/agent/runtime/system_prompt.rs + mod.rs)
      - SystemPromptCache: 首次计算后永久复用,/clear 时失效
      - 新增 SAFETY / SYSTEM_CONTEXT / TOOL_USAGE 静态 section
      - 环境/tools/skills/memory 动态 section 通过 get_or_compute 缓存

  11. ToolRegistry schema 缓存 (src/agent/tools/mod.rs)
      - schema_cache + schema_generation 版本号
      - 工具变更/过滤器变更时自动失效
      - precompute_definitions() 预计算 (AgentRuntime 初始化时调用)

  12. 迭代摘要融合 (src/agent/compact.rs, +100 行)
      - 参考 Hermes context_compressor.py
      - CollapseLog 追踪压缩历史,支持溢出合并
      - extract_prior_summary: 提取已有摘要融入新压缩

  13. SubAgent 系统提示词模块化 (src/agent/tools/subagent.rs)
      - 复用 5 个标准 section + 子代理专有上下文 section
      - 独立 ToolRegistry 构建工具列表

  ═══════ 前端 — CSS 变量主题系统 ═══════

  14. 全新主题变量体系 (dashboard/src/index.css + App.tsx + 各面板)
      - CSS 自定义属性: --bg-card, --text-main, --text-muted, --border-precision
      - 语义化颜色: --accent-blueprint, --accent-star
      - 全面替换硬编码 Tailwind 颜色 (slate-xxx → var(--xxx))
      - 文献入库提示优化 ("核心知识节点" 替代 "向量块")
      - ReaderPanel 样式变量化
2026-06-22 20:29:37 +08:00

535 lines
17 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// src/agent/runtime/hardline.rs
//
// Hardline 命令阻止层 — 不可绕过的危险命令检查。
// 参考 Hermes-Agent approval.py HARDLINE_PATTERNS 设计。
//
// 设计原则:
// 1. Hardline 规则在任何模式下都不可被绕过(包括 YOLO/Bypass 模式)
// 2. 优先级高于所有其他权限规则
// 3. 在命令执行前做反规避标准化后再匹配
//
// 阻止的命令类别:
// - 系统关机/重启
// - 磁盘擦除/格式化
// - Fork 炸弹
// - 递归删除根目录
// - kill -1(信号广播)
use regex::RegexSet;
use std::sync::LazyLock;
// ── 反规避命令标准化 ──
/// 在安全检查前对命令字符串做标准化处理。
/// 参考 Hermes `_normalize_command_for_detection()` 实现。
///
/// 转换顺序:
/// 1. 剥离 ANSI 转义序列(ECMA-48
/// 2. 剥离 null 字节
/// 3. Unicode 全角字符 NFKC 标准化
/// 4. 剥离 shell 反斜杠转义(`r\m` → `rm`
/// 5. 剥离空字符串字面量(`r''m` → `rm`
/// 6. 解析后的绝对路径还原为 `~/` 形式
pub fn normalize_command(raw: &str) -> String {
// Step 1: 剥离 ANSI 转义序列
let s = strip_ansi_escapes(raw);
// Step 2: 剥离 null 字节
let s = s.replace('\0', "");
// Step 3: Unicode NFKC 标准化(全角 → 半角)
let s = unicode_normalization::lookup(&s).unwrap_or_else(|| s.to_string());
// Step 4: 剥离 shell 反斜杠转义(`r\m` → `rm`
let s = strip_backslash_escapes(&s);
// Step 5: 剥离空字符串字面量(`r''m` → `rm`, `r""m` → `rm`
let s = strip_empty_string_literals(&s);
// Step 6: 还原 Home 路径
normalize_home_paths(&s)
}
/// 剥离 ANSI 转义序列(CSI 序列,ECMA-48 §5.4
fn strip_ansi_escapes(s: &str) -> String {
static ANSI_RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"\x1b\[[0-9;]*[a-zA-Z]").expect("ANSI regex compile"));
ANSI_RE.replace_all(s, "").to_string()
}
/// Unicode 全角字符 NFKC 标准化 + 常见全角 ASCII 映射
mod unicode_normalization {
use std::collections::HashMap;
use std::sync::LazyLock;
static FULLWIDTH_MAP: LazyLock<HashMap<char, char>> = LazyLock::new(|| {
// 全角 ASCIIU+FF01-U+FF5E)映射到半角(U+0021-U+007E
let mut map = HashMap::new();
for code in 0xFF01u32..=0xFF5E {
if let Some(c) = char::from_u32(code) {
let half_width = char::from_u32(code - 0xFEE0).unwrap_or(c);
if c != half_width {
map.insert(c, half_width);
}
}
}
// 全角空格 U+3000 → 半角空格 U+0020
map.insert('\u{3000}', ' ');
map
});
/// 如果字符串包含全角字符,返回标准化后的版本;否则返回 None(无需复制)。
pub fn lookup(s: &str) -> Option<String> {
let needs_normalize = s.chars().any(|c| FULLWIDTH_MAP.contains_key(&c));
if !needs_normalize {
return None;
}
let normalized: String = s
.chars()
.map(|c| FULLWIDTH_MAP.get(&c).copied().unwrap_or(c))
.collect();
Some(normalized)
}
}
/// 剥离 shell 反斜杠转义。
/// 匹配 `\<任意字符>` 并还原为 `<字符>`。
/// 示例:`r\m\ \-\r\f\ \/` → `rm -rf /`
fn strip_backslash_escapes(s: &str) -> String {
// 匹配 backslash 后跟任意非换行字符,捕获该字符
static BACKSLASH_ESCAPE_RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"\\(.)").expect("backslash escape regex compile"));
// 仅当有反斜杠时才执行替换(快速路径)
if s.contains('\\') {
BACKSLASH_ESCAPE_RE.replace_all(s, "$1").to_string()
} else {
s.to_string()
}
}
/// 剥离空字符串字面量。
/// 匹配 `''` 或 `""`shell 中用于分割命令名)。
/// 示例:`r''m` → `rm`, `r""m` → `rm`
fn strip_empty_string_literals(s: &str) -> String {
// 匹配连续两个单引号('')或连续两个双引号("")
static EMPTY_QUOTE_RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"'{2}|\x22{2}").expect("empty quote regex compile"));
let has_single_empty = s.contains("''");
let has_double_empty = s.contains("\"\"");
if has_single_empty || has_double_empty {
EMPTY_QUOTE_RE.replace_all(s, "").to_string()
} else {
s.to_string()
}
}
/// 将解析后的绝对 HOME 路径还原为 `~/` 形式。
fn normalize_home_paths(s: &str) -> String {
let home = std::env::var("HOME").unwrap_or_else(|_| "/home/unknown".to_string());
if home.is_empty() || home == "/" {
return s.to_string();
}
s.replace(&home, "~")
}
// ── Hardline 模式定义 ──
/// 不可绕过的硬阻止模式。
///
/// 每个模式包含:
/// - `pattern`: 正则表达式
/// - `category`: 命令类别(用于日志和错误消息)
/// - `message`: 返回给 LLM 的阻止理由
struct HardlinePattern {
pattern: &'static str,
category: &'static str,
message: &'static str,
}
/// Hardline 模式列表。
/// 参考 Hermes HARDLINE_PATTERNS + 科研场景特定扩展。
static HARDLINE_PATTERNS: LazyLock<Vec<HardlinePattern>> = LazyLock::new(|| {
vec![
// ══════ 系统关机/重启 ══════
HardlinePattern {
pattern: r"\b(?:shutdown|poweroff|halt|reboot)\b",
category: "system_shutdown",
message: "系统关机/重启命令被硬阻止",
},
HardlinePattern {
pattern: r"\binit\s+[06]\b",
category: "system_shutdown",
message: "init 运行级别切换被硬阻止",
},
HardlinePattern {
pattern: r"\bsystemctl\s+(?:poweroff|reboot|halt)\b",
category: "system_shutdown",
message: "systemctl 关机命令被硬阻止",
},
// ══════ 磁盘擦除/格式化 ══════
HardlinePattern {
pattern: r"\bmkfs\b",
category: "disk_format",
message: "磁盘格式化命令 mkfs 被硬阻止",
},
HardlinePattern {
pattern: r"\bdd\s+.*\bof=/dev/[a-z]+",
category: "dd_to_device",
message: "dd 写入块设备被硬阻止",
},
HardlinePattern {
pattern: r"\bdd\s+.*\bof=/dev/(?:sd[a-z]|nvme\d+n\d+|mmcblk\d+)",
category: "dd_to_device",
message: "dd 写入磁盘设备被硬阻止",
},
// ══════ 递归删除根目录 ══════
HardlinePattern {
pattern: r"\brm\s+-rf\s+(?:/|/\*)",
category: "rm_root",
message: "递归删除根目录被硬阻止",
},
HardlinePattern {
pattern: r"\brm\s+.*\s+-rf\s+/",
category: "rm_root",
message: "递归删除根目录被硬阻止",
},
// ══════ Fork 炸弹 ══════
HardlinePattern {
pattern: r":\(\)\s*\{[^}]*:[^}]*\}",
category: "fork_bomb",
message: "Fork 炸弹模式被硬阻止",
},
HardlinePattern {
pattern: r"\bperl\s+-e\s+.*fork.*while",
category: "fork_bomb",
message: "Perl fork 循环被硬阻止",
},
HardlinePattern {
pattern: r"\bpython3?\s+-c\s+.*while.*os\.fork",
category: "fork_bomb",
message: "Python fork 炸弹被硬阻止",
},
// ══════ Kill 信号广播 ══════
HardlinePattern {
pattern: r"\bkill\s+-1\b",
category: "kill_all",
message: "kill -1(信号广播到所有进程)被硬阻止",
},
// ══════ 覆盖关键系统文件 ══════
HardlinePattern {
pattern: r">\s*/etc/(?:passwd|shadow|sudoers|hosts)\b",
category: "system_file_overwrite",
message: "重定向覆盖关键系统文件被硬阻止",
},
HardlinePattern {
pattern: r"\bcp\s+.*\s+/etc/(?:passwd|shadow|sudoers)\b",
category: "system_file_overwrite",
message: "复制覆盖关键系统文件被硬阻止",
},
// ══════ chmod 危险操作 ══════
HardlinePattern {
pattern: r"\bchmod\s+.*777\s+/(?:etc|bin|usr|lib|sbin|boot)\b",
category: "dangerous_chmod",
message: "对系统目录执行 chmod 777 被硬阻止",
},
// ══════ chown 到 root ══════
HardlinePattern {
pattern: r"\bchown\s+-R\s+root:root\s+/",
category: "chown_root",
message: "递归 chown root 到根目录被硬阻止",
},
]
});
/// 编译后的 Hardline 正则集合(模块加载时编译一次)
static HARDLINE_REGEX_SET: LazyLock<RegexSet> = LazyLock::new(|| {
let patterns: Vec<&str> = HARDLINE_PATTERNS.iter().map(|p| p.pattern).collect();
RegexSet::new(&patterns).expect("Hardline regex patterns must compile")
});
// ── 检查 API ──
/// Hardline 检查结果
#[derive(Debug, Clone)]
pub struct HardlineResult {
/// 是否被阻止
pub blocked: bool,
/// 阻止原因(供 LLM 查看)
pub reason: String,
/// 命令类别(供日志分类)
pub category: Option<String>,
}
impl HardlineResult {
/// 通过检查
pub fn allowed() -> Self {
HardlineResult {
blocked: false,
reason: String::new(),
category: None,
}
}
/// 被拒绝
pub fn denied(reason: String, category: &str) -> Self {
HardlineResult {
blocked: true,
reason,
category: Some(category.to_string()),
}
}
}
/// 检查命令是否命中 hardline 模式。
///
/// 执行反规避标准化后再匹配,返回第一个命中的模式。
///
/// 注意:此函数在模块导入时冻结 `HERMES_YOLO_MODE`
/// 确保运行时无法通过设置环境变量绕过 hardline 检查。
pub fn check_command(raw_command: &str) -> HardlineResult {
let normalized = normalize_command(raw_command);
// ── YOLO 模式冻结 ──
// YOLO 模式在首次调用时从环境变量读取并缓存,
// 后续设置环境变量不会生效(防止注入攻击)。
static YOLO_MODE_FROZEN: LazyLock<bool> = LazyLock::new(|| {
let val = std::env::var("HERMES_YOLO_MODE")
.unwrap_or_default()
.to_lowercase();
val == "1" || val == "true" || val == "yes" || val == "on"
});
// Hardline 即使在 YOLO 模式下也不可绕过
let _yolo = *YOLO_MODE_FROZEN;
let matches: Vec<usize> = HARDLINE_REGEX_SET
.matches(&normalized)
.into_iter()
.collect();
if let Some(&idx) = matches.first() {
let pattern = &HARDLINE_PATTERNS[idx];
let reason = format!(
"⚠️ 命令被硬阻止(安全策略,不可绕过)。\n\
类别: {}\n\
原因: {}\n\
请换用更安全的替代方案实现相同目标。",
pattern.category, pattern.message
);
HardlineResult::denied(reason, pattern.category)
} else {
HardlineResult::allowed()
}
}
/// 仅做标准化(不检查 hardline),用于在其他安全检查前预处理命令。
pub fn normalize_only(raw: &str) -> String {
normalize_command(raw)
}
/// 检查命令是否包含危险的重定向操作。
/// 用于 file_write/file_edit 等非 bash 工具的路径安全检查。
pub fn check_dangerous_path(path: &str) -> HardlineResult {
let normalized = normalize_command(path);
// 检查是否尝试覆盖关键系统文件
let dangerous_prefixes = [
"/etc/passwd",
"/etc/shadow",
"/etc/sudoers",
"/etc/sudoers.d/",
"/etc/ssh/",
"/root/",
"/boot/",
"~/.ssh/authorized_keys",
"~/.ssh/id_rsa",
"~/.ssh/id_ed25519",
"~/.netrc",
"~/.pgpass",
"~/.npmrc",
"~/.pypirc",
"~/.git-credentials",
];
for prefix in &dangerous_prefixes {
if normalized.starts_with(prefix) || normalized.contains(prefix) {
return HardlineResult::denied(
format!(
"路径 '{}' 指向受保护的系统/凭据文件,写入操作被硬阻止。",
path
),
"sensitive_path",
);
}
}
HardlineResult::allowed()
}
#[cfg(test)]
mod tests {
use super::*;
// ── 标准化测试 ──
#[test]
fn test_normalize_backslash_escapes() {
assert_eq!(normalize_command(r"r\m"), "rm");
assert_eq!(normalize_command(r"r\m\ \-\r\f"), "rm -rf");
}
#[test]
fn test_normalize_empty_string_literals() {
assert_eq!(normalize_command("r''m"), "rm");
assert_eq!(normalize_command("r\"\"m"), "rm");
}
#[test]
fn test_normalize_fullwidth() {
// 全角 '' (U+FF52) → 半角 'r'
let fullwidth_rm = "\u{FF52}\u{FF4D}"; // rm
let normalized = normalize_command(fullwidth_rm);
assert_eq!(normalized, "rm");
}
#[test]
fn test_normalize_ansi_strip() {
let cmd = "\x1b[31mrm -rf /\x1b[0m";
let normalized = normalize_command(cmd);
assert_eq!(normalized, "rm -rf /");
}
#[test]
fn test_normalize_null_bytes() {
let cmd = "rm\0 -rf\0 /";
let normalized = normalize_command(cmd);
assert!(!normalized.contains('\0'));
}
// ── Hardline 检查测试 ──
#[test]
fn test_block_shutdown() {
let result = check_command("shutdown -h now");
assert!(result.blocked);
assert_eq!(result.category.as_deref(), Some("system_shutdown"));
}
#[test]
fn test_block_reboot() {
let result = check_command("reboot");
assert!(result.blocked);
}
#[test]
fn test_block_systemctl_poweroff() {
let result = check_command("systemctl poweroff");
assert!(result.blocked);
}
#[test]
fn test_block_mkfs() {
let result = check_command("mkfs.ext4 /dev/sda1");
assert!(result.blocked);
}
#[test]
fn test_block_dd_to_device() {
let result = check_command("dd if=/dev/zero of=/dev/sda bs=1M");
assert!(result.blocked);
}
#[test]
fn test_block_dd_to_nvme() {
let result = check_command("dd if=image.iso of=/dev/nvme0n1");
assert!(result.blocked);
}
#[test]
fn test_block_rm_rf_root() {
let result = check_command("rm -rf /");
assert!(result.blocked);
}
#[test]
fn test_block_rm_rf_root_wildcard() {
let result = check_command("rm -rf /*");
assert!(result.blocked);
}
#[test]
fn test_block_fork_bomb() {
let result = check_command(":(){ :|:& };:");
assert!(result.blocked);
}
#[test]
fn test_block_kill_minus_one() {
let result = check_command("kill -1 1");
assert!(result.blocked);
}
#[test]
fn test_block_redirect_overwrite_passwd() {
let result = check_command("echo 'x' > /etc/passwd");
assert!(result.blocked);
}
#[test]
fn test_block_chmod_777_etc() {
let result = check_command("chmod -R 777 /etc");
assert!(result.blocked);
}
#[test]
fn test_block_chown_root() {
let result = check_command("chown -R root:root /");
assert!(result.blocked);
}
#[test]
fn test_allow_normal_commands() {
assert!(!check_command("ls -la").blocked);
assert!(!check_command("cargo build").blocked);
assert!(!check_command("git status").blocked);
assert!(!check_command("python3 -c 'print(1+1)'").blocked);
}
#[test]
fn test_allow_safe_rm() {
// rm 单个文件不阻止
assert!(!check_command("rm file.txt").blocked);
assert!(!check_command("rm -rf ./node_modules").blocked);
}
#[test]
fn test_allow_dd_to_file() {
// dd 写入普通文件不阻止
assert!(!check_command("dd if=/dev/zero of=test.bin bs=1M count=10").blocked);
}
#[test]
fn test_evasion_backslash_escapes() {
// r\e\b\o\o\t 应该匹配 reboot
let result = check_command(r"r\e\b\o\o\t");
assert!(result.blocked);
}
#[test]
fn test_evasion_empty_quotes() {
// r''m 应该匹配
let result = check_command("r''m -rf /");
assert!(result.blocked);
}
#[test]
fn test_dangerous_path_check() {
let result = check_dangerous_path("/etc/passwd");
assert!(result.blocked);
let result = check_dangerous_path("/home/user/data.txt");
assert!(!result.blocked);
}
}