feat: route legacy configuration probe through CLI

This commit is contained in:
codex
2026-08-06 07:40:51 +03:00
parent 84ddac90a8
commit 0b890432d5
3 changed files with 75 additions and 15 deletions
+10 -1
View File
@@ -2,5 +2,14 @@ namespace Launcher.Next.App;
public partial class App : System.Windows.Application
{
protected override async void OnStartup(System.Windows.StartupEventArgs e)
{ base.OnStartup(e); if (e.Args.FirstOrDefault()?.Equals("pilot", StringComparison.OrdinalIgnoreCase) == true) { Shutdown(await PilotCli.RunAsync(e.Args.Skip(1).ToArray())); return; } new MainWindow().Show(); }
{
base.OnStartup(e);
if (e.Args.FirstOrDefault()?.Equals("legacy-configuration-probe", StringComparison.OrdinalIgnoreCase) == true)
{
Shutdown(await LegacyConfigurationProbeCli.RunAsync(e.Args.Skip(1).ToArray()));
return;
}
if (e.Args.FirstOrDefault()?.Equals("pilot", StringComparison.OrdinalIgnoreCase) == true) { Shutdown(await PilotCli.RunAsync(e.Args.Skip(1).ToArray())); return; }
new MainWindow().Show();
}
}
@@ -0,0 +1,47 @@
using Launcher.Next.Application;
using Launcher.Next.Core;
using System.IO;
namespace Launcher.Next.App;
/// <summary>Dedicated, non-Autodesk command route. It deliberately constructs
/// only the read-only legacy configuration probe dependency graph.</summary>
internal static class LegacyConfigurationProbeCli
{
internal static async Task<int> RunAsync(string[] args)
{
if (args.Length != 4 || !string.Equals(args[0], "--mode", StringComparison.Ordinal) ||
!string.Equals(args[1], "LegacyConfigurationProbe", StringComparison.Ordinal) ||
!string.Equals(args[2], "--config", StringComparison.Ordinal) || string.IsNullOrWhiteSpace(args[3]))
return Fail("LEGACY_PROBE_MODE_CONFLICT");
try
{
if (!Path.IsPathFullyQualified(args[3]) || !File.Exists(args[3]) || Directory.Exists(args[3]))
return Fail("LEGACY_PROBE_OVERRIDE_MISSING");
var overrideDocument = LegacyProbeOverrideParser.Parse(await File.ReadAllTextAsync(args[3]));
var variables = new LauncherPathVariables(new Dictionary<string, string?>
{
["LegacySettingsPath"] = overrideDocument.SettingsXml,
["LegacyDownloadListPath"] = overrideDocument.DownloadListXml,
["LegacyCompatibilityOverlayPath"] = overrideDocument.CompatibilityOverlay,
["LegacyProbeEvidenceRoot"] = overrideDocument.EvidenceOutputRoot
});
var resolver = new PathResolver();
var request = new LegacyConfigurationProbeRequest(
resolver.ResolveLegacySettingsPath(variables),
resolver.ResolveLegacyDownloadListPath(variables),
resolver.ResolveLegacyCompatibilityOverlayPath(variables),
resolver.ResolveLegacyProbeEvidenceRoot(variables),
overrideDocument.SelectedPattern,
Guid.NewGuid().ToString("N"));
var result = await new LegacyConfigurationProbe(new LegacyConfigurationAdapter()).RunAsync(request);
Console.WriteLine(result.TerminalState);
return result.TerminalState == "Succeeded" ? 0 : 2;
}
catch (InvalidOperationException ex) { return Fail(ex.Message); }
catch { return Fail("LEGACY_PROBE_INTERNAL_FAILURE"); }
}
private static int Fail(string code) { Console.Error.WriteLine(code); return 2; }
}
@@ -1,4 +1,5 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Launcher.Next.Core;
@@ -17,31 +18,34 @@ public sealed class LegacyConfigurationProbe(LegacyConfigurationAdapter adapter)
var selectionPath = request.DownloadList.Value;
var overlayPath = request.Overlay.Value;
var evidenceRoot = request.EvidenceRoot.Value;
if (!File.Exists(settingsPath) || !File.Exists(selectionPath)) throw new InvalidOperationException("LEGACY_PROBE_INPUT_PATH_INVALID");
if (Path.GetDirectoryName(settingsPath)!.Equals(evidenceRoot, StringComparison.OrdinalIgnoreCase) || Path.GetDirectoryName(selectionPath)!.Equals(evidenceRoot, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("PATH_ROLE_COLLISION");
if (!File.Exists(settingsPath) || !File.Exists(selectionPath) || !File.Exists(overlayPath)) throw new InvalidOperationException("LEGACY_PROBE_INPUT_PATH_INVALID");
if (evidenceRoot.StartsWith(@"\\", StringComparison.Ordinal) || SameOrNested(evidenceRoot, Path.GetDirectoryName(settingsPath)!) || SameOrNested(evidenceRoot, Path.GetDirectoryName(selectionPath)!) || SameOrNested(evidenceRoot, Path.GetDirectoryName(overlayPath)!)) throw new InvalidOperationException("LEGACY_PROBE_PATH_COLLISION");
Directory.CreateDirectory(evidenceRoot);
var before = await Snapshot(settingsPath, selectionPath, overlayPath, cancellationToken);
try
{
var settings = adapter.ParseSettings(await File.ReadAllTextAsync(settingsPath, cancellationToken));
var settings = adapter.ParseSettings(await ReadOnlyTextAsync(settingsPath, cancellationToken));
var pattern = settings.Patterns.Where(x => string.Equals(x.Name, request.PatternName, StringComparison.Ordinal)).ToArray();
if (pattern.Length != 1) throw new InvalidOperationException(pattern.Length == 0 ? "LEGACY_PATTERN_NOT_FOUND" : "LEGACY_PATTERN_AMBIGUOUS");
var overlay = LegacyCompatibilityOverlayParser.Parse(await File.ReadAllTextAsync(overlayPath, cancellationToken));
var requirements = adapter.Join(pattern[0], adapter.ParseDownloadList(await File.ReadAllTextAsync(selectionPath, cancellationToken)), overlay);
var overlay = LegacyCompatibilityOverlayParser.Parse(await ReadOnlyTextAsync(overlayPath, cancellationToken));
var requirements = adapter.Join(pattern[0], adapter.ParseDownloadList(await ReadOnlyTextAsync(selectionPath, cancellationToken)), overlay);
var after = await Snapshot(settingsPath, selectionPath, overlayPath, cancellationToken);
if (!before.SequenceEqual(after)) throw new InvalidOperationException("LEGACY_PROBE_SOURCE_CHANGED_DURING_READ");
var evidence = new { mode="LegacyConfigurationProbe", operationId=request.OperationId, terminalState="LEGACY_CONFIGURATION_VERIFIED", selectedPattern=pattern[0].Name, selectedIdCount=requirements.Count, sourceWrites=false, payloadCopied=false, stagingCreated=false, activationAttempted=false, autoCadLaunchAttempted=false, dfsWriteAttempted=false, inputSnapshots=before.Select(x=>Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(x)))) };
var output=Path.Combine(evidenceRoot, request.OperationId + ".json");
await File.WriteAllTextAsync(output, JsonSerializer.Serialize(evidence), cancellationToken);
return new("LEGACY_CONFIGURATION_VERIFIED",requirements,output,[]);
var output = await WriteEvidenceAsync(evidenceRoot, request.OperationId, new { evidenceSchemaVersion=1, evidenceKind="legacy-configuration-probe", mode="LegacyConfigurationProbe", operationId=request.OperationId, terminalState="Succeeded", exitClassification="Success", selectedPattern=pattern[0].Name, selectedIdCount=requirements.Count, requirements, inputs=before.Select(x=>Redact(x,request.OperationId)), sideEffects=SideEffects() }, cancellationToken);
return new("Succeeded",requirements,output,[]);
}
catch(Exception ex)
{
var output=Path.Combine(evidenceRoot, request.OperationId + ".json");
await File.WriteAllTextAsync(output, JsonSerializer.Serialize(new { mode="LegacyConfigurationProbe", operationId=request.OperationId, terminalState="PILOT_CONFIGURATION_BLOCKED", diagnostic=ex.Message, sourceWrites=false, payloadCopied=false, stagingCreated=false, activationAttempted=false, autoCadLaunchAttempted=false, dfsWriteAttempted=false }), cancellationToken);
return new("PILOT_CONFIGURATION_BLOCKED",[],output,[ex.Message]);
var code=ex.Message.StartsWith("LEGACY_",StringComparison.Ordinal)||ex.Message.StartsWith("AMBIGUOUS_",StringComparison.Ordinal)?ex.Message:"LEGACY_PROBE_INTERNAL_FAILURE";
var output=await WriteEvidenceAsync(evidenceRoot,request.OperationId,new { evidenceSchemaVersion=1,evidenceKind="legacy-configuration-probe",mode="LegacyConfigurationProbe",operationId=request.OperationId,terminalState="Blocked",exitClassification=code,diagnostics=new[]{code},inputs=before.Select(x=>Redact(x,request.OperationId)),sideEffects=SideEffects() },cancellationToken);
return new("Blocked",[],output,[code]);
}
}
private static async Task<string[]> Snapshot(string a,string b,string c,CancellationToken ct) => [await One(a,ct),await One(b,ct),await One(c,ct)];
private static async Task<string> One(string path,CancellationToken ct) { var info=new FileInfo(path); return path+"|"+info.Length+"|"+info.LastWriteTimeUtc.Ticks+"|"+Convert.ToHexString(await SHA256.HashDataAsync(File.OpenRead(path),ct)); }
private static object SideEffects()=>new { sourceWrites=false,payloadCopied=false,stagingCreated=false,packageCommitted=false,activationAttempted=false,pointerMutationAttempted=false,autoCadLaunchAttempted=false,dfsWriteAttempted=false };
private static bool SameOrNested(string child,string parent) { var c=Path.GetFullPath(child).TrimEnd(Path.DirectorySeparatorChar)+Path.DirectorySeparatorChar; var p=Path.GetFullPath(parent).TrimEnd(Path.DirectorySeparatorChar)+Path.DirectorySeparatorChar; return c.StartsWith(p,StringComparison.OrdinalIgnoreCase); }
private static async Task<string[]> Snapshot(string a,string b,string c,CancellationToken ct) => [await One("LegacySettingsPath",a,ct),await One("LegacyDownloadListPath",b,ct),await One("LegacyCompatibilityOverlayPath",c,ct)];
private static async Task<string> One(string role,string path,CancellationToken ct) { var info=new FileInfo(path); if(info.Attributes.HasFlag(FileAttributes.ReparsePoint)) throw new InvalidOperationException("LEGACY_PROBE_REPARSE_SOURCE_REJECTED"); await using var stream=new FileStream(path,FileMode.Open,FileAccess.Read,FileShare.Read); return string.Join('|',role,Path.GetFileName(path),info.Length,info.LastWriteTimeUtc.Ticks,Convert.ToHexString(await SHA256.HashDataAsync(stream,ct)),path); }
private static async Task<string> ReadOnlyTextAsync(string path,CancellationToken ct) { await using var stream=new FileStream(path,FileMode.Open,FileAccess.Read,FileShare.Read); using var reader=new StreamReader(stream,Encoding.UTF8,true); return await reader.ReadToEndAsync(ct); }
private static object Redact(string value,string operationId) { var parts=value.Split('|'); return new { role=parts[0],basename=parts[1],length=parts[2],lastWriteUtcTicks=parts[3],sha256=parts[4],pathFingerprint=Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(operationId+"|"+parts[5]))) }; }
private static async Task<string> WriteEvidenceAsync(string root,string operationId,object evidence,CancellationToken ct) { var operation=Path.Combine(root,operationId); Directory.CreateDirectory(operation); var final=Path.Combine(operation,"legacy-configuration-probe.json"); var temp=final+".tmp"; await File.WriteAllTextAsync(temp,JsonSerializer.Serialize(evidence),ct); File.Move(temp,final,true); return final; }
}