Tools can now be managed by the Enterprise IT

This commit is contained in:
Peer Schütt 2026-07-20 16:58:23 +02:00
parent 548cd12934
commit 4fc6a22752
22 changed files with 329 additions and 67 deletions

View File

@ -175,7 +175,7 @@
<ProfileSelection MarginLeft="" @bind-CurrentProfile="@this.CurrentProfile"/>
}
@if (this.SettingsManager.IsToolSelectionVisible(this.Component))
@if (this.SettingsManager.AreToolsEnabled() && this.SettingsManager.IsToolSelectionVisible(this.Component))
{
<ToolSelection Component="@this.Component" LLMProvider="@this.ProviderSettings" SelectedToolIds="@this.selectedToolIds" SelectedToolIdsChanged="@this.SelectedToolIdsChanged" Disabled="@this.IsProcessing" />
}

View File

@ -3607,6 +3607,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T3728248397
-- Tool Settings
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T3730473128"] = "Tool Settings"
-- This tool has been disabled by your organization.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T3794167684"] = "This tool has been disabled by your organization."
-- Status
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTOOLS::T6222351"] = "Status"
@ -3724,6 +3727,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3119156561"] = "Required
-- Close
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3448155331"] = "Close"
-- This tool has been disabled by your organization.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3794167684"] = "This tool has been disabled by your organization."
-- No tools are available in this context.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3904490680"] = "No tools are available in this context."
@ -6535,6 +6541,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::TOOLSETTINGSDIALOG::T3616903110"]
-- Tool Settings
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::TOOLSETTINGSDIALOG::T3730473128"] = "Tool Settings"
-- This tool has been disabled by your organization.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::TOOLSETTINGSDIALOG::T3794167684"] = "This tool has been disabled by your organization."
-- The selected tool could not be loaded.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::TOOLSETTINGSDIALOG::T3907843187"] = "The selected tool could not be loaded."
@ -9286,6 +9295,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS:
-- Optional HTTP timeout for the search request in seconds.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3078115445"] = "Optional HTTP timeout for the search request in seconds."
-- The default safe search setting must be 0, 1, or 2.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3187215042"] = "The default safe search setting must be 0, 1, or 2."
-- Search the web with a configured SearXNG instance and retrieve the readable content of the best matching pages.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T3361633224"] = "Search the web with a configured SearXNG instance and retrieve the readable content of the best matching pages."

View File

@ -125,7 +125,10 @@
<ProfileSelection MarginLeft="" CurrentProfile="@this.currentProfile" CurrentProfileChanged="@this.ProfileWasChanged" Disabled="@(!this.currentChatTemplate.AllowProfileUsage)" DisabledText="@T("Profile usage is disabled according to your chat template settings.")"/>
<ToolSelection Component="Components.CHAT" LLMProvider="@this.Provider" SelectedToolIds="@this.selectedToolIds" SelectedToolIdsChanged="@this.SelectedToolIdsChanged" Disabled="@this.IsCurrentChatStreaming" />
@if (this.SettingsManager.AreToolsEnabled())
{
<ToolSelection Component="Components.CHAT" LLMProvider="@this.Provider" SelectedToolIds="@this.selectedToolIds" SelectedToolIdsChanged="@this.SelectedToolIdsChanged" Disabled="@this.IsCurrentChatStreaming" />
}
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
{

View File

@ -37,7 +37,13 @@
</MudMenu>
</MudTd>
<MudTd>
@if (context.ConfigurationState.IsConfigured)
@if (!context.IsActive)
{
<MudTooltip Text="@T("This tool has been disabled by your organization.")">
<MudIcon Icon="@Icons.Material.Filled.Lock" Color="Color.Error" />
</MudTooltip>
}
else if (context.ConfigurationState.IsConfigured)
{
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" />
}

View File

@ -8,5 +8,5 @@
{
<ConfigurationOption OptionDescription="@T("Show tool selection in this assistant?")" LabelOn="@T("Tool selection is visible")" LabelOff="@T("Tool selection is hidden")" State="@(() => this.SettingsManager.IsToolSelectionVisible(this.Component))" StateUpdate="@(value => this.SettingsManager.SetToolSelectionVisibility(this.Component, value))" />
}
<ConfigurationMultiSelect TData="string" OptionDescription="@this.OptionTitle" SelectedValues="@this.GetSelectedValues" Data="@this.availableTools" SelectionUpdate="@this.UpdateSelection" OptionHelp="@this.OptionHelp" Disabled="@(() => this.AreDefaultToolsDisabled)" EmptySelectionText="@T("No tools selected.")" SingleSelectionText="@T("You have selected 1 tool.")" MultipleSelectionText="@T("You have selected {0} tools.")" />
<ConfigurationMultiSelect TData="string" OptionDescription="@this.OptionTitle" SelectedValues="@this.GetSelectedValues" Data="@this.availableTools" SelectionUpdate="@this.UpdateSelection" OptionHelp="@this.OptionHelp" Disabled="@(() => this.AreDefaultToolsDisabled)" IsItemLocked="@this.IsToolDisabled" EmptySelectionText="@T("No tools selected.")" SingleSelectionText="@T("You have selected 1 tool.")" MultipleSelectionText="@T("You have selected {0} tools.")" />
}

View File

@ -29,6 +29,8 @@ public partial class ToolDefaultsConfiguration : MSGComponentBase
this.Component is not AIStudio.Tools.Components.CHAT &&
!this.SettingsManager.IsToolSelectionVisible(this.Component);
private bool IsToolDisabled(string toolId) => !this.SettingsManager.IsToolActive(toolId);
protected override async Task OnInitializedAsync()
{
this.availableTools = (await this.ToolRegistry.GetCatalogAsync(this.Component))

View File

@ -47,8 +47,14 @@
<MudPaper Class="pa-2 mb-2 border rounded-lg">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudSwitch T="bool" Color="Color.Primary" Value="@isSelected" ValueChanged="@(value => this.ChangeSelection(item.Definition.Id, value))" Disabled="@(!isConfigured || isBlockedByProviderConfidence || this.Disabled || !this.SupportsTools)" />
<MudSwitch T="bool" Color="Color.Primary" Value="@isSelected" ValueChanged="@(value => this.ChangeSelection(item.Definition.Id, value))" Disabled="@(!item.IsActive || !isConfigured || isBlockedByProviderConfidence || this.Disabled || !this.SupportsTools)" />
<MudIcon Icon="@item.Implementation.Icon" Color="Color.Info" />
@if (!item.IsActive)
{
<MudTooltip Text="@T("This tool has been disabled by your organization.")">
<MudIcon Icon="@Icons.Material.Filled.Lock" Color="Color.Error" Size="Size.Small" />
</MudTooltip>
}
<MudTooltip Text="@item.Implementation.GetDescription()">
<MudText Typo="Typo.body1">@item.Implementation.GetDisplayName()</MudText>
</MudTooltip>
@ -59,6 +65,10 @@
{
<MudText Typo="Typo.caption" Color="Color.Warning">@(string.IsNullOrWhiteSpace(item.ConfigurationState.Message) ? T("Required settings are missing. Configure this tool before enabling it.") : item.ConfigurationState.Message)</MudText>
}
@if (!item.IsActive)
{
<MudText Typo="Typo.caption" Color="Color.Warning">@T("This tool has been disabled by your organization.")</MudText>
}
@if (!string.IsNullOrWhiteSpace(providerConfidenceHint))
{
<MudText Typo="Typo.caption" Color="Color.Warning">@providerConfidenceHint</MudText>

View File

@ -73,6 +73,9 @@ public partial class ToolSelection : MSGComponentBase
private async Task ChangeSelection(string toolId, bool isSelected)
{
if (isSelected && !this.SettingsManager.IsToolActive(toolId))
return;
var updated = new HashSet<string>(this.SelectedToolIds, StringComparer.Ordinal);
if (isSelected)
updated.Add(toolId);

View File

@ -19,6 +19,11 @@
@this.implementation?.GetDescription()
</MudJustifiedText>
@if (!this.SettingsManager.IsToolActive(this.toolDefinition.Id))
{
<MudAlert Severity="Severity.Warning" Class="mb-4">@T("This tool has been disabled by your organization.")</MudAlert>
}
@if (!string.IsNullOrWhiteSpace(this.validationMessage))
{
<MudAlert Severity="Severity.Error" Class="mb-4">@this.validationMessage</MudAlert>

View File

@ -1,4 +1,3 @@
using AIStudio.Settings;
using AIStudio.Tools.ToolCallingSystem;
using Microsoft.AspNetCore.Components;
@ -53,19 +52,8 @@ public partial class ToolSettingsDialog : SettingsDialogBase
return string.Format(T("{0} Default: {1}"), description, defaultValue);
}
private bool IsFieldDisabled(string fieldName)
{
if (this.toolDefinition?.Id.Equals(ToolSelectionRules.WEB_SEARCH_TOOL_ID, StringComparison.Ordinal) is true &&
fieldName.Equals("baseUrl", StringComparison.Ordinal) &&
ManagedConfiguration.TryGet(x => x.Tools, x => x.WebSearchBaseUrl, out var webSearchMeta) &&
webSearchMeta.IsLocked)
return true;
return this.toolDefinition?.Id.Equals(ToolSelectionRules.READ_WEB_PAGE_TOOL_ID, StringComparison.Ordinal) is true &&
fieldName.Equals("allowedPrivateHosts", StringComparison.Ordinal) &&
ManagedConfiguration.TryGet(x => x.Tools, x => x.ReadWebPageAllowedPrivateHosts, out var readWebPageMeta) &&
readWebPageMeta.IsLocked;
}
private bool IsFieldDisabled(string fieldName) =>
this.toolDefinition is not null && this.ToolSettingsService.IsFieldLocked(this.toolDefinition, fieldName);
private string GetFieldPlaceholder(string fieldName, ToolSettingsFieldDefinition fieldDefinition) =>
string.IsNullOrWhiteSpace(this.GetValue(fieldName)) ? this.GetFieldDefaultValue(fieldName, fieldDefinition) : string.Empty;

View File

@ -356,6 +356,15 @@ CONFIG["SETTINGS"] = {}
-- Examples are: "CmdOrControl+Shift+D", "Alt+F9", "F8"
-- CONFIG["SETTINGS"]["DataApp.ShortcutVoiceRecording"] = "CmdOrControl+1"
-- Configure whether tools are available at all. The default is true.
-- When tools are disabled globally, tool selection is hidden in chats and assistants,
-- but the global tool settings remain available to administrators.
-- CONFIG["SETTINGS"]["DataTools.EnableTools"] = false
-- Disable individual tools by their stable tool ID. The default is an empty set.
-- Unknown IDs are safely ignored and can be deployed before a future tool is installed.
-- CONFIG["SETTINGS"]["DataTools.DisabledToolIds"] = { "web_search" }
-- Configure the minimum provider confidence level required for individual tools.
-- Tool IDs include: web_search, read_web_page
-- Allowed values are: NONE, UNTRUSTED, VERY_LOW, LOW, MODERATE, MEDIUM, HIGH
@ -365,21 +374,47 @@ CONFIG["SETTINGS"] = {}
-- ["read_web_page"] = "MEDIUM"
-- }
-- Configure the SearXNG instance URL used by the Web Search tool.
-- You can enter either the instance root URL or the /search endpoint.
-- Configure the Web Search tool. All values are strings.
-- WebSearchBaseUrl: required SearXNG HTTP(S) root URL or /search endpoint; no default.
-- CONFIG["SETTINGS"]["DataTools.WebSearchBaseUrl"] = "https://searxng.website/"
-- CONFIG["SETTINGS"]["DataTools.WebSearchBaseUrl.AllowUserOverride"] = false
-- WebSearchDefaultLanguage: optional language code; default is empty.
-- CONFIG["SETTINGS"]["DataTools.WebSearchDefaultLanguage"] = "en"
-- WebSearchDefaultSafeSearch: optional SearXNG safe-search level "0", "1", or "2"; default is empty.
-- CONFIG["SETTINGS"]["DataTools.WebSearchDefaultSafeSearch"] = "1"
-- WebSearchDefaultCategories: optional comma-separated categories; default is empty.
-- WebSearchDefaultEngines: optional comma-separated engines; default is empty.
-- Categories and engines cannot both be configured.
-- CONFIG["SETTINGS"]["DataTools.WebSearchDefaultCategories"] = "general, science"
-- CONFIG["SETTINGS"]["DataTools.WebSearchDefaultEngines"] = ""
-- WebSearchMaxResults: positive integer; default 5, effective maximum 20.
-- CONFIG["SETTINGS"]["DataTools.WebSearchMaxResults"] = "5"
-- WebSearchTimeoutSeconds: positive integer; default 20, effective maximum 60.
-- CONFIG["SETTINGS"]["DataTools.WebSearchTimeoutSeconds"] = "20"
-- WebSearchMaxTotalContentCharacters: positive integer; default and maximum 100000.
-- CONFIG["SETTINGS"]["DataTools.WebSearchMaxTotalContentCharacters"] = "100000"
-- WebSearchMinContentCharactersPerResult: positive integer; default and maximum 3000.
-- The total content budget must be at least this value multiplied by the hard limit of 20 results.
-- CONFIG["SETTINGS"]["DataTools.WebSearchMinContentCharactersPerResult"] = "3000"
-- WebSearchPageTimeoutSeconds: positive integer; default and maximum 30.
-- CONFIG["SETTINGS"]["DataTools.WebSearchPageTimeoutSeconds"] = "30"
-- WebSearchRetrievalTimeoutSeconds: positive integer; default and maximum 90.
-- CONFIG["SETTINGS"]["DataTools.WebSearchRetrievalTimeoutSeconds"] = "90"
-- Configure private or VPN hosts that the Read Web Page tool may access.
-- Public web pages do not need to be listed here.
-- Private hosts listed here still require a provider with HIGH confidence before any page content is sent to the model.
-- For hosts on this allowlist, AI Studio also tries the current user's operating-system sign-in
-- automatically when the server requests integrated authentication (for example Kerberos or NTLM).
-- This does not reuse Firefox cookies or an existing browser session.
-- Separate host patterns with commas. Wildcards only match subdomains, so add the root domain separately if needed.
-- Examples:
-- Configure the Read Web Page tool. All values are strings.
-- ReadWebPageTimeoutSeconds: positive integer; default 30, effective maximum 60.
-- CONFIG["SETTINGS"]["DataTools.ReadWebPageTimeoutSeconds"] = "30"
-- ReadWebPageMaxContentCharacters: positive integer; default 30000, effective maximum 50000.
-- CONFIG["SETTINGS"]["DataTools.ReadWebPageMaxContentCharacters"] = "30000"
-- ReadWebPageAllowedPrivateHosts: optional comma-separated private or VPN host patterns; default is empty.
-- Public pages do not need to be listed. Wildcards only match subdomains, so add the root domain separately.
-- Allowed private hosts require a provider with HIGH confidence. AI Studio tries the current user's
-- operating-system sign-in when integrated authentication is requested, but does not reuse browser cookies.
-- CONFIG["SETTINGS"]["DataTools.ReadWebPageAllowedPrivateHosts"] = "dlr.de, *.dlr.de"
-- CONFIG["SETTINGS"]["DataTools.ReadWebPageAllowedPrivateHosts.AllowUserOverride"] = false
-- The 14 Web Search and Read Web Page settings are locked by default. Add
-- ".AllowUserOverride" = true to any of them to provide an editable organization default instead.
-- A saved local value then takes precedence.
-- CONFIG["SETTINGS"]["DataTools.WebSearchBaseUrl.AllowUserOverride"] = true
-- Configure the HTTP timeout for external requests, in seconds.
-- The default is 3600 (1 hour).

View File

@ -107,7 +107,8 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
//
var providerConfidence = this.Provider.GetConfidence(settingsManager).Level;
var minimumWebSearchConfidence = settingsManager.GetMinimumProviderConfidenceForTool(ToolSelectionRules.WEB_SEARCH_TOOL_ID);
var isWebSearchAllowed = ToolSelectionRules.IsProviderConfidenceAllowed(providerConfidence, minimumWebSearchConfidence);
var isWebSearchAllowed = settingsManager.IsToolActive(ToolSelectionRules.WEB_SEARCH_TOOL_ID) &&
ToolSelectionRules.IsProviderConfidenceAllowed(providerConfidence, minimumWebSearchConfidence);
IList<object> providerTools = modelCapabilities.Contains(Capability.WEB_SEARCH) && isWebSearchAllowed
? [ ProviderTools.WEB_SEARCH ]
: [];

View File

@ -16,6 +16,16 @@ public sealed class DataTools(Expression<Func<Data, DataTools>>? configSelection
public HashSet<string> VisibleToolSelectionComponents { get; set; } = [];
public bool EnableTools { get; set; } = ManagedConfiguration.Register(
configSelection,
x => x.EnableTools,
true);
public HashSet<string> DisabledToolIds { get; set; } = ManagedConfiguration.Register(
configSelection,
x => x.DisabledToolIds,
[]);
public Dictionary<string, string> MinimumProviderConfidenceByToolId { get; set; } = ManagedConfiguration.Register<DataTools, Dictionary<string, string>>(
configSelection,
x => x.MinimumProviderConfidenceByToolId,
@ -26,6 +36,66 @@ public sealed class DataTools(Expression<Func<Data, DataTools>>? configSelection
x => x.WebSearchBaseUrl,
string.Empty);
public string WebSearchDefaultLanguage { get; set; } = ManagedConfiguration.Register<DataTools>(
configSelection,
x => x.WebSearchDefaultLanguage,
string.Empty);
public string WebSearchDefaultSafeSearch { get; set; } = ManagedConfiguration.Register<DataTools>(
configSelection,
x => x.WebSearchDefaultSafeSearch,
string.Empty);
public string WebSearchDefaultCategories { get; set; } = ManagedConfiguration.Register<DataTools>(
configSelection,
x => x.WebSearchDefaultCategories,
string.Empty);
public string WebSearchDefaultEngines { get; set; } = ManagedConfiguration.Register<DataTools>(
configSelection,
x => x.WebSearchDefaultEngines,
string.Empty);
public string WebSearchMaxResults { get; set; } = ManagedConfiguration.Register<DataTools>(
configSelection,
x => x.WebSearchMaxResults,
string.Empty);
public string WebSearchTimeoutSeconds { get; set; } = ManagedConfiguration.Register<DataTools>(
configSelection,
x => x.WebSearchTimeoutSeconds,
string.Empty);
public string WebSearchMaxTotalContentCharacters { get; set; } = ManagedConfiguration.Register<DataTools>(
configSelection,
x => x.WebSearchMaxTotalContentCharacters,
string.Empty);
public string WebSearchMinContentCharactersPerResult { get; set; } = ManagedConfiguration.Register<DataTools>(
configSelection,
x => x.WebSearchMinContentCharactersPerResult,
string.Empty);
public string WebSearchPageTimeoutSeconds { get; set; } = ManagedConfiguration.Register<DataTools>(
configSelection,
x => x.WebSearchPageTimeoutSeconds,
string.Empty);
public string WebSearchRetrievalTimeoutSeconds { get; set; } = ManagedConfiguration.Register<DataTools>(
configSelection,
x => x.WebSearchRetrievalTimeoutSeconds,
string.Empty);
public string ReadWebPageTimeoutSeconds { get; set; } = ManagedConfiguration.Register<DataTools>(
configSelection,
x => x.ReadWebPageTimeoutSeconds,
string.Empty);
public string ReadWebPageMaxContentCharacters { get; set; } = ManagedConfiguration.Register<DataTools>(
configSelection,
x => x.ReadWebPageMaxContentCharacters,
string.Empty);
public string ReadWebPageAllowedPrivateHosts { get; set; } = ManagedConfiguration.Register<DataTools>(
configSelection,
x => x.ReadWebPageAllowedPrivateHosts,

View File

@ -594,6 +594,9 @@ public sealed class SettingsManager
public HashSet<string> FilterToolIdsForProvider(AIStudio.Settings.Provider provider, IEnumerable<string> selectedToolIds)
{
if (!this.AreToolsEnabled())
return [];
var toolCallingAvailability = provider.GetToolCallingAvailability();
if (!toolCallingAvailability.IsAvailable)
return [];
@ -610,6 +613,12 @@ public sealed class SettingsManager
foreach (var toolId in filtered.ToList())
{
if (!this.IsToolActive(toolId))
{
filtered.Remove(toolId);
continue;
}
var minimumToolConfidence = this.GetMinimumProviderConfidenceForTool(toolId);
if (!ToolSelectionRules.IsProviderConfidenceAllowed(providerConfidence, minimumToolConfidence))
filtered.Remove(toolId);
@ -618,6 +627,12 @@ public sealed class SettingsManager
return filtered;
}
public bool AreToolsEnabled() => this.ConfigurationData.Tools.EnableTools;
public bool IsToolActive(string toolId) =>
this.AreToolsEnabled() &&
!this.ConfigurationData.Tools.DisabledToolIds.Contains(toolId);
public bool IsToolSelectionVisible(AIStudio.Tools.Components component) => component switch
{
AIStudio.Tools.Components.CHAT => true,

View File

@ -199,13 +199,29 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
// Config: global voice recording shortcut
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ShortcutVoiceRecording, this.Id, settingsTable, dryRun);
// Config: global tool availability
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.EnableTools, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.DisabledToolIds, this.Id, settingsTable, dryRun);
// Config: minimum provider confidence per tool
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.MinimumProviderConfidenceByToolId, this.Id, settingsTable, dryRun);
// Config: SearXNG base URL for the web search tool
// Config: web search tool settings
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchBaseUrl, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchDefaultLanguage, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchDefaultSafeSearch, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchDefaultCategories, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchDefaultEngines, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchMaxResults, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchTimeoutSeconds, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchMaxTotalContentCharacters, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchMinContentCharactersPerResult, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchPageTimeoutSeconds, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.WebSearchRetrievalTimeoutSeconds, this.Id, settingsTable, dryRun);
// Config: private hosts allowed for the read web page tool
// Config: read web page tool settings
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.ReadWebPageTimeoutSeconds, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.ReadWebPageMaxContentCharacters, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Tools, x => x.ReadWebPageAllowedPrivateHosts, this.Id, settingsTable, dryRun);
// Config: timeout for external HTTP requests

View File

@ -292,15 +292,58 @@ public static partial class PluginFactory
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ShortcutVoiceRecording, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
// Check for global tool availability:
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.EnableTools, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.DisabledToolIds, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
// Check for minimum provider confidence per tool:
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.MinimumProviderConfidenceByToolId, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
// Check for the SearXNG base URL for the web search tool:
// Check for web search tool settings:
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchBaseUrl, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
// Check for private hosts allowed for the read web page tool:
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchDefaultLanguage, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchDefaultSafeSearch, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchDefaultCategories, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchDefaultEngines, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchMaxResults, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchTimeoutSeconds, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchMaxTotalContentCharacters, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchMinContentCharactersPerResult, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchPageTimeoutSeconds, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.WebSearchRetrievalTimeoutSeconds, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
// Check for read web page tool settings:
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.ReadWebPageTimeoutSeconds, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.ReadWebPageMaxContentCharacters, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Tools, x => x.ReadWebPageAllowedPrivateHosts, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;

View File

@ -113,6 +113,16 @@ public sealed class SearXNGWebSearchTool : IToolImplementation
});
}
var defaultSafeSearch = settingsValues.GetValueOrDefault("defaultSafeSearch");
if (!string.IsNullOrWhiteSpace(defaultSafeSearch) && defaultSafeSearch is not ("0" or "1" or "2"))
{
return Task.FromResult<ToolConfigurationState?>(new ToolConfigurationState
{
IsConfigured = false,
Message = TB("The default safe search setting must be 0, 1, or 2."),
});
}
if (!ToolSettingsValueParser.TryReadOptionalPositiveInt(settingsValues, "maxResults", positiveIntegerErrorFormat, out _, out var maxResultsError))
{
return Task.FromResult<ToolConfigurationState?>(new ToolConfigurationState

View File

@ -98,6 +98,8 @@ public sealed class ToolCatalogItem
public required ToolConfigurationState ConfigurationState { get; init; }
public bool IsActive { get; init; }
public ConfidenceLevel MinimumProviderConfidence { get; init; } = ConfidenceLevel.NONE;
}

View File

@ -208,6 +208,7 @@ public sealed class ToolRegistry
Definition = definition,
Implementation = implementation,
ConfigurationState = await this.toolSettingsService.GetConfigurationStateAsync(definition, implementation),
IsActive = this.settingsManager.IsToolActive(definition.Id),
MinimumProviderConfidence = this.settingsManager.GetMinimumProviderConfidenceForTool(definition.Id),
});
}
@ -223,6 +224,12 @@ public sealed class ToolRegistry
ConfidenceLevel providerConfidence,
bool isToolSelectionVisible)
{
if (!this.settingsManager.AreToolsEnabled())
{
this.logger.LogInformation("Tool calling is skipped because tools are disabled by managed configuration.");
return [];
}
if (!isToolSelectionVisible)
{
this.logger.LogInformation("Tool calling is skipped for component '{Component}' because tool selection is not visible.", component);
@ -250,6 +257,12 @@ public sealed class ToolRegistry
var result = new List<(ToolDefinition, IToolImplementation)>(definitions.Count);
foreach (var definition in definitions)
{
if (!this.settingsManager.IsToolActive(definition.Id))
{
this.logger.LogInformation("Skipping tool '{ToolId}' because it is disabled by managed configuration.", definition.Id);
continue;
}
if (!this.implementationsByKey.TryGetValue(definition.ImplementationKey, out var implementation))
{
this.logger.LogInformation("Skipping tool '{ToolId}' because no implementation is registered.", definition.Id);

View File

@ -1,12 +1,30 @@
using System.Linq.Expressions;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.Services;
namespace AIStudio.Tools.ToolCallingSystem;
public sealed class ToolSettingsService(SettingsManager settingsManager, RustService rustService)
{
private const string WEB_SEARCH_BASE_URL_FIELD = "baseUrl";
private const string READ_WEB_PAGE_ALLOWED_PRIVATE_HOSTS_FIELD = "allowedPrivateHosts";
private static readonly Dictionary<(string ToolId, string FieldName), ManagedToolSetting> MANAGED_SETTINGS = new()
{
[(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "baseUrl")] = CreateManagedToolSetting(x => x.WebSearchBaseUrl, (tools, value) => tools.WebSearchBaseUrl = value),
[(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "defaultLanguage")] = CreateManagedToolSetting(x => x.WebSearchDefaultLanguage),
[(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "defaultSafeSearch")] = CreateManagedToolSetting(x => x.WebSearchDefaultSafeSearch),
[(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "defaultCategories")] = CreateManagedToolSetting(x => x.WebSearchDefaultCategories),
[(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "defaultEngines")] = CreateManagedToolSetting(x => x.WebSearchDefaultEngines),
[(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "maxResults")] = CreateManagedToolSetting(x => x.WebSearchMaxResults),
[(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "timeoutSeconds")] = CreateManagedToolSetting(x => x.WebSearchTimeoutSeconds),
[(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "maxTotalContentCharacters")] = CreateManagedToolSetting(x => x.WebSearchMaxTotalContentCharacters),
[(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "minContentCharactersPerResult")] = CreateManagedToolSetting(x => x.WebSearchMinContentCharactersPerResult),
[(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "pageTimeoutSeconds")] = CreateManagedToolSetting(x => x.WebSearchPageTimeoutSeconds),
[(ToolSelectionRules.WEB_SEARCH_TOOL_ID, "retrievalTimeoutSeconds")] = CreateManagedToolSetting(x => x.WebSearchRetrievalTimeoutSeconds),
[(ToolSelectionRules.READ_WEB_PAGE_TOOL_ID, "timeoutSeconds")] = CreateManagedToolSetting(x => x.ReadWebPageTimeoutSeconds),
[(ToolSelectionRules.READ_WEB_PAGE_TOOL_ID, "maxContentCharacters")] = CreateManagedToolSetting(x => x.ReadWebPageMaxContentCharacters),
[(ToolSelectionRules.READ_WEB_PAGE_TOOL_ID, "allowedPrivateHosts")] = CreateManagedToolSetting(x => x.ReadWebPageAllowedPrivateHosts, (tools, value) => tools.ReadWebPageAllowedPrivateHosts = value),
};
public async Task<Dictionary<string, string>> GetSettingsAsync(ToolDefinition definition)
{
@ -16,15 +34,16 @@ public sealed class ToolSettingsService(SettingsManager settingsManager, RustSer
{
var fieldName = property.Key;
var fieldDefinition = property.Value;
if (IsWebSearchBaseUrlField(definition, fieldName))
if (TryGetManagedSetting(definition, fieldName, out var managedSetting))
{
values[fieldName] = settingsManager.ConfigurationData.Tools.WebSearchBaseUrl;
continue;
}
var meta = managedSetting.GetMeta();
if (meta?.IsLocked is true || managedSetting.SetLegacyLocalValue is not null)
values[fieldName] = managedSetting.GetValue(settingsManager.ConfigurationData.Tools);
else if (storedValues?.TryGetValue(fieldName, out var managedStoredValue) is true)
values[fieldName] = managedStoredValue;
else if (meta?.ManagedMode is ManagedConfigurationMode.EDITABLE_DEFAULT)
values[fieldName] = managedSetting.GetValue(settingsManager.ConfigurationData.Tools);
if (IsReadWebPageAllowedPrivateHostsField(definition, fieldName))
{
values[fieldName] = settingsManager.ConfigurationData.Tools.ReadWebPageAllowedPrivateHosts;
continue;
}
@ -103,18 +122,15 @@ public sealed class ToolSettingsService(SettingsManager settingsManager, RustSer
values.TryGetValue(fieldName, out var value);
value ??= string.Empty;
if (IsWebSearchBaseUrlField(definition, fieldName))
if (TryGetManagedSetting(definition, fieldName, out var managedSetting))
{
if (!IsWebSearchBaseUrlLocked())
settingsManager.ConfigurationData.Tools.WebSearchBaseUrl = value;
if (managedSetting.GetMeta()?.IsLocked is true)
continue;
continue;
}
if (IsReadWebPageAllowedPrivateHostsField(definition, fieldName))
{
if (!IsReadWebPageAllowedPrivateHostsLocked())
settingsManager.ConfigurationData.Tools.ReadWebPageAllowedPrivateHosts = value;
if (managedSetting.SetLegacyLocalValue is not null)
managedSetting.SetLegacyLocalValue(settingsManager.ConfigurationData.Tools, value);
else
storedValues[fieldName] = value;
continue;
}
@ -137,17 +153,26 @@ public sealed class ToolSettingsService(SettingsManager settingsManager, RustSer
await MessageBus.INSTANCE.SendMessage<object?>(null, Event.CONFIGURATION_CHANGED, null);
}
private static bool IsWebSearchBaseUrlField(ToolDefinition definition, string fieldName) =>
definition.Id.Equals(ToolSelectionRules.WEB_SEARCH_TOOL_ID, StringComparison.Ordinal) &&
fieldName.Equals(WEB_SEARCH_BASE_URL_FIELD, StringComparison.Ordinal);
public bool IsFieldLocked(ToolDefinition definition, string fieldName) =>
TryGetManagedSetting(definition, fieldName, out var managedSetting) &&
managedSetting.GetMeta()?.IsLocked is true;
private static bool IsWebSearchBaseUrlLocked() =>
ManagedConfiguration.TryGet(x => x.Tools, x => x.WebSearchBaseUrl, out var meta) && meta.IsLocked;
private static bool TryGetManagedSetting(ToolDefinition definition, string fieldName, out ManagedToolSetting managedSetting) =>
MANAGED_SETTINGS.TryGetValue((definition.Id, fieldName), out managedSetting!);
private static bool IsReadWebPageAllowedPrivateHostsField(ToolDefinition definition, string fieldName) =>
definition.Id.Equals(ToolSelectionRules.READ_WEB_PAGE_TOOL_ID, StringComparison.Ordinal) &&
fieldName.Equals(READ_WEB_PAGE_ALLOWED_PRIVATE_HOSTS_FIELD, StringComparison.Ordinal);
private static ManagedToolSetting CreateManagedToolSetting(
Expression<Func<DataTools, string>> propertyExpression,
Action<DataTools, string>? setLegacyLocalValue = null)
{
var getValue = propertyExpression.Compile();
return new ManagedToolSetting(
getValue,
() => ManagedConfiguration.TryGet(x => x.Tools, propertyExpression, out var meta) ? meta : null,
setLegacyLocalValue);
}
private static bool IsReadWebPageAllowedPrivateHostsLocked() =>
ManagedConfiguration.TryGet(x => x.Tools, x => x.ReadWebPageAllowedPrivateHosts, out var meta) && meta.IsLocked;
private sealed record ManagedToolSetting(
Func<DataTools, string> GetValue,
Func<ConfigMeta<DataTools, string>?> GetMeta,
Action<DataTools, string>? SetLegacyLocalValue);
}

View File

@ -1 +1,3 @@
# v26.7.4, build 249 (2026-07-xx xx:xx UTC)
- Added organization-wide management for tool availability and all Web Search and Read Web Page settings.

View File

@ -222,7 +222,7 @@ All values must be positive. The total budget must be large enough to reserve th
The two tools can be selected independently. Tool policy text tells the model not to call `read_web_page` for a URL already returned by `web_search`, because the search result already contains that page's retrieved content.
For settings that administrators should be able to manage centrally, add the setting to the appropriate `Settings/DataModel` class, register it with `ManagedConfiguration.Register(...)`, process it in `PluginConfiguration`, clean leftovers in `PluginFactory.Loading`, and document it in `Plugins/configuration/plugin.lua`.
Every non-secret tool field that administrators should be able to manage centrally must have an explicit enterprise mapping in `ToolSettingsService`. Add its backing setting to the appropriate `Settings/DataModel` class, register it with `ManagedConfiguration.Register(...)`, process it in `PluginConfiguration`, clean leftovers in `PluginFactory.Loading`, and document its allowed values, default, and limits in `Plugins/configuration/plugin.lua`. Locked enterprise values override the local field, while editable enterprise defaults apply only until a user saves a local value. Secret fields require the existing OS-keyring path and must not be routed through plain enterprise settings.
## Checklist
@ -230,6 +230,7 @@ For settings that administrators should be able to manage centrally, add the set
- Add the `IToolImplementation` class.
- Register the implementation in `Program.cs`.
- Validate settings and model arguments.
- Add the enterprise mapping for each administratively configurable non-secret setting.
- Protect secrets and sensitive trace arguments.
- Add provider-confidence checks when tool output may contain sensitive data.
- Update configuration plugin documentation when admins can manage the setting.