feat(compliance): add content compliance detection across tenant, ops, and clients
Implements the Revision 8/9 compliance design: tenant + ops backends, ops-web content-safety pages, admin-web editor/publish gate integration, desktop publish block surfacing, and supporting migrations / shared types / config plumbing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -34,6 +35,7 @@ type Config struct {
|
||||
JWT JWTConfig `mapstructure:"jwt"`
|
||||
Log LogConfig `mapstructure:"log"`
|
||||
LLM LLMConfig `mapstructure:"llm"`
|
||||
Compliance ComplianceConfig `mapstructure:"compliance"`
|
||||
Retrieval RetrievalConfig `mapstructure:"retrieval"`
|
||||
Generation GenerationConfig `mapstructure:"generation"`
|
||||
}
|
||||
@@ -137,6 +139,12 @@ type RabbitMQConfig struct {
|
||||
ImageAssetDLX string `mapstructure:"image_asset_dlx"`
|
||||
ImageAssetDLQ string `mapstructure:"image_asset_dlq"`
|
||||
ImageAssetDLQRouteKey string `mapstructure:"image_asset_dlq_routing_key"`
|
||||
ComplianceReviewExchange string `mapstructure:"compliance_review_exchange"`
|
||||
ComplianceReviewRoutingKey string `mapstructure:"compliance_review_routing_key"`
|
||||
ComplianceReviewQueue string `mapstructure:"compliance_review_queue"`
|
||||
ComplianceReviewDLX string `mapstructure:"compliance_review_dlx"`
|
||||
ComplianceReviewDLQ string `mapstructure:"compliance_review_dlq"`
|
||||
ComplianceReviewDLQRouteKey string `mapstructure:"compliance_review_dlq_routing_key"`
|
||||
ConsumerPrefetch int `mapstructure:"consumer_prefetch"`
|
||||
PublishChannelPoolSize int `mapstructure:"publish_channel_pool_size"`
|
||||
}
|
||||
@@ -282,6 +290,16 @@ type LLMConfig struct {
|
||||
WebSearchLimit int32 `mapstructure:"web_search_limit"`
|
||||
}
|
||||
|
||||
type ComplianceConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
LLMJudgeEnabled bool `mapstructure:"llm_judge_enabled"`
|
||||
SnapshotReloadInterval time.Duration `mapstructure:"snapshot_reload_interval"`
|
||||
PublishGateTimeout time.Duration `mapstructure:"publish_gate_timeout"`
|
||||
ReviewWorkerConcurrency int `mapstructure:"review_worker_concurrency"`
|
||||
ReviewWorkerTimeout time.Duration `mapstructure:"review_worker_timeout"`
|
||||
ReviewMaxAttempts int `mapstructure:"review_max_attempts"`
|
||||
}
|
||||
|
||||
type RetrievalConfig struct {
|
||||
Provider string `mapstructure:"provider"`
|
||||
BaseURL string `mapstructure:"base_url"`
|
||||
@@ -356,6 +374,7 @@ func loadWithFiles(configPath string) (*Config, []string, error) {
|
||||
normalizeMonitoringDispatchConfig(&cfg.MonitoringDispatch)
|
||||
normalizeMembershipConfig(&cfg.Membership)
|
||||
normalizeBrandLibraryConfig(&cfg.BrandLibrary)
|
||||
NormalizeComplianceConfig(&cfg.Compliance)
|
||||
NormalizeGenerationConfig(&cfg.Generation)
|
||||
|
||||
files := []string{configFile}
|
||||
@@ -432,6 +451,10 @@ func applyConfigDefaults(settings map[string]any) {
|
||||
if _, ok := cacheSettings["metrics_enabled"]; !ok {
|
||||
cacheSettings["metrics_enabled"] = true
|
||||
}
|
||||
complianceSettings := ensureMapSetting(settings, "compliance")
|
||||
if _, ok := complianceSettings["enabled"]; !ok {
|
||||
complianceSettings["enabled"] = true
|
||||
}
|
||||
}
|
||||
|
||||
func ensureMapSetting(settings map[string]any, key string) map[string]any {
|
||||
@@ -566,6 +589,27 @@ func NormalizeGenerationConfig(cfg *GenerationConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
func NormalizeComplianceConfig(cfg *ComplianceConfig) {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
if cfg.SnapshotReloadInterval <= 0 {
|
||||
cfg.SnapshotReloadInterval = 30 * time.Second
|
||||
}
|
||||
if cfg.PublishGateTimeout <= 0 {
|
||||
cfg.PublishGateTimeout = 800 * time.Millisecond
|
||||
}
|
||||
if cfg.ReviewWorkerConcurrency <= 0 {
|
||||
cfg.ReviewWorkerConcurrency = 1
|
||||
}
|
||||
if cfg.ReviewWorkerTimeout <= 0 {
|
||||
cfg.ReviewWorkerTimeout = 45 * time.Second
|
||||
}
|
||||
if cfg.ReviewMaxAttempts <= 0 {
|
||||
cfg.ReviewMaxAttempts = 3
|
||||
}
|
||||
}
|
||||
|
||||
func candidateConfigPaths(configPath string, local bool) []string {
|
||||
trimmed := strings.TrimSpace(configPath)
|
||||
if trimmed == "" {
|
||||
@@ -628,6 +672,27 @@ func applyEnvOverrides(cfg *Config) {
|
||||
if model, ok := lookupNonEmptyEnv("LLM_KNOWLEDGE_URL_MODEL"); ok {
|
||||
cfg.LLM.KnowledgeURLModel = model
|
||||
}
|
||||
if enabled, ok := lookupBoolEnv("COMPLIANCE_ENABLED"); ok {
|
||||
cfg.Compliance.Enabled = enabled
|
||||
}
|
||||
if enabled, ok := lookupBoolEnv("COMPLIANCE_LLM_ENABLED"); ok {
|
||||
cfg.Compliance.LLMJudgeEnabled = enabled
|
||||
}
|
||||
if d, ok := lookupDurationEnv("COMPLIANCE_SNAPSHOT_RELOAD_INTERVAL"); ok {
|
||||
cfg.Compliance.SnapshotReloadInterval = d
|
||||
}
|
||||
if d, ok := lookupDurationEnv("COMPLIANCE_PUBLISH_GATE_TIMEOUT"); ok {
|
||||
cfg.Compliance.PublishGateTimeout = d
|
||||
}
|
||||
if n, ok := lookupIntEnv("COMPLIANCE_REVIEW_WORKER_CONCURRENCY"); ok {
|
||||
cfg.Compliance.ReviewWorkerConcurrency = n
|
||||
}
|
||||
if d, ok := lookupDurationEnv("COMPLIANCE_REVIEW_WORKER_TIMEOUT"); ok {
|
||||
cfg.Compliance.ReviewWorkerTimeout = d
|
||||
}
|
||||
if n, ok := lookupIntEnv("COMPLIANCE_REVIEW_MAX_ATTEMPTS"); ok {
|
||||
cfg.Compliance.ReviewMaxAttempts = n
|
||||
}
|
||||
if apiKey, ok := lookupNonEmptyEnv("QDRANT_API_KEY"); ok {
|
||||
cfg.Qdrant.APIKey = apiKey
|
||||
}
|
||||
@@ -832,6 +897,24 @@ func normalizeRabbitMQConfig(cfg *RabbitMQConfig) {
|
||||
if strings.TrimSpace(cfg.ImageAssetDLQRouteKey) == "" {
|
||||
cfg.ImageAssetDLQRouteKey = "image.asset.finalize.dlq"
|
||||
}
|
||||
if strings.TrimSpace(cfg.ComplianceReviewExchange) == "" {
|
||||
cfg.ComplianceReviewExchange = "compliance.review"
|
||||
}
|
||||
if strings.TrimSpace(cfg.ComplianceReviewRoutingKey) == "" {
|
||||
cfg.ComplianceReviewRoutingKey = "compliance.review.run"
|
||||
}
|
||||
if strings.TrimSpace(cfg.ComplianceReviewQueue) == "" {
|
||||
cfg.ComplianceReviewQueue = "compliance.review.run"
|
||||
}
|
||||
if strings.TrimSpace(cfg.ComplianceReviewDLX) == "" {
|
||||
cfg.ComplianceReviewDLX = "compliance.review.dlx"
|
||||
}
|
||||
if strings.TrimSpace(cfg.ComplianceReviewDLQ) == "" {
|
||||
cfg.ComplianceReviewDLQ = "compliance.review.run.dlq"
|
||||
}
|
||||
if strings.TrimSpace(cfg.ComplianceReviewDLQRouteKey) == "" {
|
||||
cfg.ComplianceReviewDLQRouteKey = "compliance.review.run.dlq"
|
||||
}
|
||||
if cfg.PublishChannelPoolSize <= 0 {
|
||||
cfg.PublishChannelPoolSize = 16
|
||||
}
|
||||
@@ -1076,6 +1159,19 @@ func lookupDurationEnv(key string) (time.Duration, bool) {
|
||||
return duration, true
|
||||
}
|
||||
|
||||
func lookupIntEnv(key string) (int, bool) {
|
||||
value, ok := lookupNonEmptyEnv(key)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
number, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return number, true
|
||||
}
|
||||
|
||||
func maxInt(value, fallback int) int {
|
||||
if value < fallback {
|
||||
return fallback
|
||||
|
||||
Reference in New Issue
Block a user