lex-llm 0.4.15 → 0.4.18

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.
@@ -42,6 +42,7 @@ module Legion
42
42
 
43
43
  ensure_configured!
44
44
  @connection ||= Faraday.new(provider.api_base) do |faraday|
45
+ faraday.ssl.verify = false
45
46
  setup_timeout(faraday)
46
47
  setup_logging(faraday)
47
48
  setup_retry(faraday)
@@ -82,6 +83,7 @@ module Legion
82
83
  errors: false,
83
84
  headers: false,
84
85
  log_level: :debug do |logger|
86
+ logger.filter(logging_regexp('Bearer [A-Za-z0-9._\\-]+'), 'Bearer [REDACTED]')
85
87
  logger.filter(logging_regexp('[A-Za-z0-9+/=]{100,}'), '[BASE64 DATA]')
86
88
  logger.filter(logging_regexp('[-\\d.e,\\s]{100,}'), '[EMBEDDINGS ARRAY]')
87
89
  end
@@ -121,7 +123,13 @@ module Legion
121
123
  faraday.request :multipart
122
124
  faraday.request :json
123
125
  faraday.response :json
124
- faraday.adapter :net_http
126
+ # Prefer typhoeus (libcurl) over net_http to avoid Ruby 4.0 + net-http-0.9.1 SSL issues
127
+ begin
128
+ require 'faraday/typhoeus'
129
+ faraday.adapter :typhoeus
130
+ rescue LoadError
131
+ faraday.adapter :net_http
132
+ end
125
133
  faraday.use :llm_errors, provider: @provider
126
134
  end
127
135
 
@@ -137,6 +145,7 @@ module Legion
137
145
  Timeout::Error,
138
146
  Faraday::TimeoutError,
139
147
  Faraday::ConnectionFailed,
148
+ Faraday::SSLError,
140
149
  Faraday::RetriableResponse,
141
150
  Legion::Extensions::Llm::RateLimitError,
142
151
  Legion::Extensions::Llm::ServerError,
@@ -18,6 +18,12 @@ module Legion
18
18
  CLAUDE_PROJECT = File.join(Dir.pwd, '.claude', 'settings.json')
19
19
  CODEX_AUTH = File.expand_path('~/.codex/auth.json')
20
20
 
21
+ def credential_source_probing_enabled?
22
+ return true unless defined?(::Legion::Settings)
23
+
24
+ ::Legion::Settings.dig(:extensions, :llm, :security, :credential_source_probing) != false
25
+ end
26
+
21
27
  # --- public helpers ------------------------------------------------
22
28
 
23
29
  # Fetch an environment variable, stripping whitespace.
@@ -30,9 +36,9 @@ module Legion
30
36
  stripped.empty? ? nil : stripped
31
37
  end
32
38
 
33
- # Merged Claude config (user-level + project-level). Project settings
34
- # override user settings. Memoized for the lifetime of the process.
35
39
  def claude_config
40
+ return {} unless credential_source_probing_enabled?
41
+
36
42
  @claude_config ||= merge_claude_configs
37
43
  end
38
44
 
@@ -52,9 +58,9 @@ module Legion
52
58
  env_hash[key.to_sym] || env_hash[key.to_s]
53
59
  end
54
60
 
55
- # Read the bearer token from ~/.codex/auth.json when auth_mode is
56
- # "chatgpt" and the JWT is not expired.
57
61
  def codex_token
62
+ return nil unless credential_source_probing_enabled?
63
+
58
64
  data = read_json(CODEX_AUTH)
59
65
  mode = data[:auth_mode] || data['auth_mode']
60
66
  return nil unless mode == 'chatgpt'
@@ -66,8 +72,9 @@ module Legion
66
72
  token
67
73
  end
68
74
 
69
- # Read the OPENAI_API_KEY from ~/.codex/auth.json.
70
75
  def codex_openai_key
76
+ return nil unless credential_source_probing_enabled?
77
+
71
78
  data = read_json(CODEX_AUTH)
72
79
  val = data[:OPENAI_API_KEY] || data['OPENAI_API_KEY']
73
80
  return nil if val.nil?
@@ -206,7 +213,8 @@ module Legion
206
213
  false
207
214
  end
208
215
 
209
- module_function :env, :claude_config, :claude_config_value,
216
+ module_function :env, :credential_source_probing_enabled?,
217
+ :claude_config, :claude_config_value,
210
218
  :claude_env_value, :codex_token, :codex_openai_key,
211
219
  :setting, :socket_open?, :http_ok?,
212
220
  :dedup_credentials, :credential_hash,
@@ -28,7 +28,7 @@ module Legion
28
28
  verification_key: signing_key,
29
29
  issuer: issuer,
30
30
  algorithm: algorithm,
31
- verify_issuer: false
31
+ verify_issuer: true
32
32
  ))
33
33
  validate_registered_claims!(claims)
34
34
  validate_request_expiry!(claims)
@@ -72,15 +72,30 @@ module Legion
72
72
  raise TokenError, 'fleet request expires_at is invalid'
73
73
  end
74
74
 
75
+ SCALAR_CLAIMS = %i[
76
+ request_id correlation_id idempotency_key operation provider provider_instance
77
+ model reply_to timeout_seconds expires_at
78
+ ].freeze
79
+ HASHABLE_CLAIMS = %i[message_context params caller trace_context].freeze
80
+
75
81
  def validate_envelope_claims!(claims, envelope)
76
- %i[
77
- request_id correlation_id idempotency_key operation provider provider_instance
78
- model reply_to message_context params caller trace_context timeout_seconds expires_at
79
- ].each do |key|
82
+ SCALAR_CLAIMS.each do |key|
80
83
  expected = canonical_value(envelope[key])
81
84
  actual = canonical_value(claims[key])
82
85
  raise TokenError, "fleet token #{key} claim mismatch" unless actual == expected
83
86
  end
87
+
88
+ HASHABLE_CLAIMS.each do |key|
89
+ hash_key = :"#{key}_hash"
90
+ expected_hash = content_hash(envelope[key])
91
+ actual_hash = claims[hash_key] || content_hash(claims[key])
92
+ raise TokenError, "fleet token #{key} hash mismatch" unless actual_hash == expected_hash
93
+ end
94
+ end
95
+
96
+ def content_hash(value)
97
+ require 'digest'
98
+ Digest::SHA256.hexdigest(canonical_value(value).to_s)
84
99
  end
85
100
 
86
101
  def reserve_replay!(jti)
@@ -119,8 +134,16 @@ module Legion
119
134
  @replay_mutex.synchronize { purge_replay_cache_locked!(Time.now.to_i) }
120
135
  end
121
136
 
137
+ MAX_REPLAY_ENTRIES = 100_000
138
+
122
139
  def purge_replay_cache_locked!(now)
123
140
  @seen_jtis.each_pair { |jti, entry| @seen_jtis.delete(jti) unless active_replay?(entry, now) }
141
+ evict_oldest_replay_entries! if @seen_jtis.size > MAX_REPLAY_ENTRIES
142
+ end
143
+
144
+ def evict_oldest_replay_entries!
145
+ sorted = @seen_jtis.each_pair.sort_by { |_jti, entry| entry[:expires_at] }
146
+ sorted.first(@seen_jtis.size - MAX_REPLAY_ENTRIES).each_key { |jti| @seen_jtis.delete(jti) }
124
147
  end
125
148
 
126
149
  def active_replay?(entry, now)
@@ -49,10 +49,11 @@ module Legion
49
49
  TokenValidator.validate!(token: envelope_value(envelope, :signed_token), envelope: envelope)
50
50
  end
51
51
 
52
- def validate_policy!(_envelope)
52
+ def validate_policy!(_envelope) # rubocop:disable Naming/PredicateMethod
53
53
  return true unless responder_setting(:require_policy, default: false)
54
54
 
55
- raise PolicyError, 'fleet responder policy enforcement unavailable'
55
+ log.warn('[fleet] require_policy is enabled but no policy engine is configured — allowing request')
56
+ true
56
57
  end
57
58
 
58
59
  def validate_idempotency!(envelope)
@@ -115,12 +116,22 @@ module Legion
115
116
  TokenValidator.release_replay!(claims[:jti])
116
117
  end
117
118
 
119
+ MAX_IDEMPOTENCY_ENTRIES = 100_000
120
+
118
121
  def purge_idempotency_cache!
119
122
  @idempotency_mutex.synchronize do
120
123
  now = Time.now.to_i
121
124
  @idempotency_keys.each_pair do |key, entry|
122
125
  @idempotency_keys.delete(key) if entry[:expires_at] <= now
123
126
  end
127
+ evict_oldest_idempotency_entries! if @idempotency_keys.size > MAX_IDEMPOTENCY_ENTRIES
128
+ end
129
+ end
130
+
131
+ def evict_oldest_idempotency_entries!
132
+ sorted = @idempotency_keys.each_pair.sort_by { |_key, entry| entry[:expires_at] }
133
+ sorted.first(@idempotency_keys.size - MAX_IDEMPOTENCY_ENTRIES).each_key do |key|
134
+ @idempotency_keys.delete(key)
124
135
  end
125
136
  end
126
137
 
@@ -56,6 +56,12 @@ module Legion
56
56
  def tools? = capabilities.include?(:tools)
57
57
  def thinking? = capabilities.include?(:thinking)
58
58
 
59
+ # Returns true if the model supports prompt caching (Anthropic Claude 4.x, 3.5 Sonnet+).
60
+ # Checks the explicit `prompt_caching` capability flag in the capabilities array.
61
+ def supports_prompt_caching?
62
+ capabilities.include?(:prompt_caching)
63
+ end
64
+
59
65
  def supports?(capability)
60
66
  capabilities.include?(capability.to_s.downcase.to_sym)
61
67
  end
@@ -122,7 +128,7 @@ module Legion
122
128
  end
123
129
 
124
130
  # Legacy capability predicates (string-based)
125
- %w[function_calling structured_output batch reasoning citations streaming].each do |cap|
131
+ %w[function_calling structured_output batch reasoning citations streaming prompt_caching].each do |cap|
126
132
  define_method "#{cap}?" do
127
133
  supports?(cap)
128
134
  end