Module: Legion::Extensions::Llm::CredentialSources

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.



32
33
34
# File 'lib/legion/extensions/llm/credential_sources.rb', line 32

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.



38
39
40
41
# File 'lib/legion/extensions/llm/credential_sources.rb', line 38

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.



45
46
47
48
49
50
# File 'lib/legion/extensions/llm/credential_sources.rb', line 45

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.



67
68
69
70
71
72
73
74
# File 'lib/legion/extensions/llm/credential_sources.rb', line 67

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.



54
55
56
57
58
59
60
61
62
63
64
# File 'lib/legion/extensions/llm/credential_sources.rb', line 54

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.



152
153
154
155
156
157
158
159
# File 'lib/legion/extensions/llm/credential_sources.rb', line 152

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.



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/legion/extensions/llm/credential_sources.rb', line 132

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.



22
23
24
25
26
27
28
# File 'lib/legion/extensions/llm/credential_sources.rb', line 22

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)


114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/legion/extensions/llm/credential_sources.rb', line 114

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


162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/legion/extensions/llm/credential_sources.rb', line 162

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
  false
end

.setting(*path) ⇒ Object

Dig into Legion::Settings, returning nil if the module is not loaded or the path doesn’t exist.



78
79
80
81
82
83
84
# File 'lib/legion/extensions/llm/credential_sources.rb', line 78

def setting(*path)
  return nil unless defined?(::Legion::Settings)

  ::Legion::Settings.dig(*path)
rescue StandardError
  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)


88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/legion/extensions/llm/credential_sources.rb', line 88

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
  false
ensure
  sock&.close
end