Added opt-in sharing of feature usage with self-hosted servers (#1020)
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Verify (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions

Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
Dominic NeuburgandThorsten Sommer authored and GitHub committed 2026-10-04 14:44:16 +02:00
1 parent 90c90910b3
commit 50aa11a34c
27 files changed
+531 -12

No files matched your search

@@ -61,6 +61,11 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
protected abstract string SystemPrompt { get; }
protected abstract Tools.Components Component { get; }
/// <summary>
/// The name of the assistant plugin, which is only set by plugin-provided assistants.
/// </summary>
protected virtual string RuntimeAssistantName => string.Empty;
protected virtual Func<string> Result2Copy => () => this.ResultingContentBlock is null ? string.Empty : this.ResultingContentBlock.Content switch
{
@@ -375,8 +380,9 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
ChatId = Guid.NewGuid(),
Name = string.Format(this.TB("Assistant - {0}"), this.Title),
Blocks = [],
RuntimeComponent = this.Component,
};
this.AssignRuntimeIdentity(this.ChatThread);
}
protected Guid CreateChatThread(Guid workspaceId, string name)
@@ -392,12 +398,26 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
ChatId = chatId,
Name = name,
Blocks = [],
RuntimeComponent = this.Component,
};
this.AssignRuntimeIdentity(this.ChatThread);
return chatId;
}
/// <summary>
/// Sets who runs the given thread: this assistant's component and, for an assistant plugin, its name.
/// </summary>
/// <remarks>
/// Both decide which tools the thread may use and what its requests to self-hosted servers name. Every
/// way an assistant creates or sends a thread goes through here, so that none of them can forget one.
/// </remarks>
/// <param name="chatThread">The thread this assistant runs.</param>
protected void AssignRuntimeIdentity(ChatThread chatThread)
{
chatThread.RuntimeComponent = this.Component;
chatThread.RuntimeAssistantName = this.RuntimeAssistantName;
}
private Task RefreshProviderSelectionFromConfigurationAsync()
{
this.ProviderSettings = this.SettingsManager.GetPreselectedProvider(this.Component, this.ProviderSettings.Id);
@@ -519,7 +539,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
{
this.ChatThread.Blocks.Add(this.ResultingContentBlock);
this.ChatThread.SelectedProvider = this.ProviderSettings.Id;
this.ChatThread.RuntimeComponent = this.Component;
this.AssignRuntimeIdentity(this.ChatThread);
this.ChatThread.SelectedToolIds = [..this.SelectedToolIds];
this.ChatThread.RuntimeSelectedToolIds = this.GetRunnableToolIds(this.ChatThread.RequiredOutboundDataRestriction.Restriction);
this.ChatThread.RuntimeToolsAreAssistantManaged = this.AssistantManagedToolIds is not null;
@@ -604,6 +624,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
this.ChatThread.Blocks.Add(this.ResultingContentBlock);
this.ChatThread.SelectedProvider = this.ProviderSettings.Id;
this.AssignRuntimeIdentity(this.ChatThread);
await this.CheckpointAssistantSession();
await this.AIJobService.TryStartChatGenerationAsync(new ChatGenerationRequest
@@ -110,7 +110,6 @@ public partial class AssistantBatchProcessing
ChatId = Guid.NewGuid(),
Name = this.Title,
Blocks = [],
RuntimeComponent = this.Component,
// A thread of its own, which has read no mailbox yet:
RuntimeSelectedToolIds = this.GetRunnableToolIds(OutboundDataRestriction.UNRESTRICTED),
@@ -120,6 +119,7 @@ public partial class AssistantBatchProcessing
RuntimeToolsAreAssistantManaged = true,
};
this.AssignRuntimeIdentity(chatThread);
var userPrompt = new ContentText
{
Text = BuildUserPrompt(fileName, fileContent),
@@ -46,6 +46,12 @@ public partial class AssistantDynamic : AssistantBaseCore<NoSettingsPanel>
// session and media state separate while ComponentsExtensions derives their defaults from chat.
protected override Tools.Components Component => Tools.Components.DYNAMIC_ASSISTANT;
/// <inheritdoc />
/// <remarks>
/// Empty for an assistant somebody built for themselves, see MayShareAssistantName.
/// </remarks>
protected override string RuntimeAssistantName => this.sharesAssistantName ? this.assistantPlugin?.Name ?? string.Empty : string.Empty;
/// <summary>
/// Gets the plugin ID as the assistant session instance ID.
/// </summary>
@@ -68,6 +74,7 @@ public partial class AssistantDynamic : AssistantBaseCore<NoSettingsPanel>
private PluginAssistantAudit? audit;
private string securityMessage = string.Empty;
private bool isSecurityBlocked;
private bool sharesAssistantName;
private PluginAssistants? pendingChatLauncher;
private const string ASSISTANT_QUERY_KEY = "assistantId";
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
@@ -87,6 +94,7 @@ public partial class AssistantDynamic : AssistantBaseCore<NoSettingsPanel>
private static readonly AssistantSessionStateKey<PluginAssistantAudit?> AUDIT_STATE_KEY = new(nameof(audit));
private static readonly AssistantSessionStateKey<string> SECURITY_MESSAGE_STATE_KEY = new(nameof(securityMessage));
private static readonly AssistantSessionStateKey<bool> IS_SECURITY_BLOCKED_STATE_KEY = new(nameof(isSecurityBlocked));
private static readonly AssistantSessionStateKey<bool> SHARES_ASSISTANT_NAME_STATE_KEY = new(nameof(sharesAssistantName));
private bool CanReviseCurrentAssistant => this.assistantPlugin is { IsInternal: false, IsManagedByConfigServer: false } && !string.IsNullOrWhiteSpace(this.assistantPlugin.PluginPath);
@@ -109,6 +117,7 @@ public partial class AssistantDynamic : AssistantBaseCore<NoSettingsPanel>
state.Set(AUDIT_STATE_KEY, this.audit);
state.Set(SECURITY_MESSAGE_STATE_KEY, this.securityMessage);
state.Set(IS_SECURITY_BLOCKED_STATE_KEY, this.isSecurityBlocked);
state.Set(SHARES_ASSISTANT_NAME_STATE_KEY, this.sharesAssistantName);
}
/// <inheritdoc />
@@ -130,6 +139,7 @@ public partial class AssistantDynamic : AssistantBaseCore<NoSettingsPanel>
state.Restore(AUDIT_STATE_KEY, value => this.audit = value);
state.Restore(SECURITY_MESSAGE_STATE_KEY, value => this.securityMessage = value);
state.Restore(IS_SECURITY_BLOCKED_STATE_KEY, value => this.isSecurityBlocked = value);
state.Restore(SHARES_ASSISTANT_NAME_STATE_KEY, value => this.sharesAssistantName = value);
}
#region Implementation of AssistantBase
@@ -177,6 +187,7 @@ public partial class AssistantDynamic : AssistantBaseCore<NoSettingsPanel>
this.audit = this.SettingsManager.ConfigurationData.AssistantPluginAudits.FirstOrDefault(x => x.PluginId == pluginAssistant.Id && x.PluginHash == pluginHash);
var securityState = PluginAssistantSecurityResolver.Resolve(this.SettingsManager, pluginAssistant);
this.sharesAssistantName = MayShareAssistantName(pluginAssistant, securityState);
if (!securityState.CanStartAssistant)
{
this.assistantPlugin = pluginAssistant;
@@ -374,6 +385,7 @@ public partial class AssistantDynamic : AssistantBaseCore<NoSettingsPanel>
var securityState = PluginAssistantSecurityResolver.Resolve(this.SettingsManager, updatedPlugin);
this.securityMessage = securityState.CanStartAssistant ? string.Empty : securityState.Description;
this.isSecurityBlocked = !securityState.CanStartAssistant;
this.sharesAssistantName = MayShareAssistantName(updatedPlugin, securityState);
this.assistantState.Clear();
if (this.RootComponent is not null)
@@ -382,6 +394,22 @@ public partial class AssistantDynamic : AssistantBaseCore<NoSettingsPanel>
#endregion
/// <summary>
/// Whether requests may name this assistant plugin, e.g., in the User-Agent for self-hosted servers.
/// </summary>
/// <remarks>
/// Only assistants from AI Studio itself or from the organization are named: those shipped with
/// the app, rolled out by its configuration server, or approved by it. Everybody can build an
/// assistant of their own with the Assistant Builder, and its name may well be personal, such as
/// an application to one particular company. That name stays on the device; the request still
/// names the component, so the operators can count it as an assistant all the same.
/// </remarks>
/// <param name="plugin">The assistant plugin.</param>
/// <param name="securityState">Its security state, which tells whether the organization approved it.</param>
/// <returns>True when its name may be shared; otherwise, false.</returns>
private static bool MayShareAssistantName(PluginAssistants plugin, PluginAssistantSecurityState securityState) =>
plugin.IsInternal || plugin.IsManagedByConfigServer || securityState.IsEnterpriseApproved;
/// <summary>
/// Reads the tools this plugin names for its assistant.
/// </summary>
@@ -4723,6 +4723,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1898114759"]
-- Select the language for the app.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1907446663"] = "Select the language for the app."
-- When enabled, requests to self-hosted providers and LiteLLM name the feature that sent them, e.g., the chat, an assistant, or an agent, together with the version of AI Studio and your operating system. This lets the operators of these servers, usually your organization, see which features are used. Cloud providers never receive these details, and AI Studio never sends anything to its developers.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1907749697"] = "When enabled, requests to self-hosted providers and LiteLLM name the feature that sent them, e.g., the chat, an assistant, or an agent, together with the version of AI Studio and your operating system. This lets the operators of these servers, usually your organization, see which features are used. Cloud providers never receive these details, and AI Studio never sends anything to its developers."
-- Your organization has disabled update checks and installations.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1909339369"] = "Your organization has disabled update checks and installations."
@@ -4783,6 +4786,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3138812562"]
-- Spellchecking is enabled
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3165555978"] = "Spellchecking is enabled"
-- Operators of self-hosted servers learn which feature sent a request
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3294604676"] = "Operators of self-hosted servers learn which feature sent a request"
-- External HTTPS certificates
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T348936513"] = "External HTTPS certificates"
@@ -4807,6 +4813,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3652888444"]
-- Show administration settings?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3694781396"] = "Show administration settings?"
-- No usage details are sent
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3702320045"] = "No usage details are sent"
-- Read the Enterprise IT documentation for details.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3705451321"] = "Read the Enterprise IT documentation for details."
@@ -4828,6 +4837,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4067492921"]
-- Show details when suspicious content was removed?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4156872850"] = "Show details when suspicious content was removed?"
-- Share feature usage with operators of self-hosted servers?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4162855418"] = "Share feature usage with operators of self-hosted servers?"
-- Select a transcription provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4174666315"] = "Select a transcription provider"
@@ -72,6 +72,7 @@ internal sealed class StructuredLlmStageRunner(
Name = $"Visual Briefing {stage}",
SystemPrompt = systemPrompt,
SelectedProvider = provider.Id,
RuntimeComponent = Tools.Components.VISUAL_BRIEFING_ASSISTANT,
Blocks =
[
CreateBlock(time, ChatRole.USER, initialPrompt),