diff --git a/.gitattributes b/.gitattributes index 1b8a169a6c..794e109aae 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,5 @@ src/UniGetUI.PackageEngine.Managers.Chocolatey/choco-cli/** linguist-vendored -.githooks/* text eol=lf \ No newline at end of file +.githooks/* text eol=lf + +policies/samples/** linguist-generated +policies/schemas/** linguist-generated \ No newline at end of file diff --git a/.gitignore b/.gitignore index 711cadf3d3..4c20afaa00 100644 --- a/.gitignore +++ b/.gitignore @@ -96,3 +96,7 @@ src/UniGetUI.v3.ncrunchsolution # macOS Finder metadata .DS_Store /src/UniGetUI.Avalonia/Generated Files + + +policies/csharp/.vs/* +src/UniGetUI.PackageEngine.Managers.WinGet \ No newline at end of file diff --git a/global.json b/global.json index c358071f08..abe5820849 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "version": "10.0.103", - "rollForward": "latestPatch" + "rollForward": "latestFeature" } } diff --git a/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs b/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs index 601c1865f6..e04209e349 100644 --- a/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs +++ b/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs @@ -92,6 +92,9 @@ public enum K DisableClassicMode, DisableInstallerHostChangeWarning, BunPreferLatestVersions, + // NOTE: Set this to true to delegate package operations to Devolutions Agent broker + // instead of using local UAC elevation. Change default here when ready for production. + UseAgentBroker, Test1, Test2, @@ -195,6 +198,7 @@ public static string ResolveKey(K key) K.DisableClassicMode => "DisableClassicMode", K.DisableInstallerHostChangeWarning => "DisableInstallerHostChangeWarning", K.BunPreferLatestVersions => "BunPreferLatestVersions", + K.UseAgentBroker => "UseAgentBroker", K.Test1 => "TestSetting1", K.Test2 => "TestSetting2", diff --git a/src/UniGetUI.PackageEngine.AgentBroker/BrokerRequestBuilder.cs b/src/UniGetUI.PackageEngine.AgentBroker/BrokerRequestBuilder.cs new file mode 100644 index 0000000000..eba1960074 --- /dev/null +++ b/src/UniGetUI.PackageEngine.AgentBroker/BrokerRequestBuilder.cs @@ -0,0 +1,161 @@ +using Devolutions.UniGetUI.Broker.Client; +using UniGetUI.PackageEngine.Enums; +using UniGetUI.PackageEngine.Interfaces; +using UniGetUI.PackageEngine.Serializable; +// Aliased to avoid clashing with UniGetUI.PackageEngine.Enums.Architecture. +using BrokerArchitecture = Devolutions.UniGetUI.Broker.Client.Architecture; + +namespace UniGetUI.PackageEngine.AgentBroker; + +/// +/// Builds broker protocol requests from UniGetUI domain objects. +/// Maps IPackage + InstallOptions + OperationType into the canonical +/// consumed by the Devolutions Agent broker. +/// +public static class BrokerRequestBuilder +{ + private static readonly string ClientVersion = + System.Reflection.Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0"; + + /// Build a broker request from UniGetUI package operation parameters. + public static PackageRequest Build(IPackage package, InstallOptions options, OperationType role) + { + return new PackageRequest + { + RequestId = $"req-{Guid.NewGuid():N}", + CreatedAt = DateTimeOffset.UtcNow, + Operation = MapOperation(role), + Manager = new RequestManager + { + Name = MapManagerName(package.Manager.Name), + DisplayName = package.Manager.DisplayName, + ExecutableFriendlyName = Path.GetFileName(package.Manager.Status.ExecutablePath), + }, + Source = new RequestSource + { + Name = package.Source.Name, + Url = package.Source.Url?.ToString(), + IsVirtualManager = false, + }, + Package = new RequestPackage + { + Id = package.Id, + Name = package.Name, + Version = string.IsNullOrEmpty(options.Version) ? null : options.Version, + Architecture = MapArchitecture(options.Architecture), + }, + Options = new RequestOptions + { + Scope = MapScope(options.InstallationScope), + Interactive = options.InteractiveInstallation, + SkipHashCheck = options.SkipHashCheck, + PreRelease = options.PreRelease, + CustomParameters = GetCustomParameters(options, role), + CustomInstallLocation = string.IsNullOrEmpty(options.CustomInstallLocation) ? null : options.CustomInstallLocation, + KillBeforeOperation = options.KillBeforeOperation ?? [], + PreOperationCommand = GetPreCommand(options, role), + PostOperationCommand = GetPostCommand(options, role), + }, + Broker = new BrokerContext + { + RequestedElevation = options.RunAsAdministrator ? Elevation.Elevated : Elevation.Standard, + EffectiveUser = $"{Environment.UserDomainName}\\{Environment.UserName}", + ClientVersion = ClientVersion, + ClientProcessPath = Environment.ProcessPath, + }, + }; + } + + private static Operation MapOperation(OperationType role) => role switch + { + OperationType.Install => Operation.Install, + OperationType.Update => Operation.Update, + OperationType.Uninstall => Operation.Uninstall, + _ => throw new ArgumentException($"Unsupported operation type: {role}"), + }; + + /// + /// Maps UniGetUI manager names to the broker protocol canonical managers. + /// PowerShell 5 and PowerShell 7 are modeled as separate managers. + /// + private static ManagerName MapManagerName(string managerName) + { + if (managerName.Equals("Winget", StringComparison.OrdinalIgnoreCase)) + { + return ManagerName.Winget; + } + + if (managerName.Equals("PowerShell", StringComparison.OrdinalIgnoreCase)) + { + return ManagerName.PowerShell; + } + + if (managerName.Equals("PowerShell7", StringComparison.OrdinalIgnoreCase) || + managerName.Equals("pwsh", StringComparison.OrdinalIgnoreCase)) + { + return ManagerName.PowerShell7; + } + + throw new ArgumentException($"Unsupported manager for the broker: {managerName}"); + } + + private static Scope? MapScope(string? scope) + { + if (string.IsNullOrEmpty(scope)) + { + return null; + } + + return scope.ToLowerInvariant() switch + { + "user" => Scope.User, + "machine" => Scope.Machine, + "global" => Scope.Machine, + _ => null, + }; + } + + private static BrokerArchitecture? MapArchitecture(string? architecture) + { + if (string.IsNullOrEmpty(architecture)) + { + return null; + } + + return architecture.ToLowerInvariant() switch + { + "x86" => BrokerArchitecture.X86, + "x64" => BrokerArchitecture.X64, + "arm64" => BrokerArchitecture.Arm64, + "neutral" => BrokerArchitecture.Neutral, + _ => null, + }; + } + + private static List GetCustomParameters(InstallOptions options, OperationType role) => role switch + { + OperationType.Install => options.CustomParameters_Install ?? [], + OperationType.Update => options.CustomParameters_Update ?? [], + OperationType.Uninstall => options.CustomParameters_Uninstall ?? [], + _ => [], + }; + + private static string? GetPreCommand(InstallOptions options, OperationType role) => role switch + { + OperationType.Install => NullIfEmpty(options.PreInstallCommand), + OperationType.Update => NullIfEmpty(options.PreUpdateCommand), + OperationType.Uninstall => NullIfEmpty(options.PreUninstallCommand), + _ => null, + }; + + private static string? GetPostCommand(InstallOptions options, OperationType role) => role switch + { + OperationType.Install => NullIfEmpty(options.PostInstallCommand), + OperationType.Update => NullIfEmpty(options.PostUpdateCommand), + OperationType.Uninstall => NullIfEmpty(options.PostUninstallCommand), + _ => null, + }; + + private static string? NullIfEmpty(string? value) => + string.IsNullOrWhiteSpace(value) ? null : value; +} diff --git a/src/UniGetUI.PackageEngine.AgentBroker/UniGetUI.PackageEngine.AgentBroker.csproj b/src/UniGetUI.PackageEngine.AgentBroker/UniGetUI.PackageEngine.AgentBroker.csproj new file mode 100644 index 0000000000..cf2535eecb --- /dev/null +++ b/src/UniGetUI.PackageEngine.AgentBroker/UniGetUI.PackageEngine.AgentBroker.csproj @@ -0,0 +1,25 @@ + + + + $(SharedTargetFrameworks) + UniGetUI.PackageEngine.AgentBroker + + + + + + + + + + + + + + + + + + + + diff --git a/src/UniGetUI.PackageEngine.Operations/PackageOperations.cs b/src/UniGetUI.PackageEngine.Operations/PackageOperations.cs index 56741694d2..e4d290a8f2 100644 --- a/src/UniGetUI.PackageEngine.Operations/PackageOperations.cs +++ b/src/UniGetUI.PackageEngine.Operations/PackageOperations.cs @@ -4,6 +4,10 @@ using UniGetUI.Core.SettingsEngine; using UniGetUI.Core.Tools; using UniGetUI.Interface.Enums; +using UniGetUI.PackageEngine.AgentBroker; +// Aliased to avoid clashing with UniGetUI.PackageEngine.Enums.OperationStatus. +using BrokerClient = Devolutions.UniGetUI.Broker.Client.BrokerClient; +using BrokerOperationStatus = Devolutions.UniGetUI.Broker.Client.OperationStatus; using UniGetUI.PackageEngine.Classes.Packages.Classes; using UniGetUI.PackageEngine.Enums; using UniGetUI.PackageEngine.Interfaces; @@ -148,6 +152,105 @@ protected sealed override void PrepareProcessStartInfo() ); } + /// + /// Override to intercept operations and route through the Devolutions Agent broker + /// when the UseAgentBroker setting is enabled and the manager supports it (WinGet only for now). + /// Falls back to process-based execution otherwise. + /// + protected override async Task PerformOperation() + { + if (!ShouldUseAgentBroker()) + { + return await base.PerformOperation(); + } + + return await PerformBrokerOperation(); + } + + /// + /// Determines whether this operation should be routed through the agent broker. + /// + private bool ShouldUseAgentBroker() + { + // NOTE: Change this condition to enable agent broker by default when ready. + // Currently opt-in via settings. + bool settingEnabled = Settings.Get(Settings.K.UseAgentBroker); + bool isWinGet = IsWinGetManager(Package.Manager); + Logger.Info($"[AgentBroker] ShouldUseAgentBroker check: setting={settingEnabled}, isWinGet={isWinGet}, manager={Package.Manager.Name}"); + + if (!settingEnabled) + { + return false; + } + + // Only WinGet is supported in this iteration. + if (!isWinGet) + { + return false; + } + + return true; + } + + /// + /// Perform the package operation through the Devolutions Agent broker. + /// Sends the request over named pipe and interprets the response. + /// + private async Task PerformBrokerOperation() + { + Line("Routing operation through Devolutions Agent broker...", LineType.Information); + + using var client = new BrokerClient(); + + // Check broker availability. + if (!await client.IsAvailableAsync()) + { + Line("Agent broker is not available, falling back to local execution.", LineType.Information); + Logger.Warn("[AgentBroker] Broker not available, falling back to process execution"); + return await base.PerformOperation(); + } + + // Build the broker request. + var request = BrokerRequestBuilder.Build(Package, Options, Role); + + Line($"Sending request to broker: {request.RequestId}", LineType.VerboseDetails); + Line($" Package: {request.Package.Id} ({request.Operation})", LineType.VerboseDetails); + Line($" Manager: {request.Manager.Name}", LineType.VerboseDetails); + Line($" User: {request.Broker.EffectiveUser}", LineType.VerboseDetails); + + // Send to broker and poll until completion. + var status = await client.ExecuteAndWaitAsync(request); + + if (status is null) + { + Line("No response from broker — the operation could not be submitted.", LineType.Error); + Logger.Error("[AgentBroker] ExecuteAndWaitAsync returned null"); + Metadata.FailureTitle = CoreTools.Translate("Broker communication error"); + Metadata.FailureMessage = CoreTools.Translate("The agent broker did not respond. Ensure Devolutions Agent is running."); + return OperationVeredict.Failure; + } + + // Log status details. + Line($"Broker status: {status.Status}, exitCode={status.ExitCode}", LineType.Information); + if (!string.IsNullOrWhiteSpace(status.Note)) + { + Line($" Note: {status.Note}", LineType.Information); + } + + if (status.Status == BrokerOperationStatus.Completed && status.ExitCode == 0) + { + Line("Operation completed successfully via agent broker.", LineType.Information); + return OperationVeredict.Success; + } + + // Operation failed — surface a user-visible error. + string reason = status.Note ?? $"Exit code: {status.ExitCode}"; + Line($"Operation failed via broker: {reason}", LineType.Error); + Metadata.FailureTitle = CoreTools.Translate("Operation denied or failed via broker"); + Metadata.FailureMessage = reason; + return OperationVeredict.Failure; + } + protected sealed override Task GetProcessVeredict( int ReturnCode, List Output diff --git a/src/UniGetUI.PackageEngine.Operations/UniGetUI.PackageEngine.Operations.csproj b/src/UniGetUI.PackageEngine.Operations/UniGetUI.PackageEngine.Operations.csproj index 6b664b6de2..0c8c3cbec3 100644 --- a/src/UniGetUI.PackageEngine.Operations/UniGetUI.PackageEngine.Operations.csproj +++ b/src/UniGetUI.PackageEngine.Operations/UniGetUI.PackageEngine.Operations.csproj @@ -9,6 +9,7 @@ + diff --git a/src/UniGetUI.Windows.slnx b/src/UniGetUI.Windows.slnx index d3ecc29b0e..1633590b49 100644 --- a/src/UniGetUI.Windows.slnx +++ b/src/UniGetUI.Windows.slnx @@ -118,6 +118,10 @@ + + + +