8 changed files with 79 additions and 4 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
namespace Launcher.Next.Core;
public enum LauncherMode { Legacy, DryRun, NextPilot, NextPreferred }
public enum PluginExecutionMode { Smoke, DryRun }
public enum PluginExecutionMode { Smoke, DryRun, Apply }
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 StartupDefinition(string Type, string Command);
+5 -1
View File
@@ -13,6 +13,10 @@ public sealed record PilotLocalConfiguration
public bool AllowProcessStart { get; init; }
public bool RequireExplicitExecute { get; init; } = true;
public PluginExecutionMode PluginExecutionMode { get; init; } = PluginExecutionMode.Smoke;
// 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; }
@@ -57,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<string>(); 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 != 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); }
{ 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.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<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);
@@ -42,7 +42,11 @@ public sealed class PilotExecutionService(
var futureEntry = Path.Combine(request.PilotRoot, "packages", request.Manifest.PackageId, "versions", request.Manifest.Version, request.Manifest.EntryAssembly);
var content = $"_.NETLOAD\n\"{futureEntry.Replace("\"", "\\\"")}\"\n";
var scriptHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(content)));
return new(PilotPlanFactory.Create(request.Product, profile, request.Manifest, request.ManifestHash, request.PilotRoot, request.StartupScriptPath, scriptHash, safety), safety);
var plan = PilotPlanFactory.Create(request.Product, profile, request.Manifest, request.ManifestHash, request.PilotRoot, request.StartupScriptPath, scriptHash, safety);
var environment = new Dictionary<string, string> { ["P_REC_ANNOT_RUNTIME_MODE"] = request.Configuration.PluginExecutionMode.ToString() };
if (!string.IsNullOrWhiteSpace(request.Configuration.ApplyPermitPath))
environment["P_REC_ANNOT_APPLY_PERMIT_PATH"] = request.Configuration.ApplyPermitPath;
return new(plan with { Process = plan.Process with { EnvironmentVariables = environment } }, safety);
}
public async Task<PilotExecutionResult> ExecuteAsync(PilotExecutionRequest request, CancellationToken cancellationToken)
@@ -0,0 +1,31 @@
{
"date": "2026-08-03",
"launcherMode": "NextPilot",
"pluginExecutionMode": "DryRun",
"startupNetloadCount": 1,
"profileArgumentPresent": false,
"legacyBootstrapUsed": false,
"processStarted": true,
"assemblyLoadedFromVersionedPackage": true,
"orthoDetection": "Confirmed",
"viewportCount": 1,
"objectsExamined": 0,
"phpRecAnnotRuns": 2,
"pxpAavRuns": 2,
"pxpOaRuns": 1,
"commandErrors": 0,
"dbmodBefore": 0,
"dbmodAfter": 0,
"plansDeterministic": null,
"sourcePlantObjectsRead": false,
"dataLinksSheetToSourceMapping": "blocked",
"rollbackCompleted": true,
"startupScriptRemoved": true,
"activatedVersionRemoved": true,
"fixtureDwgUnchanged": null,
"fixtureDcfUnchanged": false,
"fixtureChangedFiles": ["Piping.dcf"],
"ribbonVerified": null,
"unhandledExceptions": 0,
"productionDeploymentPerformed": false
}
+2 -1
View File
@@ -8,6 +8,7 @@
"startupNetloadCount": 1,
"processStarted": true,
"processId": 9960,
"launcherRunnerExitCode": null,
"assemblyLoadedFromVersionedPackage": true,
"commands": { "PHP_REC_ANNOT": true, "PXP-AAV": true, "PXP-OA": true },
"drawingType": "NotOrtho",
@@ -24,5 +25,5 @@
"previousPointerRestored": true,
"legacyFilesModified": null,
"productionDeployment": false,
"notes": ["Legacy local sentinel files were absent; hashes were not available.", "Session records terminal RolledBack state; historic intermediate states were not persisted."]
"notes": ["The background process exit code was not captured by the launcher host.", "Legacy local sentinel files were absent; hashes were not available.", "Session records terminal RolledBack state; historic intermediate states were not persisted."]
}
+27
View File
@@ -0,0 +1,27 @@
# P_Rec_Annot Ortho DryRun
Date: 2026-08-03
Launcher Next executed the isolated `NextPilot` flow in `DryRun` mode. The
generated AutoCAD startup script contained one `NETLOAD` for the versioned
local entry assembly; no legacy bootstrap was used and no `/p` argument was
passed.
The actual Ortho drawing was `TestPipeOrtho.dwg`. `PHP_REC_ANNOT` ran twice,
`PXP-AAV` ran twice, and `PXP-OA` ran once. Each completed with Ortho detected,
one viewport, zero examined objects, zero planned operations, and no command
error. `DBMOD` was `0` both before and after the commands.
The Ortho sheet's `SelectAcPpRowIds` call raised `DLException: NotFound` in the
preceding attempt. The read adapter now treats that condition as an empty,
read-only result so it cannot fail a DryRun command. It does not yet map an
Ortho sheet back to source-model DataLinks rows; therefore successful source
Pipe discovery and a non-empty AnnotationPlan remain unverified.
After Plant 3D closed, Launcher Next reported `RolledBack`; the startup script,
version directory, current/previous pointers, and staging contents were absent.
The fixture SHA comparison found one changed project file: `Piping.dcf`.
Its last-write time predates the first DryRun command, so the observation cannot
be attributed to a P_Rec_Annot write. Nevertheless, project/DCF invariance is
not established by this run.
+2
View File
@@ -6,4 +6,6 @@ The manual runtime checkpoint confirmed `PHP_REC_ANNOT`, `PXP-AAV`, and `PXP-OA`
After normal Plant shutdown, automatic rollback completed. No `acad.exe` remained. Startup scripts, staging contents, package pointers and activated version were absent; the activation snapshot was retained. The checked local legacy Application files were absent, so no before/after hashes were available.
The background runner exit code was not captured by its `Start-Process` host. The persisted terminal state and the post-run `pilot status` command both reported `RolledBack`.
Known plugin defect: `PLANT_PROJECT_DISPLAY_NAME_EMPTY`. Plant context was found but displayed an empty project name. Impact was diagnostic/UI-only for this Smoke pilot; write protection and command execution were unaffected. Follow-up: inspect `PlantApplication.CurrentProject` metadata and distinguish absent display name from false-positive project detection.
@@ -4,6 +4,12 @@ using Xunit;
namespace Launcher.Next.Tests;
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, 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);