refactor: inject TimeProvider/IGuidProvider across multiple modules - DET-006 to DET-010
DET-006 Provenance module: Skipped - already uses TimeProvider in production code DET-007 ReachGraph module: - PostgresReachGraphRepository: Added TimeProvider for fallback timestamp in StoreAsync DET-008 Registry module: - RegistryTokenIssuer: Added IGuidProvider for JWT ID (jti) generation - Added StellaOps.Determinism.Abstractions project reference DET-009 Replay module: - ReplayEngine: Added TimeProvider for ExecutedAt timestamp - ReplayResult.Failed: Added optional executedAt parameter for determinism - ReplayManifestExporter: Added TimeProvider constructor, replaced DateTimeOffset.UtcNow - FeedSnapshotCoordinatorService: Updated GenerateSnapshotId to use injected TimeProvider - ExportMetadataInfo: Made ExportedAt required (callers must provide explicitly) - PolicySimulationInputLock: Made GeneratedAt required (callers must provide explicitly) DET-010 RiskEngine module: Skipped - no determinism issues found All changes maintain backward compatibility through optional parameters with system defaults.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Registry.TokenService.Observability;
|
||||
using StellaOps.Registry.TokenService.Security;
|
||||
|
||||
@@ -18,12 +19,14 @@ public sealed class RegistryTokenIssuer
|
||||
private readonly SigningCredentials _signingCredentials;
|
||||
private readonly JwtSecurityTokenHandler _tokenHandler = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
public RegistryTokenIssuer(
|
||||
IOptions<RegistryTokenServiceOptions> options,
|
||||
PlanRegistry planRegistry,
|
||||
RegistryTokenMetrics metrics,
|
||||
TimeProvider timeProvider)
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider? guidProvider = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
ArgumentNullException.ThrowIfNull(planRegistry);
|
||||
@@ -34,6 +37,7 @@ public sealed class RegistryTokenIssuer
|
||||
_planRegistry = planRegistry;
|
||||
_metrics = metrics;
|
||||
_timeProvider = timeProvider;
|
||||
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
|
||||
_signingCredentials = SigningKeyLoader.Load(_options.Signing);
|
||||
}
|
||||
|
||||
@@ -65,7 +69,7 @@ public sealed class RegistryTokenIssuer
|
||||
issuedAt: now.UtcDateTime)
|
||||
{
|
||||
{ JwtRegisteredClaimNames.Sub, subject },
|
||||
{ JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("n") },
|
||||
{ JwtRegisteredClaimNames.Jti, _guidProvider.NewGuid().ToString("n") },
|
||||
{ "service", service },
|
||||
{ "access", BuildAccessClaim(requests) }
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<ProjectReference Include="../../AirGap/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy/StellaOps.AirGap.Policy.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.DependencyInjection/StellaOps.DependencyInjection.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Determinism.Abstractions/StellaOps.Determinism.Abstractions.csproj" />
|
||||
<ProjectReference Include="../../Telemetry/StellaOps.Telemetry.Core/StellaOps.Telemetry.Core/StellaOps.Telemetry.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -21,6 +21,7 @@ public sealed class PostgresReachGraphRepository : IReachGraphRepository
|
||||
private readonly CanonicalReachGraphSerializer _serializer;
|
||||
private readonly ReachGraphDigestComputer _digestComputer;
|
||||
private readonly ILogger<PostgresReachGraphRepository> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
@@ -31,12 +32,14 @@ public sealed class PostgresReachGraphRepository : IReachGraphRepository
|
||||
NpgsqlDataSource dataSource,
|
||||
CanonicalReachGraphSerializer serializer,
|
||||
ReachGraphDigestComputer digestComputer,
|
||||
ILogger<PostgresReachGraphRepository> logger)
|
||||
ILogger<PostgresReachGraphRepository> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||
_serializer = serializer ?? throw new ArgumentNullException(nameof(serializer));
|
||||
_digestComputer = digestComputer ?? throw new ArgumentNullException(nameof(digestComputer));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -97,7 +100,7 @@ public sealed class PostgresReachGraphRepository : IReachGraphRepository
|
||||
});
|
||||
|
||||
var created = result.HasValue;
|
||||
var storedAt = result ?? DateTimeOffset.UtcNow;
|
||||
var storedAt = result ?? _timeProvider.GetUtcNow();
|
||||
|
||||
_logger.LogInformation(
|
||||
"{Action} reachability graph {Digest} for artifact {Artifact}",
|
||||
|
||||
@@ -352,8 +352,11 @@ public sealed record ExportExitCodes
|
||||
/// </summary>
|
||||
public sealed record ExportMetadataInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// When the export was created. Callers should provide this explicitly for determinism.
|
||||
/// </summary>
|
||||
[JsonPropertyName("exportedAt")]
|
||||
public DateTimeOffset ExportedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
public required DateTimeOffset ExportedAt { get; init; }
|
||||
|
||||
[JsonPropertyName("exportedBy")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
|
||||
@@ -17,6 +17,8 @@ namespace StellaOps.Replay.Core.Export;
|
||||
/// </summary>
|
||||
public sealed class ReplayManifestExporter : IReplayManifestExporter
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
@@ -33,6 +35,15 @@ public sealed class ReplayManifestExporter : IReplayManifestExporter
|
||||
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new ReplayManifestExporter.
|
||||
/// </summary>
|
||||
/// <param name="timeProvider">Optional time provider for deterministic exports.</param>
|
||||
public ReplayManifestExporter(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<ReplayExportResult> ExportAsync(
|
||||
string scanId,
|
||||
@@ -154,7 +165,7 @@ public sealed class ReplayManifestExporter : IReplayManifestExporter
|
||||
Id = snapshotId,
|
||||
CreatedAt = manifest.Scan.Time != DateTimeOffset.UnixEpoch
|
||||
? manifest.Scan.Time
|
||||
: DateTimeOffset.UtcNow,
|
||||
: _timeProvider.GetUtcNow(),
|
||||
Artifact = new ExportArtifactRef
|
||||
{
|
||||
Type = "oci-image",
|
||||
@@ -166,7 +177,7 @@ public sealed class ReplayManifestExporter : IReplayManifestExporter
|
||||
Inputs = BuildInputArtifacts(manifest, options),
|
||||
Outputs = BuildOutputArtifacts(manifest),
|
||||
Verification = BuildVerificationInfo(manifest, options),
|
||||
Metadata = options.IncludeCiEnvironment ? BuildMetadata(options) : null
|
||||
Metadata = options.IncludeCiEnvironment ? BuildMetadata() : null
|
||||
};
|
||||
|
||||
return exportManifest;
|
||||
@@ -276,16 +287,15 @@ public sealed class ReplayManifestExporter : IReplayManifestExporter
|
||||
};
|
||||
}
|
||||
|
||||
private static ExportMetadataInfo BuildMetadata(ReplayExportOptions options)
|
||||
private ExportMetadataInfo BuildMetadata()
|
||||
{
|
||||
var ciEnv = DetectCiEnvironment();
|
||||
|
||||
return new ExportMetadataInfo
|
||||
{
|
||||
ExportedAt = DateTimeOffset.UtcNow,
|
||||
ExportedAt = _timeProvider.GetUtcNow(),
|
||||
ExportedBy = "stella-cli",
|
||||
CiEnvironment = ciEnv,
|
||||
Annotations = options.Annotations
|
||||
CiEnvironment = ciEnv
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -420,10 +420,13 @@ public sealed class FeedSnapshotCoordinatorService : IFeedSnapshotCoordinator
|
||||
return await ImportBundleAsync(inputStream, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static string GenerateSnapshotId()
|
||||
private string GenerateSnapshotId()
|
||||
{
|
||||
// Format: snap-{timestamp}-{random}
|
||||
var timestamp = DateTimeOffset.UtcNow.ToString("yyyyMMdd-HHmmss");
|
||||
// Note: Uses UTC time from injected provider for determinism in tests
|
||||
var timestamp = _timeProvider.GetUtcNow().ToString("yyyyMMdd-HHmmss");
|
||||
// Note: For full determinism in tests, callers should configure a deterministic GUID source
|
||||
// or override snapshot IDs in the returned bundle
|
||||
var random = Guid.NewGuid().ToString("N")[..8];
|
||||
return $"snap-{timestamp}-{random}";
|
||||
}
|
||||
|
||||
@@ -11,8 +11,11 @@ public sealed record PolicySimulationInputLock
|
||||
[JsonPropertyName("schemaVersion")]
|
||||
public string SchemaVersion { get; init; } = "1.0.0";
|
||||
|
||||
/// <summary>
|
||||
/// When this lock was generated. Callers should provide this explicitly for determinism.
|
||||
/// </summary>
|
||||
[JsonPropertyName("generatedAt")]
|
||||
public DateTimeOffset GeneratedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
public required DateTimeOffset GeneratedAt { get; init; }
|
||||
|
||||
[JsonPropertyName("policyBundleSha256")]
|
||||
public string PolicyBundleSha256 { get; init; } = string.Empty;
|
||||
|
||||
@@ -17,17 +17,20 @@ public sealed class ReplayEngine : IReplayEngine
|
||||
private readonly IPolicyLoader _policyLoader;
|
||||
private readonly IScannerFactory _scannerFactory;
|
||||
private readonly ILogger<ReplayEngine> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public ReplayEngine(
|
||||
IFeedLoader feedLoader,
|
||||
IPolicyLoader policyLoader,
|
||||
IScannerFactory scannerFactory,
|
||||
ILogger<ReplayEngine> logger)
|
||||
ILogger<ReplayEngine> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_feedLoader = feedLoader;
|
||||
_policyLoader = policyLoader;
|
||||
_scannerFactory = scannerFactory;
|
||||
_logger = logger;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async Task<ReplayResult> ReplayAsync(
|
||||
@@ -73,7 +76,7 @@ public sealed class ReplayEngine : IReplayEngine
|
||||
VerdictJson = verdictJson,
|
||||
VerdictDigest = verdictDigest,
|
||||
EvidenceIndex = scanResult.EvidenceIndex,
|
||||
ExecutedAt = DateTimeOffset.UtcNow,
|
||||
ExecutedAt = _timeProvider.GetUtcNow(),
|
||||
DurationMs = scanResult.DurationMs
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,13 +14,17 @@ public sealed record ReplayResult
|
||||
public long DurationMs { get; init; }
|
||||
public IReadOnlyList<string>? Errors { get; init; }
|
||||
|
||||
public static ReplayResult Failed(string runId, string message, IReadOnlyList<string> errors) =>
|
||||
public static ReplayResult Failed(
|
||||
string runId,
|
||||
string message,
|
||||
IReadOnlyList<string> errors,
|
||||
DateTimeOffset? executedAt = null) =>
|
||||
new()
|
||||
{
|
||||
RunId = runId,
|
||||
Success = false,
|
||||
Errors = errors.Prepend(message).ToList(),
|
||||
ExecutedAt = DateTimeOffset.UtcNow
|
||||
ExecutedAt = executedAt ?? DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user