mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-08-11 20:32:11 +00:00
Added the ability to mark an assistant plugin as secure based on the Enterprise IT
This commit is contained in:
parent
c41a0cf74c
commit
687cf9119a
47
app/Build/Commands/AssistantPluginHashCommand.cs
Normal file
47
app/Build/Commands/AssistantPluginHashCommand.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using SharedTools;
|
||||
|
||||
namespace Build.Commands;
|
||||
|
||||
public sealed class AssistantPluginHashCommand
|
||||
{
|
||||
[Command("assistant-plugin-hash", Description = "Compute the canonical assistant-plugin hash for a plugin directory")]
|
||||
public void ComputeAssistantPluginHash(
|
||||
[Argument(Description = "Path to the assistant plugin directory")] string pluginDir,
|
||||
[Option("lua-snippet", Description = "Also print a Lua snippet for CONFIG[\"SETTINGS\"]")] bool luaSnippet = false)
|
||||
{
|
||||
if (!Environment.IsWorkingDirectoryValid())
|
||||
return;
|
||||
|
||||
var resolvedPath = Path.GetFullPath(pluginDir, Directory.GetCurrentDirectory());
|
||||
if (!Directory.Exists(resolvedPath))
|
||||
{
|
||||
Console.WriteLine($"- Error: The plugin directory '{resolvedPath}' does not exist.");
|
||||
return;
|
||||
}
|
||||
|
||||
var pluginHash = AssistantPluginHash.Compute(resolvedPath);
|
||||
if (string.IsNullOrWhiteSpace(pluginHash))
|
||||
{
|
||||
Console.WriteLine($"- Error: No Lua files were found in '{resolvedPath}'.");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine(pluginHash);
|
||||
|
||||
if (!luaSnippet)
|
||||
return;
|
||||
|
||||
var displayName = Path.GetFileName(resolvedPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
|
||||
var approvedAtUtc = DateTimeOffset.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("""CONFIG["SETTINGS"]["DataAssistantPluginAudit.EnterpriseApprovedPlugins"] = {""");
|
||||
Console.WriteLine(" {");
|
||||
Console.WriteLine($""" ["PluginHash"] = "{pluginHash}",""");
|
||||
Console.WriteLine($""" ["DisplayName"] = "{displayName}",""");
|
||||
Console.WriteLine(""" ["Comment"] = "<optional comment>",""");
|
||||
Console.WriteLine(""" ["ApprovedBy"] = "<optional approver>",""");
|
||||
Console.WriteLine($""" ["ApprovedAtUtc"] = "{approvedAtUtc}",""");
|
||||
Console.WriteLine(" }");
|
||||
Console.WriteLine("}");
|
||||
}
|
||||
}
|
||||
@ -6,4 +6,5 @@ app.AddCommands<CheckRidsCommand>();
|
||||
app.AddCommands<UpdateMetadataCommands>();
|
||||
app.AddCommands<UpdateWebAssetsCommand>();
|
||||
app.AddCommands<CollectI18NKeysCommand>();
|
||||
app.Run();
|
||||
app.AddCommands<AssistantPluginHashCommand>();
|
||||
app.Run();
|
||||
|
||||
@ -1984,6 +1984,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T1172211894"] = "Open Set
|
||||
-- Show or hide the detailed security information.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T1045105126"] = "Show or hide the detailed security information."
|
||||
|
||||
-- This plugin is approved by your organization. A manual security audit is not required.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T1213338416"] = "This plugin is approved by your organization. A manual security audit is not required."
|
||||
|
||||
-- Assistant Audit
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T1506922856"] = "Assistant Audit"
|
||||
|
||||
@ -1999,15 +2002,24 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T1805629238"
|
||||
-- Assistant Security
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T1841954939"] = "Assistant Security"
|
||||
|
||||
-- Company approved
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T2036497459"] = "Company approved"
|
||||
|
||||
-- Required minimum
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T2354026284"] = "Required minimum"
|
||||
|
||||
-- Audit provider
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T2757790517"] = "Audit provider"
|
||||
|
||||
-- Approval name
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T276341849"] = "Approval name"
|
||||
|
||||
-- Technical Details
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T2769062110"] = "Technical Details"
|
||||
|
||||
-- Approval comment
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T2906887599"] = "Approval comment"
|
||||
|
||||
-- No audit yet
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T3138877447"] = "No audit yet"
|
||||
|
||||
@ -2023,21 +2035,39 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T3448155331"
|
||||
-- No stored audit details are available yet.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T3647137899"] = "No stored audit details are available yet."
|
||||
|
||||
-- Enterprise approval is active
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T3816183955"] = "Enterprise approval is active"
|
||||
|
||||
-- Current hash
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T3896860082"] = "Current hash"
|
||||
|
||||
-- No user audit required
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T3916957031"] = "No user audit required"
|
||||
|
||||
-- Audited at
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T4103354206"] = "Audited at"
|
||||
|
||||
-- Approved hash
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T4170340306"] = "Approved hash"
|
||||
|
||||
-- No security findings were stored for this assistant plugin.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T4256679240"] = "No security findings were stored for this assistant plugin."
|
||||
|
||||
-- Status source
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T4289123040"] = "Status source"
|
||||
|
||||
-- Audit hash
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T53507304"] = "Audit hash"
|
||||
|
||||
-- {0} Finding(s)
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T631393016"] = "{0} Finding(s)"
|
||||
|
||||
-- Approved by
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T894543751"] = "Approved by"
|
||||
|
||||
-- Approved at
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T978873131"] = "Approved at"
|
||||
|
||||
-- Click the paperclip to attach files, or click the number to see your attached files.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ATTACHDOCUMENTS::T1358313858"] = "Click the paperclip to attach files, or click the number to see your attached files."
|
||||
|
||||
@ -3418,6 +3448,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::ASSISTANTPLUGINAUDITDIALOG::T3652671056"] =
|
||||
-- Unavailable
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::ASSISTANTPLUGINAUDITDIALOG::T3662391977"] = "Unavailable"
|
||||
|
||||
-- This assistant plugin is approved by your organization. A manual security audit is not required.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::ASSISTANTPLUGINAUDITDIALOG::T3680374624"] = "This assistant plugin is approved by your organization. A manual security audit is not required."
|
||||
|
||||
-- Plugin Structure
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::ASSISTANTPLUGINAUDITDIALOG::T371537943"] = "Plugin Structure"
|
||||
|
||||
@ -7561,6 +7594,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECUR
|
||||
-- This assistant is currently locked.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T123211529"] = "This assistant is currently locked."
|
||||
|
||||
-- Company Approved
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T1397784595"] = "Company Approved"
|
||||
|
||||
-- Audit Required
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T1669285905"] = "Audit Required"
|
||||
|
||||
@ -7591,6 +7627,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECUR
|
||||
-- The current audit result is '{0}', which is below your required minimum level '{1}'. Audit enforcement is currently disabled, so this assistant plugin can still be enabled or used.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T2774333862"] = "The current audit result is '{0}', which is below your required minimum level '{1}'. Audit enforcement is currently disabled, so this assistant plugin can still be enabled or used."
|
||||
|
||||
-- The current plugin hash matches an enterprise-managed approval. No manual security audit is required for activation or usage.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T2824524534"] = "The current plugin hash matches an enterprise-managed approval. No manual security audit is required for activation or usage."
|
||||
|
||||
-- Not Audited
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T2828154864"] = "Not Audited"
|
||||
|
||||
@ -7600,6 +7639,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECUR
|
||||
-- Open Security Check
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T290241209"] = "Open Security Check"
|
||||
|
||||
-- User Audit
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T3293963409"] = "User Audit"
|
||||
|
||||
-- Restricted
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T3325062668"] = "Restricted"
|
||||
|
||||
@ -7621,9 +7663,24 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECUR
|
||||
-- No security audit exists yet. Your current security settings do not require an audit before this assistant plugin may be used.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T3899951594"] = "No security audit exists yet. Your current security settings do not require an audit before this assistant plugin may be used."
|
||||
|
||||
-- No Approval
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T515592229"] = "No Approval"
|
||||
|
||||
-- This assistant was approved by your organization.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T538196816"] = "This assistant was approved by your organization."
|
||||
|
||||
-- Safe
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T760494712"] = "Safe"
|
||||
|
||||
-- Open Security Details
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T803119455"] = "Open Security Details"
|
||||
|
||||
-- Start Security Check
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T811648299"] = "Start Security Check"
|
||||
|
||||
-- This assistant was approved by your organization as '{0}'.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T834246718"] = "This assistant was approved by your organization as '{0}'."
|
||||
|
||||
-- This assistant currently has no stored audit.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T921972844"] = "This assistant currently has no stored audit."
|
||||
|
||||
|
||||
@ -33,6 +33,12 @@
|
||||
<MudChip T="string" Size="Size.Small" Variant="Variant.Filled" Color="@state.AuditColor">
|
||||
@state.AuditLabel
|
||||
</MudChip>
|
||||
@if (!string.IsNullOrWhiteSpace(state.SourceLabel))
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Variant="Variant.Filled" Color="@state.SourceColor" Icon="@state.SourceIcon">
|
||||
@state.SourceLabel
|
||||
</MudChip>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(state.AvailabilityLabel))
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="@state.AvailabilityColor" Icon="@state.AvailabilityIcon">
|
||||
@ -53,18 +59,28 @@
|
||||
|
||||
<MudCardContent Class="pt-0 pb-2">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="4" Class="flex-wrap">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Speed" Size="Size.Small" />
|
||||
<MudText Typo="Typo.body2">@T("Confidence"):</MudText>
|
||||
<MudProgressLinear Color="@state.AuditColor"
|
||||
Value="@this.GetConfidencePercentage()"
|
||||
Rounded="@true"
|
||||
Size="Size.Medium"
|
||||
Style="width: 80px; min-width: 80px;" />
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">
|
||||
@this.GetConfidenceLabel()
|
||||
</MudText>
|
||||
</MudStack>
|
||||
@if (state.IsEnterpriseApproved)
|
||||
{
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Business" Size="Size.Small" Color="@state.SourceColor" />
|
||||
<MudText Typo="Typo.body2">@T("Enterprise approval is active")</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Speed" Size="Size.Small" />
|
||||
<MudText Typo="Typo.body2">@T("Confidence"):</MudText>
|
||||
<MudProgressLinear Color="@state.AuditColor"
|
||||
Value="@this.GetConfidencePercentage()"
|
||||
Rounded="@true"
|
||||
Size="Size.Medium"
|
||||
Style="width: 80px; min-width: 80px;" />
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">
|
||||
@this.GetConfidenceLabel()
|
||||
</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
<MudDivider Vertical="@true" FlexItem="@true" />
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudIcon Icon="@Icons.Material.Filled.BugReport" Size="Size.Small" Color="@state.AuditColor" />
|
||||
@ -104,12 +120,63 @@
|
||||
</td>
|
||||
<td><code style="font-size: 0.8rem;">@this.Plugin.Id</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<MudText Typo="Typo.body2"><b>@T("Status source")</b></MudText>
|
||||
</td>
|
||||
<td><MudText Typo="Typo.body2">@state.SourceLabel</MudText></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<MudText Typo="Typo.body2"><b>@T("Current hash")</b></MudText>
|
||||
</td>
|
||||
<td><code style="font-size: 0.8rem;">@GetShortHash(state.CurrentHash)</code></td>
|
||||
</tr>
|
||||
@if (state.EnterpriseApproval is not null)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<MudText Typo="Typo.body2"><b>@T("Approved hash")</b></MudText>
|
||||
</td>
|
||||
<td><code style="font-size: 0.8rem;">@GetShortHash(state.EnterpriseApproval.PluginHash)</code></td>
|
||||
</tr>
|
||||
@if (!string.IsNullOrWhiteSpace(state.EnterpriseApproval.DisplayName))
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<MudText Typo="Typo.body2"><b>@T("Approval name")</b></MudText>
|
||||
</td>
|
||||
<td><MudText Typo="Typo.body2">@state.EnterpriseApproval.DisplayName</MudText></td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(state.EnterpriseApproval.ApprovedBy))
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<MudText Typo="Typo.body2"><b>@T("Approved by")</b></MudText>
|
||||
</td>
|
||||
<td><MudText Typo="Typo.body2">@state.EnterpriseApproval.ApprovedBy</MudText></td>
|
||||
</tr>
|
||||
}
|
||||
@if (state.EnterpriseApproval.ApprovedAtUtc is not null)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<MudText Typo="Typo.body2"><b>@T("Approved at")</b></MudText>
|
||||
</td>
|
||||
<td><MudText Typo="Typo.body2">@this.FormatFileTimestamp(state.EnterpriseApproval.ApprovedAtUtc.Value.ToLocalTime().DateTime)</MudText></td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(state.EnterpriseApproval.Comment))
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<MudText Typo="Typo.body2"><b>@T("Approval comment")</b></MudText>
|
||||
</td>
|
||||
<td><MudText Typo="Typo.body2">@state.EnterpriseApproval.Comment</MudText></td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
@if (state.Audit is not null)
|
||||
{
|
||||
<tr>
|
||||
@ -156,9 +223,18 @@
|
||||
|
||||
@if (state.Audit is null)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="@true">
|
||||
@T("No stored audit details are available yet.")
|
||||
</MudAlert>
|
||||
@if (state.IsEnterpriseApproved)
|
||||
{
|
||||
<MudAlert Severity="Severity.Success" Variant="Variant.Text" Dense="@true">
|
||||
@T("This plugin is approved by your organization. A manual security audit is not required.")
|
||||
</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="@true">
|
||||
@T("No stored audit details are available yet.")
|
||||
</MudAlert>
|
||||
}
|
||||
}
|
||||
else if (state.Audit.Findings.Count == 0)
|
||||
{
|
||||
|
||||
@ -103,12 +103,23 @@ public partial class AssistantPluginSecurityCard : MSGComponentBase
|
||||
|
||||
private string GetFindingSummary()
|
||||
{
|
||||
if (this.SecurityState.IsEnterpriseApproved)
|
||||
return this.T("No user audit required");
|
||||
|
||||
var count = this.SecurityState.Audit?.Findings.Count ?? 0;
|
||||
return string.Format(this.T("{0} Finding(s)"), count);
|
||||
}
|
||||
|
||||
private string GetAuditTimestampLabel()
|
||||
{
|
||||
if (this.SecurityState.IsEnterpriseApproved)
|
||||
{
|
||||
var approvedAt = this.SecurityState.EnterpriseApproval?.ApprovedAtUtc;
|
||||
return approvedAt is null
|
||||
? this.T("Company approved")
|
||||
: this.FormatFileTimestamp(approvedAt.Value.ToLocalTime().DateTime);
|
||||
}
|
||||
|
||||
var auditedAt = this.SecurityState.Audit?.AuditedAtUtc;
|
||||
return auditedAt is null
|
||||
? this.T("No audit yet")
|
||||
|
||||
@ -12,9 +12,18 @@
|
||||
else
|
||||
{
|
||||
<MudStack Spacing="2">
|
||||
<MudAlert Severity="Severity.Info" Dense="true">
|
||||
@T("This security check uses a sample prompt preview. Empty or placeholder values in the preview are expected.")
|
||||
</MudAlert>
|
||||
@if (this.securityState.IsEnterpriseApproved)
|
||||
{
|
||||
<MudAlert Severity="Severity.Success" Dense="true">
|
||||
@T("This assistant plugin is approved by your organization. A manual security audit is not required.")
|
||||
</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Dense="true">
|
||||
@T("This security check uses a sample prompt preview. Empty or placeholder values in the preview are expected.")
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
<MudPaper Class="pa-3 border-dashed border rounded-lg">
|
||||
<MudText Typo="Typo.h6">@this.plugin.Name</MudText>
|
||||
@ -298,9 +307,12 @@
|
||||
<MudButton OnClick="@this.CloseWithoutActivation" Variant="Variant.Filled">
|
||||
@(this.audit is null ? T("Cancel") : T("Close"))
|
||||
</MudButton>
|
||||
<MudButton OnClick="@this.RunAudit" Variant="Variant.Filled" Color="Color.Primary" Disabled="@(!this.CanRunAudit || this.justAudited)">
|
||||
@T("Start Security Check")
|
||||
</MudButton>
|
||||
@if (!this.securityState.IsEnterpriseApproved)
|
||||
{
|
||||
<MudButton OnClick="@this.RunAudit" Variant="Variant.Filled" Color="Color.Primary" Disabled="@(!this.CanRunAudit || this.justAudited)">
|
||||
@T("Start Security Check")
|
||||
</MudButton>
|
||||
}
|
||||
@if (this.CanEnablePlugin)
|
||||
{
|
||||
<MudButton OnClick="@this.EnablePlugin" Variant="Variant.Filled" Color="@this.EnableButtonColor">
|
||||
|
||||
@ -37,6 +37,7 @@ public partial class AssistantPluginAuditDialog : MSGComponentBase
|
||||
private IReadOnlyCollection<TreeItemData<ITreeItem>> fileSystemTreeItems = [];
|
||||
private CultureInfo currentCultureInfo = CultureInfo.InvariantCulture;
|
||||
private bool isAuditing;
|
||||
private PluginAssistantSecurityState securityState = new();
|
||||
|
||||
private AIStudio.Settings.Provider CurrentProvider => this.SettingsManager.GetPreselectedProvider(Tools.Components.AGENT_ASSISTANT_PLUGIN_AUDIT, null, true);
|
||||
|
||||
@ -50,7 +51,7 @@ public partial class AssistantPluginAuditDialog : MSGComponentBase
|
||||
|
||||
private string MinimumLevelLabel => this.MinimumLevel.GetName();
|
||||
|
||||
private bool CanRunAudit => this.plugin is not null && this.CurrentProvider != AIStudio.Settings.Provider.NONE && !this.isAuditing;
|
||||
private bool CanRunAudit => this.plugin is not null && this.CurrentProvider != AIStudio.Settings.Provider.NONE && !this.isAuditing && !this.securityState.IsEnterpriseApproved;
|
||||
|
||||
private bool IsAuditBelowMinimum => this.audit is not null && this.audit.Level < this.MinimumLevel;
|
||||
|
||||
@ -74,6 +75,7 @@ public partial class AssistantPluginAuditDialog : MSGComponentBase
|
||||
.FirstOrDefault(x => x.Id == this.PluginId);
|
||||
if (this.plugin is not null)
|
||||
{
|
||||
this.securityState = PluginAssistantSecurityResolver.Resolve(this.SettingsManager, this.plugin);
|
||||
this.promptPreview = await this.plugin.BuildAuditPromptPreviewAsync();
|
||||
this.promptFallbackPreview = this.plugin.BuildAuditPromptFallbackPreview();
|
||||
this.plugin.CreateAuditComponentSummary();
|
||||
@ -96,6 +98,7 @@ public partial class AssistantPluginAuditDialog : MSGComponentBase
|
||||
try
|
||||
{
|
||||
this.audit = await this.AssistantPluginAuditService.RunAuditAsync(this.plugin);
|
||||
this.securityState = PluginAssistantSecurityResolver.Resolve(this.SettingsManager, this.plugin);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@ -280,6 +280,23 @@ CONFIG["SETTINGS"] = {}
|
||||
-- DOCUMENT_ANALYSIS_ASSISTANT, SLIDE_BUILDER_ASSISTANT, I18N_ASSISTANT
|
||||
-- CONFIG["SETTINGS"]["DataApp.HiddenAssistants"] = { "ERI_ASSISTANT", "I18N_ASSISTANT" }
|
||||
|
||||
-- Configure enterprise approvals for assistant plugins.
|
||||
-- Each approval is matched only by the current SHA-256 hash over all Lua files
|
||||
-- in the assistant plugin folder, in canonical sorted order.
|
||||
-- When the hash matches, the assistant plugin is treated as SAFE immediately and
|
||||
-- no user-run security audit is required.
|
||||
-- You can generate the exact hash with the build-script command:
|
||||
-- dotnet run --project app/Build -- assistant-plugin-hash "<plugin-dir>" --lua-snippet
|
||||
-- CONFIG["SETTINGS"]["DataAssistantPluginAudit.EnterpriseApprovedPlugins"] = {
|
||||
-- {
|
||||
-- ["PluginHash"] = "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF",
|
||||
-- ["DisplayName"] = "Name of Plugin",
|
||||
-- ["Comment"] = "Optional comment",
|
||||
-- ["ApprovedBy"] = "Optional Approver",
|
||||
-- ["ApprovedAtUtc"] = "2026-07-02T09:30:00Z",
|
||||
-- }
|
||||
-- }
|
||||
|
||||
-- Configure a global shortcut for starting and stopping dictation.
|
||||
--
|
||||
-- The format follows the Rust and Tauri conventions. Especially,
|
||||
|
||||
@ -39,5 +39,10 @@ public sealed class DataAssistantPluginAudit(Expression<Func<Data, DataAssistant
|
||||
/// <summary>
|
||||
/// If true, the security audit will be hidden from the user and done in the background
|
||||
/// </summary>
|
||||
public bool AutomaticallyAuditAssistants { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AutomaticallyAuditAssistants, true);
|
||||
public bool AutomaticallyAuditAssistants { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AutomaticallyAuditAssistants, false);
|
||||
|
||||
/// <summary>
|
||||
/// Enterprise-managed assistant plugin hashes that are approved without requiring a user audit.
|
||||
/// </summary>
|
||||
public IList<DataAssistantPluginEnterpriseApproval> EnterpriseApprovedPlugins { get; set; } = ManagedConfiguration.Register(configSelection, n => n.EnterpriseApprovedPlugins, []);
|
||||
}
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
namespace AIStudio.Settings.DataModel;
|
||||
|
||||
/// <summary>
|
||||
/// Enterprise-managed approval entry for an assistant plugin hash.
|
||||
/// </summary>
|
||||
public sealed class DataAssistantPluginEnterpriseApproval
|
||||
{
|
||||
public string PluginHash { get; init; } = string.Empty;
|
||||
public string DisplayName { get; init; } = string.Empty;
|
||||
public string Comment { get; init; } = string.Empty;
|
||||
public string ApprovedBy { get; init; } = string.Empty;
|
||||
public DateTimeOffset? ApprovedAtUtc { get; init; }
|
||||
}
|
||||
@ -31,12 +31,27 @@ public static class CommonTools
|
||||
if (string.IsNullOrWhiteSpace(ietfTag))
|
||||
return CultureInfo.InvariantCulture;
|
||||
|
||||
var normalizedTag = ietfTag.Trim().Replace('_', '-');
|
||||
|
||||
try
|
||||
{
|
||||
return CultureInfo.GetCultureInfo(ietfTag);
|
||||
return CultureInfo.GetCultureInfo(normalizedTag);
|
||||
}
|
||||
catch (CultureNotFoundException)
|
||||
{
|
||||
var separatorIndex = normalizedTag.IndexOf('-');
|
||||
if (separatorIndex > 0)
|
||||
{
|
||||
var neutralLanguageTag = normalizedTag[..separatorIndex];
|
||||
try
|
||||
{
|
||||
return CultureInfo.GetCultureInfo(neutralLanguageTag);
|
||||
}
|
||||
catch (CultureNotFoundException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return CultureInfo.InvariantCulture;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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("Company Approved"),
|
||||
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;
|
||||
|
||||
@ -280,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;
|
||||
@ -204,6 +205,9 @@ 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: 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);
|
||||
@ -251,6 +255,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))
|
||||
|
||||
@ -319,6 +319,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)
|
||||
{
|
||||
|
||||
40
app/SharedTools/AssistantPluginHash.cs
Normal file
40
app/SharedTools/AssistantPluginHash.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace SharedTools;
|
||||
|
||||
/// <summary>
|
||||
/// Computes the canonical assistant-plugin hash across all Lua files in a plugin directory.
|
||||
/// </summary>
|
||||
public static class AssistantPluginHash
|
||||
{
|
||||
public static string Compute(string pluginDirectory)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pluginDirectory) || !Directory.Exists(pluginDirectory))
|
||||
return string.Empty;
|
||||
|
||||
var luaFiles = Directory.EnumerateFiles(pluginDirectory, "*.lua", SearchOption.AllDirectories)
|
||||
.OrderBy(path => path, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
if (luaFiles.Count == 0)
|
||||
return string.Empty;
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
using var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true);
|
||||
|
||||
foreach (var filePath in luaFiles)
|
||||
{
|
||||
var relativePath = Path.GetRelativePath(pluginDirectory, filePath).Replace('\\', '/');
|
||||
var pathBytes = Encoding.UTF8.GetBytes(relativePath);
|
||||
var contentBytes = File.ReadAllBytes(filePath);
|
||||
|
||||
writer.Write(pathBytes.Length);
|
||||
writer.Write(pathBytes);
|
||||
writer.Write(contentBytes.Length);
|
||||
writer.Write(contentBytes);
|
||||
}
|
||||
|
||||
writer.Flush();
|
||||
return Convert.ToHexString(SHA256.HashData(stream.ToArray()));
|
||||
}
|
||||
}
|
||||
@ -287,11 +287,53 @@ Currently, you can configure the following things:
|
||||
- Any number of LLM providers (self-hosted or cloud providers with encrypted API keys)
|
||||
- Any number of transcription providers for voice-to-text functionality
|
||||
- Any number of embedding providers for RAG
|
||||
- Enterprise hash approvals for assistant plugins
|
||||
- The update behavior of AI Studio
|
||||
- Various UI and feature settings (see the example configuration for details)
|
||||
|
||||
All other settings can be made by the user themselves. If you need additional settings, feel free to create an issue in our planning repository: https://github.com/MindWorkAI/Planning/issues
|
||||
|
||||
## Enterprise approval for assistant plugins
|
||||
|
||||
Enterprise configurations can approve assistant plugins by hash so that users do not need to run a local assistant audit before activation. The approval is based only on the current plugin content, not on the plugin GUID.
|
||||
|
||||
AI Studio computes the approval hash as a SHA-256 digest over all `.lua` files in the assistant plugin directory:
|
||||
|
||||
- recursively
|
||||
- sorted by relative path in ordinal order
|
||||
- using canonical `/` path separators
|
||||
- hashing relative-path length, relative path, content length, and file content for each Lua file
|
||||
|
||||
If any Lua file changes, the hash changes automatically and the enterprise approval no longer applies.
|
||||
|
||||
### Configuration example
|
||||
|
||||
Add the approval list to `CONFIG["SETTINGS"]` in your configuration plugin:
|
||||
|
||||
```lua
|
||||
CONFIG["SETTINGS"]["DataAssistantPluginAudit.EnterpriseApprovedPlugins"] = {
|
||||
{
|
||||
["PluginHash"] = "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF",
|
||||
["DisplayName"] = "Corporate Translation Assistant",
|
||||
["Comment"] = "Approved for internal rollout",
|
||||
["ApprovedBy"] = "AI Governance Board",
|
||||
["ApprovedAtUtc"] = "2026-07-02T09:30:00Z",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`PluginHash` is required. All other fields are optional and are shown in the UI as approval metadata.
|
||||
|
||||
### Generating the hash
|
||||
|
||||
Use the build-script command from the repository root:
|
||||
|
||||
```bash
|
||||
dotnet run --project app/Build -- assistant-plugin-hash "<plugin-dir>" --lua-snippet
|
||||
```
|
||||
|
||||
This prints the canonical hash and, with `--lua-snippet`, also prints a ready-to-paste Lua snippet for `CONFIG["SETTINGS"]`.
|
||||
|
||||
## Encrypted API Keys
|
||||
|
||||
You can include encrypted API keys in your configuration plugins for cloud providers (like OpenAI, Anthropic) or secured on-premise models. This feature provides obfuscation to prevent casual exposure of API keys in configuration files.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user