diff --git a/.gitattributes b/.gitattributes index 3eb52bd81..66a0b82b5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,5 @@ +/.gitattributes text eol=lf + *.rs text eol=lf *.toml text eol=lf *.cs text eol=lf @@ -6,6 +8,7 @@ *.sln text eol=crlf *.md text eol=lf *.mustache text eol=lf +*.yml text eol=lf *.yaml text eol=lf devolutions-gateway/openapi/doc/index.adoc linguist-generated merge=binary @@ -14,3 +17,6 @@ devolutions-gateway/openapi/dotnet-client/docs/** linguist-generated merge=binar devolutions-gateway/openapi/dotnet-subscriber/src/** linguist-generated merge=binary devolutions-gateway/openapi/ts-angular-client/api/** linguist-generated merge=binary devolutions-gateway/openapi/ts-angular-client/model/** linguist-generated merge=binary + +# Sample assets produce huge LoC counts; exclude them from language statistics. +crates/unigetui-broker/assets/** linguist-generated diff --git a/Cargo.lock b/Cargo.lock index 560224325..0f5da8625 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1185,6 +1185,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1576,6 +1585,29 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2 1.0.106", + "quote 1.0.45", + "rustc_version", + "syn 2.0.117", + "unicode-xid 0.2.6", +] + [[package]] name = "des" version = "0.9.0-rc.1" @@ -1637,6 +1669,7 @@ dependencies = [ "tonic 0.12.3", "tonic-build", "tracing", + "unigetui-broker", "url", "uuid", "win-api-wrappers", @@ -2303,6 +2336,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -3277,6 +3320,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inotify" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + [[package]] name = "inotify" version = "0.11.1" @@ -4631,6 +4685,24 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9737e026353e5cd0736f98eddae28665118eb6f6600902a7f50db585621fecb6" +[[package]] +name = "notify" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009" +dependencies = [ + "bitflags 2.11.1", + "filetime", + "inotify 0.10.2", + "kqueue", + "libc", + "log", + "mio", + "notify-types 1.0.1", + "walkdir", + "windows-sys 0.52.0", +] + [[package]] name = "notify" version = "8.2.0" @@ -4639,12 +4711,12 @@ checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ "bitflags 2.11.1", "fsevent-sys", - "inotify", + "inotify 0.11.1", "kqueue", "libc", "log", "mio", - "notify-types", + "notify-types 2.1.0", "walkdir", "windows-sys 0.60.2", ] @@ -4656,11 +4728,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a689eb4262184d9a1727f9087cd03883ea716682ab03ed24efec57d7716dccb8" dependencies = [ "log", - "notify", - "notify-types", + "notify 8.2.0", + "notify-types 2.1.0", "tempfile", ] +[[package]] +name = "notify-types" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585d3cb5e12e01aed9e8a1f70d5c6b5e86fe2a6e48fc8cd0b3e0b8df6f6eb174" +dependencies = [ + "instant", +] + [[package]] name = "notify-types" version = "2.1.0" @@ -4670,6 +4751,57 @@ dependencies = [ "bitflags 2.11.1", ] +[[package]] +name = "now-policy" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d87d7ba63d8fcbbae125f975c5e30a54ed4507027dd70d6daa0aa63003cac2e" +dependencies = [ + "chrono", + "schemars", + "semver", + "serde", + "serde_json", + "serde_yaml", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "now-policy-api" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3837290f7ed48038a8ca4d9dfb98a0c44a5debe141cdd7d740d59eca4df4fa5" +dependencies = [ + "base64 0.22.1", + "chrono", + "derive_more", + "now-policy", + "schemars", + "semver", + "serde", + "serde_json", + "strum", + "thiserror 2.0.18", +] + +[[package]] +name = "now-policy-server-template" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92afa468bf18b66af5d209caac39fcff241b07495c538d032221736e8a7f95c6" +dependencies = [ + "aide", + "async-trait", + "axum 0.8.9", + "now-policy", + "now-policy-api", + "schemars", + "serde", + "serde_json", + "serde_yaml", +] + [[package]] name = "now-proto-pdu" version = "0.4.3" @@ -7098,6 +7230,27 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2 1.0.106", + "quote 1.0.45", + "syn 2.0.117", +] + [[package]] name = "subtle" version = "2.6.1" @@ -8272,6 +8425,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + [[package]] name = "unicode-xid" version = "0.1.0" @@ -8284,6 +8443,43 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unigetui-broker" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "axum 0.8.9", + "base64 0.22.1", + "bytes 1.11.1", + "chrono", + "devolutions-gateway-task", + "http-body-util", + "hyper 1.9.0", + "hyper-util", + "notify 7.0.0", + "now-policy", + "now-policy-api", + "now-policy-server-template", + "regex", + "semver", + "serde", + "serde_json", + "serde_yaml", + "thiserror 2.0.18", + "tokio 1.52.3", + "tokio-util", + "tower 0.5.3", + "tower-http 0.5.2", + "tower-service", + "tracing", + "tracing-subscriber", + "url", + "uuid", + "win-api-wrappers", + "windows 0.61.3", +] + [[package]] name = "universal-hash" version = "0.5.1" diff --git a/crates/unigetui-broker/Cargo.toml b/crates/unigetui-broker/Cargo.toml new file mode 100644 index 000000000..bbc85a06a --- /dev/null +++ b/crates/unigetui-broker/Cargo.toml @@ -0,0 +1,60 @@ +[package] +name = "unigetui-broker" +version = "0.1.0" +edition = "2024" +authors = ["Devolutions Inc. "] +description = "UniGetUI package broker - policy evaluation and command execution over named pipe" +publish = false + +[lints] +workspace = true + +[dependencies] +anyhow = "1" +async-trait = "0.1" +base64 = "0.22" +devolutions-gateway-task = { path = "../devolutions-gateway-task" } +axum = { version = "0.8", default-features = false, features = ["http1", "json", "tokio", "original-uri", "matched-path"] } +now-policy = "0.1" +now-policy-api = { version = "0.1", features = ["policy-compat"] } +now-policy-server-template = { version = "0.1", features = ["policy-compat"] } +tower = "0.5" +tower-service = "0.3" +tower-http = { version = "0.5", features = ["timeout"] } +hyper = { version = "1", features = ["http1", "server"] } +hyper-util = { version = "0.1", features = ["tokio", "server", "server-auto", "service"] } +http-body-util = "0.1" +bytes = "1" +semver = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde_yaml = "0.9" +notify = { version = "7", default-features = false, features = ["macos_kqueue"] } +thiserror = "2" +tokio = { version = "1.52", features = ["full"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +url = "2" +uuid = { version = "1.17", features = ["v4"] } +chrono = { version = "0.4", features = ["serde"] } +regex = "1" + +[target.'cfg(windows)'.dependencies] +win-api-wrappers = { path = "../win-api-wrappers" } +windows = { version = "0.61", features = [ + "Win32_Security", + "Win32_Security_Authorization", + "Win32_System_Pipes", + "Win32_System_IO", + "Win32_Storage_FileSystem", + "Win32_Foundation", + "Win32_System_Threading", + "Win32_System_RemoteDesktop", + "Win32_System_Environment", + "Win32_UI_WindowsAndMessaging", +] } +tokio-util = { version = "0.7", features = ["compat"] } + +[[bin]] +name = "unigetui-broker-standalone" +path = "tools/standalone.rs" diff --git a/crates/unigetui-broker/assets/samples/corporate-allowlist.policy.json b/crates/unigetui-broker/assets/samples/corporate-allowlist.policy.json new file mode 100644 index 000000000..55827e552 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/corporate-allowlist.policy.json @@ -0,0 +1,134 @@ +{ + "$schema": "https://devolutions.net/schemas/now-policy.schema.1.0.json", + "PolicyVersion": "1.0.0", + "PolicyType": "PackageBrokerPolicy", + "Metadata": { + "Id": "contoso.desktop.standard-allowlist", + "Publisher": "Contoso IT", + "Revision": 4, + "PublishedAt": "2026-05-05T00:00:00Z", + "Description": "Fail-closed policy for standard workstation package installs." + }, + "Enforcement": { + "DefaultDecision": "Deny", + "RulePrecedence": "PriorityThenDeny" + }, + "Rules": [ + { + "Id": "deny.integrity-bypass", + "Enabled": true, + "Priority": 10, + "Decision": "Deny", + "Reason": "Integrity and publisher checks cannot be bypassed by brokered requests.", + "Match": { + "Operations": [ + "Install", + "Update" + ], + "SkipHashCheck": [ + true + ] + } + }, + { + "Id": "deny.custom-parameters", + "Enabled": true, + "Priority": 20, + "Decision": "Deny", + "Reason": "Custom package-manager parameters are not allowed in the workstation allow list.", + "Match": { + "HasCustomParameters": [ + true + ] + } + }, + { + "Id": "deny.prepost-commands", + "Enabled": true, + "Priority": 30, + "Decision": "Deny", + "Reason": "Pre and post operation commands are not allowed in the workstation allow list.", + "Match": { + "HasPrePostCommands": [ + true + ] + } + }, + { + "Id": "allow.winget.vscode", + "Enabled": true, + "Priority": 100, + "Decision": "Allow", + "Reason": "Visual Studio Code is approved for managed workstations.", + "Match": { + "Operations": [ + "Install", + "Update" + ], + "Managers": [ + "Winget" + ], + "Sources": [ + "winget" + ], + "PackageIdentifiers": [ + "Microsoft.VisualStudioCode" + ], + "Scopes": [ + "User", + "Machine" + ], + "Architectures": [ + "X64", + "Arm64" + ] + }, + "Constraints": { + "AllowInteractive": false, + "AllowSkipHashCheck": false, + "AllowPreRelease": false, + "AllowCustomParameters": false, + "AllowPrePostCommands": false, + "AllowKillBeforeOperation": false + } + }, + { + "Id": "allow.winget.powertoys", + "Enabled": true, + "Priority": 100, + "Decision": "Allow", + "Reason": "PowerToys is approved for developer workstations.", + "Match": { + "Operations": [ + "Install", + "Update" + ], + "Managers": [ + "Winget" + ], + "Sources": [ + "winget" + ], + "PackageIdentifiers": [ + "Microsoft.PowerToys" + ], + "Scopes": [ + "User", + "Machine" + ], + "Architectures": [ + "X64", + "Arm64" + ] + }, + "Constraints": { + "AllowInteractive": false, + "AllowSkipHashCheck": false, + "AllowPreRelease": false, + "AllowCustomParameters": false, + "AllowPrePostCommands": false, + "AllowKillBeforeOperation": false + } + } + ] +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/deny-risky-options.policy.json b/crates/unigetui-broker/assets/samples/deny-risky-options.policy.json new file mode 100644 index 000000000..76197cb01 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/deny-risky-options.policy.json @@ -0,0 +1,81 @@ +{ + "$schema": "https://devolutions.net/schemas/now-policy.schema.1.0.json", + "PolicyVersion": "1.0.0", + "PolicyType": "PackageBrokerPolicy", + "Metadata": { + "Id": "contoso.desktop.deny-risky-options", + "Publisher": "Contoso IT", + "Revision": 2, + "PublishedAt": "2026-05-05T00:00:00Z", + "Description": "Default-allow policy that blocks risky broker request options." + }, + "Enforcement": { + "DefaultDecision": "Allow", + "RulePrecedence": "PriorityThenDeny" + }, + "Rules": [ + { + "Id": "deny.integrity-bypass", + "Priority": 10, + "Decision": "Deny", + "Reason": "Do not broker installs that skip WinGet hash checks or PowerShell publisher checks.", + "Match": { + "Operations": [ + "Install", + "Update" + ], + "SkipHashCheck": [ + true + ] + } + }, + { + "Id": "deny.manager-custom-parameters", + "Priority": 20, + "Decision": "Deny", + "Reason": "Custom package-manager parameters require a dedicated exception policy.", + "Match": { + "HasCustomParameters": [ + true + ] + } + }, + { + "Id": "deny.prepost-commands", + "Priority": 30, + "Decision": "Deny", + "Reason": "Pre and post operation commands are outside the package manager trust boundary.", + "Match": { + "HasPrePostCommands": [ + true + ] + } + }, + { + "Id": "deny.kill-process-actions", + "Priority": 40, + "Decision": "Deny", + "Reason": "Killing processes before a brokered package operation is not allowed by this policy.", + "Match": { + "HasKillBeforeOperation": [ + true + ] + } + }, + { + "Id": "deny.unapproved-winget-source", + "Priority": 50, + "Decision": "Deny", + "Reason": "Only the default WinGet source is accepted by this deny-list sample.", + "Match": { + "Managers": [ + "Winget" + ], + "Sources": [ + "msstore", + "winget-fonts" + ] + } + } + ] +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/requests/winget-unknown-install.request.json b/crates/unigetui-broker/assets/samples/requests/winget-unknown-install.request.json new file mode 100644 index 000000000..5396eaa11 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/winget-unknown-install.request.json @@ -0,0 +1,31 @@ +{ + "RequestKind": "PackageRequest", + "RequestVersion": "1.0", + "RequestId": "req-winget-unknown-install", + "CreatedAt": "2026-05-05T12:05:00Z", + "Operation": "Install", + "Manager": "Winget", + "Source": { + "Name": "winget", + "Url": "https://cdn.winget.microsoft.com/cache" + }, + "Package": { + "Id": "Example.UnapprovedTool", + "Architecture": "X64" + }, + "Options": { + "Scope": "User", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false, + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Client": { + "RequestedElevation": "Standard", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0", + "Transport": "HttpNamedPipe", + "ClientExecutablePath": "C:\\Program Files\\Devolutions\\NOW\\now-client.exe" + } +} diff --git a/crates/unigetui-broker/assets/samples/requests/winget-vscode-install.request.json b/crates/unigetui-broker/assets/samples/requests/winget-vscode-install.request.json new file mode 100644 index 000000000..479ea486a --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/winget-vscode-install.request.json @@ -0,0 +1,32 @@ +{ + "RequestKind": "PackageRequest", + "RequestVersion": "1.0", + "RequestId": "req-winget-vscode-install", + "CreatedAt": "2026-05-05T12:00:00Z", + "Operation": "Install", + "Manager": "Winget", + "Source": { + "Name": "winget", + "Url": "https://cdn.winget.microsoft.com/cache" + }, + "Package": { + "Id": "Microsoft.VisualStudioCode", + "Architecture": "X64" + }, + "Options": { + "Scope": "Machine", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false, + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Client": { + "RequestedElevation": "Elevated", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0", + "Transport": "HttpNamedPipe", + "ClientExecutablePath": "C:\\Program Files\\Devolutions\\NOW\\now-client.exe" + }, + "IncludeCommandPreview": true +} diff --git a/crates/unigetui-broker/assets/samples/requests/winget-vscode-skiphash.request.json b/crates/unigetui-broker/assets/samples/requests/winget-vscode-skiphash.request.json new file mode 100644 index 000000000..e180ad2cf --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/winget-vscode-skiphash.request.json @@ -0,0 +1,31 @@ +{ + "RequestKind": "PackageRequest", + "RequestVersion": "1.0", + "RequestId": "req-winget-vscode-skiphash", + "CreatedAt": "2026-05-05T12:10:00Z", + "Operation": "Install", + "Manager": "Winget", + "Source": { + "Name": "winget", + "Url": "https://cdn.winget.microsoft.com/cache" + }, + "Package": { + "Id": "Microsoft.VisualStudioCode", + "Architecture": "X64" + }, + "Options": { + "Scope": "Machine", + "Interactive": false, + "SkipHashCheck": true, + "PreRelease": false, + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Client": { + "RequestedElevation": "Elevated", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0", + "Transport": "HttpNamedPipe", + "ClientExecutablePath": "C:\\Program Files\\Devolutions\\NOW\\now-client.exe" + } +} diff --git a/crates/unigetui-broker/assets/samples/scenarios/baseline.scenarios.json b/crates/unigetui-broker/assets/samples/scenarios/baseline.scenarios.json new file mode 100644 index 000000000..c8ba84719 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/scenarios/baseline.scenarios.json @@ -0,0 +1,38 @@ +{ + "ScenarioSet": "baseline", + "Description": "Broad end-to-end scenarios for the gateway-owned package broker policy flow.", + "Scenarios": [ + { + "Id": "baseline.winget.vscode.allow", + "Policy": "corporate-allowlist.policy.json", + "Request": "requests/winget-vscode-install.request.json", + "ExpectedDecision": "Allow", + "ExpectedRuleId": "allow.winget.vscode", + "Tags": ["baseline", "winget", "allowlist"] + }, + { + "Id": "baseline.winget.unknown.default-deny", + "Policy": "corporate-allowlist.policy.json", + "Request": "requests/winget-unknown-install.request.json", + "ExpectedDecision": "Deny", + "ExpectedRuleId": "", + "Tags": ["baseline", "winget", "default-deny"] + }, + { + "Id": "baseline.winget.skiphash.deny", + "Policy": "corporate-allowlist.policy.json", + "Request": "requests/winget-vscode-skiphash.request.json", + "ExpectedDecision": "Deny", + "ExpectedRuleId": "deny.integrity-bypass", + "Tags": ["baseline", "winget", "risky-options"] + }, + { + "Id": "baseline.default-allow.normal-install", + "Policy": "deny-risky-options.policy.json", + "Request": "requests/winget-vscode-install.request.json", + "ExpectedDecision": "Allow", + "ExpectedRuleId": "", + "Tags": ["baseline", "winget", "default-allow"] + } + ] +} \ No newline at end of file diff --git a/crates/unigetui-broker/src/command_builder/mod.rs b/crates/unigetui-broker/src/command_builder/mod.rs new file mode 100644 index 000000000..f50bd0a00 --- /dev/null +++ b/crates/unigetui-broker/src/command_builder/mod.rs @@ -0,0 +1,38 @@ +//! Command-line builders for package manager operations. +//! +//! Constructs the commands the broker would execute from validated request fields. +//! The broker never executes client-supplied commands directly. + +pub mod powershell; +pub mod winget; + +use now_policy_api::{ManagerName, PackageRequest}; + +/// Build a command line from a validated request, dispatching to the appropriate +/// package manager builder. +/// +/// Returns the command as a list of arguments (first element is the executable). +pub fn build_command(request: &PackageRequest) -> Vec { + match request.manager { + ManagerName::Winget => winget::build_winget_command(request), + ManagerName::PowerShell => powershell::build_powershell5_command(request), + ManagerName::PowerShell7 => powershell::build_powershell7_command(request), + } +} + +/// Append `--flag value` to command if value is `Some` and non-empty. +pub(crate) fn set_if_specified(command: &mut Vec, flag: &str, value: Option<&str>) { + if let Some(v) = value + && !v.is_empty() + { + command.push(flag.to_owned()); + command.push(v.to_owned()); + } +} + +/// Append `--flag` to command if value is true. +pub(crate) fn set_if_true(command: &mut Vec, flag: &str, value: bool) { + if value { + command.push(flag.to_owned()); + } +} diff --git a/crates/unigetui-broker/src/command_builder/powershell.rs b/crates/unigetui-broker/src/command_builder/powershell.rs new file mode 100644 index 000000000..57a4c6fa8 --- /dev/null +++ b/crates/unigetui-broker/src/command_builder/powershell.rs @@ -0,0 +1,284 @@ +//! PowerShell command-line builders for WinPS 5.x and PowerShell 7.x. +//! +//! These mirror UniGetUI's own PowerShell manager helpers so the broker runs the +//! same command the unelevated client would have run: +//! - PowerShell 5 uses PowerShellGet (`Install-Module`/`Update-Module`/`Uninstall-Module`) +//! invoked as `powershell.exe -NoProfile -Command