Files

1 line
32 KiB
JSON

{"repo": "apiguy/kodo", "files": {"lib/kodo/llm.rb": {"7102b0078956cbeb528a399a750f3fdc2ad5cf00": {"t": 1785745741.4311237, "n": 2004, "h": "2e7643f883fc26d0", "c": "# frozen_string_literal: true\n\nrequire 'ruby_llm'\n\nmodule Kodo\n module LLM\n class << self\n # Map of canonical secret names to RubyLLM config setter names\n PROVIDER_SECRETS = {\n 'anthropic_api_key' => 'anthropic',\n 'openai_api_key' => 'openai',\n 'gemini_api_key' => 'gemini',\n 'deepseek_api_key' => 'deepseek',\n 'mistral_api_key' => 'mistral',\n 'openrouter_api_key' => 'openrouter',\n 'perplexity_api_key' => 'perplexity',\n 'xai_api_key' => 'xai'\n }.freeze\n\n # Configure RubyLLM from Kodo's config, optionally resolving keys via broker\n def configure!(config, broker: nil)\n RubyLLM.configure do |c|\n if broker\n configure_via_broker(c, broker)\n else\n config.llm_api_keys.each do |provider, key|\n setter = \"#{provider}_api_key=\"\n c.send(setter, key) if c.respond_to?(setter)\n end\n end\n\n if (ollama_url = config.ollama_api_base)\n c.ollama_api_base = ollama_url\n end\n end\n\n # Refresh model registry so newly released models are recognized\n RubyLLM.models.refresh!\n\n Kodo.logger.info(\"LLM configured: #{config.llm_model}\")\n end\n\n # Create a new chat instance with the configured model\n def chat(model: nil)\n RubyLLM.chat(model: model || Kodo.config.llm_model)\n end\n\n # Create a chat instance with the utility model (for lightweight tasks like redaction)\n def utility_chat(model: nil)\n RubyLLM.chat(model: model || Kodo.config.utility_model)\n end\n\n private\n\n def configure_via_broker(ruby_llm_config, broker)\n PROVIDER_SECRETS.each do |secret_name, provider|\n key = broker.fetch(secret_name, requestor: 'llm')\n next unless key\n\n setter = \"#{provider}_api_key=\"\n ruby_llm_config.send(setter, key) if ruby_llm_config.respond_to?(setter)\n end\n end\n end\n end\nend\n"}, "130976edcb6d76e066e99065714c59098e881dd3": {"t": 1785756056.1925895, "n": 2004, "h": "2e7643f883fc26d0", "c": "# frozen_string_literal: true\n\nrequire 'ruby_llm'\n\nmodule Kodo\n module LLM\n class << self\n # Map of canonical secret names to RubyLLM config setter names\n PROVIDER_SECRETS = {\n 'anthropic_api_key' => 'anthropic',\n 'openai_api_key' => 'openai',\n 'gemini_api_key' => 'gemini',\n 'deepseek_api_key' => 'deepseek',\n 'mistral_api_key' => 'mistral',\n 'openrouter_api_key' => 'openrouter',\n 'perplexity_api_key' => 'perplexity',\n 'xai_api_key' => 'xai'\n }.freeze\n\n # Configure RubyLLM from Kodo's config, optionally resolving keys via broker\n def configure!(config, broker: nil)\n RubyLLM.configure do |c|\n if broker\n configure_via_broker(c, broker)\n else\n config.llm_api_keys.each do |provider, key|\n setter = \"#{provider}_api_key=\"\n c.send(setter, key) if c.respond_to?(setter)\n end\n end\n\n if (ollama_url = config.ollama_api_base)\n c.ollama_api_base = ollama_url\n end\n end\n\n # Refresh model registry so newly released models are recognized\n RubyLLM.models.refresh!\n\n Kodo.logger.info(\"LLM configured: #{config.llm_model}\")\n end\n\n # Create a new chat instance with the configured model\n def chat(model: nil)\n RubyLLM.chat(model: model || Kodo.config.llm_model)\n end\n\n # Create a chat instance with the utility model (for lightweight tasks like redaction)\n def utility_chat(model: nil)\n RubyLLM.chat(model: model || Kodo.config.utility_model)\n end\n\n private\n\n def configure_via_broker(ruby_llm_config, broker)\n PROVIDER_SECRETS.each do |secret_name, provider|\n key = broker.fetch(secret_name, requestor: 'llm')\n next unless key\n\n setter = \"#{provider}_api_key=\"\n ruby_llm_config.send(setter, key) if ruby_llm_config.respond_to?(setter)\n end\n end\n end\n end\nend\n"}, "1ca87278db6793f315d168da07a00a18510fa14a": {"t": 1785756057.6038904, "n": 1052, "h": "63b200f5434c6a46", "c": "# frozen_string_literal: true\n\nrequire \"ruby_llm\"\n\nmodule Kodo\n module LLM\n class << self\n # Configure RubyLLM from Kodo's config\n def configure!(config)\n RubyLLM.configure do |c|\n config.llm_api_keys.each do |provider, key|\n setter = \"#{provider}_api_key=\"\n c.send(setter, key) if c.respond_to?(setter)\n end\n\n if (ollama_url = config.ollama_api_base)\n c.ollama_api_base = ollama_url\n end\n end\n\n # Refresh model registry so newly released models are recognized\n RubyLLM.models.refresh!\n\n Kodo.logger.info(\"LLM configured: #{config.llm_model}\")\n end\n\n # Create a new chat instance with the configured model\n def chat(model: nil)\n RubyLLM.chat(model: model || Kodo.config.llm_model)\n end\n\n # Create a chat instance with the utility model (for lightweight tasks like redaction)\n def utility_chat(model: nil)\n RubyLLM.chat(model: model || Kodo.config.utility_model)\n end\n end\n end\nend\n"}, "bb7bff99cf427d600f3832b0c0ac1de95b235640": {"t": 1785756058.9312866, "n": 946, "h": "95f2969b928d52a9", "c": "# frozen_string_literal: true\n\nrequire \"ruby_llm\"\n\nmodule Kodo\n module LLM\n class << self\n # Configure RubyLLM from Kodo's config\n def configure!(config)\n RubyLLM.configure do |c|\n config.llm_api_keys.each do |provider, key|\n setter = \"#{provider}_api_key=\"\n c.send(setter, key) if c.respond_to?(setter)\n end\n\n if (ollama_url = config.ollama_api_base)\n c.ollama_api_base = ollama_url\n end\n end\n\n Kodo.logger.info(\"LLM configured: #{config.llm_model}\")\n end\n\n # Create a new chat instance with the configured model\n def chat(model: nil)\n RubyLLM.chat(model: model || Kodo.config.llm_model)\n end\n\n # Create a chat instance with the utility model (for lightweight tasks like redaction)\n def utility_chat(model: nil)\n RubyLLM.chat(model: model || Kodo.config.utility_model)\n end\n end\n end\nend\n"}, "d514675560fc262cd445513a2387a6a92827dfef": {"t": 1785756060.1757739, "n": 743, "h": "21ea1ba61f284442", "c": "# frozen_string_literal: true\n\nrequire \"ruby_llm\"\n\nmodule Kodo\n module LLM\n class << self\n # Configure RubyLLM from Kodo's config\n def configure!(config)\n RubyLLM.configure do |c|\n config.llm_api_keys.each do |provider, key|\n setter = \"#{provider}_api_key=\"\n c.send(setter, key) if c.respond_to?(setter)\n end\n\n if (ollama_url = config.ollama_api_base)\n c.ollama_api_base = ollama_url\n end\n end\n\n Kodo.logger.info(\"LLM configured: #{config.llm_model}\")\n end\n\n # Create a new chat instance with the configured model\n def chat(model: nil)\n RubyLLM.chat(model: model || Kodo.config.llm_model)\n end\n end\n end\nend\n"}}, "lib/kodo/secrets/broker.rb": {"7102b0078956cbeb528a399a750f3fdc2ad5cf00": {"t": 1785745743.562092, "n": 3810, "h": "e31241abab445bea", "c": "# frozen_string_literal: true\n\nmodule Kodo\n module Secrets\n class Broker\n GRANTS = [\n Grant.new(secret_name: 'anthropic_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'openai_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'gemini_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'deepseek_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'mistral_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'openrouter_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'perplexity_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'xai_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'tavily_api_key', requestor: 'search'),\n Grant.new(secret_name: 'telegram_bot_token', requestor: 'telegram')\n ].freeze\n\n ENV_FALLBACKS = {\n 'anthropic_api_key' => 'ANTHROPIC_API_KEY',\n 'openai_api_key' => 'OPENAI_API_KEY',\n 'gemini_api_key' => 'GEMINI_API_KEY',\n 'deepseek_api_key' => 'DEEPSEEK_API_KEY',\n 'mistral_api_key' => 'MISTRAL_API_KEY',\n 'openrouter_api_key' => 'OPENROUTER_API_KEY',\n 'perplexity_api_key' => 'PERPLEXITY_API_KEY',\n 'xai_api_key' => 'XAI_API_KEY',\n 'tavily_api_key' => 'TAVILY_API_KEY',\n 'telegram_bot_token' => 'TELEGRAM_BOT_TOKEN'\n }.freeze\n\n def initialize(store:, audit:)\n @store = store\n @audit = audit\n end\n\n def fetch(name, requestor:)\n unless authorized?(name, requestor)\n @audit.log(\n event: 'secret_access_denied',\n detail: \"secret:#{name} requestor:#{requestor}\"\n )\n return nil\n end\n\n value = @store.get(name)\n return value if value\n\n env_var = ENV_FALLBACKS[name]\n env_value = ENV[env_var] if env_var\n return env_value if env_value && !env_value.empty?\n\n nil\n end\n\n def fetch!(name, requestor:)\n value = fetch(name, requestor: requestor)\n raise Kodo::Error, \"Secret not available: #{name}\" unless value\n\n value\n end\n\n def store(name, value, source: 'user', validated: false)\n @store.put(name, value, source: source, validated: validated)\n @audit.log(\n event: 'secret_stored',\n detail: \"secret:#{name} source:#{source} validated:#{validated}\"\n )\n end\n\n def available?(name)\n return true if @store.exists?(name)\n\n env_var = ENV_FALLBACKS[name]\n return false unless env_var\n\n value = ENV[env_var]\n !value.nil? && !value.empty?\n end\n\n def configured_secrets\n all_names = GRANTS.map(&:secret_name).uniq\n all_names.select { |name| available?(name) }\n end\n\n def credential_summary\n GRANTS.map(&:secret_name).uniq.filter_map do |name|\n next unless available?(name)\n\n meta = @store.metadata(name)\n source = meta ? meta[:source] : 'env'\n validated = meta ? meta[:validated] : false\n stored_at = meta ? meta[:stored_at] : nil\n\n { name: name, source: source, validated: validated, stored_at: stored_at }\n end\n end\n\n # Returns all current secret values (store + env) for exfiltration prevention.\n # This is a security-internal method — not an LLM-accessible action.\n def sensitive_values\n GRANTS.map(&:secret_name).uniq.filter_map do |name|\n value = @store.get(name)\n next value if value\n\n env_var = ENV_FALLBACKS[name]\n next unless env_var\n\n v = ENV[env_var]\n v unless v.nil? || v.empty?\n end\n end\n\n private\n\n def authorized?(name, requestor)\n GRANTS.any? { |g| g.secret_name == name && g.requestor == requestor }\n end\n end\n end\nend\n"}, "816d1aea296c60624bcb74e6428e5df2487cb632": {"t": 1785756055.055653, "n": 3810, "h": "e31241abab445bea", "c": "# frozen_string_literal: true\n\nmodule Kodo\n module Secrets\n class Broker\n GRANTS = [\n Grant.new(secret_name: 'anthropic_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'openai_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'gemini_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'deepseek_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'mistral_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'openrouter_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'perplexity_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'xai_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'tavily_api_key', requestor: 'search'),\n Grant.new(secret_name: 'telegram_bot_token', requestor: 'telegram')\n ].freeze\n\n ENV_FALLBACKS = {\n 'anthropic_api_key' => 'ANTHROPIC_API_KEY',\n 'openai_api_key' => 'OPENAI_API_KEY',\n 'gemini_api_key' => 'GEMINI_API_KEY',\n 'deepseek_api_key' => 'DEEPSEEK_API_KEY',\n 'mistral_api_key' => 'MISTRAL_API_KEY',\n 'openrouter_api_key' => 'OPENROUTER_API_KEY',\n 'perplexity_api_key' => 'PERPLEXITY_API_KEY',\n 'xai_api_key' => 'XAI_API_KEY',\n 'tavily_api_key' => 'TAVILY_API_KEY',\n 'telegram_bot_token' => 'TELEGRAM_BOT_TOKEN'\n }.freeze\n\n def initialize(store:, audit:)\n @store = store\n @audit = audit\n end\n\n def fetch(name, requestor:)\n unless authorized?(name, requestor)\n @audit.log(\n event: 'secret_access_denied',\n detail: \"secret:#{name} requestor:#{requestor}\"\n )\n return nil\n end\n\n value = @store.get(name)\n return value if value\n\n env_var = ENV_FALLBACKS[name]\n env_value = ENV[env_var] if env_var\n return env_value if env_value && !env_value.empty?\n\n nil\n end\n\n def fetch!(name, requestor:)\n value = fetch(name, requestor: requestor)\n raise Kodo::Error, \"Secret not available: #{name}\" unless value\n\n value\n end\n\n def store(name, value, source: 'user', validated: false)\n @store.put(name, value, source: source, validated: validated)\n @audit.log(\n event: 'secret_stored',\n detail: \"secret:#{name} source:#{source} validated:#{validated}\"\n )\n end\n\n def available?(name)\n return true if @store.exists?(name)\n\n env_var = ENV_FALLBACKS[name]\n return false unless env_var\n\n value = ENV[env_var]\n !value.nil? && !value.empty?\n end\n\n def configured_secrets\n all_names = GRANTS.map(&:secret_name).uniq\n all_names.select { |name| available?(name) }\n end\n\n def credential_summary\n GRANTS.map(&:secret_name).uniq.filter_map do |name|\n next unless available?(name)\n\n meta = @store.metadata(name)\n source = meta ? meta[:source] : 'env'\n validated = meta ? meta[:validated] : false\n stored_at = meta ? meta[:stored_at] : nil\n\n { name: name, source: source, validated: validated, stored_at: stored_at }\n end\n end\n\n # Returns all current secret values (store + env) for exfiltration prevention.\n # This is a security-internal method — not an LLM-accessible action.\n def sensitive_values\n GRANTS.map(&:secret_name).uniq.filter_map do |name|\n value = @store.get(name)\n next value if value\n\n env_var = ENV_FALLBACKS[name]\n next unless env_var\n\n v = ENV[env_var]\n v unless v.nil? || v.empty?\n end\n end\n\n private\n\n def authorized?(name, requestor)\n GRANTS.any? { |g| g.secret_name == name && g.requestor == requestor }\n end\n end\n end\nend\n"}, "e0c74e43800ea88482f79faa39cd66a29ab87d6e": {"t": 1785756056.4912436, "n": 3379, "h": "7961816b24de835f", "c": "# frozen_string_literal: true\n\nmodule Kodo\n module Secrets\n class Broker\n GRANTS = [\n Grant.new(secret_name: 'anthropic_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'openai_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'gemini_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'deepseek_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'mistral_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'openrouter_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'perplexity_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'xai_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'tavily_api_key', requestor: 'search'),\n Grant.new(secret_name: 'telegram_bot_token', requestor: 'telegram')\n ].freeze\n\n ENV_FALLBACKS = {\n 'anthropic_api_key' => 'ANTHROPIC_API_KEY',\n 'openai_api_key' => 'OPENAI_API_KEY',\n 'gemini_api_key' => 'GEMINI_API_KEY',\n 'deepseek_api_key' => 'DEEPSEEK_API_KEY',\n 'mistral_api_key' => 'MISTRAL_API_KEY',\n 'openrouter_api_key' => 'OPENROUTER_API_KEY',\n 'perplexity_api_key' => 'PERPLEXITY_API_KEY',\n 'xai_api_key' => 'XAI_API_KEY',\n 'tavily_api_key' => 'TAVILY_API_KEY',\n 'telegram_bot_token' => 'TELEGRAM_BOT_TOKEN'\n }.freeze\n\n def initialize(store:, audit:)\n @store = store\n @audit = audit\n end\n\n def fetch(name, requestor:)\n unless authorized?(name, requestor)\n @audit.log(\n event: 'secret_access_denied',\n detail: \"secret:#{name} requestor:#{requestor}\"\n )\n return nil\n end\n\n value = @store.get(name)\n return value if value\n\n env_var = ENV_FALLBACKS[name]\n env_value = ENV[env_var] if env_var\n return env_value if env_value && !env_value.empty?\n\n nil\n end\n\n def fetch!(name, requestor:)\n value = fetch(name, requestor: requestor)\n raise Kodo::Error, \"Secret not available: #{name}\" unless value\n\n value\n end\n\n def store(name, value, source: 'user', validated: false)\n @store.put(name, value, source: source, validated: validated)\n @audit.log(\n event: 'secret_stored',\n detail: \"secret:#{name} source:#{source} validated:#{validated}\"\n )\n end\n\n def available?(name)\n return true if @store.exists?(name)\n\n env_var = ENV_FALLBACKS[name]\n return false unless env_var\n\n value = ENV[env_var]\n !value.nil? && !value.empty?\n end\n\n def configured_secrets\n all_names = GRANTS.map(&:secret_name).uniq\n all_names.select { |name| available?(name) }\n end\n\n # Returns all current secret values (store + env) for exfiltration prevention.\n # This is a security-internal method — not an LLM-accessible action.\n def sensitive_values\n GRANTS.map(&:secret_name).uniq.filter_map do |name|\n value = @store.get(name)\n next value if value\n\n env_var = ENV_FALLBACKS[name]\n next unless env_var\n\n v = ENV[env_var]\n v unless v.nil? || v.empty?\n end\n end\n\n private\n\n def authorized?(name, requestor)\n GRANTS.any? { |g| g.secret_name == name && g.requestor == requestor }\n end\n end\n end\nend\n"}, "130976edcb6d76e066e99065714c59098e881dd3": {"t": 1785756057.859579, "n": 2907, "h": "9527d4f654da5e0a", "c": "# frozen_string_literal: true\n\nmodule Kodo\n module Secrets\n class Broker\n GRANTS = [\n Grant.new(secret_name: 'anthropic_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'openai_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'gemini_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'deepseek_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'mistral_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'openrouter_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'perplexity_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'xai_api_key', requestor: 'llm'),\n Grant.new(secret_name: 'tavily_api_key', requestor: 'search'),\n Grant.new(secret_name: 'telegram_bot_token', requestor: 'telegram')\n ].freeze\n\n ENV_FALLBACKS = {\n 'anthropic_api_key' => 'ANTHROPIC_API_KEY',\n 'openai_api_key' => 'OPENAI_API_KEY',\n 'gemini_api_key' => 'GEMINI_API_KEY',\n 'deepseek_api_key' => 'DEEPSEEK_API_KEY',\n 'mistral_api_key' => 'MISTRAL_API_KEY',\n 'openrouter_api_key' => 'OPENROUTER_API_KEY',\n 'perplexity_api_key' => 'PERPLEXITY_API_KEY',\n 'xai_api_key' => 'XAI_API_KEY',\n 'tavily_api_key' => 'TAVILY_API_KEY',\n 'telegram_bot_token' => 'TELEGRAM_BOT_TOKEN'\n }.freeze\n\n def initialize(store:, audit:)\n @store = store\n @audit = audit\n end\n\n def fetch(name, requestor:)\n unless authorized?(name, requestor)\n @audit.log(\n event: 'secret_access_denied',\n detail: \"secret:#{name} requestor:#{requestor}\"\n )\n return nil\n end\n\n value = @store.get(name)\n return value if value\n\n env_var = ENV_FALLBACKS[name]\n env_value = ENV[env_var] if env_var\n return env_value if env_value && !env_value.empty?\n\n nil\n end\n\n def fetch!(name, requestor:)\n value = fetch(name, requestor: requestor)\n raise Kodo::Error, \"Secret not available: #{name}\" unless value\n\n value\n end\n\n def store(name, value, source: 'user', validated: false)\n @store.put(name, value, source: source, validated: validated)\n @audit.log(\n event: 'secret_stored',\n detail: \"secret:#{name} source:#{source} validated:#{validated}\"\n )\n end\n\n def available?(name)\n return true if @store.exists?(name)\n\n env_var = ENV_FALLBACKS[name]\n return false unless env_var\n\n value = ENV[env_var]\n !value.nil? && !value.empty?\n end\n\n def configured_secrets\n all_names = GRANTS.map(&:secret_name).uniq\n all_names.select { |name| available?(name) }\n end\n\n private\n\n def authorized?(name, requestor)\n GRANTS.any? { |g| g.secret_name == name && g.requestor == requestor }\n end\n end\n end\nend\n"}}, "lib/kodo/tools/store_secret.rb": {"7102b0078956cbeb528a399a750f3fdc2ad5cf00": {"t": 1785745747.593924, "n": 4955, "h": "b6fe234b47c67658", "c": "# frozen_string_literal: true\n\nrequire 'ruby_llm'\nrequire 'net/http'\nrequire 'uri'\nrequire 'json'\n\nmodule Kodo\n module Tools\n class StoreSecret < RubyLLM::Tool\n extend PromptContributor\n\n capability_name 'Secret Storage'\n capability_primary true\n enabled_guidance 'Use store_secret to store and activate API keys securely — no restart required.'\n\n MAX_PER_TURN = 2\n VALIDATION_TIMEOUT = 5\n\n KNOWN_SECRETS = {\n 'anthropic_api_key' => { description: 'Anthropic API key', prefix: 'sk-ant-' },\n 'openai_api_key' => { description: 'OpenAI API key', prefix: 'sk-' },\n 'gemini_api_key' => { description: 'Google Gemini API key' },\n 'deepseek_api_key' => { description: 'DeepSeek API key' },\n 'mistral_api_key' => { description: 'Mistral API key' },\n 'openrouter_api_key' => { description: 'OpenRouter API key', prefix: 'sk-or-' },\n 'perplexity_api_key' => { description: 'Perplexity API key' },\n 'xai_api_key' => { description: 'xAI API key' },\n 'tavily_api_key' => { description: 'Tavily API key', prefix: 'tvly-' },\n 'telegram_bot_token' => { description: 'Telegram bot token' }\n }.freeze\n\n description 'Securely store an API key or token. Use this when the user provides ' \\\n 'a key they want to configure. The key is encrypted at rest and ' \\\n 'activated immediately without requiring a restart.'\n\n param :secret_name, desc: \"The secret identifier. One of: #{KNOWN_SECRETS.keys.join(', ')}\"\n param :secret_value, desc: 'The secret value (API key or token)'\n\n def initialize(broker:, audit:, on_secret_stored: nil)\n super()\n @broker = broker\n @audit = audit\n @on_secret_stored = on_secret_stored\n @turn_count = 0\n end\n\n def reset_turn_count!\n @turn_count = 0\n end\n\n def execute(secret_name:, secret_value:) # rubocop:disable Metrics/MethodLength\n unless KNOWN_SECRETS.key?(secret_name)\n return \"Unknown secret '#{secret_name}'. Known secrets: #{KNOWN_SECRETS.keys.join(', ')}\"\n end\n\n @turn_count += 1\n if @turn_count > MAX_PER_TURN\n return \"Rate limit reached (max #{MAX_PER_TURN} secrets per message). Try again next message.\"\n end\n\n prefix_error = validate_prefix(secret_name, secret_value)\n return prefix_error if prefix_error\n\n validated = validate_key(secret_name, secret_value)\n\n @broker.store(secret_name, secret_value, source: 'chat', validated: validated)\n\n @audit.log(\n event: 'secret_stored_via_tool',\n detail: \"secret:#{secret_name} validated:#{validated}\"\n )\n\n @on_secret_stored&.call(secret_name)\n\n desc = KNOWN_SECRETS[secret_name][:description]\n if validated\n \"#{desc} stored and validated successfully. It's now active — no restart needed.\"\n else\n \"#{desc} stored and activated. Couldn't verify it online, but it's ready to use.\"\n end\n rescue Kodo::Error => e\n e.message\n end\n\n def name\n 'store_secret'\n end\n\n private\n\n def validate_prefix(secret_name, value)\n expected = KNOWN_SECRETS.dig(secret_name, :prefix)\n return nil unless expected\n\n return nil if value.start_with?(expected)\n\n \"Invalid #{KNOWN_SECRETS[secret_name][:description]}: expected to start with '#{expected}'\"\n end\n\n def validate_key(secret_name, value)\n case secret_name\n when 'tavily_api_key' then validate_tavily(value)\n when 'anthropic_api_key' then validate_anthropic(value)\n else false\n end\n rescue StandardError\n false\n end\n\n def validate_tavily(key)\n uri = URI('https://api.tavily.com/search')\n body = { api_key: key, query: 'test', max_results: 1 }.to_json\n response = http_post(uri, body, 'application/json')\n response.is_a?(Net::HTTPSuccess)\n end\n\n def validate_anthropic(key)\n uri = URI('https://api.anthropic.com/v1/messages')\n body = {\n model: 'claude-haiku-4-5-20251001',\n max_tokens: 1,\n messages: [{ role: 'user', content: 'hi' }]\n }.to_json\n response = http_post(uri, body, 'application/json',\n 'x-api-key' => key,\n 'anthropic-version' => '2023-06-01')\n response.is_a?(Net::HTTPSuccess)\n end\n\n def http_post(uri, body, content_type, extra_headers = {})\n http = Net::HTTP.new(uri.host, uri.port)\n http.use_ssl = true\n http.open_timeout = VALIDATION_TIMEOUT\n http.read_timeout = VALIDATION_TIMEOUT\n\n request = Net::HTTP::Post.new(uri)\n request['Content-Type'] = content_type\n extra_headers.each { |k, v| request[k] = v }\n request.body = body\n\n http.request(request)\n end\n end\n end\nend\n"}, "130976edcb6d76e066e99065714c59098e881dd3": {"t": 1785756056.4205694, "n": 4955, "h": "b6fe234b47c67658", "c": "# frozen_string_literal: true\n\nrequire 'ruby_llm'\nrequire 'net/http'\nrequire 'uri'\nrequire 'json'\n\nmodule Kodo\n module Tools\n class StoreSecret < RubyLLM::Tool\n extend PromptContributor\n\n capability_name 'Secret Storage'\n capability_primary true\n enabled_guidance 'Use store_secret to store and activate API keys securely — no restart required.'\n\n MAX_PER_TURN = 2\n VALIDATION_TIMEOUT = 5\n\n KNOWN_SECRETS = {\n 'anthropic_api_key' => { description: 'Anthropic API key', prefix: 'sk-ant-' },\n 'openai_api_key' => { description: 'OpenAI API key', prefix: 'sk-' },\n 'gemini_api_key' => { description: 'Google Gemini API key' },\n 'deepseek_api_key' => { description: 'DeepSeek API key' },\n 'mistral_api_key' => { description: 'Mistral API key' },\n 'openrouter_api_key' => { description: 'OpenRouter API key', prefix: 'sk-or-' },\n 'perplexity_api_key' => { description: 'Perplexity API key' },\n 'xai_api_key' => { description: 'xAI API key' },\n 'tavily_api_key' => { description: 'Tavily API key', prefix: 'tvly-' },\n 'telegram_bot_token' => { description: 'Telegram bot token' }\n }.freeze\n\n description 'Securely store an API key or token. Use this when the user provides ' \\\n 'a key they want to configure. The key is encrypted at rest and ' \\\n 'activated immediately without requiring a restart.'\n\n param :secret_name, desc: \"The secret identifier. One of: #{KNOWN_SECRETS.keys.join(', ')}\"\n param :secret_value, desc: 'The secret value (API key or token)'\n\n def initialize(broker:, audit:, on_secret_stored: nil)\n super()\n @broker = broker\n @audit = audit\n @on_secret_stored = on_secret_stored\n @turn_count = 0\n end\n\n def reset_turn_count!\n @turn_count = 0\n end\n\n def execute(secret_name:, secret_value:) # rubocop:disable Metrics/MethodLength\n unless KNOWN_SECRETS.key?(secret_name)\n return \"Unknown secret '#{secret_name}'. Known secrets: #{KNOWN_SECRETS.keys.join(', ')}\"\n end\n\n @turn_count += 1\n if @turn_count > MAX_PER_TURN\n return \"Rate limit reached (max #{MAX_PER_TURN} secrets per message). Try again next message.\"\n end\n\n prefix_error = validate_prefix(secret_name, secret_value)\n return prefix_error if prefix_error\n\n validated = validate_key(secret_name, secret_value)\n\n @broker.store(secret_name, secret_value, source: 'chat', validated: validated)\n\n @audit.log(\n event: 'secret_stored_via_tool',\n detail: \"secret:#{secret_name} validated:#{validated}\"\n )\n\n @on_secret_stored&.call(secret_name)\n\n desc = KNOWN_SECRETS[secret_name][:description]\n if validated\n \"#{desc} stored and validated successfully. It's now active — no restart needed.\"\n else\n \"#{desc} stored and activated. Couldn't verify it online, but it's ready to use.\"\n end\n rescue Kodo::Error => e\n e.message\n end\n\n def name\n 'store_secret'\n end\n\n private\n\n def validate_prefix(secret_name, value)\n expected = KNOWN_SECRETS.dig(secret_name, :prefix)\n return nil unless expected\n\n return nil if value.start_with?(expected)\n\n \"Invalid #{KNOWN_SECRETS[secret_name][:description]}: expected to start with '#{expected}'\"\n end\n\n def validate_key(secret_name, value)\n case secret_name\n when 'tavily_api_key' then validate_tavily(value)\n when 'anthropic_api_key' then validate_anthropic(value)\n else false\n end\n rescue StandardError\n false\n end\n\n def validate_tavily(key)\n uri = URI('https://api.tavily.com/search')\n body = { api_key: key, query: 'test', max_results: 1 }.to_json\n response = http_post(uri, body, 'application/json')\n response.is_a?(Net::HTTPSuccess)\n end\n\n def validate_anthropic(key)\n uri = URI('https://api.anthropic.com/v1/messages')\n body = {\n model: 'claude-haiku-4-5-20251001',\n max_tokens: 1,\n messages: [{ role: 'user', content: 'hi' }]\n }.to_json\n response = http_post(uri, body, 'application/json',\n 'x-api-key' => key,\n 'anthropic-version' => '2023-06-01')\n response.is_a?(Net::HTTPSuccess)\n end\n\n def http_post(uri, body, content_type, extra_headers = {})\n http = Net::HTTP.new(uri.host, uri.port)\n http.use_ssl = true\n http.open_timeout = VALIDATION_TIMEOUT\n http.read_timeout = VALIDATION_TIMEOUT\n\n request = Net::HTTP::Post.new(uri)\n request['Content-Type'] = content_type\n extra_headers.each { |k, v| request[k] = v }\n request.body = body\n\n http.request(request)\n end\n end\n end\nend\n"}}}}