legionio 1.7.25 → 1.7.29

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: d65ff77ca667361b558a97d1da1c3d5d2b1090bb4aad5559532406cd18e781a7
4
- data.tar.gz: 7583822cc4cf804703b8a733be36bb711d1cae70724a49dc98cafea6583268ab
3
+ metadata.gz: 5af5e876210c79ae9fcc876fad3b8086a605f3a841c450793b4759711c5b0101
4
+ data.tar.gz: 43d1c51c4a52121654e1a1e426f5bbe9e6e1771a5a95a095eb12e87474632eec
5
5
  SHA512:
6
- metadata.gz: 06f7712d8c9c6e944890ba2c55efbdf3463a6c39e906017cede6d3401215de9144e7c916c39c8b981e8f6ea6646a5439bcc10a399443fe7642b3ab06e3ef2c7a
7
- data.tar.gz: 5e5819fc7973e9cb6de4b64e88c40b74d033fd666c4b9585726a3b1e87b9468f6cf4759fed226a9c3a7bc755a10b9267403dcd001d2c10f08b7aaf36e6264812
6
+ metadata.gz: af956fad95ae9de341f700fa1e03997bed9b889983dc69b96322ba72cb7871f584224b4d00112e17b2e580a9f37e1afff3fa96e9b5bb5e83a38cf487fad26b77
7
+ data.tar.gz: 6545603e7df04444096ae7763fcfde3ee4b970603554388ab04be50a088bd36ac1f77c0fc5ce02f04eeaf5396bb5620d38fc6416f5125b900225a3f79ecc773a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Legion Changelog
2
2
 
3
+ ## [1.7.29] - 2026-04-07
4
+
5
+ ### Changed
6
+ - Skip secret resolution for all CLI commands that only need local settings: `config`, `mode`, `lex`, `doctor`, `auth`, `marketplace`, `debug`, `failover status` — eliminates noisy Vault/lease warnings on local-only operations
7
+
8
+ ## [1.7.28] - 2026-04-07
9
+
10
+ ### Fixed
11
+ - `legionio setup` pack marker and packs.json writes now rescue `Errno::EPERM`/`EACCES`, fixing Homebrew post-install crash when sandbox blocks writes to `~/.legionio/`
12
+
13
+ ## [1.7.27] - 2026-04-07
14
+
15
+ ### Changed
16
+ - `Connection.ensure_settings` accepts `resolve_secrets:` keyword (default `true`) to skip Vault/lease resolution for CLI commands that don't need infrastructure credentials
17
+ - `legionio update` now skips secret resolution, eliminating noisy "Vault not connected" and "LeaseManager not available" warnings
18
+
19
+ ## [1.7.26] - 2026-04-07
20
+
21
+ ### Added
22
+ - Phase 5 Credential Scoping — service.rb integration (§8 of `docs/plans/2026-04-07-credential-scoping-design.md`)
23
+ - Boot: call `Legion::Crypt.fetch_bootstrap_rmq_creds` after `Crypt.start` to acquire short-lived bootstrap RMQ credentials from Vault before transport connects (no-op when `dynamic_rmq_creds: false`)
24
+ - `setup_identity`: after identity resolves, call `Legion::Crypt.swap_to_identity_creds(mode:)` to swap from bootstrap to identity-scoped RMQ credentials — gated on `vault_connected? && dynamic_rmq_creds? && !lite?`; fallback identity still gets scoped creds
25
+ - `shutdown`: call `Legion::Crypt.revoke_bootstrap_lease` before Crypt shutdown for defense-in-depth lease cleanup
26
+ - `reload`: call `fetch_bootstrap_rmq_creds` after Crypt.start, `resolve_secrets!` after settings reload, and `setup_identity` (replacing static `mark_ready(:identity)`) so reloaded processes acquire identity-scoped credentials
27
+ - Specs for all Phase 5 service.rb integration paths: boot credential fetch, identity swap per mode, vault/flag/lite guards, swap failure recovery, shutdown revocation, reload credential flow
28
+
3
29
  ## [1.7.25] - 2026-04-06
4
30
 
5
31
  ### Added
@@ -20,7 +20,7 @@ module Legion
20
20
  method_option :scopes, type: :string, desc: 'OAuth scopes to request'
21
21
  def teams
22
22
  out = formatter
23
- Connection.ensure_settings
23
+ Connection.ensure_settings(resolve_secrets: false)
24
24
 
25
25
  port = begin
26
26
  Legion::Settings.dig(:api, :port) || 4567
@@ -18,7 +18,7 @@ module Legion
18
18
  def show
19
19
  out = formatter
20
20
  Connection.config_dir = options[:config_dir] if options[:config_dir]
21
- Connection.ensure_settings
21
+ Connection.ensure_settings(resolve_secrets: false)
22
22
 
23
23
  settings = if Legion::Settings.respond_to?(:to_hash)
24
24
  Legion::Settings.to_hash
@@ -110,7 +110,7 @@ module Legion
110
110
 
111
111
  # Check settings load
112
112
  begin
113
- Connection.ensure_settings
113
+ Connection.ensure_settings(resolve_secrets: false)
114
114
  out.success('Settings loaded successfully') unless options[:json]
115
115
  rescue StandardError => e
116
116
  issues << "Settings failed to load: #{e.message}"
@@ -23,7 +23,7 @@ module Legion
23
23
  @logging_ready = true
24
24
  end
25
25
 
26
- def ensure_settings
26
+ def ensure_settings(resolve_secrets: true)
27
27
  return if @settings_ready
28
28
 
29
29
  ensure_logging
@@ -31,7 +31,7 @@ module Legion
31
31
 
32
32
  dir = resolve_config_dir
33
33
  Legion::Settings.load(config_dir: dir)
34
- Legion::Settings.resolve_secrets! if Legion::Settings.respond_to?(:resolve_secrets!)
34
+ Legion::Settings.resolve_secrets! if resolve_secrets && Legion::Settings.respond_to?(:resolve_secrets!)
35
35
  @settings_ready = true
36
36
  end
37
37
 
@@ -98,7 +98,7 @@ module Legion
98
98
  def load_settings
99
99
  Connection.config_dir = options[:config_dir] if options[:config_dir]
100
100
  Connection.log_level = 'error'
101
- Connection.ensure_settings
101
+ Connection.ensure_settings(resolve_secrets: false)
102
102
  rescue StandardError
103
103
  nil
104
104
  end
@@ -70,7 +70,7 @@ module Legion
70
70
  def diagnose
71
71
  out = formatter
72
72
  begin
73
- Connection.ensure_settings
73
+ Connection.ensure_settings(resolve_secrets: false)
74
74
  rescue StandardError => e
75
75
  Legion::Logging.debug("Doctor#diagnose settings load failed: #{e.message}") if defined?(Legion::Logging)
76
76
  end
@@ -72,7 +72,7 @@ module Legion
72
72
  private
73
73
 
74
74
  def ensure_settings
75
- Connection.ensure_settings
75
+ Connection.ensure_settings(resolve_secrets: false)
76
76
  end
77
77
 
78
78
  def run_dry_run(out, target)
@@ -160,7 +160,7 @@ module Legion
160
160
  desc 'enable NAME', 'Enable an extension in settings'
161
161
  def enable(name)
162
162
  out = formatter
163
- Connection.ensure_settings
163
+ Connection.ensure_settings(resolve_secrets: false)
164
164
 
165
165
  extensions = Legion::Settings[:extensions] || {}
166
166
  if extensions.key?(name.to_sym)
@@ -176,7 +176,7 @@ module Legion
176
176
  desc 'disable NAME', 'Disable an extension in settings'
177
177
  def disable(name)
178
178
  out = formatter
179
- Connection.ensure_settings
179
+ Connection.ensure_settings(resolve_secrets: false)
180
180
 
181
181
  extensions = Legion::Settings[:extensions] || {}
182
182
  if extensions.key?(name.to_sym)
@@ -349,7 +349,7 @@ module Legion
349
349
 
350
350
  # Load settings to check enabled/disabled state
351
351
  begin
352
- Connection.ensure_settings
352
+ Connection.ensure_settings(resolve_secrets: false)
353
353
  ext_settings = Legion::Settings[:extensions] || {}
354
354
  rescue StandardError => e
355
355
  Legion::Logging.warn("LexCommand#discover_all settings load failed: #{e.message}") if defined?(Legion::Logging)
@@ -248,7 +248,7 @@ module Legion
248
248
  end
249
249
 
250
250
  begin
251
- Connection.ensure_settings
251
+ Connection.ensure_settings(resolve_secrets: false)
252
252
  Legion::Extensions::GemSource.setup!
253
253
  rescue StandardError => e
254
254
  Legion::Logging.debug("marketplace install: settings not available: #{e.message}") if defined?(Legion::Logging)
@@ -31,7 +31,7 @@ module Legion
31
31
  desc 'show', 'Show current process role and extension profile'
32
32
  def show
33
33
  out = formatter
34
- Connection.ensure_settings
34
+ Connection.ensure_settings(resolve_secrets: false)
35
35
 
36
36
  process_role = Legion::ProcessRole.current
37
37
  profile = Legion::Settings.dig(:role, :profile)&.to_s || '(none — all extensions load)'
@@ -55,7 +55,7 @@ module Legion
55
55
  desc 'list', 'List available extension profiles and process roles'
56
56
  def list
57
57
  out = formatter
58
- Connection.ensure_settings
58
+ Connection.ensure_settings(resolve_secrets: false)
59
59
 
60
60
  if options[:json]
61
61
  out.json({ profiles: PROFILE_DESCRIPTIONS, process_roles: Legion::ProcessRole::ROLES.keys })
@@ -95,7 +95,7 @@ module Legion
95
95
  option :reload, type: :boolean, default: false, desc: 'Trigger daemon reload after writing config'
96
96
  def set(profile = nil)
97
97
  out = formatter
98
- Connection.ensure_settings
98
+ Connection.ensure_settings(resolve_secrets: false)
99
99
 
100
100
  validate_inputs!(out, profile)
101
101
 
@@ -304,6 +304,8 @@ module Legion
304
304
  marker = File.join(marker_dir, pack_name.to_s)
305
305
  File.write(marker, '') unless File.exist?(marker)
306
306
  update_packs_setting(pack_name)
307
+ rescue Errno::EPERM, Errno::EACCES => e
308
+ Legion::Logging.warn("Could not write pack marker: #{e.message}") if defined?(Legion::Logging)
307
309
  end
308
310
 
309
311
  def update_packs_setting(pack_name)
@@ -318,6 +320,8 @@ module Legion
318
320
  data['packs'] = packs.sort
319
321
  FileUtils.mkdir_p(File.dirname(settings_file))
320
322
  File.write(settings_file, ::JSON.pretty_generate(data))
323
+ rescue Errno::EPERM, Errno::EACCES => e
324
+ Legion::Logging.warn("Could not update packs setting: #{e.message}") if defined?(Legion::Logging)
321
325
  rescue ::JSON::ParserError
322
326
  data = { 'packs' => [pack_name.to_s] }
323
327
  File.write(settings_file, ::JSON.pretty_generate(data))
@@ -31,7 +31,7 @@ module Legion
31
31
  raise SystemExit, 1
32
32
  end
33
33
 
34
- Connection.ensure_settings
34
+ Connection.ensure_settings(resolve_secrets: false)
35
35
  Legion::Extensions::GemSource.setup!
36
36
 
37
37
  target_gems = discover_legion_gems
@@ -56,6 +56,9 @@ module Legion
56
56
  Legion::Crypt.start
57
57
  Legion::Readiness.mark_ready(:crypt)
58
58
  setup_mtls_rotation
59
+ # Phase 5: fetch short-lived bootstrap RMQ creds from Vault before transport connects.
60
+ # Service is the authoritative gate (vault_connected? + dynamic_rmq_creds?).
61
+ fetch_phase5_bootstrap_creds unless Legion::Mode.respond_to?(:lite?) && Legion::Mode.lite?
59
62
  end
60
63
 
61
64
  Legion::Settings.resolve_secrets!
@@ -480,7 +483,7 @@ module Legion
480
483
  log.info 'Legion::Transport connected'
481
484
  end
482
485
 
483
- def setup_identity
486
+ def setup_identity # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
484
487
  require_relative 'identity/process'
485
488
  require_relative 'identity/broker'
486
489
  require_relative 'identity/lease'
@@ -495,6 +498,18 @@ module Legion
495
498
  log.info "[Identity] fallback identity: #{Legion::Identity::Process.canonical_name}"
496
499
  end
497
500
 
501
+ # Phase 5: Swap from bootstrap RMQ credentials to identity-scoped credentials.
502
+ # Gate on vault_connected? + dynamic_rmq_creds? — NOT on resolved? (fallback identity
503
+ # still needs scoped creds via the mode-based role).
504
+ if defined?(Legion::Crypt) &&
505
+ Legion::Crypt.respond_to?(:vault_connected?) && Legion::Crypt.vault_connected? &&
506
+ Legion::Crypt.respond_to?(:dynamic_rmq_creds?) && Legion::Crypt.dynamic_rmq_creds? &&
507
+ Legion::Crypt.respond_to?(:swap_to_identity_creds) &&
508
+ !Legion::Mode.lite?
509
+ log.info '[Identity] swapping to identity-scoped RMQ credentials'
510
+ Legion::Crypt.swap_to_identity_creds(mode: Legion::Mode.current)
511
+ end
512
+
498
513
  # Re-resolve secrets for any identity-scoped lease:// refs (task 2.25)
499
514
  Legion::Settings.resolve_secrets! if Legion::Settings.respond_to?(:resolve_secrets!)
500
515
 
@@ -702,7 +717,7 @@ module Legion
702
717
  handle_exception(e, level: :warn, operation: 'service.shutdown_api')
703
718
  end
704
719
 
705
- def shutdown # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
720
+ def shutdown # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize, Metrics/MethodLength
706
721
  log.info('Legion::Service.shutdown was called')
707
722
  @shutdown = true
708
723
  Legion::Settings[:client][:shutting_down] = true
@@ -767,7 +782,12 @@ module Legion
767
782
  Legion::Readiness.mark_not_ready(:transport)
768
783
 
769
784
  shutdown_mtls_rotation
770
- shutdown_component('Crypt') { Legion::Crypt.shutdown }
785
+ # Phase 5: Revoke bootstrap RMQ lease on clean shutdown (defense-in-depth;
786
+ # lease expires naturally if process crashes before identity swap).
787
+ shutdown_component('Crypt bootstrap lease') do
788
+ Legion::Crypt.revoke_bootstrap_lease if defined?(Legion::Crypt) && Legion::Crypt.respond_to?(:revoke_bootstrap_lease)
789
+ end
790
+ shutdown_component('Crypt') { Legion::Crypt.shutdown if defined?(Legion::Crypt) }
771
791
  Legion::Readiness.mark_not_ready(:crypt)
772
792
 
773
793
  Legion::Settings[:client][:ready] = false
@@ -807,7 +827,7 @@ module Legion
807
827
  shutdown_component('Transport') { Legion::Transport::Connection.shutdown }
808
828
  Legion::Readiness.mark_not_ready(:transport)
809
829
 
810
- shutdown_component('Crypt') { Legion::Crypt.shutdown }
830
+ shutdown_component('Crypt') { Legion::Crypt.shutdown if defined?(Legion::Crypt) }
811
831
  Legion::Readiness.mark_not_ready(:crypt)
812
832
 
813
833
  Legion::Readiness.wait_until_not_ready(:transport, :data, :cache, :crypt)
@@ -816,7 +836,12 @@ module Legion
816
836
  Legion::Readiness.mark_ready(:settings)
817
837
 
818
838
  Legion::Crypt.start if defined?(Legion::Crypt)
819
- Legion::Readiness.mark_ready(:crypt)
839
+ Legion::Readiness.mark_ready(:crypt) if defined?(Legion::Crypt)
840
+ # Phase 5: fetch bootstrap RMQ creds after Vault reconnects on reload.
841
+ fetch_phase5_bootstrap_creds unless Legion::Mode.lite?
842
+
843
+ # Resolve lease:// URIs with freshly loaded settings + new Vault token.
844
+ Legion::Settings.resolve_secrets! if Legion::Settings.respond_to?(:resolve_secrets!)
820
845
 
821
846
  setup_transport
822
847
  Legion::Readiness.mark_ready(:transport)
@@ -858,7 +883,9 @@ module Legion
858
883
  Legion::Readiness.mark_skipped(:gaia)
859
884
  end
860
885
 
861
- Legion::Readiness.mark_ready(:identity)
886
+ # Phase 5: re-run identity resolution + credential swap so the reloaded
887
+ # process gets identity-scoped RMQ creds (not stale bootstrap creds).
888
+ setup_identity
862
889
 
863
890
  setup_supervision
864
891
  load_extensions
@@ -868,7 +895,7 @@ module Legion
868
895
 
869
896
  register_core_tools
870
897
 
871
- Legion::Crypt.cs
898
+ Legion::Crypt.cs if defined?(Legion::Crypt)
872
899
  setup_apm if @api_enabled
873
900
  setup_api if @api_enabled
874
901
 
@@ -1027,6 +1054,18 @@ module Legion
1027
1054
 
1028
1055
  private
1029
1056
 
1057
+ # Phase 5: fetch short-lived bootstrap RMQ credentials from Vault.
1058
+ # Called after Crypt.start (boot) and after Crypt.start (reload).
1059
+ # Service owns the gate so Crypt.fetch_bootstrap_rmq_creds can be unconditional.
1060
+ def fetch_phase5_bootstrap_creds
1061
+ return unless defined?(Legion::Crypt)
1062
+ return unless Legion::Crypt.respond_to?(:fetch_bootstrap_rmq_creds)
1063
+ return unless Legion::Crypt.respond_to?(:vault_connected?) && Legion::Crypt.vault_connected?
1064
+ return unless Legion::Crypt.respond_to?(:dynamic_rmq_creds?) && Legion::Crypt.dynamic_rmq_creds?
1065
+
1066
+ Legion::Crypt.fetch_bootstrap_rmq_creds
1067
+ end
1068
+
1030
1069
  def resolve_identity_providers
1031
1070
  # Phase 4 adds lex-identity-* providers. For now, check if any are loaded.
1032
1071
  return false unless defined?(Legion::Extensions)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.7.25'
4
+ VERSION = '1.7.29'
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.7.25
4
+ version: 1.7.29
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity