""" 安全层 - 统一的指令安全检查 拦截所有工具调用,通过 AI 识别风险并请求用户确认 """ import json import threading from typing import Optional, Tuple # ── 安全模式 ── MODE_UNRESTRICTED = 0 # 无限制:直接执行 MODE_NORMAL = 1 # 普通:AI 识别风险 MODE_MANUAL = 2 # 人工:所有指令都确认 MODE_NAMES = { MODE_UNRESTRICTED: "无限制", MODE_NORMAL: "普通", MODE_MANUAL: "人工确认", } # ── Plan 模式只读白名单(黑名单法)── # 值为 "*" 表示该工具整类视为写/有副作用,一律拦截; # 值为列表时,仅当首个操作词命中列表中的写操作关键字才拦截; # 值为 [] 表示该工具整类只读,一律放行(调研必需); # 未登记的未知工具默认拦截(保守)。 PLAN_WRITE_RULES = { # 整类拦截 "cmd_executor": "*", "browser_auto": "*", "network_tools": "*", "scheduler": "*", # 仅部分操作拦截 "code_editor": ["write", "create", "append", "edit", "insert", "delete_lines", "multi", "replace", "rename", "move"], "git_tools": ["commit", "push", "checkout", "reset", "merge", "revert", "clean", "stash", "tag", "fetch", "pull", "branch -", "branch delete", "branch -d", "branch -D", "remote add", "remote set", "amend"], "file_manager": ["write", "delete", "move", "copy", "rename", "mkdir", "rm", "create", "save", "touch"], "sub_agent": ["run", "dispatch", "execute", "spawn", "create", "new", "task", "assign"], "auto_engineer": ["run", "test", "build", "fix", "format", "apply", "generate", "scaffold", "create", "refactor", "exec"], "memory": ["write", "save", "add", "set", "delete", "edit", "update", "forget"], # 整类放行(只读探索必需) "tool_search": [], "ai_search": [], "code_search": [], } def plan_is_write_operation(tool_name: str, tool_args) -> bool: """Plan 模式下判断一次工具调用是否属于「写操作」(应被拦截)。 返回 True 表示会修改状态/文件/系统,应拦截;False 表示只读,可放行。 """ rules = PLAN_WRITE_RULES.get(tool_name) if rules is None: # 未登记的未知工具:保守拦截 return True if rules == "*": return True if not rules: return False args_l = str(tool_args).strip().lower() parts = args_l.split() op = parts[0] if parts else "" for kw in rules: kwp = kw.split() if len(kwp) == 1: # 单字关键字:精确匹配操作词,或作为其前缀/后缀(避免 "editor" 误中 "edit") if op == kw or op.startswith(kw) or op.endswith(kw): return True else: # 多词规则,如 "remote add" / "branch -d" if args_l.startswith(kw) or " ".join(parts[:len(kwp)]) == kw: return True return False class SafetyLayer: """统一安全检查层""" def __init__(self): self.mode = MODE_NORMAL self.cli = None self._analysis_cache = {} # 缓存分析结果 self._lock = threading.Lock() def set_cli(self, cli): self.cli = cli def get_mode(self) -> int: return self.mode def get_mode_name(self) -> str: return MODE_NAMES.get(self.mode, "未知") def set_mode(self, mode: int): self.mode = mode def cycle_mode(self) -> str: """切换到下一个模式,返回新模式名称""" self.mode = (self.mode + 1) % 3 return self.get_mode_name() # ── 核心:拦截检查 ── def check(self, tool_name: str, tool_args: str) -> Tuple[bool, str]: """ 检查工具调用是否安全 返回: (允许执行, 消息) """ # ── Plan 模式只读约束(最高优先级,先于所有安全模式判断)── # plan 模式下只允许只读调研工具,写操作一律暂挂,待 /build 批准后执行。 cli = self.cli if cli is not None and getattr(cli, 'plan_mode', False): if plan_is_write_operation(tool_name, tool_args): return False, "PLAN 模式:写操作已暂挂(只读调研中)。请在 /build 批准后执行。" return True, "" # 无限制模式:直接放行 if self.mode == MODE_UNRESTRICTED: return True, "" # 人工确认模式:所有指令都确认 if self.mode == MODE_MANUAL: return self._ask_user_confirm( tool_name, tool_args, risk_level="中", impact="当前为人工确认模式,所有指令需要确认", reason="模式要求" ) # 普通模式:AI 识别风险 return self._ai_analyze(tool_name, tool_args) # ── AI 风险识别 ── def _ai_analyze(self, tool_name: str, tool_args: str) -> Tuple[bool, str]: """使用当前引擎独立分析指令风险(不带历史对话)""" # 快速预检:明显安全的指令直接放行 if self._is_quick_safe(tool_name, tool_args): return True, "" # 获取引擎 engine = None if self.cli and hasattr(self.cli, 'current_engine'): engine = self.cli.current_engine if not engine: # 无引擎时降级为人工确认 return self._ask_user_confirm( tool_name, tool_args, risk_level="未知", impact="无法进行 AI 风险分析", reason="无可用引擎" ) # 检查缓存 cache_key = f"{tool_name}:{tool_args[:100]}" with self._lock: if cache_key in self._analysis_cache: cached = self._analysis_cache[cache_key] if cached["safe"]: return True, "" return self._ask_user_confirm( tool_name, tool_args, risk_level=cached["level"], impact=cached["impact"], reason=cached["reason"] ) # 调用 AI 独立分析(不带任何历史对话) analysis_prompt = f"""你是一个安全分析器。分析以下工具调用是否存在安全风险。 工具: {tool_name} 参数: {tool_args} 只返回以下 JSON(不要其他内容): {{"safe": true/false, "level": "低/中/高", "reason": "风险原因", "impact": "执行后可能的影响"}} 判断标准: - safe=true: 读取操作、查看信息、无害查询 - safe=false: 删除文件、执行系统命令、修改配置、网络请求、git 危险操作 - level 低: 只读操作但可能泄露信息 - level 中: 修改操作可撤销 - level 高: 不可逆操作、系统级操作""" try: response = engine.generate_response(analysis_prompt, system_prompt="你是安全分析器。只返回 JSON,不要其他内容。") if not response: return self._ask_user_confirm( tool_name, tool_args, risk_level="未知", impact="AI 分析无响应", reason="引擎未返回结果" ) # 解析 JSON result = self._parse_analysis(response) if result and result.get("safe") is True: # 缓存安全结果 with self._lock: self._analysis_cache[cache_key] = result return True, "" # 不安全,请求用户确认 level = result.get("level", "中") if result else "未知" reason = result.get("reason", "AI 识别到潜在风险") if result else "AI 分析失败" impact = result.get("impact", "未知影响") if result else "未知影响" return self._ask_user_confirm( tool_name, tool_args, risk_level=level, impact=impact, reason=reason ) except Exception as e: return self._ask_user_confirm( tool_name, tool_args, risk_level="未知", impact=f"AI 分析异常: {e}", reason="分析过程出错" ) def _parse_analysis(self, response: str) -> Optional[dict]: """解析 AI 返回的分析结果""" import re # 尝试直接解析 JSON response = response.strip() # 去掉 markdown 代码块 if response.startswith("```"): response = response.split("\n", 1)[-1] if "\n" in response else response[3:] if response.endswith("```"): response = response[:-3] response = response.strip() try: return json.loads(response) except json.JSONDecodeError: pass # 尝试从文本中提取 JSON match = re.search(r'\{[^{}]*"safe"[^{}]*\}', response) if match: try: return json.loads(match.group()) except json.JSONDecodeError: pass return None def _is_quick_safe(self, tool_name: str, tool_args: str) -> bool: """快速预检:明显安全的操作直接放行""" args = tool_args.strip().lower() # 纯读取操作 safe_patterns = { "code_editor": ["read_range", "find ", "regex ", "symbols", "imports", "ast_info", "stats", "structure", "context", "todo", "callers", "diff ", "diff_text"], "git_tools": ["status", "log", "diff", "branch", "remote", "show", "blame", "contributors", "summary"], "file_manager": ["list", "read", "info", "search"], "cmd_executor": [], # 命令执行不跳过 "auto_engineer": ["metrics", "security", "complexity", "duplicates", "suggest", "info"], "task_manager": ["list"], "browser_auto": ["status", "title", "url", "text", "html", "attr", "value", "eval", "query", "cookies"], "sub_agent": ["list", "status", "result", "stats"], "network_tools": ["ping", "status", "headers", "ip"], "tool_search": [], } patterns = safe_patterns.get(tool_name, []) for p in patterns: if args.startswith(p): return True return False # ── 用户交互 ── def _ask_user_confirm(self, tool_name: str, tool_args: str, risk_level: str = "中", impact: str = "", reason: str = "") -> Tuple[bool, str]: """向用户展示风险信息并请求确认""" from colorama import Fore, Style # 风险等级颜色 level_colors = { "低": Fore.YELLOW, "中": Fore.LIGHTYELLOW_EX, "高": Fore.RED, "未知": Fore.LIGHTBLACK_EX, } level_color = level_colors.get(risk_level, Fore.YELLOW) # 截断过长的参数 display_args = tool_args if len(display_args) > 200: display_args = display_args[:200] + "..." print() print(f" {Fore.RED}{'═' * 50}{Style.RESET_ALL}") print(f" {Fore.RED} 安全检查{Style.RESET_ALL}") print(f" {Fore.RED}{'═' * 50}{Style.RESET_ALL}") print(f" {Fore.CYAN}工具:{Style.RESET_ALL} {tool_name}") print(f" {Fore.CYAN}指令:{Style.RESET_ALL} {display_args}") print(f" {level_color}风险:{Style.RESET_ALL} {risk_level}") if reason: print(f" {Fore.YELLOW}原因:{Style.RESET_ALL} {reason}") if impact: print(f" {Fore.YELLOW}影响:{Style.RESET_ALL} {impact}") print(f" {Fore.RED}{'─' * 50}{Style.RESET_ALL}") try: choice = input(f" {Fore.WHITE}是否执行? (y/n): {Style.RESET_ALL}").strip().lower() if choice in ('y', 'yes', '是'): return True, "用户确认执行" else: return False, "用户取消执行" except (KeyboardInterrupt, EOFError): print() return False, "用户中断" # ── 全局单例 ── _safety = SafetyLayer() def get_safety() -> SafetyLayer: return _safety