# 09 - 安全系统与工具注册 > 本文由 opsxcli 同名设计文档适配改写,结合 aiops Agent CLI 的 Web 实现(`backend/pkg/agentcli/safety/safety.go` + `tools/registry.go` + `tools/risk.go`)。 > **保留**:三级安全模式 / 五级风险 / 危险命令黑名单 / AssessBashRisk 动态评估 / Tool 接口 + Registry。 > **适配**:确认机制从 CLI 终端 `bufio.Reader` 改为 WebSocket `confirm` 事件 + 前端 `useConfirm()` 弹框;审计日志从文件改为 DB 落库。 ## 设计理念 安全系统是运维 Agent 的生命线。aiops Agent CLI 安全系统遵循两个核心原则: 1. **默认安全**:balanced 模式下,high+ 操作需要确认,覆盖 95% 日常运维 2. **可审计**:每次工具调用都通过 EventCallback 落库,事后可追溯 工具注册系统遵循**工具化思维**:万物皆 Tool,实现接口即可扩展。 --- ## Part 1: 安全系统 ### 架构总览 ``` ┌────────────────────────────────────────────────────┐ │ safety.Controller │ │ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ risk.go │ │ controller.go │ │ │ │ (在 tools 包)│ │ (在 safety 包)│ │ │ │ 黑名单 + 分级 │ │ 阈值 + ConfirmFn│ │ │ └──────────────┘ └──────────────┘ │ │ │ │ Safety Modes: │ │ strict → medium+ 确认 │ │ balanced → high+ 确认(默认) │ │ permissive → critical 仅处理 │ └────────────────────────────────────────────────────┘ ``` > 与 opsxcli 的差异:opsxcli 把风险评估和审计都放在 `safety/` 包;aiops 为避免 `tools` ↔ `safety` 循环依赖,把 `risk.go`(黑名单 + AssessBashRisk)置于 `tools` 包,由 `ssh_execute` 的 `DynamicRiskTool` 直接调用;`safety` 包只做「是否需确认」的策略。 ### Controller 结构 ```go // backend/pkg/agentcli/safety/safety.go type Controller struct { mode Mode // 当前安全模式 confirmFn ConfirmFn // 确认回调(controller 注入:推 WS confirm + 阻塞等回传) autoApprove bool // 自动批准(仅测试用,生产禁用) } ``` > 与 opsxcli 的差异:剥离 `reader *bufio.Reader`(CLI 终端交互)和 `history []ExecutionRecord`(内存审计历史),改为通过 EventCallback 落库 DB;剥离 `auditLog *AuditLogWriter`(文件审计)。 ### ConfirmFn —— Web 版的确认机制 ```go type ConfirmFn func(toolName string, args map[string]interface{}, risk tools.RiskLevel) (bool, error) ``` controller 实现的 `confirmFn`: ```go // backend/controller/agent_cli.go func (ac *agentConn) confirmFn(toolName string, args map[string]interface{}, risk tools.RiskLevel) (bool, error) { confirmID := fmt.Sprintf("cf-%d", time.Now().UnixNano()) cmd, _ := args["command"].(string) // 1. 推 WS confirm 事件(前端 useConfirm 弹框) ac.write(wsEvent{ Type: "confirm", ConfirmID: confirmID, ToolName: toolName, Command: cmd, Risk: risk.Display(), ToolArgs: args, }) // 2. 注册 channel 等待前端回传 ch := make(chan bool, 1) ac.cfMu.Lock() ac.confirms[confirmID] = ch ac.cfMu.Unlock() // 3. 阻塞等待(5 分钟超时) select { case approved := <-ch: return approved, nil case <-time.After(5 * time.Minute): ac.delConfirm(confirmID) return false, fmt.Errorf("确认超时") } } ``` 前端处理(`AgentCLI.tsx`): ```tsx case 'confirm': { const ok = await confirm({ title: '确认执行高危命令', description: `即将执行:${ev.command}`, confirmText: '确认执行', }); wsRef.current?.send(JSON.stringify({ type: ok ? 'confirm' : 'deny', confirm_id: ev.confirm_id, })); break; } ``` 这是「Web 版 CLI 终端确认」的完整替代 —— 用户在浏览器看到弹框,点确认/拒绝,结果通过 WebSocket 回传解锁引擎。 ### 三级安全模式 ```go const ( ModeStrict Mode = "strict" // medium+ 需确认 ModeBalanced Mode = "balanced" // high+ 需确认(默认) ModePermissive Mode = "permissive" // 仅 critical 处理 ) ``` | 模式 | 需要确认的等级 | 适用场景 | |------|---------------|----------| | `strict` | medium 及以上 | 生产环境、关键主机、新手操作 | | `balanced`(默认) | high 及以上 | 日常运维 | | `permissive` | 仅 critical | 测试环境、自动化脚本 | 阈值映射: ```go func (c *Controller) riskThreshold() tools.RiskLevel { switch c.mode { case ModeStrict: return tools.RiskMedium case ModePermissive: return tools.RiskCritical default: return tools.RiskHigh } } ``` ### 五级风险评估 ```go // backend/pkg/agentcli/tools/registry.go const ( RiskSafe RiskLevel = iota // 0 - 只读(cat, grep, df, ps) RiskLow // 1 - 低风险(mkdir, touch) RiskMedium // 2 - 中风险(chmod, 文件修改) RiskHigh // 3 - 高风险(ssh_execute, 服务控制) RiskCritical // 4 - 危险(rm -rf /, mkfs, dd, 数据删除) ) ``` 每级都有中文显示(`Display()`): ```go case RiskSafe: return "安全" case RiskLow: return "低风险" case RiskMedium: return "中风险" case RiskHigh: return "高风险" case RiskCritical: return "危险" ``` 前端在 `tool_call` 事件中拿到 `risk` 字段,渲染为风险标签。 ### 危险命令黑名单(硬红线) ```go // backend/pkg/agentcli/tools/risk.go var dangerousCommands = []string{ // 文件系统毁灭性操作 "rm -rf /", "rm -rf /*", "rm -rf /bin", "rm -rf /boot", "rm -rf /etc", "rm -rf /home", "rm -rf /lib", "rm -rf /lib64", "rm -rf /proc", "rm -rf /root", "rm -rf /sbin", "rm -rf /sys", "rm -rf /usr", "rm -rf /var", // 磁盘格式化 "mkfs", "mkfs.ext", "mkfs.xfs", "mkfs.btrfs", "mkfs.vfat", // 磁盘覆写 "dd if=/dev/zero", "dd if=/dev/random", "dd if=/dev/urandom", // Fork bomb ":(){ :|:& };:", ":() { : | : & }; :", // 设备重定向 "> /dev/sda", "> /dev/sdb", "> /dev/sdc", "> /dev/hda", // 数据销毁 "mv / /dev/null", "mv /* /dev/null", // 权限破坏 "chmod -R 000 /", "chmod -R 777 /", // 服务自毁 "systemctl stop ssh", "systemctl disable ssh", "kill -9 1", // 防火墙清空 "iptables -F", "iptables --flush", } ``` **特殊检测**:`rm -rf` + 根目录路径(即使不在黑名单字面中): ```go if strings.HasPrefix(lower, "rm") && strings.Contains(lower, "-rf") && containsRootPath(lower) { return true, "rm -rf 包含根目录路径" } ``` `containsRootPath` 检测 `/boot`、`/etc`、`/usr`、`/var` 等系统目录。 ### Check 策略 ```go func (c *Controller) Check(tool tools.Tool, args map[string]interface{}) (bool, error) { risk := assessToolRisk(tool, args) // 1. 黑名单硬红线:绝不允许(不可确认放行) if risk >= tools.RiskCritical { return false, nil } // 2. 未达阈值或自动批准:直接放行 if c.autoApprove || risk < c.riskThreshold() { return true, nil } // 3. 达阈值:请求用户确认 if c.confirmFn == nil { return false, nil // 无确认回调时保守拒绝(Web 场景应始终注入 confirmFn) } return c.confirmFn(tool.Name(), args, risk) } ``` **三层策略**: 1. **硬红线**(Critical):黑名单命令直接拒绝,**不可被确认放行** 2. **阈值放行**:低于模式阈值的操作直接执行 3. **用户确认**(达阈值):调 `confirmFn` 推 WS 弹框等用户决定 ### AssessBashRisk 动态评估 ```go func AssessBashRisk(command string) RiskLevel { // 1. 黑名单精确匹配 → Critical if isDangerous, _ := IsDangerousCommand(command); isDangerous { return RiskCritical } // 2. 纯只读命令 → Safe if isReadOnlyCommand(lower) { return RiskSafe } // 3. 组合命令(&& || | ;)→ 取子命令最高风险 if isCompound(lower) { return assessCompoundCommand(lower) } // 4. 单命令分类 isSudo := strings.HasPrefix(lower, "sudo ") return assessSingleCommand(strings.TrimPrefix(lower, "sudo "), isSudo) } ``` ### 命令分类表 | 类别 | 命令示例 | 风险等级 | |------|---------|---------| | 只读(Safe) | cat, grep, awk, sed, head, tail, ps, top, df, du, free, uptime, uname, hostname, ifconfig, ip addr, netstat, ss, ls, pwd, who, w, last, vmstat, iostat, lsof, find, which, file, stat, date, echo, id, whoami, dmesg, systemctl status, journalctl, lsblk, mount | Safe | | 修改(Low/Medium) | cp, mv, mkdir, rmdir, touch, chmod, chown, chgrp, ln, tar, gzip, gunzip, zip, unzip, rsync, tee | Low(sudo 时 Medium) | | 高危(Medium/High) | rm, dd, shutdown, reboot, halt, poweroff, init, systemctl restart/stop/start, kill, killall, pkill, userdel, groupdel, fdisk, parted, mkfs | Medium(sudo 时 High) | | 危险(Critical) | rm -rf /, mkfs, dd if=/dev/zero, fork bomb, > /dev/sda, chmod -R 000 / | Critical(硬拒) | ### sudo 提升 `sudo` 会让风险等级 +1(最高到 Critical 前): ```go // assessSingleCommand if isSudo { return RiskHigh // 高危命令 + sudo → High } return RiskMedium // 高危命令无 sudo → Medium ``` ### 组合命令取最高 ```go // assessCompoundCommand: 拆分 && || | ; 后取子命令最高风险 // "df -h && rm /tmp/file" → max(Safe, Medium) = Medium // "cat /etc/passwd; rm -rf /tmp" → max(Safe, Low) = Low ``` ### 审计落库(替代文件审计) > opsxcli 把审计写到 `~/.opsxcli/audit/audit.log`;aiops 直接走 DB(`agent_cli_messages` 表),多用户场景下可按 user_id / session_id 查询。 controller 的 `onEvent` 回调把每个 `tool_call` / `tool_result` 落库: ```go case core.EventToolCall: argsJSON, _ := json.Marshal(e.ToolArgs) ac.persistMsg(sid, e.Round, "tool_call", "tool", "", e.ToolName, string(argsJSON), "", e.Risk, false, "", 0) case core.EventToolResult: ac.persistMsg(sid, e.Round, "tool_result", "tool", e.Output, e.ToolName, "", "", "", e.Success, e.Duration, 0) ``` 每条记录包含:工具名、参数 JSON、风险等级、成败、耗时、轮次、会话 ID、用户 ID(通过 session 关联)。前端「会话历史」页面可完整回放。 --- ## Part 2: 工具注册系统 ### Registry 注册中心 ```go // backend/pkg/agentcli/tools/registry.go type Registry struct { tools map[string]Tool mu sync.RWMutex } ``` 核心方法: | 方法 | 说明 | |------|------| | `NewRegistry()` | 创建空注册中心 | | `Register(tool)` | 注册工具(线程安全) | | `Get(name)` | 按名获取工具(LLM 调用时按名查找) | | `List()` | 列出所有工具 | | `ToLLMTools()` | 转换为 OpenAI function calling 格式 | ### Tool 接口(5 方法) ```go type Tool interface { Name() string Description() string Parameters() map[string]interface{} // JSON Schema RiskLevel() RiskLevel Execute(ctx context.Context, args map[string]interface{}) (*Result, error) } ``` ### DynamicRiskTool 扩展接口 ```go type DynamicRiskTool interface { Tool RiskLevelForArgs(args map[string]interface{}) RiskLevel } ``` 安全控制器 `Check` 时优先调用动态评估: ```go func assessToolRisk(tool tools.Tool, args map[string]interface{}) tools.RiskLevel { if dt, ok := tool.(tools.DynamicRiskTool); ok { return dt.RiskLevelForArgs(args) // 动态优先 } return tool.RiskLevel() } ``` ### Result 结构 ```go type Result struct { Success bool `json:"success"` Output string `json:"output"` Error string `json:"error,omitempty"` } ``` ### 参数解析辅助 ```go func StringParam(args map[string]interface{}, key string) string func IntParam(args map[string]interface{}, key string) int // 兼容 int/float64/int64 ``` LLM 返回的 JSON 参数反序列化后,数字可能是 `float64`(JSON 原生),`IntParam` 统一处理。 ### 已注册工具一览 | 工具 | 静态风险 | 动态风险 | 功能 | |------|---------|---------|------| | `ssh_execute` | High | Safe~Critical | 远程命令执行(DynamicRiskTool) | | `analyze_output` | Safe | — | 输出分析(纯文本,只读) | 详细设计见 [05 - Builtin 内置工具](05-Builtin内置命令.md)。 > 与 opsxcli 的差异:剥离了 `local_bash` / `scp_transfer` / `file_read` / `file_search` 四个工具;剥离 SSH 连接池(aiops 直接用 `pkg/remoteexec`,无连接池需求);剥离 `Closer` 接口(Web 工具无资源需释放)。 ## 扩展指南 ### 添加新工具 ```go // 1. 实现 Tool 接口(必要时实现 DynamicRiskTool) type MyTool struct{} func (t *MyTool) Name() string { return "my_tool" } func (t *MyTool) Description() string { return "我的工具" } func (t *MyTool) Parameters() map[string]interface{} { return map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "target": map[string]interface{}{"type": "string", "description": "目标"}, }, "required": []string{"target"}, } } func (t *MyTool) RiskLevel() tools.RiskLevel { return tools.RiskMedium } func (t *MyTool) Execute(ctx context.Context, args map[string]interface{}) (*tools.Result, error) { // 实现 return &tools.Result{Success: true, Output: "done"}, nil } // 2. 在 agentcli.NewEngine 中注册 registry.Register(&MyTool{}) ``` ### 扩展危险命令黑名单 修改 `tools/risk.go` 的 `dangerousCommands` 切片: ```go var dangerousCommands = []string{ // ... 现有 "your-critical-command", } ``` ### 自定义确认逻辑 controller 的 `confirmFn` 可对接企业审批系统(如先走工单再确认): ```go engine.SetConfirmFn(func(toolName string, args map[string]interface{}, risk tools.RiskLevel) (bool, error) { // 1. 推 WS 弹框让用户初步确认 // 2. 高危场景额外对接审批工单系统 // 3. 返回最终批准结果 }) ``` ## 常见陷阱 | 陷阱 | 解决方案 | |------|----------| | 忘记设置 RiskLevel 导致误判 | 默认按接口返回值,高危工具必须显式声明 | | 高危工具未实现 DynamicRiskTool | 静态等级无法反映参数差异,必须实现动态评估 | | 黑名单只看前缀匹配 | `IsDangerousCommand` 同时做精确匹配 + `rm -rf` 根路径特殊检测 | | 工具 Execute 无超时 | `Config.ToolTimeout=120s` 兜底(`context.WithTimeout`) | | 确认回调无超时 | controller 的 `confirmFn` 内置 5 分钟超时 | | 黑名单可被确认放行 | Check 策略第一层 Critical 直接 `return false, nil`,不走 confirmFn | | LLM 编造工具名 | `registry.Get` 找不到时返回 observation「工具未找到」,LLM 自动换法 | | 参数 JSON 解析失败 | `json.Unmarshal` 失败时返回 observation「参数解析失败」,不 panic | ## 与 opsxcli 的差异总结 | 维度 | opsxcli | aiops | |------|---------|-------| | 确认机制 | `bufio.Reader` CLI 终端交互 | WebSocket `confirm` 事件 + 前端 `useConfirm()` 弹框 | | 审计日志 | 文件 `~/.opsxcli/audit/audit.log` | DB 表 `agent_cli_messages` | | 风险评估位置 | `safety/risk.go` | `tools/risk.go`(避免循环依赖) | | 内存审计历史 | `history []ExecutionRecord`(上限 1000 FIFO) | 剥离(直接走 DB) | | 工具集 | 6 个 | 2 个 | | SSH 连接池 | 有(`ssh_pool.go`) | 剥离(用 `pkg/remoteexec`) | | Closer 接口 | 有 | 剥离 | 安全核心(三级模式 / 五级风险 / 黑名单 / 动态评估)完全保留,差异仅在确认机制和审计载体。 > 作者:Logan.Li