Skip to content

Commit 2e6075e

Browse files
Add automation secure settings controls
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent fd02ae0 commit 2e6075e

File tree

9 files changed

+546
-10
lines changed

9 files changed

+546
-10
lines changed

cli-arguments.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,15 @@
3131
| `--automation add-source --manager name --source-name name [--source-url url]` | Adds a known or custom source through the automation service | 2026.1+ |
3232
| `--automation remove-source --manager name --source-name name [--source-url url]` | Removes a source through the automation service | 2026.1+ |
3333
| `--automation list-settings` | Lists non-sensitive settings with their current boolean/string state | 2026.1+ |
34+
| `--automation list-secure-settings [--user name]` | Lists all secure settings for the current user or a specified user in machine-readable form | 2026.1+ |
35+
| `--automation get-secure-setting --key key [--user name]` | Reads one secure setting for the current user or a specified user | 2026.1+ |
36+
| `--automation set-secure-setting --key key --enabled true\|false [--user name]` | Enables or disables one secure setting for the current user or a specified user | 2026.1+ |
3437
| `--automation get-setting --key key` | Reads a single non-sensitive setting through the automation service | 2026.1+ |
3538
| `--automation set-setting --key key (--enabled true|false \| --value text)` | Sets a boolean or string setting through the automation service | 2026.1+ |
3639
| `--automation clear-setting --key key` | Clears a string-backed setting through the automation service | 2026.1+ |
3740
| `--automation reset-settings` | Resets non-secure settings while preserving the active automation session token | 2026.1+ |
41+
| `--automation set-manager-enabled --manager name --enabled true\|false` | Enables or disables one package manager and reloads it immediately | 2026.1+ |
42+
| `--automation set-manager-update-notifications --manager name --enabled true\|false` | Enables or suppresses update notifications for one package manager | 2026.1+ |
3843
| `--automation list-desktop-shortcuts` | Lists tracked desktop shortcuts, their current keep/delete/unknown verdicts, and whether each shortcut still exists on disk | 2026.1+ |
3944
| `--automation set-desktop-shortcut --path path --status {keep\|delete}` | Marks a tracked shortcut to be kept or deleted; `delete` also removes the shortcut from disk when present | 2026.1+ |
4045
| `--automation reset-desktop-shortcut --path path` | Clears the stored verdict for one tracked desktop shortcut | 2026.1+ |
@@ -90,7 +95,7 @@
9095

9196
- `dotnet src\UniGetUI.Avalonia\bin\Release\net10.0\UniGetUI.Avalonia.dll --headless` starts the local automation daemon without opening any window or requiring a graphical desktop session.
9297
- `dotnet src\UniGetUI.Cli\bin\Release\net10.0\UniGetUI.Cli.dll <command>` is the cross-platform CLI wrapper for the automation service. It automatically prepends `--automation`, so `UniGetUI.Cli status` and `UniGetUI.Cli search-packages --manager ".NET Tool" --query dotnetsay` work directly.
93-
- Current agent-oriented command coverage includes status/version, manager/source inspection plus manager-maintenance and executable-path control, settings inspection and mutation, desktop-shortcut state management, app/history/manager log inspection, local backup creation and GitHub cloud-backup/auth flows, current bundle inspection/import/export/add/remove/install flows, package search/details/version listing, ignored-update management, and package install/update/uninstall flows.
98+
- Current agent-oriented command coverage includes status/version, manager/source inspection plus manager enablement, notification suppression, manager-maintenance and executable-path control, settings and secure-settings inspection/mutation, desktop-shortcut state management, app/history/manager log inspection, local backup creation and GitHub cloud-backup/auth flows, current bundle inspection/import/export/add/remove/install flows, package search/details/version listing, ignored-update management, and package install/update/uninstall flows.
9499

95100
<br><br>
96101
# `unigetui://` deep link

src/UniGetUI.Core.SecureSettings/SecureSettings.cs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,32 +48,42 @@ public static class Args
4848

4949
public static bool Get(K key)
5050
{
51-
string purifiedSetting = CoreTools.MakeValidFileName(ResolveKey(key));
52-
if (_cache.TryGetValue(purifiedSetting, out var value))
51+
return GetForUser(Environment.UserName, key);
52+
}
53+
54+
public static bool GetForUser(string username, K key)
55+
{
56+
return GetForUser(username, ResolveKey(key));
57+
}
58+
59+
public static bool GetForUser(string username, string setting)
60+
{
61+
string purifiedSetting = CoreTools.MakeValidFileName(setting);
62+
string purifiedUser = CoreTools.MakeValidFileName(username);
63+
string cacheKey = $"{purifiedUser}|{purifiedSetting}";
64+
if (_cache.TryGetValue(cacheKey, out var value))
5365
{
5466
return value;
5567
}
5668

57-
string purifiedUser = CoreTools.MakeValidFileName(Environment.UserName);
58-
5969
var settingsLocation = Path.Join(GetSecureSettingsRoot(), purifiedUser);
6070
var settingFile = Path.Join(settingsLocation, purifiedSetting);
6171

6272
if (!Directory.Exists(settingsLocation))
6373
{
64-
_cache[purifiedSetting] = false;
74+
_cache[cacheKey] = false;
6575
return false;
6676
}
6777

6878
bool exists = File.Exists(settingFile);
69-
_cache[purifiedSetting] = exists;
79+
_cache[cacheKey] = exists;
7080
return exists;
7181
}
7282

7383
public static async Task<bool> TrySet(K key, bool enabled)
7484
{
7585
string purifiedSetting = CoreTools.MakeValidFileName(ResolveKey(key));
76-
_cache.Remove(purifiedSetting);
86+
_cache.Remove($"{CoreTools.MakeValidFileName(Environment.UserName)}|{purifiedSetting}");
7787

7888
string purifiedUser = CoreTools.MakeValidFileName(Environment.UserName);
7989

@@ -107,9 +117,8 @@ public static int ApplyForUser(string username, string setting, bool enable)
107117
try
108118
{
109119
string purifiedSetting = CoreTools.MakeValidFileName(setting);
110-
_cache.Remove(purifiedSetting);
111-
112120
string purifiedUser = CoreTools.MakeValidFileName(username);
121+
_cache.Remove($"{purifiedUser}|{purifiedSetting}");
113122

114123
var settingsLocation = Path.Join(GetSecureSettingsRoot(), purifiedUser);
115124
var settingFile = Path.Join(settingsLocation, purifiedSetting);

src/UniGetUI.Interface.BackgroundApi/AutomationCliCommandRunner.cs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,41 @@ await client.RemoveSourceAsync(BuildSourceRequest(args))
107107
settings = await client.ListSettingsAsync(),
108108
}
109109
),
110+
"list-secure-settings" => await WriteJsonAsync(
111+
output,
112+
new
113+
{
114+
status = "success",
115+
settings = await client.ListSecureSettingsAsync(
116+
GetOptionalArgument(args, "--user")
117+
),
118+
}
119+
),
120+
"get-secure-setting" => await WriteJsonAsync(
121+
output,
122+
new
123+
{
124+
status = "success",
125+
setting = await client.GetSecureSettingAsync(
126+
GetRequiredArgument(
127+
args,
128+
"--key",
129+
"The get-secure-setting automation command requires --key."
130+
),
131+
GetOptionalArgument(args, "--user")
132+
),
133+
}
134+
),
135+
"set-secure-setting" => await WriteJsonAsync(
136+
output,
137+
new
138+
{
139+
status = "success",
140+
setting = await client.SetSecureSettingAsync(
141+
BuildSecureSettingRequest(args)
142+
),
143+
}
144+
),
110145
"get-setting" => await WriteJsonAsync(
111146
output,
112147
new
@@ -143,6 +178,24 @@ await client.RemoveSourceAsync(BuildSourceRequest(args))
143178
),
144179
}
145180
),
181+
"set-manager-enabled" => await WriteJsonAsync(
182+
output,
183+
new
184+
{
185+
status = "success",
186+
manager = await client.SetManagerEnabledAsync(BuildManagerToggleRequest(args)),
187+
}
188+
),
189+
"set-manager-update-notifications" => await WriteJsonAsync(
190+
output,
191+
new
192+
{
193+
status = "success",
194+
manager = await client.SetManagerUpdateNotificationsAsync(
195+
BuildManagerToggleRequest(args)
196+
),
197+
}
198+
),
146199
"reset-settings" => await WriteJsonAsync(
147200
output,
148201
await client.ResetSettingsAsync()
@@ -484,6 +537,31 @@ private static AutomationManagerMaintenanceRequest BuildManagerMaintenanceReques
484537
};
485538
}
486539

540+
private static AutomationSecureSettingRequest BuildSecureSettingRequest(
541+
IReadOnlyList<string> args
542+
)
543+
{
544+
return new AutomationSecureSettingRequest
545+
{
546+
SettingKey = GetRequiredArgument(args, "--key", "This automation command requires --key."),
547+
UserName = GetOptionalArgument(args, "--user"),
548+
Enabled = GetRequiredBoolArgument(args, "--enabled"),
549+
};
550+
}
551+
552+
private static AutomationManagerToggleRequest BuildManagerToggleRequest(IReadOnlyList<string> args)
553+
{
554+
return new AutomationManagerToggleRequest
555+
{
556+
ManagerName = GetRequiredArgument(
557+
args,
558+
"--manager",
559+
"This automation command requires --manager."
560+
),
561+
Enabled = GetRequiredBoolArgument(args, "--enabled"),
562+
};
563+
}
564+
487565
private static AutomationDesktopShortcutRequest BuildDesktopShortcutRequest(
488566
IReadOnlyList<string> args,
489567
bool requireStatus
@@ -677,6 +755,19 @@ string argumentName
677755
);
678756
}
679757

758+
private static bool GetRequiredBoolArgument(IReadOnlyList<string> arguments, string argumentName)
759+
{
760+
bool? value = GetOptionalBoolArgument(arguments, argumentName);
761+
if (!value.HasValue)
762+
{
763+
throw new InvalidOperationException(
764+
$"This automation command requires {argumentName} with a value of true or false."
765+
);
766+
}
767+
768+
return value.Value;
769+
}
770+
680771
private static async Task<int> WriteJsonAsync<T>(TextWriter output, T value)
681772
{
682773
await output.WriteLineAsync(

src/UniGetUI.Interface.BackgroundApi/AutomationManagerMaintenanceApi.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public sealed class AutomationManagerMaintenanceInfo
2121
public bool? UseBundledWinGet { get; set; }
2222
public bool? UseSystemChocolatey { get; set; }
2323
public bool? ScoopCleanupOnLaunch { get; set; }
24+
public bool UpdateNotificationsSuppressed { get; set; }
2425
public string? DefaultVcpkgTriplet { get; set; }
2526
public IReadOnlyList<string> AvailableVcpkgTriplets { get; set; } = [];
2627
public string? CustomVcpkgRoot { get; set; }
@@ -281,6 +282,10 @@ private static AutomationManagerMaintenanceInfo ToMaintenanceInfo(IPackageManage
281282
ScoopCleanupOnLaunch = manager.Name.Equals("Scoop", StringComparison.OrdinalIgnoreCase)
282283
? Settings.Get(Settings.K.EnableScoopCleanup)
283284
: null,
285+
UpdateNotificationsSuppressed = Settings.GetDictionaryItem<string, bool>(
286+
Settings.K.DisabledPackageManagerNotifications,
287+
manager.Name
288+
),
284289
DefaultVcpkgTriplet = manager.Name.Equals("vcpkg", StringComparison.OrdinalIgnoreCase)
285290
? Settings.GetValue(Settings.K.DefaultVcpkgTriplet)
286291
: null,

src/UniGetUI.Interface.BackgroundApi/AutomationManagerSettingsApi.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public sealed class AutomationManagerInfo
3030
public string DisplayName { get; set; } = "";
3131
public bool Enabled { get; set; }
3232
public bool Ready { get; set; }
33+
public bool NotificationsSuppressed { get; set; }
3334
public string ExecutablePath { get; set; } = "";
3435
public string ExecutableArguments { get; set; } = "";
3536
public AutomationManagerCapabilitiesInfo Capabilities { get; set; } = new();
@@ -80,6 +81,12 @@ public sealed class AutomationSettingValueRequest
8081
public string? Value { get; set; }
8182
}
8283

84+
public sealed class AutomationManagerToggleRequest
85+
{
86+
public string ManagerName { get; set; } = "";
87+
public bool Enabled { get; set; }
88+
}
89+
8390
public static class AutomationManagerSettingsApi
8491
{
8592
private static readonly HashSet<Settings.K> HiddenSettings =
@@ -190,6 +197,31 @@ public static void ResetSettingsPreservingSession()
190197
}
191198
}
192199

200+
public static async Task<AutomationManagerInfo> SetManagerEnabledAsync(
201+
AutomationManagerToggleRequest request
202+
)
203+
{
204+
ArgumentNullException.ThrowIfNull(request);
205+
var manager = ResolveManager(request.ManagerName);
206+
Settings.SetDictionaryItem(Settings.K.DisabledManagers, manager.Name, !request.Enabled);
207+
await Task.Run(manager.Initialize);
208+
return ToManagerInfo(manager);
209+
}
210+
211+
public static AutomationManagerInfo SetManagerNotifications(
212+
AutomationManagerToggleRequest request
213+
)
214+
{
215+
ArgumentNullException.ThrowIfNull(request);
216+
var manager = ResolveManager(request.ManagerName);
217+
Settings.SetDictionaryItem(
218+
Settings.K.DisabledPackageManagerNotifications,
219+
manager.Name,
220+
!request.Enabled
221+
);
222+
return ToManagerInfo(manager);
223+
}
224+
193225
private static AutomationManagerInfo ToManagerInfo(IPackageManager manager)
194226
{
195227
return new AutomationManagerInfo
@@ -198,6 +230,10 @@ private static AutomationManagerInfo ToManagerInfo(IPackageManager manager)
198230
DisplayName = manager.DisplayName,
199231
Enabled = manager.IsEnabled(),
200232
Ready = manager.IsReady(),
233+
NotificationsSuppressed = Settings.GetDictionaryItem<string, bool>(
234+
Settings.K.DisabledPackageManagerNotifications,
235+
manager.Name
236+
),
201237
ExecutablePath = manager.Status.ExecutablePath,
202238
ExecutableArguments = manager.Status.ExecutableCallArgs,
203239
Capabilities = new AutomationManagerCapabilitiesInfo
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using UniGetUI.Core.SettingsEngine.SecureSettings;
2+
3+
namespace UniGetUI.Interface;
4+
5+
public sealed class AutomationSecureSettingInfo
6+
{
7+
public string Key { get; set; } = "";
8+
public string Name { get; set; } = "";
9+
public string UserName { get; set; } = "";
10+
public bool IsCurrentUser { get; set; }
11+
public bool Enabled { get; set; }
12+
}
13+
14+
public sealed class AutomationSecureSettingRequest
15+
{
16+
public string SettingKey { get; set; } = "";
17+
public string? UserName { get; set; }
18+
public bool Enabled { get; set; }
19+
}
20+
21+
public static class AutomationSecureSettingsApi
22+
{
23+
public static IReadOnlyList<AutomationSecureSettingInfo> ListSettings(string? userName = null)
24+
{
25+
string resolvedUser = ResolveUserName(userName);
26+
return Enum.GetValues<SecureSettings.K>()
27+
.Where(key => key != SecureSettings.K.Unset)
28+
.OrderBy(key => key.ToString(), StringComparer.OrdinalIgnoreCase)
29+
.Select(key => ToSecureSettingInfo(key, resolvedUser))
30+
.ToArray();
31+
}
32+
33+
public static AutomationSecureSettingInfo GetSetting(string settingKey, string? userName = null)
34+
{
35+
return ToSecureSettingInfo(ResolveSettingKey(settingKey), ResolveUserName(userName));
36+
}
37+
38+
public static async Task<AutomationSecureSettingInfo> SetSettingAsync(
39+
AutomationSecureSettingRequest request
40+
)
41+
{
42+
ArgumentNullException.ThrowIfNull(request);
43+
var key = ResolveSettingKey(request.SettingKey);
44+
string userName = ResolveUserName(request.UserName);
45+
46+
bool success = userName.Equals(Environment.UserName, StringComparison.OrdinalIgnoreCase)
47+
? await SecureSettings.TrySet(key, request.Enabled)
48+
: SecureSettings.ApplyForUser(userName, SecureSettings.ResolveKey(key), request.Enabled) == 0;
49+
50+
if (!success)
51+
{
52+
throw new InvalidOperationException(
53+
$"Could not update secure setting \"{SecureSettings.ResolveKey(key)}\" for user \"{userName}\"."
54+
);
55+
}
56+
57+
return ToSecureSettingInfo(key, userName);
58+
}
59+
60+
private static AutomationSecureSettingInfo ToSecureSettingInfo(
61+
SecureSettings.K key,
62+
string userName
63+
)
64+
{
65+
return new AutomationSecureSettingInfo
66+
{
67+
Key = key.ToString(),
68+
Name = SecureSettings.ResolveKey(key),
69+
UserName = userName,
70+
IsCurrentUser = userName.Equals(Environment.UserName, StringComparison.OrdinalIgnoreCase),
71+
Enabled = SecureSettings.GetForUser(userName, key),
72+
};
73+
}
74+
75+
private static SecureSettings.K ResolveSettingKey(string settingKey)
76+
{
77+
if (string.IsNullOrWhiteSpace(settingKey))
78+
{
79+
throw new InvalidOperationException("The secure setting key parameter is required.");
80+
}
81+
82+
if (Enum.TryParse(settingKey, true, out SecureSettings.K enumKey) && enumKey != SecureSettings.K.Unset)
83+
{
84+
return enumKey;
85+
}
86+
87+
foreach (var key in Enum.GetValues<SecureSettings.K>())
88+
{
89+
if (
90+
key != SecureSettings.K.Unset
91+
&& SecureSettings.ResolveKey(key).Equals(settingKey, StringComparison.OrdinalIgnoreCase)
92+
)
93+
{
94+
return key;
95+
}
96+
}
97+
98+
throw new InvalidOperationException($"No secure setting matching \"{settingKey}\" was found.");
99+
}
100+
101+
private static string ResolveUserName(string? userName)
102+
{
103+
return string.IsNullOrWhiteSpace(userName) ? Environment.UserName : userName.Trim();
104+
}
105+
}

0 commit comments

Comments
 (0)