Files
opsxdb/server/cmd/opsxdb-crud-accept/main.go
T
logan 2c0bfafd8d 同步完整源码至远程:扁平化重构现状 + 未入库源文件
- 补入库:server/internal/db/dump.go、redis 分页/扫描预算测试、opsxdb-mcp main_test、
  packages/opsxdb-mcp package-lock、docs 审计与对比文档等 37 个新文件
- 含结构扁平化进行中的 80 处修改与 155 处旧结构删除(server/ src/ src-tauri/ 为当前有效源)
- 构建产物(bin/target/dist)与签名私钥仍按规则排除
2026-10-16 03:47:33 +08:00

285 lines
9.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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"
// 真库不可达时尽早退出,避免把后续的 nil 结果误报成 CRUD 逻辑故障。
if _, err := rw.Call(ctx, aitools.ToolExecuteQuery, map[string]any{
"connection_id": mysqlID, "database": "demo", "sql": "SELECT 1 AS ready",
}); err != nil {
fmt.Printf("BLOCKED: MySQL preflight: %v\n", err)
os.Exit(1)
}
if _, err := rw.Call(ctx, aitools.ToolExecuteRedisCommand, map[string]any{
"connection_id": redisID, "command": "PING",
}); err != nil {
fmt.Printf("BLOCKED: Redis preflight: %v\n", err)
os.Exit(1)
}
// 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]
}