104 lines
3.7 KiB
Go
104 lines
3.7 KiB
Go
package validator
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"browser.local/platform/domain"
|
|
)
|
|
|
|
const (
|
|
maxAIInvocationPromptSize = 8000
|
|
maxAIInvocationConfigSize = 64 * 1024
|
|
maxAIInvocationContextRefs = 12
|
|
)
|
|
|
|
func ValidateAIInvocationRequest(request domain.AIInvocationRequest) error {
|
|
request = domain.CopyAIInvocationRequest(request)
|
|
var violations []string
|
|
violations = appendRequired(violations, "requestId", request.RequestID)
|
|
violations = appendRequired(violations, "purpose", request.Purpose)
|
|
violations = appendRequired(violations, "prompt", request.Prompt)
|
|
if request.Purpose != "" && !validAIPurpose(request.Purpose) {
|
|
violations = append(violations, "purpose is not allowed")
|
|
}
|
|
if len([]byte(request.Prompt)) > maxAIInvocationPromptSize {
|
|
violations = append(violations, "prompt is too large")
|
|
}
|
|
if len([]byte(request.CurrentConfig)) > maxAIInvocationConfigSize {
|
|
violations = append(violations, "currentConfig is too large")
|
|
}
|
|
if request.ProviderID != "" && !safeIdentifier(request.ProviderID) {
|
|
violations = append(violations, "providerId is invalid")
|
|
}
|
|
if request.Model != "" && unsafeAIString(request.Model) {
|
|
violations = append(violations, "model is unsafe")
|
|
}
|
|
if len(request.ContextRefs) > maxAIInvocationContextRefs {
|
|
violations = append(violations, "contextRefs has too many keys")
|
|
}
|
|
for key, value := range request.ContextRefs {
|
|
if strings.TrimSpace(key) == "" || key != strings.TrimSpace(key) {
|
|
violations = append(violations, "contextRefs key is invalid")
|
|
}
|
|
if !validAIContextRef(value) {
|
|
violations = append(violations, fmt.Sprintf("contextRefs[%s] is invalid", key))
|
|
}
|
|
}
|
|
for _, value := range []fieldString{
|
|
{field: "requestId", value: request.RequestID},
|
|
{field: "prompt", value: request.Prompt},
|
|
{field: "currentConfig", value: request.CurrentConfig},
|
|
{field: "providerId", value: request.ProviderID},
|
|
{field: "model", value: request.Model},
|
|
} {
|
|
if unsafeAIString(value.value) {
|
|
violations = append(violations, value.field+" contains unsafe content")
|
|
}
|
|
}
|
|
return finish(violations)
|
|
}
|
|
|
|
func ValidateAIInvocationResponse(response domain.AIInvocationResponse) error {
|
|
var violations []string
|
|
violations = appendRequired(violations, "requestId", response.RequestID)
|
|
violations = appendRequired(violations, "purpose", response.Purpose)
|
|
violations = appendRequired(violations, "status", response.Status)
|
|
if unsafeAIString(response.Recommendation) {
|
|
violations = append(violations, "recommendation contains unsafe content")
|
|
}
|
|
if response.ConfigRecommendation != nil && unsafeAIString(response.ConfigRecommendation.SuggestedConfig) {
|
|
violations = append(violations, "configRecommendation contains unsafe content")
|
|
}
|
|
if response.Error != nil && unsafeAIString(response.Error.Message) {
|
|
violations = append(violations, "error message contains unsafe content")
|
|
}
|
|
return finish(violations)
|
|
}
|
|
|
|
func validAIContextRef(value string) bool {
|
|
trimmed := strings.TrimSpace(value)
|
|
if trimmed == "" || trimmed != value || unsafeAIString(value) {
|
|
return false
|
|
}
|
|
return strings.HasPrefix(value, "server://") || strings.HasPrefix(value, "log://") || strings.HasPrefix(value, "artifact://") || strings.HasPrefix(value, "input://")
|
|
}
|
|
|
|
func safeIdentifier(value string) bool {
|
|
trimmed := strings.TrimSpace(value)
|
|
if trimmed == "" || trimmed != value || len([]rune(value)) > 160 {
|
|
return false
|
|
}
|
|
for _, char := range value {
|
|
if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') || char == '_' || char == '-' || char == '.' {
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func unsafeAIString(value string) bool {
|
|
return containsUnsafeRuntimeSecret(value) || looksLikeRawHostPath(value) || strings.Contains(strings.ToLower(value), "provider base url secret")
|
|
}
|