Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
src/UniGetUI.PackageEngine.Managers.Chocolatey/choco-cli/** linguist-vendored
.githooks/* text eol=lf
.githooks/* text eol=lf

policies/samples/** linguist-generated
policies/schemas/** linguist-generated
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "10.0.103",
"rollForward": "latestPatch"
"rollForward": "latestFeature"
}
}
4 changes: 4 additions & 0 deletions src/UniGetUI.Core.Settings/SettingsEngine_Names.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand Down
161 changes: 161 additions & 0 deletions src/UniGetUI.PackageEngine.AgentBroker/BrokerRequestBuilder.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Builds broker protocol requests from UniGetUI domain objects.
/// Maps IPackage + InstallOptions + OperationType into the canonical
/// <see cref="PackageRequest"/> consumed by the Devolutions Agent broker.
/// </summary>
public static class BrokerRequestBuilder
{
private static readonly string ClientVersion =
System.Reflection.Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0";

/// <summary>Build a broker request from UniGetUI package operation parameters.</summary>
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}"),
};

/// <summary>
/// Maps UniGetUI manager names to the broker protocol canonical managers.
/// PowerShell 5 and PowerShell 7 are modeled as separate managers.
/// </summary>
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<string> 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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>$(SharedTargetFrameworks)</TargetFrameworks>
<RootNamespace>UniGetUI.PackageEngine.AgentBroker</RootNamespace>
</PropertyGroup>

<ItemGroup>
<!-- Canonical broker client + DTOs, sourced from the devolutions-gateway repo. -->
<PackageReference Include="Devolutions.UniGetUI.Broker.Client" Version="2026.6.10" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\UniGetUI.Core.Logger\UniGetUI.Core.Logging.csproj" />
<ProjectReference Include="..\UniGetUI.Core.Settings\UniGetUI.Core.Settings.csproj" />
<ProjectReference Include="..\UniGetUI.PackageEngine.Enums\UniGetUI.PackageEngine.Structs.csproj" />
<ProjectReference Include="..\UniGetUI.PAckageEngine.Interfaces\UniGetUI.PackageEngine.Interfaces.csproj" />
<ProjectReference Include="..\UniGetUI.PackageEngine.Serializable\UniGetUI.PackageEngine.Serializable.csproj" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\SharedAssemblyInfo.cs" Link="SharedAssemblyInfo.cs" />
</ItemGroup>

</Project>
103 changes: 103 additions & 0 deletions src/UniGetUI.PackageEngine.Operations/PackageOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -148,6 +152,105 @@ protected sealed override void PrepareProcessStartInfo()
);
}

/// <summary>
/// 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.
/// </summary>
protected override async Task<OperationVeredict> PerformOperation()
{
if (!ShouldUseAgentBroker())
{
return await base.PerformOperation();
}

return await PerformBrokerOperation();
}

/// <summary>
/// Determines whether this operation should be routed through the agent broker.
/// </summary>
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;
}

/// <summary>
/// Perform the package operation through the Devolutions Agent broker.
/// Sends the request over named pipe and interprets the response.
/// </summary>
private async Task<OperationVeredict> 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<OperationVeredict> GetProcessVeredict(
int ReturnCode,
List<string> Output
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<ProjectReference Include="..\UniGetUI.Core.Logger\UniGetUI.Core.Logging.csproj" />
<ProjectReference Include="..\UniGetUI.Core.Settings\UniGetUI.Core.Settings.csproj" />
<ProjectReference Include="..\UniGetUI.Core.Tools\UniGetUI.Core.Tools.csproj" />
<ProjectReference Include="..\UniGetUI.PackageEngine.AgentBroker\UniGetUI.PackageEngine.AgentBroker.csproj" />
<ProjectReference Include="..\UniGetUI.PackageEngine.Enums\UniGetUI.PackageEngine.Structs.csproj" />
<ProjectReference Include="..\UniGetUI.PackageEngine.PackageLoader\UniGetUI.PackageEngine.PackageLoaders.csproj" />
<ProjectReference Include="..\UniGetUI.PackageEngine.PackageManagerClasses\UniGetUI.PackageEngine.Classes.csproj" />
Expand Down
4 changes: 4 additions & 0 deletions src/UniGetUI.Windows.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@
<Platform Solution="*|arm64" Project="arm64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="UniGetUI.PackageEngine.AgentBroker/UniGetUI.PackageEngine.AgentBroker.csproj">
<Platform Solution="*|arm64" Project="arm64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="UniGetUI.PackageEngine.PackageEngine/UniGetUI.PackageEngine.PEInterface.csproj">
<Platform Solution="*|arm64" Project="arm64" />
<Platform Solution="*|x64" Project="x64" />
Expand Down