legionio 1.7.24 → 1.7.25

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: 8e469923d32f8ead43a892e473c93737139885622f40683a63aa0a757a0c282d
4
- data.tar.gz: 2e88c8468007f617c075178410da534337931e37a3c50fb11a67fa3801807689
3
+ metadata.gz: d65ff77ca667361b558a97d1da1c3d5d2b1090bb4aad5559532406cd18e781a7
4
+ data.tar.gz: 7583822cc4cf804703b8a733be36bb711d1cae70724a49dc98cafea6583268ab
5
5
  SHA512:
6
- metadata.gz: 4d7cd5a0513e26ca92bf5b909ac924a7041bbe2b74602d6a552357d155f9101c0d81b75c6d4db0c06b9748b08fd174363ad7d26ffa021f82001923d428ecd5ec
7
- data.tar.gz: 252dc15217cf3d320fa9f96703967d9c9320f2d3e26d18d91c992a8819dbf34ced50eca926ddcba0a3c2be1a74619d3f6106d20ca9f09919ba6c32901b8d5197
6
+ metadata.gz: 06f7712d8c9c6e944890ba2c55efbdf3463a6c39e906017cede6d3401215de9144e7c916c39c8b981e8f6ea6646a5439bcc10a399443fe7642b3ab06e3ef2c7a
7
+ data.tar.gz: 5e5819fc7973e9cb6de4b64e88c40b74d033fd666c4b9585726a3b1e87b9468f6cf4759fed226a9c3a7bc755a10b9267403dcd001d2c10f08b7aaf36e6264812
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Legion Changelog
2
2
 
3
+ ## [1.7.25] - 2026-04-06
4
+
5
+ ### Added
6
+ - Wire Format Phase 3 Group 2: `Identity::Request::SOURCE_NORMALIZATION` constant — maps middleware-emitted source values (`:api_key`, `:local`, `:jwt`, `:kerberos`, `:system`) to canonical credential enum at `from_auth_context` construction time
7
+ - Wire Format Phase 3 Group 2: `response_meta` in `API::Helpers` now includes `caller` block (`canonical_name`, `kind`, `source`) when the request is authenticated and `env['legion.principal']` is set by `Identity::Middleware`
8
+ - Wire Format Phase 3 Group 2: `POST /api/llm/inference` wires `to_caller_hash` from the authenticated principal into the pipeline `caller:` field, replacing the hardcoded `{ type: :user, credential: :api }` fallback
9
+
3
10
  ## [1.7.24] - 2026-04-06
4
11
 
5
12
  ### Fixed
@@ -13,7 +13,8 @@ module Legion
13
13
  puma: puma_defaults,
14
14
  bind_retries: 3,
15
15
  bind_retry_wait: 2,
16
- tls: tls_defaults
16
+ tls: tls_defaults,
17
+ elastic_apm: elastic_apm_defaults
17
18
  }
18
19
  end
19
20
 
@@ -31,6 +32,33 @@ module Legion
31
32
  enabled: false
32
33
  }
33
34
  end
35
+
36
+ def self.elastic_apm_defaults
37
+ {
38
+ enabled: false,
39
+ server_url: 'http://localhost:8200',
40
+ api_key: nil,
41
+ secret_token: nil,
42
+ api_buffer_size: 256,
43
+ api_request_size: '750kb',
44
+ api_request_time: '10s',
45
+ capture_body: 'off',
46
+ capture_headers: true,
47
+ capture_env: true,
48
+ disable_send: false,
49
+ environment: nil,
50
+ hostname: nil,
51
+ ignore_url_patterns: %w[/api/health /api/ready],
52
+ pool_size: 1,
53
+ service_name: 'LegionIO',
54
+ service_node_name: nil,
55
+ service_version: nil,
56
+ sample_rate: 1.0,
57
+ verify_server_cert: true,
58
+ central_config: true,
59
+ span_frames_min_duration: '5ms'
60
+ }
61
+ end
34
62
  end
35
63
  end
36
64
  end
@@ -173,10 +173,23 @@ module Legion
173
173
  private
174
174
 
175
175
  def response_meta
176
- {
176
+ meta = {
177
177
  timestamp: Time.now.utc.iso8601,
178
178
  node: Legion::Settings[:client][:name]
179
179
  }
180
+
181
+ if authenticated? && defined?(Legion::Identity::Request)
182
+ req = env['legion.principal']
183
+ if req
184
+ meta[:caller] = {
185
+ canonical_name: req.canonical_name,
186
+ kind: req.kind,
187
+ source: req.source
188
+ }
189
+ end
190
+ end
191
+
192
+ meta
180
193
  end
181
194
 
182
195
  def page_limit
@@ -280,12 +280,19 @@ module Legion
280
280
  require 'legion/llm/pipeline/request' unless defined?(Legion::LLM::Pipeline::Request)
281
281
  require 'legion/llm/pipeline/executor' unless defined?(Legion::LLM::Pipeline::Executor)
282
282
 
283
+ principal = defined?(Legion::Identity::Request) && env['legion.principal']
284
+ caller_ctx = if principal
285
+ principal.to_caller_hash
286
+ else
287
+ { requested_by: { identity: caller_identity, type: :user, credential: :api } }
288
+ end
289
+
283
290
  req = Legion::LLM::Pipeline::Request.build(
284
291
  messages: messages,
285
292
  system: body[:system],
286
293
  routing: { provider: provider, model: model },
287
294
  tools: tool_classes,
288
- caller: { requested_by: { identity: caller_identity, type: :user, credential: :api } },
295
+ caller: caller_ctx,
289
296
  conversation_id: body[:conversation_id],
290
297
  metadata: { requested_tools: requested_tools },
291
298
  stream: streaming,
@@ -49,6 +49,25 @@ module Legion
49
49
  parts << "query=#{query}" if query
50
50
  parts.join(' ')
51
51
  end
52
+
53
+ def peek_body(env)
54
+ input = env['rack.input']
55
+ return '-' unless input.respond_to?(:read) && input.respond_to?(:rewind)
56
+
57
+ begin
58
+ input.rewind
59
+ raw = input.read(1024)
60
+ raw.to_s.gsub(/\s+/, ' ')[0, 512]
61
+ rescue StandardError
62
+ '-'
63
+ ensure
64
+ begin
65
+ input.rewind
66
+ rescue StandardError
67
+ nil
68
+ end
69
+ end
70
+ end
52
71
  end
53
72
  end
54
73
  end
data/lib/legion/api.rb CHANGED
@@ -222,5 +222,7 @@ module Legion
222
222
 
223
223
  use Legion::API::Middleware::RequestLogger
224
224
  use Legion::Rbac::Middleware if defined?(Legion::Rbac::Middleware)
225
+ use ElasticAPM::Middleware if defined?(ElasticAPM::Middleware) &&
226
+ Legion::Settings.dig(:api, :elastic_apm, :enabled)
225
227
  end
226
228
  end
@@ -3,6 +3,19 @@
3
3
  module Legion
4
4
  module Identity
5
5
  class Request
6
+ # Maps middleware-emitted source values to the canonical credential enum.
7
+ # :local is emitted by Middleware#system_principal for unauthenticated loopback
8
+ # requests and must normalize to :system to maintain audit trail consistency.
9
+ # :jwt is intentionally kept distinct — JWT is the transport, not the provider.
10
+ # Entra-specific identification requires issuer inspection (Phase 7 concern).
11
+ SOURCE_NORMALIZATION = {
12
+ api_key: :api,
13
+ jwt: :jwt,
14
+ kerberos: :kerberos,
15
+ local: :system,
16
+ system: :system
17
+ }.freeze
18
+
6
19
  attr_reader :principal_id, :canonical_name, :kind, :groups, :source, :metadata
7
20
 
8
21
  def initialize(principal_id:, canonical_name:, kind:, groups: [], source: nil, metadata: {}) # rubocop:disable Metrics/ParameterLists
@@ -10,7 +23,7 @@ module Legion
10
23
  @canonical_name = canonical_name
11
24
  @kind = kind
12
25
  @groups = groups.freeze
13
- @source = source
26
+ @source = SOURCE_NORMALIZATION.fetch(source&.to_sym, source)
14
27
  @metadata = metadata.freeze
15
28
  freeze
16
29
  end
@@ -23,16 +36,19 @@ module Legion
23
36
 
24
37
  # Builds a Request from a parsed auth claims hash with symbol keys:
25
38
  # { sub:, name:, preferred_username:, kind:, groups:, source: }
39
+ # The source value is normalized via SOURCE_NORMALIZATION at construction time.
26
40
  def self.from_auth_context(claims_hash)
27
41
  raw_name = claims_hash[:name] || claims_hash[:preferred_username] || ''
28
42
  canonical = raw_name.to_s.strip.downcase.gsub('.', '-')
43
+ raw_source = claims_hash[:source]&.to_sym
44
+ normalized_source = SOURCE_NORMALIZATION.fetch(raw_source, raw_source)
29
45
 
30
46
  new(
31
47
  principal_id: claims_hash[:sub],
32
48
  canonical_name: canonical,
33
49
  kind: claims_hash[:kind] || :human,
34
50
  groups: claims_hash[:groups] || [],
35
- source: claims_hash[:source]
51
+ source: normalized_source
36
52
  )
37
53
  end
38
54
 
@@ -327,21 +327,12 @@ module Legion
327
327
  end
328
328
 
329
329
  def setup_apm
330
- apm_settings = Legion::Settings[:apm] || {}
330
+ apm_settings = Legion::Settings.dig(:api, :elastic_apm) || {}
331
331
  return unless apm_settings[:enabled]
332
332
 
333
333
  require 'elastic-apm'
334
334
 
335
- config = {
336
- service_name: apm_settings[:service_name] || "legion-#{Legion::Settings[:client][:name]}",
337
- server_url: apm_settings[:server_url] || 'http://localhost:8200',
338
- environment: apm_settings[:environment] || Legion::Settings[:environment] || 'development',
339
- secret_token: apm_settings[:secret_token],
340
- api_key: apm_settings[:api_key],
341
- log_level: apm_settings[:log_level]&.to_sym || Logger::WARN,
342
- transaction_sample_rate: apm_settings[:sample_rate] || 1.0
343
- }.compact
344
-
335
+ config = build_apm_config(apm_settings)
345
336
  ElasticAPM.start(**config)
346
337
  @apm_running = true
347
338
  log.info "Elastic APM started: server=#{config[:server_url]} service=#{config[:service_name]}"
@@ -1145,6 +1136,35 @@ module Legion
1145
1136
  }.compact
1146
1137
  end
1147
1138
 
1139
+ def build_apm_config(apm)
1140
+ {
1141
+ server_url: apm[:server_url] || 'http://localhost:8200',
1142
+ api_key: apm[:api_key],
1143
+ secret_token: apm[:secret_token],
1144
+ api_buffer_size: apm[:api_buffer_size] || 256,
1145
+ api_request_size: apm[:api_request_size] || '750kb',
1146
+ api_request_time: apm[:api_request_time] || '10s',
1147
+ capture_body: apm.fetch(:capture_body, 'off'),
1148
+ capture_headers: apm.fetch(:capture_headers, true),
1149
+ capture_env: apm.fetch(:capture_env, true),
1150
+ disable_send: apm.fetch(:disable_send, false),
1151
+ environment: apm[:environment] || Legion::Settings[:environment] || 'development',
1152
+ framework_name: 'LegionIO',
1153
+ framework_version: Legion::VERSION,
1154
+ hostname: apm[:hostname] || Legion::Settings[:client][:name],
1155
+ ignore_url_patterns: apm[:ignore_url_patterns] || %w[/api/health /api/ready],
1156
+ logger: Legion::Logging.log,
1157
+ pool_size: apm[:pool_size] || 1,
1158
+ service_name: apm[:service_name] || 'LegionIO',
1159
+ service_node_name: apm[:service_node_name] || Legion::Settings[:client][:name],
1160
+ service_version: apm[:service_version] || Legion::VERSION,
1161
+ transaction_sample_rate: apm[:sample_rate] || 1.0,
1162
+ verify_server_cert: apm.fetch(:verify_server_cert, true),
1163
+ central_config: apm.fetch(:central_config, true),
1164
+ span_frames_min_duration: apm[:span_frames_min_duration]
1165
+ }.compact
1166
+ end
1167
+
1148
1168
  def ssl_server_settings(tls_cfg, bind, port)
1149
1169
  return {} unless tls_cfg
1150
1170
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.7.24'
4
+ VERSION = '1.7.25'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legionio
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.24
4
+ version: 1.7.25
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity