legionio 1.6.0 → 1.6.2

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: 6413e545fabb634ddda16a99e884701751aa1cdd8245b649799f4836ae0c7360
4
- data.tar.gz: 4239fd5af0278ba21cf33bbd320e62e67b103d93d36f3f5266eb5269019b7859
3
+ metadata.gz: 49e219b9a9c790b6ec63903daee3a26194a5a1611cccd0013a05bb7a34a3d7c9
4
+ data.tar.gz: eedbb6e81c2ba386cee0718eb3aed307a021d25959579bb2a02c8bd011ca4fa5
5
5
  SHA512:
6
- metadata.gz: 2dee1932ef724c587ff8d26c496913cb7b90b1385f7793b3d13c7e3e703440c0b6ad4f7b431215c395ee742cd920afebcda784e29feca058d67a6afb73cb0abb
7
- data.tar.gz: fd476599db94cf729ed3e5b66f82f1f090b44215222c24e6a8cf8e6d67c9837fd204561c6054f83505b0fc194a3766aef365eadaa46712babb5750dde82383c3
6
+ metadata.gz: 141f1ad15ae3595a411e4e1ddaf7fa4544bd1fd5aa23be7425574e783a55581773d0a860ace1d7f6dce97bf3004b6fc42998374cdfe8b3555b01547e98be5f1e
7
+ data.tar.gz: a05b2f2cb76d9237dc785b396af6f72b03657866ba4f57fd9b7673594d3212634e9c920d90529c5134544cbdb8d32e9994b188e155366f6f69f7df803355dc90
data/.gitignore CHANGED
@@ -15,7 +15,8 @@
15
15
  legionio.key
16
16
  # runtime artifacts
17
17
  .DS_Store
18
- legionio.db
18
+ *.db
19
+ *.json
19
20
  logs/
20
21
  # local settings (may contain secrets)
21
22
  settings/
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Legion Changelog
2
2
 
3
+ ## [1.6.2] - 2026-03-26
4
+
5
+ ### Fixed
6
+ - `legionio update` remote check failed for all gems due to TCP connection exhaustion (24 parallel SSL connections to rubygems.org)
7
+ - Replace thread pool with 4 batched threads using persistent HTTP keep-alive connections (55 gems in ~4s)
8
+
9
+ ## [1.6.1] - 2026-03-26
10
+
11
+ ### Fixed
12
+ - `legionio update` now shows "(remote check failed)" instead of "(already latest)" when rubygems.org fetch fails
13
+ - Add HTTP timeouts (5s connect, 10s read) to remote version checks to prevent thread pool exhaustion
14
+ - Install failures now show "(install may have failed)" instead of "(already latest)"
15
+ - Distinct statuses: current, check_failed, installed, failed (was single ambiguous "updated" for all)
16
+
3
17
  ## [1.6.0] - 2026-03-26
4
18
 
5
19
  ### Added
data/legionio.gemspec CHANGED
@@ -60,8 +60,8 @@ Gem::Specification.new do |spec|
60
60
  spec.add_dependency 'legion-settings', '>= 1.3.19'
61
61
  spec.add_dependency 'legion-transport', '>= 1.4.0'
62
62
 
63
- spec.add_dependency 'legion-apollo', '>= 0.2.1'
64
- spec.add_dependency 'legion-gaia', '>= 0.9.24'
63
+ spec.add_dependency 'legion-apollo', '>= 0.3.1'
64
+ spec.add_dependency 'legion-gaia', '>= 0.9.26'
65
65
  spec.add_dependency 'legion-llm', '>= 0.5.8'
66
66
  spec.add_dependency 'legion-tty', '>= 0.4.35'
67
67
  spec.add_dependency 'lex-node'
@@ -89,61 +89,75 @@ module Legion
89
89
  remote && local && Gem::Version.new(remote) > Gem::Version.new(local)
90
90
  end
91
91
 
92
- if dry_run
93
- return gem_names.map do |name|
94
- local = local_versions[name]
95
- remote = remote_versions[name]
96
- needs_update = remote && local && Gem::Version.new(remote) > Gem::Version.new(local)
97
- { name: name, from: local, to: remote, status: needs_update ? 'available' : 'current' }
98
- end
92
+ return dry_run_results(gem_names, local_versions, remote_versions, outdated) if dry_run
93
+
94
+ return current_results(gem_names, remote_versions) if outdated.empty?
95
+
96
+ install_results(gem_names, gem_bin, remote_versions, outdated)
97
+ end
98
+
99
+ def dry_run_results(gem_names, local_versions, remote_versions, outdated)
100
+ gem_names.map do |name|
101
+ remote = remote_versions[name]
102
+ status = if outdated.include?(name) then 'available'
103
+ elsif remote then 'current'
104
+ else 'check_failed'
105
+ end
106
+ { name: name, from: local_versions[name], to: remote, status: status }
99
107
  end
108
+ end
100
109
 
101
- return gem_names.map { |name| { name: name, status: 'updated', output: '' } } if outdated.empty?
110
+ def current_results(gem_names, remote_versions)
111
+ gem_names.map do |name|
112
+ { name: name, status: remote_versions[name] ? 'current' : 'check_failed', remote: remote_versions[name] }
113
+ end
114
+ end
102
115
 
116
+ def install_results(gem_names, gem_bin, remote_versions, outdated)
103
117
  output = `#{gem_bin} install #{outdated.join(' ')} --no-document 2>&1`
104
118
  success = $CHILD_STATUS.success?
105
119
  gem_names.map do |name|
106
120
  if outdated.include?(name)
107
- { name: name, status: success ? 'updated' : 'failed', output: output.strip }
121
+ { name: name, status: success ? 'installed' : 'failed', remote: remote_versions[name], output: output.strip }
108
122
  else
109
- { name: name, status: 'updated', output: '' }
123
+ { name: name, status: remote_versions[name] ? 'current' : 'check_failed', remote: remote_versions[name] }
110
124
  end
111
125
  end
112
126
  end
113
127
 
114
128
  def fetch_remote_versions_parallel(gem_names)
115
129
  results = Concurrent::Hash.new
116
- pool = Concurrent::FixedThreadPool.new([gem_names.size, 24].min)
117
- latch = Concurrent::CountDownLatch.new(gem_names.size)
118
-
119
- gem_names.each do |name|
120
- pool.post do
121
- version = fetch_remote_version(name)
122
- results[name] = version if version
123
- rescue StandardError => e
124
- Legion::Logging.debug("UpdateCommand#fetch_remote_version #{name}: #{e.message}") if defined?(Legion::Logging)
125
- ensure
126
- latch.count_down
130
+ thread_count = [gem_names.size, 4].min
131
+ slices = gem_names.each_slice((gem_names.size / thread_count.to_f).ceil).to_a
132
+ threads = slices.map do |batch|
133
+ Thread.new(batch) do |names|
134
+ fetch_batch(names, results)
127
135
  end
128
136
  end
129
-
130
- latch.wait(30)
131
- pool.shutdown
137
+ threads.each { |t| t.join(60) }
132
138
  results
133
139
  end
134
140
 
135
- def fetch_remote_version(name)
136
- uri = URI("https://rubygems.org/api/v1/versions/#{name}/latest.json")
137
- response = Net::HTTP.get_response(uri)
138
- return nil unless response.is_a?(Net::HTTPSuccess)
141
+ def fetch_batch(names, results)
142
+ Net::HTTP.start('rubygems.org', 443, use_ssl: true, open_timeout: 10, read_timeout: 10) do |http|
143
+ names.each do |name|
144
+ response = http.request(Net::HTTP::Get.new("/api/v1/versions/#{name}/latest.json"))
145
+ next unless response.is_a?(Net::HTTPSuccess)
139
146
 
140
- data = ::JSON.parse(response.body)
141
- data['version']
147
+ data = ::JSON.parse(response.body)
148
+ results[name] = data['version'] if data['version']
149
+ rescue StandardError => e
150
+ Legion::Logging.debug("UpdateCommand#fetch_batch #{name}: #{e.message}") if defined?(Legion::Logging)
151
+ end
152
+ end
153
+ rescue StandardError => e
154
+ Legion::Logging.debug("UpdateCommand#fetch_batch connection: #{e.message}") if defined?(Legion::Logging)
142
155
  end
143
156
 
144
157
  def display_results(out, results, before, after)
145
158
  updated = []
146
159
  failed = []
160
+ check_failures = 0
147
161
 
148
162
  results.each do |r|
149
163
  name = r[:name]
@@ -152,12 +166,17 @@ module Legion
152
166
  puts " #{name}: #{r[:from]} -> #{r[:to]}"
153
167
  updated << name
154
168
  when 'current'
155
- puts " #{name}: #{r[:from] || '?'} (current)"
156
- when 'updated'
169
+ local = r[:from] || before[name]
170
+ puts " #{name}: #{local || '?'} (already latest)"
171
+ when 'check_failed'
172
+ puts " #{name}: #{before[name]} (remote check failed)"
173
+ check_failures += 1
174
+ when 'installed'
157
175
  old_v = before[name]
158
176
  new_v = after[name]
159
177
  if old_v == new_v
160
- puts " #{name}: #{old_v} (already latest)"
178
+ out.error(" #{name}: #{old_v} (install may have failed)")
179
+ failed << name
161
180
  else
162
181
  out.success(" #{name}: #{old_v} -> #{new_v}")
163
182
  updated << name
@@ -171,6 +190,8 @@ module Legion
171
190
  out.spacer
172
191
  if updated.any?
173
192
  out.success("Updated #{updated.size} gem(s)")
193
+ elsif check_failures.positive?
194
+ puts "#{check_failures} gem(s) could not be checked - retry or use --dry-run for details"
174
195
  else
175
196
  puts 'All gems are up to date'
176
197
  end
@@ -10,6 +10,30 @@ module Legion
10
10
  include Legion::Extensions::Helpers::Logger
11
11
  include Legion::JSON::Helper
12
12
 
13
+ module ClassMethods
14
+ def expose_as_mcp_tool(value = :_unset)
15
+ if value == :_unset
16
+ return @expose_as_mcp_tool unless @expose_as_mcp_tool.nil?
17
+
18
+ if defined?(Legion::Settings) && Legion::Settings.respond_to?(:dig)
19
+ Legion::Settings.dig(:mcp, :auto_expose_runners) || false
20
+ else
21
+ false
22
+ end
23
+ else
24
+ @expose_as_mcp_tool = value
25
+ end
26
+ end
27
+
28
+ def mcp_tool_prefix(value = :_unset)
29
+ if value == :_unset
30
+ @mcp_tool_prefix
31
+ else
32
+ @mcp_tool_prefix = value
33
+ end
34
+ end
35
+ end
36
+
13
37
  def function_example(function, example)
14
38
  function_set(function, :example, example)
15
39
  end
@@ -22,6 +46,34 @@ module Legion
22
46
  function_set(function, :desc, desc)
23
47
  end
24
48
 
49
+ def function_outputs(function, outputs)
50
+ function_set(function, :outputs, outputs)
51
+ end
52
+
53
+ def function_category(function, category)
54
+ function_set(function, :category, category)
55
+ end
56
+
57
+ def function_tags(function, tags)
58
+ function_set(function, :tags, tags)
59
+ end
60
+
61
+ def function_risk_tier(function, tier)
62
+ function_set(function, :risk_tier, tier)
63
+ end
64
+
65
+ def function_idempotent(function, value)
66
+ function_set(function, :idempotent, value)
67
+ end
68
+
69
+ def function_requires(function, deps)
70
+ function_set(function, :requires, deps)
71
+ end
72
+
73
+ def function_expose(function, value)
74
+ function_set(function, :expose, value)
75
+ end
76
+
25
77
  def function_set(function, key, value)
26
78
  unless respond_to? function
27
79
  log.debug "function_#{key} called but function doesn't exist, f: #{function}"
@@ -41,6 +93,7 @@ module Legion
41
93
  def self.included(base)
42
94
  base.send :extend, Legion::Extensions::Helpers::Core if base.instance_of?(Class)
43
95
  base.send :extend, Legion::Extensions::Helpers::Logger if base.instance_of?(Class)
96
+ base.extend ClassMethods if base.instance_of?(Class)
44
97
  base.extend base if base.instance_of?(Module)
45
98
  end
46
99
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.6.0'
4
+ VERSION = '1.6.2'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legionio
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.0
4
+ version: 1.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -323,28 +323,28 @@ dependencies:
323
323
  requirements:
324
324
  - - ">="
325
325
  - !ruby/object:Gem::Version
326
- version: 0.2.1
326
+ version: 0.3.1
327
327
  type: :runtime
328
328
  prerelease: false
329
329
  version_requirements: !ruby/object:Gem::Requirement
330
330
  requirements:
331
331
  - - ">="
332
332
  - !ruby/object:Gem::Version
333
- version: 0.2.1
333
+ version: 0.3.1
334
334
  - !ruby/object:Gem::Dependency
335
335
  name: legion-gaia
336
336
  requirement: !ruby/object:Gem::Requirement
337
337
  requirements:
338
338
  - - ">="
339
339
  - !ruby/object:Gem::Version
340
- version: 0.9.24
340
+ version: 0.9.26
341
341
  type: :runtime
342
342
  prerelease: false
343
343
  version_requirements: !ruby/object:Gem::Requirement
344
344
  requirements:
345
345
  - - ">="
346
346
  - !ruby/object:Gem::Version
347
- version: 0.9.24
347
+ version: 0.9.26
348
348
  - !ruby/object:Gem::Dependency
349
349
  name: legion-llm
350
350
  requirement: !ruby/object:Gem::Requirement
@@ -431,17 +431,9 @@ files:
431
431
  - deploy/helm/legion/values.yaml
432
432
  - docker_deploy.rb
433
433
  - docs/README.md
434
- - docs/plans/2026-03-18-config-import-vault-multicluster-design.md
435
- - docs/plans/2026-03-18-config-import-vault-multicluster-implementation.md
436
- - docs/plans/2026-03-18-core-lex-uplift-design.md
437
- - docs/plans/2026-03-18-core-lex-uplift-implementation.md
438
- - docs/plans/2026-03-18-legion-tty-default-cli-design.md
439
- - docs/plans/2026-03-18-legion-tty-default-cli-implementation.md
440
- - docs/plans/2026-03-19-hooks-expansion-design.md
441
434
  - exe/legion
442
435
  - exe/legionio
443
436
  - legionio.gemspec
444
- - legionio_local.db
445
437
  - lib/legion.rb
446
438
  - lib/legion/alerts.rb
447
439
  - lib/legion/api.rb
@@ -1,272 +0,0 @@
1
- # Config Import + Multi-Cluster Vault Design
2
-
3
- ## Problem
4
-
5
- LegionIO currently supports a single Vault cluster (`crypt.vault.address/port/token`). In enterprise environments, engineers work with multiple Vault clusters (dev, test, stage, production) and need different tokens for each. There's also no way to bootstrap a new developer's environment from a shared config — they must manually create JSON files in `~/.legionio/settings/`.
6
-
7
- ## Solution
8
-
9
- Three changes across three repos:
10
-
11
- ### 1. legion-crypt: Multi-Cluster Vault Support
12
-
13
- Upgrade `crypt.vault` from a single cluster to a named clusters hash with a `default` pointer.
14
-
15
- #### Settings Schema
16
-
17
- ```json
18
- {
19
- "crypt": {
20
- "vault": {
21
- "default": "prod",
22
- "clusters": {
23
- "dev": {
24
- "address": "vault-dev.example.com",
25
- "port": 8200,
26
- "protocol": "https",
27
- "namespace": "myapp",
28
- "token": null,
29
- "auth_method": "ldap"
30
- },
31
- "stage": {
32
- "address": "vault-stage.example.com",
33
- "port": 8200,
34
- "protocol": "https",
35
- "namespace": "myapp",
36
- "token": null,
37
- "auth_method": "ldap"
38
- },
39
- "prod": {
40
- "address": "vault.example.com",
41
- "port": 8200,
42
- "protocol": "https",
43
- "namespace": "myapp",
44
- "token": null,
45
- "auth_method": "ldap"
46
- }
47
- }
48
- }
49
- }
50
- }
51
- ```
52
-
53
- #### Backward Compatibility
54
-
55
- If `crypt.vault.clusters` is absent but `crypt.vault.address` is present, treat it as a single unnamed cluster (current behavior). The migration path is:
56
-
57
- ```ruby
58
- # Old style (still works)
59
- Legion::Settings[:crypt][:vault][:address] # => "vault.example.com"
60
-
61
- # New style
62
- Legion::Crypt.cluster(:prod) # => cluster config hash
63
- Legion::Crypt.cluster # => default cluster config hash
64
- Legion::Crypt.default_cluster # => "prod"
65
- ```
66
-
67
- #### New Module: `Legion::Crypt::VaultCluster`
68
-
69
- Manages per-cluster Vault connections:
70
-
71
- ```ruby
72
- module Legion::Crypt
73
- module VaultCluster
74
- # Get a configured ::Vault client for a named cluster
75
- def vault_client(name = nil)
76
- name ||= default_cluster_name
77
- @vault_clients ||= {}
78
- @vault_clients[name] ||= build_client(clusters[name])
79
- end
80
-
81
- # Cluster config hash
82
- def cluster(name = nil)
83
- name ||= default_cluster_name
84
- clusters[name]
85
- end
86
-
87
- def default_cluster_name
88
- vault_settings[:default] || clusters.keys.first
89
- end
90
-
91
- def clusters
92
- vault_settings[:clusters] || {}
93
- end
94
-
95
- # Connect to all clusters that have tokens
96
- def connect_all
97
- clusters.each do |name, config|
98
- next unless config[:token]
99
- connect_cluster(name)
100
- end
101
- end
102
-
103
- private
104
-
105
- def build_client(config)
106
- client = ::Vault::Client.new(
107
- address: "#{config[:protocol]}://#{config[:address]}:#{config[:port]}",
108
- token: config[:token]
109
- )
110
- client.namespace = config[:namespace] if config[:namespace]
111
- client
112
- end
113
- end
114
- end
115
- ```
116
-
117
- #### New Module: `Legion::Crypt::LdapAuth`
118
-
119
- LDAP authentication against Vault's LDAP auth method (HTTP API, no vault CLI):
120
-
121
- ```ruby
122
- module Legion::Crypt
123
- module LdapAuth
124
- # Authenticate to a single cluster via LDAP
125
- # POST /v1/auth/ldap/login/:username
126
- # Returns: { token:, lease_duration:, renewable:, policies: }
127
- def ldap_login(cluster_name:, username:, password:)
128
- client = vault_client(cluster_name)
129
- # Or raw HTTP if ::Vault gem doesn't expose ldap auth:
130
- response = client.post("/v1/auth/ldap/login/#{username}", password: password)
131
- token = response.auth.client_token
132
- # Store token in cluster config (in-memory only, not written to disk with password)
133
- clusters[cluster_name][:token] = token
134
- clusters[cluster_name][:connected] = true
135
- { token: token, lease_duration: response.auth.lease_duration,
136
- renewable: response.auth.renewable, policies: response.auth.policies }
137
- end
138
-
139
- # Authenticate to ALL configured clusters with same credentials
140
- def ldap_login_all(username:, password:)
141
- results = {}
142
- clusters.each do |name, config|
143
- next unless config[:auth_method] == 'ldap'
144
- results[name] = ldap_login(cluster_name: name, username: username, password: password)
145
- rescue StandardError => e
146
- results[name] = { error: e.message }
147
- end
148
- results
149
- end
150
- end
151
- end
152
- ```
153
-
154
- #### Existing Code Changes
155
-
156
- - `Legion::Crypt.start` — if `clusters` present, call `connect_all` instead of `connect_vault`
157
- - `Legion::Crypt::Vault.read/write/get` — route through `vault_client(name)` for cluster-aware reads
158
- - `Legion::Crypt::Vault.connect_vault` — still works for legacy single-cluster config
159
- - `Legion::Crypt::VaultRenewer` — renew tokens for ALL connected clusters
160
- - `Legion::Settings::Resolver` — `vault://` refs gain optional cluster prefix: `vault://prod/secret/data/myapp#password` (falls back to default cluster if no prefix)
161
-
162
- ### 2. LegionIO: `legion config import` / `legionio config import` CLI Command
163
-
164
- New subcommand under `Config`:
165
-
166
- ```
167
- legionio config import <source> # URL or local file path
168
- legion config import <source> # same command available in interactive binary
169
- ```
170
-
171
- #### Behavior
172
-
173
- 1. **Fetch source:**
174
- - If `source` starts with `http://` or `https://` — HTTP GET, follow redirects
175
- - Otherwise — read local file
176
- 2. **Decode payload:**
177
- - Try `JSON.parse(body)` first
178
- - If that fails, try `JSON.parse(Base64.decode64(body))`
179
- - If both fail, error with "not valid JSON or base64-encoded JSON"
180
- 3. **Validate structure:**
181
- - Must be a Hash
182
- - Warn on unrecognized top-level keys (not in known settings keys)
183
- 4. **Write to `~/.legionio/settings/imported.json`:**
184
- - Deep merge with existing imported.json if present
185
- - Or overwrite with `--force`
186
- 5. **Display summary:**
187
- - Which settings sections were imported (crypt, transport, cache, etc.)
188
- - How many vault clusters configured
189
- - Remind user to run `legion` for onboarding vault auth
190
-
191
- #### Example Config File
192
-
193
- ```json
194
- {
195
- "crypt": {
196
- "vault": {
197
- "default": "prod",
198
- "clusters": {
199
- "dev": { "address": "vault-dev.uhg.com", "port": 8200, "protocol": "https", "auth_method": "ldap" },
200
- "test": { "address": "vault-test.uhg.com", "port": 8200, "protocol": "https", "auth_method": "ldap" },
201
- "stage": { "address": "vault-stage.uhg.com", "port": 8200, "protocol": "https", "auth_method": "ldap" },
202
- "prod": { "address": "vault.uhg.com", "port": 8200, "protocol": "https", "auth_method": "ldap" }
203
- }
204
- }
205
- },
206
- "transport": {
207
- "host": "rabbitmq.uhg.com",
208
- "port": 5672,
209
- "vhost": "legion"
210
- },
211
- "cache": {
212
- "driver": "dalli",
213
- "servers": ["memcached.uhg.com:11211"]
214
- }
215
- }
216
- ```
217
-
218
- ### 3. legion-tty: Onboarding Vault Auth Step
219
-
220
- After the wizard (name + LLM providers), before the reveal box:
221
-
222
- ```
223
- [digital rain]
224
- [intro - kerberos identity, github quick]
225
- [wizard - name, LLM providers]
226
- [NEW: vault auth prompt]
227
- [reveal box - now includes vault cluster status]
228
- ```
229
-
230
- #### Flow
231
-
232
- 1. Check if any vault clusters are configured in settings
233
- 2. If none, skip entirely
234
- 3. If clusters exist, ask: "I found N Vault clusters. Connect now?" (TTY::Prompt confirm)
235
- 4. If yes:
236
- - Default username = kerberos `samaccountname` (from `@kerberos_identity[:samaccountname]`), fallback to `ENV['USER']`
237
- - Ask: "Username:" with default pre-filled (TTY::Prompt ask)
238
- - Ask: "Password:" with `echo: false` (hidden input)
239
- - For each LDAP-configured cluster, attempt `Legion::Crypt.ldap_login`
240
- - Show green checkmark / red X per cluster with name
241
- 5. Store tokens in memory (settings hash), NOT on disk with the password
242
- 6. Reveal box now shows vault cluster connection status
243
-
244
- #### New Background Probe: Not Needed
245
-
246
- Vault auth requires user interaction (password prompt), so it runs inline after the wizard, not in a background thread.
247
-
248
- ## Alternatives Considered
249
-
250
- **Use lex-vault instead of vault gem for multi-cluster:** lex-vault's Faraday-based client is simpler and already supports per-instance address/token/namespace. Could replace the `vault` gem dependency in legion-crypt entirely. Deferred — not a requirement for this iteration but a good future optimization.
251
-
252
- **Kerberos auth for Vault:** Not a default Vault auth method. Would require a custom Vault plugin. Deferred.
253
-
254
- **Store tokens on disk:** Vault tokens are renewable and short-lived. Storing them risks stale tokens. Better to re-auth on each `legion` startup if needed. Could add optional token caching later.
255
-
256
- ## Constraints
257
-
258
- - LDAP password is NEVER written to disk or settings files
259
- - Vault tokens are stored in-memory only during the session
260
- - `vault://` resolver must remain backward compatible (no cluster prefix = default cluster)
261
- - Single-cluster config (`crypt.vault.address`) must continue to work unchanged
262
- - Config import file is plain JSON, no wrapper format
263
- - HTTP sources must handle both raw JSON and base64-encoded JSON
264
-
265
- ## Repos Affected
266
-
267
- | Repo | Changes |
268
- |------|---------|
269
- | `legion-crypt` | `VaultCluster` module, `LdapAuth` module, multi-cluster settings, `VaultRenewer` update, backward compat |
270
- | `LegionIO` | `config import` CLI command (both binaries), HTTP fetch + base64 detection |
271
- | `legion-tty` | Onboarding vault auth step after wizard |
272
- | `legion-settings` | `Resolver` update for cluster-prefixed `vault://` refs (optional, can defer) |