legionio 1.9.28 → 1.9.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: 166725a37cf7c1d479070df9c3a19a53ec6fa915e1fe29b9e881358136392dc5
4
- data.tar.gz: '09fe43d7158d9fea1439f820c231efeb25a3a1766b3c988411504f28cdbabf1d'
3
+ metadata.gz: 952eb59489faed844f57a13d5fb71a10d4f6e737b3af38f96cc9ab7beec5a9da
4
+ data.tar.gz: e8af435fd6f5caa5324f678c45a38d7c8bde94a801a80c8380032f43d9721fbf
5
5
  SHA512:
6
- metadata.gz: 424b1f6992f9bce5ae26fa0f09c986ee9bf961a9beccf64d3498f3db651747bbcab3ef8bb6ee25f45cdfc6c1df9e0ddd031132a253b334edeba1a63b2a1532cd
7
- data.tar.gz: 2462fd9c2277a81b381f4b91c3674930606e6c80460d708c36d252a7a2a7e1475f9b397daa3d55c2c8b17b1c33dbb50eb2ceec7acd0dd3abfe59376d4913ef0a
6
+ metadata.gz: 1cd3acf9f4e8af3da0db483123afbeb5b9c8b69430dbaf538474523a3d5fb02aa8e5fd9ac262244a61ed1f9a0ed7add934bdc59ecdd239a3cd8621ddbdec02e4
7
+ data.tar.gz: dc7a8cc98adcd63d75419cc29d88f025539185fc2c9254d9c9a1b8abdeaeadc1ff7e3e1481de1352e398a63826c74e41975f8c13708e462a6dcebb35fd97ef4b
data/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.9.29] - 2026-05-11
6
+
7
+ ### Fixed
8
+ - `Subscription#activate` now checks `channel.open?` before calling `subscribe_with`; if closed, it re-prepares once and retries, preventing silent activation failures when a channel is closed between prepare and activate.
9
+ - `Transport` mixin now guards `auto_create_dlx_exchange` and `auto_create_dlx_queue` with a `remote_invocable?` check — non-remote extensions no longer attempt to create dead-letter exchanges and queues they never use.
10
+ - DLX exchange type corrected from `fanout` to `topic` for consistency with the rest of the exchange topology.
11
+ - Identity resolver DB persistence now uses Sequel models (`Identity::Provider`, `Identity::Principal`, `Identity::Identity`, `Identity::AuditLog`) instead of raw `Legion::Data.db` dataset calls that didn't exist on the module.
12
+ - Identity audit API endpoint now references correct column names (`detail_payload`, `node_ref`, `session_ref`) matching the schema.
13
+ - Fixed `LeaseRenewer#log_renewal_failure` to fall back to `$stderr` when `Legion::Logging` is not yet loaded, matching the original contract.
14
+ - Fixed `Legion::Service#log_privacy_mode_status`, `#shutdown_component`, and TLS-fallback logging specs to assert against `emit_tagged` (the actual dispatch path used by `Legion::Logging::Helper`) rather than `Legion::Logging.warn/info` directly.
15
+ - Fixed `Cluster::Leader` boot integration spec to stub `Legion::Settings[:logging]` so `log` helper initialization does not raise on unexpectedly-received arguments.
16
+
17
+ ### Changed
18
+ - Added `remote_invocable_extension?` helper to the `Transport` module; returns `lex_class.remote_invocable?` when available, `true` otherwise.
19
+ - Refactored `Identity::Resolver#persist_to_db` into extracted helpers (`upsert_providers`, `upsert_principal`, `upsert_identities`, `upsert_single_identity`) to reduce method complexity and improve readability.
20
+ - Replaced hand-rolled `log_warn`/`log_debug` methods in `Identity::Resolver`, `Identity::Broker`, and `Identity::LeaseRenewer` with `include Legion::Logging::Helper` and standard `log.debug`/`log.warn` calls.
21
+ - Added debug logging throughout `Identity::Resolver` for registration, resolution, auth racing, binding, and DB persistence.
22
+ - LLM inference API now passes `instance` and `tier` routing hints from request body through to `Legion::LLM::Inference::Request`.
23
+
5
24
  ## [1.9.28] - 2026-05-08
6
25
 
7
26
  ### Fixed
@@ -35,8 +35,8 @@ module Legion
35
35
  records = dataset.order(Sequel.desc(:created_at)).limit(100).all
36
36
  json_collection(records.map do |r|
37
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 }
38
+ trust_level: r.trust_level, detail_payload: r.detail_payload,
39
+ node_ref: r.node_ref, session_ref: r.session_ref, created_at: r.created_at }
40
40
  end)
41
41
  end
42
42
  end
@@ -268,16 +268,23 @@ module Legion
268
268
  end
269
269
 
270
270
  caller_metadata = body[:metadata].is_a?(Hash) ? body[:metadata] : {}
271
+ instance = body[:instance]
272
+ tier = body[:tier]
271
273
  request_args = {
272
274
  messages: messages,
273
275
  system: body[:system],
274
- routing: { provider: provider, model: model },
276
+ routing: { provider: provider, model: model, instance: instance }.compact,
275
277
  caller: caller_ctx,
276
278
  conversation_id: body[:conversation_id],
277
279
  metadata: caller_metadata.merge(requested_tools: requested_tools),
278
280
  stream: streaming,
279
281
  cache: { strategy: :default, cacheable: true }
280
282
  }
283
+ if tier
284
+ halt 400, Legion::JSON.dump({ error: 'invalid tier' }) unless tier.is_a?(String)
285
+ halt 400, Legion::JSON.dump({ error: 'invalid tier' }) unless %w[local fleet auto].include?(tier)
286
+ request_args[:extra] = { tier: tier.to_sym }
287
+ end
281
288
  request_args[:tools] = tool_classes if tools_present
282
289
 
283
290
  req = Legion::LLM::Inference::Request.build(**request_args)
@@ -55,10 +55,27 @@ module Legion
55
55
  description: 'LLM routing and provider integration (no cognitive stack)',
56
56
  gems: %w[
57
57
  legion-llm lex-llm lex-llm-anthropic lex-llm-azure-foundry
58
- lex-llm-bedrock lex-llm-gemini lex-llm-ledger lex-llm-mlx
58
+ lex-llm-bedrock lex-llm-gemini lex-llm-mlx
59
59
  lex-llm-ollama lex-llm-openai lex-llm-vertex lex-llm-vllm
60
60
  ]
61
61
  },
62
+ gaia: {
63
+ description: 'Cognitive coordination engine + agentic extensions (GAIA stack)',
64
+ gems: %w[
65
+ legion-gaia
66
+ lex-agentic-affect lex-agentic-attention lex-agentic-defense
67
+ lex-agentic-executive lex-agentic-homeostasis lex-agentic-inference
68
+ lex-agentic-integration lex-agentic-language lex-agentic-learning
69
+ lex-agentic-memory lex-agentic-self lex-agentic-social
70
+ lex-synapse lex-mind-growth lex-tick
71
+ ]
72
+ },
73
+ identity: {
74
+ description: 'Identity and access management (RBAC + identity providers)',
75
+ gems: %w[
76
+ legion-rbac lex-identity-system lex-identity-kerberos
77
+ ]
78
+ },
62
79
  channels: {
63
80
  description: 'Channel adapters for chat platforms',
64
81
  gems: %w[lex-slack lex-microsoft_teams]
@@ -153,6 +170,18 @@ module Legion
153
170
  install_pack(:llm)
154
171
  end
155
172
 
173
+ desc 'gaia', 'Install cognitive coordination engine and agentic extensions (GAIA stack)'
174
+ option :dry_run, type: :boolean, default: false, desc: 'Show what would be installed without installing'
175
+ def gaia
176
+ install_pack(:gaia)
177
+ end
178
+
179
+ desc 'identity', 'Install identity and access management (RBAC + identity providers)'
180
+ option :dry_run, type: :boolean, default: false, desc: 'Show what would be installed without installing'
181
+ def identity
182
+ install_pack(:identity)
183
+ end
184
+
156
185
  desc 'channels', 'Install channel adapters (Slack, Teams)'
157
186
  option :dry_run, type: :boolean, default: false, desc: 'Show what would be installed without installing'
158
187
  def channels
@@ -494,6 +523,14 @@ module Legion
494
523
  puts ' Next steps:'
495
524
  puts ' legion chat # interactive AI conversation'
496
525
  puts ' legion llm status # check provider connectivity'
526
+ when :gaia
527
+ puts ' Next steps:'
528
+ puts ' legion start # start daemon with cognitive stack'
529
+ puts ' legion start --lite # single-process, no external services'
530
+ when :identity
531
+ puts ' Next steps:'
532
+ puts ' Configure RBAC in settings: {"rbac": {"enabled": true}}'
533
+ puts ' legion start # start daemon with identity services'
497
534
  when :channels
498
535
  puts ' Next steps:'
499
536
  puts ' Configure channels in settings: {"gaia": {"channels": {"slack": {"enabled": true}}}}'
@@ -110,7 +110,20 @@ module Legion
110
110
  log.warn "[Subscription] skipping activate for #{lex_name}/#{runner_name}: no consumer (prepare failed?)"
111
111
  return
112
112
  end
113
- @queue.subscribe_with(@consumer)
113
+
114
+ if @queue.channel.open?
115
+ @queue.subscribe_with(@consumer)
116
+ else
117
+ log.warn "[Subscription] channel closed before activate for #{lex_name}/#{runner_name}, re-preparing"
118
+ prepare
119
+ if @consumer && @queue.channel.open?
120
+ @queue.subscribe_with(@consumer)
121
+ else
122
+ log.error "[Subscription] re-prepare failed for #{lex_name}/#{runner_name}, skipping activate"
123
+ return
124
+ end
125
+ end
126
+
114
127
  log.info "[Subscription] activated: #{lex_name}/#{runner_name} (consumer registered)"
115
128
  end
116
129
 
@@ -71,6 +71,8 @@ module Legion
71
71
  end
72
72
 
73
73
  def auto_create_dlx_exchange
74
+ return unless remote_invocable_extension?
75
+
74
76
  dlx = if transport_class::Exchanges.const_defined?('Dlx', false)
75
77
  transport_class::Exchanges::Dlx
76
78
  else
@@ -89,6 +91,7 @@ module Legion
89
91
  end
90
92
 
91
93
  def auto_create_dlx_queue
94
+ return unless remote_invocable_extension?
92
95
  return if transport_class::Queues.const_defined?('Dlx', false)
93
96
 
94
97
  special_name = default_exchange.new.exchange_name
@@ -207,6 +210,12 @@ module Legion
207
210
  def additional_e_to_q
208
211
  []
209
212
  end
213
+
214
+ def remote_invocable_extension?
215
+ return lex_class.remote_invocable? if lex_class.respond_to?(:remote_invocable?)
216
+
217
+ true
218
+ end
210
219
  end
211
220
  end
212
221
  end
@@ -10,6 +10,8 @@ module Legion
10
10
  AUDIT_DROP_LOG_INTERVAL = 100
11
11
 
12
12
  class << self
13
+ include Legion::Logging::Helper
14
+
13
15
  def token_for(provider_name, qualifier: nil, for_context: nil, purpose: nil, context: nil)
14
16
  name = provider_name.to_sym
15
17
  resolved = resolve_qualifier(name, qualifier: qualifier, for_context: for_context)
@@ -235,7 +237,7 @@ module Legion
235
237
 
236
238
  if audit_queue.size >= AUDIT_QUEUE_MAX
237
239
  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?
240
+ log.warn("Audit queue full, dropping event (total drops: #{drops})") if (drops % AUDIT_DROP_LOG_INTERVAL).zero?
239
241
  else
240
242
  audit_queue.push(event)
241
243
  end
@@ -289,7 +291,7 @@ module Legion
289
291
  nil
290
292
  end
291
293
  rescue StandardError => e
292
- log_warn("Broker.db_groups failed: #{e.message}")
294
+ log.warn("Broker.db_groups failed: #{e.message}")
293
295
  []
294
296
  end
295
297
 
@@ -298,14 +300,6 @@ module Legion
298
300
  Legion::Data.respond_to?(:connected?) &&
299
301
  Legion::Data.connected?
300
302
  end
301
-
302
- def log_warn(message)
303
- if defined?(Legion::Logging) && Legion::Logging.respond_to?(:warn)
304
- Legion::Logging.warn("[Identity::Broker] #{message}")
305
- else
306
- $stderr.puts "[Identity::Broker] #{message}" # rubocop:disable Style/StderrPuts
307
- end
308
- end
309
303
  end
310
304
 
311
305
  # Initialize atomics at module definition time
@@ -5,6 +5,8 @@ require 'concurrent'
5
5
  module Legion
6
6
  module Identity
7
7
  class LeaseRenewer
8
+ include Legion::Logging::Helper
9
+
8
10
  attr_reader :provider_name, :provider
9
11
 
10
12
  BACKOFF_SLEEP = 5
@@ -70,11 +72,10 @@ module Legion
70
72
  end
71
73
 
72
74
  def log_renewal_failure(error)
73
- message = "[LeaseRenewer][#{@provider_name}] renewal failed: #{error.message}"
74
- if defined?(Legion::Logging) && Legion::Logging.respond_to?(:warn)
75
- Legion::Logging.warn(message)
75
+ if defined?(Legion::Logging)
76
+ log.warn("renewal failed: #{error.message}")
76
77
  else
77
- $stderr.puts message # rubocop:disable Style/StderrPuts
78
+ $stderr.puts "[LeaseRenewer][#{@provider_name}] renewal failed: #{error.message}" # rubocop:disable Style/StderrPuts
78
79
  end
79
80
  end
80
81
  end
@@ -13,25 +13,34 @@ module Legion
13
13
  TIMEOUT_SECONDS = 5
14
14
 
15
15
  class << self
16
+ include Legion::Logging::Helper
17
+
16
18
  def register(provider)
17
19
  return if @providers.any? { |p| p.provider_name == provider.provider_name }
18
20
 
21
+ log.debug("register: #{provider.provider_name} type=#{provider.provider_type} trust=#{provider.trust_level}")
19
22
  @providers << provider
20
23
  end
21
24
 
22
25
  def resolve!(timeout: TIMEOUT_SECONDS)
26
+ log.debug("resolve!: starting with #{@providers.size} providers, timeout=#{timeout}s")
23
27
  drain_pending_registrations
24
28
 
25
29
  auth_providers, profile_providers, fallback_providers = partition_providers
30
+ log.debug("resolve!: partitioned auth=#{auth_providers.map(&:provider_name)} " \
31
+ "profile=#{profile_providers.map(&:provider_name)} " \
32
+ "fallback=#{fallback_providers.map(&:provider_name)}")
26
33
 
27
34
  winning_provider, winning_result, provider_results = resolve_auth(auth_providers, timeout: timeout)
28
35
 
29
36
  if winning_provider.nil?
37
+ log.debug('resolve!: no auth winner, trying fallback providers')
30
38
  winning_provider, winning_result, fallback_results = resolve_auth(fallback_providers, timeout: timeout)
31
39
  provider_results.merge!(fallback_results) if fallback_results
32
40
  end
33
41
 
34
42
  unless winning_provider
43
+ log.debug('resolve!: no provider resolved, identity unresolved')
35
44
  @resolved.make_false
36
45
  @composite.set(nil)
37
46
  return nil
@@ -40,8 +49,10 @@ module Legion
40
49
  canonical = winning_result[:canonical_name]
41
50
  trust_level = winning_provider.trust_level
42
51
  source = winning_provider.provider_name
52
+ log.debug("resolve!: winner=#{source} canonical=#{canonical} trust=#{trust_level}")
43
53
 
44
54
  profile_data = resolve_profiles(profile_providers, canonical, timeout: timeout)
55
+ log.debug("resolve!: profiles resolved groups=#{profile_data[:groups].size} profile_keys=#{profile_data[:profile].keys}")
45
56
 
46
57
  composite = assemble_composite(
47
58
  provider_results, profile_data,
@@ -51,6 +62,7 @@ module Legion
51
62
  )
52
63
 
53
64
  bind_and_persist(winning_provider, composite, trust_level)
65
+ log.debug("resolve!: complete canonical=#{composite[:canonical_name]} providers=#{composite[:providers].keys}")
54
66
  composite
55
67
  end
56
68
 
@@ -58,6 +70,8 @@ module Legion
58
70
  current = @composite.get
59
71
  return unless current
60
72
 
73
+ log.debug("upgrade!: provider=#{provider.provider_name} trust=#{provider.trust_level} current_canonical=#{current[:canonical_name]}")
74
+
61
75
  new_trust = provider.trust_level
62
76
  new_canonical = result[:canonical_name] || current[:canonical_name]
63
77
  canonical_changed = new_canonical != current[:canonical_name]
@@ -109,6 +123,7 @@ module Legion
109
123
 
110
124
  persist_identity_json(new_canonical, updated[:kind]) unless new_trust == :unverified
111
125
 
126
+ log.debug("upgrade!: complete canonical=#{new_canonical} trust=#{effective_trust} canonical_changed=#{canonical_changed}")
112
127
  updated
113
128
  end
114
129
 
@@ -145,6 +160,7 @@ module Legion
145
160
  pending = Legion::Identity.pending_registrations
146
161
  return if pending.nil? || pending.empty?
147
162
 
163
+ log.debug("drain_pending_registrations: draining #{pending.size} pending providers")
148
164
  drained = []
149
165
  drained << pending.shift until pending.empty?
150
166
  drained.each { |p| register(p) }
@@ -172,6 +188,7 @@ module Legion
172
188
  def resolve_auth(auth_providers, timeout:)
173
189
  return [nil, nil, {}] if auth_providers.empty?
174
190
 
191
+ log.debug("resolve_auth: racing #{auth_providers.map(&:provider_name)} timeout=#{timeout}s")
175
192
  futures = auth_providers.map do |provider|
176
193
  Concurrent::Promises.future { provider.resolve }
177
194
  end
@@ -179,10 +196,13 @@ module Legion
179
196
  deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + timeout
180
197
  provider_results = {}
181
198
  auth_providers.zip(futures).each do |provider, future|
199
+ result = nil
182
200
  remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
183
201
  future.wait(remaining.positive? ? remaining : 0)
184
202
  result = future.value(0) if future.resolved?
185
203
  status = auth_future_status(future, result)
204
+ log.debug("resolve_auth: #{provider.provider_name} status=#{status}" \
205
+ "#{" canonical=#{result[:canonical_name]}" if status == :resolved}")
186
206
 
187
207
  provider_results[provider.provider_name] = {
188
208
  status: status,
@@ -195,6 +215,7 @@ module Legion
195
215
 
196
216
  resolved_entries = provider_results.select { |_, v| v[:status] == :resolved }
197
217
  if resolved_entries.empty?
218
+ log.debug('resolve_auth: no providers resolved')
198
219
  [nil, nil, provider_results]
199
220
  else
200
221
  winner_name = resolved_entries.min_by do |_, v|
@@ -202,6 +223,7 @@ module Legion
202
223
  [-p.priority, p.trust_weight]
203
224
  end&.first
204
225
 
226
+ log.debug("resolve_auth: winner=#{winner_name}")
205
227
  winner_info = provider_results[winner_name]
206
228
  [winner_info[:provider], winner_info[:result], provider_results]
207
229
  end
@@ -302,11 +324,13 @@ module Legion
302
324
  end
303
325
 
304
326
  def bind_and_persist(winning_provider, composite, trust_level)
327
+ log.debug("bind_and_persist: binding provider=#{winning_provider.provider_name} trust=#{trust_level}")
305
328
  Legion::Identity::Process.bind!(winning_provider, composite) if defined?(Legion::Identity::Process)
306
329
 
307
330
  if defined?(Legion::Settings) && Legion::Settings.respond_to?(:loader) && Legion::Settings.loader.respond_to?(:settings)
308
331
  Legion::Settings.loader.settings[:client] ||= {}
309
332
  Legion::Settings.loader.settings[:client][:name] = Legion::Identity::Process.queue_prefix
333
+ log.debug("bind_and_persist: client name set to #{Legion::Identity::Process.queue_prefix}")
310
334
  end
311
335
 
312
336
  persist_to_db(composite)
@@ -314,72 +338,26 @@ module Legion
314
338
 
315
339
  @composite.set(composite)
316
340
  @resolved.make_true
341
+ log.debug('bind_and_persist: resolved=true')
317
342
  end
318
343
 
319
- def persist_to_db(composite) # rubocop:disable Metrics/MethodLength
320
- return unless defined?(Legion::Data) && Legion::Data.respond_to?(:connected?) && Legion::Data.connected?
321
-
322
- now = Time.now.utc
323
- db = Legion::Data.db
324
-
325
- composite[:providers]&.each_key do |name|
326
- db[:identity_providers].insert_conflict(
327
- target: :name,
328
- update: { updated_at: now }
329
- ).insert(
330
- uuid: SecureRandom.uuid,
331
- name: name.to_s,
332
- provider_type: 'authenticate',
333
- facing: 'both',
334
- source: 'resolver',
335
- enabled: true,
336
- created_at: now,
337
- updated_at: now
338
- )
344
+ def persist_to_db(composite)
345
+ unless defined?(Legion::Data) && Legion::Data.respond_to?(:connected?) && Legion::Data.connected?
346
+ log.debug('persist_to_db: skipped — Legion::Data not connected')
347
+ return
339
348
  end
340
349
 
341
- db[:identity_principals].insert_conflict(
342
- target: %i[canonical_name kind],
343
- update: { last_seen_at: now, updated_at: now }
344
- ).insert(
345
- uuid: SecureRandom.uuid,
346
- canonical_name: composite[:canonical_name],
347
- kind: composite[:kind].to_s,
348
- active: true,
349
- last_seen_at: now,
350
- created_at: now,
351
- updated_at: now
352
- )
353
-
354
- principal_row = db[:identity_principals].where(
355
- canonical_name: composite[:canonical_name], kind: composite[:kind].to_s
356
- ).first
357
- principal_id = principal_row[:id] if principal_row
358
-
359
- composite[:aliases]&.each do |provider_name, identities|
360
- provider_row = db[:identity_providers].where(name: provider_name.to_s).first
361
- next unless provider_row
350
+ log.debug("persist_to_db: persisting canonical=#{composite[:canonical_name]} providers=#{composite[:providers]&.keys}")
351
+ now = Time.now.utc
352
+ provider_model = Legion::Data::Model::Identity::Provider
353
+ audit_model = Legion::Data::Model::Identity::AuditLog
362
354
 
363
- Array(identities).each do |ident|
364
- db[:identities].insert_conflict(
365
- target: %i[principal_id provider_id provider_identity_key],
366
- update: { last_authenticated_at: now, updated_at: now }
367
- ).insert(
368
- uuid: SecureRandom.uuid,
369
- principal_id: principal_id,
370
- provider_id: provider_row[:id],
371
- provider_identity_key: ident,
372
- active: true,
373
- last_authenticated_at: now,
374
- created_at: now,
375
- updated_at: now
376
- )
377
- end
378
- end
355
+ upsert_providers(composite, provider_model, now)
356
+ principal = upsert_principal(composite, now)
357
+ upsert_identities(composite, provider_model, principal, now)
379
358
 
380
- db[:identity_audit_log].insert(
381
- uuid: SecureRandom.uuid,
382
- principal_id: principal_id,
359
+ audit_model.create(
360
+ principal_id: principal.id,
383
361
  event_type: 'identity.resolved',
384
362
  provider_name: composite[:source].to_s,
385
363
  trust_level: composite[:trust]&.to_s,
@@ -392,11 +370,79 @@ module Legion
392
370
  }
393
371
  ),
394
372
  node_ref: composite[:node_id],
395
- session_ref: @session_id,
396
- created_at: now
373
+ session_ref: @session_id
397
374
  )
398
375
  rescue StandardError => e
399
- log_warn("DB persistence failed: #{e.message}")
376
+ log.warn("DB persistence failed: #{e.message}")
377
+ end
378
+
379
+ def upsert_providers(composite, provider_model, now)
380
+ composite[:providers]&.each_key do |name|
381
+ existing = provider_model.where(name: name.to_s).first
382
+ if existing
383
+ existing.update(updated_at: now)
384
+ else
385
+ provider_model.create(
386
+ name: name.to_s,
387
+ provider_type: 'authenticate',
388
+ facing: 'both',
389
+ source: 'resolver',
390
+ enabled: true
391
+ )
392
+ end
393
+ end
394
+ end
395
+
396
+ def upsert_principal(composite, now)
397
+ principal_model = Legion::Data::Model::Identity::Principal
398
+ principal = principal_model.where(
399
+ canonical_name: composite[:canonical_name],
400
+ kind: composite[:kind].to_s
401
+ ).first
402
+
403
+ if principal
404
+ principal.update(last_seen_at: now, updated_at: now)
405
+ principal
406
+ else
407
+ principal_model.create(
408
+ canonical_name: composite[:canonical_name],
409
+ kind: composite[:kind].to_s,
410
+ active: true,
411
+ last_seen_at: now
412
+ )
413
+ end
414
+ end
415
+
416
+ def upsert_identities(composite, provider_model, principal, now)
417
+ identity_model = Legion::Data::Model::Identity::Identity
418
+ composite[:aliases]&.each do |provider_name, identities|
419
+ provider_row = provider_model.where(name: provider_name.to_s).first
420
+ next unless provider_row
421
+
422
+ Array(identities).each do |ident|
423
+ upsert_single_identity(identity_model, principal, provider_row, ident, now)
424
+ end
425
+ end
426
+ end
427
+
428
+ def upsert_single_identity(identity_model, principal, provider_row, ident, now)
429
+ existing = identity_model.where(
430
+ principal_id: principal.id,
431
+ provider_id: provider_row.id,
432
+ provider_identity_key: ident
433
+ ).first
434
+
435
+ if existing
436
+ existing.update(last_authenticated_at: now, updated_at: now)
437
+ else
438
+ identity_model.create(
439
+ principal_id: principal.id,
440
+ provider_id: provider_row.id,
441
+ provider_identity_key: ident,
442
+ active: true,
443
+ last_authenticated_at: now
444
+ )
445
+ end
400
446
  end
401
447
 
402
448
  def persist_identity_json(canonical_name, kind)
@@ -412,7 +458,7 @@ module Legion
412
458
  end
413
459
  File.write(path, json)
414
460
  rescue StandardError => e
415
- log_warn("identity.json write failed: #{e.message}")
461
+ log.warn("identity.json write failed: #{e.message}")
416
462
  end
417
463
 
418
464
  def handle_canonical_change(old_canonical, new_canonical, _composite)
@@ -424,25 +470,15 @@ module Legion
424
470
 
425
471
  return unless defined?(Legion::Data) && Legion::Data.respond_to?(:connected?) && Legion::Data.connected?
426
472
 
427
- old_row = Legion::Data.db[:identity_principals].where(canonical_name: old_canonical).first
428
- Legion::Data.db[:identity_audit_log].insert(
429
- uuid: SecureRandom.uuid,
430
- principal_id: old_row&.dig(:id),
473
+ old_principal = Legion::Data::Model::Identity::Principal.where(canonical_name: old_canonical).first
474
+ Legion::Data::Model::Identity::AuditLog.create(
475
+ principal_id: old_principal&.id,
431
476
  event_type: 'identity.canonical_changed',
432
477
  provider_name: 'resolver',
433
- detail_payload: Legion::JSON.dump({ old: old_canonical, new: new_canonical }),
434
- created_at: Time.now
478
+ detail_payload: Legion::JSON.dump({ old: old_canonical, new: new_canonical })
435
479
  )
436
480
  rescue StandardError => e
437
- log_warn("canonical change handling failed: #{e.message}")
438
- end
439
-
440
- def log_warn(message)
441
- if defined?(Legion::Logging) && Legion::Logging.respond_to?(:warn)
442
- Legion::Logging.warn("[Identity::Resolver] #{message}")
443
- else
444
- $stderr.puts "[Identity::Resolver] #{message}" # rubocop:disable Style/StderrPuts
445
- end
481
+ log.warn("canonical change handling failed: #{e.message}")
446
482
  end
447
483
  end
448
484
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.9.28'
4
+ VERSION = '1.9.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.9.28
4
+ version: 1.9.29
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity