mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-13 06:13:48 +00:00
Added support for organization-approved assistant plugins (#821)
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
This commit is contained in:
1 parent
972ed73fe8
commit
13cc2f837a
35 files changed
+954
-62
No files matched your search
@@ -0,0 +1,7 @@
|
||||
namespace AIStudio.Tools.PluginSystem.Assistants;
|
||||
|
||||
public enum AssistantPluginLaunchBehavior
|
||||
{
|
||||
NONE,
|
||||
OPEN_WORKSPACE_CHAT_BY_NAME,
|
||||
}
|
||||
+65
-2
@@ -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;
|
||||
|
||||
+8
@@ -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,9 @@ 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 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;
|
||||
@@ -61,6 +64,8 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
|
||||
message = string.Empty;
|
||||
this.HasEmbeddedProfileSelection = false;
|
||||
this.buildPromptFunction = null;
|
||||
this.LaunchBehavior = AssistantPluginLaunchBehavior.NONE;
|
||||
this.LaunchWorkspaceName = string.Empty;
|
||||
|
||||
this.RegisterLuaHelpers();
|
||||
|
||||
@@ -123,6 +128,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 +151,51 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
|
||||
return true;
|
||||
}
|
||||
|
||||
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 +280,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;
|
||||
@@ -221,6 +222,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
||||
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);
|
||||
@@ -273,6 +277,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))
|
||||
|
||||
@@ -354,6 +354,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)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user