From 72cd0cc337210e4b8b0491c02f21336fe66b820a Mon Sep 17 00:00:00 2001 From: codex Date: Mon, 3 Aug 2026 10:48:03 +0300 Subject: [PATCH] feat: require local permit for first Apply pilot --- Launcher.Next.Core/NextPilot.cs | 3 ++- Launcher.Next.Infrastructure/PilotExecutionService.cs | 5 ++++- tests/Launcher.Next.Tests/ControlledExecutionTests.cs | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Launcher.Next.Core/NextPilot.cs b/Launcher.Next.Core/NextPilot.cs index c7b9466..347fb71 100644 --- a/Launcher.Next.Core/NextPilot.cs +++ b/Launcher.Next.Core/NextPilot.cs @@ -16,6 +16,7 @@ public sealed record PilotLocalConfiguration // Apply is never a normal pilot default. It is accepted only by this // dedicated test branch and still requires the plugin-side fixture permit. public bool AllowApply { get; init; } + public string? ApplyPermitPath { get; init; } public string? PilotRoot { get; init; } public string? SelectedProductId { get; init; } public string? SelectedProfileName { get; init; } @@ -60,7 +61,7 @@ public interface INextPilotSafetyGate { PilotSafetyDecision Evaluate(PilotSafety public sealed class NextPilotSafetyGate : INextPilotSafetyGate { public PilotSafetyDecision Evaluate(PilotSafetyContext c, bool forExecution) - { var v = new List(); if (c.Configuration.LauncherMode != LauncherMode.NextPilot) v.Add("WRONG_LAUNCHER_MODE"); if (!c.Configuration.PilotEnabled) v.Add("PILOT_DISABLED"); if (!c.Configuration.RequireExplicitExecute) v.Add("EXPLICIT_EXECUTE_REQUIRED"); if (c.Configuration.PluginExecutionMode is not (PluginExecutionMode.Smoke or PluginExecutionMode.DryRun or PluginExecutionMode.Apply)) v.Add("PLUGIN_EXECUTION_MODE_NOT_ALLOWED"); if (c.Configuration.PluginExecutionMode == PluginExecutionMode.Apply && !c.Configuration.AllowApply) v.Add("APPLY_NOT_EXPLICITLY_ALLOWED"); 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); } + { var v = new List(); if (c.Configuration.LauncherMode != LauncherMode.NextPilot) v.Add("WRONG_LAUNCHER_MODE"); if (!c.Configuration.PilotEnabled) v.Add("PILOT_DISABLED"); if (!c.Configuration.RequireExplicitExecute) v.Add("EXPLICIT_EXECUTE_REQUIRED"); if (c.Configuration.PluginExecutionMode is not (PluginExecutionMode.Smoke or PluginExecutionMode.DryRun or PluginExecutionMode.Apply)) v.Add("PLUGIN_EXECUTION_MODE_NOT_ALLOWED"); if (c.Configuration.PluginExecutionMode == PluginExecutionMode.Apply && !c.Configuration.AllowApply) v.Add("APPLY_NOT_EXPLICITLY_ALLOWED"); if (c.Configuration.PluginExecutionMode == PluginExecutionMode.Apply && (!Path.IsPathFullyQualified(c.Configuration.ApplyPermitPath ?? string.Empty) || (c.Configuration.ApplyPermitPath ?? string.Empty).StartsWith("\\\\", StringComparison.Ordinal))) v.Add("APPLY_PERMIT_PATH_REQUIRED"); 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) { } else 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 Arguments, string WorkingDirectory, IReadOnlyDictionary 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); diff --git a/Launcher.Next.Infrastructure/PilotExecutionService.cs b/Launcher.Next.Infrastructure/PilotExecutionService.cs index 2c41ee6..6c9e7a6 100644 --- a/Launcher.Next.Infrastructure/PilotExecutionService.cs +++ b/Launcher.Next.Infrastructure/PilotExecutionService.cs @@ -43,7 +43,10 @@ public sealed class PilotExecutionService( var content = $"_.NETLOAD\n\"{futureEntry.Replace("\"", "\\\"")}\"\n"; var scriptHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(content))); var plan = PilotPlanFactory.Create(request.Product, profile, request.Manifest, request.ManifestHash, request.PilotRoot, request.StartupScriptPath, scriptHash, safety); - return new(plan with { Process = plan.Process with { EnvironmentVariables = new Dictionary { ["P_REC_ANNOT_RUNTIME_MODE"] = request.Configuration.PluginExecutionMode.ToString() } } }, safety); + var environment = new Dictionary { ["P_REC_ANNOT_RUNTIME_MODE"] = request.Configuration.PluginExecutionMode.ToString() }; + if (request.Configuration.PluginExecutionMode == PluginExecutionMode.Apply) + environment["P_REC_ANNOT_APPLY_PERMIT_PATH"] = request.Configuration.ApplyPermitPath!; + return new(plan with { Process = plan.Process with { EnvironmentVariables = environment } }, safety); } public async Task ExecuteAsync(PilotExecutionRequest request, CancellationToken cancellationToken) diff --git a/tests/Launcher.Next.Tests/ControlledExecutionTests.cs b/tests/Launcher.Next.Tests/ControlledExecutionTests.cs index 185c3aa..7ff9e7a 100644 --- a/tests/Launcher.Next.Tests/ControlledExecutionTests.cs +++ b/tests/Launcher.Next.Tests/ControlledExecutionTests.cs @@ -7,7 +7,9 @@ public sealed class ControlledExecutionTests [Fact] public void ApplyMode_RequiresExplicitAllowApply() { var c = new PilotLocalConfiguration { SchemaVersion = 1, LauncherMode = LauncherMode.NextPilot, PilotEnabled = true, AllowProcessStart = true, RequireExplicitExecute = true, PluginExecutionMode = PluginExecutionMode.Apply }; var d = 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("APPLY_NOT_EXPLICITLY_ALLOWED", d.Violations); } [Fact] public void ApplyMode_WithExplicitAllowApply_IsAllowedByLauncherGate() - { var c = new PilotLocalConfiguration { SchemaVersion = 1, LauncherMode = LauncherMode.NextPilot, PilotEnabled = true, AllowProcessStart = true, AllowApply = true, RequireExplicitExecute = true, PluginExecutionMode = PluginExecutionMode.Apply }; var d = 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.True(d.IsAllowed); } + { var c = new PilotLocalConfiguration { SchemaVersion = 1, LauncherMode = LauncherMode.NextPilot, PilotEnabled = true, AllowProcessStart = true, AllowApply = true, ApplyPermitPath = "C:\\test\\permit.json", RequireExplicitExecute = true, PluginExecutionMode = PluginExecutionMode.Apply }; var d = 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.True(d.IsAllowed); } + [Fact] public void ApplyMode_RequiresLocalPermitPath() + { var c = new PilotLocalConfiguration { SchemaVersion = 1, LauncherMode = LauncherMode.NextPilot, PilotEnabled = true, AllowProcessStart = true, AllowApply = true, RequireExplicitExecute = true, PluginExecutionMode = PluginExecutionMode.Apply }; var d = 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("APPLY_PERMIT_PATH_REQUIRED", d.Violations); } [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);