feat: 新增 Agent CLI — Web 版 AI 运维 Agent(参考 opsxcli 移植)
后端引擎 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>
This commit is contained in:
1 parent
f2069fe3f0
commit
92a2247390
37 files changed
+6449
-6
No files matched your search
@@ -0,0 +1,468 @@
|
||||
# 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
|
||||
Reference in new issue
Block a user