legionio 1.8.15 → 1.9.0

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: 0e89dd57228cd0a6289346f6e88acc388a955843b32f7d7e2b77e1431b20bd67
4
- data.tar.gz: 42d1ba78b03b822f55d1a961eed3ea4590ac80611c0002396ffd13172240595a
3
+ metadata.gz: 69a0d25a5f8ac2e2327e09ca2185099bd87e7b8575d8c941abc807a0b6c03dfb
4
+ data.tar.gz: f9cb3bcdb322e670a235cc7761e43377d11ed8fed8e7e34b235b8ff77773ef8a
5
5
  SHA512:
6
- metadata.gz: 3e088fa7bf6bf5d5175b42d6b6e9a4c814a89d44d055713172de17d9b2344082f6ea05738092020b55f080d4bad55e984dc636ac437b82048a43444ba3e645da
7
- data.tar.gz: b09b41ef56ba5d471594e7c83e422f53da41b3488816dabf0692456efccf246db61f35a5a5207fa8db8d879b1891fff1e4241b37fcf94865afc24820e4559858
6
+ metadata.gz: 2d7fe093823d8a62b3bbf31f1cb9733b18d56a208990f314e7b9c9ec9ec1b51af2faf18716b48032fd57039b71aec7f32629d0d2218a9500f4746c0525da432f
7
+ data.tar.gz: 51b33926ea63e730ee6407e61da81808bb058030fcb1f1f2d3cd87bcafe25b4c82b21d8a914dabb02771795329e601cc1d14f2b147efccc274895da78c11cfc0
data/CHANGELOG.md CHANGED
@@ -2,6 +2,39 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.9.0] - 2026-04-24
6
+
7
+ ### Added
8
+ - `Legion::Identity::Resolver` — composite identity resolution chain with parallel provider execution, DB persistence, and transport event publishing
9
+ - `Legion::Identity::Trust` — trust level enum (verified, authenticated, configured, cached, unverified)
10
+ - `Legion::Identity::Grant` — frozen value object for credential access auditing
11
+ - `Identity::Process` extended with trust, aliases, providers, profile composite state
12
+ - `Identity::Broker` upgraded to `[provider, qualifier]` tuple-keyed multi-instance storage with `for_context` routing and bounded async audit queue
13
+ - `Resolver.upgrade!` for post-boot identity trust escalation with canonical_name change support
14
+ - Settings client name updated from resolved identity for correct queue naming
15
+
16
+ ### Changed
17
+ - `setup_identity` gate relaxed to run with DB-only nodes (not just transport)
18
+ - `register_credential_providers` gate relaxed for DB-only nodes
19
+ - Reload lifecycle: `Resolver.reset!` preserves providers, re-resolves with existing registrations
20
+ - Middleware `system_principal` uses Resolver identity when available
21
+
22
+ ### Fixed
23
+ - `Request.from_auth_context` canonical normalization now matches DB constraint `^[a-z0-9][a-z0-9_-]*$`
24
+ - `/api/identity/audit` reads from `identity_audit_log` table instead of `AuditRecord`
25
+
26
+ ### Removed
27
+ - Legacy tree-walk identity discovery (`resolve_identity_providers`, `find_identity_providers`, `collect_identity_providers`)
28
+ - `identity_provider?` and `register_identity_provider` from extensions.rb
29
+
30
+ ## [1.8.16] - 2026-04-22
31
+
32
+ ### Added
33
+ - `legion mind-growth wire ID` CLI command — wires a built extension into the cognitive tick cycle via `Orchestrator.post_build_pipeline`; accepts `--phase` override option
34
+
35
+ ### Fixed
36
+ - `MindGrowth#wire` rescue block now logs errors via `Legion::Logging.error` before displaying them, ensuring errors are captured in the daemon log and not only printed to the terminal
37
+
5
38
  ## [1.8.15] - 2026-04-22
6
39
 
7
40
  ### Fixed
@@ -8,12 +8,23 @@ module Legion
8
8
  app.helpers IdentityAuditHelpers
9
9
 
10
10
  app.get '/api/identity/audit' do
11
- halt 503, json_error('unavailable', 'audit records not available') unless defined?(Legion::Data::Model::AuditRecord)
11
+ require_data!
12
+ halt 503, json_error('unavailable', 'identity audit log not available') unless defined?(Legion::Data::Model::IdentityAuditLog)
12
13
 
13
- dataset = Legion::Data::Model::AuditRecord.where(entity_type: 'identity')
14
+ dataset = Legion::Data::Model::IdentityAuditLog.dataset
14
15
 
15
16
  principal = params[:principal]
16
- dataset = dataset.where(Sequel.lit("metadata->>'principal' = ?", principal)) if principal
17
+ if principal && defined?(Legion::Data::Model::Principal)
18
+ principal_record = Legion::Data::Model::Principal.where(canonical_name: principal).first
19
+ halt 404, json_error('not_found', "principal '#{principal}' not found") unless principal_record
20
+ dataset = dataset.where(principal_id: principal_record.id)
21
+ end
22
+
23
+ provider = params[:provider]
24
+ dataset = dataset.where(provider_name: provider) if provider
25
+
26
+ event_type = params[:event_type]
27
+ dataset = dataset.where(event_type: event_type) if event_type
17
28
 
18
29
  since = params[:since]
19
30
  if since
@@ -23,7 +34,9 @@ module Legion
23
34
 
24
35
  records = dataset.order(Sequel.desc(:created_at)).limit(100).all
25
36
  json_collection(records.map do |r|
26
- { id: r.id, action: r.action, entity_type: r.entity_type, metadata: r.parsed_metadata, created_at: r.created_at }
37
+ { id: r.id, event_type: r.event_type, provider_name: r.provider_name,
38
+ trust_level: r.trust_level, detail: r.detail,
39
+ node_id: r.node_id, session_id: r.session_id, created_at: r.created_at }
27
40
  end)
28
41
  end
29
42
  end
@@ -179,6 +179,28 @@ module Legion
179
179
  end
180
180
  end
181
181
 
182
+ desc 'wire ID', 'Wire a built extension into the cognitive tick cycle'
183
+ option :phase, type: :string, desc: 'Override phase (auto-detected if omitted)'
184
+ def wire(proposal_id)
185
+ require_mind_growth!
186
+ result = Legion::Extensions::MindGrowth::Runners::Orchestrator.post_build_pipeline(
187
+ proposal_id: proposal_id
188
+ )
189
+
190
+ if result[:skipped]
191
+ say_status :skipped, result[:reason], :yellow
192
+ elsif result[:activated]
193
+ say_status :activated, "#{proposal_id} wired and activated", :green
194
+ elsif result[:error]
195
+ say_status :error, result[:error], :red
196
+ else
197
+ say_status :partial, "Wire: #{result[:wire]}, Test: #{result[:integration_test]}", :yellow
198
+ end
199
+ rescue StandardError => e
200
+ Legion::Logging.error(e.message) if defined?(Legion::Logging)
201
+ say_status :error, e.message, :red
202
+ end
203
+
182
204
  desc 'history', 'Show recent proposal history'
183
205
  option :limit, type: :numeric, default: 50, desc: 'Max results'
184
206
  def history
data/lib/legion/cli.rb CHANGED
@@ -267,6 +267,9 @@ module Legion
267
267
  desc 'init', 'Initialize a new Legion workspace'
268
268
  subcommand 'init', Legion::CLI::Init
269
269
 
270
+ desc 'detect SUBCOMMAND', 'Scan environment and recommend extensions'
271
+ subcommand 'detect', Legion::CLI::Detect
272
+
270
273
  # --- Interactive & shortcuts ---
271
274
  desc 'knowledge SUBCOMMAND', 'Search and manage the document knowledge base'
272
275
  subcommand 'knowledge', Legion::CLI::Knowledge
@@ -275,8 +275,6 @@ module Legion
275
275
  has_logger = extension.respond_to?(:log)
276
276
  extension.autobuild
277
277
 
278
- register_identity_provider(extension, entry) if identity_provider?(extension)
279
-
280
278
  require 'legion/transport/messages/lex_register'
281
279
  registration = Legion::Transport::Messages::LexRegister.new(function: 'save', opts: extension.runners)
282
280
  if @pending_registrations
@@ -434,39 +432,6 @@ module Legion
434
432
 
435
433
  private
436
434
 
437
- def identity_provider?(extension)
438
- extension.respond_to?(:provider_name) &&
439
- extension.respond_to?(:provider_type) &&
440
- extension.respond_to?(:facing)
441
- end
442
-
443
- def register_identity_provider(extension, entry)
444
- return unless defined?(Legion::Data) && Legion::Data.connected?
445
- return unless defined?(Legion::Data::Model::IdentityProvider)
446
-
447
- name = extension.provider_name.to_s
448
- attrs = {
449
- provider_type: extension.provider_type.to_s,
450
- facing: extension.facing.to_s,
451
- priority: extension.respond_to?(:priority) ? extension.priority : 100,
452
- trust_weight: extension.respond_to?(:trust_weight) ? extension.trust_weight : 50,
453
- capabilities: extension.respond_to?(:capabilities) ? Array(extension.capabilities).map(&:to_s) : [],
454
- source: 'gem',
455
- enabled: true
456
- }
457
-
458
- existing = Legion::Data::Model::IdentityProvider.where(name: name).first
459
- if existing
460
- diverged = attrs.any? { |k, v| existing.send(k).to_s != v.to_s }
461
- Legion::Logging.info "[identity][provider] name=#{name} source=db/gem diverged=#{diverged}" if defined?(Legion::Logging)
462
- else
463
- Legion::Data::Model::IdentityProvider.insert_conflict(target: :name, update: attrs).insert(attrs.merge(name: name))
464
- Legion::Logging.info "[identity][provider] name=#{name} registered" if defined?(Legion::Logging)
465
- end
466
- rescue StandardError => e
467
- Legion::Logging.warn "[identity][provider] registration failed for #{entry[:gem_name]}: #{e.message}" if defined?(Legion::Logging)
468
- end
469
-
470
435
  def write_lex_cli_manifest(entry, extension)
471
436
  require 'legion/cli/lex_cli_manifest'
472
437
 
@@ -6,46 +6,68 @@ module Legion
6
6
  module Identity
7
7
  module Broker
8
8
  GROUPS_CACHE_TTL = 60
9
+ AUDIT_QUEUE_MAX = 1000
10
+ AUDIT_DROP_LOG_INTERVAL = 100
9
11
 
10
12
  class << self
11
- def token_for(provider_name)
12
- lease = lease_for(provider_name)
13
- lease&.valid? ? lease.token : nil
13
+ def token_for(provider_name, qualifier: nil, for_context: nil, purpose: nil, context: nil)
14
+ name = provider_name.to_sym
15
+ resolved = resolve_qualifier(name, qualifier: qualifier, for_context: for_context)
16
+ lease = lease_for(name, qualifier: resolved)
17
+ token = lease&.valid? ? lease.token : nil
18
+ emit_audit(provider: name, qualifier: resolved, purpose: purpose, context: context, granted: !token.nil?)
19
+ token
14
20
  end
15
21
 
16
- def lease_for(provider_name)
22
+ def lease_for(provider_name, qualifier: nil)
17
23
  name = provider_name.to_sym
18
- renewer = renewers[name]
24
+ resolved = qualifier || default_qualifier_for(name)
25
+ key = [name, resolved].freeze
26
+
27
+ renewer = renewers[key]
19
28
  return renewer.current_lease if renewer
20
29
 
21
- static_ref = static_leases[name]
30
+ static_ref = static_leases[key]
22
31
  static_ref&.get
23
32
  end
24
33
 
25
- def renewer_for(provider_name)
26
- renewers[provider_name.to_sym]
34
+ def renewer_for(provider_name, qualifier: nil)
35
+ name = provider_name.to_sym
36
+ resolved = qualifier || default_qualifier_for(name)
37
+ renewers[[name, resolved].freeze]
27
38
  end
28
39
 
29
- def credentials_for(provider_name, service: nil)
30
- lease = lease_for(provider_name)
40
+ def credentials_for(provider_name, qualifier: nil, service: nil)
41
+ name = provider_name.to_sym
42
+ resolved = qualifier || default_qualifier_for(name)
43
+ lease = lease_for(name, qualifier: resolved)
31
44
  return nil unless lease&.valid?
32
45
 
33
- { token: lease.token, provider: provider_name.to_sym, service: service, lease: lease }
46
+ { token: lease.token, provider: name, service: service, lease: lease }
34
47
  end
35
48
 
36
- def register_provider(provider_name, provider:, lease:)
49
+ def register_provider(provider_name, provider:, lease:, qualifier: :default, default: false)
37
50
  name = provider_name.to_sym
51
+ qual = qualifier
52
+ key = [name, qual].freeze
53
+
54
+ # Set default qualifier: first registration or explicit default: true
55
+ default_qualifiers[name] = qual if default || !default_qualifiers.key?(name)
56
+
57
+ # Store provider instance (first-write-wins per provider name)
58
+ provider_instances[name] ||= provider
59
+
60
+ # Stop existing renewer for this specific tuple key
61
+ renewers[key]&.stop!
38
62
 
39
- renewers[name]&.stop!
40
63
  if lease&.expires_at.nil? && !lease&.renewable
41
64
  # Static credential — store without a background renewal thread
42
- renewers.delete(name)
43
- static_leases[name] = Concurrent::AtomicReference.new(lease)
44
- providers_map[name] = provider
65
+ renewers.delete(key)
66
+ static_leases[key] = Concurrent::AtomicReference.new(lease)
45
67
  else
46
68
  # Dynamic credential — create LeaseRenewer
47
- static_leases.delete(name)
48
- renewers[name] = LeaseRenewer.new(
69
+ static_leases.delete(key)
70
+ renewers[key] = LeaseRenewer.new(
49
71
  provider_name: name,
50
72
  provider: provider,
51
73
  lease: lease
@@ -53,12 +75,15 @@ module Legion
53
75
  end
54
76
  end
55
77
 
56
- def refresh_credential(provider_name)
78
+ def refresh_credential(provider_name, qualifier: nil)
57
79
  name = provider_name.to_sym
58
- ref = static_leases[name]
80
+ resolved = qualifier || default_qualifier_for(name)
81
+ key = [name, resolved].freeze
82
+
83
+ ref = static_leases[key]
59
84
  return false unless ref
60
85
 
61
- provider = providers_map[name]
86
+ provider = provider_instances[name]
62
87
  return false unless provider.respond_to?(:provide_token)
63
88
 
64
89
  new_lease = provider.provide_token
@@ -109,13 +134,29 @@ module Legion
109
134
  end
110
135
 
111
136
  def providers
112
- (renewers.keys + static_leases.keys).uniq
137
+ all_keys = (renewers.keys + static_leases.keys)
138
+ all_keys.map(&:first).uniq
139
+ end
140
+
141
+ def credentials_available(provider_name)
142
+ name = provider_name.to_sym
143
+ all_keys = (renewers.keys + static_leases.keys)
144
+ all_keys.select { |k| k.first == name }.map(&:last).uniq
113
145
  end
114
146
 
115
147
  def leases
116
- dynamic = renewers.transform_values { |r| r.current_lease&.to_h }
117
- static = static_leases.transform_values { |ref| ref.get&.to_h }
118
- dynamic.merge(static)
148
+ result = {}
149
+ renewers.each do |key, renewer|
150
+ provider_name, qualifier = key
151
+ result[provider_name] ||= {}
152
+ result[provider_name][qualifier] = renewer.current_lease&.to_h
153
+ end
154
+ static_leases.each do |key, ref|
155
+ provider_name, qualifier = key
156
+ result[provider_name] ||= {}
157
+ result[provider_name][qualifier] = ref.get&.to_h unless result[provider_name].key?(qualifier)
158
+ end
159
+ result
119
160
  end
120
161
 
121
162
  def shutdown
@@ -126,17 +167,41 @@ module Legion
126
167
  end
127
168
  renewers.clear
128
169
  static_leases.clear
129
- providers_map.clear
170
+ provider_instances.clear
171
+ default_qualifiers.clear
172
+ stop_audit_drainer
130
173
  end
131
174
 
132
175
  def reset!
133
176
  shutdown
134
177
  @groups_cache = Concurrent::AtomicReference.new(nil)
135
178
  @groups_fetch_in_progress = Concurrent::AtomicBoolean.new(false)
179
+ @audit_queue = Concurrent::Array.new
180
+ @audit_drops = Concurrent::AtomicFixnum.new(0)
181
+ @audit_drainer = nil
182
+ @audit_drainer_started = Concurrent::AtomicBoolean.new(false)
136
183
  end
137
184
 
138
185
  private
139
186
 
187
+ def resolve_qualifier(provider_name, qualifier: nil, for_context: nil)
188
+ return qualifier if qualifier
189
+
190
+ if for_context
191
+ provider = provider_instances[provider_name]
192
+ if provider.respond_to?(:resolve_qualifier)
193
+ resolved = provider.resolve_qualifier(for_context)
194
+ return resolved if resolved
195
+ end
196
+ end
197
+
198
+ default_qualifier_for(provider_name)
199
+ end
200
+
201
+ def default_qualifier_for(provider_name)
202
+ default_qualifiers[provider_name] || :default
203
+ end
204
+
140
205
  def renewers
141
206
  @renewers ||= Concurrent::Hash.new
142
207
  end
@@ -145,8 +210,56 @@ module Legion
145
210
  @static_leases ||= Concurrent::Hash.new
146
211
  end
147
212
 
148
- def providers_map
149
- @providers_map ||= Concurrent::Hash.new
213
+ def provider_instances
214
+ @provider_instances ||= Concurrent::Hash.new
215
+ end
216
+
217
+ def default_qualifiers
218
+ @default_qualifiers ||= Concurrent::Hash.new
219
+ end
220
+
221
+ def audit_queue
222
+ @audit_queue ||= Concurrent::Array.new
223
+ end
224
+
225
+ def emit_audit(provider:, qualifier:, purpose:, context:, granted:)
226
+ ensure_audit_drainer_started
227
+ event = {
228
+ provider: provider,
229
+ qualifier: qualifier,
230
+ purpose: purpose,
231
+ context: context,
232
+ granted: granted,
233
+ timestamp: Time.now
234
+ }
235
+
236
+ if audit_queue.size >= AUDIT_QUEUE_MAX
237
+ drops = (@audit_drops ||= Concurrent::AtomicFixnum.new(0)).increment
238
+ log_warn("Audit queue full, dropping event (total drops: #{drops})") if (drops % AUDIT_DROP_LOG_INTERVAL).zero?
239
+ else
240
+ audit_queue.push(event)
241
+ end
242
+ end
243
+
244
+ def ensure_audit_drainer_started
245
+ # Intentionally a no-op until publish_audit_event has a real
246
+ # implementation. Starting a drainer before a durable sink exists
247
+ # causes queued audit events to be silently discarded.
248
+ @ensure_audit_drainer_started ||= Concurrent::AtomicBoolean.new(false)
249
+ end
250
+
251
+ def stop_audit_drainer
252
+ # No background drainer is started until publish_audit_event has a
253
+ # real implementation. Keep this method for API compatibility.
254
+ @audit_drainer = nil
255
+ @audit_drainer_started = Concurrent::AtomicBoolean.new(false)
256
+ end
257
+
258
+ def publish_audit_event(event)
259
+ # Future: publish to transport / log store.
260
+ # Until then, events remain in the queue for inspection and are not
261
+ # drained by a background thread.
262
+ event
150
263
  end
151
264
 
152
265
  def fetch_groups
@@ -198,6 +311,10 @@ module Legion
198
311
  # Initialize atomics at module definition time
199
312
  @groups_cache = Concurrent::AtomicReference.new(nil)
200
313
  @groups_fetch_in_progress = Concurrent::AtomicBoolean.new(false)
314
+ @audit_queue = Concurrent::Array.new
315
+ @audit_drops = Concurrent::AtomicFixnum.new(0)
316
+ @audit_drainer = nil
317
+ @audit_drainer_started = Concurrent::AtomicBoolean.new(false)
201
318
  end
202
319
  end
203
320
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Identity
5
+ class Grant
6
+ attr_reader :grant_id, :token, :provider, :qualifier, :purpose, :result, :reason, :expires_at
7
+
8
+ def initialize(grant_id:, token:, provider:, result:, qualifier: :default, purpose: nil, reason: nil, expires_at: nil) # rubocop:disable Metrics/ParameterLists
9
+ @grant_id = grant_id
10
+ @token = token
11
+ @provider = provider
12
+ @qualifier = qualifier
13
+ @purpose = purpose
14
+ @result = result
15
+ @reason = reason
16
+ @expires_at = expires_at
17
+ freeze
18
+ end
19
+
20
+ def granted? = result == :granted
21
+ def denied? = result == :denied
22
+ end
23
+ end
24
+ end
@@ -115,13 +115,22 @@ module Legion
115
115
  end
116
116
 
117
117
  def system_principal
118
- @system_principal ||= Identity::Request.new(
119
- principal_id: 'system:local',
120
- canonical_name: 'system',
121
- kind: :service,
122
- groups: [],
123
- source: :local
124
- )
118
+ canonical = if defined?(Legion::Identity::Process) && Legion::Identity::Process.resolved?
119
+ Legion::Identity::Process.canonical_name
120
+ else
121
+ 'system'
122
+ end
123
+
124
+ if @system_principal&.canonical_name != canonical
125
+ @system_principal = Identity::Request.new(
126
+ principal_id: "system:#{canonical}",
127
+ canonical_name: canonical,
128
+ kind: :service,
129
+ groups: [],
130
+ source: :local
131
+ )
132
+ end
133
+ @system_principal
125
134
  end
126
135
  end
127
136
  end
@@ -14,7 +14,11 @@ module Legion
14
14
  source: nil,
15
15
  persistent: false,
16
16
  groups: [].freeze,
17
- metadata: {}.freeze
17
+ metadata: {}.freeze,
18
+ trust: nil,
19
+ aliases: {}.freeze,
20
+ providers: {}.freeze,
21
+ profile: {}.freeze
18
22
  }.freeze
19
23
 
20
24
  class << self
@@ -58,6 +62,22 @@ module Legion
58
62
  @state.get[:source]
59
63
  end
60
64
 
65
+ def trust
66
+ @state.get[:trust]
67
+ end
68
+
69
+ def aliases
70
+ @state.get[:aliases] || {}.freeze
71
+ end
72
+
73
+ def providers
74
+ @state.get[:providers] || {}.freeze
75
+ end
76
+
77
+ def profile
78
+ @state.get[:profile] || {}.freeze
79
+ end
80
+
61
81
  def identity_hash
62
82
  {
63
83
  id: id,
@@ -69,7 +89,11 @@ module Legion
69
89
  resolved: resolved?,
70
90
  persistent: persistent?,
71
91
  groups: @state.get[:groups] || [],
72
- metadata: @state.get[:metadata] || {}
92
+ metadata: @state.get[:metadata] || {},
93
+ trust: trust,
94
+ aliases: aliases,
95
+ providers: providers,
96
+ profile: profile
73
97
  }
74
98
  end
75
99
 
@@ -83,7 +107,11 @@ module Legion
83
107
  source: identity_hash.key?(:source) ? identity_hash[:source] : provider_source,
84
108
  persistent: identity_hash.fetch(:persistent, true),
85
109
  groups: Array(identity_hash[:groups]).compact.freeze,
86
- metadata: identity_hash[:metadata].is_a?(Hash) ? identity_hash[:metadata].dup.freeze : {}.freeze
110
+ metadata: identity_hash[:metadata].is_a?(Hash) ? identity_hash[:metadata].dup.freeze : {}.freeze,
111
+ trust: identity_hash[:trust],
112
+ aliases: identity_hash[:aliases].is_a?(Hash) ? identity_hash[:aliases].dup.freeze : {}.freeze,
113
+ providers: identity_hash[:providers].is_a?(Hash) ? identity_hash[:providers].dup.freeze : {}.freeze,
114
+ profile: identity_hash[:profile].is_a?(Hash) ? identity_hash[:profile].dup.freeze : {}.freeze
87
115
  })
88
116
  @resolved.make_true
89
117
  end
@@ -97,7 +125,11 @@ module Legion
97
125
  source: :system,
98
126
  persistent: false,
99
127
  groups: [].freeze,
100
- metadata: {}.freeze
128
+ metadata: {}.freeze,
129
+ trust: nil,
130
+ aliases: {}.freeze,
131
+ providers: {}.freeze,
132
+ profile: {}.freeze
101
133
  })
102
134
  @resolved.make_false
103
135
  end
@@ -44,7 +44,9 @@ module Legion
44
44
  # The source value is normalized via SOURCE_NORMALIZATION at construction time.
45
45
  def self.from_auth_context(claims_hash)
46
46
  raw_name = claims_hash[:name] || claims_hash[:preferred_username] || ''
47
- canonical = raw_name.to_s.strip.downcase.gsub('.', '-')
47
+ stripped = raw_name.to_s.strip.downcase
48
+ stripped = stripped.split('@', 2).first if stripped.include?('@')
49
+ canonical = stripped.gsub('.', '-').gsub(/[^a-z0-9_-]/, '')
48
50
  raw_source = claims_hash[:source]&.to_sym
49
51
  normalized_source = SOURCE_NORMALIZATION.fetch(raw_source, raw_source)
50
52
 
@@ -0,0 +1,442 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'fileutils'
5
+ require 'concurrent/array'
6
+ require 'concurrent/atomic/atomic_reference'
7
+ require 'concurrent/atomic/atomic_boolean'
8
+ require 'concurrent/promises'
9
+
10
+ module Legion
11
+ module Identity
12
+ module Resolver
13
+ TIMEOUT_SECONDS = 5
14
+
15
+ class << self
16
+ def register(provider)
17
+ return if @providers.any? { |p| p.provider_name == provider.provider_name }
18
+
19
+ @providers << provider
20
+ end
21
+
22
+ def resolve!(timeout: TIMEOUT_SECONDS)
23
+ drain_pending_registrations
24
+
25
+ auth_providers, profile_providers, fallback_providers = partition_providers
26
+
27
+ winning_provider, winning_result, provider_results = resolve_auth(auth_providers, timeout: timeout)
28
+
29
+ if winning_provider.nil?
30
+ winning_provider, winning_result, fallback_results = resolve_auth(fallback_providers, timeout: timeout)
31
+ provider_results.merge!(fallback_results) if fallback_results
32
+ end
33
+
34
+ unless winning_provider
35
+ @resolved.make_false
36
+ @composite.set(nil)
37
+ return nil
38
+ end
39
+
40
+ canonical = winning_result[:canonical_name]
41
+ trust_level = winning_provider.trust_level
42
+ source = winning_provider.provider_name
43
+
44
+ profile_data = resolve_profiles(profile_providers, canonical, timeout: timeout)
45
+
46
+ composite = assemble_composite(
47
+ provider_results, profile_data,
48
+ winning_result: winning_result,
49
+ trust_level: trust_level,
50
+ source: source
51
+ )
52
+
53
+ bind_and_persist(winning_provider, composite, trust_level)
54
+ composite
55
+ end
56
+
57
+ def upgrade!(provider, result)
58
+ current = @composite.get
59
+ return unless current
60
+
61
+ new_trust = provider.trust_level
62
+ new_canonical = result[:canonical_name] || current[:canonical_name]
63
+ canonical_changed = new_canonical != current[:canonical_name]
64
+
65
+ # Only promote the composite trust level when the new provider's trust
66
+ # is strictly higher (lower rank index) than the current level.
67
+ # This prevents an accidental downgrade if upgrade! is called with a
68
+ # lower-trust provider such as one with :unverified trust.
69
+ current_trust = current[:trust]
70
+ effective_trust = if defined?(Legion::Identity::Trust) &&
71
+ Legion::Identity::Trust.respond_to?(:above?) &&
72
+ Legion::Identity::Trust.above?(new_trust, current_trust)
73
+ new_trust
74
+ else
75
+ current_trust
76
+ end
77
+
78
+ new_aliases = current[:aliases].dup
79
+ provider_identity = result[:provider_identity]
80
+ if provider_identity
81
+ existing = Array(new_aliases[provider.provider_name])
82
+ new_aliases[provider.provider_name] = (existing + [provider_identity]).uniq
83
+ end
84
+
85
+ new_providers = current[:providers].dup
86
+ new_providers[provider.provider_name] = {
87
+ status: :resolved,
88
+ trust: new_trust,
89
+ resolved_at: Time.now
90
+ }
91
+
92
+ updated = current.merge(
93
+ canonical_name: new_canonical,
94
+ trust: effective_trust,
95
+ source: provider.provider_name,
96
+ aliases: new_aliases,
97
+ providers: new_providers
98
+ )
99
+
100
+ handle_canonical_change(current[:canonical_name], new_canonical, updated) if canonical_changed
101
+
102
+ @composite.set(updated)
103
+ Legion::Identity::Process.bind!(provider, updated) if defined?(Legion::Identity::Process)
104
+
105
+ if defined?(Legion::Settings) && Legion::Settings.respond_to?(:loader) && Legion::Settings.loader.respond_to?(:settings)
106
+ Legion::Settings.loader.settings[:client] ||= {}
107
+ Legion::Settings.loader.settings[:client][:name] = Legion::Identity::Process.queue_prefix
108
+ end
109
+
110
+ persist_identity_json(new_canonical, updated[:kind]) unless new_trust == :unverified
111
+
112
+ updated
113
+ end
114
+
115
+ def resolved?
116
+ @resolved.true?
117
+ end
118
+
119
+ def composite
120
+ @composite.get
121
+ end
122
+
123
+ def providers
124
+ @providers.dup
125
+ end
126
+
127
+ attr_reader :session_id
128
+
129
+ def reset!
130
+ @composite = Concurrent::AtomicReference.new(nil)
131
+ @resolved = Concurrent::AtomicBoolean.new(false)
132
+ @session_id = SecureRandom.uuid
133
+ end
134
+
135
+ def reset_all!
136
+ reset!
137
+ @providers = Concurrent::Array.new
138
+ end
139
+
140
+ private
141
+
142
+ def drain_pending_registrations
143
+ return unless defined?(Legion::Identity) && Legion::Identity.respond_to?(:pending_registrations)
144
+
145
+ pending = Legion::Identity.pending_registrations
146
+ return if pending.nil? || pending.empty?
147
+
148
+ drained = []
149
+ drained << pending.shift until pending.empty?
150
+ drained.each { |p| register(p) }
151
+ end
152
+
153
+ def partition_providers
154
+ auth = []
155
+ profile = []
156
+ fallback = []
157
+
158
+ @providers.each do |p|
159
+ case p.provider_type
160
+ when :auth then auth << p
161
+ when :profile then profile << p
162
+ when :fallback then fallback << p
163
+ end
164
+ end
165
+
166
+ auth.sort_by! { |p| [-p.priority, p.trust_weight] }
167
+ fallback.sort_by! { |p| [-p.priority, p.trust_weight] }
168
+
169
+ [auth, profile, fallback]
170
+ end
171
+
172
+ def resolve_auth(auth_providers, timeout:)
173
+ return [nil, nil, {}] if auth_providers.empty?
174
+
175
+ futures = auth_providers.map do |provider|
176
+ Concurrent::Promises.future { provider.resolve }
177
+ end
178
+
179
+ deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + timeout
180
+ provider_results = {}
181
+ auth_providers.zip(futures).each do |provider, future|
182
+ remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
183
+ future.wait(remaining.positive? ? remaining : 0)
184
+ result = future.value(0) if future.resolved?
185
+ status = auth_future_status(future, result)
186
+
187
+ provider_results[provider.provider_name] = {
188
+ status: status,
189
+ trust: (status == :resolved ? provider.trust_level : nil),
190
+ resolved_at: (status == :resolved ? Time.now : nil),
191
+ provider: provider,
192
+ result: (status == :resolved ? result : nil)
193
+ }
194
+ end
195
+
196
+ resolved_entries = provider_results.select { |_, v| v[:status] == :resolved }
197
+ if resolved_entries.empty?
198
+ [nil, nil, provider_results]
199
+ else
200
+ winner_name = resolved_entries.min_by do |_, v|
201
+ p = v[:provider]
202
+ [-p.priority, p.trust_weight]
203
+ end&.first
204
+
205
+ winner_info = provider_results[winner_name]
206
+ [winner_info[:provider], winner_info[:result], provider_results]
207
+ end
208
+ end
209
+
210
+ def auth_future_status(future, result)
211
+ if future.rejected?
212
+ :failed
213
+ elsif !future.resolved?
214
+ :timeout
215
+ elsif result.is_a?(Hash) && result[:canonical_name]
216
+ :resolved
217
+ else
218
+ :no_identity
219
+ end
220
+ end
221
+
222
+ def resolve_profiles(profile_providers, canonical, timeout:)
223
+ return { groups: [], profile: {}, provider_results: {} } if profile_providers.empty?
224
+
225
+ futures = profile_providers.map do |provider|
226
+ Concurrent::Promises.future { resolve_profile_provider(provider, canonical) }
227
+ end
228
+
229
+ deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + timeout
230
+ groups = []
231
+ profile = {}
232
+ pr = {}
233
+
234
+ profile_providers.zip(futures).each do |provider, future|
235
+ remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
236
+ future.wait(remaining.positive? ? remaining : 0)
237
+ result = future.resolved? ? future.value(0) : nil
238
+
239
+ if future.fulfilled? && result.is_a?(Hash)
240
+ groups.concat(Array(result[:groups])) if result[:groups]
241
+ profile.merge!(result[:profile]) if result[:profile].is_a?(Hash)
242
+ pr[provider.provider_name] = { status: :resolved, trust: provider.trust_level, resolved_at: Time.now }
243
+ else
244
+ pr[provider.provider_name] = { status: (future.rejected? ? :failed : :timeout), trust: nil, resolved_at: nil }
245
+ end
246
+ end
247
+
248
+ { groups: groups.uniq, profile: profile, provider_results: pr }
249
+ end
250
+
251
+ def resolve_profile_provider(provider, canonical)
252
+ if provider.respond_to?(:resolve_all)
253
+ provider.resolve_all(canonical_name: canonical)
254
+ else
255
+ provider.resolve(canonical_name: canonical)
256
+ end
257
+ end
258
+
259
+ def assemble_composite(provider_results, profile_data, winning_result:, trust_level:, source:)
260
+ aliases = build_aliases(provider_results)
261
+ providers_map = build_providers_map(provider_results, profile_data)
262
+
263
+ {
264
+ id: nil,
265
+ canonical_name: winning_result[:canonical_name],
266
+ kind: winning_result[:kind] || :human,
267
+ trust: trust_level,
268
+ source: source,
269
+ persistent: true,
270
+ aliases: aliases,
271
+ groups: profile_data[:groups],
272
+ profile: profile_data[:profile],
273
+ providers: providers_map,
274
+ metadata: {}
275
+ }
276
+ end
277
+
278
+ def build_aliases(provider_results)
279
+ aliases = {}
280
+ provider_results.each do |name, info|
281
+ next unless info[:status] == :resolved && info[:result]
282
+
283
+ pi = info[:result][:provider_identity]
284
+ aliases[name] = [pi] if pi
285
+ end
286
+ aliases
287
+ end
288
+
289
+ def build_providers_map(provider_results, profile_data)
290
+ providers_map = {}
291
+ provider_results.each do |name, info|
292
+ providers_map[name] = {
293
+ status: info[:status],
294
+ trust: info[:trust],
295
+ resolved_at: info[:resolved_at]
296
+ }
297
+ end
298
+ profile_data[:provider_results].each do |name, info|
299
+ providers_map[name] = info
300
+ end
301
+ providers_map
302
+ end
303
+
304
+ def bind_and_persist(winning_provider, composite, trust_level)
305
+ Legion::Identity::Process.bind!(winning_provider, composite) if defined?(Legion::Identity::Process)
306
+
307
+ if defined?(Legion::Settings) && Legion::Settings.respond_to?(:loader) && Legion::Settings.loader.respond_to?(:settings)
308
+ Legion::Settings.loader.settings[:client] ||= {}
309
+ Legion::Settings.loader.settings[:client][:name] = Legion::Identity::Process.queue_prefix
310
+ end
311
+
312
+ persist_to_db(composite)
313
+ persist_identity_json(composite[:canonical_name], composite[:kind]) unless trust_level == :unverified
314
+
315
+ @composite.set(composite)
316
+ @resolved.make_true
317
+ end
318
+
319
+ def persist_to_db(composite) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
320
+ return unless defined?(Legion::Data) && Legion::Data.respond_to?(:connected?) && Legion::Data.connected?
321
+ return unless defined?(Legion::Data::Connection) &&
322
+ Legion::Data::Connection.respond_to?(:adapter) &&
323
+ Legion::Data::Connection.adapter == :postgres
324
+
325
+ # upsert identity_providers
326
+ composite[:providers]&.each do |name, info|
327
+ Legion::Data.db[:identity_providers].insert_conflict(
328
+ target: :name,
329
+ update: { status: info[:status].to_s, trust_level: info[:trust]&.to_s, last_seen_at: Time.now }
330
+ ).insert(name: name.to_s, status: info[:status].to_s, trust_level: info[:trust]&.to_s, last_seen_at: Time.now)
331
+ end
332
+
333
+ # upsert principals
334
+ Legion::Data.db[:principals].insert_conflict(
335
+ target: :canonical_name,
336
+ update: { kind: composite[:kind].to_s, updated_at: Time.now }
337
+ ).insert(
338
+ canonical_name: composite[:canonical_name],
339
+ kind: composite[:kind].to_s,
340
+ created_at: Time.now,
341
+ updated_at: Time.now
342
+ )
343
+
344
+ principal_row = Legion::Data.db[:principals].where(canonical_name: composite[:canonical_name]).first
345
+ principal_id = principal_row[:id] if principal_row
346
+
347
+ # upsert identities per provider alias
348
+ composite[:aliases]&.each do |provider_name, identities|
349
+ Array(identities).each do |ident|
350
+ Legion::Data.db[:identities].insert_conflict(
351
+ target: %i[principal_id provider_name provider_identity],
352
+ update: { updated_at: Time.now }
353
+ ).insert(
354
+ principal_id: principal_id,
355
+ provider_name: provider_name.to_s,
356
+ provider_identity: ident,
357
+ created_at: Time.now,
358
+ updated_at: Time.now
359
+ )
360
+ end
361
+ end
362
+
363
+ # insert audit log
364
+ Legion::Data.db[:identity_audit_log].insert(
365
+ principal_id: principal_id,
366
+ event_type: 'identity.resolved',
367
+ provider_name: composite[:source].to_s,
368
+ trust_level: composite[:trust]&.to_s,
369
+ detail: Legion::JSON.dump(
370
+ {
371
+ source: composite[:source],
372
+ trust: composite[:trust],
373
+ node_id: composite[:node_id],
374
+ session_id: @session_id
375
+ }
376
+ ),
377
+ node_id: composite[:node_id],
378
+ session_id: @session_id,
379
+ created_at: Time.now
380
+ )
381
+ rescue StandardError => e
382
+ log_warn("DB persistence failed: #{e.message}")
383
+ end
384
+
385
+ def persist_identity_json(canonical_name, kind)
386
+ dir = File.expand_path('~/.legionio/settings')
387
+ FileUtils.mkdir_p(dir)
388
+ path = File.join(dir, 'identity.json')
389
+ payload = { canonical_name: canonical_name, kind: kind }
390
+ json = if defined?(Legion::JSON)
391
+ Legion::JSON.dump(payload)
392
+ else
393
+ require 'json'
394
+ ::JSON.generate(payload)
395
+ end
396
+ File.write(path, json)
397
+ rescue StandardError => e
398
+ log_warn("identity.json write failed: #{e.message}")
399
+ end
400
+
401
+ def handle_canonical_change(old_canonical, new_canonical, _composite)
402
+ if defined?(Legion::Settings) && Legion::Settings.respond_to?(:loader)
403
+ settings = Legion::Settings.loader.settings
404
+ settings[:client] ||= {}
405
+ settings[:client][:name] = new_canonical
406
+ end
407
+
408
+ return unless defined?(Legion::Data) && Legion::Data.respond_to?(:connected?) && Legion::Data.connected?
409
+ return unless defined?(Legion::Data::Connection) &&
410
+ Legion::Data::Connection.respond_to?(:adapter) &&
411
+ Legion::Data::Connection.adapter == :postgres
412
+
413
+ # Audit the canonical change
414
+ old_row = Legion::Data.db[:principals].where(canonical_name: old_canonical).first
415
+ Legion::Data.db[:identity_audit_log].insert(
416
+ principal_id: old_row&.dig(:id),
417
+ event_type: 'identity.canonical_changed',
418
+ provider_name: '',
419
+ detail: Legion::JSON.dump({ old: old_canonical, new: new_canonical }),
420
+ created_at: Time.now
421
+ )
422
+ rescue StandardError => e
423
+ log_warn("canonical change handling failed: #{e.message}")
424
+ end
425
+
426
+ def log_warn(message)
427
+ if defined?(Legion::Logging) && Legion::Logging.respond_to?(:warn)
428
+ Legion::Logging.warn("[Identity::Resolver] #{message}")
429
+ else
430
+ $stderr.puts "[Identity::Resolver] #{message}" # rubocop:disable Style/StderrPuts
431
+ end
432
+ end
433
+ end
434
+
435
+ # Initialize atomics at module definition time
436
+ @providers = Concurrent::Array.new
437
+ @composite = Concurrent::AtomicReference.new(nil)
438
+ @resolved = Concurrent::AtomicBoolean.new(false)
439
+ @session_id = SecureRandom.uuid
440
+ end
441
+ end
442
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Identity
5
+ module Trust
6
+ LEVELS = %i[verified authenticated configured cached unverified].freeze
7
+ RANK = LEVELS.each_with_index.to_h.freeze
8
+
9
+ module_function
10
+
11
+ def levels
12
+ LEVELS
13
+ end
14
+
15
+ def rank(level)
16
+ RANK[level]
17
+ end
18
+
19
+ def above?(level_a, level_b)
20
+ rank_a = RANK[level_a]
21
+ rank_b = RANK[level_b]
22
+ return false if rank_a.nil? || rank_b.nil?
23
+
24
+ rank_a < rank_b
25
+ end
26
+
27
+ def at_least?(level, minimum)
28
+ rank_level = RANK[level]
29
+ rank_min = RANK[minimum]
30
+ return false if rank_level.nil? || rank_min.nil?
31
+
32
+ rank_level <= rank_min
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent/array'
4
+
5
+ module Legion
6
+ module Identity
7
+ class << self
8
+ attr_accessor :pending_registrations
9
+ end
10
+ self.pending_registrations = Concurrent::Array.new
11
+ end
12
+ end
13
+
14
+ require_relative 'identity/trust'
15
+ require_relative 'identity/resolver'
@@ -161,6 +161,8 @@ module Legion
161
161
  setup_safety_metrics
162
162
  setup_supervision if supervision
163
163
 
164
+ require_relative 'identity' if File.exist?(File.expand_path('identity.rb', __dir__))
165
+
164
166
  if extensions
165
167
  load_extensions
166
168
  Legion::Readiness.mark_ready(:extensions)
@@ -168,8 +170,9 @@ module Legion
168
170
  end
169
171
 
170
172
  # Identity resolution — after extensions so lex-identity-* providers are loaded
171
- setup_identity if transport
172
- register_credential_providers if extensions && transport
173
+ db_available = defined?(Legion::Data) && Legion::Data.respond_to?(:connected?) && Legion::Data.connected?
174
+ setup_identity if transport || db_available
175
+ register_credential_providers if extensions && (transport || db_available)
173
176
 
174
177
  register_core_tools
175
178
 
@@ -506,8 +509,11 @@ module Legion
506
509
  require_relative 'identity/middleware'
507
510
 
508
511
  # Resolve identity from available providers (Phase 4 adds real providers)
509
- resolved = resolve_identity_providers
510
- unless resolved
512
+ require_relative 'identity' unless defined?(Legion::Identity::Resolver)
513
+
514
+ Legion::Identity::Resolver.resolve!
515
+
516
+ unless Legion::Identity::Resolver.resolved?
511
517
  Legion::Identity::Process.bind_fallback!
512
518
  log.info "[Identity] fallback identity: #{Legion::Identity::Process.canonical_name}"
513
519
  end
@@ -897,15 +903,19 @@ module Legion
897
903
  Legion::Readiness.mark_skipped(:gaia)
898
904
  end
899
905
 
900
- # Phase 5: re-run identity resolution + credential swap so the reloaded
901
- # process gets identity-scoped RMQ creds (not stale bootstrap creds).
902
- setup_identity
903
-
904
906
  setup_supervision
905
907
  load_extensions
906
908
  Legion::Readiness.mark_ready(:extensions)
907
909
 
908
- register_credential_providers
910
+ # Phase 5: re-run identity resolution after extensions are loaded so that
911
+ # any identity providers registered by lex-identity-* extensions are
912
+ # available to the resolver (mirrors the boot-time ordering).
913
+ Legion::Identity::Resolver.reset! if defined?(Legion::Identity::Resolver)
914
+ setup_identity
915
+
916
+ db_available = defined?(Legion::Data) && Legion::Data.respond_to?(:connected?) && Legion::Data.connected?
917
+ transport_available = defined?(Legion::Transport::Connection) && Legion::Transport::Connection.respond_to?(:session_open?) && Legion::Transport::Connection.session_open?
918
+ register_credential_providers if transport_available || db_available
909
919
  Legion::Extensions.flush_pending_registrations! if defined?(Legion::Extensions) && Legion::Extensions.respond_to?(:flush_pending_registrations!)
910
920
 
911
921
  register_core_tools
@@ -1088,64 +1098,6 @@ module Legion
1088
1098
  Legion::Crypt.fetch_bootstrap_rmq_creds
1089
1099
  end
1090
1100
 
1091
- def resolve_identity_providers
1092
- # Phase 4 adds lex-identity-* providers. For now, check if any are loaded.
1093
- return false unless defined?(Legion::Extensions)
1094
-
1095
- providers = find_identity_providers
1096
- return false if providers.empty?
1097
-
1098
- # Parallel resolution with 5s per-provider timeout (NO Timeout.timeout — uses future.value)
1099
- pool = Concurrent::FixedThreadPool.new([providers.size, 4].min)
1100
- futures = providers.map do |provider|
1101
- Concurrent::Promises.future_on(pool, provider, &:resolve)
1102
- end
1103
-
1104
- winner_pair = providers.zip(futures).find do |_provider, future|
1105
- result = begin
1106
- future.value(5) # 5s timeout per provider
1107
- rescue StandardError => e
1108
- handle_exception(e, level: :debug, operation: 'service.resolve_identity_providers.future')
1109
- nil
1110
- end
1111
- result.is_a?(Hash) && result[:canonical_name]
1112
- end
1113
-
1114
- if winner_pair
1115
- provider, future = winner_pair
1116
- identity = future.value
1117
- Legion::Identity::Process.bind!(provider, identity)
1118
- log.info "[Identity] resolved via #{provider.class.name}: #{identity[:canonical_name]}"
1119
-
1120
- # Phase 8: Register winning auth provider with Broker so extensions can
1121
- # call Broker.token_for(:provider_name) without managing tokens themselves.
1122
- register_provider_with_broker(provider)
1123
-
1124
- true
1125
- else
1126
- false
1127
- end
1128
- rescue StandardError => e
1129
- handle_exception(e, level: :warn, operation: 'service.resolve_identity_providers')
1130
- false
1131
- ensure
1132
- pool&.shutdown
1133
- pool&.kill unless pool&.wait_for_termination(2)
1134
- end
1135
-
1136
- def register_provider_with_broker(provider)
1137
- return unless provider.respond_to?(:provide_token) && defined?(Legion::Identity::Broker)
1138
-
1139
- lease = provider.provide_token
1140
- return unless lease
1141
-
1142
- provider_name = provider.respond_to?(:provider_name) ? provider.provider_name : provider.class.name.to_sym
1143
- Legion::Identity::Broker.register_provider(provider_name, provider: provider, lease: lease)
1144
- log.info "[Identity] registered provider #{provider_name} with Broker"
1145
- rescue StandardError => e
1146
- handle_exception(e, level: :warn, operation: 'service.register_provider_with_broker')
1147
- end
1148
-
1149
1101
  def register_credential_providers
1150
1102
  return unless defined?(Legion::Identity::Broker) && defined?(Legion::Extensions)
1151
1103
 
@@ -1176,32 +1128,6 @@ module Legion
1176
1128
  identity
1177
1129
  end
1178
1130
 
1179
- def find_identity_providers
1180
- return [] unless defined?(Legion::Extensions)
1181
-
1182
- collect_identity_providers(Legion::Extensions)
1183
- end
1184
-
1185
- def collect_identity_providers(namespace, visited = Set.new)
1186
- return [] unless namespace.is_a?(Module)
1187
- return [] if visited.include?(namespace.object_id)
1188
-
1189
- visited.add(namespace.object_id)
1190
- providers = []
1191
-
1192
- namespace.constants(false).each do |const_name|
1193
- mod = namespace.const_get(const_name, false)
1194
- next unless mod.is_a?(Module)
1195
-
1196
- providers << mod if mod.respond_to?(:resolve) && mod.respond_to?(:provider_name)
1197
- providers.concat(collect_identity_providers(mod, visited))
1198
- rescue StandardError
1199
- next
1200
- end
1201
-
1202
- providers
1203
- end
1204
-
1205
1131
  def bootstrap_log_level(cli_level)
1206
1132
  cli_level = nil if cli_level.respond_to?(:empty?) && cli_level.empty?
1207
1133
  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.8.15'
4
+ VERSION = '1.9.0'
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.8.15
4
+ version: 1.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -853,12 +853,16 @@ files:
853
853
  - lib/legion/graph/exporter.rb
854
854
  - lib/legion/guardrails.rb
855
855
  - lib/legion/helpers/context.rb
856
+ - lib/legion/identity.rb
856
857
  - lib/legion/identity/broker.rb
858
+ - lib/legion/identity/grant.rb
857
859
  - lib/legion/identity/lease.rb
858
860
  - lib/legion/identity/lease_renewer.rb
859
861
  - lib/legion/identity/middleware.rb
860
862
  - lib/legion/identity/process.rb
861
863
  - lib/legion/identity/request.rb
864
+ - lib/legion/identity/resolver.rb
865
+ - lib/legion/identity/trust.rb
862
866
  - lib/legion/ingress.rb
863
867
  - lib/legion/isolation.rb
864
868
  - lib/legion/leader.rb