feat: harden package lifecycle content boundaries

This commit is contained in:
codex
2026-08-06 09:59:17 +03:00
parent 0e38d8fad2
commit 64fa1f5664
4 changed files with 66 additions and 15 deletions
+3 -2
View File
@@ -17,13 +17,14 @@ public sealed record ValidationResult(IReadOnlyList<ValidationIssue> Issues)
{ public bool IsValid => Issues.Count == 0; public static ValidationResult Success { get; } = new([]); }
public sealed record PackageProgress(string Stage, int Completed, int Total);
public sealed record ActivationResult(string CurrentVersion, string? PreviousVersion);
public sealed record PackageActivationReceipt(string OperationId, string PackageId, string Version, string? PreviousVersion, DateTimeOffset ActivatedUtc, long PointerGeneration = 0, string? ActiveRootIdentity = null, int SchemaVersion = 1, string? ManifestSha256 = null, string? PayloadSetSha256 = null, string? RosterIdentity = null);
public sealed record PackagePayloadBinding(string RelativePath,long Length,string Sha256);
public sealed record PackageActivationReceipt(string OperationId, string PackageId, string Version, string? PreviousVersion, DateTimeOffset ActivatedUtc, long PointerGeneration = 0, string? ActiveRootIdentity = null, int SchemaVersion = 2, string? ManifestSha256 = null, string? PayloadSetSha256 = null, string? RosterIdentity = null, IReadOnlyList<PackagePayloadBinding>? PayloadFiles = null, long PointerGenerationBefore = 0, string? EvidenceCorrelationId = null);
public sealed record PackageActivationResult(string Outcome, PackageActivationReceipt? Receipt);
public sealed record PackageRollbackResult(string Outcome, string? CurrentVersion);
public sealed record ImmutableLocalPackage(string PackageId, string Version, string LocalPackageRoot);
public sealed record PackageStagingOwnership(string OperationId, string PackageId, string Version, string StagingDirectory, DateTimeOffset CreatedUtc, string MarkerIdentity);
public sealed record PackageStagingCleanupResult(string Outcome, string OperationId);
public enum PackageLifecycleFaultPoint { AfterStagingAllocated, DuringPayloadCopy, BeforeImmutableCommit, BeforePointerTempWrite, BeforePointerReplace, BeforePointerReadback, BeforeRollbackReplace, DuringOwnedStagingCleanup }
public enum PackageLifecycleFaultPoint { AfterStagingAllocated, DuringPayloadCopy, AfterPayloadCopyBeforeValidation, BeforeImmutableCommit, AfterImmutableCommitBeforeReturn, BeforePointerTempWrite, BeforePointerReplace, BeforePointerReadback, BeforeRollbackReplace, BeforeRollbackReadback, DuringOwnedStagingCleanup }
public interface IPackageLifecycleFaultInjector { void ThrowIfRequested(PackageLifecycleFaultPoint point); }
public sealed class NoOpPackageLifecycleFaultInjector : IPackageLifecycleFaultInjector { public void ThrowIfRequested(PackageLifecycleFaultPoint point) { } }
public interface IPackageMaterializer { Task<ImmutableLocalPackage> MaterializeAsync(PackageManifest manifest, string sourceRoot, CancellationToken cancellationToken, IProgress<PackageProgress>? progress = null); }
+19 -1
View File
@@ -37,6 +37,15 @@ public sealed record ResolvedPackageStagingRoot { internal ResolvedPackageStagin
public sealed record ResolvedActivePackageRoot { internal ResolvedActivePackageRoot(string value)=>Value=value; public string Value{get;} }
public sealed record ResolvedPackageLifecycleEvidenceRoot { internal ResolvedPackageLifecycleEvidenceRoot(string value)=>Value=value; public string Value{get;} }
public sealed record ResolvedPackagePilotRoot { internal ResolvedPackagePilotRoot(string value)=>Value=value; public string Value{get;} }
public sealed record ResolvedPackageCatalogRoot { internal ResolvedPackageCatalogRoot(string value)=>Value=value; public string Value{get;} }
public sealed record ResolvedImmutablePackagePath { internal ResolvedImmutablePackagePath(string value)=>Value=value; public string Value{get;} }
public sealed record ResolvedPackageLifecycleRoots(
ResolvedPackagePilotRoot Pilot,
ResolvedPackageCatalogRoot Catalog,
ResolvedPackageStagingRoot Staging,
ResolvedPackageVersionStoreRoot VersionStore,
ResolvedActivePackageRoot Active,
ResolvedPackageLifecycleEvidenceRoot Evidence);
public interface IPathResolver
{
@@ -48,6 +57,7 @@ public interface IPathResolver
ResolvedLegacyCompatibilityOverlayPath ResolveLegacyCompatibilityOverlayPath(LauncherPathVariables variables);
ResolvedLegacyProbeEvidenceRoot ResolveLegacyProbeEvidenceRoot(LauncherPathVariables variables);
ResolvedPackagePilotRoot ResolvePackagePilotRoot(string value);
ResolvedPackageLifecycleRoots ResolvePackageLifecycleRoots(string value);
}
public sealed class PathResolver(IReadOnlyDictionary<string, string?>? environment = null) : IPathResolver
@@ -117,7 +127,15 @@ public sealed class PathResolver(IReadOnlyDictionary<string, string?>? environme
return new(value);
}
public ResolvedPackagePilotRoot ResolvePackagePilotRoot(string value)
{ if (string.IsNullOrWhiteSpace(value)||value.Contains('*')||value.Contains('?')||value.StartsWith(@"\\")||!Path.IsPathFullyQualified(value)) throw new InvalidOperationException("PACKAGE_PILOT_ROOT_NOT_LOCAL"); var full=Path.GetFullPath(value); var info=new DirectoryInfo(full); if(info.Exists&&info.Attributes.HasFlag(FileAttributes.ReparsePoint)) throw new InvalidOperationException("PACKAGE_ROOT_REPARSE_REJECTED"); return new(full); }
{ if (string.IsNullOrWhiteSpace(value)||Uri.IsWellFormedUriString(value,UriKind.Absolute)||value.Contains('*')||value.Contains('?')||value.StartsWith(@"\\")||!Path.IsPathFullyQualified(value)) throw new InvalidOperationException("PACKAGE_PILOT_ROOT_NOT_LOCAL"); var full=Path.GetFullPath(value); var info=new DirectoryInfo(full); if(info.Exists&&info.Attributes.HasFlag(FileAttributes.ReparsePoint)) throw new InvalidOperationException("PACKAGE_ROOT_REPARSE_REJECTED"); return new(full); }
public ResolvedPackageLifecycleRoots ResolvePackageLifecycleRoots(string value)
{
var pilot=ResolvePackagePilotRoot(value); string Child(string name)=>Path.GetFullPath(Path.Combine(pilot.Value,name));
var paths=new[]{Child("catalog"),Child("staging"),Child("version-store"),Child("active"),Child("evidence")};
if(paths.Distinct(StringComparer.OrdinalIgnoreCase).Count()!=paths.Length) throw new InvalidOperationException("PACKAGE_ROOT_COLLISION");
foreach(var path in paths) if(!path.StartsWith(pilot.Value.TrimEnd(Path.DirectorySeparatorChar)+Path.DirectorySeparatorChar,StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("PACKAGE_ROOT_TOPOLOGY_INVALID");
return new(pilot,new(paths[0]),new(paths[1]),new(paths[2]),new(paths[3]),new(paths[4]));
}
private string ResolveExistingFile(string role, LauncherPathVariables variables)
{
var value=ResolvePath(Resolve(variables,[role]).Require(role),variables);