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.expand_path('~/.claude/settings.json')
CLAUDE_PROJECT =
File.join(Dir.pwd, '.claude', 'settings.json')
CODEX_AUTH =
File.expand_path('~/.codex/auth.json')

Class Method Summary collapse

Class Method Details

.claude_configObject

Merged Claude config (user-level + project-level). Project settings override user settings. Memoized for the lifetime of the process.



35
36
37
# File 'lib/legion/extensions/llm/credential_sources.rb', line 35

def claude_config
  @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.



41
42
43
44
# File 'lib/legion/extensions/llm/credential_sources.rb', line 41

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.



48
49
50
51
52
53
# File 'lib/legion/extensions/llm/credential_sources.rb', line 48

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_keyObject

Read the OPENAI_API_KEY from ~/.codex/auth.json.



70
71
72
73
74
75
76
77
# File 'lib/legion/extensions/llm/credential_sources.rb', line 70

def codex_openai_key
  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_tokenObject

Read the bearer token from ~/.codex/auth.json when auth_mode is “chatgpt” and the JWT is not expired.



57
58
59
60
61
62
63
64
65
66
67
# File 'lib/legion/extensions/llm/credential_sources.rb', line 57

def codex_token
  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

.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.



161
162
163
164
165
166
167
168
# File 'lib/legion/extensions/llm/credential_sources.rb', line 161

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

.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.



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/legion/extensions/llm/credential_sources.rb', line 141

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.



25
26
27
28
29
30
31
# File 'lib/legion/extensions/llm/credential_sources.rb', line 25

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.

Returns:

  • (Boolean)


121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/legion/extensions/llm/credential_sources.rb', line 121

def http_ok?(url, path:, timeout: 2)
  require 'faraday'

  conn = Faraday.new(url: url) do |f|
    f.options.timeout = timeout
    f.options.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.

Returns:

  • (Boolean)


171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/legion/extensions/llm/credential_sources.rb', line 171

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.



81
82
83
84
85
86
87
88
89
# File 'lib/legion/extensions/llm/credential_sources.rb', line 81

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.

Returns:

  • (Boolean)


93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/legion/extensions/llm/credential_sources.rb', line 93

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