mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-06-28 20:56:28 +00:00
Adding security features
This commit is contained in:
parent
b55663ef62
commit
84a9f88f8b
@ -676,12 +676,13 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
ToolCalls = responseMessage.ToolCalls,
|
ToolCalls = responseMessage.ToolCalls,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var maxToolCalls = 30;
|
||||||
foreach (var toolCall in responseMessage.ToolCalls)
|
foreach (var toolCall in responseMessage.ToolCalls)
|
||||||
{
|
{
|
||||||
toolCallCount++;
|
toolCallCount++;
|
||||||
if (toolCallCount > 10)
|
if (toolCallCount > maxToolCalls)
|
||||||
{
|
{
|
||||||
var limitMessage = "Tool calling stopped because the maximum of 10 tool calls was reached.";
|
var limitMessage = $"Tool calling stopped because the maximum of {maxToolCalls} tool calls was reached.";
|
||||||
currentAssistantContent.ToolInvocations.Add(new ToolInvocationTrace
|
currentAssistantContent.ToolInvocations.Add(new ToolInvocationTrace
|
||||||
{
|
{
|
||||||
Order = toolCallCount,
|
Order = toolCallCount,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
|
using System.Text;
|
||||||
using HtmlAgilityPack;
|
using HtmlAgilityPack;
|
||||||
using ReverseMarkdown;
|
using ReverseMarkdown;
|
||||||
|
|
||||||
@ -10,6 +11,7 @@ public sealed class HTMLParser
|
|||||||
{
|
{
|
||||||
private const string USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) MindWorkAIStudio/1.0";
|
private const string USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) MindWorkAIStudio/1.0";
|
||||||
private const int MAX_REDIRECTS = 10;
|
private const int MAX_REDIRECTS = 10;
|
||||||
|
private const int DEFAULT_MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
|
||||||
|
|
||||||
private static readonly Config MARKDOWN_PARSER_CONFIG = new()
|
private static readonly Config MARKDOWN_PARSER_CONFIG = new()
|
||||||
{
|
{
|
||||||
@ -42,7 +44,7 @@ public sealed class HTMLParser
|
|||||||
return innerHtml;
|
return innerHtml;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<HTMLParserWebPage> LoadWebPageAsync(Uri url, CancellationToken token = default, int timeoutSeconds = 30, Func<Uri, CancellationToken, Task<IReadOnlyList<IPAddress>>>? resolveUrlAddressesAsync = null)
|
public async Task<HTMLParserWebPage> LoadWebPageAsync(Uri url, CancellationToken token = default, int timeoutSeconds = 30, Func<Uri, CancellationToken, Task<IReadOnlyList<IPAddress>>>? resolveUrlAddressesAsync = null, int maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES)
|
||||||
{
|
{
|
||||||
using var handler = new SocketsHttpHandler
|
using var handler = new SocketsHttpHandler
|
||||||
{
|
{
|
||||||
@ -89,7 +91,7 @@ public sealed class HTMLParser
|
|||||||
throw new HttpRequestException($"The server returned HTTP {statusCode} ({reasonPhrase}) for '{currentUrl}'.", null, response.StatusCode);
|
throw new HttpRequestException($"The server returned HTTP {statusCode} ({reasonPhrase}) for '{currentUrl}'.", null, response.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
var html = await response.Content.ReadAsStringAsync(timeoutCts.Token);
|
var html = await ReadContentAsStringWithLimitAsync(response.Content, maxResponseBytes, timeoutCts.Token);
|
||||||
var document = new HtmlDocument();
|
var document = new HtmlDocument();
|
||||||
document.LoadHtml(html);
|
document.LoadHtml(html);
|
||||||
|
|
||||||
@ -178,6 +180,46 @@ public sealed class HTMLParser
|
|||||||
|
|
||||||
private static bool IsRedirect(HttpStatusCode statusCode) => (int)statusCode is >= 300 and <= 399;
|
private static bool IsRedirect(HttpStatusCode statusCode) => (int)statusCode is >= 300 and <= 399;
|
||||||
|
|
||||||
|
private static async Task<string> ReadContentAsStringWithLimitAsync(HttpContent content, int maxResponseBytes, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (content.Headers.ContentLength is long contentLength && contentLength > maxResponseBytes)
|
||||||
|
throw new HttpRequestException($"The response body is too large. Maximum allowed size is {maxResponseBytes} bytes.");
|
||||||
|
|
||||||
|
await using var stream = await content.ReadAsStreamAsync(token);
|
||||||
|
await using var buffer = new MemoryStream();
|
||||||
|
var chunk = new byte[8192];
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var read = await stream.ReadAsync(chunk, token);
|
||||||
|
if (read == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (buffer.Length + read > maxResponseBytes)
|
||||||
|
throw new HttpRequestException($"The response body is too large. Maximum allowed size is {maxResponseBytes} bytes.");
|
||||||
|
|
||||||
|
buffer.Write(chunk, 0, read);
|
||||||
|
}
|
||||||
|
|
||||||
|
var encoding = TryGetContentEncoding(content) ?? Encoding.UTF8;
|
||||||
|
return encoding.GetString(buffer.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Encoding? TryGetContentEncoding(HttpContent content)
|
||||||
|
{
|
||||||
|
var charset = content.Headers.ContentType?.CharSet?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(charset))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Encoding.GetEncoding(charset.Trim('"'));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public string ExtractTitle(HtmlDocument document)
|
public string ExtractTitle(HtmlDocument document)
|
||||||
{
|
{
|
||||||
var title = document.DocumentNode.SelectSingleNode("//title")?.InnerText?.Trim();
|
var title = document.DocumentNode.SelectSingleNode("//title")?.InnerText?.Trim();
|
||||||
|
|||||||
@ -14,6 +14,9 @@ public sealed class ReadWebPageTool(HTMLParser htmlParser, ILogger<ReadWebPageTo
|
|||||||
|
|
||||||
private const int DEFAULT_TIMEOUT_SECONDS = 30;
|
private const int DEFAULT_TIMEOUT_SECONDS = 30;
|
||||||
private const int DEFAULT_MAX_CONTENT_CHARACTERS = 12000;
|
private const int DEFAULT_MAX_CONTENT_CHARACTERS = 12000;
|
||||||
|
private const int MAX_TIMEOUT_SECONDS = 60;
|
||||||
|
private const int MAX_CONTENT_CHARACTERS = 50000;
|
||||||
|
private const int MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
|
||||||
private const int MAX_TRACE_LENGTH = 12000;
|
private const int MAX_TRACE_LENGTH = 12000;
|
||||||
private const string ALLOWED_PRIVATE_HOSTS_SETTING = "allowedPrivateHosts";
|
private const string ALLOWED_PRIVATE_HOSTS_SETTING = "allowedPrivateHosts";
|
||||||
|
|
||||||
@ -99,8 +102,8 @@ public sealed class ReadWebPageTool(HTMLParser htmlParser, ILogger<ReadWebPageTo
|
|||||||
if (!Uri.TryCreate(urlText, UriKind.Absolute, out var url) || url is not { Scheme: "http" or "https" })
|
if (!Uri.TryCreate(urlText, UriKind.Absolute, out var url) || url is not { Scheme: "http" or "https" })
|
||||||
throw new ArgumentException("Argument 'url' must be a valid HTTP or HTTPS URL.");
|
throw new ArgumentException("Argument 'url' must be a valid HTTP or HTTPS URL.");
|
||||||
|
|
||||||
var timeoutSeconds = ReadOptionalPositiveIntSetting(context.SettingsValues, "timeoutSeconds") ?? DEFAULT_TIMEOUT_SECONDS;
|
var timeoutSeconds = Math.Min(ReadOptionalPositiveIntSetting(context.SettingsValues, "timeoutSeconds") ?? DEFAULT_TIMEOUT_SECONDS, MAX_TIMEOUT_SECONDS);
|
||||||
var maxContentCharacters = ReadOptionalPositiveIntSetting(context.SettingsValues, "maxContentCharacters") ?? DEFAULT_MAX_CONTENT_CHARACTERS;
|
var maxContentCharacters = Math.Min(ReadOptionalPositiveIntSetting(context.SettingsValues, "maxContentCharacters") ?? DEFAULT_MAX_CONTENT_CHARACTERS, MAX_CONTENT_CHARACTERS);
|
||||||
if (!TryReadAllowedPrivateHostPatterns(context.SettingsValues.GetValueOrDefault(ALLOWED_PRIVATE_HOSTS_SETTING), out var allowedPrivateHosts, out var allowlistError))
|
if (!TryReadAllowedPrivateHostPatterns(context.SettingsValues.GetValueOrDefault(ALLOWED_PRIVATE_HOSTS_SETTING), out var allowedPrivateHosts, out var allowlistError))
|
||||||
throw new InvalidOperationException(allowlistError);
|
throw new InvalidOperationException(allowlistError);
|
||||||
|
|
||||||
@ -111,7 +114,8 @@ public sealed class ReadWebPageTool(HTMLParser htmlParser, ILogger<ReadWebPageTo
|
|||||||
url,
|
url,
|
||||||
token,
|
token,
|
||||||
timeoutSeconds,
|
timeoutSeconds,
|
||||||
async (candidateUrl, validationToken) => await this.ResolveValidatedUrlAddressesAsync(candidateUrl, allowedPrivateHosts, context.ProviderConfidence, validationToken));
|
async (candidateUrl, validationToken) => await this.ResolveValidatedUrlAddressesAsync(candidateUrl, allowedPrivateHosts, context.ProviderConfidence, validationToken),
|
||||||
|
MAX_RESPONSE_BYTES);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (!token.IsCancellationRequested)
|
catch (OperationCanceledException) when (!token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -12,6 +12,10 @@ public sealed class SearXNGWebSearchTool : IToolImplementation
|
|||||||
|
|
||||||
private const int DEFAULT_MAX_RESULTS = 5;
|
private const int DEFAULT_MAX_RESULTS = 5;
|
||||||
private const int DEFAULT_TIMEOUT_SECONDS = 20;
|
private const int DEFAULT_TIMEOUT_SECONDS = 20;
|
||||||
|
private const int MAX_RESULTS = 20;
|
||||||
|
private const int MAX_PAGE = 20;
|
||||||
|
private const int MAX_TIMEOUT_SECONDS = 60;
|
||||||
|
private const int MAX_RESPONSE_BYTES = 1024 * 1024;
|
||||||
private const int MAX_TRACE_LENGTH = 4000;
|
private const int MAX_TRACE_LENGTH = 4000;
|
||||||
|
|
||||||
public string ImplementationKey => "web_search";
|
public string ImplementationKey => "web_search";
|
||||||
@ -127,8 +131,10 @@ public sealed class SearXNGWebSearchTool : IToolImplementation
|
|||||||
throw new InvalidOperationException(TB("Default categories and default engines cannot both be set for the web search tool."));
|
throw new InvalidOperationException(TB("Default categories and default engines cannot both be set for the web search tool."));
|
||||||
|
|
||||||
var defaultLimit = ReadOptionalPositiveIntSetting(context.SettingsValues, "maxResults") ?? DEFAULT_MAX_RESULTS;
|
var defaultLimit = ReadOptionalPositiveIntSetting(context.SettingsValues, "maxResults") ?? DEFAULT_MAX_RESULTS;
|
||||||
var effectiveLimit = requestedLimit ?? defaultLimit;
|
var effectiveLimit = Math.Min(requestedLimit ?? defaultLimit, MAX_RESULTS);
|
||||||
var timeoutSeconds = ReadOptionalPositiveIntSetting(context.SettingsValues, "timeoutSeconds") ?? DEFAULT_TIMEOUT_SECONDS;
|
var timeoutSeconds = Math.Min(ReadOptionalPositiveIntSetting(context.SettingsValues, "timeoutSeconds") ?? DEFAULT_TIMEOUT_SECONDS, MAX_TIMEOUT_SECONDS);
|
||||||
|
if (page is > MAX_PAGE)
|
||||||
|
throw new ArgumentException($"Argument 'page' must be less than or equal to {MAX_PAGE}.");
|
||||||
|
|
||||||
var queryParameters = new List<KeyValuePair<string, string>>
|
var queryParameters = new List<KeyValuePair<string, string>>
|
||||||
{
|
{
|
||||||
@ -163,7 +169,7 @@ public sealed class SearXNGWebSearchTool : IToolImplementation
|
|||||||
timeoutCts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds));
|
timeoutCts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds));
|
||||||
|
|
||||||
using var response = await SendAsync(httpClient, request, timeoutCts.Token, timeoutSeconds, token);
|
using var response = await SendAsync(httpClient, request, timeoutCts.Token, timeoutSeconds, token);
|
||||||
var responseBody = await response.Content.ReadAsStringAsync(token);
|
var responseBody = await ReadContentAsStringWithLimitAsync(response.Content, MAX_RESPONSE_BYTES, token);
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var responseDetails = string.IsNullOrWhiteSpace(responseBody) ? string.Empty : $" Response body: {responseBody[..Math.Min(responseBody.Length, 400)]}";
|
var responseDetails = string.IsNullOrWhiteSpace(responseBody) ? string.Empty : $" Response body: {responseBody[..Math.Min(responseBody.Length, 400)]}";
|
||||||
@ -409,6 +415,29 @@ public sealed class SearXNGWebSearchTool : IToolImplementation
|
|||||||
return int.TryParse(value, out var parsedValue) && parsedValue > 0 ? parsedValue : null;
|
return int.TryParse(value, out var parsedValue) && parsedValue > 0 ? parsedValue : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task<string> ReadContentAsStringWithLimitAsync(HttpContent content, int maxResponseBytes, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (content.Headers.ContentLength is long contentLength && contentLength > maxResponseBytes)
|
||||||
|
throw new InvalidOperationException($"The SearXNG response body is too large. Maximum allowed size is {maxResponseBytes} bytes.");
|
||||||
|
|
||||||
|
await using var stream = await content.ReadAsStreamAsync(token);
|
||||||
|
await using var buffer = new MemoryStream();
|
||||||
|
var chunk = new byte[8192];
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var read = await stream.ReadAsync(chunk, token);
|
||||||
|
if (read == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (buffer.Length + read > maxResponseBytes)
|
||||||
|
throw new InvalidOperationException($"The SearXNG response body is too large. Maximum allowed size is {maxResponseBytes} bytes.");
|
||||||
|
|
||||||
|
buffer.Write(chunk, 0, read);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetString(buffer.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryReadOptionalPositiveInt(
|
private static bool TryReadOptionalPositiveInt(
|
||||||
IReadOnlyDictionary<string, string> settingsValues,
|
IReadOnlyDictionary<string, string> settingsValues,
|
||||||
string key,
|
string key,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user