后端引擎 backend/pkg/agentcli/(纯引擎库,零业务依赖,剥离 CLI/TUI 外壳): - llm: OpenAI 兼容客户端,复用 model.LLMConfig - tools: ssh_execute(复用 pkg/remoteexec)+ analyze_output + 风险评估黑名单 - safety: 三级安全模式 + 五级风险 + WebSocket 异步确认 - prompt: 五层 System Prompt - core: ReAct 16 轮循环 + 自适应检查点 + 循环检测 - evolver: 四层记忆 + 进化循环,data/agentcli/ 文件存储 集成层: - controller/agent_cli.go: WebSocket 控制器(事件流推流 + 高危确认 + 会话持久化) - model/agent_cli.go: agent_cli_sessions/messages 表 + AutoMigrate - 路由 /ws/agent-cli + /api/v1/aiops/agent-cli/sessions - seed 权限 aiops:agent-cli / aiops:llm-config 前端: - AgentCLI.tsx: 对话流 + 工具块 + 高危确认 + 四模块 tips 面板 - LLMConfig.tsx: 大模型设置独立页 - api/agentCli.ts + Sidebar/App 菜单路由 文档: - docs/agent/ 11 篇(opsxcli 同名文档适配改写 + 00-README 映射总览) 其他: - CLAUDE.md 补充 agent-cli 模块说明 - 修正 .gitignore 根级二进制规则(前导 /,避免误伤 frontend/src/components/aiops/) Co-Authored-By: Claude <noreply@anthropic.com>
16 KiB
09 - 安全系统与工具注册
本文由 opsxcli 同名设计文档适配改写,结合 aiops Agent CLI 的 Web 实现(
backend/pkg/agentcli/safety/safety.go+tools/registry.go+tools/risk.go)。 保留:三级安全模式 / 五级风险 / 危险命令黑名单 / AssessBashRisk 动态评估 / Tool 接口 + Registry。 适配:确认机制从 CLI 终端bufio.Reader改为 WebSocketconfirm事件 + 前端useConfirm()弹框;审计日志从文件改为 DB 落库。
设计理念
安全系统是运维 Agent 的生命线。aiops Agent CLI 安全系统遵循两个核心原则:
- 默认安全:balanced 模式下,high+ 操作需要确认,覆盖 95% 日常运维
- 可审计:每次工具调用都通过 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 结构
// 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 版的确认机制
type ConfirmFn func(toolName string, args map[string]interface{}, risk tools.RiskLevel) (bool, error)
controller 实现的 confirmFn:
// 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):
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 回传解锁引擎。
三级安全模式
const (
ModeStrict Mode = "strict" // medium+ 需确认
ModeBalanced Mode = "balanced" // high+ 需确认(默认)
ModePermissive Mode = "permissive" // 仅 critical 处理
)
| 模式 | 需要确认的等级 | 适用场景 |
|---|---|---|
strict |
medium 及以上 | 生产环境、关键主机、新手操作 |
balanced(默认) |
high 及以上 | 日常运维 |
permissive |
仅 critical | 测试环境、自动化脚本 |
阈值映射:
func (c *Controller) riskThreshold() tools.RiskLevel {
switch c.mode {
case ModeStrict: return tools.RiskMedium
case ModePermissive: return tools.RiskCritical
default: return tools.RiskHigh
}
}
五级风险评估
// 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()):
case RiskSafe: return "安全"
case RiskLow: return "低风险"
case RiskMedium: return "中风险"
case RiskHigh: return "高风险"
case RiskCritical: return "危险"
前端在 tool_call 事件中拿到 risk 字段,渲染为风险标签。
危险命令黑名单(硬红线)
// 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 + 根目录路径(即使不在黑名单字面中):
if strings.HasPrefix(lower, "rm") && strings.Contains(lower, "-rf") && containsRootPath(lower) {
return true, "rm -rf 包含根目录路径"
}
containsRootPath 检测 /boot、/etc、/usr、/var 等系统目录。
Check 策略
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)
}
三层策略:
- 硬红线(Critical):黑名单命令直接拒绝,不可被确认放行
- 阈值放行:低于模式阈值的操作直接执行
- 用户确认(达阈值):调
confirmFn推 WS 弹框等用户决定
AssessBashRisk 动态评估
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 前):
// assessSingleCommand
if isSudo {
return RiskHigh // 高危命令 + sudo → High
}
return RiskMedium // 高危命令无 sudo → Medium
组合命令取最高
// 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 落库:
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 注册中心
// 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 方法)
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 扩展接口
type DynamicRiskTool interface {
Tool
RiskLevelForArgs(args map[string]interface{}) RiskLevel
}
安全控制器 Check 时优先调用动态评估:
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 结构
type Result struct {
Success bool `json:"success"`
Output string `json:"output"`
Error string `json:"error,omitempty"`
}
参数解析辅助
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 内置工具。
与 opsxcli 的差异:剥离了
local_bash/scp_transfer/file_read/file_search四个工具;剥离 SSH 连接池(aiops 直接用pkg/remoteexec,无连接池需求);剥离Closer接口(Web 工具无资源需释放)。
扩展指南
添加新工具
// 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 切片:
var dangerousCommands = []string{
// ... 现有
"your-critical-command",
}
自定义确认逻辑
controller 的 confirmFn 可对接企业审批系统(如先走工单再确认):
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