feat(scanner): Implement Deno analyzer and associated tests
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Added Deno analyzer with comprehensive metadata and evidence structure. - Created a detailed implementation plan for Sprint 130 focusing on Deno analyzer. - Introduced AdvisoryAiGuardrailOptions for managing guardrail configurations. - Developed GuardrailPhraseLoader for loading blocked phrases from JSON files. - Implemented tests for AdvisoryGuardrailOptions binding and phrase loading. - Enhanced telemetry for Advisory AI with metrics tracking. - Added VexObservationProjectionService for querying VEX observations. - Created extensive tests for VexObservationProjectionService functionality. - Introduced Ruby language analyzer with tests for simple and complex workspaces. - Added Ruby application fixtures for testing purposes.
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Hosting;
|
||||
|
||||
public sealed class AdvisoryAiGuardrailOptions
|
||||
{
|
||||
private const int DefaultMaxPromptLength = 16000;
|
||||
|
||||
public int? MaxPromptLength { get; set; } = DefaultMaxPromptLength;
|
||||
|
||||
public bool RequireCitations { get; set; } = true;
|
||||
|
||||
public string? BlockedPhraseFile { get; set; }
|
||||
= null;
|
||||
|
||||
public List<string> BlockedPhrases { get; set; } = new();
|
||||
}
|
||||
@@ -18,6 +18,8 @@ public sealed class AdvisoryAiServiceOptions
|
||||
|
||||
public AdvisoryAiInferenceOptions Inference { get; set; } = new();
|
||||
|
||||
public AdvisoryAiGuardrailOptions Guardrails { get; set; } = new();
|
||||
|
||||
internal string ResolveQueueDirectory(string contentRoot)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(contentRoot);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using StellaOps.AdvisoryAI.Inference;
|
||||
@@ -71,6 +72,15 @@ internal static class AdvisoryAiServiceOptionsValidator
|
||||
}
|
||||
}
|
||||
|
||||
options.Guardrails ??= new AdvisoryAiGuardrailOptions();
|
||||
options.Guardrails.BlockedPhrases ??= new List<string>();
|
||||
|
||||
if (options.Guardrails.MaxPromptLength.HasValue && options.Guardrails.MaxPromptLength.Value <= 0)
|
||||
{
|
||||
error = "AdvisoryAI:Guardrails:MaxPromptLength must be greater than zero when specified.";
|
||||
return false;
|
||||
}
|
||||
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Hosting;
|
||||
|
||||
internal static class GuardrailPhraseLoader
|
||||
{
|
||||
public static IReadOnlyCollection<string> Load(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
throw new ArgumentException("Guardrail phrase file path must be provided.", nameof(path));
|
||||
}
|
||||
|
||||
using var stream = File.OpenRead(path);
|
||||
using var document = JsonDocument.Parse(stream);
|
||||
var root = document.RootElement;
|
||||
return root.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Array => ExtractValues(root),
|
||||
JsonValueKind.Object when root.TryGetProperty("phrases", out var phrases) => ExtractValues(phrases),
|
||||
_ => throw new InvalidDataException($"Guardrail phrase file {path} must be a JSON array or object with a phrases array."),
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyCollection<string> ExtractValues(JsonElement element)
|
||||
{
|
||||
var phrases = new List<string>();
|
||||
foreach (var item in element.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var value = item.GetString();
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
phrases.Add(value.Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return phrases;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http.Headers;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.AdvisoryAI.Caching;
|
||||
using StellaOps.AdvisoryAI.DependencyInjection;
|
||||
using StellaOps.AdvisoryAI.Inference;
|
||||
using StellaOps.AdvisoryAI.Metrics;
|
||||
using StellaOps.AdvisoryAI.Guardrails;
|
||||
using StellaOps.AdvisoryAI.Outputs;
|
||||
using StellaOps.AdvisoryAI.Providers;
|
||||
using StellaOps.AdvisoryAI.Queue;
|
||||
@@ -86,6 +91,12 @@ public static class ServiceCollectionExtensions
|
||||
services.AddAdvisoryPipeline();
|
||||
services.AddAdvisoryPipelineInfrastructure();
|
||||
|
||||
services.AddOptions<AdvisoryGuardrailOptions>()
|
||||
.Configure<IOptions<AdvisoryAiServiceOptions>, IHostEnvironment>((options, aiOptions, environment) =>
|
||||
{
|
||||
ApplyGuardrailConfiguration(options, aiOptions.Value.Guardrails, environment);
|
||||
});
|
||||
|
||||
services.Replace(ServiceDescriptor.Singleton<IAdvisoryTaskQueue, FileSystemAdvisoryTaskQueue>());
|
||||
services.Replace(ServiceDescriptor.Singleton<IAdvisoryPlanCache, FileSystemAdvisoryPlanCache>());
|
||||
services.Replace(ServiceDescriptor.Singleton<IAdvisoryOutputStore, FileSystemAdvisoryOutputStore>());
|
||||
@@ -93,4 +104,87 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static void ApplyGuardrailConfiguration(
|
||||
AdvisoryGuardrailOptions target,
|
||||
AdvisoryAiGuardrailOptions? source,
|
||||
IHostEnvironment? environment)
|
||||
{
|
||||
if (source is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (source.MaxPromptLength.HasValue && source.MaxPromptLength.Value > 0)
|
||||
{
|
||||
target.MaxPromptLength = source.MaxPromptLength.Value;
|
||||
}
|
||||
|
||||
target.RequireCitations = source.RequireCitations;
|
||||
|
||||
var defaults = target.BlockedPhrases.ToList();
|
||||
var merged = new SortedSet<string>(defaults, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (source.BlockedPhrases is { Count: > 0 })
|
||||
{
|
||||
foreach (var phrase in source.BlockedPhrases)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(phrase))
|
||||
{
|
||||
merged.Add(phrase.Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(source.BlockedPhraseFile))
|
||||
{
|
||||
var resolvedPath = ResolveGuardrailPath(source.BlockedPhraseFile!, environment);
|
||||
foreach (var phrase in GuardrailPhraseLoader.Load(resolvedPath))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(phrase))
|
||||
{
|
||||
merged.Add(phrase.Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (merged.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
target.BlockedPhrases.Clear();
|
||||
foreach (var phrase in merged)
|
||||
{
|
||||
target.BlockedPhrases.Add(phrase);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolveGuardrailPath(string configuredPath, IHostEnvironment? environment)
|
||||
{
|
||||
var trimmed = configuredPath.Trim();
|
||||
if (Path.IsPathRooted(trimmed))
|
||||
{
|
||||
if (!File.Exists(trimmed))
|
||||
{
|
||||
throw new FileNotFoundException($"Guardrail phrase file {trimmed} was not found.", trimmed);
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
var root = environment?.ContentRootPath;
|
||||
if (string.IsNullOrWhiteSpace(root))
|
||||
{
|
||||
root = AppContext.BaseDirectory;
|
||||
}
|
||||
|
||||
var resolved = Path.GetFullPath(Path.Combine(root!, trimmed));
|
||||
if (!File.Exists(resolved))
|
||||
{
|
||||
throw new FileNotFoundException($"Guardrail phrase file {resolved} was not found.", resolved);
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.AdvisoryAI.Guardrails;
|
||||
using StellaOps.AdvisoryAI.Hosting;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Tests;
|
||||
|
||||
public sealed class AdvisoryGuardrailOptionsBindingTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task AddAdvisoryAiCore_ConfiguresGuardrailOptionsFromServiceOptions()
|
||||
{
|
||||
var tempRoot = CreateTempDirectory();
|
||||
var phrasePath = Path.Combine(tempRoot, "guardrail-phrases.json");
|
||||
await File.WriteAllTextAsync(phrasePath, "{\n \"phrases\": [\"extract secrets\", \"dump cache\"]\n}");
|
||||
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["AdvisoryAI:Guardrails:MaxPromptLength"] = "32000",
|
||||
["AdvisoryAI:Guardrails:RequireCitations"] = "false",
|
||||
["AdvisoryAI:Guardrails:BlockedPhraseFile"] = "guardrail-phrases.json",
|
||||
["AdvisoryAI:Guardrails:BlockedPhrases:0"] = "custom override"
|
||||
})
|
||||
.Build();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IHostEnvironment>(new FakeHostEnvironment(tempRoot));
|
||||
services.AddAdvisoryAiCore(configuration);
|
||||
|
||||
await using var provider = services.BuildServiceProvider();
|
||||
var options = provider.GetRequiredService<IOptions<AdvisoryGuardrailOptions>>().Value;
|
||||
|
||||
options.MaxPromptLength.Should().Be(32000);
|
||||
options.RequireCitations.Should().BeFalse();
|
||||
options.BlockedPhrases.Should().Contain("custom override");
|
||||
options.BlockedPhrases.Should().Contain("extract secrets");
|
||||
options.BlockedPhrases.Should().Contain("dump cache");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddAdvisoryAiCore_ThrowsWhenPhraseFileMissing()
|
||||
{
|
||||
var tempRoot = CreateTempDirectory();
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["AdvisoryAI:Guardrails:BlockedPhraseFile"] = "missing.json"
|
||||
})
|
||||
.Build();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IHostEnvironment>(new FakeHostEnvironment(tempRoot));
|
||||
services.AddAdvisoryAiCore(configuration);
|
||||
|
||||
await using var provider = services.BuildServiceProvider();
|
||||
var action = () => provider.GetRequiredService<IOptions<AdvisoryGuardrailOptions>>().Value;
|
||||
action.Should().Throw<FileNotFoundException>();
|
||||
}
|
||||
|
||||
private static string CreateTempDirectory()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), "advisoryai-guardrails", Guid.NewGuid().ToString("n"));
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
private sealed class FakeHostEnvironment : IHostEnvironment
|
||||
{
|
||||
public FakeHostEnvironment(string contentRoot)
|
||||
{
|
||||
ContentRootPath = contentRoot;
|
||||
}
|
||||
|
||||
public string EnvironmentName { get; set; } = Environments.Development;
|
||||
|
||||
public string ApplicationName { get; set; } = "StellaOps.AdvisoryAI.Tests";
|
||||
|
||||
public string ContentRootPath { get; set; }
|
||||
= string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,12 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
|
||||
</ItemGroup>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.AdvisoryAI\StellaOps.AdvisoryAI.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.AdvisoryAI.Hosting\StellaOps.AdvisoryAI.Hosting.csproj" />
|
||||
|
||||
Reference in New Issue
Block a user