Class: Kodo::Config

Inherits:
Object
  • Object
show all
Defined in:
lib/kodo/config.rb

Constant Summary collapse

PROVIDER_KEY_MAP =

Map of Kodo config key → RubyLLM config setter name

{
  'anthropic' => 'anthropic',
  'openai' => 'openai',
  'gemini' => 'gemini',
  'deepseek' => 'deepseek',
  'mistral' => 'mistral',
  'openrouter' => 'openrouter',
  'perplexity' => 'perplexity',
  'xai' => 'xai'
}.freeze
DEFAULTS =
{
  'daemon' => {
    'port' => 7377,
    'heartbeat_interval' => 60
  },
  'llm' => {
    'model' => 'claude-sonnet-4-6',
    'utility_model' => 'claude-haiku-4-5-20251001',
    'providers' => {
      'anthropic' => { 'api_key_env' => 'ANTHROPIC_API_KEY' }
    }
  },
  'channels' => {
    'telegram' => {
      'enabled' => false,
      'bot_token_env' => 'TELEGRAM_BOT_TOKEN'
    }
  },
  'memory' => {
    'encryption' => false,
    'passphrase_env' => 'KODO_PASSPHRASE',
    'store' => 'file'
  },
  'search' => {
    'provider' => nil,
    'providers' => {
      'tavily' => { 'api_key_env' => 'TAVILY_API_KEY' }
    }
  },
  'logging' => {
    'level' => 'info',
    'audit' => true
  },
  'web' => {
    'fetch_url_enabled' => true,
    'web_search_enabled' => true,
    'injection_scan' => true,
    'audit_urls' => true,
    'fetch_blocklist' => [],
    'fetch_allowlist' => [],
    'ssrf_bypass_hosts' => []
  }
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(data) ⇒ Config

Returns a new instance of Config.



67
68
69
# File 'lib/kodo/config.rb', line 67

def initialize(data)
  @data = data
end

Instance Attribute Details

#data ⇒ Object (readonly)

Returns the value of attribute data.



65
66
67
# File 'lib/kodo/config.rb', line 65

def data
  @data
end

Class Method Details

.config_path ⇒ Object



79
80
81
# File 'lib/kodo/config.rb', line 79

def config_path
  File.join(Kodo.home_dir, 'config.yml')
end

.ensure_home_dir! ⇒ Object



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/kodo/config.rb', line 83

def ensure_home_dir!
  dirs = [
    Kodo.home_dir,
    File.join(Kodo.home_dir, 'memory', 'conversations'),
    File.join(Kodo.home_dir, 'memory', 'knowledge'),
    File.join(Kodo.home_dir, 'memory', 'reminders'),
    File.join(Kodo.home_dir, 'memory', 'audit'),
    File.join(Kodo.home_dir, 'skills')
  ]
  dirs.each { |d| FileUtils.mkdir_p(d) }

  return if File.exist?(config_path)

  File.write(config_path, YAML.dump(DEFAULTS))
end

.load(path = nil) ⇒ Object



72
73
74
75
76
77
# File 'lib/kodo/config.rb', line 72

def load(path = nil)
  path ||= config_path
  user_config = File.exist?(path) ? YAML.safe_load_file(path) : {}
  merged = deep_merge(DEFAULTS, user_config || {})
  new(merged)
end

Instance Method Details

#audit_enabled? ⇒ Boolean

Returns:

  • (Boolean)


178
# File 'lib/kodo/config.rb', line 178

def audit_enabled?  = data.dig('logging', 'audit') != false

#heartbeat_interval ⇒ Object



114
# File 'lib/kodo/config.rb', line 114

def heartbeat_interval = data.dig('daemon', 'heartbeat_interval')

#llm_api_keys ⇒ Object

Returns a hash of { "provider_name" => "actual_api_key" } for all configured providers

Raises:



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/kodo/config.rb', line 121

def llm_api_keys
  providers = data.dig('llm', 'providers') || {}
  keys = {}

  providers.each do |provider, settings|
    env_var = settings['api_key_env']
    next unless env_var

    key = ENV[env_var]
    if key && !key.empty?
      ruby_llm_name = PROVIDER_KEY_MAP[provider] || provider
      keys[ruby_llm_name] = key
    end
  end

  raise Error, 'No LLM API keys found. Set at least one provider key (e.g. ANTHROPIC_API_KEY)' if keys.empty?

  keys
end

#llm_model ⇒ Object

--- LLM ---



117
# File 'lib/kodo/config.rb', line 117

def llm_model = data.dig('llm', 'model')

#log_level ⇒ Object

--- Logging ---



177
# File 'lib/kodo/config.rb', line 177

def log_level       = data.dig('logging', 'level')&.to_sym || :info

#memory_encryption? ⇒ Boolean

--- Memory / Encryption ---

Returns:

  • (Boolean)


147
# File 'lib/kodo/config.rb', line 147

def memory_encryption? = data.dig('memory', 'encryption') == true

#memory_passphrase ⇒ Object



153
154
155
156
157
158
159
160
# File 'lib/kodo/config.rb', line 153

def memory_passphrase
  passphrase = ENV[memory_passphrase_env]
  if memory_encryption? && (passphrase.nil? || passphrase.empty?)
    raise Error, "Memory encryption is enabled but #{memory_passphrase_env} is not set"
  end

  passphrase
end

#memory_passphrase_env ⇒ Object



149
150
151
# File 'lib/kodo/config.rb', line 149

def memory_passphrase_env
  data.dig('memory', 'passphrase_env') || 'KODO_PASSPHRASE'
end

#ollama_api_base ⇒ Object

Optional: Ollama base URL for local models



142
143
144
# File 'lib/kodo/config.rb', line 142

def ollama_api_base
  data.dig('llm', 'providers', 'ollama', 'api_base') || ENV['OLLAMA_API_BASE']
end

#port ⇒ Object

--- Daemon ---



113
# File 'lib/kodo/config.rb', line 113

def port               = data.dig('daemon', 'port')

#search_api_key ⇒ Object



199
200
201
202
203
204
205
206
207
# File 'lib/kodo/config.rb', line 199

def search_api_key
  return nil unless search_configured?

  provider = search_provider
  env_var = data.dig('search', 'providers', provider, 'api_key_env')
  return nil unless env_var

  ENV[env_var]
end

#search_configured? ⇒ Boolean

Returns:

  • (Boolean)


195
196
197
# File 'lib/kodo/config.rb', line 195

def search_configured?
  !search_provider.nil? && !search_provider.empty?
end

#search_provider ⇒ Object

--- Search ---



191
192
193
# File 'lib/kodo/config.rb', line 191

def search_provider
  data.dig('search', 'provider')
end

#search_provider_instance ⇒ Object



227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/kodo/config.rb', line 227

def search_provider_instance
  return nil unless search_configured?

  case search_provider
  when 'tavily'
    api_key = search_api_key
    unless api_key
      raise Error,
            "Tavily API key not set (#{data.dig('search', 'providers', 'tavily', 'api_key_env')})"
    end

    Search::Tavily.new(api_key: api_key)
  else
    raise Error, "Unknown search provider: #{search_provider}"
  end
end

#secrets_passphrase ⇒ Object

--- Secrets ---



163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/kodo/config.rb', line 163

def secrets_passphrase
  passphrase_path = File.join(Kodo.home_dir, '.passphrase')
  if File.exist?(passphrase_path)
    File.read(passphrase_path).strip
  else
    passphrase = SecureRandom.hex(32)
    FileUtils.mkdir_p(Kodo.home_dir)
    File.write(passphrase_path, passphrase)
    File.chmod(0o600, passphrase_path)
    passphrase
  end
end

#telegram_bot_token ⇒ Object

--- Channels ---



181
182
183
184
# File 'lib/kodo/config.rb', line 181

def telegram_bot_token
  env_var = data.dig('channels', 'telegram', 'bot_token_env')
  ENV.fetch(env_var) { raise Error, "Missing environment variable: #{env_var}" }
end

#telegram_enabled? ⇒ Boolean

Returns:

  • (Boolean)


186
187
188
# File 'lib/kodo/config.rb', line 186

def telegram_enabled?
  data.dig('channels', 'telegram', 'enabled') == true
end

#utility_model ⇒ Object



118
# File 'lib/kodo/config.rb', line 118

def utility_model = data.dig('llm', 'utility_model') || llm_model

#web_audit_urls? ⇒ Boolean

Returns:

  • (Boolean)


213
# File 'lib/kodo/config.rb', line 213

def web_audit_urls?        = data.dig('web', 'audit_urls') != false

#web_fetch_allowlist ⇒ Object



219
220
221
# File 'lib/kodo/config.rb', line 219

def web_fetch_allowlist
  data.dig('web', 'fetch_allowlist') || []
end

#web_fetch_blocklist ⇒ Object



215
216
217
# File 'lib/kodo/config.rb', line 215

def web_fetch_blocklist
  data.dig('web', 'fetch_blocklist') || []
end

#web_fetch_url_enabled? ⇒ Boolean

--- Web ---

Returns:

  • (Boolean)


210
# File 'lib/kodo/config.rb', line 210

def web_fetch_url_enabled? = data.dig('web', 'fetch_url_enabled') != false

#web_injection_scan? ⇒ Boolean

Returns:

  • (Boolean)


212
# File 'lib/kodo/config.rb', line 212

def web_injection_scan?    = data.dig('web', 'injection_scan') != false

#web_search_enabled? ⇒ Boolean

Returns:

  • (Boolean)


211
# File 'lib/kodo/config.rb', line 211

def web_search_enabled?    = data.dig('web', 'web_search_enabled') != false

#web_ssrf_bypass_hosts ⇒ Object



223
224
225
# File 'lib/kodo/config.rb', line 223

def web_ssrf_bypass_hosts
  data.dig('web', 'ssrf_bypass_hosts') || []
end