Files
opsxdb/server/cmd/opsxdb-crud-accept/main.go
T
logan 1513f4bc2d feat(mcp): 设置 → MCP 子页 + 策略 API + 官网/文档接入
借鉴 DBX:安装检测(Node/npm/包/Go 二进制)、只读与 allow_dangerous、
连接白名单、多客户端配置片段;GET/PUT /api/ai/mcp-policy、/mcp-install;
文档 /docs/mcp 与落地页 AI/MCP 块;对照 docs/16 勾选设置项、列仍未完清单。
2026-10-15 14:13:20 +08:00

272 lines
8.9 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"
// 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]
}