Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions server/app/models/agents/component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,28 @@ class Component < ApplicationRecord
validates :component_type, presence: true

store :position, coder: JSON
<<<<<<< HEAD
=======

# Returns configuration with auth_config values masked for API responses.
# Currently applies to :a2a_agent components and masks all non-blank strings under auth_config.
def masked_configuration
return configuration if configuration.blank?
return configuration unless a2a_agent?

mask_a2a_secrets(configuration.deep_dup)
end

private

def mask_a2a_secrets(config)
config["api_key"] = Utils::SecretMasking::MASKED_VALUE if config["api_key"].present?
if config["auth_config"].present?
config["auth_config"] =
Utils::SecretMasking.mask_nested_values(config["auth_config"])
end
config
end
>>>>>>> 6e0b1e4c6 (fix(CE): return masked configuration for model (#1840))
end
end
51 changes: 51 additions & 0 deletions server/app/models/agents/knowledge_base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# frozen_string_literal: true

module Agents
class KnowledgeBase < ApplicationRecord
EMBEDDING_CONFIG_JSON_SCHEMA = Rails.root.join(
"app/models/schema_validations/knowledge_bases/configuration_embedding.json"
)
STORAGE_CONFIG_JSON_SCHEMA = Rails.root.join(
"app/models/schema_validations/knowledge_bases/configuration_storage.json"
)

belongs_to :workspace
belongs_to :hosted_data_store, optional: true
belongs_to :source_connector, class_name: "Connector", optional: true
belongs_to :destination_connector, class_name: "Connector", optional: true
has_many :knowledge_base_files, class_name: "Agents::KnowledgeBaseFile", dependent: :destroy

enum :knowledge_base_type, { vector_store: 0, semantic_data_model: 1 }

validates :name, presence: true
validates :knowledge_base_type, presence: true
validates :size, presence: true
validates :embedding_config, presence: true, json: { schema: lambda {
embedding_config_schema_validation
} }
validates :storage_config, presence: true, json: { schema: lambda {
storage_config_schema_validation
} }

def masked_embedding_config
return embedding_config if embedding_config.blank?

mask_secret_values(embedding_config.deep_dup)
end

private

def embedding_config_schema_validation
EMBEDDING_CONFIG_JSON_SCHEMA
end

def storage_config_schema_validation
STORAGE_CONFIG_JSON_SCHEMA
end

def mask_secret_values(config)
config["api_key"] = Utils::SecretMasking.mask_nested_values(config["api_key"]) if config["api_key"].present?
config
end
end
end
58 changes: 58 additions & 0 deletions server/app/models/agents/tool.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# frozen_string_literal: true

module Agents
class Tool < ApplicationRecord
MCP_CONFIG_JSON_SCHEMA = Rails.root.join(
"app/models/schema_validations/tools/mcp.json"
)

belongs_to :workspace

enum :tool_type, { mcp: 0 }

validates :name, presence: true, uniqueness: { scope: :workspace_id, case_sensitive: false }
validates :tool_type, presence: true
validates :configuration, presence: true, json: { schema: -> { configuration_schema } }

scope :recently_updated, -> { order(updated_at: :desc) }

scope :search_by_name, ->(query) { where("name ILIKE ?", "%#{sanitize_sql_like(query)}%") }

# Returns connection config for MCP tools
# Used by MCP client to establish connection
def connection_config
return {} unless mcp? && configuration.present?

cfg = configuration.with_indifferent_access
{
url: cfg["url"],
transport: cfg["transport"],
auth_type: cfg["auth_type"],
auth_config: cfg["auth_config"] || {},
headers: cfg["headers"] || {},
timeout: cfg["timeout"] || 30
}.compact
end

# Mask sensitive configuration values for API responses
def masked_configuration
return configuration if configuration.blank?

mask_mcp_secrets(configuration.deep_dup)
end

private

def configuration_schema
MCP_CONFIG_JSON_SCHEMA
end

def mask_mcp_secrets(config)
if config["auth_config"].present?
config["auth_config"] =
Utils::SecretMasking.mask_nested_values(config["auth_config"])
end
config
end
end
end
11 changes: 9 additions & 2 deletions server/app/models/connector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -214,12 +214,12 @@ def resolved_configuration

def masked_configuration
spec = connector_client.new.connector_spec[:connection_specification].with_indifferent_access
secret_keys = extract_secret_keys(spec)
mask_secret_values(configuration.deep_dup, secret_keys)
Utils::SecretMasking.mask_by_keys(configuration.deep_dup, spec)
end

private

<<<<<<< HEAD
def extract_secret_keys(schema, keys = [])
return keys unless schema.is_a?(Hash)

Expand Down Expand Up @@ -249,6 +249,13 @@ def mask_secret_values(config, secret_keys)
config.map { |item| mask_secret_values(item, secret_keys) }
else
config
=======
def format_llm_payload(payload)
if connector_name == "Anthropic"
payload.to_json
else
payload
>>>>>>> 6e0b1e4c6 (fix(CE): return masked configuration for model (#1840))
end
end
end
Expand Down
10 changes: 10 additions & 0 deletions server/app/models/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ def json_schema
configuration["json_schema"]
end

def masked_configuration
return configuration if configuration.blank?

schema_path = configuration_schema_validation
return configuration if schema_path.nil?

schema = JSON.parse(File.read(schema_path)).with_indifferent_access
Utils::SecretMasking.mask_by_keys(configuration.deep_dup, schema)
end

private

def configuration_schema_validation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"properties": {
"api_key": {
"type": "string",
"minLength": 1
"minLength": 1,
"multiwoven_secret": true
},
"model": {
"type": "string",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"properties": {
"api_key": {
"type": "string",
"minLength": 1
"minLength": 1,
"multiwoven_secret": true
},
"model": {
"type": "string",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "array",
"items": {
"type": "object",
"properties": {
"from": { "type": "string" },
"to": { "type": "string" },
"embedding_config": {
"type": "object",
"properties": {
"mode": { "type": "string" },
"model": { "type": "string" },
"api_key": {
"type": "string",
"multiwoven_secret": true
}
}
}
}
}
}
8 changes: 8 additions & 0 deletions server/app/models/sync.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
# updated_at :datetime not null
#
class Sync < ApplicationRecord # rubocop:disable Metrics/ClassLength
SYNC_CONFIG_JSON_SCHEMA = Rails.root.join("app/models/schema_validations/syncs/configuration_embedding.json")
include AASM
include Discard::Model

Expand Down Expand Up @@ -80,6 +81,13 @@ class Sync < ApplicationRecord # rubocop:disable Metrics/ClassLength
end
end

def masked_configuration
return configuration if configuration.blank?

schema = JSON.parse(File.read(SYNC_CONFIG_JSON_SCHEMA)).with_indifferent_access
Utils::SecretMasking.mask_by_keys(configuration.deep_dup, schema)
end

def to_protocol
catalog = destination.catalog
Multiwoven::Integrations::Protocol::SyncConfig.new(
Expand Down
6 changes: 2 additions & 4 deletions server/app/serializers/model_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,9 @@ def configuration
if object.ai_ml?
connector = object.connector
json_schema = connector.catalog.json_schema(connector.connector_name)
existing_config = object.configuration

existing_config.merge({ json_schema: })
object.masked_configuration.merge({ json_schema: })
else
object.configuration
object.masked_configuration
end
end
end
8 changes: 8 additions & 0 deletions server/app/serializers/sync_serializer.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
# frozen_string_literal: true

class SyncSerializer < ActiveModel::Serializer
<<<<<<< HEAD
attributes :id, :source_id, :destination_id, :model_id, :configuration,
=======
attributes :id, :name, :source_id, :destination_id, :model_id,
>>>>>>> 6e0b1e4c6 (fix(CE): return masked configuration for model (#1840))
:schedule_type, :sync_mode, :sync_interval, :sync_interval_unit, :cron_expression,
:stream_name, :status, :cursor_field, :current_cursor_field,
:updated_at, :created_at

attribute :configuration do
object.masked_configuration
end

attribute :source do
ConnectorSerializer.new(object.source).attributes.except(:configuration)
end
Expand Down
62 changes: 62 additions & 0 deletions server/lib/utils/secret_masking.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# frozen_string_literal: true

module Utils
module SecretMasking
MASKED_VALUE = "*************"

module_function

def mask_by_keys(config, schema)
secret_keys = extract_secret_keys(schema)
return config if secret_keys.empty?

mask_values(config, secret_keys)
end

def mask_nested_values(obj)
case obj
when Hash
obj.transform_values { |v| mask_nested_values(v) }
when Array
obj.map { |v| mask_nested_values(v) }
when String
obj.present? ? MASKED_VALUE : obj
else
obj
end
end

def mask_values(config, secret_keys)
case config
when Hash
config.each_with_object({}) do |(key, value), result|
result[key] = if secret_keys.include?(key.to_s)
MASKED_VALUE
else
mask_values(value, secret_keys)
end
end
when Array
config.map { |item| mask_values(item, secret_keys) }
else
config
end
end

def extract_secret_keys(schema, keys = [])
return keys unless schema.is_a?(Hash)

schema = schema.with_indifferent_access
(schema["properties"] || {}).each do |key, subschema|
keys << key.to_s if subschema["multiwoven_secret"]
extract_secret_keys(subschema, keys)
end

extract_secret_keys(schema["items"], keys) if schema["items"]

keys
end

private_class_method :extract_secret_keys, :mask_values
end
end
Loading
Loading