// opsxdb-crud-accept:MySQL + Redis 增删改查冒烟(进程内 mode=read_write,不改 mcp.json)。 package main import ( "context" "encoding/json" "fmt" "os" "strings" "time" "opsxdb/server/internal/aitools" ) func dump(v any) string { b, _ := json.MarshalIndent(v, "", " ") return string(b) } func short(v any, n int) string { s := dump(v) if len(s) > n { return s[:n] + "…" } return s } type step struct { Name string OK bool Note string } func main() { ctx := context.Background() mysqlID := "accept-mysql-demo" redisID := "accept-redis-demo" marker := fmt.Sprintf("opsxdb_crud_%d", time.Now().Unix()) email := marker + "@smoke.local" redisKey := "opsxdb:crud:smoke:" + marker var steps []step record := func(name string, ok bool, note string) { steps = append(steps, step{Name: name, OK: ok, Note: note}) tag := "OK" if !ok { tag = "FAIL" } fmt.Printf("[%s] %s — %s\n", tag, name, note) } // --- A) 默认 mcp.json read_only:写必须拒 --- ro, err := aitools.DefaultRuntime() if err != nil { fmt.Println("FAIL: DefaultRuntime", err) os.Exit(1) } _, err = ro.Call(ctx, aitools.ToolExecuteQuery, map[string]any{ "connection_id": mysqlID, "database": "demo", "sql": "INSERT INTO users (name, email, status) VALUES ('crud_ro', '" + email + "', 0)", }) record("MySQL INSERT under read_only", err != nil, fmt.Sprintf("err=%v", err)) _, err = ro.Call(ctx, aitools.ToolExecuteRedisCommand, map[string]any{ "connection_id": redisID, "command": "SET " + redisKey + " v1", }) record("Redis SET under read_only", err != nil, fmt.Sprintf("err=%v", err)) // --- B) 进程内 read_write Runtime --- rw := aitools.NewRuntime(aitools.Policy{ AllowedConnectionIDs: []string{mysqlID, redisID}, Mode: aitools.ModeReadWrite, AllowDangerous: true, // 验收 DROP 确认路径;生产默认 false DefaultLimit: 100, MaxLimit: 500, }) rw.Source = "crud-accept" // B1 无确认 → needConfirm out, err := rw.Call(ctx, aitools.ToolExecuteQuery, map[string]any{ "connection_id": mysqlID, "database": "demo", "sql": "INSERT INTO users (name, email, status) VALUES ('crud_pending', '" + email + "', 0)", }) need := false bind := "" if err == nil { if m, ok := out.(map[string]any); ok { need, _ = m["needConfirm"].(bool) bind, _ = m["confirmBind"].(string) } } record("MySQL INSERT no confirm → needConfirm", err == nil && need && bind != "", fmt.Sprintf("needConfirm=%v bind=%s… out=%s", need, trunc(bind, 12), short(out, 220))) // B1b allow_dangerous=false 时 DROP 即便确认也拒 rwSafe := aitools.NewRuntime(aitools.Policy{ AllowedConnectionIDs: []string{mysqlID, redisID}, Mode: aitools.ModeReadWrite, AllowDangerous: false, }) rwSafe.AllowConfirmedWrite = true _, err = rwSafe.Call(ctx, aitools.ToolExecuteQuery, map[string]any{ "connection_id": mysqlID, "database": "demo", "sql": "DROP TABLE IF EXISTS __opsxdb_crud_deny_ddl__", "confirm_danger": true, }) record("MySQL DROP denied without allow_dangerous", err != nil && strings.Contains(fmt.Sprint(err), "dangerous sql disabled"), fmt.Sprintf("err=%v", err)) // B2 DROP 无确认 → needConfirm(不真执行) out, err = rw.Call(ctx, aitools.ToolExecuteQuery, map[string]any{ "connection_id": mysqlID, "database": "demo", "sql": "DROP TABLE IF EXISTS __opsxdb_crud_should_deny__", }) need = false if err == nil { if m, ok := out.(map[string]any); ok { need, _ = m["needConfirm"].(bool) } } record("MySQL DROP no confirm → needConfirm", err == nil && need, short(out, 200)) // B3 SELECT 基线 out, err = rw.Call(ctx, aitools.ToolExecuteQuery, map[string]any{ "connection_id": mysqlID, "database": "demo", "sql": "SELECT id, name, email, status FROM users ORDER BY id DESC LIMIT 5", "limit": 5, }) record("MySQL SELECT baseline", err == nil, short(out, 400)) // B4 describe users out, err = rw.Call(ctx, aitools.ToolDescribeTable, map[string]any{ "connection_id": mysqlID, "database": "demo", "table": "users", }) record("MySQL describe users", err == nil, short(out, 350)) // B5 INSERT + confirm insSQL := "INSERT INTO users (name, email, status) VALUES ('crud_smoke', '" + email + "', 1)" out, err = rw.Call(ctx, aitools.ToolExecuteQuery, map[string]any{ "connection_id": mysqlID, "database": "demo", "sql": insSQL, "confirm_danger": true, }) record("MySQL INSERT confirm", err == nil, short(out, 300)) // B6 SELECT 确认插入 out, err = rw.Call(ctx, aitools.ToolExecuteQuery, map[string]any{ "connection_id": mysqlID, "database": "demo", "sql": "SELECT id, name, email, status FROM users WHERE email = '" + email + "'", }) var insertedID any rowCount := 0 if err == nil { s := dump(out) if strings.Contains(s, `"rowCount": 1`) || strings.Contains(s, `"rowCount":1`) { rowCount = 1 } if strings.Contains(s, "crud_smoke") && strings.Contains(s, email) { insertedID = "found" } } record("MySQL SELECT after INSERT", err == nil && rowCount >= 1 && insertedID != nil, fmt.Sprintf("rowCount=%d id=%v %s", rowCount, insertedID, short(out, 350))) // B7 UPDATE + confirm updSQL := fmt.Sprintf("UPDATE users SET name = 'crud_updated', status = 0 WHERE email = '%s'", email) out, err = rw.Call(ctx, aitools.ToolExecuteQuery, map[string]any{ "connection_id": mysqlID, "database": "demo", "sql": updSQL, "confirm_danger": true, }) record("MySQL UPDATE confirm", err == nil, short(out, 250)) out, err = rw.Call(ctx, aitools.ToolExecuteQuery, map[string]any{ "connection_id": mysqlID, "database": "demo", "sql": "SELECT id, name, email, status FROM users WHERE email = '" + email + "'", }) nameOK := false if err == nil { s := dump(out) nameOK = strings.Contains(s, "crud_updated") } record("MySQL SELECT after UPDATE", err == nil && nameOK, short(out, 350)) // B8 DELETE + confirm(清理) delSQL := fmt.Sprintf("DELETE FROM users WHERE email = '%s'", email) out, err = rw.Call(ctx, aitools.ToolExecuteQuery, map[string]any{ "connection_id": mysqlID, "database": "demo", "sql": delSQL, "confirm_danger": true, }) record("MySQL DELETE confirm (cleanup)", err == nil, short(out, 250)) out, err = rw.Call(ctx, aitools.ToolExecuteQuery, map[string]any{ "connection_id": mysqlID, "database": "demo", "sql": "SELECT id FROM users WHERE email = '" + email + "'", }) gone := false if err == nil { if m, ok := out.(map[string]any); ok { switch rc := m["rowCount"].(type) { case int: gone = rc == 0 case float64: gone = int(rc) == 0 } } } record("MySQL SELECT after DELETE (expect 0)", err == nil && gone, short(out, 200)) // --- C) Redis --- out, err = rw.Call(ctx, aitools.ToolExecuteRedisCommand, map[string]any{ "connection_id": redisID, "command": "SET " + redisKey + " hello-opsxdb", }) need = false if err == nil { if m, ok := out.(map[string]any); ok { need, _ = m["needConfirm"].(bool) } } record("Redis SET no confirm → needConfirm", err == nil && need, short(out, 220)) out, err = rw.Call(ctx, aitools.ToolExecuteRedisCommand, map[string]any{ "connection_id": redisID, "command": "SET " + redisKey + " hello-opsxdb", "confirm_danger": true, }) record("Redis SET confirm", err == nil, short(out, 220)) out, err = rw.Call(ctx, aitools.ToolExecuteRedisCommand, map[string]any{ "connection_id": redisID, "command": "GET " + redisKey, }) got := strings.Contains(dump(out), "hello-opsxdb") record("Redis GET", err == nil && got, short(out, 220)) out, err = rw.Call(ctx, aitools.ToolExecuteRedisCommand, map[string]any{ "connection_id": redisID, "command": "DEL " + redisKey, "confirm_danger": true, }) record("Redis DEL confirm (cleanup)", err == nil, short(out, 220)) out, err = rw.Call(ctx, aitools.ToolExecuteRedisCommand, map[string]any{ "connection_id": redisID, "command": "GET " + redisKey, }) record("Redis GET after DEL", err == nil, short(out, 200)) _, err = rw.Call(ctx, aitools.ToolExecuteRedisCommand, map[string]any{ "connection_id": redisID, "command": "FLUSHDB", "confirm_danger": true, }) record("Redis FLUSHDB always deny (even confirmed)", err != nil, fmt.Sprintf("err=%v", err)) _, err = rw.Call(ctx, aitools.ToolExecuteRedisCommand, map[string]any{ "connection_id": redisID, "command": "KEYS *", "confirm_danger": true, }) record("Redis KEYS always deny", err != nil, fmt.Sprintf("err=%v", err)) // --- D) incomplete / max_steps 若可(不依赖 LLM,仅看 harness 配置字段)--- // 用极小 MaxRounds 的 Agent 需要 LLM;这里只验证 ConfirmBindHash 与 outcome 字段在包内可用。 bind2 := aitools.ConfirmBindHash(mysqlID, insSQL) record("ConfirmBindHash non-empty", bind2 != "", "bind="+trunc(bind2, 16)) failN := 0 for _, s := range steps { if !s.OK { failN++ } } fmt.Println("---") fmt.Printf("TOTAL %d steps, FAIL=%d marker=%s\n", len(steps), failN, marker) if failN > 0 { os.Exit(1) } fmt.Println("ALL CRUD ACCEPTANCE PASSED") } func trunc(s string, n int) string { if len(s) <= n { return s } return s[:n] }