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..db3cd0385b
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.AgentBroker/BrokerRequestBuilder.cs
@@ -0,0 +1,148 @@
+using Devolutions.Now.Policy.Api;
+using Devolutions.Now.Policy.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.Now.Policy.Api.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
+{
+ /// Build a broker request from UniGetUI package operation parameters.
+ public static PackageOperationRequest Build(IPackage package, InstallOptions options, OperationType role)
+ {
+ return new PackageOperationRequest
+ {
+ RequestId = BrokerClient.GenerateRequestId(),
+ CreatedAt = DateTimeOffset.UtcNow,
+ Operation = MapOperation(role),
+ Manager = MapManagerName(package.Manager.Name),
+ CaptureOutput = true,
+ Source = new RequestSource
+ {
+ Name = package.Source.Name,
+ Url = package.Source.Url?.ToString(),
+ },
+ Package = new RequestPackage
+ {
+ Id = package.Id,
+ 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 = NullIfEmpty(options.CustomInstallLocation),
+ KillBeforeOperation = options.KillBeforeOperation ?? [],
+ PreOperationCommand = GetPreCommand(options, role),
+ PostOperationCommand = GetPostCommand(options, role),
+ UninstallPrevious = role is OperationType.Update && options.UninstallPreviousVersionsOnUpdate,
+ NoUpgrade = role is OperationType.Update && options.SkipMinorUpdates,
+ },
+ };
+ }
+
+ 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..890336facc
--- /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..1421e699df 100644
--- a/src/UniGetUI.PackageEngine.Operations/PackageOperations.cs
+++ b/src/UniGetUI.PackageEngine.Operations/PackageOperations.cs
@@ -1,9 +1,11 @@
+using System.Text;
using UniGetUI.Core.Classes;
using UniGetUI.Core.Data;
using UniGetUI.Core.Logging;
using UniGetUI.Core.SettingsEngine;
using UniGetUI.Core.Tools;
using UniGetUI.Interface.Enums;
+using UniGetUI.PackageEngine.AgentBroker;
using UniGetUI.PackageEngine.Classes.Packages.Classes;
using UniGetUI.PackageEngine.Enums;
using UniGetUI.PackageEngine.Interfaces;
@@ -11,6 +13,12 @@
using UniGetUI.PackageEngine.PackageLoader;
using UniGetUI.PackageEngine.Serializable;
using UniGetUI.PackageOperations;
+using BrokerClient = Devolutions.Now.Policy.Client.BrokerClient;
+using BrokerClientErrorKind = Devolutions.Now.Policy.Client.BrokerClientErrorKind;
+using BrokerClientException = Devolutions.Now.Policy.Client.BrokerClientException;
+using BrokerClientOptions = Devolutions.Now.Policy.Client.BrokerClientOptions;
+using BrokerElevation = Devolutions.Now.Policy.Api.Elevation;
+using BrokerOperationStatus = Devolutions.Now.Policy.Api.OperationStatus;
#if WINDOWS
using UniGetUI.PackageEngine.Managers.WingetManager;
#endif
@@ -148,6 +156,188 @@ 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 = CreateBrokerClient(RequiresAdminRights());
+
+ // Check broker availability.
+ if (!await client.IsAvailable())
+ {
+ 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}", LineType.VerboseDetails);
+ Line($" User: {GetEffectiveUser()}", LineType.VerboseDetails);
+
+ try
+ {
+ // Send to broker and poll until completion.
+ var status = await client.ExecuteAndWait(request);
+
+ // Log status details.
+ Line($"Broker status: {status.Status}, exitCode={status.ExitCode}", LineType.Information);
+ if (!string.IsNullOrWhiteSpace(status.Message))
+ {
+ Line($" Message: {status.Message}", LineType.Information);
+ }
+ var output = DisplayBrokerOutput(status.Stdout);
+
+ if (status.Status == BrokerOperationStatus.Completed)
+ {
+ var veredict = await GetProcessVeredict(status.ExitCode ?? -1, output);
+ if (veredict is OperationVeredict.Success)
+ {
+ Line("Operation completed successfully via agent broker.", LineType.Information);
+ }
+ else if (!string.IsNullOrWhiteSpace(status.Message))
+ {
+ Metadata.FailureMessage = status.Message;
+ }
+
+ return veredict;
+ }
+
+ // Operation failed — surface a user-visible error.
+ string reason = status.Message ?? $"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;
+ }
+ catch (BrokerClientException ex)
+ {
+ Line($"Broker operation failed: {ex.Message}", LineType.Error);
+ Logger.Error($"[AgentBroker] Broker operation failed: {ex}");
+ Metadata.FailureTitle = CoreTools.Translate(GetBrokerFailureTitle(ex.Kind));
+ Metadata.FailureMessage = ex.Message;
+ return OperationVeredict.Failure;
+ }
+ }
+
+ private List DisplayBrokerOutput(string? encodedStdout)
+ {
+ List output = [];
+ if (string.IsNullOrWhiteSpace(encodedStdout))
+ {
+ return output;
+ }
+
+ string decoded;
+ try
+ {
+ decoded = Encoding.UTF8.GetString(Convert.FromBase64String(encodedStdout));
+ }
+ catch (FormatException ex)
+ {
+ Logger.Error($"[AgentBroker] Broker returned invalid base64 stdout: {ex}");
+ Line("Broker returned captured output in an invalid format.", LineType.Error);
+ return output;
+ }
+
+ foreach (var line in decoded.Replace("\r\n", "\n").Replace('\r', '\n').Split('\n'))
+ {
+ if (line.Length == 0)
+ {
+ continue;
+ }
+
+ output.Add(line);
+ Line(line, LineType.Information);
+ }
+
+ return output;
+ }
+
+ private static BrokerClient CreateBrokerClient(bool requestedElevation) =>
+ new(
+ new BrokerClientOptions
+ {
+ RequestedElevation = requestedElevation
+ ? BrokerElevation.Elevated
+ : BrokerElevation.Standard,
+ EffectiveUser = GetEffectiveUser(),
+ ClientExecutablePath = Environment.ProcessPath,
+ ClientVersion =
+ System.Reflection.Assembly.GetEntryAssembly()?.GetName().Version?.ToString()
+ ?? "0.0.0",
+ }
+ )
+ {
+ Trace = message => Logger.Info($"[AgentBroker] {message}"),
+ };
+
+ private static string GetEffectiveUser()
+ {
+ if (string.IsNullOrWhiteSpace(Environment.UserDomainName))
+ {
+ return Environment.UserName;
+ }
+
+ return $"{Environment.UserDomainName}\\{Environment.UserName}";
+ }
+
+ private static string GetBrokerFailureTitle(BrokerClientErrorKind kind) =>
+ kind switch
+ {
+ BrokerClientErrorKind.PolicyDenied => "Operation denied by policy",
+ BrokerClientErrorKind.UnsupportedCapability => "Operation unsupported by broker",
+ BrokerClientErrorKind.BrokerUnavailable or BrokerClientErrorKind.Timeout => "Broker communication error",
+ _ => "Operation failed via broker",
+ };
+
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 @@
+
+
+
+