release orchestrator v1 draft and build fixes

This commit is contained in:
master
2026-01-12 12:24:17 +02:00
parent f3de858c59
commit 9873f80830
1598 changed files with 240385 additions and 5944 deletions

View File

@@ -0,0 +1,217 @@
namespace StellaOps.AdvisoryAI.Plugin.Unified;
using System.Runtime.CompilerServices;
using StellaOps.AdvisoryAI.Inference.LlmProviders;
using StellaOps.Plugin.Abstractions;
using StellaOps.Plugin.Abstractions.Capabilities;
using StellaOps.Plugin.Abstractions.Context;
using StellaOps.Plugin.Abstractions.Health;
using StellaOps.Plugin.Abstractions.Lifecycle;
// Type aliases to disambiguate between AdvisoryAI and Plugin.Abstractions types
using AdvisoryLlmRequest = StellaOps.AdvisoryAI.Inference.LlmProviders.LlmCompletionRequest;
using AdvisoryLlmResult = StellaOps.AdvisoryAI.Inference.LlmProviders.LlmCompletionResult;
using AdvisoryStreamChunk = StellaOps.AdvisoryAI.Inference.LlmProviders.LlmStreamChunk;
using PluginLlmRequest = StellaOps.Plugin.Abstractions.Capabilities.LlmCompletionRequest;
using PluginLlmResult = StellaOps.Plugin.Abstractions.Capabilities.LlmCompletionResult;
using PluginStreamChunk = StellaOps.Plugin.Abstractions.Capabilities.LlmStreamChunk;
/// <summary>
/// Adapts an existing ILlmProvider to the unified IPlugin and ILlmCapability interfaces.
/// This enables gradual migration of AdvisoryAI LLM providers to the unified plugin architecture.
/// </summary>
public sealed class LlmPluginAdapter : IPlugin, ILlmCapability
{
private readonly ILlmProvider _inner;
private readonly ILlmProviderPlugin _plugin;
private readonly int _priority;
private IPluginContext? _context;
private PluginLifecycleState _state = PluginLifecycleState.Discovered;
private List<LlmModelInfo> _models = new();
/// <summary>
/// Creates a new adapter for an existing LLM provider.
/// </summary>
/// <param name="inner">The existing LLM provider to wrap.</param>
/// <param name="plugin">The plugin metadata for this provider.</param>
/// <param name="priority">Provider priority (higher = preferred).</param>
public LlmPluginAdapter(ILlmProvider inner, ILlmProviderPlugin plugin, int priority = 10)
{
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
_plugin = plugin ?? throw new ArgumentNullException(nameof(plugin));
_priority = priority;
}
/// <inheritdoc />
public PluginInfo Info => new(
Id: $"com.stellaops.llm.{_inner.ProviderId}",
Name: _plugin.DisplayName,
Version: "1.0.0",
Vendor: "Stella Ops",
Description: _plugin.Description);
/// <inheritdoc />
public PluginTrustLevel TrustLevel => PluginTrustLevel.BuiltIn;
/// <inheritdoc />
public PluginCapabilities Capabilities => PluginCapabilities.Llm | PluginCapabilities.Network;
/// <inheritdoc />
public PluginLifecycleState State => _state;
#region ILlmCapability
/// <inheritdoc />
public string ProviderId => _inner.ProviderId;
/// <inheritdoc />
public int Priority => _priority;
/// <inheritdoc />
public IReadOnlyList<LlmModelInfo> AvailableModels => _models;
/// <inheritdoc />
public Task<bool> IsAvailableAsync(CancellationToken ct)
{
return _inner.IsAvailableAsync(ct);
}
/// <inheritdoc />
public async Task<PluginLlmResult> CompleteAsync(PluginLlmRequest request, CancellationToken ct)
{
var advisoryRequest = ToAdvisoryRequest(request);
var result = await _inner.CompleteAsync(advisoryRequest, ct);
return ToPluginResult(result);
}
/// <inheritdoc />
public IAsyncEnumerable<PluginStreamChunk> CompleteStreamAsync(PluginLlmRequest request, CancellationToken ct)
{
var advisoryRequest = ToAdvisoryRequest(request);
return StreamAdapter(_inner.CompleteStreamAsync(advisoryRequest, ct), ct);
}
/// <inheritdoc />
public Task<LlmEmbeddingResult?> EmbedAsync(string text, CancellationToken ct)
{
// Embedding is not supported by the base ILlmProvider interface
// Specific providers that support embedding would need custom adapters
return Task.FromResult<LlmEmbeddingResult?>(null);
}
#endregion
#region IPlugin
/// <inheritdoc />
public async Task InitializeAsync(IPluginContext context, CancellationToken ct)
{
_context = context;
_state = PluginLifecycleState.Initializing;
// Check if the provider is available
var available = await _inner.IsAvailableAsync(ct);
if (!available)
{
_state = PluginLifecycleState.Failed;
throw new InvalidOperationException($"LLM provider '{_inner.ProviderId}' is not available");
}
// Initialize with a default model entry (provider-specific models would be discovered at runtime)
_models = new List<LlmModelInfo>
{
new(
Id: _inner.ProviderId,
Name: _plugin.DisplayName,
Description: _plugin.Description,
ParameterCount: null,
ContextLength: null,
Capabilities: new[] { "chat", "completion" })
};
_state = PluginLifecycleState.Active;
context.Logger.Info("LLM plugin adapter initialized for {ProviderId}", _inner.ProviderId);
}
/// <inheritdoc />
public async Task<HealthCheckResult> HealthCheckAsync(CancellationToken ct)
{
try
{
var available = await _inner.IsAvailableAsync(ct);
if (available)
{
return HealthCheckResult.Healthy()
.WithDetails(new Dictionary<string, object>
{
["providerId"] = _inner.ProviderId,
["priority"] = _priority
});
}
return HealthCheckResult.Unhealthy($"LLM provider '{_inner.ProviderId}' is not available");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy(ex);
}
}
/// <inheritdoc />
public ValueTask DisposeAsync()
{
_state = PluginLifecycleState.Stopped;
_inner.Dispose();
return ValueTask.CompletedTask;
}
#endregion
#region Type Mapping
private static AdvisoryLlmRequest ToAdvisoryRequest(PluginLlmRequest request)
{
return new AdvisoryLlmRequest
{
UserPrompt = request.UserPrompt,
SystemPrompt = request.SystemPrompt,
Model = request.Model,
Temperature = request.Temperature,
MaxTokens = request.MaxTokens,
Seed = request.Seed,
StopSequences = request.StopSequences,
RequestId = request.RequestId
};
}
private static PluginLlmResult ToPluginResult(AdvisoryLlmResult result)
{
return new PluginLlmResult(
Content: result.Content,
ModelId: result.ModelId,
ProviderId: result.ProviderId,
InputTokens: result.InputTokens,
OutputTokens: result.OutputTokens,
TimeToFirstTokenMs: result.TimeToFirstTokenMs,
TotalTimeMs: result.TotalTimeMs,
FinishReason: result.FinishReason,
Deterministic: result.Deterministic,
RequestId: result.RequestId);
}
private static async IAsyncEnumerable<PluginStreamChunk> StreamAdapter(
IAsyncEnumerable<AdvisoryStreamChunk> source,
[EnumeratorCancellation] CancellationToken ct)
{
await foreach (var chunk in source.WithCancellation(ct))
{
yield return new PluginStreamChunk(
Content: chunk.Content,
IsFinal: chunk.IsFinal,
FinishReason: chunk.FinishReason);
}
}
#endregion
}

View File

@@ -0,0 +1,137 @@
namespace StellaOps.AdvisoryAI.Plugin.Unified;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.AdvisoryAI.Inference.LlmProviders;
using StellaOps.Plugin.Abstractions;
using StellaOps.Plugin.Abstractions.Capabilities;
/// <summary>
/// Factory for creating unified LLM plugin adapters from existing providers.
/// </summary>
public sealed class LlmPluginAdapterFactory
{
private readonly LlmProviderCatalog _catalog;
private readonly IServiceProvider _serviceProvider;
private readonly Dictionary<string, LlmPluginAdapter> _adapters = new(StringComparer.OrdinalIgnoreCase);
private readonly object _lock = new();
/// <summary>
/// Creates a new factory instance.
/// </summary>
/// <param name="catalog">The LLM provider catalog.</param>
/// <param name="serviceProvider">Service provider for creating providers.</param>
public LlmPluginAdapterFactory(LlmProviderCatalog catalog, IServiceProvider serviceProvider)
{
_catalog = catalog ?? throw new ArgumentNullException(nameof(catalog));
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
}
/// <summary>
/// Gets all available unified LLM plugins.
/// </summary>
/// <returns>List of unified LLM plugins.</returns>
public IReadOnlyList<IPlugin> GetAllPlugins()
{
var plugins = _catalog.GetAvailablePlugins(_serviceProvider);
var result = new List<IPlugin>();
foreach (var plugin in plugins)
{
var adapter = GetOrCreateAdapter(plugin.ProviderId);
if (adapter != null)
{
result.Add(adapter);
}
}
return result;
}
/// <summary>
/// Gets a unified LLM plugin by provider ID.
/// </summary>
/// <param name="providerId">Provider identifier.</param>
/// <returns>Unified LLM plugin, or null if not found.</returns>
public IPlugin? GetPlugin(string providerId)
{
return GetOrCreateAdapter(providerId);
}
/// <summary>
/// Gets the LLM capability for a provider.
/// </summary>
/// <param name="providerId">Provider identifier.</param>
/// <returns>LLM capability, or null if not found.</returns>
public ILlmCapability? GetCapability(string providerId)
{
return GetOrCreateAdapter(providerId);
}
private LlmPluginAdapter? GetOrCreateAdapter(string providerId)
{
lock (_lock)
{
if (_adapters.TryGetValue(providerId, out var existing))
{
return existing;
}
var plugin = _catalog.GetPlugin(providerId);
if (plugin == null)
{
return null;
}
var config = _catalog.GetConfiguration(providerId);
if (config == null)
{
return null;
}
var validation = plugin.ValidateConfiguration(config);
if (!validation.IsValid)
{
return null;
}
var provider = plugin.Create(_serviceProvider, config);
var priority = GetPriority(providerId);
var adapter = new LlmPluginAdapter(provider, plugin, priority);
_adapters[providerId] = adapter;
return adapter;
}
}
private static int GetPriority(string providerId)
{
// Default priorities (higher = preferred for local/airgap scenarios)
return providerId.ToLowerInvariant() switch
{
"llama-server" => 100, // Local, highest priority for airgap
"ollama" => 90, // Local
"claude" => 20, // Remote, lower priority
"openai" => 10, // Remote, lowest priority
_ => 50
};
}
}
/// <summary>
/// Extension methods for registering unified LLM plugin services.
/// </summary>
public static class LlmPluginAdapterExtensions
{
/// <summary>
/// Adds unified LLM plugin adapter services to the service collection.
/// </summary>
/// <param name="services">Service collection.</param>
/// <returns>Service collection for chaining.</returns>
public static IServiceCollection AddUnifiedLlmPlugins(this IServiceCollection services)
{
services.AddSingleton<LlmPluginAdapterFactory>();
return services;
}
}

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Description>Unified plugin adapter for AdvisoryAI LLM providers</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.AdvisoryAI\StellaOps.AdvisoryAI.csproj" />
<ProjectReference Include="..\..\Plugin\StellaOps.Plugin.Abstractions\StellaOps.Plugin.Abstractions.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,347 @@
namespace StellaOps.AdvisoryAI.Scm.Plugin.Unified;
using StellaOps.AdvisoryAI.Remediation.ScmConnector;
using StellaOps.Plugin.Abstractions;
using StellaOps.Plugin.Abstractions.Capabilities;
using StellaOps.Plugin.Abstractions.Context;
using StellaOps.Plugin.Abstractions.Health;
using StellaOps.Plugin.Abstractions.Lifecycle;
// Type aliases to disambiguate between AdvisoryAI and Plugin.Abstractions types
using AdvisoryBranchResult = StellaOps.AdvisoryAI.Remediation.ScmConnector.BranchResult;
using AdvisoryFileUpdateResult = StellaOps.AdvisoryAI.Remediation.ScmConnector.FileUpdateResult;
using AdvisoryPrCreateResult = StellaOps.AdvisoryAI.Remediation.ScmConnector.PrCreateResult;
using AdvisoryPrStatusResult = StellaOps.AdvisoryAI.Remediation.ScmConnector.PrStatusResult;
using AdvisoryCiStatusResult = StellaOps.AdvisoryAI.Remediation.ScmConnector.CiStatusResult;
using AdvisoryCiCheck = StellaOps.AdvisoryAI.Remediation.ScmConnector.CiCheck;
/// <summary>
/// Adapts an existing IScmConnector to the unified IPlugin and IScmCapability interfaces.
/// This enables gradual migration of AdvisoryAI SCM connectors to the unified plugin architecture.
/// </summary>
/// <remarks>
/// The AdvisoryAI IScmConnector focuses on PR/write operations while IScmCapability focuses on
/// read operations. This adapter bridges both, implementing what it can from the underlying connector.
/// </remarks>
public sealed class ScmPluginAdapter : IPlugin, IScmCapability
{
private readonly IScmConnector _inner;
private readonly IScmConnectorPlugin _plugin;
private readonly ScmConnectorOptions _options;
private IPluginContext? _context;
private PluginLifecycleState _state = PluginLifecycleState.Discovered;
/// <summary>
/// Creates a new adapter for an existing SCM connector.
/// </summary>
/// <param name="inner">The existing SCM connector to wrap.</param>
/// <param name="plugin">The plugin metadata for this connector.</param>
/// <param name="options">Connector configuration options.</param>
public ScmPluginAdapter(IScmConnector inner, IScmConnectorPlugin plugin, ScmConnectorOptions options)
{
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
_plugin = plugin ?? throw new ArgumentNullException(nameof(plugin));
_options = options ?? throw new ArgumentNullException(nameof(options));
}
/// <inheritdoc />
public PluginInfo Info => new(
Id: $"com.stellaops.scm.{_inner.ScmType}",
Name: $"{_plugin.DisplayName} SCM Connector",
Version: "1.0.0",
Vendor: "Stella Ops",
Description: $"{_plugin.DisplayName} source control management connector");
/// <inheritdoc />
public PluginTrustLevel TrustLevel => PluginTrustLevel.BuiltIn;
/// <inheritdoc />
public PluginCapabilities Capabilities => PluginCapabilities.Scm | PluginCapabilities.Network;
/// <inheritdoc />
public PluginLifecycleState State => _state;
#region IScmCapability - Core properties
/// <inheritdoc />
public string ConnectorType => $"scm.{_inner.ScmType}";
/// <inheritdoc />
public string DisplayName => _plugin.DisplayName;
/// <inheritdoc />
public string ScmType => _inner.ScmType;
#endregion
#region IScmCapability - Connection
/// <inheritdoc />
public bool CanHandle(string repositoryUrl)
{
return _plugin.CanHandle(repositoryUrl);
}
/// <inheritdoc />
public async Task<ConnectionTestResult> TestConnectionAsync(CancellationToken ct)
{
// Try to perform any operation to test connectivity
// Since IScmConnector doesn't have a dedicated test method, we assume success
// if the connector was created successfully
await Task.Delay(0, ct);
return ConnectionTestResult.Succeeded(TimeSpan.FromMilliseconds(1));
}
/// <inheritdoc />
public async Task<ConnectionInfo> GetConnectionInfoAsync(CancellationToken ct)
{
await Task.Delay(0, ct);
return new ConnectionInfo(
EndpointUrl: _options.BaseUrl ?? $"https://api.{_inner.ScmType}.com",
AuthenticatedAs: "configured-token",
Metadata: new Dictionary<string, object>
{
["scmType"] = _inner.ScmType,
["hasToken"] = !string.IsNullOrEmpty(_options.ApiToken)
});
}
#endregion
#region IScmCapability - Read operations (limited support via IScmConnector)
/// <inheritdoc />
public Task<IReadOnlyList<ScmBranch>> ListBranchesAsync(string repositoryUrl, CancellationToken ct)
{
// IScmConnector doesn't support listing branches
// This would require extending the adapter or using direct API calls
throw new NotSupportedException(
"Branch listing not supported via IScmConnector adapter. " +
"Use the IScmCapability-native implementation or extend this adapter.");
}
/// <inheritdoc />
public Task<IReadOnlyList<ScmCommit>> ListCommitsAsync(
string repositoryUrl,
string branch,
int limit = 50,
CancellationToken ct = default)
{
throw new NotSupportedException(
"Commit listing not supported via IScmConnector adapter.");
}
/// <inheritdoc />
public Task<ScmCommit> GetCommitAsync(string repositoryUrl, string commitSha, CancellationToken ct)
{
throw new NotSupportedException(
"Commit retrieval not supported via IScmConnector adapter.");
}
/// <inheritdoc />
public Task<ScmFileContent> GetFileAsync(
string repositoryUrl,
string filePath,
string? reference = null,
CancellationToken ct = default)
{
throw new NotSupportedException(
"File retrieval not supported via IScmConnector adapter.");
}
/// <inheritdoc />
public Task<Stream> GetArchiveAsync(
string repositoryUrl,
string reference,
ArchiveFormat format = ArchiveFormat.TarGz,
CancellationToken ct = default)
{
throw new NotSupportedException(
"Archive download not supported via IScmConnector adapter.");
}
/// <inheritdoc />
public Task<ScmWebhook> UpsertWebhookAsync(
string repositoryUrl,
ScmWebhookConfig config,
CancellationToken ct)
{
throw new NotSupportedException(
"Webhook management not supported via IScmConnector adapter.");
}
/// <inheritdoc />
public Task<ScmUser> GetCurrentUserAsync(CancellationToken ct)
{
throw new NotSupportedException(
"User info retrieval not supported via IScmConnector adapter.");
}
#endregion
#region Extended SCM operations (from IScmConnector)
/// <summary>
/// Creates a branch from the base branch.
/// </summary>
public async Task<AdvisoryBranchResult> CreateBranchAsync(
string owner,
string repo,
string branchName,
string baseBranch,
CancellationToken ct)
{
return await _inner.CreateBranchAsync(owner, repo, branchName, baseBranch, ct);
}
/// <summary>
/// Updates or creates a file in a branch.
/// </summary>
public async Task<AdvisoryFileUpdateResult> UpdateFileAsync(
string owner,
string repo,
string branch,
string filePath,
string content,
string commitMessage,
CancellationToken ct)
{
return await _inner.UpdateFileAsync(owner, repo, branch, filePath, content, commitMessage, ct);
}
/// <summary>
/// Creates a pull request / merge request.
/// </summary>
public async Task<AdvisoryPrCreateResult> CreatePullRequestAsync(
string owner,
string repo,
string headBranch,
string baseBranch,
string title,
string body,
CancellationToken ct)
{
return await _inner.CreatePullRequestAsync(owner, repo, headBranch, baseBranch, title, body, ct);
}
/// <summary>
/// Gets pull request details and status.
/// </summary>
public async Task<AdvisoryPrStatusResult> GetPullRequestStatusAsync(
string owner,
string repo,
int prNumber,
CancellationToken ct)
{
return await _inner.GetPullRequestStatusAsync(owner, repo, prNumber, ct);
}
/// <summary>
/// Gets CI/CD pipeline status for a commit.
/// </summary>
public async Task<AdvisoryCiStatusResult> GetCiStatusAsync(
string owner,
string repo,
string commitSha,
CancellationToken ct)
{
return await _inner.GetCiStatusAsync(owner, repo, commitSha, ct);
}
/// <summary>
/// Updates pull request body/description.
/// </summary>
public async Task<bool> UpdatePullRequestAsync(
string owner,
string repo,
int prNumber,
string? title,
string? body,
CancellationToken ct)
{
return await _inner.UpdatePullRequestAsync(owner, repo, prNumber, title, body, ct);
}
/// <summary>
/// Adds a comment to a pull request.
/// </summary>
public async Task<bool> AddCommentAsync(
string owner,
string repo,
int prNumber,
string comment,
CancellationToken ct)
{
return await _inner.AddCommentAsync(owner, repo, prNumber, comment, ct);
}
/// <summary>
/// Closes a pull request without merging.
/// </summary>
public async Task<bool> ClosePullRequestAsync(
string owner,
string repo,
int prNumber,
CancellationToken ct)
{
return await _inner.ClosePullRequestAsync(owner, repo, prNumber, ct);
}
#endregion
#region IPlugin
/// <inheritdoc />
public async Task InitializeAsync(IPluginContext context, CancellationToken ct)
{
_context = context;
_state = PluginLifecycleState.Initializing;
// Verify the connector is available
if (!_plugin.IsAvailable(_options))
{
_state = PluginLifecycleState.Failed;
throw new InvalidOperationException(
$"SCM connector '{_inner.ScmType}' is not available. Check API token configuration.");
}
_state = PluginLifecycleState.Active;
context.Logger.Info("SCM plugin adapter initialized for {ScmType}", _inner.ScmType);
await Task.CompletedTask;
}
/// <inheritdoc />
public async Task<HealthCheckResult> HealthCheckAsync(CancellationToken ct)
{
try
{
var testResult = await TestConnectionAsync(ct);
if (testResult.Success)
{
return HealthCheckResult.Healthy()
.WithDetails(new Dictionary<string, object>
{
["scmType"] = _inner.ScmType,
["displayName"] = _plugin.DisplayName,
["latencyMs"] = testResult.Latency?.TotalMilliseconds ?? 0
});
}
return HealthCheckResult.Unhealthy(testResult.Message ?? "Connection test failed");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy(ex);
}
}
/// <inheritdoc />
public ValueTask DisposeAsync()
{
_state = PluginLifecycleState.Stopped;
return ValueTask.CompletedTask;
}
#endregion
}

View File

@@ -0,0 +1,134 @@
namespace StellaOps.AdvisoryAI.Scm.Plugin.Unified;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.AdvisoryAI.Remediation.ScmConnector;
using StellaOps.Plugin.Abstractions;
using StellaOps.Plugin.Abstractions.Capabilities;
/// <summary>
/// Factory for creating unified SCM plugin adapters from existing connectors.
/// </summary>
public sealed class ScmPluginAdapterFactory
{
private readonly ScmConnectorCatalog _catalog;
private readonly Dictionary<string, ScmPluginAdapter> _adapters = new(StringComparer.OrdinalIgnoreCase);
private readonly object _lock = new();
/// <summary>
/// Creates a new factory instance.
/// </summary>
/// <param name="catalog">The SCM connector catalog.</param>
public ScmPluginAdapterFactory(ScmConnectorCatalog catalog)
{
_catalog = catalog ?? throw new ArgumentNullException(nameof(catalog));
}
/// <summary>
/// Gets all available unified SCM plugins.
/// </summary>
/// <param name="options">Connector options.</param>
/// <returns>List of unified SCM plugins.</returns>
public IReadOnlyList<IPlugin> GetAllPlugins(ScmConnectorOptions options)
{
var result = new List<IPlugin>();
foreach (var plugin in _catalog.Plugins)
{
if (plugin.IsAvailable(options))
{
var adapter = GetOrCreateAdapter(plugin.ScmType, options);
if (adapter != null)
{
result.Add(adapter);
}
}
}
return result;
}
/// <summary>
/// Gets a unified SCM plugin by SCM type.
/// </summary>
/// <param name="scmType">SCM type identifier.</param>
/// <param name="options">Connector options.</param>
/// <returns>Unified SCM plugin, or null if not found.</returns>
public IPlugin? GetPlugin(string scmType, ScmConnectorOptions options)
{
return GetOrCreateAdapter(scmType, options);
}
/// <summary>
/// Gets a unified SCM plugin that can handle the given repository URL.
/// </summary>
/// <param name="repositoryUrl">Repository URL to handle.</param>
/// <param name="options">Connector options.</param>
/// <returns>Unified SCM plugin, or null if not found.</returns>
public IPlugin? GetPluginForRepository(string repositoryUrl, ScmConnectorOptions options)
{
var plugin = _catalog.Plugins.FirstOrDefault(p => p.CanHandle(repositoryUrl));
if (plugin == null)
return null;
return GetOrCreateAdapter(plugin.ScmType, options);
}
/// <summary>
/// Gets the SCM capability for a connector.
/// </summary>
/// <param name="scmType">SCM type identifier.</param>
/// <param name="options">Connector options.</param>
/// <returns>SCM capability, or null if not found.</returns>
public IScmCapability? GetCapability(string scmType, ScmConnectorOptions options)
{
return GetOrCreateAdapter(scmType, options);
}
private ScmPluginAdapter? GetOrCreateAdapter(string scmType, ScmConnectorOptions options)
{
lock (_lock)
{
if (_adapters.TryGetValue(scmType, out var existing))
{
return existing;
}
var plugin = _catalog.Plugins.FirstOrDefault(
p => p.ScmType.Equals(scmType, StringComparison.OrdinalIgnoreCase));
if (plugin == null || !plugin.IsAvailable(options))
{
return null;
}
var connector = _catalog.GetConnector(scmType, options);
if (connector == null)
{
return null;
}
var adapter = new ScmPluginAdapter(connector, plugin, options);
_adapters[scmType] = adapter;
return adapter;
}
}
}
/// <summary>
/// Extension methods for registering unified SCM plugin services.
/// </summary>
public static class ScmPluginAdapterExtensions
{
/// <summary>
/// Adds unified SCM plugin adapter services to the service collection.
/// </summary>
/// <param name="services">Service collection.</param>
/// <returns>Service collection for chaining.</returns>
public static IServiceCollection AddUnifiedScmPlugins(this IServiceCollection services)
{
services.AddSingleton<ScmPluginAdapterFactory>();
return services;
}
}

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Description>Unified plugin adapter for AdvisoryAI SCM connectors</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.AdvisoryAI\StellaOps.AdvisoryAI.csproj" />
<ProjectReference Include="..\..\Plugin\StellaOps.Plugin.Abstractions\StellaOps.Plugin.Abstractions.csproj" />
</ItemGroup>
</Project>

View File

@@ -6,6 +6,7 @@ using System.Collections.Immutable;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using StellaOps.Determinism;
using StellaOps.Evidence.Pack;
using StellaOps.Evidence.Pack.Models;
@@ -87,6 +88,8 @@ public static class EvidencePackEndpoints
private static async Task<IResult> HandleCreateEvidencePack(
CreateEvidencePackRequest request,
IEvidencePackService evidencePackService,
TimeProvider timeProvider,
IGuidProvider guidProvider,
HttpContext httpContext,
CancellationToken cancellationToken)
{
@@ -108,7 +111,7 @@ public static class EvidencePackEndpoints
var claims = request.Claims.Select(c => new EvidenceClaim
{
ClaimId = c.ClaimId ?? $"claim-{Guid.NewGuid():N}"[..16],
ClaimId = c.ClaimId ?? $"claim-{guidProvider.NewGuid():N}"[..16],
Text = c.Text,
Type = Enum.TryParse<ClaimType>(c.Type, true, out var ct) ? ct : ClaimType.Custom,
Status = c.Status,
@@ -119,11 +122,11 @@ public static class EvidencePackEndpoints
var evidence = request.Evidence.Select(e => new EvidenceItem
{
EvidenceId = e.EvidenceId ?? $"ev-{Guid.NewGuid():N}"[..12],
EvidenceId = e.EvidenceId ?? $"ev-{guidProvider.NewGuid():N}"[..12],
Type = Enum.TryParse<EvidenceType>(e.Type, true, out var et) ? et : EvidenceType.Custom,
Uri = e.Uri,
Digest = e.Digest ?? "sha256:unknown",
CollectedAt = e.CollectedAt ?? DateTimeOffset.UtcNow,
CollectedAt = e.CollectedAt ?? timeProvider.GetUtcNow(),
Snapshot = EvidenceSnapshot.Custom(e.SnapshotType ?? "custom", (e.SnapshotData ?? new Dictionary<string, object>()).ToImmutableDictionary(x => x.Key, x => (object?)x.Value))
}).ToArray();

View File

@@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using StellaOps.AdvisoryAI.Runs;
using StellaOps.Determinism;
namespace StellaOps.AdvisoryAI.WebService.Endpoints;
@@ -407,6 +408,8 @@ public static class RunEndpoints
string runId,
[FromBody] AddArtifactRequestDto request,
[FromServices] IRunService runService,
[FromServices] TimeProvider timeProvider,
[FromServices] IGuidProvider guidProvider,
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
CancellationToken ct)
{
@@ -416,11 +419,11 @@ public static class RunEndpoints
{
var run = await runService.AddArtifactAsync(tenantId, runId, new RunArtifact
{
ArtifactId = request.ArtifactId ?? Guid.NewGuid().ToString("N"),
ArtifactId = request.ArtifactId ?? guidProvider.NewGuid().ToString("N"),
Type = request.Type,
Name = request.Name,
Description = request.Description,
CreatedAt = DateTimeOffset.UtcNow,
CreatedAt = timeProvider.GetUtcNow(),
ContentDigest = request.ContentDigest,
ContentSize = request.ContentSize,
MediaType = request.MediaType,

View File

@@ -55,6 +55,12 @@ public sealed class InMemoryAiConsentStore : IAiConsentStore
{
private readonly Dictionary<string, AiConsentRecord> _consents = new();
private readonly object _lock = new();
private readonly TimeProvider _timeProvider;
public InMemoryAiConsentStore(TimeProvider? timeProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
}
public Task<AiConsentRecord?> GetConsentAsync(string tenantId, string userId, CancellationToken cancellationToken = default)
{
@@ -64,7 +70,7 @@ public sealed class InMemoryAiConsentStore : IAiConsentStore
if (_consents.TryGetValue(key, out var record))
{
// Check expiration
if (record.ExpiresAt.HasValue && record.ExpiresAt.Value < DateTimeOffset.UtcNow)
if (record.ExpiresAt.HasValue && record.ExpiresAt.Value < _timeProvider.GetUtcNow())
{
_consents.Remove(key);
return Task.FromResult<AiConsentRecord?>(null);
@@ -78,7 +84,7 @@ public sealed class InMemoryAiConsentStore : IAiConsentStore
public Task<AiConsentRecord> GrantConsentAsync(string tenantId, string userId, AiConsentGrant grant, CancellationToken cancellationToken = default)
{
var key = MakeKey(tenantId, userId);
var now = DateTimeOffset.UtcNow;
var now = _timeProvider.GetUtcNow();
var record = new AiConsentRecord
{
TenantId = tenantId,

View File

@@ -53,11 +53,18 @@ public sealed record AiJustificationResult
public sealed class DefaultAiJustificationGenerator : IAiJustificationGenerator
{
private readonly ILogger<DefaultAiJustificationGenerator> _logger;
private readonly TimeProvider _timeProvider;
private readonly StellaOps.Determinism.IGuidProvider _guidProvider;
private const string ModelVersion = "advisory-ai-v1.2.0";
public DefaultAiJustificationGenerator(ILogger<DefaultAiJustificationGenerator> logger)
public DefaultAiJustificationGenerator(
ILogger<DefaultAiJustificationGenerator> logger,
TimeProvider? timeProvider = null,
StellaOps.Determinism.IGuidProvider? guidProvider = null)
{
_logger = logger;
_timeProvider = timeProvider ?? TimeProvider.System;
_guidProvider = guidProvider ?? StellaOps.Determinism.SystemGuidProvider.Instance;
}
public Task<AiJustificationResult> GenerateAsync(AiJustificationRequest request, CancellationToken cancellationToken = default)
@@ -74,13 +81,13 @@ public sealed class DefaultAiJustificationGenerator : IAiJustificationGenerator
var result = new AiJustificationResult
{
JustificationId = $"justify-{Guid.NewGuid():N}",
JustificationId = $"justify-{_guidProvider.NewGuid():N}",
DraftJustification = justification,
SuggestedJustificationType = suggestedType,
ConfidenceScore = confidence,
EvidenceSuggestions = evidenceSuggestions,
ModelVersion = ModelVersion,
GeneratedAt = DateTimeOffset.UtcNow,
GeneratedAt = _timeProvider.GetUtcNow(),
TraceId = request.CorrelationId
};

View File

@@ -17,5 +17,7 @@
<ProjectReference Include="..\..\__Libraries\StellaOps.AdvisoryAI.Attestation\StellaOps.AdvisoryAI.Attestation.csproj" />
<!-- Evidence Packs (Sprint: SPRINT_20260109_011_005) -->
<ProjectReference Include="..\..\__Libraries\StellaOps.Evidence.Pack\StellaOps.Evidence.Pack.csproj" />
<!-- Determinism abstractions -->
<ProjectReference Include="..\..\__Libraries\StellaOps.Determinism.Abstractions\StellaOps.Determinism.Abstractions.csproj" />
</ItemGroup>
</Project>

View File

@@ -168,17 +168,19 @@ public sealed record ReplayVerificationResult
public sealed class AIArtifactReplayer : IAIArtifactReplayer
{
private readonly ILlmProvider _provider;
private readonly TimeProvider _timeProvider;
public AIArtifactReplayer(ILlmProvider provider)
public AIArtifactReplayer(ILlmProvider provider, TimeProvider? timeProvider = null)
{
_provider = provider;
_timeProvider = timeProvider ?? TimeProvider.System;
}
public async Task<ReplayResult> ReplayAsync(
AIArtifactReplayManifest manifest,
CancellationToken cancellationToken = default)
{
var startTime = DateTime.UtcNow;
var startTime = _timeProvider.GetUtcNow();
try
{
@@ -191,7 +193,7 @@ public sealed class AIArtifactReplayer : IAIArtifactReplayer
ReplayedOutput = string.Empty,
ReplayedOutputHash = string.Empty,
Identical = false,
Duration = DateTime.UtcNow - startTime,
Duration = _timeProvider.GetUtcNow() - startTime,
ErrorMessage = "Replay requires temperature=0 for determinism"
};
}
@@ -205,7 +207,7 @@ public sealed class AIArtifactReplayer : IAIArtifactReplayer
ReplayedOutput = string.Empty,
ReplayedOutputHash = string.Empty,
Identical = false,
Duration = DateTime.UtcNow - startTime,
Duration = _timeProvider.GetUtcNow() - startTime,
ErrorMessage = $"Model {manifest.ModelId} is not available"
};
}
@@ -233,7 +235,7 @@ public sealed class AIArtifactReplayer : IAIArtifactReplayer
ReplayedOutput = result.Content,
ReplayedOutputHash = replayedHash,
Identical = identical,
Duration = DateTime.UtcNow - startTime
Duration = _timeProvider.GetUtcNow() - startTime
};
}
catch (Exception ex)
@@ -244,7 +246,7 @@ public sealed class AIArtifactReplayer : IAIArtifactReplayer
ReplayedOutput = string.Empty,
ReplayedOutputHash = string.Empty,
Identical = false,
Duration = DateTime.UtcNow - startTime,
Duration = _timeProvider.GetUtcNow() - startTime,
ErrorMessage = ex.Message
};
}

View File

@@ -20,6 +20,7 @@ public sealed class ConversationStore : IConversationStore, IAsyncDisposable
private readonly NpgsqlDataSource _dataSource;
private readonly ILogger<ConversationStore> _logger;
private readonly ConversationStoreOptions _options;
private readonly TimeProvider _timeProvider;
private static readonly JsonSerializerOptions JsonOptions = new()
{
@@ -32,11 +33,13 @@ public sealed class ConversationStore : IConversationStore, IAsyncDisposable
public ConversationStore(
NpgsqlDataSource dataSource,
ILogger<ConversationStore> logger,
ConversationStoreOptions? options = null)
ConversationStoreOptions? options = null,
TimeProvider? timeProvider = null)
{
_dataSource = dataSource;
_logger = logger;
_options = options ?? new ConversationStoreOptions();
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// <inheritdoc />
@@ -217,7 +220,7 @@ public sealed class ConversationStore : IConversationStore, IAsyncDisposable
WHERE updated_at < @cutoff
""";
var cutoff = DateTimeOffset.UtcNow - maxAge;
var cutoff = _timeProvider.GetUtcNow() - maxAge;
await using var cmd = _dataSource.CreateCommand(sql);
cmd.Parameters.AddWithValue("cutoff", cutoff);

View File

@@ -13,7 +13,6 @@
<PackageReference Include="Microsoft.Extensions.Configuration" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
</ItemGroup>
<ItemGroup>