| 1 | package config |
| 2 | |
| 3 | import ( |
| 4 | "crypto/hmac" |
| 5 | "crypto/rand" |
| 6 | "crypto/sha256" |
| 7 | "encoding/hex" |
| 8 | "encoding/json" |
| 9 | ) |
| 10 | |
| 11 | // Model fingerprints are process-local opaque values. Including credentials in |
| 12 | // a keyed digest detects rotations without publishing a password oracle. |
| 13 | var modelSnapshotKey = func() []byte { |
| 14 | key := make([]byte, 32) |
| 15 | if _, err := rand.Read(key); err != nil { |
| 16 | panic(err) |
| 17 | } |
| 18 | return key |
| 19 | }() |
| 20 | |
| 21 | // LoadModelRuntimeSnapshot reads the config and credentials under their writer |
| 22 | // locks. In particular, boot cannot mix half of two connection transactions. |
| 23 | func LoadModelRuntimeSnapshot(root string, sessionRefs ...string) (*Config, error) { |
| 24 | unlock := LockUserConfigEdits() |
| 25 | defer unlock() |
| 26 | unlockCredentials, err := LockUserCredentialEdits() |
| 27 | if err != nil { |
| 28 | return nil, err |
| 29 | } |
| 30 | defer unlockCredentials() |
| 31 | if err := currentUserConfigEditLockError(); err != nil { |
| 32 | return nil, err |
| 33 | } |
| 34 | c, err := LoadForRootReadOnly(root) |
| 35 | if err != nil { |
| 36 | return nil, err |
| 37 | } |
| 38 | // Restored session aliases may introduce providers absent from the file. |
| 39 | // Expand them while both locks are held, before freezing their credentials. |
| 40 | NormalizeLegacyMimoCustomProvidersForRefs(c, sessionRefs...) |
| 41 | c.FreezeProviderCredentials() |
| 42 | return c, nil |
| 43 | } |
| 44 | |
| 45 | // FreezeProviderCredentials fixes both present and absent credentials for the |
| 46 | // lifetime of a runtime. Lazy child/model resolvers must never reread .env. |
| 47 | func (c *Config) FreezeProviderCredentials() { |
| 48 | for i := range c.Providers { |
| 49 | p := &c.Providers[i] |
| 50 | p.resolvedAPIKey = p.APIKey() |
| 51 | p.credentialsFrozen = true |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | // ModelRuntimeFingerprint describes the model resolver built for a session. |
| 56 | // DefaultModel is deliberately excluded: the session owns its selected model. |
| 57 | // All available providers participate because child/vision/search selection may |
| 58 | // choose one later in the same run. UI-only provider labels do not participate. |
| 59 | func (c *Config) ModelRuntimeFingerprint(model string) string { |
| 60 | type providerSnapshot struct { |
| 61 | Entry ProviderEntry |
| 62 | Key string |
| 63 | } |
| 64 | providers := make([]providerSnapshot, 0, len(c.Providers)) |
| 65 | for _, entry := range c.Providers { |
| 66 | key := entry.APIKey() |
| 67 | entry.DisplayName, entry.PresetID, entry.BalanceURL, entry.ModelsURL = "", "", "", "" |
| 68 | entry.APIKeyEnv = "" // a changed reference with the same resolved key is not a runtime change |
| 69 | entry.PresetVersion = 0 |
| 70 | providers = append(providers, providerSnapshot{entry, key}) |
| 71 | } |
| 72 | data, err := json.Marshal(struct { |
| 73 | Model string |
| 74 | Providers []providerSnapshot |
| 75 | Access []string |
| 76 | Planner, Vision, Search, Guardian, Recovery, Subagent, Effort string |
| 77 | SubagentModels, SubagentEfforts map[string]string |
| 78 | Depth, Concurrency, Writers int |
| 79 | }{model, providers, c.Desktop.ProviderAccess, |
| 80 | c.Agent.PlannerModel, c.Agent.VisionModel, c.Agent.WebSearchModel, |
| 81 | c.Agent.GuardianModel, c.Agent.RecoveryModel, c.Agent.SubagentModel, |
| 82 | c.Agent.SubagentEffort, c.Agent.SubagentModels, c.Agent.SubagentEfforts, |
| 83 | c.Agent.MaxSubagentDepth, c.Agent.MaxSubagentConcurrency, c.Agent.MaxParallelWriters}) |
| 84 | if err != nil { |
| 85 | return "" |
| 86 | } |
| 87 | mac := hmac.New(sha256.New, modelSnapshotKey) |
| 88 | _, _ = mac.Write(data) |
| 89 | return hex.EncodeToString(mac.Sum(nil)) |
| 90 | } |
| 91 |