Module: Legion::Extensions::Llm::CredentialSources
- Extended by:
- Logging::Helper
- Includes:
- Logging::Helper
- Defined in:
- lib/legion/extensions/llm/credential_sources.rb
Overview
Read-only helpers that provider gems use to probe common credential locations (env vars, Claude config, Codex auth, Legion settings, and network probes). All methods are pure readers — the calling provider decides what to do with the result.
Constant Summary collapse
- CLAUDE_SETTINGS =
File.('~/.claude/settings.json')
- CLAUDE_PROJECT =
File.join(Dir.pwd, '.claude', 'settings.json')
- CODEX_AUTH =
File.('~/.codex/auth.json')
Class Method Summary collapse
- .claude_config ⇒ Object
-
.claude_config_value(key) ⇒ Object
Read a single key from the merged Claude config, trying both symbol and string variants.
-
.claude_env_value(key) ⇒ Object
Read a key from the :env hash inside Claude config, trying both symbol and string variants.
- .codex_openai_key ⇒ Object
- .codex_token ⇒ Object
-
.config_fingerprint(config) ⇒ Object
Extract fingerprint from a config hash by finding the first credential field.
-
.credential_fingerprint(value) ⇒ Object
Generate a short fingerprint (first 8 chars of SHA-256) for a credential value.
-
.credential_hash(config) ⇒ Object
SHA-256 hex digest of the first credential value found in the config hash (checks api_key, bearer_token, access_token in order).
- .credential_source_probing_enabled? ⇒ Boolean
-
.dedup_credentials(candidates) ⇒ Object
Deduplicate credential configs by the SHA-256 of their credential value (api_key / bearer_token / access_token).
-
.env(key) ⇒ Object
Fetch an environment variable, stripping whitespace.
-
.http_ok?(url, path:, timeout: 2) ⇒ Boolean
HTTP GET probe via Faraday.
-
.localhost?(url) ⇒ Boolean
Returns true when the URL points to localhost / 127.0.0.1 / ::1.
-
.setting(*path) ⇒ Object
Dig into Legion::Settings, returning nil if the module is not loaded or the path doesn’t exist.
-
.socket_open?(host, port, timeout: 0.1) ⇒ Boolean
TCP connect probe with a short timeout.
-
.source_tag(type, location, key = nil) ⇒ Object
Build a human-readable source tag describing where a credential was found.
Class Method Details
.claude_config ⇒ Object
39 40 41 42 43 |
# File 'lib/legion/extensions/llm/credential_sources.rb', line 39 def claude_config return {} unless credential_source_probing_enabled? @claude_config ||= merge_claude_configs end |
.claude_config_value(key) ⇒ Object
Read a single key from the merged Claude config, trying both symbol and string variants.
47 48 49 50 |
# File 'lib/legion/extensions/llm/credential_sources.rb', line 47 def claude_config_value(key) cfg = claude_config cfg[key.to_sym] || cfg[key.to_s] end |
.claude_env_value(key) ⇒ Object
Read a key from the :env hash inside Claude config, trying both symbol and string variants.
54 55 56 57 58 59 |
# File 'lib/legion/extensions/llm/credential_sources.rb', line 54 def claude_env_value(key) env_hash = claude_config_value(:env) return nil unless env_hash.is_a?(Hash) env_hash[key.to_sym] || env_hash[key.to_s] end |
.codex_openai_key ⇒ Object
75 76 77 78 79 80 81 82 83 84 |
# File 'lib/legion/extensions/llm/credential_sources.rb', line 75 def codex_openai_key return nil unless credential_source_probing_enabled? data = read_json(CODEX_AUTH) val = data[:OPENAI_API_KEY] || data['OPENAI_API_KEY'] return nil if val.nil? stripped = val.to_s.strip stripped.empty? ? nil : stripped end |
.codex_token ⇒ Object
61 62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/legion/extensions/llm/credential_sources.rb', line 61 def codex_token return nil unless credential_source_probing_enabled? data = read_json(CODEX_AUTH) mode = data[:auth_mode] || data['auth_mode'] return nil unless mode == 'chatgpt' token = data[:bearer_token] || data['bearer_token'] return nil if token.nil? || token.to_s.strip.empty? return nil unless token_valid?(token) token end |
.config_fingerprint(config) ⇒ Object
Extract fingerprint from a config hash by finding the first credential field.
194 195 196 197 198 199 |
# File 'lib/legion/extensions/llm/credential_sources.rb', line 194 def config_fingerprint(config) val = config[:api_key] || config['api_key'] || config[:bearer_token] || config['bearer_token'] || config[:access_token] || config['access_token'] credential_fingerprint(val) end |
.credential_fingerprint(value) ⇒ Object
Generate a short fingerprint (first 8 chars of SHA-256) for a credential value. Stable for the lifetime of the credential; safe to log and include in audit events.
187 188 189 190 191 |
# File 'lib/legion/extensions/llm/credential_sources.rb', line 187 def credential_fingerprint(value) return nil if value.nil? || value.to_s.strip.empty? Digest::SHA256.hexdigest(value.to_s)[0, 8] end |
.credential_hash(config) ⇒ Object
SHA-256 hex digest of the first credential value found in the config hash (checks api_key, bearer_token, access_token in order). Returns nil when no credential field is present.
168 169 170 171 172 173 174 175 |
# File 'lib/legion/extensions/llm/credential_sources.rb', line 168 def credential_hash(config) val = config[:api_key] || config['api_key'] || config[:bearer_token] || config['bearer_token'] || config[:access_token] || config['access_token'] return nil if val.nil? Digest::SHA256.hexdigest(val.to_s) end |
.credential_source_probing_enabled? ⇒ Boolean
21 22 23 24 25 |
# File 'lib/legion/extensions/llm/credential_sources.rb', line 21 def credential_source_probing_enabled? return true unless defined?(::Legion::Settings) ::Legion::Settings.dig(:extensions, :llm, :security, :credential_source_probing) != false end |
.dedup_credentials(candidates) ⇒ Object
Deduplicate credential configs by the SHA-256 of their credential value (api_key / bearer_token / access_token). First source wins. Entries without a credential value are always kept.
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
# File 'lib/legion/extensions/llm/credential_sources.rb', line 148 def dedup_credentials(candidates) seen = {} result = {} candidates.each do |instance_id, config| hash = credential_hash(config) if hash.nil? result[instance_id] = config elsif !seen.key?(hash) seen[hash] = instance_id result[instance_id] = config end end result end |
.env(key) ⇒ Object
Fetch an environment variable, stripping whitespace. Returns nil when the variable is unset or blank.
31 32 33 34 35 36 37 |
# File 'lib/legion/extensions/llm/credential_sources.rb', line 31 def env(key) val = ENV.fetch(key, nil) return nil if val.nil? stripped = val.strip stripped.empty? ? nil : stripped end |
.http_ok?(url, path:, timeout: 2) ⇒ Boolean
HTTP GET probe via Faraday. Returns true only on a 2xx status.
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/legion/extensions/llm/credential_sources.rb', line 128 def http_ok?(url, path:, timeout: 2) require 'faraday' conn = Faraday.new(url: url) do |f| f..timeout = timeout f..open_timeout = timeout end response = conn.get(path) response.status >= 200 && response.status < 300 rescue StandardError => e handle_exception(e, level: :debug, handled: true, operation: 'llm.credential_sources.http_ok', path:) false ensure conn&.close if conn.respond_to?(:close) end |
.localhost?(url) ⇒ Boolean
Returns true when the URL points to localhost / 127.0.0.1 / ::1.
202 203 204 205 206 207 208 209 210 211 212 213 214 |
# File 'lib/legion/extensions/llm/credential_sources.rb', line 202 def localhost?(url) return false if url.nil? uri = URI.parse(url.to_s) host = uri.host return false if host.nil? normalized = host.delete_prefix('[').delete_suffix(']') %w[localhost 127.0.0.1 ::1].include?(normalized) rescue URI::InvalidURIError => e handle_exception(e, level: :debug, handled: true, operation: 'llm.credential_sources.localhost') false end |
.setting(*path) ⇒ Object
Dig into Legion::Settings, returning nil if the module is not loaded or the path doesn’t exist.
88 89 90 91 92 93 94 95 96 |
# File 'lib/legion/extensions/llm/credential_sources.rb', line 88 def setting(*path) return nil unless defined?(::Legion::Settings) ::Legion::Settings.dig(*path) rescue StandardError => e handle_exception(e, level: :debug, handled: true, operation: 'llm.credential_sources.setting', path: path.map(&:to_s)) nil end |
.socket_open?(host, port, timeout: 0.1) ⇒ Boolean
TCP connect probe with a short timeout. Returns true if the port is reachable, false otherwise.
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/legion/extensions/llm/credential_sources.rb', line 100 def socket_open?(host, port, timeout: 0.1) require 'socket' addr = Socket.sockaddr_in(port, host) sock = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0) sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true) begin sock.connect_nonblock(addr) rescue IO::WaitWritable return false unless sock.wait_writable(timeout) begin sock.connect_nonblock(addr) rescue Errno::EISCONN # already connected — success end end true rescue StandardError => e handle_exception(e, level: :debug, handled: true, operation: 'llm.credential_sources.socket_open', host:, port:) false ensure sock&.close end |
.source_tag(type, location, key = nil) ⇒ Object
Build a human-readable source tag describing where a credential was found. Format: “type:location:key” e.g. “env:ANTHROPIC_API_KEY”, “file:~/.claude/settings.json:anthropicApiKey”
179 180 181 182 183 |
# File 'lib/legion/extensions/llm/credential_sources.rb', line 179 def source_tag(type, location, key = nil) parts = [type.to_s, location.to_s] parts << key.to_s if key && !key.to_s.empty? parts.join(':') end |