legionio 1.7.19 → 1.7.21

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.
@@ -3,6 +3,7 @@
3
3
  require 'timeout'
4
4
  require 'legion/logging'
5
5
  require_relative 'readiness'
6
+ require_relative 'mode'
6
7
  require_relative 'process_role'
7
8
 
8
9
  module Legion
@@ -101,7 +102,11 @@ module Legion
101
102
  end
102
103
  end
103
104
 
104
- setup_rbac if data
105
+ if data
106
+ setup_rbac
107
+ else
108
+ Legion::Readiness.mark_skipped(:rbac)
109
+ end
105
110
  setup_cluster if data
106
111
 
107
112
  if llm
@@ -111,9 +116,13 @@ module Legion
111
116
  rescue LoadError => e
112
117
  handle_exception(e, level: :debug, operation: 'service.initialize.llm', availability: 'missing')
113
118
  log.info 'Legion::LLM gem is not installed'
119
+ Legion::Readiness.mark_skipped(:llm)
114
120
  rescue StandardError => e
115
121
  handle_exception(e, level: :warn, operation: 'service.initialize.llm')
122
+ Legion::Readiness.mark_skipped(:llm)
116
123
  end
124
+ else
125
+ Legion::Readiness.mark_skipped(:llm)
117
126
  end
118
127
 
119
128
  begin
@@ -122,8 +131,10 @@ module Legion
122
131
  rescue LoadError => e
123
132
  handle_exception(e, level: :debug, operation: 'service.initialize.apollo', availability: 'missing')
124
133
  log.info 'Legion::Apollo gem is not installed, starting without Apollo'
134
+ Legion::Readiness.mark_skipped(:apollo)
125
135
  rescue StandardError => e
126
136
  handle_exception(e, level: :warn, operation: 'service.initialize.apollo')
137
+ Legion::Readiness.mark_skipped(:apollo)
127
138
  end
128
139
 
129
140
  if gaia
@@ -133,9 +144,13 @@ module Legion
133
144
  rescue LoadError => e
134
145
  handle_exception(e, level: :debug, operation: 'service.initialize.gaia', availability: 'missing')
135
146
  log.info 'Legion::Gaia gem is not installed'
147
+ Legion::Readiness.mark_skipped(:gaia)
136
148
  rescue StandardError => e
137
149
  handle_exception(e, level: :warn, operation: 'service.initialize.gaia')
150
+ Legion::Readiness.mark_skipped(:gaia)
138
151
  end
152
+ else
153
+ Legion::Readiness.mark_skipped(:gaia)
139
154
  end
140
155
 
141
156
  setup_telemetry
@@ -149,6 +164,9 @@ module Legion
149
164
  setup_generated_functions
150
165
  end
151
166
 
167
+ # Identity resolution — after extensions so lex-identity-* providers are loaded
168
+ setup_identity if transport
169
+
152
170
  register_core_tools
153
171
 
154
172
  Legion::Gaia.registry&.rediscover if gaia && defined?(Legion::Gaia) && Legion::Gaia.started?
@@ -174,6 +192,7 @@ module Legion
174
192
  require 'legion/api/default_settings'
175
193
  api_settings = Legion::Settings[:api]
176
194
  @api_enabled = api && api_settings[:enabled]
195
+ setup_apm if @api_enabled
177
196
  setup_api if @api_enabled
178
197
  setup_network_watchdog
179
198
  Legion::Settings[:client][:ready] = true
@@ -204,8 +223,7 @@ module Legion
204
223
  end
205
224
 
206
225
  def lite_mode?
207
- ENV['LEGION_MODE'] == 'lite' ||
208
- Legion::Settings[:mode].to_s == 'lite'
226
+ Legion::Mode.lite?
209
227
  end
210
228
 
211
229
  def setup_data
@@ -229,8 +247,10 @@ module Legion
229
247
  rescue LoadError => e
230
248
  handle_exception(e, level: :debug, operation: 'service.setup_rbac', availability: 'missing')
231
249
  log.debug 'Legion::Rbac gem is not installed, starting without RBAC'
250
+ Legion::Readiness.mark_skipped(:rbac)
232
251
  rescue StandardError => e
233
252
  handle_exception(e, level: :warn, operation: 'service.setup_rbac')
253
+ Legion::Readiness.mark_skipped(:rbac)
234
254
  end
235
255
 
236
256
  def setup_cluster
@@ -306,6 +326,42 @@ module Legion
306
326
  )
307
327
  end
308
328
 
329
+ def setup_apm
330
+ apm_settings = Legion::Settings[:apm] || {}
331
+ return unless apm_settings[:enabled]
332
+
333
+ require 'elastic-apm'
334
+
335
+ config = {
336
+ service_name: apm_settings[:service_name] || "legion-#{Legion::Settings[:client][:name]}",
337
+ server_url: apm_settings[:server_url] || 'http://localhost:8200',
338
+ environment: apm_settings[:environment] || Legion::Settings[:environment] || 'development',
339
+ secret_token: apm_settings[:secret_token],
340
+ api_key: apm_settings[:api_key],
341
+ log_level: apm_settings[:log_level]&.to_sym || Logger::WARN,
342
+ transaction_sample_rate: apm_settings[:sample_rate] || 1.0
343
+ }.compact
344
+
345
+ ElasticAPM.start(**config)
346
+ @apm_running = true
347
+ log.info "Elastic APM started: server=#{config[:server_url]} service=#{config[:service_name]}"
348
+ rescue LoadError => e
349
+ handle_exception(e, level: :debug, operation: 'service.setup_apm', availability: 'missing')
350
+ log.info 'elastic-apm gem is not installed, starting without APM'
351
+ rescue StandardError => e
352
+ handle_exception(e, level: :warn, operation: 'service.setup_apm')
353
+ end
354
+
355
+ def shutdown_apm
356
+ return unless @apm_running
357
+
358
+ ElasticAPM.stop if defined?(ElasticAPM) && ElasticAPM.running?
359
+ @apm_running = false
360
+ log.info 'Elastic APM stopped'
361
+ rescue StandardError => e
362
+ handle_exception(e, level: :warn, operation: 'service.shutdown_apm')
363
+ end
364
+
309
365
  def setup_api # rubocop:disable Metrics/MethodLength
310
366
  if @api_thread&.alive?
311
367
  log.warn 'API already running, skipping duplicate setup_api call'
@@ -344,6 +400,12 @@ module Legion
344
400
  log.info "Starting Legion API on #{bind}:#{port}"
345
401
  end
346
402
 
403
+ # Mount identity middleware — bridges legion.auth to legion.principal
404
+ if defined?(Legion::Identity::Middleware)
405
+ require_auth = Legion::Identity::Middleware.require_auth?(bind: bind, mode: Legion::Mode.current)
406
+ Legion::API.use Legion::Identity::Middleware, require_auth: require_auth
407
+ end
408
+
347
409
  @api_thread = Thread.new do
348
410
  retries = 0
349
411
  max_retries = api_settings[:bind_retries]
@@ -427,6 +489,45 @@ module Legion
427
489
  log.info 'Legion::Transport connected'
428
490
  end
429
491
 
492
+ def setup_identity
493
+ require_relative 'identity/process'
494
+ require_relative 'identity/broker'
495
+ require_relative 'identity/lease'
496
+ require_relative 'identity/lease_renewer'
497
+ require_relative 'identity/request'
498
+ require_relative 'identity/middleware'
499
+
500
+ # Resolve identity from available providers (Phase 4 adds real providers)
501
+ resolved = resolve_identity_providers
502
+ unless resolved
503
+ Legion::Identity::Process.bind_fallback!
504
+ log.info "[Identity] fallback identity: #{Legion::Identity::Process.canonical_name}"
505
+ end
506
+
507
+ # Re-resolve secrets for any identity-scoped lease:// refs (task 2.25)
508
+ Legion::Settings.resolve_secrets! if Legion::Settings.respond_to?(:resolve_secrets!)
509
+
510
+ # Fire-and-forget JWKS prefetch
511
+ jwks_url = Legion::Settings.dig(:identity, :jwks_endpoint) || Legion::Settings.dig(:crypt, :jwt, :jwks_endpoint)
512
+ if jwks_url && defined?(Legion::Crypt::JwksClient)
513
+ Legion::Crypt::JwksClient.prefetch!(jwks_url)
514
+ Legion::Crypt::JwksClient.start_background_refresh!(jwks_url)
515
+ end
516
+
517
+ log.info "[Identity] resolved=#{Legion::Identity::Process.resolved?} mode=#{Legion::Mode.current} queue_prefix=#{Legion::Identity::Process.queue_prefix}"
518
+ rescue StandardError => e
519
+ handle_exception(e, level: :warn, operation: 'service.setup_identity')
520
+ Legion::Identity::Process.bind_fallback! if defined?(Legion::Identity::Process) && !Legion::Identity::Process.resolved?
521
+ ensure
522
+ Legion::Readiness.mark_ready(:identity)
523
+ begin
524
+ Legion::Extensions.flush_pending_registrations! if defined?(Legion::Extensions) &&
525
+ Legion::Extensions.respond_to?(:flush_pending_registrations!)
526
+ rescue StandardError => e
527
+ handle_exception(e, level: :warn, operation: 'service.setup_identity.flush_pending_registrations')
528
+ end
529
+ end
530
+
430
531
  def setup_logging_transport # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
431
532
  return unless defined?(Legion::Transport::Connection)
432
533
  return unless Legion::Transport::Connection.session_open?
@@ -610,7 +711,7 @@ module Legion
610
711
  handle_exception(e, level: :warn, operation: 'service.shutdown_api')
611
712
  end
612
713
 
613
- def shutdown # rubocop:disable Metrics/CyclomaticComplexity
714
+ def shutdown # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
614
715
  log.info('Legion::Service.shutdown was called')
615
716
  @shutdown = true
616
717
  Legion::Settings[:client][:shutting_down] = true
@@ -619,6 +720,7 @@ module Legion
619
720
  shutdown_network_watchdog
620
721
  shutdown_audit_archiver
621
722
  shutdown_api
723
+ shutdown_apm
622
724
 
623
725
  Legion::Metrics.reset! if defined?(Legion::Metrics)
624
726
 
@@ -658,6 +760,17 @@ module Legion
658
760
  shutdown_component('Cache') { Legion::Cache.shutdown }
659
761
  Legion::Readiness.mark_not_ready(:cache)
660
762
 
763
+ # Identity: cooperative shutdown of Broker (stops all LeaseRenewer threads)
764
+ if defined?(Legion::Identity::Broker)
765
+ shutdown_component('Identity::Broker') { Legion::Identity::Broker.shutdown }
766
+ Legion::Readiness.mark_not_ready(:identity)
767
+ end
768
+
769
+ # Stop JWKS background refresh
770
+ if defined?(Legion::Crypt::JwksClient) && Legion::Crypt::JwksClient.respond_to?(:stop_background_refresh!)
771
+ Legion::Crypt::JwksClient.stop_background_refresh!
772
+ end
773
+
661
774
  teardown_logging_transport
662
775
  shutdown_component('Transport') { Legion::Transport::Connection.shutdown }
663
776
  Legion::Readiness.mark_not_ready(:transport)
@@ -679,6 +792,7 @@ module Legion
679
792
 
680
793
  shutdown_network_watchdog
681
794
  shutdown_api
795
+ shutdown_apm
682
796
 
683
797
  if defined?(Legion::Gaia) && Legion::Gaia.respond_to?(:started?) && Legion::Gaia.started?
684
798
  shutdown_component('Gaia') { Legion::Gaia.shutdown }
@@ -718,6 +832,8 @@ module Legion
718
832
  teardown_logging_transport
719
833
  setup_logging_transport
720
834
 
835
+ Legion::Identity::Process.refresh_credentials if defined?(Legion::Identity::Process)
836
+
721
837
  require 'legion/cache' unless defined?(Legion::Cache)
722
838
  Legion::Cache.setup
723
839
  Legion::Readiness.mark_ready(:cache)
@@ -725,19 +841,44 @@ module Legion
725
841
  setup_data
726
842
  Legion::Readiness.mark_ready(:data)
727
843
 
728
- setup_rbac if defined?(Legion::Rbac)
729
- setup_llm if defined?(Legion::LLM)
844
+ if defined?(Legion::Rbac)
845
+ setup_rbac
846
+ else
847
+ Legion::Readiness.mark_skipped(:rbac)
848
+ end
730
849
 
731
- setup_gaia if defined?(Legion::Gaia)
732
- Legion::Readiness.mark_ready(:gaia)
850
+ if defined?(Legion::LLM)
851
+ setup_llm
852
+ else
853
+ Legion::Readiness.mark_skipped(:llm)
854
+ end
855
+
856
+ if defined?(Legion::Apollo)
857
+ setup_apollo
858
+ Legion::Readiness.mark_ready(:apollo)
859
+ else
860
+ Legion::Readiness.mark_skipped(:apollo)
861
+ end
862
+
863
+ if defined?(Legion::Gaia)
864
+ setup_gaia
865
+ Legion::Readiness.mark_ready(:gaia)
866
+ else
867
+ Legion::Readiness.mark_skipped(:gaia)
868
+ end
869
+
870
+ Legion::Readiness.mark_ready(:identity)
733
871
 
734
872
  setup_supervision
735
873
  load_extensions
736
874
  Legion::Readiness.mark_ready(:extensions)
737
875
 
876
+ Legion::Extensions.flush_pending_registrations! if defined?(Legion::Extensions) && Legion::Extensions.respond_to?(:flush_pending_registrations!)
877
+
738
878
  register_core_tools
739
879
 
740
880
  Legion::Crypt.cs
881
+ setup_apm if @api_enabled
741
882
  setup_api if @api_enabled
742
883
 
743
884
  if defined?(Legion::MCP)
@@ -895,6 +1036,72 @@ module Legion
895
1036
 
896
1037
  private
897
1038
 
1039
+ def resolve_identity_providers
1040
+ # Phase 4 adds lex-identity-* providers. For now, check if any are loaded.
1041
+ return false unless defined?(Legion::Extensions)
1042
+
1043
+ providers = find_identity_providers
1044
+ return false if providers.empty?
1045
+
1046
+ # Parallel resolution with 5s per-provider timeout (NO Timeout.timeout — uses future.value)
1047
+ pool = Concurrent::FixedThreadPool.new([providers.size, 4].min)
1048
+ futures = providers.map do |provider|
1049
+ Concurrent::Promises.future_on(pool, provider, &:resolve)
1050
+ end
1051
+
1052
+ winner_pair = providers.zip(futures).find do |_provider, future|
1053
+ result = begin
1054
+ future.value(5) # 5s timeout per provider
1055
+ rescue StandardError => e
1056
+ handle_exception(e, level: :debug, operation: 'service.resolve_identity_providers.future')
1057
+ nil
1058
+ end
1059
+ result.is_a?(Hash) && result[:canonical_name]
1060
+ end
1061
+
1062
+ if winner_pair
1063
+ provider, future = winner_pair
1064
+ identity = future.value
1065
+ Legion::Identity::Process.bind!(provider, identity)
1066
+ log.info "[Identity] resolved via #{provider.class.name}: #{identity[:canonical_name]}"
1067
+ true
1068
+ else
1069
+ false
1070
+ end
1071
+ rescue StandardError => e
1072
+ handle_exception(e, level: :warn, operation: 'service.resolve_identity_providers')
1073
+ false
1074
+ ensure
1075
+ pool&.shutdown
1076
+ pool&.kill unless pool&.wait_for_termination(2)
1077
+ end
1078
+
1079
+ def find_identity_providers
1080
+ return [] unless defined?(Legion::Extensions)
1081
+
1082
+ collect_identity_providers(Legion::Extensions)
1083
+ end
1084
+
1085
+ def collect_identity_providers(namespace, visited = Set.new)
1086
+ return [] unless namespace.is_a?(Module)
1087
+ return [] if visited.include?(namespace.object_id)
1088
+
1089
+ visited.add(namespace.object_id)
1090
+ providers = []
1091
+
1092
+ namespace.constants(false).each do |const_name|
1093
+ mod = namespace.const_get(const_name, false)
1094
+ next unless mod.is_a?(Module)
1095
+
1096
+ providers << mod if mod.respond_to?(:resolve) && mod.respond_to?(:provider_name)
1097
+ providers.concat(collect_identity_providers(mod, visited))
1098
+ rescue StandardError
1099
+ next
1100
+ end
1101
+
1102
+ providers
1103
+ end
1104
+
898
1105
  def bootstrap_log_level(cli_level)
899
1106
  cli_level = nil if cli_level.respond_to?(:empty?) && cli_level.empty?
900
1107
  return cli_level if cli_level
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.7.19'
4
+ VERSION = '1.7.21'
5
5
  end
data/lib/legion.rb CHANGED
@@ -6,6 +6,7 @@ require 'securerandom'
6
6
  require 'legion/version'
7
7
  require 'legion/logging'
8
8
  require 'legion/events'
9
+ require 'legion/mode'
9
10
  require 'legion/ingress'
10
11
  require 'legion/process'
11
12
  require 'legion/service'
@@ -18,6 +19,12 @@ module Legion
18
19
  autoload :Leader, 'legion/leader'
19
20
  autoload :Prompts, 'legion/prompts'
20
21
 
22
+ @instance_id = ENV.fetch('LEGIONIO_INSTANCE_ID') { SecureRandom.uuid }.downcase.strip.gsub(/[^a-z0-9-]/, '')
23
+
24
+ def self.instance_id
25
+ @instance_id
26
+ end
27
+
21
28
  attr_reader :service
22
29
 
23
30
  def self.start
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.19
4
+ version: 1.7.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -490,6 +490,7 @@ files:
490
490
  - lib/legion/api/graphql/types/task_type.rb
491
491
  - lib/legion/api/graphql/types/worker_type.rb
492
492
  - lib/legion/api/helpers.rb
493
+ - lib/legion/api/identity_audit.rb
493
494
  - lib/legion/api/inbound_webhooks.rb
494
495
  - lib/legion/api/knowledge.rb
495
496
  - lib/legion/api/lex_dispatch.rb
@@ -641,11 +642,13 @@ files:
641
642
  - lib/legion/cli/do_command.rb
642
643
  - lib/legion/cli/docs_command.rb
643
644
  - lib/legion/cli/doctor.rb
645
+ - lib/legion/cli/doctor/api_bind_check.rb
644
646
  - lib/legion/cli/doctor/bundle_check.rb
645
647
  - lib/legion/cli/doctor/cache_check.rb
646
648
  - lib/legion/cli/doctor/config_check.rb
647
649
  - lib/legion/cli/doctor/database_check.rb
648
650
  - lib/legion/cli/doctor/extensions_check.rb
651
+ - lib/legion/cli/doctor/mode_check.rb
649
652
  - lib/legion/cli/doctor/permissions_check.rb
650
653
  - lib/legion/cli/doctor/pid_check.rb
651
654
  - lib/legion/cli/doctor/rabbitmq_check.rb
@@ -841,6 +844,12 @@ files:
841
844
  - lib/legion/graph/exporter.rb
842
845
  - lib/legion/guardrails.rb
843
846
  - lib/legion/helpers/context.rb
847
+ - lib/legion/identity/broker.rb
848
+ - lib/legion/identity/lease.rb
849
+ - lib/legion/identity/lease_renewer.rb
850
+ - lib/legion/identity/middleware.rb
851
+ - lib/legion/identity/process.rb
852
+ - lib/legion/identity/request.rb
844
853
  - lib/legion/ingress.rb
845
854
  - lib/legion/isolation.rb
846
855
  - lib/legion/leader.rb
@@ -848,6 +857,7 @@ files:
848
857
  - lib/legion/lock.rb
849
858
  - lib/legion/memory/consolidator.rb
850
859
  - lib/legion/metrics.rb
860
+ - lib/legion/mode.rb
851
861
  - lib/legion/notebook/generator.rb
852
862
  - lib/legion/notebook/parser.rb
853
863
  - lib/legion/notebook/renderer.rb