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 +4 -4
- data/CHANGELOG.md +19 -0
- data/CLAUDE.md +1 -0
- data/Gemfile +1 -0
- data/README.md +2 -0
- data/docs/plans/2026-03-17-config-error-filename-design.md +56 -0
- data/docs/plans/2026-03-17-config-error-filename-implementation.md +34 -0
- data/lib/legion/settings/dns_bootstrap.rb +80 -0
- data/lib/legion/settings/loader.rb +109 -4
- data/lib/legion/settings/version.rb +1 -1
- data/lib/legion/settings.rb +1 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 220738db670fe9bbda99acc5d64d903de88268afdc83bb0ef6c482f550b7b769
|
|
4
|
+
data.tar.gz: b46f3ff6ed58a9a7a0269230814ea2a9204953970c93877747c5d28c23410c8b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
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(
|
|
124
|
-
Legion::Logging.
|
|
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
|
data/lib/legion/settings.rb
CHANGED
|
@@ -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.
|
|
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
|