From dfab48678d9d08cad9e53b1761adb0dad5b0dbda Mon Sep 17 00:00:00 2001
From: Mike Alhayek
Date: Thu, 16 Jul 2026 11:07:11 -0700
Subject: [PATCH 1/2] Add per-profile anti-spam throttle overrides for AI
Profiles
Reshape PromptSecurityProfileSettings to anti-spam throttle-only (max
messages/window, message window, max anonymous sessions/window, anonymous
session window). Each unset value falls back to the site-wide
PromptSecurityOptions default so AI Profiles and templates can raise or
lower quotas per use case. High-level input/output security guards remain
global-only.
- Honor per-profile overrides in DefaultChatRateLimiter (already) and
DefaultChatSessionStartRateLimiter (now); keep guards global in
DefaultPromptSecurityService and SecurityPromptOrchestrationHandler.
- Expose the 4 override fields on the Blazor and MVC AI Profile and
Template create/edit forms.
- Add unit coverage for override (raise/lower), disable, and partial
fallback (one field overridden, the other inherited from the site).
- Update prompt-security and ai-profiles docs plus the changelog.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1ebd38d1-431b-47c2-a8c9-f40236a5df35
---
.../Security/PromptSecurityProfileSettings.cs | 62 ++++----------
.../docs/changelog/v1.0.0.md | 1 +
.../docs/core/ai-profiles.md | 14 +--
.../docs/core/prompt-security.md | 28 +++---
.../DefaultChatSessionStartRateLimiter.cs | 11 ++-
.../Security/DefaultPromptSecurityService.cs | 19 ++---
.../SecurityPromptOrchestrationHandler.cs | 50 ++---------
.../Pages/AI/AIProfiles/Create.razor | 65 +++-----------
.../Components/Pages/AI/AIProfiles/Edit.razor | 65 +++-----------
.../Pages/AI/Templates/Create.razor | 64 +++-----------
.../Components/Pages/AI/Templates/Edit.razor | 64 +++-----------
.../ViewModels/AIProfileViewModel.cs | 48 +++++------
.../ViewModels/AITemplateViewModel.cs | 57 ++++++-------
.../Areas/AI/ViewModels/AIProfileViewModel.cs | 48 +++++------
.../AI/ViewModels/AITemplateViewModel.cs | 46 +++++-----
.../Areas/AI/Views/AIProfile/Create.cshtml | 65 +++-----------
.../Areas/AI/Views/AIProfile/Edit.cshtml | 65 +++-----------
.../Areas/AI/Views/AITemplate/Create.cshtml | 65 +++-----------
.../Areas/AI/Views/AITemplate/Edit.cshtml | 65 +++-----------
.../Security/DefaultChatRateLimiterTests.cs | 31 +++++++
...DefaultChatSessionStartRateLimiterTests.cs | 72 ++++++++++++++++
.../DefaultPromptSecurityServiceTests.cs | 85 +++----------------
22 files changed, 370 insertions(+), 720 deletions(-)
diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Security/PromptSecurityProfileSettings.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Security/PromptSecurityProfileSettings.cs
index fbecfa66..573f0cd4 100644
--- a/src/Abstractions/CrestApps.Core.AI.Abstractions/Security/PromptSecurityProfileSettings.cs
+++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Security/PromptSecurityProfileSettings.cs
@@ -1,68 +1,40 @@
namespace CrestApps.Core.AI.Security;
///
-/// Per-profile settings that control prompt security behavior.
+/// Per-profile anti-spam throttling overrides for an AI Profile.
/// Stored on using profile.WithSettings(new PromptSecurityProfileSettings { ... }).
-/// When present, these settings override the site-level defaults
-/// for the specific profile.
+/// When present, these values override the site-level throttling defaults defined on
+/// for the specific profile, letting each use case raise or lower
+/// its limits. High-level input and output security guards (injection detection, output filtering,
+/// security preamble, input delimiters, blocking threshold, and maximum prompt length) are intentionally
+/// not part of this model; those remain global concerns configured through .
///
public sealed class PromptSecurityProfileSettings
{
///
- /// Gets or sets a value indicating whether the prompt security layer is enabled for this profile.
- /// When , the site-level default is used.
- /// Set to to explicitly disable security for this profile.
- ///
- public bool? IsEnabled { get; set; }
-
- ///
- /// Gets or sets a value indicating whether injection pattern detection is enabled for this profile.
- /// When , the site-level default is used.
- ///
- public bool? EnableInjectionDetection { get; set; }
-
- ///
- /// Gets or sets a value indicating whether output security filtering is enabled for this profile.
- /// When , the site-level default is used.
- ///
- public bool? EnableOutputFiltering { get; set; }
-
- ///
- /// Gets or sets a value indicating whether the hardened security preamble is prepended
- /// to the system prompt for this profile.
- /// When , the site-level default is used.
- ///
- public bool? EnableSecurityPreamble { get; set; }
-
- ///
- /// Gets or sets a value indicating whether user messages are wrapped with boundary
- /// delimiters for this profile.
- /// When , the site-level default is used.
- ///
- public bool? EnableInputDelimiters { get; set; }
-
- ///
- /// Gets or sets the maximum allowed prompt length for this profile.
+ /// Gets or sets the maximum number of messages allowed within the rate limit window for this profile.
/// When , the site-level default is used.
+ /// Set to 0 to explicitly disable message rate limiting for this profile.
///
- public int? MaxPromptLength { get; set; }
+ public int? MaxMessagesPerWindow { get; set; }
///
- /// Gets or sets the minimum risk level at which prompts are blocked for this profile.
+ /// Gets or sets the rate limit window duration for this profile.
/// When , the site-level default is used.
///
- public PromptRiskLevel? BlockingThreshold { get; set; }
+ public TimeSpan? RateLimitWindow { get; set; }
///
- /// Gets or sets the maximum number of messages allowed within the rate limit window for this profile.
+ /// Gets or sets the maximum number of anonymous chat sessions that can be started
+ /// within the anonymous session rate-limit window for this profile.
/// When , the site-level default is used.
- /// Set to 0 to explicitly disable rate limiting for this profile.
+ /// Set to 0 to explicitly disable anonymous session-start throttling for this profile.
///
- public int? MaxMessagesPerWindow { get; set; }
+ public int? MaxAnonymousSessionsPerWindow { get; set; }
///
- /// Gets or sets the rate limit window duration for this profile.
+ /// Gets or sets the anonymous session-start rate-limit window duration for this profile.
/// When , the site-level default is used.
///
- public TimeSpan? RateLimitWindow { get; set; }
+ public TimeSpan? AnonymousSessionRateLimitWindow { get; set; }
}
diff --git a/src/CrestApps.Core.Docs/docs/changelog/v1.0.0.md b/src/CrestApps.Core.Docs/docs/changelog/v1.0.0.md
index 1aa63b90..33687dbc 100644
--- a/src/CrestApps.Core.Docs/docs/changelog/v1.0.0.md
+++ b/src/CrestApps.Core.Docs/docs/changelog/v1.0.0.md
@@ -15,6 +15,7 @@ description: Initial standalone release notes for the CrestApps.Core repository.
- ships the shared abstractions, infrastructure, AI runtime, provider integrations, and protocol packages under the `CrestApps.Core` name
- assigns stable first-party anonymous visitor IDs to AI chat sessions, uses those visitor IDs for unique-visitor analytics, makes remote-address capture configurable with privacy-first hashed defaults plus optional plain-text or encrypted-at-rest storage, protects anonymous chat session starts with shared ASP.NET Core plus hub-level rate limiting, and documents how hosts can tune thresholds or replace the default endpoint policy with their own `Microsoft.AspNetCore.RateLimiting` policy
+- lets AI Profiles and AI Profile-source templates override the site-wide anti-spam throttle limits through `PromptSecurityProfileSettings` (max messages per window, message window, max anonymous sessions per window, and anonymous session window), with each unset value falling back to the `PromptSecurityOptions` site default so profiles can raise or lower quotas per use case; both the message and anonymous session-start limiters honor the overrides, while high-level input and output security guards (injection detection, output filtering, security preamble, input delimiters, blocking threshold, and maximum prompt length) remain global-only concerns
- defers AI Profile initial-prompt persistence until the first real user prompt arrives and stops the sample chat widgets from auto-creating empty sessions on initial page load
- includes a reference MVC host and an Aspire host for local composition and testing
- includes a dedicated `CrestApps.Core.Tests` project for framework validation
diff --git a/src/CrestApps.Core.Docs/docs/core/ai-profiles.md b/src/CrestApps.Core.Docs/docs/core/ai-profiles.md
index ce5fa4dc..61e03b9b 100644
--- a/src/CrestApps.Core.Docs/docs/core/ai-profiles.md
+++ b/src/CrestApps.Core.Docs/docs/core/ai-profiles.md
@@ -127,15 +127,17 @@ Profiles can opt into user memory so experiences can carry durable context forwa
That toggle is stored directly as `MemoryMetadata`, so profile and template consumers read and write one shared metadata shape instead of carrying legacy memory-setting aliases forward.
-### 8. Prompt security
+### 8. Anti-spam throttling
-Profiles can also override the site-level prompt security defaults through `PromptSecurityProfileSettings`.
+Profiles can override the site-level anti-spam throttle limits through `PromptSecurityProfileSettings`.
-That lets you keep a strong global baseline while adjusting individual profile behavior for cases such as:
+That lets you keep a strong global baseline while raising or lowering throttle quotas for individual profiles, for cases such as:
-- stricter blocking for high-sensitivity assistants
-- longer prompt limits for carefully managed internal workflows
-- disabling the security layer for intentionally operator-controlled profiles
+- tighter per-minute message limits for public, unauthenticated widgets
+- higher limits for carefully managed internal or authenticated workflows
+- adjusting anonymous session-start quotas for a specific use case
+
+Only anti-spam throttling is per-profile. High-level input and output security guards (injection detection, output filtering, security preamble, input delimiters, blocking threshold, and maximum prompt length) remain global-only and are configured through `PromptSecurityOptions`.
See [Prompt Security](./prompt-security.md) for the full option set, scoring model, and limitations.
diff --git a/src/CrestApps.Core.Docs/docs/core/prompt-security.md b/src/CrestApps.Core.Docs/docs/core/prompt-security.md
index 125730b0..e89df9d5 100644
--- a/src/CrestApps.Core.Docs/docs/core/prompt-security.md
+++ b/src/CrestApps.Core.Docs/docs/core/prompt-security.md
@@ -149,22 +149,20 @@ The prompt security layer is controlled by `PromptSecurityOptions`.
The framework supports two layers of configuration:
1. **site-level defaults** through `PromptSecurityOptions`
-2. **per-profile overrides** through `PromptSecurityProfileSettings`
+2. **per-profile anti-spam throttle overrides** through `PromptSecurityProfileSettings`
-Both MVC and Blazor sample hosts expose the site defaults in admin settings, and AI Profiles plus AI Profile-source templates can override the default behavior.
+Both MVC and Blazor sample hosts expose the site defaults in admin settings. AI Profiles and AI Profile-source templates can override the anti-spam throttle limits so each use case can raise or lower its quotas.
-Per-profile overrides support:
+Per-profile overrides are intentionally scoped to anti-spam throttling only:
-- enabling or disabling the prompt security layer
-- enabling or disabling injection detection
-- enabling or disabling output filtering
-- enabling or disabling the security preamble
-- enabling or disabling input delimiters
-- overriding maximum prompt length
-- overriding the blocking threshold
-- overriding rate limit settings (messages per window, window duration)
+- `MaxMessagesPerWindow` — maximum messages allowed within the message window
+- `RateLimitWindow` — the message sliding-window duration
+- `MaxAnonymousSessionsPerWindow` — maximum anonymous session starts within the session window
+- `AnonymousSessionRateLimitWindow` — the anonymous session-start window duration
-That model lets you keep strong defaults globally while allowing carefully chosen profiles to be more permissive or more strict.
+High-level input and output security guards — injection detection, output filtering, the security preamble, input delimiters, the blocking threshold, and the maximum prompt length — remain **global concerns** configured only through `PromptSecurityOptions`. They are deliberately not overridable per profile so protective posture stays consistent across every profile.
+
+That model lets you keep strong global guards while allowing carefully chosen profiles to be more permissive or more strict on throttling.
## Rate limiting
@@ -196,17 +194,19 @@ The default limiter behavior is privacy-first but configurable through `AIChatRa
### Per-profile override
-Rate limiting can be customized per AI Profile using `PromptSecurityProfileSettings`:
+Anti-spam throttling can be customized per AI Profile (or AI Profile-source template) using `PromptSecurityProfileSettings`. Any field left `null` falls back to the site-level default:
```csharp
profile.WithSettings(new PromptSecurityProfileSettings
{
MaxMessagesPerWindow = 10,
RateLimitWindow = TimeSpan.FromMinutes(2),
+ MaxAnonymousSessionsPerWindow = 3,
+ AnonymousSessionRateLimitWindow = TimeSpan.FromMinutes(10),
});
```
-Set `MaxMessagesPerWindow` to `0` on a profile to disable rate limiting for that profile even when site-level rate limiting is enabled.
+Both the message throttle (`DefaultChatRateLimiter`) and the anonymous session-start throttle (`DefaultChatSessionStartRateLimiter`) honor these overrides, falling back to the site defaults on `PromptSecurityOptions` when a value is not set. Set `MaxMessagesPerWindow` (or `MaxAnonymousSessionsPerWindow`) to `0` on a profile to disable that throttle for the profile even when site-level rate limiting is enabled.
### How it works
diff --git a/src/Primitives/CrestApps.Core.AI/Security/DefaultChatSessionStartRateLimiter.cs b/src/Primitives/CrestApps.Core.AI/Security/DefaultChatSessionStartRateLimiter.cs
index 41275a6a..ddac5b55 100644
--- a/src/Primitives/CrestApps.Core.AI/Security/DefaultChatSessionStartRateLimiter.cs
+++ b/src/Primitives/CrestApps.Core.AI/Security/DefaultChatSessionStartRateLimiter.cs
@@ -1,4 +1,5 @@
using System.Collections.Concurrent;
+using CrestApps.Core.AI.Models;
using CrestApps.Core.Support;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -7,7 +8,8 @@ namespace CrestApps.Core.AI.Security;
///
/// Default implementation of that limits
-/// anonymous session creation using a sliding window.
+/// anonymous session creation using a sliding window. Per-profile overrides on
+/// take precedence over the site-level defaults.
///
public sealed class DefaultChatSessionStartRateLimiter : IChatSessionStartRateLimiter
{
@@ -50,7 +52,11 @@ public ValueTask EvaluateAsync(PromptSecurityContext context, C
}
var options = _options.Value;
- var maxSessions = options.MaxAnonymousSessionsPerWindow;
+
+ // Resolve per-profile anti-spam overrides, falling back to site-level defaults.
+ var profileSettings = context.Profile?.TryGetSettings(out var ps) == true ? ps : null;
+ var maxSessions = profileSettings?.MaxAnonymousSessionsPerWindow ?? options.MaxAnonymousSessionsPerWindow;
+ var window = profileSettings?.AnonymousSessionRateLimitWindow ?? options.AnonymousSessionRateLimitWindow;
if (maxSessions <= 0)
{
@@ -64,7 +70,6 @@ public ValueTask EvaluateAsync(PromptSecurityContext context, C
return ValueTask.FromResult(RateLimitResult.Allowed);
}
- var window = options.AnonymousSessionRateLimitWindow;
var now = _timeProvider.GetUtcNow();
var windowStart = now - window;
diff --git a/src/Primitives/CrestApps.Core.AI/Security/DefaultPromptSecurityService.cs b/src/Primitives/CrestApps.Core.AI/Security/DefaultPromptSecurityService.cs
index 00eb3b8c..9eabd843 100644
--- a/src/Primitives/CrestApps.Core.AI/Security/DefaultPromptSecurityService.cs
+++ b/src/Primitives/CrestApps.Core.AI/Security/DefaultPromptSecurityService.cs
@@ -7,7 +7,8 @@ namespace CrestApps.Core.AI.Security;
///
/// Default implementation of that validates user prompts
/// against known prompt injection patterns and configurable security policies.
-/// Respects per-profile security settings that override site-level defaults.
+/// Security guards are governed by the site-level , while the
+/// injected rate limiter honors per-profile anti-spam throttle overrides.
///
public sealed class DefaultPromptSecurityService : IPromptSecurityService
{
@@ -50,16 +51,8 @@ public async Task ValidateInputAsync(PromptSecurityContext
var siteOptions = _options.Value;
- // Resolve per-profile security settings.
- var profileSettings = context.Profile?.TryGetSettings(out var ps) == true ? ps : null;
-
- // Check if security is entirely disabled for this profile.
- if (profileSettings?.IsEnabled == false)
- {
- return PromptSecurityResult.Safe;
- }
-
// Rate limit check (before expensive regex evaluation).
+ // The rate limiter honors per-profile anti-spam throttle overrides.
var rateLimitResult = await _rateLimiter.EvaluateAsync(context, cancellationToken);
if (rateLimitResult.IsThrottled)
@@ -77,7 +70,7 @@ public async Task ValidateInputAsync(PromptSecurityContext
return rateLimitBlockedResult;
}
- var injectionDetectionEnabled = profileSettings?.EnableInjectionDetection ?? siteOptions.EnableInjectionDetection;
+ var injectionDetectionEnabled = siteOptions.EnableInjectionDetection;
if (!injectionDetectionEnabled)
{
@@ -89,8 +82,8 @@ public async Task ValidateInputAsync(PromptSecurityContext
return PromptSecurityResult.Safe;
}
- var blockingThreshold = profileSettings?.BlockingThreshold ?? siteOptions.BlockingThreshold;
- var maxPromptLength = profileSettings?.MaxPromptLength ?? siteOptions.MaxPromptLength;
+ var blockingThreshold = siteOptions.BlockingThreshold;
+ var maxPromptLength = siteOptions.MaxPromptLength;
var evaluationContext = new PromptSecurityEvaluationContext
{
OriginalInput = context.Prompt,
diff --git a/src/Primitives/CrestApps.Core.AI/Security/SecurityPromptOrchestrationHandler.cs b/src/Primitives/CrestApps.Core.AI/Security/SecurityPromptOrchestrationHandler.cs
index 7e87dc64..33f7af69 100644
--- a/src/Primitives/CrestApps.Core.AI/Security/SecurityPromptOrchestrationHandler.cs
+++ b/src/Primitives/CrestApps.Core.AI/Security/SecurityPromptOrchestrationHandler.cs
@@ -69,22 +69,16 @@ public async Task BuiltAsync(OrchestrationContextBuiltContext context, Cancellat
// Only apply security to AI Profile-based chats.
// Chat Interactions give the user full control (system prompt, model, MCP)
// so the security layer is not applicable.
- if (context.Resource is not AIProfile profile)
+ if (context.Resource is not AIProfile)
{
return;
}
- // Resolve effective options by merging site-level defaults with per-profile overrides.
- var effectiveOptions = ResolveEffectiveOptions(profile);
-
- // If security is entirely disabled for this profile, skip.
- if (!effectiveOptions.IsEnabled)
- {
- return;
- }
+ // Security guards are governed globally by the site-level options.
+ var siteOptions = _options.Value;
// Prepend the security preamble to the system message.
- if (effectiveOptions.EnableSecurityPreamble)
+ if (siteOptions.EnableSecurityPreamble)
{
var preamble = await _templateService.RenderAsync(SecurityPreambleTemplateId, cancellationToken: cancellationToken);
@@ -113,7 +107,7 @@ public async Task BuiltAsync(OrchestrationContextBuiltContext context, Cancellat
}
// Wrap user message with delimiters to establish clear input boundaries.
- if (effectiveOptions.EnableInputDelimiters && !string.IsNullOrEmpty(context.OrchestrationContext.UserMessage))
+ if (siteOptions.EnableInputDelimiters && !string.IsNullOrEmpty(context.OrchestrationContext.UserMessage))
{
// Sanitize user input by removing any injected delimiter tokens.
var sanitizedMessage = context.OrchestrationContext.UserMessage
@@ -140,38 +134,4 @@ public async Task BuiltAsync(OrchestrationContextBuiltContext context, Cancellat
}
}
}
-
- private EffectiveSecurityOptions ResolveEffectiveOptions(AIProfile profile)
- {
- var siteOptions = _options.Value;
- var profileSettings = profile.TryGetSettings(out var settings) ? settings : null;
-
- return new EffectiveSecurityOptions
- {
- IsEnabled = profileSettings?.IsEnabled ?? true,
- EnableSecurityPreamble = profileSettings?.EnableSecurityPreamble ?? siteOptions.EnableSecurityPreamble,
- EnableInputDelimiters = profileSettings?.EnableInputDelimiters ?? siteOptions.EnableInputDelimiters,
- EnableInjectionDetection = profileSettings?.EnableInjectionDetection ?? siteOptions.EnableInjectionDetection,
- EnableOutputFiltering = profileSettings?.EnableOutputFiltering ?? siteOptions.EnableOutputFiltering,
- MaxPromptLength = profileSettings?.MaxPromptLength ?? siteOptions.MaxPromptLength,
- BlockingThreshold = profileSettings?.BlockingThreshold ?? siteOptions.BlockingThreshold,
- };
- }
-
- private sealed class EffectiveSecurityOptions
- {
- public bool IsEnabled { get; init; }
-
- public bool EnableSecurityPreamble { get; init; }
-
- public bool EnableInputDelimiters { get; init; }
-
- public bool EnableInjectionDetection { get; init; }
-
- public bool EnableOutputFiltering { get; init; }
-
- public int MaxPromptLength { get; init; }
-
- public PromptRiskLevel BlockingThreshold { get; init; }
- }
}
diff --git a/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIProfiles/Create.razor b/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIProfiles/Create.razor
index 1e47239b..8f71193e 100644
--- a/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIProfiles/Create.razor
+++ b/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIProfiles/Create.razor
@@ -1144,73 +1144,34 @@
-
Prompt Security Override
+
Anti-Spam Throttle Override
- Override the site-level prompt security defaults for this profile. Leave options at "Use default" to inherit the global settings.
+ Override the site-level anti-spam throttle limits for this profile. Leave a field empty to inherit the global setting.
-
-
-
-
-
-
-
Override whether the security layer is active for this profile.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
Leave empty to use the site default. Set to 0 to disable message throttling for this profile.
-
-
-
-
-
-
+
+
+
Leave empty to use the site default.
-
-
-
-
-
-
+
+
+
Leave empty to use the site default. Set to 0 to disable anonymous session-start throttling for this profile.
- Override the site-level prompt security defaults for this profile. Leave options at "Use default" to inherit the global settings.
+ Override the site-level anti-spam throttle limits for this profile. Leave a field empty to inherit the global setting.
-
-
-
-
-
-
-
Override whether the security layer is active for this profile.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
Leave empty to use the site default. Set to 0 to disable message throttling for this profile.
-
-
-
-
-
-
+
+
+
Leave empty to use the site default.
-
-
-
-
-
-
+
+
+
Leave empty to use the site default. Set to 0 to disable anonymous session-start throttling for this profile.