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
The codex auth file is an optional source: absent means "no codex credential" (probe semantics); present-but-unreadable raises from read_json (O11 fail-closed).
-
.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 (the one shared host-locality detector — Utils.localhost_url?).
-
.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
79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/legion/extensions/llm/credential_sources.rb', line 79 def codex_openai_key return nil unless credential_source_probing_enabled? return nil unless File.exist?(CODEX_AUTH) 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
The codex auth file is an optional source: absent means "no codex credential" (probe semantics); present-but-unreadable raises from read_json (O11 fail-closed).
64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
# File 'lib/legion/extensions/llm/credential_sources.rb', line 64 def codex_token return nil unless credential_source_probing_enabled? return nil unless File.exist?(CODEX_AUTH) 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.
199 200 201 202 203 204 |
# File 'lib/legion/extensions/llm/credential_sources.rb', line 199 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.
192 193 194 195 196 |
# File 'lib/legion/extensions/llm/credential_sources.rb', line 192 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.
173 174 175 176 177 178 179 180 |
# File 'lib/legion/extensions/llm/credential_sources.rb', line 173 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.
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
# File 'lib/legion/extensions/llm/credential_sources.rb', line 153 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.
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/legion/extensions/llm/credential_sources.rb', line 133 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: :warn, 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 (the one shared host-locality detector — Utils.localhost_url?).
208 209 210 211 212 |
# File 'lib/legion/extensions/llm/credential_sources.rb', line 208 def localhost?(url) return false if url.nil? Utils.localhost_url?(url) end |
.setting(*path) ⇒ Object
Dig into Legion::Settings, returning nil if the module is not loaded or the path doesn't exist.
93 94 95 96 97 98 99 100 101 |
# File 'lib/legion/extensions/llm/credential_sources.rb', line 93 def setting(*path) return nil unless defined?(::Legion::Settings) ::Legion::Settings.dig(*path) rescue StandardError => e handle_exception(e, level: :warn, 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.
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/legion/extensions/llm/credential_sources.rb', line 105 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: :warn, 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"
184 185 186 187 188 |
# File 'lib/legion/extensions/llm/credential_sources.rb', line 184 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 |