Class: Legion::Settings::Loader

Inherits:
Object
  • Object
show all
Includes:
Logging::Helper, OS
Defined in:
lib/legion/settings/loader.rb

Defined Under Namespace

Classes: Error

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from OS

linux?, mac?, #os, unix?, windows?

Constructor Details

#initializeLoader

Returns a new instance of Loader.



39
40
41
42
43
44
45
46
47
# File 'lib/legion/settings/loader.rb', line 39

def initialize
  @warnings = []
  @errors = []
  @settings = default_settings
  @indifferent_access = false
  @loaded_files = []
  @merged_modules = {}
  log.debug('Initialized Legion::Settings::Loader with default settings')
end

Instance Attribute Details

#errorsObject (readonly)

Returns the value of attribute errors.



20
21
22
# File 'lib/legion/settings/loader.rb', line 20

def errors
  @errors
end

#loaded_filesObject (readonly)

Returns the value of attribute loaded_files.



20
21
22
# File 'lib/legion/settings/loader.rb', line 20

def loaded_files
  @loaded_files
end

#merged_modulesObject (readonly)

Returns the value of attribute merged_modules.



20
21
22
# File 'lib/legion/settings/loader.rb', line 20

def merged_modules
  @merged_modules
end

#settingsObject (readonly)

Returns the value of attribute settings.



20
21
22
# File 'lib/legion/settings/loader.rb', line 20

def settings
  @settings
end

#warningsObject (readonly)

Returns the value of attribute warnings.



20
21
22
# File 'lib/legion/settings/loader.rb', line 20

def warnings
  @warnings
end

Class Method Details

.default_directoriesObject



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/legion/settings/loader.rb', line 22

def self.default_directories
  env_dirs = ENV.fetch('LEGION_SETTINGS_DIRS', nil)
  if env_dirs && !env_dirs.strip.empty?
    env_dirs_list = env_dirs.split(File::PATH_SEPARATOR).map(&:strip).reject(&:empty?).map { |p| File.expand_path(p) }
    return env_dirs_list unless env_dirs_list.empty?
  end

  dirs = [File.expand_path('~/.legionio/settings')]
  if OS.windows?
    appdata = ENV.fetch('APPDATA', nil)
    dirs << File.join(appdata, 'legionio', 'settings') if appdata && !appdata.strip.empty?
  else
    dirs << '/etc/legionio/settings'
  end
  dirs
end

Instance Method Details

#[](key) ⇒ Object

Direct key lookup — does NOT trigger indifferent_access! rebuild. This is the hot path called by every Settings access. Supports both symbol and string keys without converting the whole tree.



148
149
150
151
152
153
# File 'lib/legion/settings/loader.rb', line 148

def [](key)
  result = @settings[key]
  return result unless result.nil? && key.is_a?(String)

  @settings[key.to_sym]
end

#[]=(key, value) ⇒ Object



167
168
169
170
# File 'lib/legion/settings/loader.rb', line 167

def []=(key, value)
  @settings[key] = value
  mark_dirty!
end

#client_defaultsObject



69
70
71
72
73
74
75
76
# File 'lib/legion/settings/loader.rb', line 69

def client_defaults
  {
    hostname: system_hostname,
    address:  system_address,
    name:     "#{::Socket.gethostname.tr('.', '_')}.#{::Process.pid}",
    ready:    false
  }
end

#default_settingsObject

No more per-module defaults methods in the Loader. Tier 1 deps (json, logging) are called directly in default_settings. Tier 2 libraries (transport, cache, etc.) self-register via Legion::Settings.register_library when they load.



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/legion/settings/loader.rb', line 83

def default_settings
  {
    # --- Tier 1: gemspec dependencies (always installed with legion-settings) ---
    # legion-logging: always available, has Settings.default
    logging:                    Legion::Logging::Settings.default,
    # legion-json: always available, no Settings module yet — stub
    # until legion-json adds Legion::JSON::Settings.default
    json:                       Concurrent::Hash.new,

    # --- Structural: owned by legion-settings itself ---
    client:                     client_defaults,
    cluster:                    Concurrent::Hash.new,
    dns:                        dns_defaults,
    extensions:                 Concurrent::Hash[
      core:               %w[
        lex-node lex-tasker lex-scheduler lex-health lex-ping
        lex-telemetry lex-metering lex-log lex-audit
        lex-conditioner lex-transformer lex-exec lex-lex lex-codegen
      ],
      ai:                 %w[lex-claude lex-openai lex-gemini],
      gaia:               %w[lex-tick lex-mesh lex-apollo],
      categories:         {
        core:    { type: :list, tier: 1 },
        ai:      { type: :list, tier: 2 },
        gaia:    { type: :list, tier: 3 },
        agentic: { type: :prefix, tier: 4 }
      },
      blocked:            [],
      reserved_prefixes:  %w[core ai agentic gaia],
      reserved_words:     %w[transport cache crypt data settings json logging llm rbac legion],
      agentic:            { allowed: nil, blocked: [] },
      parallel_pool_size: 24
    ],
    reload:                     false,
    reloading:                  false,
    auto_install_missing_lex:   true,
    default_extension_settings: {},
    role:                       { profile: nil, extensions: [] },
    region:                     { current: nil, primary: nil, failover: nil, peers: [],
                                  default_affinity: 'any', data_residency: {} },
    process:                    { role: 'full' },

    # --- Tier 2: stubs for libraries that self-register via register_library ---
    # These ensure Settings[:key] returns a hash (not nil) before
    # the owning library loads. The library replaces these with its
    # full defaults when it calls Legion::Settings.register_library.
    absorbers:                  Concurrent::Hash.new,
    cache:                      Concurrent::Hash.new,
    crypt:                      Concurrent::Hash.new,
    data:                       Concurrent::Hash.new,
    transport:                  Concurrent::Hash.new
  }
end

#dig(*keys) ⇒ Object

Direct nested lookup — does NOT trigger indifferent_access! rebuild.



156
157
158
159
160
161
162
163
164
165
# File 'lib/legion/settings/loader.rb', line 156

def dig(*keys)
  keys.reduce(self) do |current, key|
    return nil unless current.respond_to?(:[])

    value = current.is_a?(Loader) ? current[key] : (current[key] || current[key.to_s])
    return nil if value.nil? && !current.is_a?(Loader)

    value
  end
end

#dns_defaultsObject



49
50
51
52
53
54
55
56
57
58
# File 'lib/legion/settings/loader.rb', line 49

def dns_defaults
  resolv_config = read_resolv_config
  {
    fqdn:           nil, # lazy — resolved on first access via resolve_fqdn!
    default_domain: resolv_config[:search_domains]&.first,
    search_domains: resolv_config[:search_domains] || [],
    nameservers:    resolv_config[:nameservers] || [],
    bootstrap:      { enabled: true }
  }
end

#hexdigestObject



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/legion/settings/loader.rb', line 172

def hexdigest
  if @hexdigest && @indifferent_access
    @hexdigest
  else
    hash = case legion_service_name
           when 'client', 'rspec'
             to_hash
           else
             to_hash.reject do |key, _value|
               key.to_s == 'client'
             end
           end
    @hexdigest = Digest::SHA256.hexdigest(hash.to_s)
  end
end

#load_client_overridesObject



259
260
261
262
263
264
265
266
267
268
# File 'lib/legion/settings/loader.rb', line 259

def load_client_overrides
  @settings[:client][:subscriptions] ||= []
  if @settings[:client][:subscriptions].is_a?(Array)
    @settings[:client][:subscriptions] << "client:#{@settings[:client][:name]}"
    @settings[:client][:subscriptions].uniq!
    mark_dirty!
  else
    log.warn('unable to apply legion client overrides, reason: client subscriptions is not an array')
  end
end

#load_directory(directory) ⇒ Object



248
249
250
251
252
253
254
255
256
257
# File 'lib/legion/settings/loader.rb', line 248

def load_directory(directory)
  path = directory.gsub(/\\(?=\S)/, '/')
  if File.readable?(path) && File.executable?(path)
    files = Dir.glob(File.join(path, '**', '*.json'))
    files.each { |file| load_file(file) }
    log.info("Settings: loaded directory #{path} (#{files.size} files)")
  else
    load_error('insufficient permissions for loading', directory: directory)
  end
end

#load_dns_bootstrap(cache_dir: nil) ⇒ Object



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/legion/settings/loader.rb', line 193

def load_dns_bootstrap(cache_dir: nil)
  return if ENV['LEGION_DNS_BOOTSTRAP'] == 'false'

  domain = @settings.dig(:dns, :default_domain)
  return unless domain
  return unless @settings.dig(:dns, :bootstrap, :enabled)

  dir = cache_dir || File.expand_path('~/.legionio/settings')
  bootstrap = DnsBootstrap.new(default_domain: domain, cache_dir: dir)

  config = if bootstrap.cache_exists?
             load_dns_from_cache(bootstrap)
           else
             load_dns_first_boot(bootstrap)
           end

  return unless config

  merge_dns_config(config, bootstrap)
end

#load_envObject



188
189
190
191
# File 'lib/legion/settings/loader.rb', line 188

def load_env
  load_api_env
  load_privacy_env
end

#load_file(file) ⇒ Object



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/legion/settings/loader.rb', line 229

def load_file(file)
  log.debug("Trying to load file #{file}")
  if File.file?(file) && File.readable?(file)
    begin
      contents = read_config_file(file)
      config = contents.empty? ? {} : Legion::JSON.load(contents)
      @settings = deep_merge(@settings, config)
      mark_dirty!
      @loaded_files << file
      log.debug("Loaded settings file #{file}")
    rescue Legion::JSON::ParseError => e
      log.error("config file must be valid json: #{file}")
      log.error("  parse error: #{e.message}")
    end
  else
    log.warn("Config file does not exist or is not readable file:#{file}")
  end
end

#load_module_default(config) ⇒ Object



222
223
224
225
226
227
# File 'lib/legion/settings/loader.rb', line 222

def load_module_default(config)
  mod_name = config.keys.first
  log.debug("Loading module defaults: #{mod_name}")
  @settings = deep_merge(config, @settings)
  mark_dirty!
end

#load_module_settings(config) ⇒ Object



214
215
216
217
218
219
220
# File 'lib/legion/settings/loader.rb', line 214

def load_module_settings(config)
  mod_name = config.keys.first
  log.debug("Loading module settings: #{mod_name}")
  @merged_modules = deep_merge(@merged_modules, config)
  @settings = deep_merge(config, @settings)
  mark_dirty!
end

#load_overrides!Object



270
271
272
# File 'lib/legion/settings/loader.rb', line 270

def load_overrides!
  load_client_overrides if %w[client rspec].include?(legion_service_name)
end

#mark_dirty!Object



391
392
393
394
# File 'lib/legion/settings/loader.rb', line 391

def mark_dirty!
  @indifferent_access = false
  @hexdigest = nil
end

#resolve_fqdn!String?

Lazily resolve the FQDN on first access instead of blocking at init.

Returns:

  • (String, nil)

    the fully qualified domain name, or nil



63
64
65
66
67
# File 'lib/legion/settings/loader.rb', line 63

def resolve_fqdn!
  return @settings[:dns][:fqdn] if @settings.dig(:dns, :fqdn)

  @settings[:dns][:fqdn] = detect_fqdn
end

#set_env!Object



274
275
276
# File 'lib/legion/settings/loader.rb', line 274

def set_env!
  ENV['LEGION_LOADED_TEMPFILE'] = create_loaded_tempfile!
end

#to_hashObject



137
138
139
140
141
142
143
# File 'lib/legion/settings/loader.rb', line 137

def to_hash
  unless @indifferent_access
    indifferent_access!
    @hexdigest = nil
  end
  @settings
end

#validateObject



278
279
280
281
282
# File 'lib/legion/settings/loader.rb', line 278

def validate
  Legion::Settings.validate!
rescue Legion::Settings::ValidationError
  # errors are already collected in @errors
end