partly or unimplemented features - now implemented

This commit is contained in:
master
2026-02-09 08:53:51 +02:00
parent 1bf6bbf395
commit 4bdc298ec1
674 changed files with 90194 additions and 2271 deletions

View File

@@ -0,0 +1,342 @@
// Licensed to StellaOps under the BUSL-1.1 license.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StellaOps.Cli.Configuration;
using StellaOps.Cli.Plugins;
using StellaOps.PolicyDsl;
using System.CommandLine;
using System.Text.Json;
using System.Text;
namespace StellaOps.Cli.Plugins.Policy;
/// <summary>
/// CLI plugin module for policy DSL commands.
/// Provides 'stella policy lint', 'stella policy compile', and 'stella policy simulate'.
/// </summary>
public sealed class PolicyCliCommandModule : ICliCommandModule
{
public string Name => "stellaops.cli.plugins.policy";
public bool IsAvailable(IServiceProvider services) => true;
public void RegisterCommands(
RootCommand root,
IServiceProvider services,
StellaOpsCliOptions options,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(root);
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(verboseOption);
root.Add(BuildPolicyCommand(services, verboseOption, options, cancellationToken));
}
private static Command BuildPolicyCommand(
IServiceProvider services,
Option<bool> verboseOption,
StellaOpsCliOptions options,
CancellationToken cancellationToken)
{
var policy = new Command("policy", "Policy DSL operations: lint, compile, and simulate.");
policy.Add(BuildLintCommand(services, verboseOption));
policy.Add(BuildCompileCommand(services, verboseOption));
policy.Add(BuildSimulateCommand(services, verboseOption));
return policy;
}
private static Command BuildLintCommand(
IServiceProvider services,
Option<bool> verboseOption)
{
var fileArg = new Argument<FileInfo>("file", "Path to .stella policy file to lint.");
var outputOption = new Option<string?>("--output", new[] { "-o" })
{
Description = "Output format: text, json. Default: text"
};
var lintCommand = new Command("lint", "Lint a policy DSL file for syntax and semantic errors.")
{
fileArg,
outputOption
};
lintCommand.SetHandler(async (file, output, verbose) =>
{
var logger = services.GetRequiredService<ILogger<PolicyCliCommandModule>>();
if (!file.Exists)
{
Console.Error.WriteLine($"Error: File not found: {file.FullName}");
Environment.ExitCode = 1;
return;
}
var source = await File.ReadAllTextAsync(file.FullName);
var result = PolicyParser.Parse(source);
var outputFormat = output?.ToLowerInvariant() ?? "text";
if (outputFormat == "json")
{
var jsonResult = new
{
file = file.FullName,
success = !result.Diagnostics.Any(d => d.Severity == StellaOps.Policy.PolicyIssueSeverity.Error),
diagnostics = result.Diagnostics.Select(d => new
{
severity = d.Severity.ToString().ToLowerInvariant(),
code = d.Code,
message = d.Message,
path = d.Path,
location = d.Location is not null ? new
{
line = d.Location.Line,
column = d.Location.Column
} : null
})
};
Console.WriteLine(JsonSerializer.Serialize(jsonResult, new JsonSerializerOptions { WriteIndented = true }));
}
else
{
var errors = result.Diagnostics.Where(d => d.Severity == StellaOps.Policy.PolicyIssueSeverity.Error).ToList();
var warnings = result.Diagnostics.Where(d => d.Severity == StellaOps.Policy.PolicyIssueSeverity.Warning).ToList();
var infos = result.Diagnostics.Where(d => d.Severity == StellaOps.Policy.PolicyIssueSeverity.Info).ToList();
if (errors.Count == 0 && warnings.Count == 0)
{
Console.WriteLine($"✓ {file.Name}: No issues found.");
}
else
{
Console.WriteLine($"Linting {file.Name}:");
foreach (var diag in result.Diagnostics.OrderBy(d => d.Location?.Line ?? 0))
{
var symbol = diag.Severity switch
{
StellaOps.Policy.PolicyIssueSeverity.Error => "✗",
StellaOps.Policy.PolicyIssueSeverity.Warning => "⚠",
_ => ""
};
var location = diag.Location is not null ? $":{diag.Location.Line}:{diag.Location.Column}" : "";
Console.WriteLine($" {symbol} [{diag.Code}]{location}: {diag.Message}");
}
Console.WriteLine();
Console.WriteLine($"Summary: {errors.Count} error(s), {warnings.Count} warning(s), {infos.Count} info(s)");
}
if (errors.Count > 0)
{
Environment.ExitCode = 1;
}
}
}, fileArg, outputOption, verboseOption);
return lintCommand;
}
private static Command BuildCompileCommand(
IServiceProvider services,
Option<bool> verboseOption)
{
var fileArg = new Argument<FileInfo>("file", "Path to .stella policy file to compile.");
var outputOption = new Option<FileInfo?>("--output", new[] { "-o" })
{
Description = "Output path for compiled IR (.json). Default: stdout"
};
var checksumOnlyOption = new Option<bool>("--checksum-only")
{
Description = "Only output the deterministic checksum."
};
var compileCommand = new Command("compile", "Compile a policy DSL file to intermediate representation.")
{
fileArg,
outputOption,
checksumOnlyOption
};
compileCommand.SetHandler(async (file, output, checksumOnly, verbose) =>
{
var logger = services.GetRequiredService<ILogger<PolicyCliCommandModule>>();
if (!file.Exists)
{
Console.Error.WriteLine($"Error: File not found: {file.FullName}");
Environment.ExitCode = 1;
return;
}
var source = await File.ReadAllTextAsync(file.FullName);
var compiler = new PolicyCompiler();
var result = compiler.Compile(source);
if (!result.Success)
{
Console.Error.WriteLine($"Compilation failed for {file.Name}:");
foreach (var diag in result.Diagnostics.Where(d => d.Severity == StellaOps.Policy.PolicyIssueSeverity.Error))
{
var location = diag.Location is not null ? $":{diag.Location.Line}:{diag.Location.Column}" : "";
Console.Error.WriteLine($" ✗ [{diag.Code}]{location}: {diag.Message}");
}
Environment.ExitCode = 1;
return;
}
if (checksumOnly)
{
Console.WriteLine(result.Checksum);
return;
}
var irBytes = PolicyIrSerializer.Serialize(result.Document!);
var irJson = Encoding.UTF8.GetString(irBytes.AsSpan());
if (output is not null)
{
await File.WriteAllTextAsync(output.FullName, irJson);
Console.WriteLine($"✓ Compiled {file.Name} -> {output.Name}");
Console.WriteLine($" Checksum: {result.Checksum}");
}
else
{
Console.WriteLine(irJson);
}
}, fileArg, outputOption, checksumOnlyOption, verboseOption);
return compileCommand;
}
private static Command BuildSimulateCommand(
IServiceProvider services,
Option<bool> verboseOption)
{
var fileArg = new Argument<FileInfo>("file", "Path to .stella policy file.");
var signalsOption = new Option<FileInfo?>("--signals", new[] { "-s" })
{
Description = "Path to signals context JSON file."
};
var outputOption = new Option<string?>("--output", new[] { "-o" })
{
Description = "Output format: text, json. Default: text"
};
var simulateCommand = new Command("simulate", "Simulate policy evaluation with given signal context.")
{
fileArg,
signalsOption,
outputOption
};
simulateCommand.SetHandler(async (file, signals, output, verbose) =>
{
var logger = services.GetRequiredService<ILogger<PolicyCliCommandModule>>();
if (!file.Exists)
{
Console.Error.WriteLine($"Error: Policy file not found: {file.FullName}");
Environment.ExitCode = 1;
return;
}
var source = await File.ReadAllTextAsync(file.FullName);
var factory = new PolicyEngineFactory();
var engineResult = factory.CreateFromSource(source);
if (engineResult.Engine is null)
{
Console.Error.WriteLine($"Compilation failed. Run 'stella policy lint' for details.");
foreach (var diag in engineResult.Diagnostics.Where(d => d.Severity == StellaOps.Policy.PolicyIssueSeverity.Error))
{
var location = diag.Location is not null ? $":{diag.Location.Line}:{diag.Location.Column}" : "";
Console.Error.WriteLine($" ✗ [{diag.Code}]{location}: {diag.Message}");
}
Environment.ExitCode = 1;
return;
}
// Load signals context
SignalContext signalContext;
if (signals is not null && signals.Exists)
{
var signalsJson = await File.ReadAllTextAsync(signals.FullName);
var signalsDict = JsonSerializer.Deserialize<Dictionary<string, object?>>(signalsJson);
signalContext = new SignalContext(signalsDict ?? new Dictionary<string, object?>());
}
else
{
signalContext = new SignalContext();
}
// Run simulation
var engine = engineResult.Engine;
var evaluationResult = engine.Evaluate(signalContext);
var outputFormat = output?.ToLowerInvariant() ?? "text";
var verdict = evaluationResult.MatchedRules.Length > 0 ? "match" : "no-match";
if (outputFormat == "json")
{
var jsonResult = new
{
policy = evaluationResult.PolicyName,
policyChecksum = evaluationResult.PolicyChecksum,
verdict,
matchedRules = evaluationResult.MatchedRules,
actions = evaluationResult.Actions.Select(a => new
{
ruleName = a.RuleName,
action = a.Action.ActionName,
wasElseBranch = a.WasElseBranch
})
};
Console.WriteLine(JsonSerializer.Serialize(jsonResult, new JsonSerializerOptions { WriteIndented = true }));
}
else
{
Console.WriteLine($"Policy: {evaluationResult.PolicyName}");
Console.WriteLine($"Checksum: {evaluationResult.PolicyChecksum}");
Console.WriteLine();
if (evaluationResult.MatchedRules.Length > 0)
{
Console.WriteLine($"✓ Matched Rules ({evaluationResult.MatchedRules.Length}):");
foreach (var rule in evaluationResult.MatchedRules)
{
Console.WriteLine($" - {rule}");
}
}
else
{
Console.WriteLine("No rules matched.");
}
if (evaluationResult.Actions.Length > 0)
{
Console.WriteLine();
Console.WriteLine($"Actions ({evaluationResult.Actions.Length}):");
foreach (var action in evaluationResult.Actions)
{
var branch = action.WasElseBranch ? " (else)" : "";
Console.WriteLine($" - [{action.RuleName}]{branch}: {action.Action.ActionName}");
}
}
}
}, fileArg, signalsOption, outputOption, verboseOption);
return simulateCommand;
}
}

View File

@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- StellaOps.Cli.Plugins.Policy: CLI plugin for policy DSL operations (lint, compile, simulate) -->
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<PluginOutputDirectory>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\..\plugins\cli\StellaOps.Cli.Plugins.Policy\'))</PluginOutputDirectory>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.Cli\StellaOps.Cli.csproj" />
<ProjectReference Include="..\..\..\Policy\StellaOps.PolicyDsl\StellaOps.PolicyDsl.csproj" />
</ItemGroup>
<Target Name="CopyPluginBinaries" AfterTargets="Build">
<MakeDir Directories="$(PluginOutputDirectory)" />
<ItemGroup>
<PluginArtifacts Include="$(TargetDir)$(TargetFileName)" />
<PluginArtifacts Include="$(TargetDir)$(TargetName).pdb" Condition="Exists('$(TargetDir)$(TargetName).pdb')" />
<PluginArtifacts Include="$(TargetDir)$(TargetName).deps.json" Condition="Exists('$(TargetDir)$(TargetName).deps.json')" />
<PluginArtifacts Include="$(TargetDir)$(TargetName).runtimeconfig.json" Condition="Exists('$(TargetDir)$(TargetName).runtimeconfig.json')" />
<PluginArtifacts Include="@(ReferenceCopyLocalPaths)" />
</ItemGroup>
<Copy SourceFiles="@(PluginArtifacts)" DestinationFolder="$(PluginOutputDirectory)" SkipUnchangedFiles="true" />
</Target>
</Project>