legionio 1.6.8 → 1.6.9

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: cd3587ed7997208005a0a8746aca477701811f8e506c1d8d9345a40d180317fc
4
- data.tar.gz: e805096f6577c2b1317807d50beab27bbb91527e2a02807231596fbd6a1d91ca
3
+ metadata.gz: e67d5abaf8c274cad673af9996e29577949a9e945c66ee099b1c8e23eeff4a3f
4
+ data.tar.gz: 4c3113323f086005de660cd84128036d59919ee2f2482ce84a855334bab007c4
5
5
  SHA512:
6
- metadata.gz: 5d6c20753f9803fc34cd4bb577304bc31d708df54f9e49f266dbb32073c3d06835e2d625efbed610244b5888085d823222627fb07ad889c1ff85d37d028f603a
7
- data.tar.gz: 0a1dbbff5e24580f3b8b97fb434c626f029f449501b138d0a29f9fcb0eb15edd05808c5b34353260a6bdc4808d4f747362d8f4e6e9f3afdd47362b56035c1e0f
6
+ metadata.gz: d221a3d63b90d155b24fabd47594aef550467d6136a96092208a7f710455f87f416ae4110815e323c7bfcb83aec13ec4c41dff7b2db14b0846dafbe55ca9be66
7
+ data.tar.gz: b495e7daddcbb0e5cbda7c93f152ca285e044972ee8b74c9d432f91e97c4536d1087539f7b8daf784910cb359935453b6aa1f9e972c8d81b688d55c8986e2f34
data/.rubocop.yml CHANGED
@@ -32,6 +32,7 @@ Metrics/BlockLength:
32
32
  Max: 40
33
33
  Exclude:
34
34
  - 'spec/**/*'
35
+ - 'integration/**/*'
35
36
  - 'legionio.gemspec'
36
37
  - 'lib/legion/cli/chat_command.rb'
37
38
  - 'lib/legion/cli/plan_command.rb'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Legion Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ### Added
6
+ - End-to-end integration test for TBI Phase 5 self-generating functions loop (9 examples)
7
+ - Test dependencies: lex-codegen, lex-eval added to Gemfile for integration testing
8
+ - Specs for `legion codegen` CLI subcommand (8 subcommands, 22 examples)
9
+ - Specs for `/api/codegen/*` API routes (8 routes, 20 examples)
10
+ - Specs for `setup_generated_functions` boot loading in Service (4 examples)
11
+
12
+ ### Fixed
13
+ - Guard `Legion::Transport::Messages::Dynamic` stub definition in integration spec with `unless defined?` to prevent redefinition conflicts when real implementations are present
14
+ - Wrap `lex-codegen` and `lex-eval` requires in `LoadError` rescue guards in integration spec; sets `LEGION_CODEGEN_EXTENSION_AVAILABLE` / `LEGION_EVAL_EXTENSION_AVAILABLE` flags and skips entire example group via `before(:all)` when extensions are unavailable
15
+ - Move `Legion::LLM.chat` stub to `RSpec.configure before(:each)` block so it always intercepts regardless of whether the real `legion-llm` gem is loaded, preventing external LLM calls in integration tests
16
+ - Fix `service_setup_apollo_spec` "starts Apollo::Local" example: stub `Legion::Apollo.start` to prevent internal double-call of `Apollo::Local.start`
17
+
18
+ ## [1.6.9] - 2026-03-26
19
+
20
+ ### Added
21
+ - `Helpers::Secret` mixin with `SecretAccessor` for per-user and per-lex Vault KV v2 secret access
22
+ - Identity resolution chain: Kerberos principal -> Entra UPN -> explicit user -> ENV['USER']
23
+ - `secret[:name]` / `secret[:name] = { ... }` / `secret.write` / `secret.exist?` / `secret.delete`
24
+ - `shared: true` option for extension-scoped (non-user) secrets
25
+
3
26
  ## [1.6.8] - 2026-03-26
4
27
 
5
28
  ### Added
data/Gemfile CHANGED
@@ -23,6 +23,8 @@ gem 'mysql2'
23
23
 
24
24
  group :test do
25
25
  gem 'graphql'
26
+ gem 'lex-codegen'
27
+ gem 'lex-eval'
26
28
  gem 'rack-test'
27
29
  gem 'rake'
28
30
  gem 'rspec'
@@ -0,0 +1,398 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require 'tmpdir'
5
+ require 'fileutils'
6
+
7
+ # Load core gems
8
+ require 'legion/json'
9
+ require 'legion/logging'
10
+ require 'legion/settings'
11
+
12
+ # Stub modules that may not be available in isolation
13
+ unless defined?(Legion::Transport::Messages::Dynamic)
14
+ module Legion
15
+ module Transport
16
+ module Messages
17
+ class Dynamic
18
+ attr_reader :function, :data
19
+
20
+ def initialize(function:, data:, **)
21
+ @function = function
22
+ @data = data
23
+ end
24
+
25
+ def publish
26
+ Legion::Transport::Local.publish('codegen', @function, Legion::JSON.dump(@data))
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ # Ensure Legion::LLM module exists so it can be stubbed, but don't overwrite a real implementation.
35
+ unless defined?(Legion::LLM)
36
+ module Legion
37
+ module LLM
38
+ end
39
+ end
40
+ end
41
+
42
+ # Load transport Local for InProcess mode
43
+ require 'legion/transport/local'
44
+
45
+ # Load codegen extension
46
+ begin
47
+ require 'legion/extensions/codegen'
48
+ LEGION_CODEGEN_EXTENSION_AVAILABLE = true
49
+ rescue LoadError => e
50
+ LEGION_CODEGEN_EXTENSION_AVAILABLE = false
51
+ warn "lex-codegen / legion codegen extension not available; skipping dependent behavior: #{e.message}"
52
+ end
53
+
54
+ # Load eval extension (only code_review runner + security evaluator)
55
+ begin
56
+ require 'legion/extensions/eval'
57
+ LEGION_EVAL_EXTENSION_AVAILABLE = true
58
+ rescue LoadError => e
59
+ LEGION_EVAL_EXTENSION_AVAILABLE = false
60
+ warn "lex-eval / legion eval extension not available; skipping dependent behavior: #{e.message}"
61
+ end
62
+
63
+ # Stub MCP Server if not available
64
+ unless defined?(Legion::MCP::Server)
65
+ module Legion
66
+ module MCP
67
+ module Server
68
+ @tool_registry = []
69
+ @tool_registry_lock = Mutex.new
70
+
71
+ class << self
72
+ attr_reader :tool_registry
73
+
74
+ def register_tool(tool_class)
75
+ @tool_registry_lock.synchronize do
76
+ return if tool_registry.any? { |tc| tc.respond_to?(:tool_name) && tc.tool_name == tool_class.tool_name }
77
+
78
+ tool_registry << tool_class
79
+ end
80
+ end
81
+
82
+ def unregister_tool(tool_name)
83
+ @tool_registry_lock.synchronize do
84
+ tool_registry.reject! { |tc| tc.respond_to?(:tool_name) && tc.tool_name == tool_name }
85
+ end
86
+ end
87
+
88
+ def reset_caches!; end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ LLM_STUB_CODE = <<~RUBY
96
+ # frozen_string_literal: true
97
+
98
+ module Legion
99
+ module Generated
100
+ module GreetUser
101
+ extend self
102
+
103
+ def greet(name:)
104
+ { success: true, greeting: "Hello, \#{name}!" }
105
+ end
106
+ end
107
+ end
108
+ end
109
+ RUBY
110
+
111
+ RSpec.configure do |config|
112
+ config.disable_monkey_patching!
113
+ config.expect_with(:rspec) { |c| c.syntax = :expect }
114
+
115
+ config.before(:each) do
116
+ allow(Legion::LLM).to receive(:chat) do |messages:, _caller: nil, **_kwargs|
117
+ messages.last[:content]
118
+ Struct.new(:content).new(LLM_STUB_CODE)
119
+ end
120
+ end
121
+ end
122
+
123
+ RSpec.describe 'Self-Generating Functions End-to-End' do
124
+ # Skip this entire example group if the required extensions are not available.
125
+ before(:all) do
126
+ extensions_unavailable =
127
+ (defined?(LEGION_CODEGEN_EXTENSION_AVAILABLE) && !LEGION_CODEGEN_EXTENSION_AVAILABLE) ||
128
+ (defined?(LEGION_EVAL_EXTENSION_AVAILABLE) && !LEGION_EVAL_EXTENSION_AVAILABLE)
129
+
130
+ skip('Legion Codegen/Eval extensions are not available; skipping self-generate integration specs.') if extensions_unavailable
131
+ end
132
+
133
+ let(:output_dir) { Dir.mktmpdir('legion_e2e_codegen') }
134
+
135
+ before do
136
+ # Reset Local transport
137
+ Legion::Transport::Local.reset! if Legion::Transport::Local.respond_to?(:reset!)
138
+
139
+ # Reset GeneratedRegistry (only if Codegen extension is loaded)
140
+ Legion::Extensions::Codegen::Helpers::GeneratedRegistry.reset! if defined?(Legion::Extensions::Codegen::Helpers::GeneratedRegistry)
141
+
142
+ # Configure settings for test
143
+ allow(Legion::Settings).to receive(:dig).and_return(nil)
144
+ allow(Legion::Settings).to receive(:dig).with(:codegen, :self_generate, :enabled).and_return(true)
145
+ allow(Legion::Settings).to receive(:dig).with(:codegen, :self_generate, :cooldown_seconds).and_return(0)
146
+ allow(Legion::Settings).to receive(:dig).with(:codegen, :self_generate, :max_gaps_per_cycle).and_return(5)
147
+ allow(Legion::Settings).to receive(:dig).with(:codegen, :self_generate, :runner_method, :output_dir).and_return(output_dir)
148
+ allow(Legion::Settings).to receive(:dig).with(:codegen, :self_generate, :validation).and_return(
149
+ { syntax_check: true, run_specs: false, llm_review: false, max_retries: 2 }
150
+ )
151
+ allow(Legion::Settings).to receive(:dig).with(:codegen, :self_generate, :corroboration, :enabled).and_return(false)
152
+ allow(Legion::Settings).to receive(:dig).with(:codegen, :self_generate, :corroboration, :min_agents).and_return(2)
153
+ allow(Legion::Settings).to receive(:dig).with(:node, :name).and_return('test-node')
154
+ allow(Legion::Settings).to receive(:[]).and_return(nil)
155
+ end
156
+
157
+ after do
158
+ FileUtils.rm_rf(output_dir)
159
+ end
160
+
161
+ describe 'gap detection -> generation -> validation -> registration' do
162
+ let(:synthetic_gap) do
163
+ {
164
+ gap_id: 'gap_e2e_001',
165
+ gap_type: 'unmatched_intent',
166
+ intent: 'greet user',
167
+ occurrence_count: 3,
168
+ priority: 0.7,
169
+ metadata: {}
170
+ }
171
+ end
172
+
173
+ it 'generates code from a gap and passes validation' do
174
+ # Phase 1: GapSubscriber receives a gap and generates code
175
+ subscriber = Object.new
176
+ subscriber.extend(Legion::Extensions::Codegen::Actor::GapSubscriber)
177
+
178
+ generation = subscriber.action(synthetic_gap)
179
+
180
+ expect(generation[:success]).to be true
181
+ expect(generation[:generation_id]).to start_with('gen_')
182
+ expect(generation[:tier]).to eq(:simple)
183
+ expect(generation[:code]).to include('module Legion')
184
+ expect(generation[:file_path]).to start_with(output_dir)
185
+ expect(File.exist?(generation[:file_path])).to be true
186
+ end
187
+
188
+ it 'validates generated code through the review pipeline' do
189
+ # Phase 1: Generate
190
+ subscriber = Object.new
191
+ subscriber.extend(Legion::Extensions::Codegen::Actor::GapSubscriber)
192
+ generation = subscriber.action(synthetic_gap)
193
+ expect(generation[:success]).to be true
194
+
195
+ # Phase 2: Review (simulating what CodeReviewSubscriber does)
196
+ review = Legion::Extensions::Eval::Runners::CodeReview.review_generated(
197
+ code: generation[:code],
198
+ spec_code: generation[:spec_code],
199
+ context: { gap_type: 'unmatched_intent', intent: 'greet user' }
200
+ )
201
+
202
+ expect(review[:passed]).to be true
203
+ expect(review[:verdict]).to eq(:approve)
204
+ expect(review[:confidence]).to be > 0.0
205
+ expect(review[:stages][:syntax][:passed]).to be true
206
+ expect(review[:stages][:security][:passed]).to be true
207
+ end
208
+
209
+ it 'registers approved code via ReviewHandler' do
210
+ # Phase 1: Generate
211
+ subscriber = Object.new
212
+ subscriber.extend(Legion::Extensions::Codegen::Actor::GapSubscriber)
213
+ generation = subscriber.action(synthetic_gap)
214
+ expect(generation[:success]).to be true
215
+
216
+ # Phase 2: Persist to registry
217
+ registry_record = Legion::Extensions::Codegen::Helpers::GeneratedRegistry.persist(
218
+ generation: {
219
+ id: generation[:generation_id],
220
+ gap_id: generation[:gap_id],
221
+ gap_type: generation[:gap_type],
222
+ tier: generation[:tier],
223
+ name: 'greet_user',
224
+ file_path: generation[:file_path],
225
+ spec_path: generation[:spec_path]
226
+ }
227
+ )
228
+ expect(registry_record[:status]).to eq('pending')
229
+
230
+ # Phase 3: Review
231
+ review_result = {
232
+ generation_id: generation[:generation_id],
233
+ verdict: :approve,
234
+ confidence: 0.95,
235
+ issues: [],
236
+ scores: {}
237
+ }
238
+
239
+ # Phase 4: ReviewHandler processes the verdict
240
+ result = Legion::Extensions::Codegen::Runners::ReviewHandler.handle_verdict(review: review_result)
241
+
242
+ expect(result[:success]).to be true
243
+ expect(result[:action]).to eq(:approved)
244
+
245
+ # Verify registry updated
246
+ record = Legion::Extensions::Codegen::Helpers::GeneratedRegistry.get(id: generation[:generation_id])
247
+ expect(record[:status]).to eq('approved')
248
+ end
249
+
250
+ it 'parks rejected code' do
251
+ subscriber = Object.new
252
+ subscriber.extend(Legion::Extensions::Codegen::Actor::GapSubscriber)
253
+ generation = subscriber.action(synthetic_gap)
254
+ expect(generation[:success]).to be true
255
+
256
+ Legion::Extensions::Codegen::Helpers::GeneratedRegistry.persist(
257
+ generation: {
258
+ id: generation[:generation_id],
259
+ gap_id: generation[:gap_id],
260
+ gap_type: generation[:gap_type],
261
+ tier: generation[:tier],
262
+ name: 'greet_user',
263
+ file_path: generation[:file_path],
264
+ spec_path: generation[:spec_path]
265
+ }
266
+ )
267
+
268
+ result = Legion::Extensions::Codegen::Runners::ReviewHandler.handle_verdict(
269
+ review: { generation_id: generation[:generation_id], verdict: :reject, confidence: 0.1, issues: ['unsafe code'] }
270
+ )
271
+
272
+ expect(result[:success]).to be true
273
+ expect(result[:action]).to eq(:parked)
274
+
275
+ record = Legion::Extensions::Codegen::Helpers::GeneratedRegistry.get(id: generation[:generation_id])
276
+ expect(record[:status]).to eq('parked')
277
+ end
278
+
279
+ it 'retries on revise verdict up to max_retries then parks' do
280
+ subscriber = Object.new
281
+ subscriber.extend(Legion::Extensions::Codegen::Actor::GapSubscriber)
282
+ generation = subscriber.action(synthetic_gap)
283
+ expect(generation[:success]).to be true
284
+
285
+ Legion::Extensions::Codegen::Helpers::GeneratedRegistry.persist(
286
+ generation: {
287
+ id: generation[:generation_id],
288
+ gap_id: generation[:gap_id],
289
+ gap_type: generation[:gap_type],
290
+ tier: generation[:tier],
291
+ name: 'greet_user',
292
+ file_path: generation[:file_path],
293
+ spec_path: generation[:spec_path],
294
+ attempt_count: 2
295
+ }
296
+ )
297
+
298
+ result = Legion::Extensions::Codegen::Runners::ReviewHandler.handle_verdict(
299
+ review: { generation_id: generation[:generation_id], verdict: :revise, confidence: 0.4, issues: ['needs improvement'] }
300
+ )
301
+
302
+ expect(result[:success]).to be true
303
+ expect(result[:action]).to eq(:parked)
304
+ end
305
+
306
+ it 'exercises the full loop: generate -> validate -> register -> boot load' do
307
+ # Step 1: Generate
308
+ subscriber = Object.new
309
+ subscriber.extend(Legion::Extensions::Codegen::Actor::GapSubscriber)
310
+ generation = subscriber.action(synthetic_gap)
311
+ expect(generation[:success]).to be true
312
+
313
+ # Step 2: Validate
314
+ review = Legion::Extensions::Eval::Runners::CodeReview.review_generated(
315
+ code: generation[:code], spec_code: generation[:spec_code], context: {}
316
+ )
317
+ expect(review[:verdict]).to eq(:approve)
318
+
319
+ # Step 3: Persist + Approve
320
+ Legion::Extensions::Codegen::Helpers::GeneratedRegistry.persist(
321
+ generation: {
322
+ id: generation[:generation_id],
323
+ gap_id: generation[:gap_id],
324
+ gap_type: generation[:gap_type],
325
+ tier: generation[:tier],
326
+ name: 'greet_user',
327
+ file_path: generation[:file_path],
328
+ spec_path: generation[:spec_path]
329
+ }
330
+ )
331
+
332
+ Legion::Extensions::Codegen::Runners::ReviewHandler.handle_verdict(
333
+ review: { generation_id: generation[:generation_id], verdict: :approve, confidence: 0.95, issues: [] }
334
+ )
335
+
336
+ # Step 4: Boot load (simulates service restart)
337
+ loaded = Legion::Extensions::Codegen::Helpers::GeneratedRegistry.load_on_boot
338
+ expect(loaded).to eq(1)
339
+
340
+ # Step 5: Verify the generated module is actually loaded
341
+ expect(defined?(Legion::Generated::GreetUser)).to be_truthy
342
+ result = Legion::Generated::GreetUser.greet(name: 'World')
343
+ expect(result[:success]).to be true
344
+ expect(result[:greeting]).to eq('Hello, World!')
345
+ end
346
+ end
347
+
348
+ describe 'tier classification' do
349
+ it 'classifies low occurrence gaps as simple' do
350
+ tier = Legion::Extensions::Codegen::Helpers::TierClassifier.classify(gap: { occurrence_count: 5 })
351
+ expect(tier).to eq(:simple)
352
+ end
353
+
354
+ it 'classifies high occurrence gaps as complex' do
355
+ allow(Legion::Settings).to receive(:dig).with(:codegen, :self_generate, :tier, :simple_max_occurrences).and_return(10)
356
+ tier = Legion::Extensions::Codegen::Helpers::TierClassifier.classify(gap: { occurrence_count: 15 })
357
+ expect(tier).to eq(:complex)
358
+ end
359
+ end
360
+
361
+ describe 'ReviewSubscriber actor' do
362
+ it 'routes verdict through ReviewHandler' do
363
+ subscriber = Object.new
364
+ subscriber.extend(Legion::Extensions::Codegen::Actor::GapSubscriber)
365
+ generation = subscriber.action(
366
+ gap_id: 'gap_rs_001', gap_type: 'unmatched_intent', intent: 'greet user',
367
+ occurrence_count: 3, priority: 0.7
368
+ )
369
+ expect(generation[:success]).to be true
370
+
371
+ Legion::Extensions::Codegen::Helpers::GeneratedRegistry.persist(
372
+ generation: {
373
+ id: generation[:generation_id],
374
+ gap_id: generation[:gap_id],
375
+ gap_type: generation[:gap_type],
376
+ tier: generation[:tier],
377
+ name: 'greet_user',
378
+ file_path: generation[:file_path],
379
+ spec_path: generation[:spec_path]
380
+ }
381
+ )
382
+
383
+ review_sub = Object.new
384
+ review_sub.extend(Legion::Extensions::Codegen::Actor::ReviewSubscriber)
385
+
386
+ result = review_sub.action(
387
+ generation_id: generation[:generation_id],
388
+ verdict: 'approve',
389
+ confidence: 0.9,
390
+ issues: [],
391
+ scores: {}
392
+ )
393
+
394
+ expect(result[:success]).to be true
395
+ expect(result[:action]).to eq(:approved)
396
+ end
397
+ end
398
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'legion/json/helper'
4
+ require_relative 'secret'
4
5
 
5
6
  module Legion
6
7
  module Extensions
@@ -9,6 +10,7 @@ module Legion
9
10
  include Legion::Extensions::Helpers::Core
10
11
  include Legion::Extensions::Helpers::Logger
11
12
  include Legion::JSON::Helper
13
+ include Legion::Extensions::Helpers::Secret
12
14
 
13
15
  module ClassMethods
14
16
  def expose_as_mcp_tool(value = :_unset)
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Helpers
6
+ class SecretAccessor
7
+ def initialize(lex_name:)
8
+ @lex_name = lex_name
9
+ @warned = false
10
+ end
11
+
12
+ def [](name, shared: false, user: nil)
13
+ return nil unless crypt_available?
14
+
15
+ Legion::Crypt.get(resolve_path(name, shared: shared, user: user))
16
+ rescue StandardError => e
17
+ log_warn("secret read failed for #{name}: #{e.message}")
18
+ nil
19
+ end
20
+
21
+ def []=(name, value)
22
+ return unless crypt_available?
23
+
24
+ Legion::Crypt.write(resolve_path(name, shared: false, user: nil), **value)
25
+ rescue StandardError => e
26
+ log_warn("secret write failed for #{name}: #{e.message}")
27
+ end
28
+
29
+ def write(name, shared: false, user: nil, **data)
30
+ return false unless crypt_available?
31
+
32
+ Legion::Crypt.write(resolve_path(name, shared: shared, user: user), **data)
33
+ true
34
+ rescue StandardError => e
35
+ log_warn("secret write failed for #{name}: #{e.message}")
36
+ false
37
+ end
38
+
39
+ def exist?(name, shared: false, user: nil)
40
+ return false unless crypt_available?
41
+
42
+ Legion::Crypt.exist?(resolve_path(name, shared: shared, user: user))
43
+ rescue StandardError
44
+ false
45
+ end
46
+
47
+ def delete(name, shared: false, user: nil)
48
+ return false unless crypt_available?
49
+
50
+ Legion::Crypt.delete(resolve_path(name, shared: shared, user: user))
51
+ true
52
+ rescue StandardError => e
53
+ log_warn("secret delete failed for #{name}: #{e.message}")
54
+ false
55
+ end
56
+
57
+ private
58
+
59
+ def resolve_path(name, shared:, user:)
60
+ prefix = shared ? 'shared' : "users/#{resolve_user(user)}"
61
+ "#{prefix}/#{@lex_name}/#{name}"
62
+ end
63
+
64
+ def resolve_user(explicit_user)
65
+ return explicit_user if explicit_user
66
+
67
+ Secret.resolved_identity || ENV.fetch('USER', 'default')
68
+ end
69
+
70
+ def crypt_available?
71
+ return false unless defined?(Legion::Crypt)
72
+
73
+ unless @warned || vault_connected?
74
+ log_warn('Vault not connected — secret operations may fail')
75
+ @warned = true
76
+ end
77
+ true
78
+ end
79
+
80
+ def vault_connected?
81
+ defined?(Legion::Settings) &&
82
+ Legion::Settings[:crypt]&.dig(:vault, :connected) == true
83
+ rescue StandardError
84
+ false
85
+ end
86
+
87
+ def log_warn(msg)
88
+ Legion::Logging.warn("[Secret] #{msg}") if defined?(Legion::Logging)
89
+ end
90
+ end
91
+
92
+ module Secret
93
+ @resolved_identity = nil
94
+ @identity_source = nil
95
+
96
+ class << self
97
+ attr_reader :resolved_identity, :identity_source
98
+
99
+ def resolve_identity!
100
+ @resolved_identity = nil
101
+ @identity_source = nil
102
+
103
+ if defined?(Legion::Crypt) && Legion::Crypt.respond_to?(:kerberos_principal) &&
104
+ Legion::Crypt.kerberos_principal
105
+ @resolved_identity = Legion::Crypt.kerberos_principal
106
+ @identity_source = :kerberos
107
+ elsif entra_principal
108
+ @resolved_identity = entra_principal
109
+ @identity_source = :entra
110
+ end
111
+
112
+ @resolved_identity
113
+ end
114
+
115
+ def reset_identity!
116
+ @resolved_identity = nil
117
+ @identity_source = nil
118
+ end
119
+
120
+ private
121
+
122
+ def entra_principal
123
+ return nil unless defined?(Legion::Extensions::MicrosoftTeams::Helpers::TokenCache)
124
+
125
+ cache = Legion::Extensions::MicrosoftTeams::Helpers::TokenCache
126
+ return nil unless cache.respond_to?(:instance)
127
+
128
+ instance = cache.instance
129
+ return nil unless instance.respond_to?(:user_principal)
130
+
131
+ principal = instance.user_principal
132
+ principal unless principal.nil? || principal.empty?
133
+ rescue StandardError
134
+ nil
135
+ end
136
+ end
137
+
138
+ def secret
139
+ @secret ||= SecretAccessor.new(lex_name: lex_name)
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.6.8'
4
+ VERSION = '1.6.9'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legionio
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.8
4
+ version: 1.6.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -433,6 +433,7 @@ files:
433
433
  - docs/README.md
434
434
  - exe/legion
435
435
  - exe/legionio
436
+ - integration/self_generate_spec.rb
436
437
  - legionio.gemspec
437
438
  - lib/legion.rb
438
439
  - lib/legion/alerts.rb
@@ -767,6 +768,7 @@ files:
767
768
  - lib/legion/extensions/helpers/lex.rb
768
769
  - lib/legion/extensions/helpers/llm.rb
769
770
  - lib/legion/extensions/helpers/logger.rb
771
+ - lib/legion/extensions/helpers/secret.rb
770
772
  - lib/legion/extensions/helpers/segments.rb
771
773
  - lib/legion/extensions/helpers/task.rb
772
774
  - lib/legion/extensions/helpers/transport.rb