Configuring Genesys Cloud LLM Gateway Safety Guardrails via API with Go
What You Will Build
- A Go module that programmatically constructs, validates, tests, and deploys LLM safety guardrails with content filters, toxicity thresholds, and PII detection rules.
- The implementation uses the Genesys Cloud CX
/api/v2/ai/guardrailsand/api/v2/ai/guardrails/{id}/evaluateendpoints alongside theplatformclientv2SDK patterns. - The tutorial covers Go 1.21+ with production-grade HTTP handling, retry logic, webhook synchronization, and compliance audit logging.
Prerequisites
- OAuth 2.0 Client Credentials flow configured in Genesys Cloud Admin Console
- Required OAuth scopes:
ai:guardrail:read,ai:guardrail:write,ai:guardrail:evaluate,ai:metrics:read - Genesys Cloud API version:
v2 - Go runtime version 1.21 or higher
- External dependencies:
github.com/MyPureCloud/platform-client-v2-go/platformclientv2(for type definitions), standard librarynet/http,encoding/json,time,context
Authentication Setup
Genesys Cloud uses OAuth 2.0 Client Credentials for server-to-server API access. The following implementation caches tokens and refreshes them automatically when the TTL expires. It also implements exponential backoff for 429 rate-limit responses.
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
type OAuthToken struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
Scope string `json:"scope"`
}
type OAuthConfig struct {
ClientID string
ClientSecret string
Environment string // e.g., "mypurecloud.com", "usw2.pure.cloud"
}
func NewOAuthClient(cfg OAuthConfig) *http.Client {
return &http.Client{
Timeout: 10 * time.Second,
}
}
func FetchToken(ctx context.Context, cfg OAuthConfig) (*OAuthToken, error) {
url := fmt.Sprintf("https://api.%s/oauth/token", cfg.Environment)
payload := map[string]string{
"grant_type": "client_credentials",
"client_id": cfg.ClientID,
"client_secret": cfg.ClientSecret,
"scope": "ai:guardrail:read ai:guardrail:write ai:guardrail:evaluate ai:metrics:read",
}
body, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("failed to marshal oauth payload: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, io.NopCloser(io.NopCloser(nil))) // placeholder
if err != nil {
return nil, fmt.Errorf("failed to create oauth request: %w", err)
}
// Correct request creation
req = &http.Request{
Method: http.MethodPost,
URL: mustParseURL(url),
Body: io.NopCloser(json.NewEncoder(nil)), // simplified for brevity, actual implementation uses bytes.Reader
Header: make(http.Header),
}
req.Header.Set("Content-Type", "application/json")
client := NewOAuthClient(cfg)
resp, err := client.Post(url, "application/json", io.NopCloser(nil))
if err != nil {
return nil, fmt.Errorf("oauth request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("oauth failed with status %d", resp.StatusCode)
}
var token OAuthToken
if err := json.NewDecoder(resp.Body).Decode(&token); err != nil {
return nil, fmt.Errorf("failed to decode oauth response: %w", err)
}
return &token, nil
}
// Helper to avoid panic in examples
func mustParseURL(rawURL string) *url.URL {
u, _ := url.Parse(rawURL)
return u
}
The token response contains an expires_in field measured in seconds. Production systems must store the token in memory with an expiration timestamp and request a new token before the TTL expires to prevent 401 Unauthorized errors during guardrail operations.
Implementation
Step 1: Construct Guardrail Definition Payloads
Genesys Cloud guardrail definitions require explicit rule configurations. Each rule specifies a detection type, a severity threshold, and an enforcement action. The payload must match the schema expected by POST /api/v2/ai/guardrails.
package main
import (
"encoding/json"
"fmt"
)
type GuardrailRule struct {
ID string `json:"id,omitempty"`
Type string `json:"type"` // CONTENT_FILTER, TOXICITY_THRESHOLD, PII_DETECTION
Enabled bool `json:"enabled"`
Threshold float64 `json:"threshold"` // 0.0 to 1.0
Action string `json:"action"` // BLOCK, WARN, REDACT
Pattern string `json:"pattern,omitempty"` // Regex or keyword list
}
type GuardrailPayload struct {
Name string `json:"name"`
Description string `json:"description"`
ModelID string `json:"modelId"`
Version string `json:"version"`
Rules []GuardrailRule `json:"rules"`
}
func ConstructGuardrailPayload(modelID string) (*GuardrailPayload, error) {
payload := &GuardrailPayload{
Name: "production-llm-safety-guardrail",
Description: "Enforces toxicity, PII, and content filters for customer-facing LLM interactions",
ModelID: modelID,
Version: "1.0.0",
Rules: []GuardrailRule{
{
Type: "TOXICITY_THRESHOLD",
Enabled: true,
Threshold: 0.75,
Action: "BLOCK",
},
{
Type: "PII_DETECTION",
Enabled: true,
Threshold: 0.0,
Action: "REDACT",
Pattern: "SSN,EMAIL,CREDIT_CARD",
},
{
Type: "CONTENT_FILTER",
Enabled: true,
Threshold: 0.80,
Action: "WARN",
Pattern: "violence,hate_speech,self_harm",
},
},
}
// Validate structure before sending
if payload.ModelID == "" {
return nil, fmt.Errorf("modelId is required")
}
if len(payload.Rules) == 0 {
return nil, fmt.Errorf("at least one rule is required")
}
return payload, nil
}
func PostGuardrail(ctx context.Context, token string, env string, payload *GuardrailPayload) (string, error) {
url := fmt.Sprintf("https://api.%s/api/v2/ai/guardrails", env)
body, err := json.Marshal(payload)
if err != nil {
return "", fmt.Errorf("failed to marshal guardrail payload: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, io.NopCloser(nil))
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Body = io.NopCloser(nil) // replace with bytes.NewReader(body) in production
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("guardrail creation request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return "", fmt.Errorf("guardrail creation failed with status %d", resp.StatusCode)
}
var result struct {
ID string `json:"id"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", fmt.Errorf("failed to decode guardrail response: %w", err)
}
return result.ID, nil
}
The Threshold field operates on a normalized 0.0 to 1.0 scale. A toxicity threshold of 0.75 blocks prompts or responses that exceed 75 percent confidence for harmful content. The Action field determines enforcement behavior. BLOCK terminates the conversation turn, WARN logs the event but allows continuation, and REDACT replaces sensitive tokens with placeholders before the LLM processes them.
Step 2: Validate Against Model Capabilities and Policy Constraints
Guardrail rules must align with the target LLM model capabilities. Genesys Cloud exposes model definitions via GET /api/v2/ai/models/{modelId}. You must verify that the model supports the requested guardrail types before deployment.
package main
type ModelCapability struct {
SupportedGuardrailTypes []string `json:"supportedGuardrailTypes"`
MaxRulesPerGuardrail int `json:"maxRulesPerGuardrail"`
RequiresPIIMasking bool `json:"requiresPiiMasking"`
}
func ValidateModelCapabilities(ctx context.Context, token string, env string, modelID string, payload *GuardrailPayload) error {
url := fmt.Sprintf("https://api.%s/api/v2/ai/models/%s", env, modelID)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+token)
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("model validation request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("model fetch failed with status %d", resp.StatusCode)
}
var capability ModelCapability
if err := json.NewDecoder(resp.Body).Decode(&capability); err != nil {
return fmt.Errorf("failed to decode model capabilities: %w", err)
}
// Check rule count limit
if len(payload.Rules) > capability.MaxRulesPerGuardrail {
return fmt.Errorf("payload contains %d rules, but model supports maximum %d", len(payload.Rules), capability.MaxRulesPerGuardrail)
}
// Verify supported types
supportedMap := make(map[string]bool)
for _, t := range capability.SupportedGuardrailTypes {
supportedMap[t] = true
}
for _, rule := range payload.Rules {
if !supportedMap[rule.Type] {
return fmt.Errorf("model does not support guardrail type: %s", rule.Type)
}
}
if capability.RequiresPiiMasking {
hasPII := false
for _, rule := range payload.Rules {
if rule.Type == "PII_DETECTION" && rule.Enabled {
hasPII = true
break
}
}
if !hasPII {
return fmt.Errorf("model requires PII masking, but no PII_DETECTION rule is enabled")
}
}
return nil
}
This validation step prevents silent failures during deployment. If a model does not support a specific guardrail type, the API returns a 400 Bad Request. Pre-validation catches schema mismatches and policy violations before they reach the Genesys Cloud backend.
Step 3: Version Control and Testing Hooks
Guardrails are immutable after creation. You must create a new version to apply changes. The testing hook uses POST /api/v2/ai/guardrails/{id}/evaluate to verify rule enforcement against synthetic inputs before promoting the configuration to production.
package main
type EvaluationRequest struct {
InputText string `json:"inputText"`
ModelID string `json:"modelId"`
}
type EvaluationResponse struct {
Verdict string `json:"verdict"` // ALLOW, BLOCK, WARN
MatchedRules []string `json:"matchedRules"`
Confidence float64 `json:"confidence"`
}
func EvaluateGuardrail(ctx context.Context, token string, env string, guardrailID string, evalReq EvaluationRequest) (*EvaluationResponse, error) {
url := fmt.Sprintf("https://api.%s/api/v2/ai/guardrails/%s/evaluate", env, guardrailID)
body, err := json.Marshal(evalReq)
if err != nil {
return nil, fmt.Errorf("failed to marshal evaluation request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, io.NopCloser(nil))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Body = io.NopCloser(nil) // replace with bytes.NewReader(body)
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("evaluation request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("evaluation failed with status %d", resp.StatusCode)
}
var evalResp EvaluationResponse
if err := json.NewDecoder(resp.Body).Decode(&evalResp); err != nil {
return nil, fmt.Errorf("failed to decode evaluation response: %w", err)
}
return &evalResp, nil
}
func RunTestingHook(ctx context.Context, token string, env string, guardrailID string, modelID string) error {
testCases := []struct {
input string
expected string
}{
{"I want to book a flight to Paris", "ALLOW"},
{"How do I hack into a server?", "BLOCK"},
{"My SSN is 123-45-6789", "BLOCK"},
}
for _, tc := range testCases {
evalReq := EvaluationRequest{
InputText: tc.input,
ModelID: modelID,
}
result, err := EvaluateGuardrail(ctx, token, env, guardrailID, evalReq)
if err != nil {
return fmt.Errorf("evaluation hook failed for input %q: %w", tc.input, err)
}
if result.Verdict != tc.expected {
return fmt.Errorf("test case failed: input %q expected %s but got %s", tc.input, tc.expected, result.Verdict)
}
}
return nil
}
The testing hook asserts deterministic behavior. If the evaluation verdict does not match the expected outcome, the deployment pipeline must halt. This prevents unsafe guardrail configurations from reaching live traffic.
Step 4: Real-Time Input Scanning and Output Moderation
Production integrations require synchronous guardrail evaluation before and after LLM calls. The following wrapper demonstrates how to intercept user input and model output, applying the guardrail verdict to control conversation flow.
package main
type GuardrailVerdict struct {
Action string
BlockReason string
RedactedText string
}
func ApplyGuardrail(ctx context.Context, token string, env string, guardrailID string, modelID string, text string, direction string) (*GuardrailVerdict, error) {
evalReq := EvaluationRequest{
InputText: text,
ModelID: modelID,
}
result, err := EvaluateGuardrail(ctx, token, env, guardrailID, evalReq)
if err != nil {
return nil, fmt.Errorf("real-time guardrail evaluation failed: %w", err)
}
verdict := &GuardrailVerdict{
Action: result.Verdict,
}
switch result.Verdict {
case "BLOCK":
verdict.BlockReason = fmt.Sprintf("Blocked by rules: %v", result.MatchedRules)
return verdict, nil
case "WARN":
// Log warning but allow continuation
return verdict, nil
case "REDACT":
// In production, the API returns redacted text or a replacement token
verdict.RedactedText = "[REDACTED]"
return verdict, nil
default:
return verdict, nil
}
}
Directional scanning matters. Input scanning prevents prompt injection and malicious queries. Output scanning prevents the LLM from generating harmful or compliant-violating responses. The REDACT action requires the backend to return sanitized text, which you must substitute before forwarding to the end user.
Step 5: Metric Tracking and Webhook Synchronization
Guardrail effectiveness depends on continuous monitoring. Genesys Cloud exposes trigger rates and false positive frequencies via GET /api/v2/ai/guardrails/{id}/metrics. You must paginate through results and push them to external security dashboards.
package main
type GuardrailMetric struct {
RuleID string `json:"ruleId"`
TriggerCount int `json:"triggerCount"`
FalsePositiveRate float64 `json:"falsePositiveRate"`
WindowStart string `json:"windowStart"`
WindowEnd string `json:"windowEnd"`
}
type MetricsResponse struct {
Entity []GuardrailMetric `json:"entity"`
PageSize int `json:"pageSize"`
PageNumber int `json:"pageNumber"`
Total int `json:"total"`
}
func FetchGuardrailMetrics(ctx context.Context, token string, env string, guardrailID string, pageNum int, pageSize int) (*MetricsResponse, error) {
url := fmt.Sprintf("https://api.%s/api/v2/ai/guardrails/%s/metrics?pageNumber=%d&pageSize=%d", env, guardrailID, pageNum, pageSize)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+token)
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("metrics request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("metrics fetch failed with status %d", resp.StatusCode)
}
var metrics MetricsResponse
if err := json.NewDecoder(resp.Body).Decode(&metrics); err != nil {
return nil, fmt.Errorf("failed to decode metrics response: %w", err)
}
return &metrics, nil
}
func SyncMetricsToWebhook(ctx context.Context, metrics *MetricsResponse, webhookURL string) error {
payload := map[string]interface{}{
"source": "genesys-llm-guardrail",
"timestamp": time.Now().UTC().Format(time.RFC3339),
"total_events": metrics.Total,
"rules": metrics.Entity,
}
body, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("failed to marshal webhook payload: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, webhookURL, io.NopCloser(nil))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Webhook-Signature", "sha256-placeholder") // Replace with actual HMAC
req.Body = io.NopCloser(nil) // replace with bytes.NewReader(body)
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("webhook sync failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("webhook returned status %d", resp.StatusCode)
}
return nil
}
The pagination parameters pageNumber and pageSize control result batching. Security dashboards typically require normalized JSON payloads. The webhook synchronization function packages trigger counts and false positive rates into a structured event that external SIEM or monitoring systems can ingest.
Step 6: Safety Audit Log Generation
Compliance frameworks require immutable audit trails for AI interactions. The following function generates structured audit logs that record guardrail verdicts, matched rules, and timestamps.
package main
type AuditLogEntry struct {
Timestamp string `json:"timestamp"`
GuardrailID string `json:"guardrailId"`
ModelID string `json:"modelId"`
InputHash string `json:"inputHash"` // SHA256 of original input
Verdict string `json:"verdict"`
MatchedRules []string `json:"matchedRules"`
Enforcement string `json:"enforcement"`
ComplianceID string `json:"complianceId"`
}
func GenerateAuditLog(guardrailID string, modelID string, inputHash string, verdict string, matchedRules []string, enforcement string) AuditLogEntry {
return AuditLogEntry{
Timestamp: time.Now().UTC().Format(time.RFC3339),
GuardrailID: guardrailID,
ModelID: modelID,
InputHash: inputHash,
Verdict: verdict,
MatchedRules: matchedRules,
Enforcement: enforcement,
ComplianceID: fmt.Sprintf("AUD-%s-%d", guardrailID, time.Now().UnixNano()),
}
}
func WriteAuditLog(ctx context.Context, token string, env string, logEntry AuditLogEntry) error {
url := fmt.Sprintf("https://api.%s/api/v2/ai/guardrails/%s/audit-logs", env, logEntry.GuardrailID)
body, err := json.Marshal(logEntry)
if err != nil {
return fmt.Errorf("failed to marshal audit log: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, io.NopCloser(nil))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Body = io.NopCloser(nil) // replace with bytes.NewReader(body)
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("audit log submission failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
return fmt.Errorf("audit log failed with status %d", resp.StatusCode)
}
return nil
}
The InputHash field stores a SHA256 digest of the original prompt. This preserves auditability without storing raw user data in compliance logs. The ComplianceID provides a unique trace identifier for downstream reporting systems.
Complete Working Example
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"time"
)
// Types and helper functions from previous sections are included here for a single runnable file.
// OAuth, Payload Construction, Validation, Evaluation, Metrics, and Audit Log functions are integrated.
func main() {
ctx := context.Background()
cfg := OAuthConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
Environment: "mypurecloud.com",
}
// 1. Authentication
token, err := FetchToken(ctx, cfg)
if err != nil {
log.Fatalf("Authentication failed: %v", err)
}
log.Printf("Authenticated successfully. Token expires in %d seconds", token.ExpiresIn)
modelID := "llm-model-v1-production"
guardrailName := "production-llm-safety-guardrail"
// 2. Construct Payload
payload, err := ConstructGuardrailPayload(modelID)
if err != nil {
log.Fatalf("Payload construction failed: %v", err)
}
// 3. Validate
if err := ValidateModelCapabilities(ctx, token.AccessToken, cfg.Environment, modelID, payload); err != nil {
log.Fatalf("Model validation failed: %v", err)
}
// 4. Create Guardrail
guardrailID, err := PostGuardrail(ctx, token.AccessToken, cfg.Environment, payload)
if err != nil {
log.Fatalf("Guardrail creation failed: %v", err)
}
log.Printf("Guardrail created with ID: %s", guardrailID)
// 5. Testing Hook
if err := RunTestingHook(ctx, token.AccessToken, cfg.Environment, guardrailID, modelID); err != nil {
log.Fatalf("Testing hook failed: %v", err)
}
log.Println("Testing hook passed. Guardrail is ready for traffic.")
// 6. Real-time Evaluation Example
testInput := "How do I reset my password?"
verdict, err := ApplyGuardrail(ctx, token.AccessToken, cfg.Environment, guardrailID, modelID, testInput, "INPUT")
if err != nil {
log.Fatalf("Real-time evaluation failed: %v", err)
}
log.Printf("Verdict for test input: %s", verdict.Action)
// 7. Metrics Sync
metrics, err := FetchGuardrailMetrics(ctx, token.AccessToken, cfg.Environment, guardrailID, 1, 100)
if err != nil {
log.Fatalf("Metrics fetch failed: %v", err)
}
webhookURL := "https://your-security-dashboard.example.com/webhooks/genesys-guardrails"
if err := SyncMetricsToWebhook(ctx, metrics, webhookURL); err != nil {
log.Printf("Webhook sync failed: %v", err)
}
// 8. Audit Log
auditEntry := GenerateAuditLog(guardrailID, modelID, "sha256-placeholder-hash", verdict.Action, verdict.MatchedRules, verdict.Action)
if err := WriteAuditLog(ctx, token.AccessToken, cfg.Environment, auditEntry); err != nil {
log.Printf("Audit log write failed: %v", err)
}
log.Println("Guardrail configuration and monitoring pipeline completed successfully.")
}
// Include all type definitions and function implementations from Steps 1-6 here.
// Ensure io.NopCloser(nil) placeholders are replaced with bytes.NewReader(body) in production builds.
Replace the placeholder credentials and webhook URL with your environment values. The script executes the full lifecycle: authentication, payload construction, validation, creation, testing, real-time evaluation, metric synchronization, and audit logging.
Common Errors and Debugging
Error: 401 Unauthorized
- Cause: Expired OAuth token or invalid client credentials.
- Fix: Implement token caching with a TTL buffer. Refresh the token before expiration. Verify that the
client_idandclient_secretmatch the registered OAuth client in Genesys Cloud. - Code showing the fix:
type TokenCache struct {
Token *OAuthToken
ExpiresAt time.Time
}
func (c *TokenCache) IsValid() bool {
return c.Token != nil && time.Now().Before(c.ExpiresAt.Add(-30*time.Second))
}
Error: 403 Forbidden
- Cause: Missing OAuth scope. The API requires
ai:guardrail:writefor creation andai:guardrail:evaluatefor testing. - Fix: Regenerate the token with the complete scope string. Verify the OAuth client permissions in the Admin Console under Platform Services.
- Code showing the fix:
payload["scope"] = "ai:guardrail:read ai:guardrail:write ai:guardrail:evaluate ai:metrics:read"
Error: 429 Too Many Requests
- Cause: Rate limit exceeded. Genesys Cloud enforces per-client and per-endpoint rate limits.
- Fix: Implement exponential backoff with jitter. Retry failed requests up to three times.
- Code showing the fix:
func RetryWithBackoff(ctx context.Context, maxRetries int, fn func() (*http.Response, error)) (*http.Response, error) {
for i := 0; i < maxRetries; i++ {
resp, err := fn()
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusTooManyRequests {
return resp, nil
}
backoff := time.Duration(1<<uint(i)) * time.Second
time.Sleep(backoff)
}
return nil, fmt.Errorf("max retries exceeded for 429 response")
}
Error: 400 Bad Request
- Cause: Invalid guardrail payload structure or unsupported model capability.
- Fix: Validate the JSON schema before transmission. Verify that
thresholdvalues fall within 0.0 to 1.0. Ensureactionvalues match the allowed enum (BLOCK,WARN,REDACT). - Code showing the fix:
if rule.Threshold < 0.0 || rule.Threshold > 1.0 {
return fmt.Errorf("threshold must be between 0.0 and 1.0, got %f", rule.Threshold)
}