后端引擎 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>
469 lines
16 KiB
Markdown
469 lines
16 KiB
Markdown
# 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
|