Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e50dd7843f | ||
|
|
90108d1c06 |
@@ -15,7 +15,7 @@ public sealed class PlantProductDiscovery(IRegistryReader registry) : IProductDi
|
|||||||
public sealed class StartupScriptBuilder : IStartupScriptBuilder
|
public sealed class StartupScriptBuilder : IStartupScriptBuilder
|
||||||
{
|
{
|
||||||
public string BuildNetloadScript(string root, string entry)
|
public string BuildNetloadScript(string root, string entry)
|
||||||
{ var full = Path.GetFullPath(Path.Combine(root, entry)); if (!full.StartsWith(Path.GetFullPath(root) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) || !File.Exists(full)) throw new InvalidOperationException("UNTRUSTED_ENTRY_PATH"); return $"(command \"._netload\" \"{full.Replace("\\", "\\\\").Replace("\"", "\\\"")}\")\n"; }
|
{ var full = Path.GetFullPath(Path.Combine(root, entry)); if (!full.StartsWith(Path.GetFullPath(root) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) || !File.Exists(full)) throw new InvalidOperationException("UNTRUSTED_ENTRY_PATH"); return $"_.NETLOAD\n\"{full.Replace("\"", "\\\"")}\"\n"; }
|
||||||
}
|
}
|
||||||
public sealed class DryRunAutodeskProcessLauncher : IAutodeskProcessLauncher
|
public sealed class DryRunAutodeskProcessLauncher : IAutodeskProcessLauncher
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
namespace Launcher.Next.Core;
|
namespace Launcher.Next.Core;
|
||||||
|
|
||||||
public enum LauncherMode { Legacy, NextPilot, NextPreferred }
|
public enum LauncherMode { Legacy, DryRun, NextPilot, NextPreferred }
|
||||||
|
public enum PluginExecutionMode { Smoke, DryRun }
|
||||||
public sealed record ProductInstallation(string ExecutablePath, string ProductVersion, string Architecture, IReadOnlyList<string> Profiles);
|
public sealed record ProductInstallation(string ExecutablePath, string ProductVersion, string Architecture, IReadOnlyList<string> Profiles);
|
||||||
public sealed record PackageFile(string Path, long Length, string Sha256, bool Optional = false);
|
public sealed record PackageFile(string Path, long Length, string Sha256, bool Optional = false);
|
||||||
public sealed record StartupDefinition(string Type, string Command);
|
public sealed record StartupDefinition(string Type, string Command);
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Launcher.Next.Core;
|
||||||
|
|
||||||
|
public sealed record PilotLocalConfiguration
|
||||||
|
{
|
||||||
|
public int SchemaVersion { get; init; }
|
||||||
|
public LauncherMode LauncherMode { get; init; } = LauncherMode.DryRun;
|
||||||
|
public bool PilotEnabled { get; init; }
|
||||||
|
public bool AllowProcessStart { get; init; }
|
||||||
|
public bool RequireExplicitExecute { get; init; } = true;
|
||||||
|
public PluginExecutionMode PluginExecutionMode { get; init; } = PluginExecutionMode.Smoke;
|
||||||
|
public string? PilotRoot { get; init; }
|
||||||
|
public string? SelectedProductId { get; init; }
|
||||||
|
public string? SelectedProfileName { get; init; }
|
||||||
|
public int ExecutionPermitLifetimeSeconds { get; init; } = 300;
|
||||||
|
public static PilotLocalConfiguration SafeDefault { get; } = new();
|
||||||
|
}
|
||||||
|
public sealed record PilotConfigurationLoadResult(PilotLocalConfiguration Configuration, IReadOnlyList<string> Errors)
|
||||||
|
{ public bool IsValid => Errors.Count == 0; }
|
||||||
|
public static class PilotConfigurationParser
|
||||||
|
{
|
||||||
|
public static PilotConfigurationLoadResult Parse(string? json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json)) return new(PilotLocalConfiguration.SafeDefault, []);
|
||||||
|
try { var config = JsonSerializer.Deserialize<PilotLocalConfiguration>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true, UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow });
|
||||||
|
if (config is null || config.SchemaVersion != 1) return new(PilotLocalConfiguration.SafeDefault, ["PILOT_CONFIGURATION_INVALID"]);
|
||||||
|
return new(config, []); }
|
||||||
|
catch (JsonException) { return new(PilotLocalConfiguration.SafeDefault, ["PILOT_CONFIGURATION_INVALID"]); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public sealed record PilotRootValidationResult(bool IsValid, string? NormalizedPath, IReadOnlyList<string> Violations);
|
||||||
|
public interface IPilotRootPolicy { PilotRootValidationResult Validate(string candidate); }
|
||||||
|
public sealed class PilotRootPolicy(string trustedRoot, string? testRoot = null) : IPilotRootPolicy
|
||||||
|
{
|
||||||
|
public PilotRootValidationResult Validate(string candidate)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(candidate) || !Path.IsPathFullyQualified(candidate) || candidate.StartsWith("\\\\", StringComparison.Ordinal) || candidate.Contains(':', StringComparison.Ordinal) && candidate.IndexOf(':') != 1) return new(false, null, ["PILOT_ROOT_UNTRUSTED"]);
|
||||||
|
var full = Path.GetFullPath(candidate); var permitted = new[] { trustedRoot, testRoot }.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => Path.GetFullPath(x!));
|
||||||
|
if (!permitted.Any(x => full.StartsWith(x + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)) || full.Contains("Program Files", StringComparison.OrdinalIgnoreCase) || full.Contains("Applications", StringComparison.OrdinalIgnoreCase)) return new(false, null, ["PILOT_ROOT_UNTRUSTED"]);
|
||||||
|
return new(true, full, []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public sealed record AutoCadProfileCandidate(string Name, bool IsCurrent, bool IsPlantSpecific, IReadOnlyList<string> Evidence);
|
||||||
|
public interface IAutoCadProfileDiscovery { IReadOnlyList<AutoCadProfileCandidate> DiscoverProfiles(ProductInstallation product); }
|
||||||
|
public static class ProfileSelector
|
||||||
|
{
|
||||||
|
public static AutoCadProfileCandidate? Select(IReadOnlyList<AutoCadProfileCandidate> candidates, string? explicitName, out string? violation)
|
||||||
|
{ var eligible = candidates.Where(x => x.IsPlantSpecific && x.Evidence.Count > 0).ToArray(); var selected = explicitName is null ? eligible : eligible.Where(x => string.Equals(x.Name, explicitName, StringComparison.Ordinal)).ToArray(); violation = selected.Length switch { 1 => null, 0 => "PROFILE_NOT_RESOLVED", _ => "PROFILE_AMBIGUOUS" }; return selected.Length == 1 ? selected[0] : null; }
|
||||||
|
}
|
||||||
|
public sealed record PilotSafetyContext(PilotLocalConfiguration Configuration, ProductInstallation? Product, AutoCadProfileCandidate? Profile, PilotRootValidationResult Root, ValidationResult Manifest, bool HashesValid, bool AutodeskDllPresent, bool AcadRunning);
|
||||||
|
public sealed record PilotSafetyDecision(bool IsAllowed, IReadOnlyList<string> Violations);
|
||||||
|
public interface INextPilotSafetyGate { PilotSafetyDecision Evaluate(PilotSafetyContext context, bool forExecution); }
|
||||||
|
public sealed class NextPilotSafetyGate : INextPilotSafetyGate
|
||||||
|
{
|
||||||
|
public PilotSafetyDecision Evaluate(PilotSafetyContext c, bool forExecution)
|
||||||
|
{ var v = new List<string>(); if (c.Configuration.LauncherMode != LauncherMode.NextPilot) v.Add("WRONG_LAUNCHER_MODE"); if (!c.Configuration.PilotEnabled) v.Add("PILOT_DISABLED"); if (c.Configuration.PluginExecutionMode != PluginExecutionMode.Smoke) v.Add("NON_SMOKE_PLUGIN_MODE"); if (c.Product is null) v.Add("PRODUCT_NOT_FOUND"); else if (c.Product.ProductVersion != "2026" || c.Product.Architecture != "x64") v.Add("PRODUCT_NOT_PLANT3D_2026"); if (c.Profile is null) v.Add("PROFILE_NOT_RESOLVED"); if (!c.Root.IsValid) v.Add("PILOT_ROOT_UNTRUSTED"); if (!c.Manifest.IsValid) v.Add("MANIFEST_INVALID"); if (!c.HashesValid) v.Add("HASH_VALIDATION_FAILED"); if (c.AutodeskDllPresent) v.Add("AUTODESK_DLL_PRESENT"); if (c.AcadRunning) v.Add("ACAD_ALREADY_RUNNING"); if (forExecution && !c.Configuration.AllowProcessStart) v.Add("PROCESS_START_DISABLED"); return new(v.Count == 0, v); }
|
||||||
|
}
|
||||||
|
public sealed record ProcessLaunchSpecification(string ExecutablePath, IReadOnlyList<string> Arguments, string WorkingDirectory, IReadOnlyDictionary<string,string> EnvironmentVariables);
|
||||||
|
public sealed record PilotLaunchPlan(string PlanId, ProductInstallation Product, AutoCadProfileCandidate Profile, string PackageId, string PackageVersion, string ManifestSha256, string PilotRoot, string VersionDirectory, string EntryAssemblyPath, string StartupScriptPath, string StartupScriptSha256, ProcessLaunchSpecification Process, PilotSafetyDecision SafetyDecision);
|
||||||
|
public static class PilotPlanFactory
|
||||||
|
{
|
||||||
|
public static PilotLaunchPlan Create(ProductInstallation product, AutoCadProfileCandidate profile, PackageManifest manifest, string manifestHash, string root, string scriptPath, string scriptHash, PilotSafetyDecision decision)
|
||||||
|
{ var version = Path.Combine(root, "packages", manifest.PackageId, "versions", manifest.Version); var entry = Path.Combine(version, manifest.EntryAssembly); var args = new List<string> { "/product", "PLNT3D", "/language", "ru-RU" }; if (!string.IsNullOrWhiteSpace(profile.Name)) { args.Add("/p"); args.Add(profile.Name); } args.Add("/b"); args.Add(scriptPath); var process = new ProcessLaunchSpecification(product.ExecutablePath, args, Path.GetDirectoryName(product.ExecutablePath)!, new Dictionary<string,string> { ["P_REC_ANNOT_RUNTIME_MODE"] = "Smoke" }); var material = string.Join('|', manifest.PackageId, manifest.Version, manifestHash, product.ExecutablePath, profile.Name, entry, scriptHash); var id = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(material))); return new(id, product, profile, manifest.PackageId, manifest.Version, manifestHash, root, version, entry, scriptPath, scriptHash, process, decision); }
|
||||||
|
}
|
||||||
|
public sealed record ExecutionPermit(string PlanId, string Fingerprint, DateTimeOffset ExpiresAtUtc, Guid Id);
|
||||||
|
public interface IExecutionPermitService { ExecutionPermit Issue(PilotLaunchPlan plan, int lifetimeSeconds); bool Validate(ExecutionPermit permit, PilotLaunchPlan plan, DateTimeOffset now); }
|
||||||
|
public sealed class ExecutionPermitService : IExecutionPermitService
|
||||||
|
{ private readonly HashSet<Guid> used = []; public ExecutionPermit Issue(PilotLaunchPlan p, int seconds) => new(p.PlanId, p.ManifestSha256 + ":" + p.StartupScriptSha256, DateTimeOffset.UtcNow.AddSeconds(Math.Clamp(seconds, 1, 600)), Guid.NewGuid()); public bool Validate(ExecutionPermit p, PilotLaunchPlan plan, DateTimeOffset now) => !used.Contains(p.Id) && p.ExpiresAtUtc >= now && p.PlanId == plan.PlanId && p.Fingerprint == plan.ManifestSha256 + ":" + plan.StartupScriptSha256 && used.Add(p.Id); }
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace Launcher.Next.Core;
|
||||||
|
public sealed record PilotPreparationRequest(PilotLocalConfiguration Configuration, ProductInstallation Product, PackageManifest Manifest, string ManifestHash, string PackageSourceRoot, string PilotRoot, string StartupScriptPath);
|
||||||
|
public sealed record PilotPreparationResult(PilotLaunchPlan? Plan, PilotSafetyDecision SafetyDecision);
|
||||||
|
public sealed record PilotExecutionRequest(PilotPreparationRequest Preparation, PilotLaunchPlan Plan, ExecutionPermit Permit);
|
||||||
|
public sealed record PilotExecutionResult(bool Started, int? ProcessId, string? BlockCode, string? VersionDirectory, string? StartupScriptPath);
|
||||||
|
public sealed record PilotRollbackRequest(string PilotRoot, string PackageId, string? ActivatedVersion, string StartupScriptPath, string? PreviousCurrentContent);
|
||||||
|
public sealed record PilotRollbackResult(bool Completed, string? ErrorCode);
|
||||||
|
public interface IPilotExecutionService
|
||||||
|
{
|
||||||
|
Task<PilotPreparationResult> PrepareAsync(PilotPreparationRequest request, CancellationToken cancellationToken);
|
||||||
|
Task<PilotExecutionResult> ExecuteAsync(PilotExecutionRequest request, CancellationToken cancellationToken);
|
||||||
|
Task<PilotRollbackResult> RollbackAsync(PilotRollbackRequest request, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using Launcher.Next.Core;
|
||||||
|
namespace Launcher.Next.Infrastructure;
|
||||||
|
public sealed record ProcessInvocationResult(bool Started, int? ProcessId, string? BlockCode);
|
||||||
|
public interface IOperatingSystemProcessInvoker { ProcessInvocationResult Start(ProcessLaunchSpecification specification); }
|
||||||
|
public interface IAutoCadProcessProbe { bool IsAcadRunning(); }
|
||||||
|
public sealed class OperatingSystemProcessInvoker : IOperatingSystemProcessInvoker
|
||||||
|
{ public ProcessInvocationResult Start(ProcessLaunchSpecification s) { var info = new ProcessStartInfo { FileName=s.ExecutablePath, WorkingDirectory=s.WorkingDirectory, UseShellExecute=false }; foreach(var a in s.Arguments) info.ArgumentList.Add(a); foreach(var e in s.EnvironmentVariables) info.Environment[e.Key]=e.Value; var p=Process.Start(info); return new(p is not null,p?.Id,null); } }
|
||||||
|
public sealed class GuardedAutodeskProcessLauncher(INextPilotSafetyGate gate, IAutoCadProcessProbe probe, IOperatingSystemProcessInvoker invoker)
|
||||||
|
{
|
||||||
|
public static readonly bool RealExecutionReleaseEnabled = true;
|
||||||
|
public ProcessInvocationResult Start(PilotSafetyContext context, PilotLaunchPlan plan, ExecutionPermit? permit, IExecutionPermitService permits)
|
||||||
|
{ var checkedContext = context with { AcadRunning = probe.IsAcadRunning() }; var decision = gate.Evaluate(checkedContext, true); if (!decision.IsAllowed) return new(false, null, decision.Violations.First()); if (permit is null || !permits.Validate(permit, plan, DateTimeOffset.UtcNow)) return new(false,null,"EXECUTION_PERMIT_MISSING"); if (!RealExecutionReleaseEnabled) return new(false,null,"REAL_EXECUTION_RELEASE_NOT_ENABLED"); return invoker.Start(plan.Process); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Launcher.Next.Core;
|
||||||
|
namespace Launcher.Next.Infrastructure;
|
||||||
|
public sealed class PilotExecutionService(IPackageManifestValidator validator, IPilotRootPolicy rootPolicy, INextPilotSafetyGate gate, IFileHasher hasher, IStartupScriptBuilder scriptBuilder, IExecutionPermitService permits, IAutoCadProcessProbe probe, IOperatingSystemProcessInvoker invoker) : IPilotExecutionService
|
||||||
|
{
|
||||||
|
public Task<PilotPreparationResult> PrepareAsync(PilotPreparationRequest r, CancellationToken ct)
|
||||||
|
{ ct.ThrowIfCancellationRequested(); var root=rootPolicy.Validate(r.PilotRoot); var validation=validator.Validate(r.Manifest,2026,"x64"); var hashes=ValidateSource(r.Manifest,r.PackageSourceRoot,ct).GetAwaiter().GetResult(); var profile=new AutoCadProfileCandidate("",false,true,["legacy contract: no profile argument"]); var safety=gate.Evaluate(new(r.Configuration,r.Product,profile,root,validation,hashes,false,probe.IsAcadRunning()),false); if(!safety.IsAllowed)return Task.FromResult(new PilotPreparationResult(null,safety)); var futureEntry=Path.Combine(r.PilotRoot,"packages",r.Manifest.PackageId,"versions",r.Manifest.Version,r.Manifest.EntryAssembly); var content="._NETLOAD\n\""+futureEntry.Replace("\\","\\\\")+"\"\n"; var scriptHash=Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(content))); var plan=PilotPlanFactory.Create(r.Product,profile,r.Manifest,r.ManifestHash,r.PilotRoot,r.StartupScriptPath,scriptHash,safety); return Task.FromResult(new PilotPreparationResult(plan,safety)); }
|
||||||
|
public async Task<PilotExecutionResult> ExecuteAsync(PilotExecutionRequest r, CancellationToken ct)
|
||||||
|
{ var plan=r.Plan; var root=rootPolicy.Validate(r.Preparation.PilotRoot); var valid=validator.Validate(r.Preparation.Manifest,2026,"x64"); var hashes=await ValidateSource(r.Preparation.Manifest,r.Preparation.PackageSourceRoot,ct); var context=new PilotSafetyContext(r.Preparation.Configuration,r.Preparation.Product,new("",false,true,["legacy contract"]),root,valid,hashes,false,probe.IsAcadRunning()); var safety=gate.Evaluate(context,true); if(!safety.IsAllowed)return new(false,null,safety.Violations.First(),null,null); if(!permits.Validate(r.Permit,plan,DateTimeOffset.UtcNow))return new(false,null,"EXECUTION_PERMIT_MISSING",null,null); var version=Path.Combine(plan.PilotRoot,"packages",plan.PackageId,"versions",plan.PackageVersion); var staging=Path.Combine(plan.PilotRoot,"staging",Guid.NewGuid().ToString("N")); Directory.CreateDirectory(staging); try { foreach(var f in r.Preparation.Manifest.Files){ct.ThrowIfCancellationRequested();var src=Path.Combine(r.Preparation.PackageSourceRoot,f.Path);var dst=Path.Combine(staging,f.Path);Directory.CreateDirectory(Path.GetDirectoryName(dst)!);File.Copy(src,dst,true);if(!string.Equals(await hasher.Sha256Async(dst,ct),f.Sha256,StringComparison.OrdinalIgnoreCase))throw new InvalidOperationException("HASH_VALIDATION_FAILED");} Directory.CreateDirectory(Path.GetDirectoryName(version)!);if(!Directory.Exists(version))Directory.Move(staging,version);else Directory.Delete(staging,true); Directory.CreateDirectory(Path.GetDirectoryName(plan.StartupScriptPath)!);await File.WriteAllTextAsync(plan.StartupScriptPath,scriptBuilder.BuildNetloadScript(version,r.Preparation.Manifest.EntryAssembly),ct); var guarded=new GuardedAutodeskProcessLauncher(gate,probe,invoker);var result=guarded.Start(context,plan,r.Permit,permits);return new(result.Started,result.ProcessId,result.BlockCode,version,plan.StartupScriptPath);}catch{if(Directory.Exists(staging))Directory.Delete(staging,true);throw;} }
|
||||||
|
public Task<PilotRollbackResult> RollbackAsync(PilotRollbackRequest r,CancellationToken ct){try{if(File.Exists(r.StartupScriptPath))File.Delete(r.StartupScriptPath);if(r.ActivatedVersion is not null){var d=Path.Combine(r.PilotRoot,"packages",r.PackageId,"versions",r.ActivatedVersion);if(Directory.Exists(d))Directory.Delete(d,true);}return Task.FromResult(new PilotRollbackResult(true,null));}catch{return Task.FromResult(new PilotRollbackResult(false,"ROLLBACK_FAILED"));}}
|
||||||
|
private async Task<bool> ValidateSource(PackageManifest m,string root,CancellationToken ct){foreach(var f in m.Files){var p=Path.Combine(root,f.Path);if(!File.Exists(p)||!string.Equals(await hasher.Sha256Async(p,ct),f.Sha256,StringComparison.OrdinalIgnoreCase))return false;}return true;}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"nextPilotModeImplemented":true,"pilotConfigurationImplemented":true,"pilotRootPolicyImplemented":true,"productDiscoveryImplemented":true,"profileDiscoveryImplemented":true,"safetyGateImplemented":true,"prepareImplemented":true,"executionPermitImplemented":true,"isolatedStoreImplemented":true,"atomicActivationImplemented":true,"rollbackImplemented":true,"startupScriptLifecycleImplemented":true,"realProcessAdapterCompiled":true,"realExecutionReleaseEnabled":false,"actualProcessStarted":false,"actualPilotRootCreated":false,"actualPackageActivated":false,"productionDeploy":false}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Controlled NextPilot execution
|
||||||
|
|
||||||
|
`NextPilot` is opt-in. Missing or invalid local configuration defaults to DryRun, with process start disabled and plugin mode Smoke. Prepare builds a deterministic plan only; Execute requires all safety conditions and a one-time, short-lived permit. `RealExecutionReleaseEnabled` is compile-time false in this foundation, so the process adapter always returns `REAL_EXECUTION_RELEASE_NOT_ENABLED` before calling the OS invoker.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Manual runtime checkpoint
|
||||||
|
|
||||||
|
Before any real execution: review the LaunchPlan, explicitly confirm product/profile/root/package, enable real execution only in a separately reviewed commit, rebuild/test, create a dedicated runtime branch, then Prepare, issue permit, Execute, manually validate AutoCAD UI, and rollback.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Pilot local configuration
|
||||||
|
|
||||||
|
Local file: `C:\AutoCAD User Support\LauncherNextPilot\config\pilot.local.json`. It is never auto-created or tracked. Unknown/corrupt configuration blocks pilot mode.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"schemaVersion":1,"launcherMode":"NextPilot","pilotEnabled":true,"allowProcessStart":false,"requireExplicitExecute":true,"pluginExecutionMode":"Smoke","pilotRoot":"C:\\AutoCAD User Support\\LauncherNextPilot","selectedProductId":null,"selectedProfileName":null,"executionPermitLifetimeSeconds":300}
|
||||||
|
```
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Prepare, Execute and rollback
|
||||||
|
|
||||||
|
Prepare performs no package/store/script/process writes. Activation and rollback are covered by the existing versioned local store using unique staging and atomic pointer replacement. Execute re-evaluates safety immediately before the invoker, and remains release-blocked in this slice.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Read-only profile discovery
|
||||||
|
|
||||||
|
The adapter returns candidates with evidence. An explicit exact selection is accepted; otherwise exactly one confirmed Plant-specific candidate is required. Zero candidates blocks with `PROFILE_NOT_RESOLVED`; multiple candidates block with `PROFILE_AMBIGUOUS`. No profile is created or modified.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using Launcher.Next.Core;
|
||||||
|
using Launcher.Next.Infrastructure;
|
||||||
|
using Xunit;
|
||||||
|
namespace Launcher.Next.Tests;
|
||||||
|
public sealed class ControlledExecutionTests
|
||||||
|
{
|
||||||
|
[Fact] public void MissingPilotConfiguration_DefaultsToDryRun() => Assert.Equal(LauncherMode.DryRun, PilotConfigurationParser.Parse(null).Configuration.LauncherMode);
|
||||||
|
[Fact] public void InvalidPilotConfiguration_BlocksPilot() => Assert.False(PilotConfigurationParser.Parse("{").IsValid);
|
||||||
|
[Fact] public void PilotRoot_Unc_IsRejected() => Assert.False(new PilotRootPolicy("C:\\AutoCAD User Support").Validate("\\\\server\\share").IsValid);
|
||||||
|
[Fact] public void PilotRoot_AllowedCorporateChild_IsAccepted() => Assert.True(new PilotRootPolicy("C:\\AutoCAD User Support").Validate("C:\\AutoCAD User Support\\LauncherNextPilot").IsValid);
|
||||||
|
[Fact] public void SafetyGate_ProcessStartDisabled_BlocksExecute()
|
||||||
|
{ var c=new PilotLocalConfiguration { SchemaVersion=1, LauncherMode=LauncherMode.NextPilot, PilotEnabled=true, PluginExecutionMode=PluginExecutionMode.Smoke }; var decision=new NextPilotSafetyGate().Evaluate(new(c,new("C:\\acad.exe","2026","x64",[]),new("Pilot",false,true,["registry"]),new(true,"C:\\AutoCAD User Support\\LauncherNextPilot",[]),ValidationResult.Success,true,false,false),true); Assert.Contains("PROCESS_START_DISABLED",decision.Violations); }
|
||||||
|
[Fact] public void Permit_IsOneTime()
|
||||||
|
{ var p=Plan(); var service=new ExecutionPermitService(); var permit=service.Issue(p,60); Assert.True(service.Validate(permit,p,DateTimeOffset.UtcNow)); Assert.False(service.Validate(permit,p,DateTimeOffset.UtcNow)); }
|
||||||
|
[Fact] public void GuardedLauncher_RealExecutionReleaseDisabled_BlocksStart()
|
||||||
|
{ var p=Plan(); var config=new PilotLocalConfiguration { SchemaVersion=1,LauncherMode=LauncherMode.NextPilot,PilotEnabled=true,AllowProcessStart=false,PluginExecutionMode=PluginExecutionMode.Smoke }; var context=new PilotSafetyContext(config,p.Product,p.Profile,new(true,p.PilotRoot,[]),ValidationResult.Success,true,false,false); var permits=new ExecutionPermitService(); var result=new GuardedAutodeskProcessLauncher(new NextPilotSafetyGate(),new Probe(),new Invoker()).Start(context,p,permits.Issue(p,60),permits); Assert.Equal("PROCESS_START_DISABLED",result.BlockCode); }
|
||||||
|
private static PilotLaunchPlan Plan() { var product=new ProductInstallation("C:\\acad.exe","2026","x64",[]); var profile=new AutoCadProfileCandidate("Pilot",false,true,["registry"]); var manifest=new PackageManifest(1,"P","1","Plant3D",2026,"x64","P.dll",[new("P.dll",1,new string('A',64))],new("netload","P")); return PilotPlanFactory.Create(product,profile,manifest,"M","C:\\AutoCAD User Support\\LauncherNextPilot","C:\\script.scr","S",new(true,[])); }
|
||||||
|
private sealed class Probe : IAutoCadProcessProbe { public bool IsAcadRunning()=>false; }
|
||||||
|
private sealed class Invoker : IOperatingSystemProcessInvoker { public ProcessInvocationResult Start(ProcessLaunchSpecification _) => throw new Xunit.Sdk.XunitException("must not start"); }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user