2026-07-05 13:20:29 +00:00
using System.Globalization ;
2025-06-01 19:14:21 +00:00
using AIStudio.Settings ;
2026-04-10 15:11:05 +00:00
using AIStudio.Settings.DataModel ;
2026-02-07 21:59:41 +00:00
using AIStudio.Tools.Services ;
2025-06-01 19:14:21 +00:00
using Lua ;
namespace AIStudio.Tools.PluginSystem ;
public sealed class PluginConfiguration ( bool isInternal , LuaState state , PluginType type ) : PluginBase ( isInternal , state , type )
{
private static string TB ( string fallbackEN ) = > I18N . I . T ( fallbackEN , typeof ( PluginConfiguration ) . Namespace , nameof ( PluginConfiguration ) ) ;
2026-06-10 19:01:27 +00:00
private static SettingsManager SettingsManagerAccess = > Program . SERVICE_PROVIDER . GetRequiredService < SettingsManager > ( ) ;
2026-02-07 21:59:41 +00:00
private static readonly ILogger LOG = Program . LOGGER_FACTORY . CreateLogger ( nameof ( PluginConfiguration ) ) ;
2025-08-26 08:59:56 +00:00
private List < PluginConfigurationObject > configObjects = [ ] ;
2026-04-10 15:11:05 +00:00
private List < DataMandatoryInfo > mandatoryInfos = [ ] ;
2026-06-20 18:28:22 +00:00
private List < DataIntroduction > introductions = [ ] ;
2025-08-18 18:40:52 +00:00
/// <summary>
/// The list of configuration objects. Configuration objects are, e.g., providers or chat templates.
/// </summary>
public IEnumerable < PluginConfigurationObject > ConfigObjects = > this . configObjects ;
2026-02-19 19:43:47 +00:00
2026-04-10 15:11:05 +00:00
/// <summary>
/// The list of mandatory infos provided by this configuration plugin.
2026-06-20 18:28:22 +00:00
/// Mandatory infos are live plugin content and are not persisted to ConfigurationData.
2026-04-10 15:11:05 +00:00
/// </summary>
public IReadOnlyList < DataMandatoryInfo > MandatoryInfos = > this . mandatoryInfos ;
2026-06-20 18:28:22 +00:00
/// <summary>
/// The list of introductions provided by this configuration plugin.
/// Introductions are live plugin content and are not persisted to ConfigurationData.
/// </summary>
public IReadOnlyList < DataIntroduction > Introductions = > this . introductions ;
2026-02-19 19:43:47 +00:00
/// <summary>
/// True/false when explicitly configured in the plugin, otherwise null.
/// </summary>
public bool? DeployedUsingConfigServer { get ; } = ReadDeployedUsingConfigServer ( state ) ;
2026-08-08 16:35:46 +00:00
/// <summary>
/// The priority of this configuration plugin. Defaults to zero when the plugin declares none.
/// </summary>
/// <remarks>
/// Configuration plugins with a higher priority are applied later and therefore win when two of
/// them manage the same setting or define the same configuration object. This lets an
/// organization deploy one base configuration for everybody and additional configurations which
/// refine it, e.g. per department.
/// </remarks>
public int Priority { get ; } = ReadPriority ( state ) ;
2026-08-09 17:01:58 +00:00
/// <summary>
/// How many settings this configuration plugin declares.
/// </summary>
/// <remarks>
/// This counts the entries of the Lua SETTINGS table, without the <c>.AllowUserOverride</c>
/// companions. We need it for the import preview: a dry run does not lock anything, so the
/// number of settings the plugin would take over cannot be read from the managed configuration
/// at that point.
/// </remarks>
public int DeclaredSettingsCount { get ; private set ; }
2025-08-18 18:40:52 +00:00
2025-08-09 17:29:43 +00:00
public async Task InitializeAsync ( bool dryRun )
2025-06-01 19:14:21 +00:00
{
2025-08-09 17:29:43 +00:00
if ( ! this . TryProcessConfiguration ( dryRun , out var issue ) )
2026-04-09 08:08:37 +00:00
this . PluginIssues . Add ( issue ) ;
2025-08-09 17:29:43 +00:00
if ( ! dryRun )
{
2026-02-07 21:59:41 +00:00
// Store any decrypted API keys from enterprise configuration in the OS keyring:
await StoreEnterpriseApiKeysAsync ( ) ;
2026-05-18 14:26:51 +00:00
await StoreEnterpriseSecretsAsync ( ) ;
2026-02-07 21:59:41 +00:00
2026-06-10 19:01:27 +00:00
await SettingsManagerAccess . StoreSettings ( ) ;
2025-08-09 17:29:43 +00:00
await MessageBus . INSTANCE . SendMessage < bool > ( null , Event . CONFIGURATION_CHANGED ) ;
}
2025-06-01 19:14:21 +00:00
}
2025-08-09 17:29:43 +00:00
2026-05-18 14:26:51 +00:00
/// <summary>
/// Stores any pending enterprise secrets in the OS keyring.
/// </summary>
private static async Task StoreEnterpriseSecretsAsync ( )
{
var pendingSecrets = PendingEnterpriseSecrets . GetAndClear ( ) ;
if ( pendingSecrets . Count = = 0 )
return ;
LOG . LogInformation ( $"Storing {pendingSecrets.Count} enterprise secret(s) in the OS keyring." ) ;
var rustService = Program . SERVICE_PROVIDER . GetRequiredService < RustService > ( ) ;
foreach ( var pendingSecret in pendingSecrets )
{
try
{
var secretId = new TemporarySecretId ( pendingSecret . SecretId , pendingSecret . SecretName ) ;
var result = await rustService . SetSecret ( secretId , pendingSecret . SecretData , pendingSecret . StoreType ) ;
if ( result . Success )
LOG . LogDebug ( $"Successfully stored enterprise secret for '{pendingSecret.SecretName}' in the OS keyring." ) ;
else
LOG . LogWarning ( $"Failed to store enterprise secret for '{pendingSecret.SecretName}': {result.Issue}" ) ;
}
catch ( Exception ex )
{
LOG . LogError ( ex , $"Exception while storing enterprise secret for '{pendingSecret.SecretName}'." ) ;
}
}
}
2026-02-07 21:59:41 +00:00
/// <summary>
/// Stores any pending enterprise API keys in the OS keyring.
/// </summary>
private static async Task StoreEnterpriseApiKeysAsync ( )
{
var pendingKeys = PendingEnterpriseApiKeys . GetAndClear ( ) ;
if ( pendingKeys . Count = = 0 )
return ;
LOG . LogInformation ( $"Storing {pendingKeys.Count} enterprise API key(s) in the OS keyring." ) ;
var rustService = Program . SERVICE_PROVIDER . GetRequiredService < RustService > ( ) ;
foreach ( var pendingKey in pendingKeys )
{
try
{
// Create a temporary secret ID object for storing the key:
var secretId = new TemporarySecretId ( pendingKey . SecretId , pendingKey . SecretName ) ;
var result = await rustService . SetAPIKey ( secretId , pendingKey . ApiKey , pendingKey . StoreType ) ;
if ( result . Success )
LOG . LogDebug ( $"Successfully stored enterprise API key for '{pendingKey.SecretName}' in the OS keyring." ) ;
else
LOG . LogWarning ( $"Failed to store enterprise API key for '{pendingKey.SecretName}': {result.Issue}" ) ;
}
catch ( Exception ex )
{
LOG . LogError ( ex , $"Exception while storing enterprise API key for '{pendingKey.SecretName}'." ) ;
}
}
}
/// <summary>
/// Temporary implementation of ISecretId for storing enterprise API keys.
/// </summary>
private sealed record TemporarySecretId ( string SecretId , string SecretName ) : ISecretId ;
2026-02-19 19:43:47 +00:00
private static bool? ReadDeployedUsingConfigServer ( LuaState state )
{
if ( state . Environment [ "DEPLOYED_USING_CONFIG_SERVER" ] . TryRead < bool > ( out var deployedUsingConfigServer ) )
return deployedUsingConfigServer ;
return null ;
}
2026-08-08 16:35:46 +00:00
private static int ReadPriority ( LuaState state )
{
if ( state . Environment [ "PRIORITY" ] . TryRead < int > ( out var priority ) )
return priority ;
return 0 ;
}
2026-08-09 17:01:58 +00:00
/// <summary>
/// Counts the settings a configuration plugin declares, ignoring the <c>.AllowUserOverride</c>
/// companion keys: those refine a setting instead of adding one.
/// </summary>
private static int CountDeclaredSettings ( LuaTable settingsTable )
{
const string USER_OVERRIDE_SUFFIX = ".AllowUserOverride" ;
var count = 0 ;
var previousKey = LuaValue . Nil ;
while ( settingsTable . TryGetNext ( previousKey , out var pair ) )
{
previousKey = pair . Key ;
if ( pair . Key . TryRead < string > ( out var settingName ) & & ! settingName . EndsWith ( USER_OVERRIDE_SUFFIX , StringComparison . Ordinal ) )
count + + ;
}
return count ;
}
2025-06-01 19:14:21 +00:00
/// <summary>
/// Tries to initialize the UI text content of the plugin.
/// </summary>
2025-08-18 18:40:52 +00:00
/// <param name="dryRun">When true, the method will not apply any changes but only check if the configuration can be read.</param>
2025-06-01 19:14:21 +00:00
/// <param name="message">The error message, when the UI text content could not be read.</param>
/// <returns>True, when the UI text content could be read successfully.</returns>
2025-08-09 17:29:43 +00:00
private bool TryProcessConfiguration ( bool dryRun , out string message )
2025-06-01 19:14:21 +00:00
{
2025-08-18 18:40:52 +00:00
this . configObjects . Clear ( ) ;
2026-04-10 15:11:05 +00:00
this . mandatoryInfos . Clear ( ) ;
2026-06-20 18:28:22 +00:00
this . introductions . Clear ( ) ;
2025-08-18 18:40:52 +00:00
2025-06-01 19:14:21 +00:00
// Ensure that the main CONFIG table exists and is a valid Lua table:
2026-04-09 08:08:37 +00:00
if ( ! this . State . Environment [ "CONFIG" ] . TryRead < LuaTable > ( out var mainTable ) )
2025-06-01 19:14:21 +00:00
{
message = TB ( "The CONFIG table does not exist or is not a valid table." ) ;
return false ;
}
2025-08-26 08:59:56 +00:00
// Check for the main SETTINGS table:
2025-06-01 19:14:21 +00:00
if ( ! mainTable . TryGetValue ( "SETTINGS" , out var settingsValue ) | | ! settingsValue . TryRead < LuaTable > ( out var settingsTable ) )
{
message = TB ( "The SETTINGS table does not exist or is not a valid table." ) ;
return false ;
}
2026-08-09 17:01:58 +00:00
this . DeclaredSettingsCount = CountDeclaredSettings ( settingsTable ) ;
2025-08-09 17:29:43 +00:00
2025-08-26 08:59:56 +00:00
// Config: check for updates, and if so, how often?
2025-08-26 18:06:13 +00:00
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . UpdateInterval , this . Id , settingsTable , dryRun ) ;
// Config: how should updates be installed?
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . UpdateInstallation , this . Id , settingsTable , dryRun ) ;
2026-03-21 17:05:06 +00:00
// Config: what should be the start page?
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . StartPage , this . Id , settingsTable , dryRun ) ;
2026-06-04 14:24:40 +00:00
2026-06-20 18:28:22 +00:00
// Config: show built-in introduction on the home page?
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . ShowIntroduction , this . Id , settingsTable , dryRun ) ;
2026-06-04 14:24:40 +00:00
// Config: show quick start guide on the home page?
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . ShowQuickStartGuide , this . Id , settingsTable , dryRun ) ;
2026-07-15 11:27:34 +00:00
// Config: show last changelog on the home page?
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . ShowLastChangelog , this . Id , settingsTable , dryRun ) ;
// Config: show vision panel on the home page?
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . ShowVision , this . Id , settingsTable , dryRun ) ;
2025-08-26 08:59:56 +00:00
// Config: allow the user to add providers?
2025-08-09 17:29:43 +00:00
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . AllowUserToAddProvider , this . Id , settingsTable , dryRun ) ;
2026-02-07 21:59:41 +00:00
2026-08-06 08:42:20 +00:00
// Config: allow the user to import plugin archives?
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . AllowUserToImportPlugins , this . Id , settingsTable , dryRun ) ;
2026-08-09 17:01:58 +00:00
// Config: allow the user to import configuration plugin archives?
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . AllowUserToImportConfigurationPlugins , this . Id , settingsTable , dryRun ) ;
2026-08-06 08:42:20 +00:00
// Config: allow the user to share or export plugins?
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . AllowUserToSharePlugins , this . Id , settingsTable , dryRun ) ;
2026-02-07 21:59:41 +00:00
// Config: show administration settings?
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . ShowAdminSettings , this . Id , settingsTable , dryRun ) ;
2025-06-01 19:14:21 +00:00
2025-10-19 09:51:28 +00:00
// Config: preview features visibility
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . PreviewVisibility , this . Id , settingsTable , dryRun ) ;
2026-02-20 11:40:38 +00:00
// Config: enabled preview features (plugin contribution; users can enable additional features)
ManagedConfiguration . TryProcessConfigurationWithPluginContribution ( x = > x . App , x = > x . EnabledPreviewFeatures , this . Id , settingsTable , dryRun ) ;
2025-10-19 09:51:28 +00:00
2026-01-12 19:43:45 +00:00
// Config: hide some assistants?
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . HiddenAssistants , this . Id , settingsTable , dryRun ) ;
2026-01-24 19:05:34 +00:00
// Config: global voice recording shortcut
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . ShortcutVoiceRecording , this . Id , settingsTable , dryRun ) ;
2026-05-21 14:48:34 +00:00
// Config: timeout for external HTTP requests
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . HttpClientTimeoutSeconds , this . Id , settingsTable , dryRun ) ;
2026-05-31 16:46:54 +00:00
// Config: custom root certificates for external HTTP requests
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . ExternalHttpCustomRootCertificatesEnabled , this . Id , settingsTable , dryRun ) ;
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . ExternalHttpCustomRootCertificateBundlePath , this . Id , settingsTable , dryRun ) ;
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . ExternalHttpCustomRootCertificateAllowedHosts , this . Id , settingsTable , dryRun ) ;
2026-06-21 09:52:02 +00:00
// Config: provider confidence settings
ManagedConfiguration . TryProcessConfiguration ( x = > x . Confidence , x = > x . EnforceGlobalMinimumConfidence , this . Id , settingsTable , dryRun ) ;
ManagedConfiguration . TryProcessConfiguration ( x = > x . Confidence , x = > x . GlobalMinimumConfidence , this . Id , settingsTable , dryRun ) ;
ManagedConfiguration . TryProcessConfiguration ( x = > x . Confidence , x = > x . ShowProviderConfidence , this . Id , settingsTable , dryRun ) ;
ManagedConfiguration . TryProcessConfiguration ( x = > x . Confidence , x = > x . ConfidenceScheme , this . Id , settingsTable , dryRun ) ;
ManagedConfiguration . TryProcessConfiguration ( x = > x . Confidence , x = > x . CustomConfidenceScheme , this . Id , settingsTable , dryRun ) ;
2026-06-21 13:16:37 +00:00
// Config: data source security settings
ManagedConfiguration . TryProcessConfiguration ( x = > x . DataSourceSecurity , x = > x . TrustedProviderIds , this . Id , settingsTable , dryRun ) ;
2026-07-05 10:59:03 +00:00
2026-07-05 12:46:31 +00:00
// Config: data source selection agent settings
ManagedConfiguration . TryProcessConfiguration ( x = > x . AgentDataSourceSelection , x = > x . PreselectAgentOptions , this . Id , settingsTable , dryRun ) ;
ManagedConfiguration . TryProcessConfiguration ( x = > x . AgentDataSourceSelection , x = > x . PreselectedAgentProvider , Guid . Empty , this . Id , settingsTable , dryRun ) ;
// Config: retrieval context validation agent settings
ManagedConfiguration . TryProcessConfiguration ( x = > x . AgentRetrievalContextValidation , x = > x . EnableRetrievalContextValidation , this . Id , settingsTable , dryRun ) ;
ManagedConfiguration . TryProcessConfiguration ( x = > x . AgentRetrievalContextValidation , x = > x . PreselectAgentOptions , this . Id , settingsTable , dryRun ) ;
ManagedConfiguration . TryProcessConfiguration ( x = > x . AgentRetrievalContextValidation , x = > x . PreselectedAgentProvider , Guid . Empty , this . Id , settingsTable , dryRun ) ;
ManagedConfiguration . TryProcessConfiguration ( x = > x . AgentRetrievalContextValidation , x = > x . NumParallelValidations , this . Id , settingsTable , dryRun ) ;
2026-07-05 10:59:03 +00:00
// Config: assistant plugin audit settings
ManagedConfiguration . TryProcessConfiguration ( x = > x . AssistantPluginAudit , x = > x . RequireAuditBeforeActivation , this . Id , settingsTable , dryRun ) ;
ManagedConfiguration . TryProcessConfiguration ( x = > x . AssistantPluginAudit , x = > x . PreselectedAgentProvider , Guid . Empty , this . Id , settingsTable , dryRun ) ;
ManagedConfiguration . TryProcessConfiguration ( x = > x . AssistantPluginAudit , x = > x . MinimumLevel , this . Id , settingsTable , dryRun ) ;
ManagedConfiguration . TryProcessConfiguration ( x = > x . AssistantPluginAudit , x = > x . BlockActivationBelowMinimum , this . Id , settingsTable , dryRun ) ;
ManagedConfiguration . TryProcessConfiguration ( x = > x . AssistantPluginAudit , x = > x . AutomaticallyAuditAssistants , this . Id , settingsTable , dryRun ) ;
2026-07-05 13:20:29 +00:00
// Config: enterprise-managed approvals for assistant plugins
this . TryProcessEnterpriseApprovedAssistantPlugins ( settingsTable , dryRun ) ;
2026-01-24 19:05:34 +00:00
2025-08-26 08:59:56 +00:00
// Handle configured LLM providers:
2025-09-03 20:41:28 +00:00
PluginConfigurationObject . TryParse ( PluginConfigurationObjectType . LLM_PROVIDER , x = > x . Providers , x = > x . NextProviderNum , mainTable , this . Id , ref this . configObjects , dryRun ) ;
2026-01-09 14:41:54 +00:00
2026-01-09 14:49:44 +00:00
// Handle configured transcription providers:
PluginConfigurationObject . TryParse ( PluginConfigurationObjectType . TRANSCRIPTION_PROVIDER , x = > x . TranscriptionProviders , x = > x . NextTranscriptionNum , mainTable , this . Id , ref this . configObjects , dryRun ) ;
2026-01-09 14:41:54 +00:00
// Handle configured embedding providers:
PluginConfigurationObject . TryParse ( PluginConfigurationObjectType . EMBEDDING_PROVIDER , x = > x . EmbeddingProviders , x = > x . NextEmbeddingNum , mainTable , this . Id , ref this . configObjects , dryRun ) ;
2025-08-26 08:59:56 +00:00
// Handle configured chat templates:
2026-05-22 13:46:03 +00:00
PluginConfigurationObject . TryParse ( PluginConfigurationObjectType . CHAT_TEMPLATE , x = > x . ChatTemplates , x = > x . NextChatTemplateNum , mainTable , this . Id , ref this . configObjects , dryRun , this . PluginPath ) ;
2026-05-18 14:26:51 +00:00
// Handle configured data sources:
PluginConfigurationObject . TryParseDataSources ( mainTable , this . Id , ref this . configObjects , dryRun ) ;
2025-08-18 18:40:52 +00:00
2025-11-14 11:04:01 +00:00
// Handle configured profiles:
PluginConfigurationObject . TryParse ( PluginConfigurationObjectType . PROFILE , x = > x . Profiles , x = > x . NextProfileNum , mainTable , this . Id , ref this . configObjects , dryRun ) ;
2026-02-01 13:50:19 +00:00
// Handle configured document analysis policies:
PluginConfigurationObject . TryParse ( PluginConfigurationObjectType . DOCUMENT_ANALYSIS_POLICY , x = > x . DocumentAnalysis . Policies , x = > x . NextDocumentAnalysisPolicyNum , mainTable , this . Id , ref this . configObjects , dryRun ) ;
2026-04-10 15:11:05 +00:00
// Handle configured mandatory infos:
this . TryReadMandatoryInfos ( mainTable ) ;
2026-06-20 18:28:22 +00:00
// Handle configured introductions:
this . TryReadIntroductions ( mainTable ) ;
2026-02-01 13:50:19 +00:00
2026-02-20 09:08:06 +00:00
// Config: preselected provider?
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . PreselectedProvider , Guid . Empty , this . Id , settingsTable , dryRun ) ;
2025-11-14 11:04:01 +00:00
// Config: preselected profile?
2025-12-04 14:37:13 +00:00
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . PreselectedProfile , Guid . Empty , this . Id , settingsTable , dryRun ) ;
2026-01-09 14:49:44 +00:00
2026-06-21 13:59:23 +00:00
// Config: preselected chat options?
ManagedConfiguration . TryProcessConfiguration ( x = > x . Chat , x = > x . PreselectOptions , this . Id , settingsTable , dryRun ) ;
ManagedConfiguration . TryProcessConfiguration ( x = > x . Chat , x = > x . PreselectedProvider , Guid . Empty , this . Id , settingsTable , dryRun ) ;
ManagedConfiguration . TryProcessConfiguration ( x = > x . Chat , x = > x . PreselectedProfile , this . Id , settingsTable , dryRun ) ;
ManagedConfiguration . TryProcessConfiguration ( x = > x . Chat , x = > x . PreselectedChatTemplate , this . Id , settingsTable , dryRun ) ;
2026-07-05 12:46:31 +00:00
ManagedConfiguration . TryProcessConfiguration ( x = > x . Chat , x = > x . PreselectedDataSourcesDisabled , this . Id , settingsTable , dryRun ) ;
ManagedConfiguration . TryProcessConfiguration ( x = > x . Chat , x = > x . PreselectedDataSourcesAutomaticSelection , this . Id , settingsTable , dryRun ) ;
ManagedConfiguration . TryProcessConfiguration ( x = > x . Chat , x = > x . PreselectedDataSourcesAutomaticValidation , this . Id , settingsTable , dryRun ) ;
ManagedConfiguration . TryProcessConfiguration ( x = > x . Chat , x = > x . PreselectedDataSourceIds , this . Id , settingsTable , dryRun ) ;
ManagedConfiguration . TryProcessConfiguration ( x = > x . Chat , x = > x . SendToChatDataSourceBehavior , this . Id , settingsTable , dryRun ) ;
2026-06-21 13:59:23 +00:00
2026-01-09 14:49:44 +00:00
// Config: transcription provider?
ManagedConfiguration . TryProcessConfiguration ( x = > x . App , x = > x . UseTranscriptionProvider , Guid . Empty , this . Id , settingsTable , dryRun ) ;
2025-08-26 08:59:56 +00:00
message = string . Empty ;
2025-08-18 18:40:52 +00:00
return true ;
}
2026-04-10 15:11:05 +00:00
2026-07-05 13:20:29 +00:00
private void TryProcessEnterpriseApprovedAssistantPlugins ( LuaTable settingsTable , bool dryRun )
{
if ( ! ManagedConfiguration . TryGet ( x = > x . AssistantPluginAudit , x = > x . EnterpriseApprovedPlugins , out ConfigMeta < DataAssistantPluginAudit , IList < DataAssistantPluginEnterpriseApproval > > configMeta ) )
return ;
var settingName = SettingsManager . ToSettingName < DataAssistantPluginAudit , IList < DataAssistantPluginEnterpriseApproval > > ( x = > x . EnterpriseApprovedPlugins ) ;
var successful = false ;
IList < DataAssistantPluginEnterpriseApproval > configuredApprovals = [ ] ;
if ( settingsTable . TryGetValue ( settingName , out var configuredLuaValue )
& & configuredLuaValue . Type is LuaValueType . Table
& & configuredLuaValue . TryRead < LuaTable > ( out var approvalsTable ) )
{
var approvals = new List < DataAssistantPluginEnterpriseApproval > ( approvalsTable . ArrayLength ) ;
for ( var index = 1 ; index < = approvalsTable . ArrayLength ; index + + )
{
var entryValue = approvalsTable [ index ] ;
if ( entryValue . TryRead < string > ( out var hashText ) )
{
var normalizedHash = NormalizeApprovalHash ( hashText ) ;
if ( ! string . IsNullOrWhiteSpace ( normalizedHash ) )
approvals . Add ( new ( ) { PluginHash = normalizedHash } ) ;
else
LOG . LogWarning ( "The enterprise assistant approval entry at index {Index} contains an empty hash (config plugin id: {ConfigPluginId})." , index , this . Id ) ;
continue ;
}
if ( ! entryValue . TryRead < LuaTable > ( out var entryTable ) )
{
LOG . LogWarning ( "The enterprise assistant approval entry at index {Index} is neither a string nor a table (config plugin id: {ConfigPluginId})." , index , this . Id ) ;
continue ;
}
if ( ! TryParseEnterpriseApprovedAssistantPlugin ( index , entryTable , this . Id , out var approval ) )
continue ;
approvals . Add ( approval ) ;
}
configuredApprovals = approvals ;
successful = true ;
}
if ( dryRun )
return ;
2026-08-09 17:01:58 +00:00
//
// Only a configuration which speaks for an organization may approve assistant plugins: one
// deployed by a configuration server, or one staged in the test directory. An approval marks
// a plugin as safe without any security audit, and the user interface states that the
// organization approved it. No local configuration plugin may make that claim: it would
// disable the security audit for arbitrary assistant plugins while telling the user that
// their organization vouched for them.
//
// We decide by the plugin path. The self-declared DEPLOYED_USING_CONFIG_SERVER field would
// not do, because any plugin can set it to true.
//
if ( ! PluginFactory . IsOrganizationConfigurationPath ( this . PluginPath ) )
{
if ( successful )
LOG . LogWarning ( "The configuration plugin '{ConfigPluginId}' at '{PluginPath}' declares enterprise approvals for assistant plugins, but your organization's IT did not deploy it. Ignoring these approvals: only configuration plugins from a configuration server or from the test directory may approve assistant plugins." , this . Id , this . PluginPath ) ;
return ;
}
if ( PluginFactory . IsEnterpriseTestConfigurationPath ( this . PluginPath ) )
LOG . LogWarning ( "The test configuration plugin '{ConfigPluginId}' at '{PluginPath}' approves assistant plugins. These approvals are valid for this session only: AI Studio empties the test directory on every start." , this . Id , this . PluginPath ) ;
2026-07-05 13:20:29 +00:00
switch ( successful )
{
case true :
2026-08-08 16:35:46 +00:00
//
// Approvals of several configuration plugins add up. An approval list is a pure
// allowlist over hashes: not listing a plugin already means "not approved", so
// replacing the list would only ever withdraw the approvals of another
// configuration without expressing anything new.
//
configMeta . SetPluginContribution ( configuredApprovals , this . Id ) ;
// Merge into the stored list right away, so the approvals of this plugin take
// effect immediately. PluginFactory.LoadAll recomputes the authoritative list once
// every configuration plugin has contributed:
var mergedApprovals = new List < DataAssistantPluginEnterpriseApproval > ( configMeta . GetValue ( ) ) ;
var knownHashes = mergedApprovals . Select ( approval = > approval . PluginHash ) . ToHashSet ( StringComparer . Ordinal ) ;
mergedApprovals . AddRange ( configuredApprovals . Where ( approval = > knownHashes . Add ( approval . PluginHash ) ) ) ;
configMeta . SetValue ( mergedApprovals ) ;
2026-07-05 13:20:29 +00:00
configMeta . LockConfiguration ( this . Id ) ;
break ;
case false when configMeta . IsLocked & & configMeta . LockedByConfigPluginId = = this . Id :
2026-08-08 16:35:46 +00:00
configMeta . RemovePluginContribution ( this . Id ) ;
2026-07-05 13:20:29 +00:00
configMeta . ResetLockedConfiguration ( ) ;
break ;
2026-08-08 16:35:46 +00:00
case false :
configMeta . RemovePluginContribution ( this . Id ) ;
break ;
2026-07-05 13:20:29 +00:00
}
}
2026-08-08 16:35:46 +00:00
/// <summary>
/// Recomputes the effective enterprise approvals from the contributions of all configuration plugins.
/// </summary>
/// <remarks>
/// Every configuration plugin merges its own approvals into the stored list while it starts, but
/// nothing there can withdraw the approvals of a plugin which was removed in the meantime. This
/// method rebuilds the list from the remaining contributions and is therefore called once all
/// configuration plugins have been started.
/// </remarks>
/// <returns>True when the effective approvals changed, otherwise false.</returns>
public static bool RefreshEnterpriseApprovedAssistantPlugins ( )
{
if ( ! ManagedConfiguration . TryGet ( x = > x . AssistantPluginAudit , x = > x . EnterpriseApprovedPlugins , out ConfigMeta < DataAssistantPluginAudit , IList < DataAssistantPluginEnterpriseApproval > > configMeta ) )
return false ;
var effectiveApprovals = new List < DataAssistantPluginEnterpriseApproval > ( ) ;
var effectiveHashes = new HashSet < string > ( StringComparer . Ordinal ) ;
foreach ( var approval in configMeta . PluginContributions . Values . SelectMany ( contribution = > contribution ) )
if ( effectiveHashes . Add ( approval . PluginHash ) )
effectiveApprovals . Add ( approval ) ;
// Compare by hash, so a different order alone does not rewrite the settings on every start:
var currentApprovals = configMeta . GetValue ( ) ;
if ( currentApprovals . Count = = effectiveApprovals . Count & & effectiveHashes . SetEquals ( currentApprovals . Select ( approval = > approval . PluginHash ) ) )
return false ;
LOG . LogInformation ( $"The enterprise approvals for assistant plugins changed from {currentApprovals.Count} to {effectiveApprovals.Count} entries, contributed by {configMeta.PluginContributions.Count} configuration plugin(s)." ) ;
configMeta . SetValue ( effectiveApprovals ) ;
return true ;
}
2026-07-05 13:20:29 +00:00
private static bool TryParseEnterpriseApprovedAssistantPlugin ( int index , LuaTable table , Guid configPluginId , out DataAssistantPluginEnterpriseApproval approval )
{
approval = new ( ) ;
if ( ! table . TryGetValue ( "PluginHash" , out var pluginHashValue ) | | ! pluginHashValue . TryRead < string > ( out var pluginHash ) )
{
LOG . LogWarning ( "The enterprise assistant approval entry at index {Index} is missing a valid PluginHash (config plugin id: {ConfigPluginId})." , index , configPluginId ) ;
return false ;
}
var normalizedHash = NormalizeApprovalHash ( pluginHash ) ;
if ( string . IsNullOrWhiteSpace ( normalizedHash ) )
{
LOG . LogWarning ( "The enterprise assistant approval entry at index {Index} contains an empty PluginHash (config plugin id: {ConfigPluginId})." , index , configPluginId ) ;
return false ;
}
var displayName = TryReadOptionalString ( table , "DisplayName" ) ;
var comment = TryReadOptionalString ( table , "Comment" ) ;
var approvedBy = TryReadOptionalString ( table , "ApprovedBy" ) ;
var approvedAtUtc = TryReadOptionalDateTimeOffset ( table , "ApprovedAtUtc" , index , configPluginId ) ;
approval = new ( )
{
PluginHash = normalizedHash ,
DisplayName = displayName ,
Comment = comment ,
ApprovedBy = approvedBy ,
ApprovedAtUtc = approvedAtUtc ,
} ;
return true ;
}
private static string TryReadOptionalString ( LuaTable table , string key )
{
return table . TryGetValue ( key , out var value ) & & value . TryRead < string > ( out var text )
? text
: string . Empty ;
}
private static DateTimeOffset ? TryReadOptionalDateTimeOffset ( LuaTable table , string key , int index , Guid configPluginId )
{
if ( ! table . TryGetValue ( key , out var value ) )
return null ;
if ( value . TryRead < string > ( out var text ) & & DateTimeOffset . TryParse ( text , CultureInfo . InvariantCulture , DateTimeStyles . AssumeUniversal , out var parsed ) )
return parsed . ToUniversalTime ( ) ;
LOG . LogWarning ( "The enterprise assistant approval entry at index {Index} contains an invalid {Key} value (config plugin id: {ConfigPluginId})." , index , key , configPluginId ) ;
return null ;
}
private static string NormalizeApprovalHash ( string hash ) = > string . IsNullOrWhiteSpace ( hash ) ? string . Empty : hash . Trim ( ) . ToUpperInvariant ( ) ;
2026-04-10 15:11:05 +00:00
private void TryReadMandatoryInfos ( LuaTable mainTable )
{
if ( ! mainTable . TryGetValue ( "MANDATORY_INFOS" , out var mandatoryInfosValue ) | | ! mandatoryInfosValue . TryRead < LuaTable > ( out var mandatoryInfosTable ) )
return ;
for ( var i = 1 ; i < = mandatoryInfosTable . ArrayLength ; i + + )
{
var luaMandatoryInfoValue = mandatoryInfosTable [ i ] ;
if ( ! luaMandatoryInfoValue . TryRead < LuaTable > ( out var luaMandatoryInfoTable ) )
{
LOG . LogWarning ( "The table 'MANDATORY_INFOS' entry at index {Index} is not a valid table (config plugin id: {ConfigPluginId})." , i , this . Id ) ;
continue ;
}
if ( DataMandatoryInfo . TryParseConfiguration ( i , luaMandatoryInfoTable , this . Id , out var mandatoryInfo ) )
this . mandatoryInfos . Add ( mandatoryInfo ) ;
else
LOG . LogWarning ( "The table 'MANDATORY_INFOS' entry at index {Index} does not contain a valid mandatory info (config plugin id: {ConfigPluginId})." , i , this . Id ) ;
}
}
2026-06-20 18:28:22 +00:00
private void TryReadIntroductions ( LuaTable mainTable )
{
if ( ! mainTable . TryGetValue ( "INTRODUCTIONS" , out var introductionsValue ) | | ! introductionsValue . TryRead < LuaTable > ( out var introductionsTable ) )
return ;
for ( var i = 1 ; i < = introductionsTable . ArrayLength ; i + + )
{
var luaIntroductionValue = introductionsTable [ i ] ;
if ( ! luaIntroductionValue . TryRead < LuaTable > ( out var luaIntroductionTable ) )
{
LOG . LogWarning ( "The table 'INTRODUCTIONS' entry at index {Index} is not a valid table (config plugin id: {ConfigPluginId})." , i , this . Id ) ;
continue ;
}
if ( DataIntroduction . TryParseConfiguration ( i , luaIntroductionTable , this . Id , out var introduction ) )
this . introductions . Add ( introduction ) ;
else
LOG . LogWarning ( "The table 'INTRODUCTIONS' entry at index {Index} does not contain a valid introduction (config plugin id: {ConfigPluginId})." , i , this . Id ) ;
}
}
2026-02-07 21:59:41 +00:00
}