feat: 加固 Agent CLI 执行边界并更新模型目录
收紧 SSH 命令护栏、权限校验与远程执行会话,并同步主流大模型供应商列表。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
67660ae1d6
commit
2fd1624bec
18 files changed
+480
-49
No files matched your search
@@ -523,12 +523,13 @@ func (ac *agentConn) buildTargetInfo() string {
|
||||
|
||||
// confirmFn 高危命令确认回调:推 confirm 事件并阻塞等前端回传 // Logan.Li
|
||||
func (ac *agentConn) confirmFn(toolName string, args map[string]interface{}, risk tools.RiskLevel) (bool, error) {
|
||||
safeArgs := guardrail.SanitizeMap(args)
|
||||
confirmID := fmt.Sprintf("cf-%d", time.Now().UnixNano())
|
||||
cmd, _ := args["command"].(string)
|
||||
cmd, _ := safeArgs["command"].(string)
|
||||
if cmd == "" && (toolName == "kubernetes_action" || toolName == "k8s_action") {
|
||||
cmd = fmt.Sprintf("%v %v", args["action"], args["name"])
|
||||
cmd = fmt.Sprintf("%v %v", safeArgs["action"], safeArgs["name"])
|
||||
}
|
||||
ac.write(wsEvent{Type: "confirm", Phase: core.PhaseApproval, Summary: "等待用户确认", ConfirmID: confirmID, ToolName: toolName, Command: cmd, Risk: risk.Display(), ToolArgs: args})
|
||||
ac.write(wsEvent{Type: "confirm", Phase: core.PhaseApproval, Summary: "等待用户确认", ConfirmID: confirmID, ToolName: toolName, Command: cmd, Risk: risk.Display(), ToolArgs: safeArgs})
|
||||
|
||||
ch := make(chan bool, 1)
|
||||
ac.cfMu.Lock()
|
||||
@@ -537,7 +538,7 @@ func (ac *agentConn) confirmFn(toolName string, args map[string]interface{}, ris
|
||||
|
||||
select {
|
||||
case approved := <-ch:
|
||||
ac.recordConfirmSpan(toolName, args, risk, approved)
|
||||
ac.recordConfirmSpan(toolName, safeArgs, risk, approved)
|
||||
return approved, nil
|
||||
case <-ac.taskCtx.Done():
|
||||
ac.delConfirm(confirmID)
|
||||
|
||||
@@ -84,8 +84,7 @@ func (h *AgentCLIHandler) DeleteSession(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, model.Response{Code: 500, Message: "删除消息失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
_ = h.db.Where("run_id = ?", sess.RunID).Delete(&model.AIRunSpan{}).Error
|
||||
_ = h.db.Where("run_id = ?", sess.RunID).Delete(&model.OpsCommand{}).Error
|
||||
// 会话历史可删除,但远程执行 Span 与命令审计必须独立保留。 // Logan.Li
|
||||
if err := h.db.Delete(&model.AIRun{}, id).Error; err != nil {
|
||||
c.JSON(http.StatusOK, model.Response{Code: 500, Message: "删除会话失败: " + err.Error()})
|
||||
return
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"ezops/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestDeleteAgentCLISessionRetainsExecutionAudit(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("打开测试数据库失败: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.AIRun{}, &model.AIRunMessage{}, &model.AIRunSpan{}, &model.OpsCommand{}); err != nil {
|
||||
t.Fatalf("创建测试表失败: %v", err)
|
||||
}
|
||||
run := model.AIRun{RunID: "run-delete-audit", UserID: 7, Feature: "agent_cli", StartedAt: time.Now()}
|
||||
if err := db.Create(&run).Error; err != nil {
|
||||
t.Fatalf("创建会话失败: %v", err)
|
||||
}
|
||||
db.Create(&model.AIRunMessage{RunID: run.RunID, Role: "tool", Type: "tool_result", Content: "ok"})
|
||||
db.Create(&model.AIRunSpan{RunID: run.RunID, SpanID: "span-delete-audit", Kind: "tool", Name: "ssh_execute", StartedAt: time.Now()})
|
||||
db.Create(&model.OpsCommand{RunID: run.RunID, CommandID: "cmd-delete-audit", UserID: 7, Command: "uptime", ExecutedAt: time.Now()})
|
||||
|
||||
router := gin.New()
|
||||
router.DELETE("/sessions/:id", func(c *gin.Context) {
|
||||
c.Set("userID", uint(7))
|
||||
NewAgentCLIHandler(db).DeleteSession(c)
|
||||
})
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, httptest.NewRequest(http.MethodDelete, "/sessions/"+strconv.FormatUint(uint64(run.ID), 10), nil))
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("删除响应状态=%d, body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
|
||||
assertCount := func(name string, target interface{}, want int64) {
|
||||
t.Helper()
|
||||
var count int64
|
||||
if err := db.Model(target).Where("run_id = ?", run.RunID).Count(&count).Error; err != nil {
|
||||
t.Fatalf("查询 %s 失败: %v", name, err)
|
||||
}
|
||||
if count != want {
|
||||
t.Fatalf("%s 数量=%d, want %d", name, count, want)
|
||||
}
|
||||
}
|
||||
assertCount("消息", &model.AIRunMessage{}, 0)
|
||||
assertCount("Span", &model.AIRunSpan{}, 1)
|
||||
assertCount("命令审计", &model.OpsCommand{}, 1)
|
||||
}
|
||||
@@ -141,11 +141,13 @@ func resolveEndpoint(provider, endpoint string) string {
|
||||
return "https://api.moonshot.cn/v1"
|
||||
case "kimi": // Kimi 编码套餐 OpenAI 兼容端点(注意:该端点有客户端白名单,通用调用可能被拒) // Logan.Li
|
||||
return "https://api.kimi.com/coding/v1"
|
||||
case "mimo": // 小米 MiMo OpenAI 兼容端点 // Logan.Li
|
||||
return "https://api.mimo.mi.com/v1"
|
||||
case "mimo": // 小米 MiMo 按量 OpenAI 兼容端点 // Logan.Li
|
||||
return "https://api.xiaomimimo.com/v1"
|
||||
case "mimo-plan": // 小米 MiMo Token Plan 包月专用端点(OpenAI 兼容,套餐密钥) // Logan.Li
|
||||
return "https://token-plan-cn.xiaomimimo.com/v1"
|
||||
case "anthropic": // Anthropic 官方 Messages 协议(anthropic.go 客户端拼 /v1/messages) // Logan.Li
|
||||
return "https://api.anthropic.com"
|
||||
case "glm-coding": // 智谱 GLM 编码套餐 Anthropic 协议端点(模型 GLM-5.2) // Logan.Li
|
||||
case "glm-coding": // 智谱 GLM 编码套餐 Anthropic 协议端点(模型 GLM-5.3) // Logan.Li
|
||||
return "https://open.bigmodel.cn/api/anthropic"
|
||||
case "minimax": // MiniMax 按量 OpenAI 兼容端点 // Logan.Li
|
||||
return "https://api.minimaxi.com/v1"
|
||||
|
||||
Reference in new issue
Block a user