Merge branch 'main' into chunk-data

Resolved 29 conflicting files. The notable decisions:

Confidence: main's tool-calling gate (RequiredProviderConfidence) and this
branch's local-RAG gate (DataConfidenceLevel) turned out to be the same rule
on the same axis, so they are now one field. Both tool results and data
sources raise it through RequireProviderConfidence(). The gate checks the
level strictly and no longer exempts providers trusted by configuration:
TrustedProviderIds is documented as applying to data-source security checks
only, and organizations set confidence through DataConfidence
.CustomConfidenceScheme instead. The security axis (DataSecurity, ERI,
IsTrustedForDataSourceSecurityChecks) is unchanged.

Provider creation: main's CreateProvider signature won (hfEndpointKind,
capabilityOverrides, no model parameter); tokenizerPath was added to it and
is set for every provider, including the new Hetzner, IONOS and LiteLLM.
Provider and EmbeddingProvider combine the record parameters, Lua parsing and
Lua serialization of both sides.

File types: main's hierarchy (ODT leaf, WORD parent, PowerPoint without the
legacy .ppt, TABULAR instead of DELIMITED_TABLE) plus this branch's
SPREADSHEET parent with ODS and the xlsm/xlsb/xla/xlam extensions, which the
runtime already reads. Both sides had added a conflicting HTML filter; the
reading family keeps the name, and the export path uses a narrow
HTML_DOCUMENT, following the existing LATEX/TEX split.

Runtime: main's file_data.rs is the base, including the prompt-injection
sanitizer and the extraction routes. Token counting and chunk segmentation
moved into take_released, so they act on the text the filter has released
rather than on text it is still holding. A failed count is logged and left
out instead of ending the extraction, because the app counts such a segment
itself.

Data sources: the participating-provider checks of this branch are kept, and
main's GetAllowedDataSources overload now builds on them. DirectChatService
resolves the launched chat's data source options before the check, so filter
and chat see the same options.

.NET and Rust both build clean; I18N regenerated to 4060 keys.
This commit is contained in:
Thorsten Sommer committed 2026-09-05 21:20:33 +02:00
commit fe35630eff
640 files changed
+45059 -4952

No files matched your search

@@ -63,11 +63,41 @@ public sealed class Data
/// </summary>
public Dictionary<string, ManagedEditableDefaultState> ManagedEditableDefaults { get; set; } = [];
/// <summary>
/// The configuration plugin that owns each locked managed setting.
/// </summary>
public Dictionary<string, Guid> ManagedLockedConfigurations { get; set; } = [];
/// <summary>
/// The value each managed setting had before a configuration plugin took it over, as JSON.
/// </summary>
/// <remarks>
/// A configuration plugin might be removed later, e.g. when a test configuration ends or when an
/// organization withdraws its configuration. The value the user had chosen before belongs to the
/// user, so we keep it here and restore it instead of falling back to the app's default value.
/// The snapshot is taken once, when a setting becomes managed, and is consumed when no
/// configuration plugin manages that setting anymore.
/// </remarks>
public Dictionary<string, string> ManagedUserValueSnapshots { get; set; } = [];
/// <summary>
/// Cached audit results for assistant plugins.
/// </summary>
public List<PluginAssistantAudit> AssistantPluginAudits { get; set; } = [];
/// <summary>
/// The assistant plugin hashes whose organization default for the activation was already applied.
/// </summary>
/// <remarks>
/// An organization may enable an assistant plugin it approved while still letting the user switch
/// it off again. That is a default, not a rule, so it must be applied exactly once: applying it on
/// every start would keep switching the assistant back on against the user's decision. We remember
/// the hashes it was applied for, and forget one as soon as no approval asks for it anymore, so a
/// later rollout of the same plugin takes effect again. Activations the user may not override are
/// not listed here: those are decided live and never touch the list of enabled plugins.
/// </remarks>
public List<string> AppliedEnterpriseAssistantActivations { get; set; } = [];
/// <summary>
/// The next provider number to use.
/// </summary>
@@ -119,6 +149,11 @@ public sealed class Data
public DataDocumentAnalysis DocumentAnalysis { get; init; } = new();
/// <summary>
/// Gets the managed Batch Processing Assistant defaults.
/// </summary>
public DataBatchProcessing BatchProcessing { get; init; } = new(x => x.BatchProcessing);
public DataMandatoryInformation MandatoryInformation { get; init; } = new();
public DataTextSummarizer TextSummarizer { get; init; } = new();
@@ -159,4 +194,6 @@ public sealed class Data
public DataBiasOfTheDay BiasOfTheDay { get; init; } = new();
public DataI18N I18N { get; init; } = new();
public DataTools Tools { get; init; } = new(x => x.Tools);
}
@@ -57,6 +57,11 @@ public sealed class DataApp(Expression<Func<Data, DataApp>>? configSelection = n
/// </summary>
public StartPage StartPage { get; set; } = ManagedConfiguration.Register(configSelection, n => n.StartPage, StartPage.HOME);
/// <summary>
/// Whether an alert dialog should be shown when prompt-injection content is blocked.
/// </summary>
public bool ShowPromptInjectionAlert { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ShowPromptInjectionAlert, true);
/// <summary>
/// Should the built-in introduction be visible on the home page?
/// </summary>
@@ -148,7 +153,27 @@ public sealed class DataApp(Expression<Func<Data, DataApp>>? configSelection = n
/// Should the user be allowed to add providers?
/// </summary>
public bool AllowUserToAddProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AllowUserToAddProvider, true);
/// <summary>
/// Should the user be allowed to import plugin archives from disk?
/// </summary>
public bool AllowUserToImportPlugins { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AllowUserToImportPlugins, true);
/// <summary>
/// Should the user be allowed to import configuration plugin archives from disk?
/// </summary>
/// <remarks>
/// This is a second gate on top of AllowUserToImportPlugins, and both must allow the import.
/// Configuration plugins deserve their own switch because they are far more powerful than an
/// assistant: they define LLM providers and data sources, and they lock settings.
/// </remarks>
public bool AllowUserToImportConfigurationPlugins { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AllowUserToImportConfigurationPlugins, true);
/// <summary>
/// Should the user be allowed to share or export plugins as archives?
/// </summary>
public bool AllowUserToSharePlugins { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AllowUserToSharePlugins, true);
/// <summary>
/// Should administration settings be visible in the UI?
/// </summary>
@@ -10,4 +10,27 @@ public sealed class DataAssistantPluginEnterpriseApproval
public string Comment { get; init; } = string.Empty;
public string ApprovedBy { get; init; } = string.Empty;
public DateTimeOffset? ApprovedAtUtc { get; init; }
/// <summary>
/// Whether the organization wants this assistant plugin to be enabled, instead of leaving that
/// to the user.
/// </summary>
/// <remarks>
/// An approval only ever states that a plugin is safe. Enabling it is a separate decision, and
/// without this field it stays with the user: a rolled-out assistant is approved, but every
/// colleague still has to switch it on. This field is how an organization makes that decision
/// instead.
/// </remarks>
public bool Activate { get; init; }
/// <summary>
/// Whether the user may switch an assistant plugin the organization activated off again.
/// </summary>
/// <remarks>
/// This follows the AllowUserOverride convention of every managed setting: without it, what the
/// organization set is locked; with it, the organization only provides a default the user may
/// change. It has no meaning of its own while Activate is false, because there is nothing to
/// override then.
/// </remarks>
public bool AllowUserOverride { get; init; }
}
@@ -0,0 +1,72 @@
using System.Linq.Expressions;
using AIStudio.Assistants.BatchProcessing;
using AIStudio.Provider;
namespace AIStudio.Settings.DataModel;
/// <summary>
/// Stores managed defaults for the Batch Processing Assistant.
/// </summary>
/// <param name="configSelection">The managed-configuration selector.</param>
public sealed class DataBatchProcessing(Expression<Func<Data, DataBatchProcessing>>? configSelection = null)
{
public const string DEFAULT_FILE_PATTERNS = "*.pdf;*.docx;*.pptx;*.xlsx;*.md;*.txt;*.mp3;*.wav;*.wave;*.aac;*.flac;*.ogg;*.opus;*.m4a;*.m4b;*.wma;*.alac;*.aif;*.aiff;*.caf;*.mp4;*.m4v;*.avi;*.mkv;*.mov;*.wmv;*.flv;*.webm";
public const int MIN_DELAY_SECONDS = 6;
public const int MAX_DELAY_SECONDS = 300;
public const int DEFAULT_MIN_DELAY_SECONDS = 6;
public const int DEFAULT_MAX_DELAY_SECONDS = 10;
/// <summary>
/// Initializes an unmanaged Batch Processing settings instance.
/// </summary>
public DataBatchProcessing() : this(null)
{
}
public bool PreselectOptions { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PreselectOptions, false);
public string InputDirectory { get; set; } = ManagedConfiguration.Register(configSelection, value => value.InputDirectory, string.Empty);
public string OutputDirectory { get; set; } = ManagedConfiguration.Register(configSelection, value => value.OutputDirectory, string.Empty);
public string FilePatterns { get; set; } = ManagedConfiguration.Register(configSelection, value => value.FilePatterns, DEFAULT_FILE_PATTERNS);
public bool IncludeSubdirectories { get; set; } = ManagedConfiguration.Register(configSelection, value => value.IncludeSubdirectories, false);
public BatchProcessingPromptSource PromptSource { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PromptSource, BatchProcessingPromptSource.FREE_PROMPT);
public string FreePrompt { get; set; } = ManagedConfiguration.Register(configSelection, value => value.FreePrompt, string.Empty);
public string PromptFilePath { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PromptFilePath, string.Empty);
public string PreselectedPolicyId { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PreselectedPolicyId, string.Empty);
public BatchProcessingOutputMode OutputMode { get; set; } = ManagedConfiguration.Register(configSelection, value => value.OutputMode, BatchProcessingOutputMode.INDIVIDUAL_FILES);
/// <summary>
/// The file format of the individual result files, one per processed document.
/// </summary>
/// <remarks>
/// Only formats which hold an entire answer, see FileExportFormatExtensions.ANSWER_FORMATS.
/// The tabular formats belong to the output mode TABLE_ONLY, which writes one table for the
/// whole run instead of one file per document.
/// </remarks>
public FileExportFormat ResultFileFormat { get; set; } = ManagedConfiguration.Register(configSelection, value => value.ResultFileFormat, FileExportFormat.MARKDOWN);
public string CsvFileName { get; set; } = ManagedConfiguration.Register(configSelection, value => value.CsvFileName, string.Empty);
public string ResultColumnHeader { get; set; } = ManagedConfiguration.Register(configSelection, value => value.ResultColumnHeader, string.Empty);
public BatchProcessingCsvSeparator CsvSeparator { get; set; } = ManagedConfiguration.Register(configSelection, value => value.CsvSeparator, BatchProcessingCsvSeparator.SEMICOLON);
public string CustomCsvSeparator { get; set; } = ManagedConfiguration.Register(configSelection, value => value.CustomCsvSeparator, string.Empty);
public int MinimumDelaySeconds { get; set; } = ManagedConfiguration.Register(configSelection, value => value.MinimumDelaySeconds, DEFAULT_MIN_DELAY_SECONDS);
public int MaximumDelaySeconds { get; set; } = DEFAULT_MAX_DELAY_SECONDS;
public ConfidenceLevel MinimumProviderConfidence { get; set; } = ManagedConfiguration.Register(configSelection, value => value.MinimumProviderConfidence, ConfidenceLevel.NONE);
public string PreselectedProvider { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PreselectedProvider, string.Empty);
}
@@ -57,6 +57,19 @@ public sealed record DataDocumentAnalysisPolicy : ConfigurationBaseObject
/// The minimum confidence level required for a provider to be considered.
/// </summary>
public ConfidenceLevel MinimumProviderConfidence { get; set; } = ConfidenceLevel.NONE;
/// <summary>
/// The tools this policy permits the model to use.
/// </summary>
/// <remarks>
/// A limit, not a preselection: a tool absent from this list cannot be chosen for a run of this
/// policy. Empty therefore means no tools at all, which is what a policy written before this
/// field existed gets — an analysis keeps working exactly as its author wrote it.<br/><br/>
/// This narrows what the user may pick; it never widens what a tool is allowed to do. Every
/// permitted tool still has to pass the provider confidence checks, so a tool demanding High
/// confidence stays out of reach of a weaker provider whether a policy lists it or not.
/// </remarks>
public HashSet<string> AllowedToolIds { get; set; } = [];
/// <summary>
/// Which LLM provider should be preselected?
@@ -130,6 +143,23 @@ public sealed record DataDocumentAnalysisPolicy : ConfigurationBaseObject
if (table.TryGetValue("HidePolicyDefinition", out var hideValue) && hideValue.TryRead<bool>(out var hide))
hidePolicyDefinition = hide;
//
// Unknown tool IDs are kept rather than rejected: an organization may roll out a policy
// before the plugin providing that tool reaches every workstation. A tool that does not
// exist simply never shows up, and the policy starts working once it does.
//
var allowedToolIds = new HashSet<string>(StringComparer.Ordinal);
if (table.TryGetValue("AllowedToolIds", out var toolIdsValue) && toolIdsValue.TryRead<LuaTable>(out var toolIdsTable))
{
for (var toolIdx = 1; toolIdx <= toolIdsTable.ArrayLength; toolIdx++)
{
if (toolIdsTable[toolIdx].TryRead<string>(out var toolId) && !string.IsNullOrWhiteSpace(toolId))
allowedToolIds.Add(toolId.Trim());
else
LOG.LogWarning("The configured document analysis policy {PolicyIndex} contains an invalid entry in its AllowedToolIds list.", idx);
}
}
policy = new DataDocumentAnalysisPolicy
{
Id = id.ToString(),
@@ -139,6 +169,7 @@ public sealed record DataDocumentAnalysisPolicy : ConfigurationBaseObject
AnalysisRules = analysisRules,
OutputRules = outputRules,
MinimumProviderConfidence = minimumConfidence,
AllowedToolIds = allowedToolIds,
PreselectedProvider = preselectedProvider,
PreselectedProfile = preselectedProfile,
HidePolicyDefinition = hidePolicyDefinition,
@@ -0,0 +1,67 @@
using System.Linq.Expressions;
namespace AIStudio.Settings.DataModel;
public sealed class DataTools(Expression<Func<Data, DataTools>>? configSelection = null)
{
public DataTools() : this(null)
{
}
/// <summary>
/// The settings the user entered per tool: tool ID, then field name.
/// </summary>
public Dictionary<string, Dictionary<string, string>> Settings { get; set; } = [];
public Dictionary<string, HashSet<string>> DefaultToolIdsByComponent { get; set; } = [];
public HashSet<string> VisibleToolSelectionComponents { get; set; } = [];
public bool EnableTools { get; set; } = ManagedConfiguration.Register(
configSelection,
x => x.EnableTools,
true);
public HashSet<string> DisabledToolIds { get; set; } = ManagedConfiguration.Register(
configSelection,
x => x.DisabledToolIds,
[]);
public Dictionary<string, string> MinimumProviderConfidenceByToolId { get; set; } = ManagedConfiguration.Register(
configSelection,
x => x.MinimumProviderConfidenceByToolId,
new Dictionary<string, string>(StringComparer.Ordinal));
/// <summary>
/// Tool settings an organization fixed, which the user cannot change. Keys are
/// "toolId.fieldName".
/// </summary>
/// <remarks>
/// Keyed by tool and field rather than held in a property per setting, because a property per
/// setting only works for the tools AI Studio ships. Tools defined by plugin authors are not
/// known at compile time, yet an organization has to be able to configure them the same way.
/// <br/><br/>
/// A secret field travels here too, but only encrypted with the enterprise secret, in the
/// same "ENC:v1:" form the providers use for their API keys. What is stored is therefore
/// ciphertext, worthless without a secret that lives outside every deployed file. A plaintext
/// secret is refused rather than used, and a secret is never accepted as a pre-filled default
/// — see the tool settings service for both rules.
/// </remarks>
public Dictionary<string, string> LockedToolSettings { get; set; } = ManagedConfiguration.Register(
configSelection,
x => x.LockedToolSettings,
new Dictionary<string, string>(StringComparer.Ordinal));
/// <summary>
/// Tool settings an organization pre-filled but left changeable. Keys are "toolId.fieldName".
/// </summary>
/// <remarks>
/// Applies until the user saves a value of their own, which then wins. That is the difference
/// to the locked settings above, and the reason both exist: an organization can fix the search
/// instance while leaving the timeouts to the user.
/// </remarks>
public Dictionary<string, string> DefaultToolSettings { get; set; } = ManagedConfiguration.Register(
configSelection,
x => x.DefaultToolSettings,
new Dictionary<string, string>(StringComparer.Ordinal));
}