| 1 | package config |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "strings" |
| 6 | |
| 7 | "reasonix/internal/permissionpreset" |
| 8 | ) |
| 9 | |
| 10 | // normalizeLoadedConfig applies post-merge compatibility repairs and validates |
| 11 | // mode enums in their established order. |
| 12 | func normalizeLoadedConfig(cfg *Config) error { |
| 13 | normalizePermissionPresetFields(cfg) |
| 14 | normalizePluginCommandLines(cfg) |
| 15 | normalizeLegacyEffort(cfg) |
| 16 | cfg.ignoredLegacyStepLimits = normalizeLegacyAgentStepLimits(cfg) |
| 17 | normalizeRetiredAutoPlan(cfg) |
| 18 | if err := validateCompletionValidationModes(cfg.Agent.CompletionValidation); err != nil { |
| 19 | return err |
| 20 | } |
| 21 | normalizeLegacyMCPTiers(cfg) |
| 22 | normalizeLegacyStepFunBaseURLs(cfg) |
| 23 | normalizeLegacyLongCatContextWindows(cfg) |
| 24 | normalizeLegacyQwenContextWindows(cfg) |
| 25 | normalizeLegacyKimiK3Catalog(cfg) |
| 26 | normalizeOpenCodeGoRuntimeCompatibility(cfg) |
| 27 | normalizeLegacyOpenCodeGoInstalls(cfg) |
| 28 | normalizeLegacyMimoCustomProviders(cfg) |
| 29 | normalizeLegacyProviderModels(cfg) |
| 30 | normalizeDesktopOfficialProviderAccess(cfg) |
| 31 | // Project reasonix.toml files remain read-only during runtime loading, but |
| 32 | // exact catalog routes can still safely repair a stale protocol in memory. |
| 33 | // The user-global startup path persists the same repair before this load. |
| 34 | repairProviderEndpointContracts(cfg) |
| 35 | normalizeOfficialDeepSeekModels(cfg) |
| 36 | migrateBillingDisplayCurrency(cfg) |
| 37 | freezeProviderBillingCurrencies(cfg) |
| 38 | applyDeepSeekOfficialDefaultPricing(cfg) |
| 39 | backfillDeepSeekOfficialPrices(cfg) |
| 40 | normalizeEffortConfig(cfg) |
| 41 | backfillDeepSeekPro(cfg) |
| 42 | return nil |
| 43 | } |
| 44 | |
| 45 | // normalizePermissionPresetFields is the load-time compatibility boundary for |
| 46 | // user-facing execution presets. Known legacy values are migrated to canonical |
| 47 | // names. Unknown non-empty values fail closed and produce a visible load |
| 48 | // warning rather than silently inheriting a more permissive default. |
| 49 | func normalizePermissionPresetFields(cfg *Config) { |
| 50 | if cfg == nil { |
| 51 | return |
| 52 | } |
| 53 | normalize := func(path string, value *string, emptyDefault bool) { |
| 54 | raw := strings.ToLower(strings.TrimSpace(*value)) |
| 55 | if raw == "" { |
| 56 | if emptyDefault { |
| 57 | *value = string(permissionpreset.WorkspaceWrite) |
| 58 | } |
| 59 | return |
| 60 | } |
| 61 | known := permissionpreset.Valid(raw) |
| 62 | if !known { |
| 63 | switch raw { |
| 64 | case "ask", "auto", "yolo", "readonly", "read_only", "workspace", "workspace_write", "danger_full_access", "full", "full-access", "bypass": |
| 65 | known = true |
| 66 | } |
| 67 | } |
| 68 | *value = string(permissionpreset.Normalize(raw)) |
| 69 | if !known { |
| 70 | cfg.addLoadWarning(fmt.Sprintf("%s has unknown permission preset %q; using read-only", path, raw)) |
| 71 | } |
| 72 | } |
| 73 | normalize("desktop.default_tool_approval_mode", &cfg.Desktop.DefaultToolApprovalMode, true) |
| 74 | normalize("bot.tool_approval_mode", &cfg.Bot.ToolApprovalMode, true) |
| 75 | normalize("bot.qq.tool_approval_mode", &cfg.Bot.QQ.ToolApprovalMode, false) |
| 76 | normalize("bot.dingtalk.tool_approval_mode", &cfg.Bot.Dingtalk.ToolApprovalMode, false) |
| 77 | for i := range cfg.Bot.Connections { |
| 78 | normalize(fmt.Sprintf("bot.connections[%d].tool_approval_mode", i), &cfg.Bot.Connections[i].ToolApprovalMode, false) |
| 79 | } |
| 80 | for i := range cfg.Bot.Routes { |
| 81 | normalize(fmt.Sprintf("bot.routes[%d].tool_approval_mode", i), &cfg.Bot.Routes[i].ToolApprovalMode, false) |
| 82 | } |
| 83 | } |
| 84 |