Files
LocalAI/core/config/runtime_settings_registry.go
LocalAI [bot] c1a891662c refactor(settings): single declarative registry for runtime settings (fixes the #10845 bug class) (#10864)
* feat(settings): add declarative runtime-settings field registry

One fieldSpec row per RuntimeSettings field, with a reflection
completeness spec so a field added without a registry row is a red
test instead of a silently-dropped setting (the #10845 bug class).

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-fable-5

* refactor(settings): drive ToRuntimeSettings/ApplyRuntimeSettings from the field registry

Behavior-preserving: ~350 hand-written per-field lines become two loops
over runtimeSettingsFields, gated by a To->Apply->To round-trip spec.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-fable-5

* feat(settings): baseline-driven startup merge for persisted runtime settings

ApplyRuntimeSettingsAtStartup compares the live config against
DefaultRuntimeBaseline (option-less-run defaults incl. kong-injected
flag defaults) instead of per-field == 0 guards. Fixes persisted
lru_eviction_max_retries, tracing_max_items, agent_job_retention_days,
memory_reclaimer_threshold, galleries and autoload flags being
silently ignored at boot.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-fable-5

* fix(settings): registry-driven startup merge, applied before consumers

loadRuntimeSettingsFromFile becomes a thin wrapper over
ApplyRuntimeSettingsAtStartup and runs at the top of New(), before
model configs capture app-level defaults. WithThreads stops eagerly
resolving 0 so a persisted thread count survives restart while
LOCALAI_THREADS still wins (#10845); the physical-core fallback moves
after the merge.

Also: run.go now injects the memory-reclaimer threshold unconditionally
so the option-less boot matches DefaultRuntimeBaseline and a UI-saved
threshold survives restart.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-fable-5

* refactor(settings): file watcher delegates to the registry merge; shared API-key merge

Manual edits to runtime_settings.json now behave like a boot-time load
(env still wins) instead of the inverted diverged-from-startup guard
that ignored most manual edits. MergeAPIKeys dedups env keys in one
place for the endpoint and the watcher.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-fable-5

* docs(settings): document unified runtime-settings precedence

Document the single env/CLI > runtime_settings.json > defaults rule,
applied identically at boot, on POST /api/settings, and on manual file
edits, plus the two known limitations (default-valued env vars are
indistinguishable from unset; API-changed fields hot-apply on the next
restart only). Also add a completion debug log when the watcher applies
runtime_settings.json.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-fable-5

* test(settings): reset the global VRAM cap leaked by the round-trip spec

The round-trip spec applies vram_budget=12GiB, whose post-loop hook
installs a process-global default cap; without a reset every spec
ordered after it runs under that phantom budget. Also drop a stale
enumeration in the ApplyRuntimeSettings doc comment.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude Code:claude-fable-5

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-07-16 22:39:59 +02:00

469 lines
21 KiB
Go

package config
import (
"slices"
"time"
"github.com/mudler/LocalAI/pkg/model"
"github.com/mudler/xlog"
)
// fieldSpec is one runtime setting: how to snapshot it out of
// ApplicationConfig, how to apply it back, and how to decide at load time
// whether env/CLI already claimed the value. Every RuntimeSettings field is
// owned by exactly one spec (the registry completeness spec pins this), so
// GET /api/settings, POST /api/settings, boot, and the file watcher can
// never drift apart per-field again (the bug class behind #10845).
type fieldSpec struct {
// jsonNames this spec owns; a single entry except for composite rows
// (max_active_backends + its deprecated alias single_backend).
jsonNames []string
// requiresRestart marks fields whose live change makes
// ApplyRuntimeSettings report a restart to the settings endpoint.
requiresRestart bool
// fileAuthoritative: at startup/watch time the persisted value applies
// unconditionally - either no env/CLI source exists (branding) or the
// file is defined as the owner (enable_backend_logging,
// tracing_max_body_bytes).
fileAuthoritative bool
// snapshotOnly: echoed by ToRuntimeSettings but never applied by the
// loops (api_keys: the endpoint and file watcher own the env merge).
snapshotOnly bool
snapshot func(o *ApplicationConfig, s *RuntimeSettings)
isSet func(s *RuntimeSettings) bool
// apply writes the (non-nil) settings value into o and reports whether
// it was accepted (validation may reject it, e.g. a bad duration).
apply func(o *ApplicationConfig, s *RuntimeSettings) bool
// envSet reports whether env/CLI already set this field: the live value
// differs from the option-less-run baseline (DefaultRuntimeBaseline).
envSet func(current, baseline *ApplicationConfig) bool
}
type fieldOpt func(*fieldSpec)
func restartRequired() fieldOpt { return func(f *fieldSpec) { f.requiresRestart = true } }
func fromFileAlways() fieldOpt { return func(f *fieldSpec) { f.fileAuthoritative = true } }
func field[T comparable](name string, sel func(*RuntimeSettings) **T, get func(*ApplicationConfig) T, set func(*ApplicationConfig, T), opts ...fieldOpt) fieldSpec {
return fieldEq(name, sel, get, set, func(a, b T) bool { return a == b }, opts...)
}
func fieldEq[T any](name string, sel func(*RuntimeSettings) **T, get func(*ApplicationConfig) T, set func(*ApplicationConfig, T), eq func(a, b T) bool, opts ...fieldOpt) fieldSpec {
f := fieldSpec{
jsonNames: []string{name},
snapshot: func(o *ApplicationConfig, s *RuntimeSettings) { v := get(o); *sel(s) = &v },
isSet: func(s *RuntimeSettings) bool { return *sel(s) != nil },
apply: func(o *ApplicationConfig, s *RuntimeSettings) bool { set(o, **sel(s)); return true },
envSet: func(cur, base *ApplicationConfig) bool { return !eq(get(cur), get(base)) },
}
for _, opt := range opts {
opt(&f)
}
return f
}
// durationField maps a time.Duration config member to a duration-string
// settings field. fallback is the presentation default ToRuntimeSettings
// reports while the member is unset (0). envSet compares the raw durations
// rather than the formatted strings so the fallback text cannot mask an
// env-set value that happens to equal it.
func durationField(name string, sel func(*RuntimeSettings) **string, dur func(*ApplicationConfig) *time.Duration, fallback string, opts ...fieldOpt) fieldSpec {
f := fieldSpec{
jsonNames: []string{name},
snapshot: func(o *ApplicationConfig, s *RuntimeSettings) {
v := fallback
if d := *dur(o); d > 0 {
v = d.String()
}
*sel(s) = &v
},
isSet: func(s *RuntimeSettings) bool { return *sel(s) != nil },
apply: func(o *ApplicationConfig, s *RuntimeSettings) bool {
d, err := time.ParseDuration(**sel(s))
if err != nil {
xlog.Warn("invalid duration in runtime settings", "field", name, "value", **sel(s), "error", err)
return false
}
*dur(o) = d
return true
},
envSet: func(cur, base *ApplicationConfig) bool { return *dur(cur) != *dur(base) },
}
for _, opt := range opts {
opt(&f)
}
return f
}
// runtimeSettingsFields is THE per-field description of every runtime
// setting. ToRuntimeSettings, ApplyRuntimeSettings, and
// ApplyRuntimeSettingsAtStartup are loops over this table; adding a setting
// means adding a RuntimeSettings struct field plus one row here (the
// registry completeness spec fails until both exist).
var runtimeSettingsFields = []fieldSpec{
// Watchdog. The live master flag is post-processed by the apply loops
// (WatchDog follows idle||busy; see ApplyRuntimeSettings).
field("watchdog_enabled",
func(s *RuntimeSettings) **bool { return &s.WatchdogEnabled },
func(o *ApplicationConfig) bool { return o.WatchDog },
func(o *ApplicationConfig, v bool) { o.WatchDog = v },
restartRequired()),
field("watchdog_idle_enabled",
func(s *RuntimeSettings) **bool { return &s.WatchdogIdleEnabled },
func(o *ApplicationConfig) bool { return o.WatchDogIdle },
func(o *ApplicationConfig, v bool) { o.WatchDogIdle = v },
restartRequired()),
field("watchdog_busy_enabled",
func(s *RuntimeSettings) **bool { return &s.WatchdogBusyEnabled },
func(o *ApplicationConfig) bool { return o.WatchDogBusy },
func(o *ApplicationConfig, v bool) { o.WatchDogBusy = v },
restartRequired()),
durationField("watchdog_idle_timeout",
func(s *RuntimeSettings) **string { return &s.WatchdogIdleTimeout },
func(o *ApplicationConfig) *time.Duration { return &o.WatchDogIdleTimeout },
"15m", restartRequired()),
durationField("watchdog_busy_timeout",
func(s *RuntimeSettings) **string { return &s.WatchdogBusyTimeout },
func(o *ApplicationConfig) *time.Duration { return &o.WatchDogBusyTimeout },
"5m", restartRequired()),
durationField("watchdog_interval",
func(s *RuntimeSettings) **string { return &s.WatchdogInterval },
func(o *ApplicationConfig) *time.Duration { return &o.WatchDogInterval },
model.DefaultWatchdogInterval.String(), restartRequired()),
// Backend management. max_active_backends and its deprecated alias
// single_backend are one composite row: they must stay mutually
// consistent and max_active_backends wins when both are posted.
{
jsonNames: []string{"max_active_backends", "single_backend"},
requiresRestart: true,
snapshot: func(o *ApplicationConfig, s *RuntimeSettings) {
mab := o.MaxActiveBackends
sb := o.SingleBackend
s.MaxActiveBackends = &mab
s.SingleBackend = &sb
},
isSet: func(s *RuntimeSettings) bool { return s.MaxActiveBackends != nil || s.SingleBackend != nil },
apply: func(o *ApplicationConfig, s *RuntimeSettings) bool {
if s.MaxActiveBackends != nil {
o.MaxActiveBackends = *s.MaxActiveBackends
o.SingleBackend = *s.MaxActiveBackends == 1
return true
}
o.SingleBackend = *s.SingleBackend
if *s.SingleBackend {
o.MaxActiveBackends = 1
} else {
o.MaxActiveBackends = 0
}
return true
},
envSet: func(cur, base *ApplicationConfig) bool {
return cur.MaxActiveBackends != base.MaxActiveBackends || cur.SingleBackend != base.SingleBackend
},
},
field("auto_upgrade_backends",
func(s *RuntimeSettings) **bool { return &s.AutoUpgradeBackends },
func(o *ApplicationConfig) bool { return o.AutoUpgradeBackends },
func(o *ApplicationConfig, v bool) { o.AutoUpgradeBackends = v }),
field("prefer_development_backends",
func(s *RuntimeSettings) **bool { return &s.PreferDevelopmentBackends },
func(o *ApplicationConfig) bool { return o.PreferDevelopmentBackends },
func(o *ApplicationConfig, v bool) { o.PreferDevelopmentBackends = v }),
// Memory reclaimer. Enabling it forces the watchdog master flag - that
// cross-field invariant lives in the apply loops, not here.
field("memory_reclaimer_enabled",
func(s *RuntimeSettings) **bool { return &s.MemoryReclaimerEnabled },
func(o *ApplicationConfig) bool { return o.MemoryReclaimerEnabled },
func(o *ApplicationConfig, v bool) { o.MemoryReclaimerEnabled = v },
restartRequired()),
{
jsonNames: []string{"memory_reclaimer_threshold"},
requiresRestart: true,
snapshot: func(o *ApplicationConfig, s *RuntimeSettings) {
v := o.MemoryReclaimerThreshold
s.MemoryReclaimerThreshold = &v
},
isSet: func(s *RuntimeSettings) bool { return s.MemoryReclaimerThreshold != nil },
apply: func(o *ApplicationConfig, s *RuntimeSettings) bool {
v := *s.MemoryReclaimerThreshold
if v <= 0 || v > 1.0 {
xlog.Warn("memory_reclaimer_threshold out of range (0,1], ignoring", "value", v)
return false
}
o.MemoryReclaimerThreshold = v
return true
},
envSet: func(cur, base *ApplicationConfig) bool {
return cur.MemoryReclaimerThreshold != base.MemoryReclaimerThreshold
},
},
// Eviction.
field("force_eviction_when_busy",
func(s *RuntimeSettings) **bool { return &s.ForceEvictionWhenBusy },
func(o *ApplicationConfig) bool { return o.ForceEvictionWhenBusy },
func(o *ApplicationConfig, v bool) { o.ForceEvictionWhenBusy = v }),
field("size_aware_eviction",
func(s *RuntimeSettings) **bool { return &s.SizeAwareEviction },
func(o *ApplicationConfig) bool { return o.SizeAwareEviction },
func(o *ApplicationConfig, v bool) { o.SizeAwareEviction = v }),
field("lru_eviction_max_retries",
func(s *RuntimeSettings) **int { return &s.LRUEvictionMaxRetries },
func(o *ApplicationConfig) int { return o.LRUEvictionMaxRetries },
func(o *ApplicationConfig, v int) { o.LRUEvictionMaxRetries = v }),
durationField("lru_eviction_retry_interval",
func(s *RuntimeSettings) **string { return &s.LRUEvictionRetryInterval },
func(o *ApplicationConfig) *time.Duration { return &o.LRUEvictionRetryInterval },
"1s"),
// Performance.
field("threads",
func(s *RuntimeSettings) **int { return &s.Threads },
func(o *ApplicationConfig) int { return o.Threads },
func(o *ApplicationConfig, v int) { o.Threads = v }),
field("context_size",
func(s *RuntimeSettings) **int { return &s.ContextSize },
func(o *ApplicationConfig) int { return o.ContextSize },
func(o *ApplicationConfig, v int) { o.ContextSize = v }),
// VRAM budget: the cap string ("80%"/"12GB"/"" = uncapped). The live
// side effect (xsysinfo.SetDefaultVRAMBudget) is post-processing in the
// apply loop, not here - the row only owns the config member, matching
// how the memory-reclaimer/watchdog cross-field effects are handled.
field("vram_budget",
func(s *RuntimeSettings) **string { return &s.VRAMBudget },
func(o *ApplicationConfig) string { return o.VRAMBudget },
func(o *ApplicationConfig, v string) { o.VRAMBudget = v }),
field("f16",
func(s *RuntimeSettings) **bool { return &s.F16 },
func(o *ApplicationConfig) bool { return o.F16 },
func(o *ApplicationConfig, v bool) { o.F16 = v }),
field("debug",
func(s *RuntimeSettings) **bool { return &s.Debug },
func(o *ApplicationConfig) bool { return o.Debug },
func(o *ApplicationConfig, v bool) { o.Debug = v }),
field("enable_tracing",
func(s *RuntimeSettings) **bool { return &s.EnableTracing },
func(o *ApplicationConfig) bool { return o.EnableTracing },
func(o *ApplicationConfig, v bool) { o.EnableTracing = v }),
field("tracing_max_items",
func(s *RuntimeSettings) **int { return &s.TracingMaxItems },
func(o *ApplicationConfig) int { return o.TracingMaxItems },
func(o *ApplicationConfig, v int) { o.TracingMaxItems = v }),
// The on-disk setting overrides the CLI/env default by design: 0 in the
// file means "uncapped" and must not be mistaken for "unset".
field("tracing_max_body_bytes",
func(s *RuntimeSettings) **int { return &s.TracingMaxBodyBytes },
func(o *ApplicationConfig) int { return o.TracingMaxBodyBytes },
func(o *ApplicationConfig, v int) { o.TracingMaxBodyBytes = v },
fromFileAlways()),
// Backend logging defaults on in single mode; a persisted false is the
// UI toggle-off and must win over that default on restart. No env/CLI
// source exists, so the file is authoritative.
field("enable_backend_logging",
func(s *RuntimeSettings) **bool { return &s.EnableBackendLogging },
func(o *ApplicationConfig) bool { return o.EnableBackendLogging },
func(o *ApplicationConfig, v bool) { o.EnableBackendLogging = v },
fromFileAlways()),
// Security / CORS. The "csrf" wire field carries DisableCSRF (historic
// naming kept for API compatibility).
field("cors",
func(s *RuntimeSettings) **bool { return &s.CORS },
func(o *ApplicationConfig) bool { return o.CORS },
func(o *ApplicationConfig, v bool) { o.CORS = v }),
field("csrf",
func(s *RuntimeSettings) **bool { return &s.CSRF },
func(o *ApplicationConfig) bool { return o.DisableCSRF },
func(o *ApplicationConfig, v bool) { o.DisableCSRF = v }),
field("cors_allow_origins",
func(s *RuntimeSettings) **string { return &s.CORSAllowOrigins },
func(o *ApplicationConfig) string { return o.CORSAllowOrigins },
func(o *ApplicationConfig, v string) { o.CORSAllowOrigins = v }),
// P2P.
field("p2p_token",
func(s *RuntimeSettings) **string { return &s.P2PToken },
func(o *ApplicationConfig) string { return o.P2PToken },
func(o *ApplicationConfig, v string) { o.P2PToken = v }),
field("p2p_network_id",
func(s *RuntimeSettings) **string { return &s.P2PNetworkID },
func(o *ApplicationConfig) string { return o.P2PNetworkID },
func(o *ApplicationConfig, v string) { o.P2PNetworkID = v }),
field("federated",
func(s *RuntimeSettings) **bool { return &s.Federated },
func(o *ApplicationConfig) bool { return o.Federated },
func(o *ApplicationConfig, v bool) { o.Federated = v }),
// Galleries. Gallery is comparable (string fields + a pointer), so
// slices.Equal gives element-wise comparison against the baseline's
// default gallery list.
fieldEq("galleries",
func(s *RuntimeSettings) **[]Gallery { return &s.Galleries },
func(o *ApplicationConfig) []Gallery { return o.Galleries },
func(o *ApplicationConfig, v []Gallery) { o.Galleries = v },
slices.Equal),
fieldEq("backend_galleries",
func(s *RuntimeSettings) **[]Gallery { return &s.BackendGalleries },
func(o *ApplicationConfig) []Gallery { return o.BackendGalleries },
func(o *ApplicationConfig, v []Gallery) { o.BackendGalleries = v },
slices.Equal),
field("autoload_galleries",
func(s *RuntimeSettings) **bool { return &s.AutoloadGalleries },
func(o *ApplicationConfig) bool { return o.AutoloadGalleries },
func(o *ApplicationConfig, v bool) { o.AutoloadGalleries = v }),
field("autoload_backend_galleries",
func(s *RuntimeSettings) **bool { return &s.AutoloadBackendGalleries },
func(o *ApplicationConfig) bool { return o.AutoloadBackendGalleries },
func(o *ApplicationConfig, v bool) { o.AutoloadBackendGalleries = v }),
// API keys: echoed for the UI, but the apply loops never touch them.
// The settings endpoint and the file watcher own the env+runtime merge
// (MergeAPIKeys) because env keys must always survive.
{
jsonNames: []string{"api_keys"},
snapshotOnly: true,
snapshot: func(o *ApplicationConfig, s *RuntimeSettings) {
keys := o.ApiKeys
s.ApiKeys = &keys
},
isSet: func(s *RuntimeSettings) bool { return s.ApiKeys != nil },
},
field("agent_job_retention_days",
func(s *RuntimeSettings) **int { return &s.AgentJobRetentionDays },
func(o *ApplicationConfig) int { return o.AgentJobRetentionDays },
func(o *ApplicationConfig, v int) { o.AgentJobRetentionDays = v }),
// Open Responses TTL: "0" or "" mean "no expiration" on the wire.
{
jsonNames: []string{"open_responses_store_ttl"},
snapshot: func(o *ApplicationConfig, s *RuntimeSettings) {
v := "0"
if o.OpenResponsesStoreTTL > 0 {
v = o.OpenResponsesStoreTTL.String()
}
s.OpenResponsesStoreTTL = &v
},
isSet: func(s *RuntimeSettings) bool { return s.OpenResponsesStoreTTL != nil },
apply: func(o *ApplicationConfig, s *RuntimeSettings) bool {
v := *s.OpenResponsesStoreTTL
if v == "0" || v == "" {
o.OpenResponsesStoreTTL = 0
return true
}
d, err := time.ParseDuration(v)
if err != nil {
xlog.Warn("invalid open_responses_store_ttl in runtime settings", "value", v, "error", err)
return false
}
o.OpenResponsesStoreTTL = d
return true
},
envSet: func(cur, base *ApplicationConfig) bool {
return cur.OpenResponsesStoreTTL != base.OpenResponsesStoreTTL
},
},
// Agent Pool.
field("agent_pool_enabled",
func(s *RuntimeSettings) **bool { return &s.AgentPoolEnabled },
func(o *ApplicationConfig) bool { return o.AgentPool.Enabled },
func(o *ApplicationConfig, v bool) { o.AgentPool.Enabled = v },
restartRequired()),
field("agent_pool_default_model",
func(s *RuntimeSettings) **string { return &s.AgentPoolDefaultModel },
func(o *ApplicationConfig) string { return o.AgentPool.DefaultModel },
func(o *ApplicationConfig, v string) { o.AgentPool.DefaultModel = v },
restartRequired()),
field("agent_pool_embedding_model",
func(s *RuntimeSettings) **string { return &s.AgentPoolEmbeddingModel },
func(o *ApplicationConfig) string { return o.AgentPool.EmbeddingModel },
func(o *ApplicationConfig, v string) { o.AgentPool.EmbeddingModel = v },
restartRequired()),
field("agent_pool_max_chunking_size",
func(s *RuntimeSettings) **int { return &s.AgentPoolMaxChunkingSize },
func(o *ApplicationConfig) int { return o.AgentPool.MaxChunkingSize },
func(o *ApplicationConfig, v int) { o.AgentPool.MaxChunkingSize = v },
restartRequired()),
field("agent_pool_chunk_overlap",
func(s *RuntimeSettings) **int { return &s.AgentPoolChunkOverlap },
func(o *ApplicationConfig) int { return o.AgentPool.ChunkOverlap },
func(o *ApplicationConfig, v int) { o.AgentPool.ChunkOverlap = v },
restartRequired()),
field("agent_pool_enable_logs",
func(s *RuntimeSettings) **bool { return &s.AgentPoolEnableLogs },
func(o *ApplicationConfig) bool { return o.AgentPool.EnableLogs },
func(o *ApplicationConfig, v bool) { o.AgentPool.EnableLogs = v },
restartRequired()),
field("agent_pool_collection_db_path",
func(s *RuntimeSettings) **string { return &s.AgentPoolCollectionDBPath },
func(o *ApplicationConfig) string { return o.AgentPool.CollectionDBPath },
func(o *ApplicationConfig, v string) { o.AgentPool.CollectionDBPath = v },
restartRequired()),
field("agent_pool_vector_engine",
func(s *RuntimeSettings) **string { return &s.AgentPoolVectorEngine },
func(o *ApplicationConfig) string { return o.AgentPool.VectorEngine },
func(o *ApplicationConfig, v string) { o.AgentPool.VectorEngine = v },
restartRequired()),
field("agent_pool_database_url",
func(s *RuntimeSettings) **string { return &s.AgentPoolDatabaseURL },
func(o *ApplicationConfig) string { return o.AgentPool.DatabaseURL },
func(o *ApplicationConfig, v string) { o.AgentPool.DatabaseURL = v },
restartRequired()),
field("agent_pool_agent_hub_url",
func(s *RuntimeSettings) **string { return &s.AgentPoolAgentHubURL },
func(o *ApplicationConfig) string { return o.AgentPool.AgentHubURL },
func(o *ApplicationConfig, v string) { o.AgentPool.AgentHubURL = v },
restartRequired()),
// LocalAI Assistant: stored as the negation for UI clarity.
field("localai_assistant_enabled",
func(s *RuntimeSettings) **bool { return &s.LocalAIAssistantEnabled },
func(o *ApplicationConfig) bool { return !o.DisableLocalAIAssistant },
func(o *ApplicationConfig, v bool) { o.DisableLocalAIAssistant = !v }),
// Branding: the file is the only source (no env/CLI), so it is always
// authoritative. Without fromFileAlways a restart silently drops the
// configured instance name and asset filenames.
field("instance_name",
func(s *RuntimeSettings) **string { return &s.InstanceName },
func(o *ApplicationConfig) string { return o.Branding.InstanceName },
func(o *ApplicationConfig, v string) { o.Branding.InstanceName = v },
fromFileAlways()),
field("instance_tagline",
func(s *RuntimeSettings) **string { return &s.InstanceTagline },
func(o *ApplicationConfig) string { return o.Branding.InstanceTagline },
func(o *ApplicationConfig, v string) { o.Branding.InstanceTagline = v },
fromFileAlways()),
field("logo_file",
func(s *RuntimeSettings) **string { return &s.LogoFile },
func(o *ApplicationConfig) string { return o.Branding.LogoFile },
func(o *ApplicationConfig, v string) { o.Branding.LogoFile = v },
fromFileAlways()),
field("logo_horizontal_file",
func(s *RuntimeSettings) **string { return &s.LogoHorizontalFile },
func(o *ApplicationConfig) string { return o.Branding.LogoHorizontalFile },
func(o *ApplicationConfig, v string) { o.Branding.LogoHorizontalFile = v },
fromFileAlways()),
field("favicon_file",
func(s *RuntimeSettings) **string { return &s.FaviconFile },
func(o *ApplicationConfig) string { return o.Branding.FaviconFile },
func(o *ApplicationConfig, v string) { o.Branding.FaviconFile = v },
fromFileAlways()),
field("mitm_listen",
func(s *RuntimeSettings) **string { return &s.MITMListen },
func(o *ApplicationConfig) string { return o.MITMListen },
func(o *ApplicationConfig, v string) { o.MITMListen = v }),
// PII default detectors: cloned on both directions so callers can't
// alias the live config's slice.
fieldEq("pii_default_detectors",
func(s *RuntimeSettings) **[]string { return &s.PIIDefaultDetectors },
func(o *ApplicationConfig) []string { return append([]string(nil), o.PIIDefaultDetectors...) },
func(o *ApplicationConfig, v []string) { o.PIIDefaultDetectors = append([]string(nil), v...) },
slices.Equal),
}