legion-settings 1.3.2 → 1.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 905fefabffcdb64f2d654827742feb7c2651a3603625eb7f4e2eb71930416c8e
4
- data.tar.gz: e19c105841be33b885565d5dac97d2549ee9813897ea4286bf68977e35e95e63
3
+ metadata.gz: 220738db670fe9bbda99acc5d64d903de88268afdc83bb0ef6c482f550b7b769
4
+ data.tar.gz: b46f3ff6ed58a9a7a0269230814ea2a9204953970c93877747c5d28c23410c8b
5
5
  SHA512:
6
- metadata.gz: 71a352c5cd2a45145619ae24d31f9b9f3280c33c0d2f864c21c9549f7a78a4a48b45f06d4172bc6ed30d314a355ab1548bc141efa63638917734816b1c66c0ba
7
- data.tar.gz: 6a7a29a655bc77c13352631a62763bc20c8cf9d2a7e84efeaa43b92cc70301425081aa8ed82e6d34a2c77aac9944500ff241ce404f09d0af76bb4de9aaf68baf
6
+ metadata.gz: 2e8a71bf3705c3f2a809b454c1bc676f7ab1f7c2e3c2dd1b3d6b317f5126ba84bde201b75a1f65e5111d31f1b67408e173eef8da3683d4d19ee48af83c69bdc0
7
+ data.tar.gz: a537874627f4d55ee13bb3ec4cbce0e869337a15c0f6c1bfd012a16f081c79f8bb7a56f39d7431c7eb6c1e6e525cda8a98155f534bfac32177a135b1aa96e7cd
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Legion::Settings Changelog
2
2
 
3
+ ## [1.3.5] - 2026-03-19
4
+
5
+ ### Added
6
+ - DNS-based bootstrap discovery: auto-detect corporate config from `legion-bootstrap.<search-domain>`
7
+ - `Settings[:dns]` populated with FQDN, default domain, search domains, and nameservers
8
+ - `DnsBootstrap` class with DNS resolution, HTTPS fetch, local caching, and background refresh
9
+ - First boot blocks on fetch; subsequent boots use cache with async refresh
10
+ - Opt-out via `LEGION_DNS_BOOTSTRAP=false` environment variable
11
+
12
+ ## [1.3.4] - 2026-03-18
13
+
14
+ ### Fixed
15
+ - Added `logger` gem to test dependencies for Ruby 4.0 compatibility
16
+
17
+ ## [1.3.3] - 2026-03-17
18
+
19
+ ### Fixed
20
+ - Config JSON parse error now includes the filename at ERROR level instead of burying it in DEBUG
21
+
3
22
  ## [1.3.2] - 2026-03-17
4
23
 
5
24
  ### Added
data/CLAUDE.md CHANGED
@@ -8,6 +8,7 @@
8
8
  Hash-like configuration store for the LegionIO framework. Loads settings from JSON files, directories, and environment variables. Provides a unified `Legion::Settings[:key]` accessor used by all other Legion gems. Includes schema-based validation with type inference, enum constraints, and cross-module checks.
9
9
 
10
10
  **GitHub**: https://github.com/LegionIO/legion-settings
11
+ **Version**: 1.3.4
11
12
  **License**: Apache-2.0
12
13
 
13
14
  ## Architecture
data/Gemfile CHANGED
@@ -5,6 +5,7 @@ source 'https://rubygems.org'
5
5
  gemspec
6
6
  group :test do
7
7
  gem 'legion-logging'
8
+ gem 'logger'
8
9
  gem 'rake'
9
10
  gem 'rspec'
10
11
  gem 'rspec_junit_formatter'
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Configuration management module for the [LegionIO](https://github.com/LegionIO/LegionIO) framework. Loads settings from JSON files, directories, and environment variables. Provides a unified `Legion::Settings[:key]` accessor used by all other Legion gems.
4
4
 
5
+ **Version**: 1.3.4
6
+
5
7
  ## Installation
6
8
 
7
9
  ```bash
@@ -0,0 +1,56 @@
1
+ # Design: Include Filename in JSON Parse Error Messages
2
+
3
+ ## Problem
4
+
5
+ When a user edits a config file and introduces a JSON syntax error, the settings loader logs:
6
+
7
+ ```
8
+ ERROR config file must be valid json
9
+ ```
10
+
11
+ The filename is only logged at DEBUG level on the next line:
12
+
13
+ ```ruby
14
+ Legion::Logging.error('config file must be valid json')
15
+ Legion::Logging.debug("file:#{file}, error: #{e}")
16
+ ```
17
+
18
+ This makes it nearly impossible for users to identify which file is broken, especially when multiple config files exist in `~/.legionio/settings/`. The settings loader silently skips the broken file, and downstream code fails with confusing errors (e.g., RubyLLM complaining about missing API keys because `llm.json` wasn't loaded).
19
+
20
+ ## Proposed Solution
21
+
22
+ Change `load_file` in `loader.rb` to include the filename and parse error in the ERROR message:
23
+
24
+ ```ruby
25
+ rescue Legion::JSON::ParseError => e
26
+ Legion::Logging.error("config file must be valid json: #{file}")
27
+ Legion::Logging.error(" parse error: #{e.message}")
28
+ end
29
+ ```
30
+
31
+ This is a one-line change (plus one added line) in `loader.rb:140-142`.
32
+
33
+ ### Before
34
+
35
+ ```
36
+ [2026-03-17 18:27:27 -0500] ERROR config file must be valid json
37
+ ```
38
+
39
+ ### After
40
+
41
+ ```
42
+ [2026-03-17 18:27:27 -0500] ERROR config file must be valid json: /Users/matt/.legionio/settings/llm.json
43
+ [2026-03-17 18:27:27 -0500] ERROR parse error: unexpected token at '{ "llm": { ... '
44
+ ```
45
+
46
+ ## Alternatives Considered
47
+
48
+ 1. **Raise instead of logging** — rejected; the current behavior of skipping broken files and continuing is correct for resilience. But the user needs to know what happened.
49
+ 2. **Add a `config validate` pre-check** — already exists (`legion config validate`), but users won't run it proactively. The error message at load time is the first line of defense.
50
+ 3. **Return the broken files in `loaded_files`** — would require API changes for minimal benefit.
51
+
52
+ ## Constraints
53
+
54
+ - Do not change the error-handling behavior (continue loading other files)
55
+ - Do not change method signatures
56
+ - The debug line can be removed since the info is now in the error message
@@ -0,0 +1,34 @@
1
+ # Implementation: Include Filename in JSON Parse Error Messages
2
+
3
+ ## Phase 1: Update Error Logging
4
+
5
+ ### Files to Modify
6
+
7
+ - `lib/legion/settings/loader.rb` — lines 140-142
8
+
9
+ ### Changes
10
+
11
+ Replace:
12
+
13
+ ```ruby
14
+ rescue Legion::JSON::ParseError => e
15
+ Legion::Logging.error('config file must be valid json')
16
+ Legion::Logging.debug("file:#{file}, error: #{e}")
17
+ ```
18
+
19
+ With:
20
+
21
+ ```ruby
22
+ rescue Legion::JSON::ParseError => e
23
+ Legion::Logging.error("config file must be valid json: #{file}")
24
+ Legion::Logging.error(" parse error: #{e.message}")
25
+ ```
26
+
27
+ ### Spec Coverage
28
+
29
+ - Add spec in `spec/legion/loader_spec.rb` that writes invalid JSON to a temp file, calls `load_file`, and asserts the error was logged with the filename
30
+
31
+ ### Version Bump
32
+
33
+ - Bump patch version in `lib/legion/settings/version.rb`
34
+ - Update CHANGELOG.md
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+ require 'net/http'
6
+ require 'resolv'
7
+ require 'uri'
8
+
9
+ module Legion
10
+ module Settings
11
+ class DnsBootstrap
12
+ CACHE_FILENAME = '_dns_bootstrap.json'
13
+ HOSTNAME_PREFIX = 'legion-bootstrap'
14
+ URL_PATH = '/legion/bootstrap.json'
15
+ HTTP_TIMEOUT = 10
16
+
17
+ attr_reader :default_domain, :hostname, :url, :cache_path
18
+
19
+ def initialize(default_domain:, cache_dir: nil)
20
+ @default_domain = default_domain
21
+ @hostname = "#{HOSTNAME_PREFIX}.#{default_domain}"
22
+ @url = "https://#{@hostname}#{URL_PATH}"
23
+ dir = cache_dir || File.expand_path('~/.legionio/settings')
24
+ @cache_path = File.join(dir, CACHE_FILENAME)
25
+ end
26
+
27
+ def resolve?
28
+ Resolv.getaddress(@hostname)
29
+ true
30
+ rescue Resolv::ResolvError, Resolv::ResolvTimeout
31
+ false
32
+ end
33
+
34
+ def fetch
35
+ return nil unless resolve?
36
+
37
+ uri = URI.parse(@url)
38
+ http = Net::HTTP.new(uri.host, uri.port)
39
+ http.use_ssl = true
40
+ http.open_timeout = HTTP_TIMEOUT
41
+ http.read_timeout = HTTP_TIMEOUT
42
+ response = http.request(Net::HTTP::Get.new(uri))
43
+ return nil unless response.is_a?(Net::HTTPSuccess)
44
+
45
+ ::JSON.parse(response.body, symbolize_names: true)
46
+ rescue StandardError
47
+ nil
48
+ end
49
+
50
+ def write_cache(config)
51
+ FileUtils.mkdir_p(File.dirname(@cache_path))
52
+ payload = config.merge(
53
+ _dns_bootstrap_meta: {
54
+ fetched_at: Time.now.utc.iso8601,
55
+ hostname: @hostname,
56
+ url: @url
57
+ }
58
+ )
59
+ tmp = "#{@cache_path}.tmp"
60
+ File.write(tmp, ::JSON.pretty_generate(payload))
61
+ File.rename(tmp, @cache_path)
62
+ end
63
+
64
+ def read_cache
65
+ return nil unless File.exist?(@cache_path)
66
+
67
+ raw = ::JSON.parse(File.read(@cache_path), symbolize_names: true)
68
+ raw.delete(:_dns_bootstrap_meta)
69
+ raw
70
+ rescue ::JSON::ParserError
71
+ FileUtils.rm_f(@cache_path)
72
+ nil
73
+ end
74
+
75
+ def cache_exists?
76
+ File.exist?(@cache_path)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'resolv'
3
4
  require 'socket'
4
5
  require 'legion/settings/os'
6
+ require_relative 'dns_bootstrap'
5
7
 
6
8
  module Legion
7
9
  module Settings
@@ -19,6 +21,17 @@ module Legion
19
21
  @loaded_files = []
20
22
  end
21
23
 
24
+ def dns_defaults
25
+ resolv_config = read_resolv_config
26
+ {
27
+ fqdn: detect_fqdn,
28
+ default_domain: resolv_config[:search_domains]&.first,
29
+ search_domains: resolv_config[:search_domains] || [],
30
+ nameservers: resolv_config[:nameservers] || [],
31
+ bootstrap: { enabled: true }
32
+ }
33
+ end
34
+
22
35
  def client_defaults
23
36
  {
24
37
  hostname: system_hostname,
@@ -38,7 +51,25 @@ module Legion
38
51
  vault: { connected: false }
39
52
  },
40
53
  cache: { enabled: true, connected: false, driver: 'dalli' },
41
- extensions: {},
54
+ extensions: {
55
+ core: %w[
56
+ lex-node lex-tasker lex-scheduler lex-health lex-ping
57
+ lex-telemetry lex-metering lex-log lex-audit
58
+ lex-conditioner lex-transformer lex-exec lex-lex lex-codegen
59
+ ],
60
+ ai: %w[lex-claude lex-openai lex-gemini],
61
+ gaia: %w[lex-tick lex-mesh lex-apollo lex-cortex],
62
+ categories: {
63
+ core: { type: :list, tier: 1 },
64
+ ai: { type: :list, tier: 2 },
65
+ gaia: { type: :list, tier: 3 },
66
+ agentic: { type: :prefix, tier: 4 }
67
+ },
68
+ blocked: [],
69
+ reserved_prefixes: %w[core ai agentic gaia],
70
+ reserved_words: %w[transport cache crypt data settings json logging llm rbac legion],
71
+ agentic: { allowed: nil, blocked: [] }
72
+ },
42
73
  reload: false,
43
74
  reloading: false,
44
75
  auto_install_missing_lex: true,
@@ -53,7 +84,8 @@ module Legion
53
84
  },
54
85
  transport: { connected: false },
55
86
  data: { connected: false },
56
- role: { profile: nil, extensions: [] }
87
+ role: { profile: nil, extensions: [] },
88
+ dns: dns_defaults
57
89
  }
58
90
  end
59
91
 
@@ -98,6 +130,27 @@ module Legion
98
130
  load_api_env
99
131
  end
100
132
 
133
+ def load_dns_bootstrap(cache_dir: nil)
134
+ return if ENV['LEGION_DNS_BOOTSTRAP'] == 'false'
135
+
136
+ domain = @settings.dig(:dns, :default_domain)
137
+ return unless domain
138
+ return unless @settings.dig(:dns, :bootstrap, :enabled)
139
+
140
+ dir = cache_dir || File.expand_path('~/.legionio/settings')
141
+ bootstrap = DnsBootstrap.new(default_domain: domain, cache_dir: dir)
142
+
143
+ config = if bootstrap.cache_exists?
144
+ load_dns_from_cache(bootstrap)
145
+ else
146
+ load_dns_first_boot(bootstrap)
147
+ end
148
+
149
+ return unless config
150
+
151
+ merge_dns_config(config, bootstrap)
152
+ end
153
+
101
154
  def load_module_settings(config)
102
155
  @settings = deep_merge(config, @settings)
103
156
  end
@@ -120,8 +173,8 @@ module Legion
120
173
  # @indifferent_access = false
121
174
  @loaded_files << file
122
175
  rescue Legion::JSON::ParseError => e
123
- Legion::Logging.error('config file must be valid json')
124
- Legion::Logging.debug("file:#{file}, error: #{e}")
176
+ Legion::Logging.error("config file must be valid json: #{file}")
177
+ Legion::Logging.error(" parse error: #{e.message}")
125
178
  end
126
179
  else
127
180
  Legion::Logging.warn("Config file does not exist or is not readable file:#{file}")
@@ -166,6 +219,39 @@ module Legion
166
219
 
167
220
  private
168
221
 
222
+ def load_dns_from_cache(bootstrap)
223
+ config = bootstrap.read_cache
224
+ start_dns_background_refresh(bootstrap) if config
225
+ config
226
+ end
227
+
228
+ def load_dns_first_boot(bootstrap)
229
+ Legion::Logging.debug("DNS bootstrap: first boot, fetching from #{bootstrap.url}")
230
+ config = bootstrap.fetch
231
+ bootstrap.write_cache(config) if config
232
+ config
233
+ end
234
+
235
+ def merge_dns_config(config, bootstrap)
236
+ @settings = deep_merge(config, @settings)
237
+ @settings[:dns] ||= {}
238
+ @settings[:dns][:corp_bootstrap] = {
239
+ discovered: true,
240
+ hostname: bootstrap.hostname,
241
+ url: bootstrap.url
242
+ }
243
+ @indifferent_access = false
244
+ end
245
+
246
+ def start_dns_background_refresh(bootstrap)
247
+ Thread.new do
248
+ fresh = bootstrap.fetch
249
+ bootstrap.write_cache(fresh) if fresh
250
+ rescue StandardError
251
+ # Background refresh is best-effort
252
+ end
253
+ end
254
+
169
255
  def setting_category(category)
170
256
  @settings[category].map do |name, details|
171
257
  details.merge(name: name.to_s)
@@ -283,6 +369,25 @@ module Legion
283
369
  Legion::Logging.error(message)
284
370
  raise(Error, message)
285
371
  end
372
+
373
+ def read_resolv_config
374
+ config = Resolv::DNS::Config.default_config_hash
375
+ {
376
+ search_domains: config[:search]&.map(&:to_s)&.uniq,
377
+ nameservers: config[:nameserver]&.map(&:to_s)&.uniq
378
+ }
379
+ rescue StandardError
380
+ { search_domains: [], nameservers: [] }
381
+ end
382
+
383
+ def detect_fqdn
384
+ fqdn = Addrinfo.getaddrinfo(Socket.gethostname, nil).first&.canonname
385
+ return nil if fqdn.nil?
386
+
387
+ fqdn.include?('.') ? fqdn : nil
388
+ rescue StandardError
389
+ nil
390
+ end
286
391
  end
287
392
  end
288
393
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Settings
5
- VERSION = '1.3.2'
5
+ VERSION = '1.3.5'
6
6
  end
7
7
  end
@@ -17,6 +17,7 @@ module Legion
17
17
  def load(options = {})
18
18
  @loader = Legion::Settings::Loader.new
19
19
  @loader.load_env
20
+ @loader.load_dns_bootstrap
20
21
  @loader.load_file(options[:config_file]) if options[:config_file]
21
22
  @loader.load_directory(options[:config_dir]) if options[:config_dir]
22
23
  options[:config_dirs]&.each do |directory|
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-settings
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.2
4
+ version: 1.3.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -42,8 +42,11 @@ files:
42
42
  - Gemfile
43
43
  - LICENSE
44
44
  - README.md
45
+ - docs/plans/2026-03-17-config-error-filename-design.md
46
+ - docs/plans/2026-03-17-config-error-filename-implementation.md
45
47
  - legion-settings.gemspec
46
48
  - lib/legion/settings.rb
49
+ - lib/legion/settings/dns_bootstrap.rb
47
50
  - lib/legion/settings/loader.rb
48
51
  - lib/legion/settings/os.rb
49
52
  - lib/legion/settings/resolver.rb