discourse/plugins/discourse-ai/app/models/ai_tool_secret_binding.rb
Sam 10d109e4c8
FEATURE: add credential bindings for AI tools (#37891)
Introduce a "secret contracts" system that lets AI tools
declare required credentials by alias and bind them to
existing AiSecret records via a new join table.

Key changes:
- New `ai_tool_secret_bindings` table and AiToolSecretBinding
  model linking AiTool to AiSecret by alias
- `secret_contracts` JSONB column on ai_tools for declaring
  required credential aliases
- `secrets.get(alias)` API available in tool scripts at
  runtime, with preloaded secret caching
- Admin UI for managing credential contracts and bindings on
  the tool editor form, with status badges on the tool list
- Tool presets (stock_quote, image generation) updated to use
  `secrets.get()` instead of hardcoded placeholder keys
- Secrets list editor shows tool usages alongside LLM and
  embedding usages
- Test modal supports passing secret bindings and rendering
  custom_raw output
- Validation, error handling, and i18n for contracts/bindings
- Persona import/export updated to include secret_contracts

---------

Co-authored-by: Keegan George <kgeorge13@gmail.com>
Co-authored-by: awesomerobot <kris.aubuchon@discourse.org>
2026-02-20 07:56:15 +11:00

62 lines
1.7 KiB
Ruby
Vendored

# frozen_string_literal: true
class AiToolSecretBinding < ActiveRecord::Base
belongs_to :ai_tool
belongs_to :ai_secret
belongs_to :created_by, class_name: "User", optional: true
validates :alias,
presence: true,
length: {
maximum: 100,
},
format: {
with: AiTool::SECRET_ALIAS_PATTERN,
message: I18n.t("discourse_ai.tools.name.characters"),
},
uniqueness: {
scope: :ai_tool_id,
}
validates :ai_secret_id, presence: true
validate :secret_exists
validate :alias_declared
private
def secret_exists
return if ai_secret_id.blank?
return if AiSecret.exists?(ai_secret_id)
errors.add(:ai_secret_id, I18n.t("discourse_ai.tools.secret_bindings.secret_not_found"))
end
def alias_declared
return if self[:alias].blank? || ai_tool.blank?
return if ai_tool.secret_contract_for(self[:alias]).present?
errors.add(
:alias,
I18n.t("discourse_ai.tools.secret_bindings.alias_not_declared", alias: self[:alias]),
)
end
end
# == Schema Information
#
# Table name: ai_tool_secret_bindings
#
# id :bigint not null, primary key
# alias :string(100) not null
# created_at :datetime not null
# updated_at :datetime not null
# ai_secret_id :bigint not null
# ai_tool_id :bigint not null
# created_by_id :integer
#
# Indexes
#
# index_ai_tool_secret_bindings_on_ai_secret_id (ai_secret_id)
# index_ai_tool_secret_bindings_on_ai_tool_id (ai_tool_id)
# index_ai_tool_secret_bindings_on_ai_tool_id_and_alias (ai_tool_id,alias) UNIQUE
#