mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-09-13 09:10:38 +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<UpdateMetadataCommands>();
|
||||||
app.AddCommands<UpdateWebAssetsCommand>();
|
app.AddCommands<UpdateWebAssetsCommand>();
|
||||||
app.AddCommands<CollectI18NKeysCommand>();
|
app.AddCommands<CollectI18NKeysCommand>();
|
||||||
|
app.AddCommands<AssistantPluginHashCommand>();
|
||||||
app.Run();
|
app.Run();
|
||||||
@ -1984,6 +1984,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T1172211894"] = "Open Set
|
|||||||
-- Show or hide the detailed security information.
|
-- Show or hide the detailed security information.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T1045105126"] = "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
|
-- Assistant Audit
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T1506922856"] = "Assistant Audit"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T1506922856"] = "Assistant Audit"
|
||||||
|
|
||||||
@ -1999,15 +2002,24 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T1805629238"
|
|||||||
-- Assistant Security
|
-- Assistant Security
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T1841954939"] = "Assistant Security"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T1841954939"] = "Assistant Security"
|
||||||
|
|
||||||
|
-- Company approved
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T2036497459"] = "Company approved"
|
||||||
|
|
||||||
-- Required minimum
|
-- Required minimum
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T2354026284"] = "Required minimum"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T2354026284"] = "Required minimum"
|
||||||
|
|
||||||
-- Audit provider
|
-- Audit provider
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T2757790517"] = "Audit provider"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T2757790517"] = "Audit provider"
|
||||||
|
|
||||||
|
-- Approval name
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T276341849"] = "Approval name"
|
||||||
|
|
||||||
-- Technical Details
|
-- Technical Details
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T2769062110"] = "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
|
-- No audit yet
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T3138877447"] = "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.
|
-- No stored audit details are available yet.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T3647137899"] = "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
|
-- Current hash
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T3896860082"] = "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
|
-- Audited at
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T4103354206"] = "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.
|
-- 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."
|
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
|
-- Audit hash
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T53507304"] = "Audit hash"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T53507304"] = "Audit hash"
|
||||||
|
|
||||||
-- {0} Finding(s)
|
-- {0} Finding(s)
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTPLUGINSECURITYCARD::T631393016"] = "{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.
|
-- 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."
|
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
|
-- Unavailable
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::ASSISTANTPLUGINAUDITDIALOG::T3662391977"] = "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
|
-- Plugin Structure
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::ASSISTANTPLUGINAUDITDIALOG::T371537943"] = "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.
|
-- This assistant is currently locked.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T123211529"] = "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
|
-- Audit Required
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T1669285905"] = "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.
|
-- 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."
|
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
|
-- Not Audited
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T2828154864"] = "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
|
-- Open Security Check
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T290241209"] = "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
|
-- Restricted
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T3325062668"] = "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.
|
-- 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."
|
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
|
-- Start Security Check
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T811648299"] = "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.
|
-- This assistant currently has no stored audit.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T921972844"] = "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">
|
<MudChip T="string" Size="Size.Small" Variant="Variant.Filled" Color="@state.AuditColor">
|
||||||
@state.AuditLabel
|
@state.AuditLabel
|
||||||
</MudChip>
|
</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))
|
@if (!string.IsNullOrWhiteSpace(state.AvailabilityLabel))
|
||||||
{
|
{
|
||||||
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="@state.AvailabilityColor" Icon="@state.AvailabilityIcon">
|
<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">
|
<MudCardContent Class="pt-0 pb-2">
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="4" Class="flex-wrap">
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="4" Class="flex-wrap">
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
@if (state.IsEnterpriseApproved)
|
||||||
<MudIcon Icon="@Icons.Material.Filled.Speed" Size="Size.Small" />
|
{
|
||||||
<MudText Typo="Typo.body2">@T("Confidence"):</MudText>
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||||
<MudProgressLinear Color="@state.AuditColor"
|
<MudIcon Icon="@Icons.Material.Filled.Business" Size="Size.Small" Color="@state.SourceColor" />
|
||||||
Value="@this.GetConfidencePercentage()"
|
<MudText Typo="Typo.body2">@T("Enterprise approval is active")</MudText>
|
||||||
Rounded="@true"
|
</MudStack>
|
||||||
Size="Size.Medium"
|
}
|
||||||
Style="width: 80px; min-width: 80px;" />
|
else
|
||||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">
|
{
|
||||||
@this.GetConfidenceLabel()
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||||
</MudText>
|
<MudIcon Icon="@Icons.Material.Filled.Speed" Size="Size.Small" />
|
||||||
</MudStack>
|
<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" />
|
<MudDivider Vertical="@true" FlexItem="@true" />
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||||
<MudIcon Icon="@Icons.Material.Filled.BugReport" Size="Size.Small" Color="@state.AuditColor" />
|
<MudIcon Icon="@Icons.Material.Filled.BugReport" Size="Size.Small" Color="@state.AuditColor" />
|
||||||
@ -104,12 +120,63 @@
|
|||||||
</td>
|
</td>
|
||||||
<td><code style="font-size: 0.8rem;">@this.Plugin.Id</code></td>
|
<td><code style="font-size: 0.8rem;">@this.Plugin.Id</code></td>
|
||||||
</tr>
|
</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>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<MudText Typo="Typo.body2"><b>@T("Current hash")</b></MudText>
|
<MudText Typo="Typo.body2"><b>@T("Current hash")</b></MudText>
|
||||||
</td>
|
</td>
|
||||||
<td><code style="font-size: 0.8rem;">@GetShortHash(state.CurrentHash)</code></td>
|
<td><code style="font-size: 0.8rem;">@GetShortHash(state.CurrentHash)</code></td>
|
||||||
</tr>
|
</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)
|
@if (state.Audit is not null)
|
||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
@ -156,9 +223,18 @@
|
|||||||
|
|
||||||
@if (state.Audit is null)
|
@if (state.Audit is null)
|
||||||
{
|
{
|
||||||
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="@true">
|
@if (state.IsEnterpriseApproved)
|
||||||
@T("No stored audit details are available yet.")
|
{
|
||||||
</MudAlert>
|
<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)
|
else if (state.Audit.Findings.Count == 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -103,12 +103,23 @@ public partial class AssistantPluginSecurityCard : MSGComponentBase
|
|||||||
|
|
||||||
private string GetFindingSummary()
|
private string GetFindingSummary()
|
||||||
{
|
{
|
||||||
|
if (this.SecurityState.IsEnterpriseApproved)
|
||||||
|
return this.T("No user audit required");
|
||||||
|
|
||||||
var count = this.SecurityState.Audit?.Findings.Count ?? 0;
|
var count = this.SecurityState.Audit?.Findings.Count ?? 0;
|
||||||
return string.Format(this.T("{0} Finding(s)"), count);
|
return string.Format(this.T("{0} Finding(s)"), count);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetAuditTimestampLabel()
|
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;
|
var auditedAt = this.SecurityState.Audit?.AuditedAtUtc;
|
||||||
return auditedAt is null
|
return auditedAt is null
|
||||||
? this.T("No audit yet")
|
? this.T("No audit yet")
|
||||||
|
|||||||
@ -12,9 +12,18 @@
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
<MudStack Spacing="2">
|
<MudStack Spacing="2">
|
||||||
<MudAlert Severity="Severity.Info" Dense="true">
|
@if (this.securityState.IsEnterpriseApproved)
|
||||||
@T("This security check uses a sample prompt preview. Empty or placeholder values in the preview are expected.")
|
{
|
||||||
</MudAlert>
|
<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">
|
<MudPaper Class="pa-3 border-dashed border rounded-lg">
|
||||||
<MudText Typo="Typo.h6">@this.plugin.Name</MudText>
|
<MudText Typo="Typo.h6">@this.plugin.Name</MudText>
|
||||||
@ -298,9 +307,12 @@
|
|||||||
<MudButton OnClick="@this.CloseWithoutActivation" Variant="Variant.Filled">
|
<MudButton OnClick="@this.CloseWithoutActivation" Variant="Variant.Filled">
|
||||||
@(this.audit is null ? T("Cancel") : T("Close"))
|
@(this.audit is null ? T("Cancel") : T("Close"))
|
||||||
</MudButton>
|
</MudButton>
|
||||||
<MudButton OnClick="@this.RunAudit" Variant="Variant.Filled" Color="Color.Primary" Disabled="@(!this.CanRunAudit || this.justAudited)">
|
@if (!this.securityState.IsEnterpriseApproved)
|
||||||
@T("Start Security Check")
|
{
|
||||||
</MudButton>
|
<MudButton OnClick="@this.RunAudit" Variant="Variant.Filled" Color="Color.Primary" Disabled="@(!this.CanRunAudit || this.justAudited)">
|
||||||
|
@T("Start Security Check")
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
@if (this.CanEnablePlugin)
|
@if (this.CanEnablePlugin)
|
||||||
{
|
{
|
||||||
<MudButton OnClick="@this.EnablePlugin" Variant="Variant.Filled" Color="@this.EnableButtonColor">
|
<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 IReadOnlyCollection<TreeItemData<ITreeItem>> fileSystemTreeItems = [];
|
||||||
private CultureInfo currentCultureInfo = CultureInfo.InvariantCulture;
|
private CultureInfo currentCultureInfo = CultureInfo.InvariantCulture;
|
||||||
private bool isAuditing;
|
private bool isAuditing;
|
||||||
|
private PluginAssistantSecurityState securityState = new();
|
||||||
|
|
||||||
private AIStudio.Settings.Provider CurrentProvider => this.SettingsManager.GetPreselectedProvider(Tools.Components.AGENT_ASSISTANT_PLUGIN_AUDIT, null, true);
|
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 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;
|
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);
|
.FirstOrDefault(x => x.Id == this.PluginId);
|
||||||
if (this.plugin is not null)
|
if (this.plugin is not null)
|
||||||
{
|
{
|
||||||
|
this.securityState = PluginAssistantSecurityResolver.Resolve(this.SettingsManager, this.plugin);
|
||||||
this.promptPreview = await this.plugin.BuildAuditPromptPreviewAsync();
|
this.promptPreview = await this.plugin.BuildAuditPromptPreviewAsync();
|
||||||
this.promptFallbackPreview = this.plugin.BuildAuditPromptFallbackPreview();
|
this.promptFallbackPreview = this.plugin.BuildAuditPromptFallbackPreview();
|
||||||
this.plugin.CreateAuditComponentSummary();
|
this.plugin.CreateAuditComponentSummary();
|
||||||
@ -96,6 +98,7 @@ public partial class AssistantPluginAuditDialog : MSGComponentBase
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
this.audit = await this.AssistantPluginAuditService.RunAuditAsync(this.plugin);
|
this.audit = await this.AssistantPluginAuditService.RunAuditAsync(this.plugin);
|
||||||
|
this.securityState = PluginAssistantSecurityResolver.Resolve(this.SettingsManager, this.plugin);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@ -280,6 +280,23 @@ CONFIG["SETTINGS"] = {}
|
|||||||
-- DOCUMENT_ANALYSIS_ASSISTANT, SLIDE_BUILDER_ASSISTANT, I18N_ASSISTANT
|
-- DOCUMENT_ANALYSIS_ASSISTANT, SLIDE_BUILDER_ASSISTANT, I18N_ASSISTANT
|
||||||
-- CONFIG["SETTINGS"]["DataApp.HiddenAssistants"] = { "ERI_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.
|
-- Configure a global shortcut for starting and stopping dictation.
|
||||||
--
|
--
|
||||||
-- The format follows the Rust and Tauri conventions. Especially,
|
-- The format follows the Rust and Tauri conventions. Especially,
|
||||||
|
|||||||
@ -39,5 +39,10 @@ public sealed class DataAssistantPluginAudit(Expression<Func<Data, DataAssistant
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// If true, the security audit will be hidden from the user and done in the background
|
/// If true, the security audit will be hidden from the user and done in the background
|
||||||
/// </summary>
|
/// </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))
|
if (string.IsNullOrWhiteSpace(ietfTag))
|
||||||
return CultureInfo.InvariantCulture;
|
return CultureInfo.InvariantCulture;
|
||||||
|
|
||||||
|
var normalizedTag = ietfTag.Trim().Replace('_', '-');
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return CultureInfo.GetCultureInfo(ietfTag);
|
return CultureInfo.GetCultureInfo(normalizedTag);
|
||||||
}
|
}
|
||||||
catch (CultureNotFoundException)
|
catch (CultureNotFoundException)
|
||||||
{
|
{
|
||||||
|
var separatorIndex = normalizedTag.IndexOf('-');
|
||||||
|
if (separatorIndex > 0)
|
||||||
|
{
|
||||||
|
var neutralLanguageTag = normalizedTag[..separatorIndex];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return CultureInfo.GetCultureInfo(neutralLanguageTag);
|
||||||
|
}
|
||||||
|
catch (CultureNotFoundException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return CultureInfo.InvariantCulture;
|
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 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)
|
private static string GetAvailabilityLabel(bool requiresAudit, bool hasAudit, bool hasHashMismatch, bool isBlocked, bool canOverride)
|
||||||
{
|
{
|
||||||
if (hasHashMismatch)
|
if (hasHashMismatch)
|
||||||
@ -75,10 +77,55 @@ public static class PluginAssistantSecurityResolver
|
|||||||
var auditSettings = settingsManager.ConfigurationData.AssistantPluginAudit;
|
var auditSettings = settingsManager.ConfigurationData.AssistantPluginAudit;
|
||||||
var enforceAuditBeforeActivation = auditSettings.RequireAuditBeforeActivation;
|
var enforceAuditBeforeActivation = auditSettings.RequireAuditBeforeActivation;
|
||||||
var isEnforcementDisabled = !enforceAuditBeforeActivation;
|
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 audit = settingsManager.ConfigurationData.AssistantPluginAudits.FirstOrDefault(x => x.PluginId == plugin.Id);
|
||||||
var hasAudit = audit is not null && audit.Level is not AssistantAuditLevel.UNKNOWN;
|
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 hasHashMismatch = hasAudit && !hashMatches;
|
||||||
var isBelowMinimum = hashMatches && audit is not null && audit.Level < auditSettings.MinimumLevel;
|
var isBelowMinimum = hashMatches && audit is not null && audit.Level < auditSettings.MinimumLevel;
|
||||||
var meetsMinimum = 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,
|
Plugin = plugin,
|
||||||
Audit = null,
|
Audit = null,
|
||||||
Settings = auditSettings,
|
Settings = auditSettings,
|
||||||
|
Source = PluginAssistantSecurityStatusSource.NONE,
|
||||||
CurrentHash = currentHash,
|
CurrentHash = currentHash,
|
||||||
HashMatches = false,
|
HashMatches = false,
|
||||||
HasHashMismatch = false,
|
HasHashMismatch = false,
|
||||||
@ -111,6 +159,9 @@ public static class PluginAssistantSecurityResolver
|
|||||||
AvailabilityColor = GetAvailabilityColor(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
|
AvailabilityColor = GetAvailabilityColor(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
|
||||||
AvailabilityIcon = GetAvailabilityIcon(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
|
AvailabilityIcon = GetAvailabilityIcon(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
|
||||||
StatusLabel = GetAvailabilityLabel(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),
|
BadgeIcon = GetSecurityBadgeIcon(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
|
||||||
Headline = requiresAudit ? TB("This assistant is currently locked.") : TB("This assistant currently has no stored audit."),
|
Headline = requiresAudit ? TB("This assistant is currently locked.") : TB("This assistant currently has no stored audit."),
|
||||||
Description = requiresAudit
|
Description = requiresAudit
|
||||||
@ -129,6 +180,7 @@ public static class PluginAssistantSecurityResolver
|
|||||||
Plugin = plugin,
|
Plugin = plugin,
|
||||||
Audit = audit,
|
Audit = audit,
|
||||||
Settings = auditSettings,
|
Settings = auditSettings,
|
||||||
|
Source = PluginAssistantSecurityStatusSource.NONE,
|
||||||
CurrentHash = currentHash,
|
CurrentHash = currentHash,
|
||||||
HashMatches = false,
|
HashMatches = false,
|
||||||
HasHashMismatch = true,
|
HasHashMismatch = true,
|
||||||
@ -146,6 +198,9 @@ public static class PluginAssistantSecurityResolver
|
|||||||
AvailabilityColor = GetAvailabilityColor(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
|
AvailabilityColor = GetAvailabilityColor(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
|
||||||
AvailabilityIcon = GetAvailabilityIcon(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
|
AvailabilityIcon = GetAvailabilityIcon(requiresAudit, hasAudit, hasHashMismatch, isBlocked, canOverride: false),
|
||||||
StatusLabel = GetAvailabilityLabel(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),
|
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."),
|
Headline = requiresAudit ? TB("This assistant is locked until it is audited again.") : TB("This assistant changed after its last audit."),
|
||||||
Description = requiresAudit
|
Description = requiresAudit
|
||||||
@ -167,6 +222,7 @@ public static class PluginAssistantSecurityResolver
|
|||||||
Plugin = plugin,
|
Plugin = plugin,
|
||||||
Audit = audit,
|
Audit = audit,
|
||||||
Settings = auditSettings,
|
Settings = auditSettings,
|
||||||
|
Source = PluginAssistantSecurityStatusSource.USER_AUDIT,
|
||||||
CurrentHash = currentHash,
|
CurrentHash = currentHash,
|
||||||
HashMatches = true,
|
HashMatches = true,
|
||||||
HasHashMismatch = false,
|
HasHashMismatch = false,
|
||||||
@ -184,6 +240,9 @@ public static class PluginAssistantSecurityResolver
|
|||||||
AvailabilityColor = GetAvailabilityColor(requiresAudit: false, hasAudit, hasHashMismatch: false, isBlockedByMinimum, canOverride),
|
AvailabilityColor = GetAvailabilityColor(requiresAudit: false, hasAudit, hasHashMismatch: false, isBlockedByMinimum, canOverride),
|
||||||
AvailabilityIcon = GetAvailabilityIcon(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),
|
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),
|
BadgeIcon = GetSecurityBadgeIcon(requiresAudit: false, hasAudit, hasHashMismatch: false, isBlockedByMinimum, canOverride),
|
||||||
Headline = isBlockedByMinimum
|
Headline = isBlockedByMinimum
|
||||||
? TB("This assistant is currently locked.")
|
? TB("This assistant is currently locked.")
|
||||||
@ -208,6 +267,7 @@ public static class PluginAssistantSecurityResolver
|
|||||||
Plugin = plugin,
|
Plugin = plugin,
|
||||||
Audit = audit,
|
Audit = audit,
|
||||||
Settings = auditSettings,
|
Settings = auditSettings,
|
||||||
|
Source = PluginAssistantSecurityStatusSource.USER_AUDIT,
|
||||||
CurrentHash = currentHash,
|
CurrentHash = currentHash,
|
||||||
HashMatches = true,
|
HashMatches = true,
|
||||||
HasHashMismatch = false,
|
HasHashMismatch = false,
|
||||||
@ -225,6 +285,9 @@ public static class PluginAssistantSecurityResolver
|
|||||||
AvailabilityColor = GetAvailabilityColor(requiresAudit: false, hasAudit, hasHashMismatch: false, isBlocked: false, canOverride: false),
|
AvailabilityColor = GetAvailabilityColor(requiresAudit: false, hasAudit, hasHashMismatch: false, isBlocked: false, canOverride: false),
|
||||||
AvailabilityIcon = GetAvailabilityIcon(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),
|
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),
|
BadgeIcon = GetSecurityBadgeIcon(requiresAudit: false, hasAudit, hasHashMismatch: false, isBlocked: false, canOverride: false),
|
||||||
Headline = TB("This assistant is currently unlocked."),
|
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()),
|
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 PluginAssistants Plugin { get; init; } = null!;
|
||||||
public PluginAssistantAudit? Audit { get; init; }
|
public PluginAssistantAudit? Audit { get; init; }
|
||||||
|
public DataAssistantPluginEnterpriseApproval? EnterpriseApproval { get; init; }
|
||||||
public DataAssistantPluginAudit Settings { get; init; } = new();
|
public DataAssistantPluginAudit Settings { get; init; } = new();
|
||||||
|
public PluginAssistantSecurityStatusSource Source { get; init; } = PluginAssistantSecurityStatusSource.NONE;
|
||||||
public string CurrentHash { get; init; } = string.Empty;
|
public string CurrentHash { get; init; } = string.Empty;
|
||||||
public bool HasAudit => this.Audit is not null;
|
public bool HasAudit => this.Audit is not null;
|
||||||
|
public bool IsEnterpriseApproved => this.Source is PluginAssistantSecurityStatusSource.ENTERPRISE_APPROVAL;
|
||||||
public bool HashMatches { get; init; }
|
public bool HashMatches { get; init; }
|
||||||
public bool HasHashMismatch { get; init; }
|
public bool HasHashMismatch { get; init; }
|
||||||
public bool IsBelowMinimum { get; init; }
|
public bool IsBelowMinimum { get; init; }
|
||||||
@ -32,6 +35,9 @@ public sealed class PluginAssistantSecurityState
|
|||||||
public Color AvailabilityColor { get; init; } = Color.Info;
|
public Color AvailabilityColor { get; init; } = Color.Info;
|
||||||
public string AvailabilityIcon { get; init; } = MudBlazor.Icons.Material.Filled.Lock;
|
public string AvailabilityIcon { get; init; } = MudBlazor.Icons.Material.Filled.Lock;
|
||||||
public string StatusLabel { get; init; } = string.Empty;
|
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 Headline { get; init; } = string.Empty;
|
||||||
public string Description { get; init; } = string.Empty;
|
public string Description { get; init; } = string.Empty;
|
||||||
public Color StatusColor { get; init; } = Color.Info;
|
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;
|
||||||
using AIStudio.Tools.PluginSystem.Assistants.DataModel.Layout;
|
using AIStudio.Tools.PluginSystem.Assistants.DataModel.Layout;
|
||||||
using Lua;
|
using Lua;
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using AssistantPluginHash = SharedTools.AssistantPluginHash;
|
||||||
|
|
||||||
namespace AIStudio.Tools.PluginSystem.Assistants;
|
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
|
/// sequence of relative path length, relative path, content length, and content
|
||||||
/// for each file in ordinal path order.
|
/// for each file in ordinal path order.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ComputeAuditHash()
|
public string ComputeAuditHash() => AssistantPluginHash.Compute(this.PluginPath);
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BuildSecureSystemPrompt(string pluginSystemPrompt)
|
private static string BuildSecureSystemPrompt(string pluginSystemPrompt)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
using System.Globalization;
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
using AIStudio.Settings.DataModel;
|
using AIStudio.Settings.DataModel;
|
||||||
using AIStudio.Tools.Services;
|
using AIStudio.Tools.Services;
|
||||||
@ -205,6 +206,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
|||||||
// Config: data source security settings
|
// Config: data source security settings
|
||||||
ManagedConfiguration.TryProcessConfiguration(x => x.DataSourceSecurity, x => x.TrustedProviderIds, this.Id, settingsTable, dryRun);
|
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:
|
// Handle configured LLM providers:
|
||||||
PluginConfigurationObject.TryParse(PluginConfigurationObjectType.LLM_PROVIDER, x => x.Providers, x => x.NextProviderNum, mainTable, this.Id, ref this.configObjects, dryRun);
|
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;
|
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)
|
private void TryReadMandatoryInfos(LuaTable mainTable)
|
||||||
{
|
{
|
||||||
if (!mainTable.TryGetValue("MANDATORY_INFOS", out var mandatoryInfosValue) || !mandatoryInfosValue.TryRead<LuaTable>(out var mandatoryInfosTable))
|
if (!mainTable.TryGetValue("MANDATORY_INFOS", out var mandatoryInfosValue) || !mandatoryInfosValue.TryRead<LuaTable>(out var mandatoryInfosTable))
|
||||||
|
|||||||
@ -320,6 +320,10 @@ public static partial class PluginFactory
|
|||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AssistantPluginAudit, x => x.AutomaticallyAuditAssistants, AVAILABLE_PLUGINS))
|
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AssistantPluginAudit, x => x.AutomaticallyAuditAssistants, AVAILABLE_PLUGINS))
|
||||||
wasConfigurationChanged = true;
|
wasConfigurationChanged = true;
|
||||||
|
|
||||||
|
// Check enterprise-managed assistant plugin approvals
|
||||||
|
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AssistantPluginAudit, x => x.EnterpriseApprovedPlugins, AVAILABLE_PLUGINS))
|
||||||
|
wasConfigurationChanged = true;
|
||||||
|
|
||||||
if (wasConfigurationChanged)
|
if (wasConfigurationChanged)
|
||||||
{
|
{
|
||||||
await SettingsManagerAccess.StoreSettings();
|
await SettingsManagerAccess.StoreSettings();
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Settings.DataModel;
|
||||||
|
using AIStudio.Tools.PluginSystem.Assistants;
|
||||||
|
|
||||||
namespace AIStudio.Tools.PluginSystem;
|
namespace AIStudio.Tools.PluginSystem;
|
||||||
|
|
||||||
@ -79,11 +82,36 @@ public static partial class PluginFactory
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LogAssistantPluginStartupState();
|
||||||
|
|
||||||
// Inform all components that the plugins have been reloaded or started:
|
// Inform all components that the plugins have been reloaded or started:
|
||||||
await MessageBus.INSTANCE.SendMessage<bool>(null, Event.PLUGINS_RELOADED);
|
await MessageBus.INSTANCE.SendMessage<bool>(null, Event.PLUGINS_RELOADED);
|
||||||
return configObjects;
|
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)
|
private static async Task<PluginBase> Start(IAvailablePlugin meta, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var pluginMainFile = Path.Join(meta.LocalPath, "plugin.lua");
|
var pluginMainFile = Path.Join(meta.LocalPath, "plugin.lua");
|
||||||
|
|||||||
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 LLM providers (self-hosted or cloud providers with encrypted API keys)
|
||||||
- Any number of transcription providers for voice-to-text functionality
|
- Any number of transcription providers for voice-to-text functionality
|
||||||
- Any number of embedding providers for RAG
|
- Any number of embedding providers for RAG
|
||||||
|
- Enterprise hash approvals for assistant plugins
|
||||||
- The update behavior of AI Studio
|
- The update behavior of AI Studio
|
||||||
- Various UI and feature settings (see the example configuration for details)
|
- 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
|
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
|
## 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.
|
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