Merge branch 'main' into chunk-data

This commit is contained in:
PaulKoudelka committed 2026-08-04 18:01:25 +02:00
commit 9932b5870b
484 files changed
+45549 -2467

No files matched your search

@@ -40,6 +40,8 @@ public class AssistantComponentFactory
return new AssistantWebContentReader { Props = props, Children = children };
case AssistantComponentType.FILE_CONTENT_READER:
return new AssistantFileContentReader { Props = props, Children = children };
case AssistantComponentType.FILE_ATTACHMENTS:
return new AssistantFileAttachment { Props = props, Children = children };
case AssistantComponentType.IMAGE:
return new AssistantImage { Props = props, Children = children };
case AssistantComponentType.COLOR_PICKER:
@@ -7,9 +7,12 @@ namespace AIStudio.Tools.PluginSystem.Assistants;
/// </summary>
public sealed class AssistantPluginAuditService(AssistantAuditAgent auditAgent)
{
public async Task<PluginAssistantAudit> RunAuditAsync(PluginAssistants plugin, CancellationToken token = default)
/// <summary>
/// Runs an assistant plugin audit, optionally falling back to the supplied provider when no audit provider is configured.
/// </summary>
public async Task<PluginAssistantAudit> RunAuditAsync(PluginAssistants plugin, CancellationToken token = default, Settings.Provider? fallbackProvider = null)
{
var result = await auditAgent.AuditAsync(plugin, token);
var result = await auditAgent.AuditAsync(plugin, token, fallbackProvider);
var provider = auditAgent.ProviderSettings;
var promptPreview = await plugin.BuildAuditPromptPreviewAsync(token);
@@ -0,0 +1,7 @@
namespace AIStudio.Tools.PluginSystem.Assistants;
public enum AssistantPluginLaunchBehavior
{
NONE,
OPEN_WORKSPACE_CHAT_BY_NAME,
}
@@ -15,6 +15,7 @@ public enum AssistantComponentType
LIST,
WEB_CONTENT_READER,
FILE_CONTENT_READER,
FILE_ATTACHMENTS,
IMAGE,
COLOR_PICKER,
DATE_PICKER,
@@ -19,6 +19,7 @@ public static class AssistantComponentTypeExtensions
AssistantComponentType.LIST => TB("List"),
AssistantComponentType.WEB_CONTENT_READER => TB("Web Content Reader"),
AssistantComponentType.FILE_CONTENT_READER => TB("File Content Reader"),
AssistantComponentType.FILE_ATTACHMENTS => TB("File Attachments"),
AssistantComponentType.IMAGE => TB("Image"),
AssistantComponentType.COLOR_PICKER => TB("Color Selection"),
AssistantComponentType.DATE_PICKER => TB("Date Selection"),
@@ -47,6 +48,7 @@ public static class AssistantComponentTypeExtensions
AssistantComponentType.LIST => MudBlazor.Icons.Material.Filled.List,
AssistantComponentType.WEB_CONTENT_READER => MudBlazor.Icons.Material.Filled.Public,
AssistantComponentType.FILE_CONTENT_READER => MudBlazor.Icons.Material.Filled.AttachFile,
AssistantComponentType.FILE_ATTACHMENTS => MudBlazor.Icons.Material.Filled.AttachFile,
AssistantComponentType.IMAGE => MudBlazor.Icons.Material.Filled.Image,
AssistantComponentType.COLOR_PICKER => MudBlazor.Icons.Material.Filled.Palette,
AssistantComponentType.DATE_PICKER => MudBlazor.Icons.Material.Filled.CalendarMonth,
@@ -61,4 +63,4 @@ public static class AssistantComponentTypeExtensions
AssistantComponentType.FORM => MudBlazor.Icons.Material.Filled.AccountTree,
_ => MudBlazor.Icons.Material.Filled.AccountTree,
};
}
}
@@ -0,0 +1,66 @@
using System.Text;
using AIStudio.Assistants.Dynamic;
namespace AIStudio.Tools.PluginSystem.Assistants.DataModel;
internal sealed class AssistantFileAttachment : StatefulAssistantComponentBase
{
public override AssistantComponentType Type => AssistantComponentType.FILE_ATTACHMENTS;
public override Dictionary<string, object> Props { get; set; } = new();
public override List<IAssistantComponent> Children { get; set; } = new();
public string Heading
{
get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Heading));
set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Heading), value);
}
public bool CatchAllDocuments
{
get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.CatchAllDocuments), true);
set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.CatchAllDocuments), value);
}
public bool UseSmallForm
{
get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.UseSmallForm));
set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.UseSmallForm), value);
}
public string Class
{
get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Class));
set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Class), value);
}
public string Style
{
get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Style));
set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Style), value);
}
#region Implementation of IStatefulAssistantComponent
public override void InitializeState(AssistantState state)
{
if (!state.FileAttachments.ContainsKey(this.Name))
state.FileAttachments[this.Name] = new FileAttachmentState();
}
public override string UserPromptFallback(AssistantState state)
{
state.FileAttachments.TryGetValue(this.Name, out var fileState);
if (fileState == null || fileState.DocumentPaths.Count == 0)
return this.BuildAuditPromptBlock(null);
var builder = new StringBuilder();
foreach (var attachment in fileState.DocumentPaths.OrderBy(static attachment => attachment.FilePath, StringComparer.Ordinal))
builder.AppendLine(attachment.FilePath);
return this.BuildAuditPromptBlock(builder.ToString());
}
#endregion
}
@@ -8,6 +8,12 @@ internal sealed class AssistantFileContentReader : StatefulAssistantComponentBas
public override Dictionary<string, object> Props { get; set; } = new();
public override List<IAssistantComponent> Children { get; set; } = new();
public bool ShowAttachedDocumentState
{
get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.ShowAttachedDocumentState), true);
set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.ShowAttachedDocumentState), value);
}
public string Class
{
get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Class));
@@ -1,4 +1,5 @@
using AIStudio.Assistants.Dynamic;
using AIStudio.Chat;
using Lua;
namespace AIStudio.Tools.PluginSystem.Assistants.DataModel;
@@ -11,6 +12,7 @@ public sealed class AssistantState
public readonly Dictionary<string, bool> Booleans = new(StringComparer.Ordinal);
public readonly Dictionary<string, WebContentState> WebContent = new(StringComparer.Ordinal);
public readonly Dictionary<string, FileContentState> FileContent = new(StringComparer.Ordinal);
public readonly Dictionary<string, FileAttachmentState> FileAttachments = new(StringComparer.Ordinal);
public readonly Dictionary<string, string> Colors = new(StringComparer.Ordinal);
public readonly Dictionary<string, string> Dates = new(StringComparer.Ordinal);
public readonly Dictionary<string, string> DateRanges = new(StringComparer.Ordinal);
@@ -24,12 +26,57 @@ public sealed class AssistantState
this.Booleans.Clear();
this.WebContent.Clear();
this.FileContent.Clear();
this.FileAttachments.Clear();
this.Colors.Clear();
this.Dates.Clear();
this.DateRanges.Clear();
this.Times.Clear();
}
/// <summary>
/// Copies all dynamic assistant state values from another state instance.
/// </summary>
/// <param name="other">The state instance to copy from.</param>
public void CopyFrom(AssistantState other)
{
this.Clear();
CopyDictionary(other.Text, this.Text);
CopyDictionary(other.SingleSelect, this.SingleSelect);
CopyDictionary(other.MultiSelect, this.MultiSelect);
CopyDictionary(other.Booleans, this.Booleans);
CopyDictionary(other.WebContent, this.WebContent);
CopyDictionary(other.FileContent, this.FileContent);
CopyDictionary(other.FileAttachments, this.FileAttachments);
CopyDictionary(other.Colors, this.Colors);
CopyDictionary(other.Dates, this.Dates);
CopyDictionary(other.DateRanges, this.DateRanges);
CopyDictionary(other.Times, this.Times);
}
/// <summary>
/// Creates a copy of the dynamic assistant state.
/// </summary>
/// <returns>A copied assistant state instance.</returns>
public AssistantState Clone()
{
var clone = new AssistantState();
clone.CopyFrom(this);
return clone;
}
/// <summary>
/// Copies all entries from one dictionary into another dictionary.
/// </summary>
/// <typeparam name="TKey">The dictionary key type.</typeparam>
/// <typeparam name="TValue">The dictionary value type.</typeparam>
/// <param name="source">The source dictionary.</param>
/// <param name="target">The target dictionary.</param>
private static void CopyDictionary<TKey, TValue>(Dictionary<TKey, TValue> source, Dictionary<TKey, TValue> target) where TKey : notnull
{
foreach (var (key, value) in source)
target[key] = value;
}
public bool TryApplyValue(string fieldName, LuaValue value, out string expectedType)
{
expectedType = string.Empty;
@@ -100,6 +147,22 @@ public sealed class AssistantState
return true;
}
if (this.FileAttachments.TryGetValue(fieldName, out var fileAttachmentState))
{
expectedType = "string[]";
if (value.TryRead<LuaTable>(out var fileAttachmentTable))
{
fileAttachmentState.DocumentPaths = ReadFileAttachmentValues(fileAttachmentTable);
return true;
}
if (!value.TryRead<string>(out var fileAttachmentValue))
return false;
fileAttachmentState.DocumentPaths = string.IsNullOrWhiteSpace(fileAttachmentValue) ? [] : [FileAttachment.FromPath(fileAttachmentValue)];
return true;
}
if (this.Colors.ContainsKey(fieldName))
{
expectedType = "string";
@@ -188,6 +251,11 @@ public sealed class AssistantState
return webContentValue.Content;
if (this.FileContent.TryGetValue(name, out var fileContentValue))
return fileContentValue.Content;
if (this.FileAttachments.TryGetValue(name, out var fileAttachmentsValue))
return AssistantLuaConversion.CreateLuaArray(
fileAttachmentsValue.DocumentPaths
.OrderBy(static attachment => attachment.FilePath, StringComparer.Ordinal)
.Select(static attachment => attachment.FilePath));
if (this.Colors.TryGetValue(name, out var colorValue))
return colorValue;
if (this.Dates.TryGetValue(name, out var dateValue))
@@ -256,4 +324,17 @@ public sealed class AssistantState
return parsedValues;
}
private static HashSet<FileAttachment> ReadFileAttachmentValues(LuaTable values)
{
var parsedValues = new HashSet<FileAttachment>();
foreach (var entry in values)
{
if (entry.Value.TryRead<string>(out var value) && !string.IsNullOrWhiteSpace(value))
parsedValues.Add(FileAttachment.FromPath(value));
}
return parsedValues;
}
}
@@ -82,7 +82,12 @@ public static class ComponentPropSpecs
),
[AssistantComponentType.FILE_CONTENT_READER] = new(
required: ["Name"],
optional: ["UserPrompt", "Class", "Style"],
optional: ["UserPrompt", "ShowAttachedDocumentState", "Class", "Style"],
nonWriteable: ["Name", "UserPrompt", "ShowAttachedDocumentState", "Class", "Style" ]
),
[AssistantComponentType.FILE_ATTACHMENTS] = new(
required: ["Name"],
optional: ["Heading", "UserPrompt", "CatchAllDocuments", "UseSmallForm", "Class", "Style"],
nonWriteable: ["Name", "UserPrompt", "Class", "Style" ]
),
[AssistantComponentType.IMAGE] = new(
@@ -8,6 +8,8 @@ public static class PluginAssistantSecurityResolver
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginAssistantSecurityResolver).Namespace, nameof(PluginAssistantSecurityResolver));
private static string NormalizeHash(string hash) => string.IsNullOrWhiteSpace(hash) ? string.Empty : hash.Trim().ToUpperInvariant();
private static string GetAvailabilityLabel(bool requiresAudit, bool hasAudit, bool hasHashMismatch, bool isBlocked, bool canOverride)
{
if (hasHashMismatch)
@@ -75,10 +77,55 @@ public static class PluginAssistantSecurityResolver
var auditSettings = settingsManager.ConfigurationData.AssistantPluginAudit;
var enforceAuditBeforeActivation = auditSettings.RequireAuditBeforeActivation;
var isEnforcementDisabled = !enforceAuditBeforeActivation;
var currentHash = plugin.ComputeAuditHash();
var currentHash = NormalizeHash(plugin.ComputeAuditHash());
var enterpriseApproval = auditSettings.EnterpriseApprovedPlugins
.FirstOrDefault(x => string.Equals(NormalizeHash(x.PluginHash), currentHash, StringComparison.Ordinal));
if (enterpriseApproval is not null)
{
var enterpriseHeadline = string.IsNullOrWhiteSpace(enterpriseApproval.DisplayName)
? TB("This assistant was approved by your organization.")
: string.Format(TB("This assistant was approved by your organization as '{0}'."), enterpriseApproval.DisplayName);
return new PluginAssistantSecurityState
{
Plugin = plugin,
Audit = null,
EnterpriseApproval = enterpriseApproval,
Settings = auditSettings,
Source = PluginAssistantSecurityStatusSource.ENTERPRISE_APPROVAL,
CurrentHash = currentHash,
HashMatches = true,
HasHashMismatch = false,
IsBelowMinimum = false,
MeetsMinimumLevel = true,
RequiresAudit = false,
IsBlocked = false,
CanOverride = false,
CanActivatePlugin = true,
CanStartAssistant = true,
AuditLabel = TB("Safe"),
AuditColor = AssistantAuditLevel.SAFE.GetColor(),
AuditIcon = AssistantAuditLevel.SAFE.GetIcon(),
AvailabilityLabel = GetAvailabilityLabel(requiresAudit: false, hasAudit: true, hasHashMismatch: false, isBlocked: false, canOverride: false),
AvailabilityColor = GetAvailabilityColor(requiresAudit: false, hasAudit: true, hasHashMismatch: false, isBlocked: false, canOverride: false),
AvailabilityIcon = GetAvailabilityIcon(requiresAudit: false, hasAudit: true, hasHashMismatch: false, isBlocked: false, canOverride: false),
StatusLabel = TB("Unlocked"),
SourceLabel = TB("Approved by your organization"),
SourceColor = Color.Success,
SourceIcon = MudBlazor.Icons.Material.Filled.Business,
BadgeIcon = MudBlazor.Icons.Material.Filled.Business,
Headline = enterpriseHeadline,
Description = TB("The current plugin hash matches an enterprise-managed approval. No manual security audit is required for activation or usage."),
StatusColor = Color.Success,
StatusIcon = MudBlazor.Icons.Material.Filled.VerifiedUser,
ActionLabel = TB("Open Security Details"),
};
}
var audit = settingsManager.ConfigurationData.AssistantPluginAudits.FirstOrDefault(x => x.PluginId == plugin.Id);
var hasAudit = audit is not null && audit.Level is not AssistantAuditLevel.UNKNOWN;
var hashMatches = hasAudit && string.Equals(audit!.PluginHash, currentHash, StringComparison.Ordinal);
var hashMatches = hasAudit && string.Equals(NormalizeHash(audit!.PluginHash), currentHash, StringComparison.Ordinal);
var hasHashMismatch = hasAudit && !hashMatches;
var isBelowMinimum = hashMatches && audit is not null && audit.Level < auditSettings.MinimumLevel;
var meetsMinimum = hashMatches && audit is not null && audit.Level >= auditSettings.MinimumLevel;
@@ -94,6 +141,7 @@ public static class PluginAssistantSecurityResolver
Plugin = plugin,
Audit = null,
Settings = auditSettings,
Source = PluginAssistantSecurityStatusSource.NONE,
CurrentHash = currentHash,
HashMatches = false,
HasHashMismatch = false,
@@ -111,6 +159,9 @@ public static class PluginAssistantSecurityResolver
AvailabilityColor = GetAvailabilityColor(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
AvailabilityIcon = GetAvailabilityIcon(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
StatusLabel = GetAvailabilityLabel(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
SourceLabel = TB("No Approval"),
SourceColor = Color.Default,
SourceIcon = MudBlazor.Icons.Material.Filled.HelpOutline,
BadgeIcon = GetSecurityBadgeIcon(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
Headline = requiresAudit ? TB("This assistant is currently locked.") : TB("This assistant currently has no stored audit."),
Description = requiresAudit
@@ -129,6 +180,7 @@ public static class PluginAssistantSecurityResolver
Plugin = plugin,
Audit = audit,
Settings = auditSettings,
Source = PluginAssistantSecurityStatusSource.NONE,
CurrentHash = currentHash,
HashMatches = false,
HasHashMismatch = true,
@@ -146,6 +198,9 @@ public static class PluginAssistantSecurityResolver
AvailabilityColor = GetAvailabilityColor(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
AvailabilityIcon = GetAvailabilityIcon(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
StatusLabel = GetAvailabilityLabel(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
SourceLabel = TB("No Approval"),
SourceColor = Color.Default,
SourceIcon = MudBlazor.Icons.Material.Filled.Warning,
BadgeIcon = GetSecurityBadgeIcon(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
Headline = requiresAudit ? TB("This assistant is locked until it is audited again.") : TB("This assistant changed after its last audit."),
Description = requiresAudit
@@ -167,6 +222,7 @@ public static class PluginAssistantSecurityResolver
Plugin = plugin,
Audit = audit,
Settings = auditSettings,
Source = PluginAssistantSecurityStatusSource.USER_AUDIT,
CurrentHash = currentHash,
HashMatches = true,
HasHashMismatch = false,
@@ -184,6 +240,9 @@ public static class PluginAssistantSecurityResolver
AvailabilityColor = GetAvailabilityColor(requiresAudit: false, hasAudit, hasHashMismatch: false, isBlockedByMinimum, canOverride),
AvailabilityIcon = GetAvailabilityIcon(requiresAudit: false, hasAudit, hasHashMismatch: false, isBlockedByMinimum, canOverride),
StatusLabel = GetAvailabilityLabel(requiresAudit: false, hasAudit, hasHashMismatch: false, isBlockedByMinimum, canOverride),
SourceLabel = TB("User Audit"),
SourceColor = auditLevel.GetColor(),
SourceIcon = MudBlazor.Icons.Material.Filled.Verified,
BadgeIcon = GetSecurityBadgeIcon(requiresAudit: false, hasAudit, hasHashMismatch: false, isBlockedByMinimum, canOverride),
Headline = isBlockedByMinimum
? TB("This assistant is currently locked.")
@@ -208,6 +267,7 @@ public static class PluginAssistantSecurityResolver
Plugin = plugin,
Audit = audit,
Settings = auditSettings,
Source = PluginAssistantSecurityStatusSource.USER_AUDIT,
CurrentHash = currentHash,
HashMatches = true,
HasHashMismatch = false,
@@ -225,6 +285,9 @@ public static class PluginAssistantSecurityResolver
AvailabilityColor = GetAvailabilityColor(requiresAudit: false, hasAudit, hasHashMismatch: false, isBlocked: false, canOverride: false),
AvailabilityIcon = GetAvailabilityIcon(requiresAudit: false, hasAudit, hasHashMismatch: false, isBlocked: false, canOverride: false),
StatusLabel = GetAvailabilityLabel(requiresAudit: false, hasAudit, hasHashMismatch: false, isBlocked: false, canOverride: false),
SourceLabel = TB("User Audit"),
SourceColor = auditLevelDefault.GetColor(),
SourceIcon = MudBlazor.Icons.Material.Filled.Verified,
BadgeIcon = GetSecurityBadgeIcon(requiresAudit: false, hasAudit, hasHashMismatch: false, isBlocked: false, canOverride: false),
Headline = TB("This assistant is currently unlocked."),
Description = string.Format(TB("The stored audit matches the current plugin code and meets your required minimum level '{0}'."), auditSettings.MinimumLevel.GetName()),
@@ -13,9 +13,12 @@ public sealed class PluginAssistantSecurityState
{
public PluginAssistants Plugin { get; init; } = null!;
public PluginAssistantAudit? Audit { get; init; }
public DataAssistantPluginEnterpriseApproval? EnterpriseApproval { get; init; }
public DataAssistantPluginAudit Settings { get; init; } = new();
public PluginAssistantSecurityStatusSource Source { get; init; } = PluginAssistantSecurityStatusSource.NONE;
public string CurrentHash { get; init; } = string.Empty;
public bool HasAudit => this.Audit is not null;
public bool IsEnterpriseApproved => this.Source is PluginAssistantSecurityStatusSource.ENTERPRISE_APPROVAL;
public bool HashMatches { get; init; }
public bool HasHashMismatch { get; init; }
public bool IsBelowMinimum { get; init; }
@@ -32,6 +35,9 @@ public sealed class PluginAssistantSecurityState
public Color AvailabilityColor { get; init; } = Color.Info;
public string AvailabilityIcon { get; init; } = MudBlazor.Icons.Material.Filled.Lock;
public string StatusLabel { get; init; } = string.Empty;
public string SourceLabel { get; init; } = string.Empty;
public Color SourceColor { get; init; } = Color.Info;
public string SourceIcon { get; init; } = MudBlazor.Icons.Material.Filled.Info;
public string Headline { get; init; } = string.Empty;
public string Description { get; init; } = string.Empty;
public Color StatusColor { get; init; } = Color.Info;
@@ -0,0 +1,8 @@
namespace AIStudio.Tools.PluginSystem.Assistants;
public enum PluginAssistantSecurityStatusSource
{
NONE,
USER_AUDIT,
ENTERPRISE_APPROVAL,
}
@@ -2,8 +2,8 @@ using System.Collections.Immutable;
using AIStudio.Tools.PluginSystem.Assistants.DataModel;
using AIStudio.Tools.PluginSystem.Assistants.DataModel.Layout;
using Lua;
using System.Security.Cryptography;
using System.Text;
using AssistantPluginHash = SharedTools.AssistantPluginHash;
namespace AIStudio.Tools.PluginSystem.Assistants;
@@ -36,6 +36,12 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
public bool AllowProfiles { get; private set; } = true;
public bool HasEmbeddedProfileSelection { get; private set; }
public bool HasCustomPromptBuilder => this.buildPromptFunction is not null;
public bool IsAssistantBuilderGenerated { get; private set; }
public bool HasDeploymentManagementMetadata { get; private set; }
public bool IsManagedByConfigServer { get; private set; }
public AssistantPluginLaunchBehavior LaunchBehavior { get; private set; }
public string LaunchWorkspaceName { get; private set; } = string.Empty;
public bool StartsChatDirectly => this.LaunchBehavior is AssistantPluginLaunchBehavior.OPEN_WORKSPACE_CHAT_BY_NAME;
public const int TEXT_AREA_MAX_VALUE = 524288;
private LuaFunction? buildPromptFunction;
@@ -60,9 +66,16 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
{
message = string.Empty;
this.HasEmbeddedProfileSelection = false;
this.IsAssistantBuilderGenerated = false;
this.HasDeploymentManagementMetadata = false;
this.IsManagedByConfigServer = false;
this.buildPromptFunction = null;
this.LaunchBehavior = AssistantPluginLaunchBehavior.NONE;
this.LaunchWorkspaceName = string.Empty;
this.RegisterLuaHelpers();
this.TryReadAssistantBuilderMetadata();
this.TryReadDeploymentMetadata();
// Ensure that the main ASSISTANT table exists and is a valid Lua table:
if (!this.State.Environment["ASSISTANT"].TryRead<LuaTable>(out var assistantTable))
@@ -123,6 +136,12 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
this.SubmitText = assistantSubmitText;
this.AllowProfiles = assistantAllowProfiles;
if (!this.TryReadLaunchConfiguration(assistantTable, out var launchConfigIssue))
{
message = launchConfigIssue;
return false;
}
// Ensure that the UI table exists nested in the ASSISTANT table and is a valid Lua table:
if (!assistantTable.TryGetValue("UI", out var uiVal) || !uiVal.TryRead<LuaTable>(out var uiTable))
{
@@ -140,6 +159,69 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
return true;
}
private void TryReadAssistantBuilderMetadata()
{
if (!this.State.Environment["AI_STUDIO_ASSISTANT_BUILDER"].TryRead<LuaTable>(out var builderTable))
return;
if (builderTable.TryGetValue("Generated", out var generatedValue) && generatedValue.TryRead<bool>(out var generated))
this.IsAssistantBuilderGenerated = generated;
}
private void TryReadDeploymentMetadata()
{
if (this.State.Environment["DEPLOYED_USING_CONFIG_SERVER"].TryRead<bool>(out var deployedUsingConfigServer))
{
this.HasDeploymentManagementMetadata = true;
this.IsManagedByConfigServer = deployedUsingConfigServer;
}
}
private bool TryReadLaunchConfiguration(LuaTable assistantTable, out string message)
{
message = string.Empty;
if (!assistantTable.TryGetValue("LaunchBehavior", out var launchBehaviorValue))
return true;
if (!launchBehaviorValue.TryRead<string>(out var launchBehaviorText) ||
!Enum.TryParse<AssistantPluginLaunchBehavior>(launchBehaviorText, true, out var launchBehavior))
{
message = TB("The ASSISTANT table contains an invalid LaunchBehavior value.");
return false;
}
this.LaunchBehavior = launchBehavior;
if (launchBehavior is AssistantPluginLaunchBehavior.NONE)
return true;
switch (launchBehavior)
{
case AssistantPluginLaunchBehavior.OPEN_WORKSPACE_CHAT_BY_NAME:
if (!assistantTable.TryGetValue("WorkspaceName", out var workspaceNameValue) ||
!workspaceNameValue.TryRead<string>(out var workspaceName))
{
message = TB("The ASSISTANT table contains the LaunchBehavior 'OPEN_WORKSPACE_CHAT_BY_NAME' but no valid WorkspaceName.");
return false;
}
workspaceName = workspaceName.Trim();
if (string.IsNullOrWhiteSpace(workspaceName))
{
message = TB("The ASSISTANT table contains an empty WorkspaceName for LaunchBehavior 'OPEN_WORKSPACE_CHAT_BY_NAME'.");
return false;
}
this.LaunchWorkspaceName = workspaceName;
return true;
default:
message = TB("The ASSISTANT table contains an unsupported LaunchBehavior value.");
return false;
}
}
public async Task<string?> TryBuildPromptAsync(LuaTable input, CancellationToken cancellationToken = default)
{
if (this.buildPromptFunction is null)
@@ -224,33 +306,7 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
/// sequence of relative path length, relative path, content length, and content
/// for each file in ordinal path order.
/// </summary>
public string ComputeAuditHash()
{
var luaFiles = this.ReadAllLuaFiles();
if (luaFiles.Count == 0)
return string.Empty;
using var stream = new MemoryStream();
using var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true);
foreach (var (relativePath, content) in luaFiles.OrderBy(pair => pair.Key, StringComparer.Ordinal))
{
var normalizedPath = relativePath.Replace('\\', '/');
var pathBytes = Encoding.UTF8.GetBytes(normalizedPath);
var contentBytes = Encoding.UTF8.GetBytes(content);
writer.Write(pathBytes.Length);
writer.Write(pathBytes);
writer.Write(contentBytes.Length);
writer.Write(contentBytes);
}
writer.Flush();
var bytes = SHA256.HashData(stream.ToArray());
return Convert.ToHexString(bytes);
}
public string ComputeAuditHash() => AssistantPluginHash.Compute(this.PluginPath);
private static string BuildSecureSystemPrompt(string pluginSystemPrompt)
{
@@ -1,3 +1,4 @@
using System.Globalization;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.Services;
@@ -170,7 +171,13 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
// Config: show quick start guide on the home page?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ShowQuickStartGuide, this.Id, settingsTable, dryRun);
// 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);
// Config: allow the user to add providers?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToAddProvider, this.Id, settingsTable, dryRun);
@@ -206,6 +213,26 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
// Config: data source security settings
ManagedConfiguration.TryProcessConfiguration(x => x.DataSourceSecurity, x => x.TrustedProviderIds, this.Id, settingsTable, dryRun);
// 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);
// 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);
// Config: enterprise-managed approvals for assistant plugins
this.TryProcessEnterpriseApprovedAssistantPlugins(settingsTable, dryRun);
// Handle configured LLM providers:
PluginConfigurationObject.TryParse(PluginConfigurationObjectType.LLM_PROVIDER, x => x.Providers, x => x.NextProviderNum, mainTable, this.Id, ref this.configObjects, dryRun);
@@ -245,6 +272,11 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
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);
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);
// Config: transcription provider?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.UseTranscriptionProvider, Guid.Empty, this.Id, settingsTable, dryRun);
@@ -253,6 +285,120 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
return true;
}
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;
switch (successful)
{
case true:
configMeta.SetValue(configuredApprovals);
configMeta.LockConfiguration(this.Id);
break;
case false when configMeta.IsLocked && configMeta.LockedByConfigPluginId == this.Id:
configMeta.ResetLockedConfiguration();
break;
}
}
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();
private void TryReadMandatoryInfos(LuaTable mainTable)
{
if (!mainTable.TryGetValue("MANDATORY_INFOS", out var mandatoryInfosValue) || !mandatoryInfosValue.TryRead<LuaTable>(out var mandatoryInfosTable))
@@ -65,7 +65,7 @@ public static partial class PluginFactory
await response.Content.CopyToAsync(tempFileStream, cancellationToken);
}
ZipFile.ExtractToDirectory(tempDownloadFile, stagedDirectory);
ExtractConfigPluginArchive(tempDownloadFile, stagedDirectory);
var configDirectory = Path.Join(CONFIGURATION_PLUGINS_ROOT, configPlugId.ToString());
if (Directory.Exists(configDirectory))
@@ -129,4 +129,70 @@ public static partial class PluginFactory
return wasSuccessful;
}
// Compatibility shim for Windows-created ZIPs with backslashes in entry names (dotnet/runtime#27620).
// See documentation/compatibility-shims/2026-07-enterprise-config-zip-backslashes.md.
private static void ExtractConfigPluginArchive(string sourceArchiveFileName, string destinationDirectory)
{
using var archive = ZipFile.OpenRead(sourceArchiveFileName);
Directory.CreateDirectory(destinationDirectory);
var destinationDirectoryFullPath = Path.GetFullPath(destinationDirectory);
if (!destinationDirectoryFullPath.EndsWith(Path.DirectorySeparatorChar))
destinationDirectoryFullPath += Path.DirectorySeparatorChar;
foreach (var entry in archive.Entries)
{
var normalizedEntryName = NormalizeConfigPluginZipEntryName(entry.FullName);
var destinationPath = GetConfigPluginZipEntryDestinationPath(destinationDirectoryFullPath, normalizedEntryName);
if (normalizedEntryName.EndsWith('/'))
{
if (entry.Length != 0)
throw new InvalidDataException($"The enterprise configuration plugin archive contains a directory entry with data: '{entry.FullName}'.");
Directory.CreateDirectory(destinationPath);
continue;
}
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);
entry.ExtractToFile(destinationPath);
}
if (!Directory.EnumerateFiles(destinationDirectory, "plugin.lua", SearchOption.AllDirectories).Any())
throw new InvalidDataException("The enterprise configuration plugin archive does not contain a plugin.lua file.");
}
private static string NormalizeConfigPluginZipEntryName(string entryName)
{
var normalizedEntryName = entryName.Replace('\\', '/');
if (string.IsNullOrWhiteSpace(normalizedEntryName))
throw new InvalidDataException("The enterprise configuration plugin archive contains an empty entry name.");
if (normalizedEntryName.Contains('\0'))
throw new InvalidDataException($"The enterprise configuration plugin archive contains an invalid entry name: '{entryName}'.");
if (normalizedEntryName.StartsWith('/'))
throw new InvalidDataException($"The enterprise configuration plugin archive contains a rooted entry name: '{entryName}'.");
if (normalizedEntryName is [_, ':', ..])
throw new InvalidDataException($"The enterprise configuration plugin archive contains a drive-qualified entry name: '{entryName}'.");
var pathSegments = normalizedEntryName.Split('/', StringSplitOptions.RemoveEmptyEntries);
if (pathSegments.Length == 0 || pathSegments.Any(segment => segment is "." or ".."))
throw new InvalidDataException($"The enterprise configuration plugin archive contains an unsafe entry name: '{entryName}'.");
return normalizedEntryName;
}
private static string GetConfigPluginZipEntryDestinationPath(string destinationDirectoryFullPath, string normalizedEntryName)
{
var pathSegments = normalizedEntryName.Split('/', StringSplitOptions.RemoveEmptyEntries);
var relativePath = Path.Combine(pathSegments);
var destinationPath = Path.GetFullPath(Path.Combine(destinationDirectoryFullPath, relativePath));
if (!destinationPath.StartsWith(destinationDirectoryFullPath, StringComparison.Ordinal))
throw new InvalidDataException($"The enterprise configuration plugin archive contains an entry outside the destination directory: '{normalizedEntryName}'.");
return destinationPath;
}
}
@@ -35,8 +35,9 @@ public static partial class PluginFactory
return;
}
if (!await PLUGIN_LOAD_SEMAPHORE.WaitAsync(0, cancellationToken))
return;
// Wait for ongoing reloads instead of silently skipping this request.
// This caller must return only after its reload has run.
await PLUGIN_LOAD_SEMAPHORE.WaitAsync(cancellationToken);
var configObjectList = new List<PluginConfigurationObject>();
@@ -120,6 +121,8 @@ public static partial class PluginFactory
LOG.LogWarning($"The configuration plugin '{plugin.Id}' does not define 'DEPLOYED_USING_CONFIG_SERVER'. Falling back to the plugin path and treating it as managed because it is stored under '{CONFIGURATION_PLUGINS_ROOT}'.");
}
}
else if (plugin is PluginAssistants assistantPlugin)
isManagedByConfigServer = assistantPlugin.IsManagedByConfigServer;
// For configuration plugins, validate that the plugin ID matches the enterprise config ID
// (the directory name under which the plugin was downloaded):
@@ -214,6 +217,21 @@ public static partial class PluginFactory
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Chat, x => x.PreselectedChatTemplate, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Chat, x => x.PreselectedDataSourcesDisabled, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Chat, x => x.PreselectedDataSourcesAutomaticSelection, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Chat, x => x.PreselectedDataSourcesAutomaticValidation, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Chat, x => x.PreselectedDataSourceIds, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Chat, x => x.SendToChatDataSourceBehavior, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
// Check for the update interval:
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.UpdateInterval, AVAILABLE_PLUGINS))
@@ -234,7 +252,15 @@ public static partial class PluginFactory
// Check for the quick start guide visibility:
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ShowQuickStartGuide, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
// Check for the last changelog visibility:
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ShowLastChangelog, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
// Check for the vision panel visibility:
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ShowVision, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
// Check for users allowed to added providers:
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.AllowUserToAddProvider, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
@@ -300,6 +326,26 @@ public static partial class PluginFactory
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.DataSourceSecurity, x => x.TrustedProviderIds, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
// Check data source selection agent settings:
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AgentDataSourceSelection, x => x.PreselectAgentOptions, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AgentDataSourceSelection, x => x.PreselectedAgentProvider, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
// Check retrieval context validation agent settings:
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AgentRetrievalContextValidation, x => x.EnableRetrievalContextValidation, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AgentRetrievalContextValidation, x => x.PreselectAgentOptions, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AgentRetrievalContextValidation, x => x.PreselectedAgentProvider, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AgentRetrievalContextValidation, x => x.NumParallelValidations, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
// Check if audit is required before it can be activated
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AssistantPluginAudit, x => x.RequireAuditBeforeActivation, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
@@ -319,6 +365,10 @@ public static partial class PluginFactory
// Check if security audits are invoked automatically and transparent for the user
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AssistantPluginAudit, x => x.AutomaticallyAuditAssistants, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
// Check enterprise-managed assistant plugin approvals
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AssistantPluginAudit, x => x.EnterpriseApprovedPlugins, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
if (wasConfigurationChanged)
{
@@ -1,4 +1,7 @@
using System.Text;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.PluginSystem.Assistants;
namespace AIStudio.Tools.PluginSystem;
@@ -78,11 +81,36 @@ public static partial class PluginFactory
LOG.LogError(e, $"An error occurred while starting the plugin: Id='{availablePlugin.Id}', Type='{availablePlugin.Type}', Name='{availablePlugin.Name}', Version='{availablePlugin.Version}'.");
}
}
LogAssistantPluginStartupState();
// Inform all components that the plugins have been reloaded or started:
await MessageBus.INSTANCE.SendMessage<bool>(null, Event.PLUGINS_RELOADED);
return configObjects;
}
private static void LogAssistantPluginStartupState()
{
ManagedConfiguration.TryGet(x => x.AssistantPluginAudit, x => x.EnterpriseApprovedPlugins, out ConfigMeta<DataAssistantPluginAudit, IList<DataAssistantPluginEnterpriseApproval>> configMeta);
var approvedByConfigPluginId = configMeta is { IsLocked: true } ? configMeta.LockedByConfigPluginId : Guid.Empty;
var approvedByConfigPluginName = approvedByConfigPluginId == Guid.Empty
? string.Empty
: AVAILABLE_PLUGINS.FirstOrDefault(x => x.Id == approvedByConfigPluginId)?.Name ?? string.Empty;
foreach (var assistantPlugin in RUNNING_PLUGINS.OfType<PluginAssistants>())
{
var securityState = PluginAssistantSecurityResolver.Resolve(SettingsManagerAccess, assistantPlugin);
if (securityState.IsEnterpriseApproved)
{
LOG.LogInformation(
$"Successfully started assistant plugin: Id='{assistantPlugin.Id}', Type='{assistantPlugin.Type}', Name='{assistantPlugin.Name}', Version='{assistantPlugin.Version}', SecuritySource='EnterpriseApproval', ApprovedByConfigPluginId='{approvedByConfigPluginId}', ApprovedByConfigPluginName='{approvedByConfigPluginName}'");
continue;
}
LOG.LogInformation(
$"Successfully started assistant plugin: Id='{assistantPlugin.Id}', Type='{assistantPlugin.Type}', Name='{assistantPlugin.Name}', Version='{assistantPlugin.Version}'");
}
}
private static async Task<PluginBase> Start(IAvailablePlugin meta, CancellationToken cancellationToken = default)
{