package database import ( "encoding/json" "strconv" "strings" "cyberstrike-ai/internal/mcp" ) const legacyToolGuardPrefix = "工具调用已被安全规则拦截" // Only the exact envelope emitted by the old local guard is recognized here. // New executions use the structured marker and never infer policy from text. func isLegacyToolGuardRefusal(text string) bool { if !strings.HasPrefix(text, legacyToolGuardPrefix+":") && !strings.HasPrefix(text, legacyToolGuardPrefix+"\n规则: ") { return false } matchIndex := strings.LastIndex(text, "\n匹配内容: ") if matchIndex < 0 { return false } if _, err := strconv.Unquote(text[matchIndex+len("\n匹配内容: "):]); err != nil { return false } ruleIndex := strings.LastIndex(text[:matchIndex], "\n规则: ") if ruleIndex < 0 { return false } rule := text[ruleIndex+len("\n规则: ") : matchIndex] idIndex := strings.LastIndex(rule, " (") return idIndex > 0 && strings.HasSuffix(rule, ")") && len(rule[idIndex+2:len(rule)-1]) > 0 && !strings.Contains(rule, "\n") } // migrateLegacyToolGuardBlocks is idempotent because only failed records qualify. // Keeping status and accumulated failure counts in one transaction makes monitor // filters, badges and statistics agree immediately after upgrading. func (db *DB) migrateLegacyToolGuardBlocks() error { tx, err := db.Begin() if err != nil { return err } defer tx.Rollback() rows, err := tx.Query(`SELECT id, tool_name, error, COALESCE(result, '') FROM tool_executions WHERE status = 'failed' AND error LIKE ?`, legacyToolGuardPrefix+"%") if err != nil { return err } type record struct{ id, tool, reason, result string } var records []record for rows.Next() { var r record if err := rows.Scan(&r.id, &r.tool, &r.reason, &r.result); err != nil { rows.Close() return err } if isLegacyToolGuardRefusal(r.reason) { records = append(records, r) } } err = rows.Err() rows.Close() if err != nil { return err } for _, r := range records { var result mcp.ToolResult _ = json.Unmarshal([]byte(r.result), &result) if len(result.Content) == 0 { result.Content = []mcp.Content{{Type: "text", Text: r.reason}} } result.Blocked, result.IsError = true, true encoded, err := json.Marshal(result) if err != nil { return err } if _, err := tx.Exec(`UPDATE tool_executions SET status = 'blocked', result = ? WHERE id = ?`, string(encoded), r.id); err != nil { return err } if _, err := tx.Exec(`UPDATE tool_stats SET failed_calls = MAX(0, failed_calls - 1) WHERE tool_name = ?`, r.tool); err != nil { return err } } return tx.Commit() }