diff --git a/.gitattributes b/.gitattributes index 3eb52bd81..20397d89d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -14,3 +14,8 @@ 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 and schema files produce huge LoC counts; exclude them from language statistics and +# treat them as generated files. +crates/unigetui-broker/assets/** linguist-generated +crates/unigetui-broker/schema/** linguist-generated diff --git a/.github/workflows/publish-libraries.yml b/.github/workflows/publish-libraries.yml index d27366691..691771be2 100644 --- a/.github/workflows/publish-libraries.yml +++ b/.github/workflows/publish-libraries.yml @@ -40,7 +40,7 @@ jobs: strategy: fail-fast: false matrix: - library: [ dotnet-client, dotnet-subscriber, utils, pedm-dotnet-client ] + library: [ dotnet-client, dotnet-subscriber, utils, pedm-dotnet-client, unigetui-broker-client ] include: - library: dotnet-client libpath: ./devolutions-gateway/openapi/dotnet-client @@ -50,6 +50,8 @@ jobs: libpath: ./utils/dotnet - library: pedm-dotnet-client libpath: ./crates/devolutions-pedm/openapi/dotnet-client + - library: unigetui-broker-client + libpath: ./crates/unigetui-broker/dotnet steps: - name: Check out ${{ github.repository }} diff --git a/Cargo.lock b/Cargo.lock index 427810a10..040bb778b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1578,6 +1578,7 @@ dependencies = [ "tokio 1.52.3", "tokio-rustls", "tracing", + "unigetui-broker", "url", "uuid", "win-api-wrappers", @@ -2244,6 +2245,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" @@ -2436,6 +2447,7 @@ dependencies = [ "devolutions-gateway", "devolutions-pedm", "serde_yaml", + "unigetui-broker", "utoipa", ] @@ -3199,6 +3211,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" @@ -4541,6 +4564,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" @@ -4549,12 +4590,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", ] @@ -4566,11 +4607,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" @@ -8023,6 +8073,41 @@ 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 = [ + "aide", + "anyhow", + "async-trait", + "axum 0.8.9", + "bytes 1.11.1", + "chrono", + "devolutions-gateway-task", + "http-body-util", + "hyper 1.9.0", + "hyper-util", + "notify 7.0.0", + "regex", + "schemars", + "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..79e382a4e --- /dev/null +++ b/crates/unigetui-broker/Cargo.toml @@ -0,0 +1,61 @@ +[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" +devolutions-gateway-task = { path = "../devolutions-gateway-task" } +axum = { version = "0.8", default-features = false, features = ["http1", "json", "tokio", "original-uri", "matched-path"] } +aide = { version = "0.14", features = ["axum", "axum-json"] } +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" +schemars = { version = "0.8", features = ["chrono"] } +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", +] } +tokio-util = { version = "0.7", features = ["compat"] } + +[[bin]] +name = "unigetui-broker-standalone" +path = "src/bin/standalone.rs" + +[[bin]] +name = "generate-broker-schema" +path = "src/bin/generate_schema.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..fe788a436 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/corporate-allowlist.policy.json @@ -0,0 +1,134 @@ +{ + "$schema": "https://aka.ms/unigetui/package-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/corporate-allowlist.policy.yaml b/crates/unigetui-broker/assets/samples/corporate-allowlist.policy.yaml new file mode 100644 index 000000000..20d9519aa --- /dev/null +++ b/crates/unigetui-broker/assets/samples/corporate-allowlist.policy.yaml @@ -0,0 +1,52 @@ +"$schema": https://aka.ms/unigetui/package-policy.schema.1.0.json +PolicyVersion: 1.0.0 +PolicyType: PackageBrokerPolicy +Metadata: + Id: contoso.desktop.standard-allowlist-yaml + Publisher: Contoso IT + Revision: 1 + PublishedAt: "2026-05-05T00:00:00Z" + Description: Fail-closed YAML 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: 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 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..ac0896fa8 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/deny-risky-options.policy.json @@ -0,0 +1,81 @@ +{ + "$schema": "https://aka.ms/unigetui/package-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/invalid/policies/invalid-failure-decision.policy.json b/crates/unigetui-broker/assets/samples/invalid/policies/invalid-failure-decision.policy.json new file mode 100644 index 000000000..9439e34b5 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/invalid/policies/invalid-failure-decision.policy.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://aka.ms/unigetui/package-policy.schema.1.0.json", + "PolicyVersion": "1.0.0", + "PolicyType": "PackageBrokerPolicy", + "Metadata": { + "Id": "contoso.invalid.failure-decision", + "Publisher": "Contoso IT", + "Revision": 1, + "PublishedAt": "2026-05-05T00:00:00Z" + }, + "Enforcement": { + "DefaultDecision": "Allow", + "failureDecision": "Allow", + "RulePrecedence": "PriorityThenDeny" + }, + "Rules": [ + { + "Id": "allow.everything", + "Priority": 100, + "Decision": "Allow", + "Match": { + "PackageIdentifiers": [ + "*" + ] + } + } + ] +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/invalid/requests/missing-package-id.request.json b/crates/unigetui-broker/assets/samples/invalid/requests/missing-package-id.request.json new file mode 100644 index 000000000..91efb7cb2 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/invalid/requests/missing-package-id.request.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-invalid-missing-package-id", + "CreatedAt": "2026-05-05T12:23:00Z", + "Operation": "Install", + "Manager": { + "Name": "Winget", + "DisplayName": "WinGet", + "ExecutableFriendlyName": "winget.exe" + }, + "Source": { + "Name": "winget", + "Url": "https://cdn.winget.microsoft.com/cache", + "IsVirtualManager": false + }, + "Package": { + "Name": "Microsoft Visual Studio Code", + "Architecture": "X64" + }, + "Options": { + "Scope": "Machine", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false, + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Broker": { + "RequestedElevation": "Elevated", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/powershell-advanced.policy.json b/crates/unigetui-broker/assets/samples/powershell-advanced.policy.json new file mode 100644 index 000000000..9aa4cd8a4 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/powershell-advanced.policy.json @@ -0,0 +1,82 @@ +{ + "$schema": "https://aka.ms/unigetui/package-policy.schema.1.0.json", + "PolicyVersion": "1.0.0", + "PolicyType": "PackageBrokerPolicy", + "Metadata": { + "Id": "contoso.powershell.advanced-scenarios", + "Publisher": "Contoso IT", + "Revision": 1, + "PublishedAt": "2026-05-05T00:00:00Z", + "Description": "PowerShell policy fixture for source, version range, and update operation coverage." + }, + "Enforcement": { + "DefaultDecision": "Deny", + "RulePrecedence": "PriorityThenDeny" + }, + "Rules": [ + { + "Id": "deny.powershell.untrusted-source", + "Priority": 5, + "Decision": "Deny", + "Reason": "Only PSGallery is approved for brokered PowerShell module operations.", + "Match": { + "Managers": [ + "PowerShell" + ], + "Sources": [ + "PoshTestGallery" + ] + } + }, + { + "Id": "deny.powershell.prerelease", + "Priority": 10, + "Decision": "Deny", + "Reason": "Prerelease PowerShell modules are not approved in advanced scenarios.", + "Match": { + "Managers": [ + "PowerShell" + ], + "PreRelease": [ + true + ] + } + }, + { + "Id": "allow.powershell.pester.versioned", + "Priority": 100, + "Decision": "Allow", + "Reason": "Pester is approved from PSGallery for CurrentUser install and update operations within the supported version range.", + "Match": { + "Operations": [ + "Install", + "Update" + ], + "Managers": [ + "PowerShell" + ], + "Sources": [ + "PSGallery" + ], + "PackageIdentifiers": [ + "Pester" + ], + "VersionRange": { + "MinVersion": "5.0.0", + "MaxVersion": "6.0.0", + "IncludePrerelease": false + }, + "Scopes": [ + "User" + ] + }, + "Constraints": { + "AllowSkipHashCheck": false, + "AllowPreRelease": false, + "AllowCustomParameters": false, + "AllowPrePostCommands": false, + "AllowKillBeforeOperation": false + } + } + ] +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/powershell-current-user.policy.json b/crates/unigetui-broker/assets/samples/powershell-current-user.policy.json new file mode 100644 index 000000000..36af55b7d --- /dev/null +++ b/crates/unigetui-broker/assets/samples/powershell-current-user.policy.json @@ -0,0 +1,91 @@ +{ + "$schema": "https://aka.ms/unigetui/package-policy.schema.1.0.json", + "PolicyVersion": "1.0.0", + "PolicyType": "PackageBrokerPolicy", + "Metadata": { + "Id": "contoso.powershell.current-user-modules", + "Publisher": "Contoso IT", + "Revision": 3, + "PublishedAt": "2026-05-05T00:00:00Z", + "Description": "PowerShell Gallery module policy for non-admin CurrentUser installs." + }, + "Enforcement": { + "DefaultDecision": "Deny", + "RulePrecedence": "PriorityThenDeny" + }, + "Rules": [ + { + "Id": "deny.powershell.machine-scope", + "Priority": 10, + "Decision": "Deny", + "Reason": "PowerShell module installs through the broker must use CurrentUser scope.", + "Match": { + "Managers": [ + "PowerShell" + ], + "Scopes": [ + "Machine" + ] + } + }, + { + "Id": "deny.powershell.elevated", + "Priority": 20, + "Decision": "Deny", + "Reason": "PowerShell module installs must not request an elevated broker context.", + "Match": { + "Managers": [ + "PowerShell" + ], + "Elevation": [ + "Elevated" + ] + } + }, + { + "Id": "deny.powershell.prerelease", + "Priority": 30, + "Decision": "Deny", + "Reason": "Prerelease PowerShell modules are not approved.", + "Match": { + "Managers": [ + "PowerShell" + ], + "PreRelease": [ + true + ] + } + }, + { + "Id": "allow.powershell.pester", + "Priority": 100, + "Decision": "Allow", + "Reason": "Pester is approved from PSGallery for CurrentUser installs.", + "Match": { + "Operations": [ + "Install", + "Update" + ], + "Managers": [ + "PowerShell" + ], + "Sources": [ + "PSGallery" + ], + "PackageIdentifiers": [ + "Pester" + ], + "Scopes": [ + "User" + ] + }, + "Constraints": { + "AllowSkipHashCheck": false, + "AllowPreRelease": false, + "AllowCustomParameters": false, + "AllowPrePostCommands": false, + "AllowKillBeforeOperation": false + } + } + ] +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/requests/powershell-pester-allusers.request.json b/crates/unigetui-broker/assets/samples/requests/powershell-pester-allusers.request.json new file mode 100644 index 000000000..f17907204 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/powershell-pester-allusers.request.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-powershell-pester-allusers", + "CreatedAt": "2026-05-05T12:25:00Z", + "Operation": "Install", + "Manager": { + "Name": "PowerShell", + "DisplayName": "PowerShell 5.x", + "ExecutableFriendlyName": "powershell.exe" + }, + "Source": { + "Name": "PSGallery", + "Url": "https://www.powershellgallery.com/api/v2", + "IsVirtualManager": false + }, + "Package": { + "Id": "Pester", + "Name": "Pester" + }, + "Options": { + "Scope": "Machine", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false, + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Broker": { + "RequestedElevation": "Elevated", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/requests/powershell-pester-currentuser.request.json b/crates/unigetui-broker/assets/samples/requests/powershell-pester-currentuser.request.json new file mode 100644 index 000000000..314bcc499 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/powershell-pester-currentuser.request.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-powershell-pester-currentuser", + "CreatedAt": "2026-05-05T12:20:00Z", + "Operation": "Install", + "Manager": { + "Name": "PowerShell", + "DisplayName": "PowerShell 5.x", + "ExecutableFriendlyName": "powershell.exe" + }, + "Source": { + "Name": "PSGallery", + "Url": "https://www.powershellgallery.com/api/v2", + "IsVirtualManager": false + }, + "Package": { + "Id": "Pester", + "Name": "Pester" + }, + "Options": { + "Scope": "User", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false, + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Broker": { + "RequestedElevation": "Standard", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/requests/powershell-pester-poshtestgallery.request.json b/crates/unigetui-broker/assets/samples/requests/powershell-pester-poshtestgallery.request.json new file mode 100644 index 000000000..d20a5d9c7 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/powershell-pester-poshtestgallery.request.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-powershell-pester-poshtestgallery", + "CreatedAt": "2026-05-05T12:21:00Z", + "Operation": "Install", + "Manager": { + "Name": "PowerShell", + "DisplayName": "PowerShell", + "ExecutableFriendlyName": "pwsh.exe" + }, + "Source": { + "Name": "PoshTestGallery", + "Url": "https://www.poshtestgallery.com/api/v2", + "IsVirtualManager": false + }, + "Package": { + "Id": "Pester", + "Name": "Pester" + }, + "Options": { + "Scope": "User", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false, + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Broker": { + "RequestedElevation": "Standard", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/requests/powershell-pester-prerelease.request.json b/crates/unigetui-broker/assets/samples/requests/powershell-pester-prerelease.request.json new file mode 100644 index 000000000..326bc744d --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/powershell-pester-prerelease.request.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-powershell-pester-prerelease", + "CreatedAt": "2026-05-05T12:21:00Z", + "Operation": "Install", + "Manager": { + "Name": "PowerShell", + "DisplayName": "PowerShell", + "ExecutableFriendlyName": "PowerShellGet" + }, + "Source": { + "Name": "PSGallery", + "Url": "https://www.powershellgallery.com/api/v2", + "IsVirtualManager": false + }, + "Package": { + "Id": "Pester", + "Name": "Pester" + }, + "Options": { + "Scope": "User", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": true, + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Broker": { + "RequestedElevation": "Standard", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/requests/powershell-pester-skipcheck.request.json b/crates/unigetui-broker/assets/samples/requests/powershell-pester-skipcheck.request.json new file mode 100644 index 000000000..f0124a5a0 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/powershell-pester-skipcheck.request.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-powershell-pester-skipcheck", + "CreatedAt": "2026-05-05T12:22:00Z", + "Operation": "Install", + "Manager": { + "Name": "PowerShell", + "DisplayName": "PowerShell", + "ExecutableFriendlyName": "PowerShellGet" + }, + "Source": { + "Name": "PSGallery", + "Url": "https://www.powershellgallery.com/api/v2", + "IsVirtualManager": false + }, + "Package": { + "Id": "Pester", + "Name": "Pester" + }, + "Options": { + "Scope": "User", + "Interactive": false, + "SkipHashCheck": true, + "PreRelease": false, + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Broker": { + "RequestedElevation": "Standard", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/requests/powershell-pester-update-currentuser.request.json b/crates/unigetui-broker/assets/samples/requests/powershell-pester-update-currentuser.request.json new file mode 100644 index 000000000..1b6e0ac07 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/powershell-pester-update-currentuser.request.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-powershell-pester-update-currentuser", + "CreatedAt": "2026-05-05T12:24:00Z", + "Operation": "Update", + "Manager": { + "Name": "PowerShell", + "DisplayName": "PowerShell", + "ExecutableFriendlyName": "pwsh.exe" + }, + "Source": { + "Name": "PSGallery", + "Url": "https://www.powershellgallery.com/api/v2", + "IsVirtualManager": false + }, + "Package": { + "Id": "Pester", + "Name": "Pester", + "Version": "5.5.0" + }, + "Options": { + "Scope": "User", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false, + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Broker": { + "RequestedElevation": "Standard", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/requests/powershell-pester-version-allowed.request.json b/crates/unigetui-broker/assets/samples/requests/powershell-pester-version-allowed.request.json new file mode 100644 index 000000000..1e03c2292 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/powershell-pester-version-allowed.request.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-powershell-pester-version-allowed", + "CreatedAt": "2026-05-05T12:22:00Z", + "Operation": "Install", + "Manager": { + "Name": "PowerShell", + "DisplayName": "PowerShell", + "ExecutableFriendlyName": "pwsh.exe" + }, + "Source": { + "Name": "PSGallery", + "Url": "https://www.powershellgallery.com/api/v2", + "IsVirtualManager": false + }, + "Package": { + "Id": "Pester", + "Name": "Pester", + "Version": "5.5.0" + }, + "Options": { + "Scope": "User", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false, + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Broker": { + "RequestedElevation": "Standard", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/requests/powershell-pester-version-out-of-range.request.json b/crates/unigetui-broker/assets/samples/requests/powershell-pester-version-out-of-range.request.json new file mode 100644 index 000000000..4fc1dc88d --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/powershell-pester-version-out-of-range.request.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-powershell-pester-version-out-of-range", + "CreatedAt": "2026-05-05T12:23:00Z", + "Operation": "Install", + "Manager": { + "Name": "PowerShell", + "DisplayName": "PowerShell", + "ExecutableFriendlyName": "pwsh.exe" + }, + "Source": { + "Name": "PSGallery", + "Url": "https://www.powershellgallery.com/api/v2", + "IsVirtualManager": false + }, + "Package": { + "Id": "Pester", + "Name": "Pester", + "Version": "6.1.0" + }, + "Options": { + "Scope": "User", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false, + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Broker": { + "RequestedElevation": "Standard", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/requests/status-query-minimal.request.json b/crates/unigetui-broker/assets/samples/requests/status-query-minimal.request.json new file mode 100644 index 000000000..0e3756e9c --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/status-query-minimal.request.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://aka.ms/unigetui/package-operation-status-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperationStatus", + "RequestId": "req-abc-123", + "Broker": { + "RequestedElevation": "Standard", + "EffectiveUser": "WORKSTATION\\bob" + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/requests/status-query-running.request.json b/crates/unigetui-broker/assets/samples/requests/status-query-running.request.json new file mode 100644 index 000000000..3583bb3a2 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/status-query-running.request.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://aka.ms/unigetui/package-operation-status-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperationStatus", + "RequestId": "req-winget-vscode-install", + "Broker": { + "RequestedElevation": "Elevated", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/requests/winget-git-custom-location-allowed.request.json b/crates/unigetui-broker/assets/samples/requests/winget-git-custom-location-allowed.request.json new file mode 100644 index 000000000..74bf01c32 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/winget-git-custom-location-allowed.request.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-winget-git-custom-location-allowed", + "CreatedAt": "2026-05-05T12:15:00Z", + "Operation": "Install", + "Manager": { + "Name": "Winget", + "DisplayName": "WinGet", + "ExecutableFriendlyName": "winget.exe" + }, + "Source": { + "Name": "winget", + "Url": "https://cdn.winget.microsoft.com/cache", + "IsVirtualManager": false + }, + "Package": { + "Id": "Git.Git", + "Name": "Git", + "Architecture": "X64" + }, + "Options": { + "Scope": "Machine", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false, + "CustomInstallLocation": "C:\\Tools\\Git", + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Broker": { + "RequestedElevation": "Elevated", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/requests/winget-git-custom-location-denied.request.json b/crates/unigetui-broker/assets/samples/requests/winget-git-custom-location-denied.request.json new file mode 100644 index 000000000..0eb1b501e --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/winget-git-custom-location-denied.request.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-winget-git-custom-location-denied", + "CreatedAt": "2026-05-05T12:16:00Z", + "Operation": "Install", + "Manager": { + "Name": "Winget", + "DisplayName": "WinGet", + "ExecutableFriendlyName": "winget.exe" + }, + "Source": { + "Name": "winget", + "Url": "https://cdn.winget.microsoft.com/cache", + "IsVirtualManager": false + }, + "Package": { + "Id": "Git.Git", + "Name": "Git", + "Architecture": "X64" + }, + "Options": { + "Scope": "Machine", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false, + "CustomInstallLocation": "C:\\Temp\\Git", + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Broker": { + "RequestedElevation": "Elevated", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/requests/winget-git-custom-param-allowed.request.json b/crates/unigetui-broker/assets/samples/requests/winget-git-custom-param-allowed.request.json new file mode 100644 index 000000000..90b9ab3a6 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/winget-git-custom-param-allowed.request.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-winget-git-custom-param-allowed", + "CreatedAt": "2026-05-05T12:17:00Z", + "Operation": "Install", + "Manager": { + "Name": "Winget", + "DisplayName": "WinGet", + "ExecutableFriendlyName": "winget.exe" + }, + "Source": { + "Name": "winget", + "Url": "https://cdn.winget.microsoft.com/cache", + "IsVirtualManager": false + }, + "Package": { + "Id": "Git.Git", + "Name": "Git", + "Architecture": "X64" + }, + "Options": { + "Scope": "Machine", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false, + "CustomParameters": [ + "--accept-source-agreements" + ], + "KillBeforeOperation": [] + }, + "Broker": { + "RequestedElevation": "Elevated", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/requests/winget-git-custom-param-denied.request.json b/crates/unigetui-broker/assets/samples/requests/winget-git-custom-param-denied.request.json new file mode 100644 index 000000000..1980e3e5e --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/winget-git-custom-param-denied.request.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-winget-git-custom-param-denied", + "CreatedAt": "2026-05-05T12:18:00Z", + "Operation": "Install", + "Manager": { + "Name": "Winget", + "DisplayName": "WinGet", + "ExecutableFriendlyName": "winget.exe" + }, + "Source": { + "Name": "winget", + "Url": "https://cdn.winget.microsoft.com/cache", + "IsVirtualManager": false + }, + "Package": { + "Id": "Git.Git", + "Name": "Git", + "Architecture": "X64" + }, + "Options": { + "Scope": "Machine", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false, + "CustomParameters": [ + "--override" + ], + "KillBeforeOperation": [] + }, + "Broker": { + "RequestedElevation": "Elevated", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/requests/winget-git-uninstall.request.json b/crates/unigetui-broker/assets/samples/requests/winget-git-uninstall.request.json new file mode 100644 index 000000000..32ea2a3e7 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/winget-git-uninstall.request.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-winget-git-uninstall", + "CreatedAt": "2026-05-05T12:13:00Z", + "Operation": "Uninstall", + "Manager": { + "Name": "Winget", + "DisplayName": "WinGet", + "ExecutableFriendlyName": "winget.exe" + }, + "Source": { + "Name": "winget", + "Url": "https://cdn.winget.microsoft.com/cache", + "IsVirtualManager": false + }, + "Package": { + "Id": "Git.Git", + "Name": "Git", + "Architecture": "X64" + }, + "Options": { + "Scope": "Machine", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false, + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Broker": { + "RequestedElevation": "Elevated", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/requests/winget-powertoys-install.request.json b/crates/unigetui-broker/assets/samples/requests/winget-powertoys-install.request.json new file mode 100644 index 000000000..2dc0fa11e --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/winget-powertoys-install.request.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-winget-powertoys-install", + "CreatedAt": "2026-05-05T12:10:00Z", + "Operation": "Install", + "Manager": { + "Name": "Winget", + "DisplayName": "WinGet", + "ExecutableFriendlyName": "winget.exe" + }, + "Source": { + "Name": "winget", + "Url": "https://cdn.winget.microsoft.com/cache", + "IsVirtualManager": false + }, + "Package": { + "Id": "Microsoft.PowerToys", + "Name": "Microsoft PowerToys", + "Architecture": "X64" + }, + "Options": { + "Scope": "Machine", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false, + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Broker": { + "RequestedElevation": "Elevated", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ 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..4e04d7844 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/winget-unknown-install.request.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-winget-unknown-install", + "CreatedAt": "2026-05-05T12:05:00Z", + "Operation": "Install", + "Manager": { + "Name": "Winget", + "DisplayName": "WinGet", + "ExecutableFriendlyName": "winget.exe" + }, + "Source": { + "Name": "winget", + "Url": "https://cdn.winget.microsoft.com/cache", + "IsVirtualManager": false + }, + "Package": { + "Id": "Example.UnapprovedTool", + "Name": "Unapproved Tool", + "Architecture": "X64" + }, + "Options": { + "Scope": "User", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false, + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Broker": { + "RequestedElevation": "Standard", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/requests/winget-vscode-custom-param.request.json b/crates/unigetui-broker/assets/samples/requests/winget-vscode-custom-param.request.json new file mode 100644 index 000000000..40451f472 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/winget-vscode-custom-param.request.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-winget-vscode-custom-param", + "CreatedAt": "2026-05-05T12:15:00Z", + "Operation": "Install", + "Manager": { + "Name": "Winget", + "DisplayName": "WinGet", + "ExecutableFriendlyName": "winget.exe" + }, + "Source": { + "Name": "winget", + "Url": "https://cdn.winget.microsoft.com/cache", + "IsVirtualManager": false + }, + "Package": { + "Id": "Microsoft.VisualStudioCode", + "Name": "Microsoft Visual Studio Code", + "Architecture": "X64" + }, + "Options": { + "Scope": "Machine", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false, + "CustomParameters": [ + "--override", + "/VERYSILENT /NORESTART" + ], + "KillBeforeOperation": [] + }, + "Broker": { + "RequestedElevation": "Elevated", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file 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..fb1a6191c --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/winget-vscode-install.request.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-winget-vscode-install", + "CreatedAt": "2026-05-05T12:00:00Z", + "Operation": "Install", + "Manager": { + "Name": "Winget", + "DisplayName": "WinGet", + "ExecutableFriendlyName": "winget.exe" + }, + "Source": { + "Name": "winget", + "Url": "https://cdn.winget.microsoft.com/cache", + "IsVirtualManager": false + }, + "Package": { + "Id": "Microsoft.VisualStudioCode", + "Name": "Microsoft Visual Studio Code", + "Architecture": "X64" + }, + "Options": { + "Scope": "Machine", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false, + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Broker": { + "RequestedElevation": "Elevated", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/requests/winget-vscode-install.request.yaml b/crates/unigetui-broker/assets/samples/requests/winget-vscode-install.request.yaml new file mode 100644 index 000000000..f9cd71f27 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/winget-vscode-install.request.yaml @@ -0,0 +1,29 @@ +"$schema": https://aka.ms/unigetui/package-request.schema.1.0.json +requestVersion: 1.0.0 +requestType: packageOperation +requestId: req-winget-vscode-install-yaml +createdAt: "2026-05-05T12:00:00Z" +operation: install +manager: + name: Winget + displayName: WinGet + executableFriendlyName: winget.exe +source: + name: winget + url: https://cdn.winget.microsoft.com/cache + isVirtualManager: false +package: + id: Microsoft.VisualStudioCode + name: Microsoft Visual Studio Code +options: + scope: machine + architecture: x64 + interactive: false + skipHashCheck: false + preRelease: false + customParameters: [] + killBeforeOperation: [] +broker: + requestedElevation: elevated + effectiveUser: 'CONTOSO\alice' + clientVersion: 3.2.0 diff --git a/crates/unigetui-broker/assets/samples/requests/winget-vscode-interactive.request.json b/crates/unigetui-broker/assets/samples/requests/winget-vscode-interactive.request.json new file mode 100644 index 000000000..10c4fa395 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/winget-vscode-interactive.request.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-winget-vscode-interactive", + "CreatedAt": "2026-05-05T12:14:00Z", + "Operation": "Install", + "Manager": { + "Name": "Winget", + "DisplayName": "WinGet", + "ExecutableFriendlyName": "winget.exe" + }, + "Source": { + "Name": "winget", + "Url": "https://cdn.winget.microsoft.com/cache", + "IsVirtualManager": false + }, + "Package": { + "Id": "Microsoft.VisualStudioCode", + "Name": "Microsoft Visual Studio Code", + "Architecture": "X64" + }, + "Options": { + "Scope": "Machine", + "Interactive": true, + "SkipHashCheck": false, + "PreRelease": false, + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Broker": { + "RequestedElevation": "Elevated", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/requests/winget-vscode-kill-before.request.json b/crates/unigetui-broker/assets/samples/requests/winget-vscode-kill-before.request.json new file mode 100644 index 000000000..7f489858c --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/winget-vscode-kill-before.request.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-winget-vscode-kill-before", + "CreatedAt": "2026-05-05T12:20:00Z", + "Operation": "Install", + "Manager": { + "Name": "Winget", + "DisplayName": "WinGet", + "ExecutableFriendlyName": "winget.exe" + }, + "Source": { + "Name": "winget", + "Url": "https://cdn.winget.microsoft.com/cache", + "IsVirtualManager": false + }, + "Package": { + "Id": "Microsoft.VisualStudioCode", + "Name": "Microsoft Visual Studio Code", + "Architecture": "X64" + }, + "Options": { + "Scope": "Machine", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false, + "CustomParameters": [], + "KillBeforeOperation": [ + "Code" + ] + }, + "Broker": { + "RequestedElevation": "Elevated", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/requests/winget-vscode-msstore.request.json b/crates/unigetui-broker/assets/samples/requests/winget-vscode-msstore.request.json new file mode 100644 index 000000000..dfcd5043f --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/winget-vscode-msstore.request.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-winget-vscode-msstore", + "CreatedAt": "2026-05-05T12:18:00Z", + "Operation": "Install", + "Manager": { + "Name": "Winget", + "DisplayName": "WinGet", + "ExecutableFriendlyName": "winget.exe" + }, + "Source": { + "Name": "msstore", + "Url": "https://storeedgefd.dsx.mp.microsoft.com/v9.0", + "IsVirtualManager": false + }, + "Package": { + "Id": "Microsoft.VisualStudioCode", + "Name": "Microsoft Visual Studio Code", + "Architecture": "X64" + }, + "Options": { + "Scope": "User", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false, + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Broker": { + "RequestedElevation": "Standard", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/requests/winget-vscode-prepost.request.json b/crates/unigetui-broker/assets/samples/requests/winget-vscode-prepost.request.json new file mode 100644 index 000000000..513bb43c6 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/winget-vscode-prepost.request.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-winget-vscode-prepost", + "CreatedAt": "2026-05-05T12:19:00Z", + "Operation": "Install", + "Manager": { + "Name": "Winget", + "DisplayName": "WinGet", + "ExecutableFriendlyName": "winget.exe" + }, + "Source": { + "Name": "winget", + "Url": "https://cdn.winget.microsoft.com/cache", + "IsVirtualManager": false + }, + "Package": { + "Id": "Microsoft.VisualStudioCode", + "Name": "Microsoft Visual Studio Code", + "Architecture": "X64" + }, + "Options": { + "Scope": "Machine", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false, + "PreOperationCommand": "Write-Host preparing", + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Broker": { + "RequestedElevation": "Elevated", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/requests/winget-vscode-prerelease.request.json b/crates/unigetui-broker/assets/samples/requests/winget-vscode-prerelease.request.json new file mode 100644 index 000000000..6cf35de94 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/winget-vscode-prerelease.request.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-winget-vscode-prerelease", + "CreatedAt": "2026-05-05T12:13:00Z", + "Operation": "Install", + "Manager": { + "Name": "Winget", + "DisplayName": "WinGet", + "ExecutableFriendlyName": "winget.exe" + }, + "Source": { + "Name": "winget", + "Url": "https://cdn.winget.microsoft.com/cache", + "IsVirtualManager": false + }, + "Package": { + "Id": "Microsoft.VisualStudioCode", + "Name": "Microsoft Visual Studio Code", + "Architecture": "X64", + "Version": "1.96.0-beta.1" + }, + "Options": { + "Scope": "Machine", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": true, + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Broker": { + "RequestedElevation": "Elevated", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file 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..6a66e1e71 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/winget-vscode-skiphash.request.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-winget-vscode-skiphash", + "CreatedAt": "2026-05-05T12:10:00Z", + "Operation": "Install", + "Manager": { + "Name": "Winget", + "DisplayName": "WinGet", + "ExecutableFriendlyName": "winget.exe" + }, + "Source": { + "Name": "winget", + "Url": "https://cdn.winget.microsoft.com/cache", + "IsVirtualManager": false + }, + "Package": { + "Id": "Microsoft.VisualStudioCode", + "Name": "Microsoft Visual Studio Code", + "Architecture": "X64" + }, + "Options": { + "Scope": "Machine", + "Interactive": false, + "SkipHashCheck": true, + "PreRelease": false, + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Broker": { + "RequestedElevation": "Elevated", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/requests/winget-vscode-update-in-range.request.json b/crates/unigetui-broker/assets/samples/requests/winget-vscode-update-in-range.request.json new file mode 100644 index 000000000..bc59f50ab --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/winget-vscode-update-in-range.request.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-winget-vscode-update-in-range", + "CreatedAt": "2026-05-05T12:12:00Z", + "Operation": "Update", + "Manager": { + "Name": "Winget", + "DisplayName": "WinGet", + "ExecutableFriendlyName": "winget.exe" + }, + "Source": { + "Name": "winget", + "Url": "https://cdn.winget.microsoft.com/cache", + "IsVirtualManager": false + }, + "Package": { + "Id": "Microsoft.VisualStudioCode", + "Name": "Microsoft Visual Studio Code", + "Architecture": "X64", + "Version": "1.96.0" + }, + "Options": { + "Scope": "Machine", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false, + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Broker": { + "RequestedElevation": "Elevated", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/requests/winget-vscode-version-in-range.request.json b/crates/unigetui-broker/assets/samples/requests/winget-vscode-version-in-range.request.json new file mode 100644 index 000000000..8670cc04a --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/winget-vscode-version-in-range.request.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-winget-vscode-version-in-range", + "CreatedAt": "2026-05-05T12:11:00Z", + "Operation": "Install", + "Manager": { + "Name": "Winget", + "DisplayName": "WinGet", + "ExecutableFriendlyName": "winget.exe" + }, + "Source": { + "Name": "winget", + "Url": "https://cdn.winget.microsoft.com/cache", + "IsVirtualManager": false + }, + "Package": { + "Id": "Microsoft.VisualStudioCode", + "Name": "Microsoft Visual Studio Code", + "Architecture": "X64", + "Version": "1.95.0" + }, + "Options": { + "Scope": "Machine", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false, + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Broker": { + "RequestedElevation": "Elevated", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/requests/winget-vscode-version-out-of-range.request.json b/crates/unigetui-broker/assets/samples/requests/winget-vscode-version-out-of-range.request.json new file mode 100644 index 000000000..fb2a8ae63 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/winget-vscode-version-out-of-range.request.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-winget-vscode-version-out-of-range", + "CreatedAt": "2026-05-05T12:12:00Z", + "Operation": "Install", + "Manager": { + "Name": "Winget", + "DisplayName": "WinGet", + "ExecutableFriendlyName": "winget.exe" + }, + "Source": { + "Name": "winget", + "Url": "https://cdn.winget.microsoft.com/cache", + "IsVirtualManager": false + }, + "Package": { + "Id": "Microsoft.VisualStudioCode", + "Name": "Microsoft Visual Studio Code", + "Architecture": "X64", + "Version": "3.0.0" + }, + "Options": { + "Scope": "Machine", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false, + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Broker": { + "RequestedElevation": "Elevated", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/requests/winget-vscode-winget-fonts.request.json b/crates/unigetui-broker/assets/samples/requests/winget-vscode-winget-fonts.request.json new file mode 100644 index 000000000..3ed51a510 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/winget-vscode-winget-fonts.request.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-winget-vscode-winget-fonts", + "CreatedAt": "2026-05-05T12:14:00Z", + "Operation": "Install", + "Manager": { + "Name": "Winget", + "DisplayName": "WinGet", + "ExecutableFriendlyName": "winget.exe" + }, + "Source": { + "Name": "winget-fonts", + "Url": "https://cdn.winget.microsoft.com/cache", + "IsVirtualManager": false + }, + "Package": { + "Id": "Microsoft.VisualStudioCode", + "Name": "Microsoft Visual Studio Code", + "Architecture": "X64", + "Version": "1.95.0" + }, + "Options": { + "Scope": "Machine", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false, + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Broker": { + "RequestedElevation": "Elevated", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/requests/winget-vscode-x86.request.json b/crates/unigetui-broker/assets/samples/requests/winget-vscode-x86.request.json new file mode 100644 index 000000000..975bbc68b --- /dev/null +++ b/crates/unigetui-broker/assets/samples/requests/winget-vscode-x86.request.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-winget-vscode-x86", + "CreatedAt": "2026-05-05T12:15:00Z", + "Operation": "Install", + "Manager": { + "Name": "Winget", + "DisplayName": "WinGet", + "ExecutableFriendlyName": "winget.exe" + }, + "Source": { + "Name": "winget", + "Url": "https://cdn.winget.microsoft.com/cache", + "IsVirtualManager": false + }, + "Package": { + "Id": "Microsoft.VisualStudioCode", + "Name": "Microsoft Visual Studio Code", + "Architecture": "X86", + "Version": "1.95.0" + }, + "Options": { + "Scope": "Machine", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false, + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Broker": { + "RequestedElevation": "Elevated", + "EffectiveUser": "CONTOSO\\alice", + "ClientVersion": "3.2.0" + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/responses/status-completed.response.json b/crates/unigetui-broker/assets/samples/responses/status-completed.response.json new file mode 100644 index 000000000..9d18550db --- /dev/null +++ b/crates/unigetui-broker/assets/samples/responses/status-completed.response.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://aka.ms/unigetui/package-operation-status-response.schema.1.0.json", + "ResponseVersion": "1.0.0", + "ResponseType": "PackageOperationStatusResponse", + "Broker": { + "Name": "Devolutions Agent UniGetUI Broker", + "ProtocolVersion": "1.0", + "Transport": "HttpNamedPipe", + "PipeName": "\\\\.\\pipe\\UniGetUI.PackageBroker.v1", + "ElevatedSimulation": false + }, + "RequestId": "req-winget-vscode-install", + "Status": "Completed", + "StartedAt": "2026-05-05T12:00:02Z", + "CompletedAt": "2026-05-05T12:00:15Z", + "ExitCode": 0, + "Note": "Process exited successfully." +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/responses/status-failed.response.json b/crates/unigetui-broker/assets/samples/responses/status-failed.response.json new file mode 100644 index 000000000..877191c72 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/responses/status-failed.response.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://aka.ms/unigetui/package-operation-status-response.schema.1.0.json", + "ResponseVersion": "1.0.0", + "ResponseType": "PackageOperationStatusResponse", + "Broker": { + "Name": "Devolutions Agent UniGetUI Broker", + "ProtocolVersion": "1.0", + "Transport": "HttpNamedPipe", + "PipeName": "\\\\.\\pipe\\UniGetUI.PackageBroker.v1", + "ElevatedSimulation": false + }, + "RequestId": "req-winget-git-uninstall", + "Status": "Failed", + "StartedAt": "2026-05-05T12:01:00Z", + "CompletedAt": "2026-05-05T12:01:05Z", + "ExitCode": 1, + "Note": "Process exited with code 1." +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/responses/status-running.response.json b/crates/unigetui-broker/assets/samples/responses/status-running.response.json new file mode 100644 index 000000000..6e3046aeb --- /dev/null +++ b/crates/unigetui-broker/assets/samples/responses/status-running.response.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://aka.ms/unigetui/package-operation-status-response.schema.1.0.json", + "ResponseVersion": "1.0.0", + "ResponseType": "PackageOperationStatusResponse", + "Broker": { + "Name": "Devolutions Agent UniGetUI Broker", + "ProtocolVersion": "1.0", + "Transport": "HttpNamedPipe", + "PipeName": "\\\\.\\pipe\\UniGetUI.PackageBroker.v1", + "ElevatedSimulation": false + }, + "RequestId": "req-winget-vscode-install", + "Status": "Running", + "StartedAt": "2026-05-05T12:00:02Z" +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/responses/status-starting.response.json b/crates/unigetui-broker/assets/samples/responses/status-starting.response.json new file mode 100644 index 000000000..e0e9449f7 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/responses/status-starting.response.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://aka.ms/unigetui/package-operation-status-response.schema.1.0.json", + "ResponseVersion": "1.0.0", + "ResponseType": "PackageOperationStatusResponse", + "Broker": { + "Name": "Devolutions Agent UniGetUI Broker", + "ProtocolVersion": "1.0", + "Transport": "HttpNamedPipe", + "PipeName": "\\\\.\\pipe\\UniGetUI.PackageBroker.v1", + "ElevatedSimulation": false + }, + "RequestId": "req-abc-123", + "Status": "Starting" +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/responses/status-timeout.response.json b/crates/unigetui-broker/assets/samples/responses/status-timeout.response.json new file mode 100644 index 000000000..89fff0da9 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/responses/status-timeout.response.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://aka.ms/unigetui/package-operation-status-response.schema.1.0.json", + "ResponseVersion": "1.0.0", + "ResponseType": "PackageOperationStatusResponse", + "Broker": { + "Name": "Devolutions Agent UniGetUI Broker", + "ProtocolVersion": "1.0", + "Transport": "HttpNamedPipe", + "PipeName": "\\\\.\\pipe\\UniGetUI.PackageBroker.v1", + "ElevatedSimulation": false + }, + "RequestId": "req-long-running-op", + "Status": "Failed", + "StartedAt": "2026-05-05T11:00:00Z", + "CompletedAt": "2026-05-05T12:00:00Z", + "Note": "Operation timed out after 1 hour." +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/responses/winget-vscode-install.allowed.response.json b/crates/unigetui-broker/assets/samples/responses/winget-vscode-install.allowed.response.json new file mode 100644 index 000000000..6cc458d13 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/responses/winget-vscode-install.allowed.response.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://aka.ms/unigetui/package-broker-response.schema.1.0.json", + "ResponseVersion": "1.0.0", + "ResponseType": "PackageBrokerResponse", + "Broker": { + "Name": "UniGetUI Package Broker", + "ProtocolVersion": "1.0", + "Transport": "HttpNamedPipe", + "PipeName": "\\\\.\\pipe\\UniGetUI.PackageBroker.v1", + "ElevatedSimulation": false + }, + "AuditId": "audit-20260505-000001", + "RequestId": "req-winget-vscode-install", + "ReceivedAt": "2026-05-05T12:00:01Z", + "CompletedAt": "2026-05-05T12:00:01Z", + "Manager": "Winget", + "Source": "winget", + "PackageId": "Microsoft.VisualStudioCode", + "Operation": "Install", + "Decision": "Allow", + "RuleId": "allow.winget.vscode", + "Reason": "Visual Studio Code is approved for managed workstations.", + "WouldExecute": true, + "Policy": { + "Id": "contoso.desktop.standard-allowlist", + "Revision": 4, + "PolicyVersion": "1.0.0" + }, + "Execution": { + "Mode": "Elevated", + "Command": [ + "winget.exe", + "Install", + "--id", + "Microsoft.VisualStudioCode", + "--exact", + "--source", + "winget", + "--scope", + "Machine", + "--silent", + "--architecture", + "X64" + ], + "Note": "Command was constructed by the broker from validated request fields." + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/responses/winget-vscode-skiphash.denied.response.json b/crates/unigetui-broker/assets/samples/responses/winget-vscode-skiphash.denied.response.json new file mode 100644 index 000000000..d08733adb --- /dev/null +++ b/crates/unigetui-broker/assets/samples/responses/winget-vscode-skiphash.denied.response.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://aka.ms/unigetui/package-broker-response.schema.1.0.json", + "ResponseVersion": "1.0.0", + "ResponseType": "PackageBrokerResponse", + "Broker": { + "Name": "UniGetUI Package Broker", + "ProtocolVersion": "1.0", + "Transport": "HttpNamedPipe", + "PipeName": "\\\\.\\pipe\\UniGetUI.PackageBroker.v1", + "ElevatedSimulation": false + }, + "AuditId": "audit-20260505-000002", + "RequestId": "req-winget-vscode-skiphash", + "ReceivedAt": "2026-05-05T12:10:01Z", + "CompletedAt": "2026-05-05T12:10:01Z", + "Manager": "Winget", + "Source": "winget", + "PackageId": "Microsoft.VisualStudioCode", + "Operation": "Install", + "Decision": "Deny", + "RuleId": "deny.integrity-bypass", + "Reason": "Integrity and publisher checks cannot be bypassed by brokered requests.", + "WouldExecute": false, + "Policy": { + "Id": "contoso.desktop.standard-allowlist", + "Revision": 4, + "PolicyVersion": "1.0.0" + }, + "Execution": { + "Mode": "Elevated", + "Command": [], + "Note": "Policy denied the request; the broker must not execute a package manager command." + } +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/scenario-coverage.policy.json b/crates/unigetui-broker/assets/samples/scenario-coverage.policy.json new file mode 100644 index 000000000..0447ec9cc --- /dev/null +++ b/crates/unigetui-broker/assets/samples/scenario-coverage.policy.json @@ -0,0 +1,257 @@ +{ + "$schema": "https://aka.ms/unigetui/package-policy.schema.1.0.json", + "PolicyVersion": "1.0.0", + "PolicyType": "PackageBrokerPolicy", + "Metadata": { + "Id": "contoso.desktop.scenario-coverage", + "Publisher": "Contoso IT", + "Revision": 1, + "PublishedAt": "2026-05-05T00:00:00Z", + "Description": "Focused policy used to exercise simulator precedence, version, and constraint behavior." + }, + "Enforcement": { + "DefaultDecision": "Deny", + "RulePrecedence": "PriorityThenDeny" + }, + "Rules": [ + { + "Id": "deny.disabled-powertoys", + "Enabled": false, + "Priority": 1, + "Decision": "Deny", + "Reason": "Disabled rules must not participate in policy decisions.", + "Match": { + "Managers": [ + "Winget" + ], + "PackageIdentifiers": [ + "Microsoft.PowerToys" + ] + } + }, + { + "Id": "deny.interactive", + "Priority": 5, + "Decision": "Deny", + "Reason": "Interactive brokered installs are not allowed in the scenario coverage policy.", + "Match": { + "Interactive": [ + true + ] + } + }, + { + "Id": "deny.tie.custom-parameters", + "Priority": 10, + "Decision": "Deny", + "Reason": "Deny must win when allow and deny rules match at the same priority.", + "Match": { + "Managers": [ + "Winget" + ], + "PackageIdentifiers": [ + "Microsoft.VisualStudioCode" + ], + "HasCustomParameters": [ + true + ] + } + }, + { + "Id": "allow.tie.vscode-custom-parameters", + "Priority": 10, + "Decision": "Allow", + "Reason": "This intentionally ties the deny rule to prove deny wins ties.", + "Match": { + "Managers": [ + "Winget" + ], + "PackageIdentifiers": [ + "Microsoft.VisualStudioCode" + ], + "HasCustomParameters": [ + true + ] + }, + "Constraints": { + "AllowCustomParameters": true + } + }, + { + "Id": "deny.prepost-commands", + "Priority": 20, + "Decision": "Deny", + "Reason": "Pre and post commands are outside the package manager trust boundary.", + "Match": { + "HasPrePostCommands": [ + true + ] + } + }, + { + "Id": "deny.kill-process-actions", + "Priority": 30, + "Decision": "Deny", + "Reason": "Killing processes before a brokered operation is not allowed.", + "Match": { + "HasKillBeforeOperation": [ + true + ] + } + }, + { + "Id": "allow.winget.powertoys", + "Priority": 100, + "Decision": "Allow", + "Reason": "PowerToys is allowed and proves disabled deny rules are ignored.", + "Match": { + "Operations": [ + "Install", + "Update" + ], + "Managers": [ + "Winget" + ], + "Sources": [ + "winget" + ], + "PackageIdentifiers": [ + "Microsoft.PowerToys" + ], + "Scopes": [ + "Machine" + ], + "Architectures": [ + "X64" + ] + }, + "Constraints": { + "AllowInteractive": false, + "AllowSkipHashCheck": false, + "AllowPreRelease": false, + "AllowCustomParameters": false, + "AllowPrePostCommands": false, + "AllowKillBeforeOperation": false + } + }, + { + "Id": "allow.winget.vscode.version-range", + "Priority": 100, + "Decision": "Allow", + "Reason": "VS Code is allowed only within the tested version range.", + "Match": { + "Operations": [ + "Install", + "Update" + ], + "Managers": [ + "Winget" + ], + "Sources": [ + "winget" + ], + "PackageIdentifiers": [ + "Microsoft.VisualStudioCode" + ], + "VersionRange": { + "MinVersion": "1.90.0", + "MaxVersion": "2.0.0", + "IncludePrerelease": false + }, + "Scopes": [ + "Machine" + ], + "Architectures": [ + "X64" + ] + }, + "Constraints": { + "AllowInteractive": false, + "AllowSkipHashCheck": false, + "AllowPreRelease": false, + "AllowCustomParameters": false, + "AllowPrePostCommands": false, + "AllowKillBeforeOperation": false + } + }, + { + "Id": "allow.winget.git.customized", + "Priority": 100, + "Decision": "Allow", + "Reason": "Git is allowed with tightly constrained customization.", + "Match": { + "Operations": [ + "Install", + "Update" + ], + "Managers": [ + "Winget" + ], + "Sources": [ + "winget" + ], + "PackageIdentifiers": [ + "Git.Git" + ], + "Scopes": [ + "Machine" + ], + "Architectures": [ + "X64" + ] + }, + "Constraints": { + "AllowInteractive": false, + "AllowSkipHashCheck": false, + "AllowPreRelease": false, + "AllowCustomInstallLocation": true, + "AllowedInstallLocationPatterns": [ + "C:\\Tools\\Git*" + ], + "AllowCustomParameters": true, + "AllowedCustomParameters": [ + "--accept-source-agreements" + ], + "DeniedCustomParameters": [ + "--override*" + ], + "AllowPrePostCommands": false, + "AllowKillBeforeOperation": false + } + }, + { + "Id": "allow.winget.git.uninstall", + "Priority": 100, + "Decision": "Allow", + "Reason": "Git uninstall is allowed for the same corporate package source and machine scope.", + "Match": { + "Operations": [ + "Uninstall" + ], + "Managers": [ + "Winget" + ], + "Sources": [ + "winget" + ], + "PackageIdentifiers": [ + "Git.Git" + ], + "Scopes": [ + "Machine" + ], + "Architectures": [ + "X64" + ] + }, + "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/scenarios/baseline.scenarios.json b/crates/unigetui-broker/assets/samples/scenarios/baseline.scenarios.json new file mode 100644 index 000000000..568e6aed5 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/scenarios/baseline.scenarios.json @@ -0,0 +1,102 @@ +{ + "ScenarioSet": "baseline", + "Description": "Existing baseline scenarios for package broker policy simulation.", + "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"] + }, + { + "Id": "baseline.default-allow.skiphash.deny", + "Policy": "deny-risky-options.policy.json", + "Request": "requests/winget-vscode-skiphash.request.json", + "ExpectedDecision": "Deny", + "ExpectedRuleId": "deny.integrity-bypass", + "Tags": ["baseline", "winget", "risky-options"] + }, + { + "Id": "baseline.default-allow.custom-parameters.deny", + "Policy": "deny-risky-options.policy.json", + "Request": "requests/winget-vscode-custom-param.request.json", + "ExpectedDecision": "Deny", + "ExpectedRuleId": "deny.manager-custom-parameters", + "Tags": ["baseline", "winget", "risky-options"] + }, + { + "Id": "baseline.default-allow.msstore.deny", + "Policy": "deny-risky-options.policy.json", + "Request": "requests/winget-vscode-msstore.request.json", + "ExpectedDecision": "Deny", + "ExpectedRuleId": "deny.unapproved-winget-source", + "Tags": ["baseline", "winget", "source"] + }, + { + "Id": "baseline.powershell.pester.currentuser.allow", + "Policy": "powershell-current-user.policy.json", + "Request": "requests/powershell-pester-currentuser.request.json", + "ExpectedDecision": "Allow", + "ExpectedRuleId": "allow.powershell.pester", + "Tags": ["baseline", "powershell", "scope"] + }, + { + "Id": "baseline.powershell.pester.allusers.deny", + "Policy": "powershell-current-user.policy.json", + "Request": "requests/powershell-pester-allusers.request.json", + "ExpectedDecision": "Deny", + "ExpectedRuleId": "deny.powershell.machine-scope", + "Tags": ["baseline", "powershell", "scope"] + }, + { + "Id": "baseline.yaml.policy-yaml.request-yaml.allow", + "Policy": "corporate-allowlist.policy.yaml", + "Request": "requests/winget-vscode-install.request.yaml", + "ExpectedDecision": "Allow", + "ExpectedRuleId": "allow.winget.vscode", + "Tags": ["baseline", "yaml", "winget"] + }, + { + "Id": "baseline.yaml.policy-yaml.request-json.deny", + "Policy": "corporate-allowlist.policy.yaml", + "Request": "requests/winget-vscode-skiphash.request.json", + "ExpectedDecision": "Deny", + "ExpectedRuleId": "deny.integrity-bypass", + "Tags": ["baseline", "yaml", "winget", "risky-options"] + }, + { + "Id": "baseline.yaml.policy-json.request-yaml.allow", + "Policy": "corporate-allowlist.policy.json", + "Request": "requests/winget-vscode-install.request.yaml", + "ExpectedDecision": "Allow", + "ExpectedRuleId": "allow.winget.vscode", + "Tags": ["baseline", "yaml", "winget"] + } + ] +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/scenarios/extended.scenarios.json b/crates/unigetui-broker/assets/samples/scenarios/extended.scenarios.json new file mode 100644 index 000000000..c7f90030f --- /dev/null +++ b/crates/unigetui-broker/assets/samples/scenarios/extended.scenarios.json @@ -0,0 +1,198 @@ +{ + "ScenarioSet": "extended", + "Description": "Extended scenarios for precedence, versioning, constraints, risky options, and validation failures.", + "Scenarios": [ + { + "Id": "extended.precedence.deny-wins-tie", + "Policy": "scenario-coverage.policy.json", + "Request": "requests/winget-vscode-custom-param.request.json", + "ExpectedDecision": "Deny", + "ExpectedRuleId": "deny.tie.custom-parameters", + "Tags": ["extended", "precedence", "winget"] + }, + { + "Id": "extended.precedence.disabled-rule-ignored", + "Policy": "scenario-coverage.policy.json", + "Request": "requests/winget-powertoys-install.request.json", + "ExpectedDecision": "Allow", + "ExpectedRuleId": "allow.winget.powertoys", + "Tags": ["extended", "precedence", "winget"] + }, + { + "Id": "extended.version.range.allow", + "Policy": "scenario-coverage.policy.json", + "Request": "requests/winget-vscode-version-in-range.request.json", + "ExpectedDecision": "Allow", + "ExpectedRuleId": "allow.winget.vscode.version-range", + "Tags": ["extended", "version", "winget"] + }, + { + "Id": "extended.operation.update.allow", + "Policy": "scenario-coverage.policy.json", + "Request": "requests/winget-vscode-update-in-range.request.json", + "ExpectedDecision": "Allow", + "ExpectedRuleId": "allow.winget.vscode.version-range", + "Tags": ["extended", "operation", "Update", "winget"] + }, + { + "Id": "extended.operation.uninstall.allow", + "Policy": "scenario-coverage.policy.json", + "Request": "requests/winget-git-uninstall.request.json", + "ExpectedDecision": "Allow", + "ExpectedRuleId": "allow.winget.git.uninstall", + "Tags": ["extended", "operation", "Uninstall", "winget"] + }, + { + "Id": "extended.version.range.default-deny", + "Policy": "scenario-coverage.policy.json", + "Request": "requests/winget-vscode-version-out-of-range.request.json", + "ExpectedDecision": "Deny", + "ExpectedRuleId": "", + "Tags": ["extended", "version", "winget", "default-deny"] + }, + { + "Id": "extended.match.source.default-deny", + "Policy": "scenario-coverage.policy.json", + "Request": "requests/winget-vscode-winget-fonts.request.json", + "ExpectedDecision": "Deny", + "ExpectedRuleId": "", + "Tags": ["extended", "source", "winget", "default-deny"] + }, + { + "Id": "extended.match.architecture.default-deny", + "Policy": "scenario-coverage.policy.json", + "Request": "requests/winget-vscode-x86.request.json", + "ExpectedDecision": "Deny", + "ExpectedRuleId": "", + "Tags": ["extended", "architecture", "winget", "default-deny"] + }, + { + "Id": "extended.version.prerelease.default-deny", + "Policy": "scenario-coverage.policy.json", + "Request": "requests/winget-vscode-prerelease.request.json", + "ExpectedDecision": "Deny", + "ExpectedRuleId": "", + "Tags": ["extended", "version", "winget", "prerelease"] + }, + { + "Id": "extended.risky.interactive.deny", + "Policy": "scenario-coverage.policy.json", + "Request": "requests/winget-vscode-interactive.request.json", + "ExpectedDecision": "Deny", + "ExpectedRuleId": "deny.interactive", + "Tags": ["extended", "risky-options", "winget"] + }, + { + "Id": "extended.constraint.install-location.allow", + "Policy": "scenario-coverage.policy.json", + "Request": "requests/winget-git-custom-location-allowed.request.json", + "ExpectedDecision": "Allow", + "ExpectedRuleId": "allow.winget.git.customized", + "Tags": ["extended", "constraints", "winget"] + }, + { + "Id": "extended.constraint.install-location.default-deny", + "Policy": "scenario-coverage.policy.json", + "Request": "requests/winget-git-custom-location-denied.request.json", + "ExpectedDecision": "Deny", + "ExpectedRuleId": "", + "Tags": ["extended", "constraints", "winget", "default-deny"] + }, + { + "Id": "extended.constraint.custom-parameter.allow", + "Policy": "scenario-coverage.policy.json", + "Request": "requests/winget-git-custom-param-allowed.request.json", + "ExpectedDecision": "Allow", + "ExpectedRuleId": "allow.winget.git.customized", + "Tags": ["extended", "constraints", "winget"] + }, + { + "Id": "extended.constraint.custom-parameter.default-deny", + "Policy": "scenario-coverage.policy.json", + "Request": "requests/winget-git-custom-param-denied.request.json", + "ExpectedDecision": "Deny", + "ExpectedRuleId": "", + "Tags": ["extended", "constraints", "winget", "default-deny"] + }, + { + "Id": "extended.risky.prepost.deny", + "Policy": "scenario-coverage.policy.json", + "Request": "requests/winget-vscode-prepost.request.json", + "ExpectedDecision": "Deny", + "ExpectedRuleId": "deny.prepost-commands", + "Tags": ["extended", "risky-options", "winget"] + }, + { + "Id": "extended.risky.kill-before-operation.deny", + "Policy": "scenario-coverage.policy.json", + "Request": "requests/winget-vscode-kill-before.request.json", + "ExpectedDecision": "Deny", + "ExpectedRuleId": "deny.kill-process-actions", + "Tags": ["extended", "risky-options", "winget"] + }, + { + "Id": "extended.powershell.prerelease.deny", + "Policy": "powershell-current-user.policy.json", + "Request": "requests/powershell-pester-prerelease.request.json", + "ExpectedDecision": "Deny", + "ExpectedRuleId": "deny.powershell.prerelease", + "Tags": ["extended", "powershell", "prerelease"] + }, + { + "Id": "extended.powershell.publisher-bypass.deny", + "Policy": "deny-risky-options.policy.json", + "Request": "requests/powershell-pester-skipcheck.request.json", + "ExpectedDecision": "Deny", + "ExpectedRuleId": "deny.integrity-bypass", + "Tags": ["extended", "powershell", "risky-options"] + }, + { + "Id": "extended.powershell.source.deny", + "Policy": "powershell-advanced.policy.json", + "Request": "requests/powershell-pester-poshtestgallery.request.json", + "ExpectedDecision": "Deny", + "ExpectedRuleId": "deny.powershell.untrusted-source", + "Tags": ["extended", "powershell", "source"] + }, + { + "Id": "extended.powershell.version.allow", + "Policy": "powershell-advanced.policy.json", + "Request": "requests/powershell-pester-version-allowed.request.json", + "ExpectedDecision": "Allow", + "ExpectedRuleId": "allow.powershell.pester.versioned", + "Tags": ["extended", "powershell", "version"] + }, + { + "Id": "extended.powershell.version.default-deny", + "Policy": "powershell-advanced.policy.json", + "Request": "requests/powershell-pester-version-out-of-range.request.json", + "ExpectedDecision": "Deny", + "ExpectedRuleId": "", + "Tags": ["extended", "powershell", "version", "default-deny"] + }, + { + "Id": "extended.powershell.update.allow", + "Policy": "powershell-advanced.policy.json", + "Request": "requests/powershell-pester-update-currentuser.request.json", + "ExpectedDecision": "Allow", + "ExpectedRuleId": "allow.powershell.pester.versioned", + "Tags": ["extended", "powershell", "operation", "Update"] + }, + { + "Id": "extended.validation.request-missing-package-id.deny", + "Policy": "corporate-allowlist.policy.json", + "Request": "invalid/requests/missing-package-id.request.json", + "ExpectedDecision": "Deny", + "ExpectedRuleId": "", + "Tags": ["extended", "validation", "fail-closed"] + }, + { + "Id": "extended.validation.policy-invalid-failure-decision.deny", + "Policy": "invalid/policies/invalid-failure-decision.policy.json", + "Request": "requests/winget-vscode-install.request.json", + "ExpectedDecision": "Deny", + "ExpectedRuleId": "", + "Tags": ["extended", "validation", "fail-closed"] + } + ] +} \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/wire/named-pipe-allow.http b/crates/unigetui-broker/assets/samples/wire/named-pipe-allow.http new file mode 100644 index 000000000..1f5b1ad36 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/wire/named-pipe-allow.http @@ -0,0 +1,38 @@ +POST /v1/package-operations/evaluate HTTP/1.1 +Host: unigetui-broker +UniGetUI-Protocol-Version: 1.0 +UniGetUI-Request-Id: req-winget-vscode-install +Content-Type: application/vnd.unigetui.package-request+json; version=1.0 +Accept: application/vnd.unigetui.package-broker-response+json; version=1.0 +Content-Length: + +{ + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-winget-vscode-install", + "CreatedAt": "2026-05-05T12:00:00Z", + "Operation": "Install", + "Manager": { "Name": "Winget", "DisplayName": "WinGet", "ExecutableFriendlyName": "winget.exe" }, + "Source": { "Name": "winget", "Url": "https://cdn.winget.microsoft.com/cache", "IsVirtualManager": false }, + "Package": { "Id": "Microsoft.VisualStudioCode", "Name": "Microsoft Visual Studio Code" }, + "Options": { + "Scope": "Machine", + "Architecture": "X64", + "Interactive": false, + "SkipHashCheck": false, + "PreRelease": false, + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Broker": { "RequestedElevation": "Elevated", "EffectiveUser": "CONTOSO\\alice", "ClientVersion": "3.2.0" } +} + +HTTP/1.1 200 OK +UniGetUI-Protocol-Version: 1.0 +UniGetUI-Audit-Id: audit-20260505-000001 +UniGetUI-Policy-Id: contoso.desktop.standard-allowlist +UniGetUI-Policy-Revision: 4 +Content-Type: application/vnd.unigetui.package-broker-response+json; version=1.0 +Content-Length: + + \ No newline at end of file diff --git a/crates/unigetui-broker/assets/samples/wire/named-pipe-deny.http b/crates/unigetui-broker/assets/samples/wire/named-pipe-deny.http new file mode 100644 index 000000000..e380b88a8 --- /dev/null +++ b/crates/unigetui-broker/assets/samples/wire/named-pipe-deny.http @@ -0,0 +1,38 @@ +POST /v1/package-operations/evaluate HTTP/1.1 +Host: unigetui-broker +UniGetUI-Protocol-Version: 1.0 +UniGetUI-Request-Id: req-winget-vscode-skiphash +Content-Type: application/vnd.unigetui.package-request+json; version=1.0 +Accept: application/vnd.unigetui.package-broker-response+json; version=1.0 +Content-Length: + +{ + "RequestVersion": "1.0.0", + "RequestType": "PackageOperation", + "RequestId": "req-winget-vscode-skiphash", + "CreatedAt": "2026-05-05T12:10:00Z", + "Operation": "Install", + "Manager": { "Name": "Winget", "DisplayName": "WinGet", "ExecutableFriendlyName": "winget.exe" }, + "Source": { "Name": "winget", "Url": "https://cdn.winget.microsoft.com/cache", "IsVirtualManager": false }, + "Package": { "Id": "Microsoft.VisualStudioCode", "Name": "Microsoft Visual Studio Code" }, + "Options": { + "Scope": "Machine", + "Architecture": "X64", + "Interactive": false, + "SkipHashCheck": true, + "PreRelease": false, + "CustomParameters": [], + "KillBeforeOperation": [] + }, + "Broker": { "RequestedElevation": "Elevated", "EffectiveUser": "CONTOSO\\alice", "ClientVersion": "3.2.0" } +} + +HTTP/1.1 403 Forbidden +UniGetUI-Protocol-Version: 1.0 +UniGetUI-Audit-Id: audit-20260505-000002 +UniGetUI-Policy-Id: contoso.desktop.standard-allowlist +UniGetUI-Policy-Revision: 4 +Content-Type: application/vnd.unigetui.package-broker-response+json; version=1.0 +Content-Length: + + \ No newline at end of file diff --git a/crates/unigetui-broker/dotnet/.gitignore b/crates/unigetui-broker/dotnet/.gitignore new file mode 100644 index 000000000..cd42ee34e --- /dev/null +++ b/crates/unigetui-broker/dotnet/.gitignore @@ -0,0 +1,2 @@ +bin/ +obj/ diff --git a/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client.Tests/Devolutions.UniGetUI.Broker.Client.Tests.csproj b/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client.Tests/Devolutions.UniGetUI.Broker.Client.Tests.csproj new file mode 100644 index 000000000..bcbe024c7 --- /dev/null +++ b/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client.Tests/Devolutions.UniGetUI.Broker.Client.Tests.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + latest + enable + enable + false + Devolutions.UniGetUI.Broker.Client.Tests + + + + + + + + + + + + + + diff --git a/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client.Tests/DtoRoundTripTests.cs b/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client.Tests/DtoRoundTripTests.cs new file mode 100644 index 000000000..37049aabb --- /dev/null +++ b/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client.Tests/DtoRoundTripTests.cs @@ -0,0 +1,60 @@ +using System.Text.Json; + +using NJsonSchema; + +using Xunit; + +namespace Devolutions.UniGetUI.Broker.Client.Tests; + +/// +/// Parity tests: the hand-written C# DTOs must consume the exact sample documents the +/// Rust crate uses, and re-serialize to output that still validates against the same +/// JSON Schemas. Uses so a sample field missing from a DTO +/// fails the test (DTO completeness), mirroring the Rust `deny_unknown_fields` contract. +/// +public class DtoRoundTripTests +{ + [Theory] + [MemberData(nameof(TestData.RequestSamples), MemberType = typeof(TestData))] + public async Task PackageRequest_round_trips_and_validates(string path) + => await AssertRoundTrip(path, TestData.RequestSchema); + + [Theory] + [MemberData(nameof(TestData.ResponseSamples), MemberType = typeof(TestData))] + public async Task BrokerResponse_round_trips_and_validates(string path) + => await AssertRoundTrip(path, TestData.ResponseSchema); + + [Theory] + [MemberData(nameof(TestData.StatusRequestSamples), MemberType = typeof(TestData))] + public async Task StatusRequest_round_trips_and_validates(string path) + => await AssertRoundTrip(path, TestData.StatusRequestSchema); + + [Theory] + [MemberData(nameof(TestData.StatusResponseSamples), MemberType = typeof(TestData))] + public async Task StatusResponse_round_trips_and_validates(string path) + => await AssertRoundTrip(path, TestData.StatusResponseSchema); + + [Theory] + [MemberData(nameof(TestData.PolicySamples), MemberType = typeof(TestData))] + public async Task PolicyDocument_round_trips_and_validates(string path) + => await AssertRoundTrip(path, TestData.PolicySchema); + + private static async Task AssertRoundTrip(string samplePath, string schemaPath) + { + var original = await File.ReadAllTextAsync(samplePath); + + // 1. Deserialize the canonical sample into the DTO (strict: every field must map). + var dto = JsonSerializer.Deserialize(original, TestData.Strict); + Assert.NotNull(dto); + + // 2. Re-serialize and validate the output against the same schema. + var reserialized = JsonSerializer.Serialize(dto, BrokerJson.Options); + var schema = await JsonSchema.FromFileAsync(schemaPath); + var errors = schema.Validate(reserialized); + + Assert.True( + errors.Count == 0, + $"Re-serialized {typeof(T).Name} from {Path.GetFileName(samplePath)} failed schema validation:\n" + + string.Join("\n", errors.Select(e => $" {e.Kind} at {e.Path}"))); + } +} diff --git a/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client.Tests/SchemaValidationTests.cs b/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client.Tests/SchemaValidationTests.cs new file mode 100644 index 000000000..5f0f072c3 --- /dev/null +++ b/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client.Tests/SchemaValidationTests.cs @@ -0,0 +1,56 @@ +using NJsonSchema; + +using Xunit; + +namespace Devolutions.UniGetUI.Broker.Client.Tests; + +/// +/// Cross-checks that the bundled sample documents validate against the shared JSON +/// Schemas, and that intentionally-invalid fixtures are rejected. This confirms the C# +/// tooling reads the very same schema + data files the Rust tests use. +/// +public class SchemaValidationTests +{ + [Theory] + [MemberData(nameof(TestData.RequestSamples), MemberType = typeof(TestData))] + public async Task Request_samples_are_schema_valid(string path) + => await AssertValid(path, TestData.RequestSchema); + + [Theory] + [MemberData(nameof(TestData.ResponseSamples), MemberType = typeof(TestData))] + public async Task Response_samples_are_schema_valid(string path) + => await AssertValid(path, TestData.ResponseSchema); + + [Theory] + [MemberData(nameof(TestData.StatusResponseSamples), MemberType = typeof(TestData))] + public async Task Status_response_samples_are_schema_valid(string path) + => await AssertValid(path, TestData.StatusResponseSchema); + + [Theory] + [MemberData(nameof(TestData.PolicySamples), MemberType = typeof(TestData))] + public async Task Policy_samples_are_schema_valid(string path) + => await AssertValid(path, TestData.PolicySchema); + + [Fact] + public async Task Invalid_request_is_rejected_by_schema() + { + var path = Path.Combine(TestData.SamplesDir, "invalid", "requests", "missing-package-id.request.json"); + Assert.True(File.Exists(path), $"missing invalid fixture: {path}"); + + var schema = await JsonSchema.FromFileAsync(TestData.RequestSchema); + var errors = schema.Validate(await File.ReadAllTextAsync(path)); + + Assert.True(errors.Count > 0, "expected the empty package id to fail schema validation"); + } + + private static async Task AssertValid(string samplePath, string schemaPath) + { + var schema = await JsonSchema.FromFileAsync(schemaPath); + var errors = schema.Validate(await File.ReadAllTextAsync(samplePath)); + + Assert.True( + errors.Count == 0, + $"{Path.GetFileName(samplePath)} failed schema validation:\n" + + string.Join("\n", errors.Select(e => $" {e.Kind} at {e.Path}"))); + } +} diff --git a/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client.Tests/TestData.cs b/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client.Tests/TestData.cs new file mode 100644 index 000000000..d067eb85b --- /dev/null +++ b/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client.Tests/TestData.cs @@ -0,0 +1,69 @@ +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Devolutions.UniGetUI.Broker.Client.Tests; + +/// +/// Resolves the shared schema and sample files that the Rust crate uses, so the C# +/// client is validated against the exact same fixtures (no copies, no drift). +/// +public static class TestData +{ + /// Absolute path to the crate root (`crates/unigetui-broker`). + public static string CrateRoot { get; } = ResolveCrateRoot(); + + public static string SchemaDir => Path.Combine(CrateRoot, "schema"); + public static string SamplesDir => Path.Combine(CrateRoot, "assets", "samples"); + + public static string RequestSchema => Path.Combine(SchemaDir, "unigetui.package-request.schema.json"); + public static string ResponseSchema => Path.Combine(SchemaDir, "unigetui.package-broker-response.schema.json"); + public static string PolicySchema => Path.Combine(SchemaDir, "unigetui.package-policy.schema.json"); + public static string StatusRequestSchema => Path.Combine(SchemaDir, "unigetui.package-operation-status-request.schema.json"); + public static string StatusResponseSchema => Path.Combine(SchemaDir, "unigetui.package-operation-status-response.schema.json"); + + /// + /// Strict options: deserialization fails if a sample contains a field the DTO does + /// not declare, ensuring the C# models cover the full wire shape. + /// + public static readonly JsonSerializerOptions Strict = new(BrokerJson.Options) + { + UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow, + }; + + /// JSON files under `assets/samples/requests` that are package requests (not status). + public static IEnumerable RequestSamples() => + JsonFiles(Path.Combine(SamplesDir, "requests")) + .Where(f => !Path.GetFileName(f).StartsWith("status-", StringComparison.Ordinal)) + .Select(f => new object[] { f }); + + public static IEnumerable StatusRequestSamples() => + JsonFiles(Path.Combine(SamplesDir, "requests")) + .Where(f => Path.GetFileName(f).StartsWith("status-", StringComparison.Ordinal)) + .Select(f => new object[] { f }); + + public static IEnumerable ResponseSamples() => + JsonFiles(Path.Combine(SamplesDir, "responses")) + .Where(f => !Path.GetFileName(f).StartsWith("status-", StringComparison.Ordinal)) + .Select(f => new object[] { f }); + + public static IEnumerable StatusResponseSamples() => + JsonFiles(Path.Combine(SamplesDir, "responses")) + .Where(f => Path.GetFileName(f).StartsWith("status-", StringComparison.Ordinal)) + .Select(f => new object[] { f }); + + /// JSON policy samples (top-level `assets/samples/*.policy.json`). + public static IEnumerable PolicySamples() => + Directory.GetFiles(SamplesDir, "*.policy.json") + .Select(f => new object[] { f }); + + private static IEnumerable JsonFiles(string dir) => + Directory.Exists(dir) ? Directory.GetFiles(dir, "*.json") : []; + + private static string ResolveCrateRoot([CallerFilePath] string thisFile = "") + { + // thisFile = /dotnet/Devolutions.UniGetUI.Broker.Client.Tests/TestData.cs + var testsDir = Path.GetDirectoryName(thisFile)!; + return Path.GetFullPath(Path.Combine(testsDir, "..", "..")); + } +} diff --git a/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client.slnx b/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client.slnx new file mode 100644 index 000000000..656ca1040 --- /dev/null +++ b/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client/BrokerClient.cs b/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client/BrokerClient.cs new file mode 100644 index 000000000..ddf52fef9 --- /dev/null +++ b/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client/BrokerClient.cs @@ -0,0 +1,302 @@ +using System.IO.Pipes; +using System.Text; +using System.Text.Json; + +namespace Devolutions.UniGetUI.Broker.Client; + +/// +/// Client for communicating with the Devolutions Agent UniGetUI package broker over a +/// Windows named pipe using the HTTP/1.1 wire protocol. +/// +public sealed class BrokerClient : IDisposable +{ + public const string DefaultPipeName = "UniGetUI.PackageBroker.v1"; + + private const string ProtocolVersion = "1.0"; + private const string RequestMediaType = "application/vnd.unigetui.package-request+json; version=1.0"; + private const string ResponseMediaType = "application/vnd.unigetui.package-broker-response+json; version=1.0"; + private const int ConnectTimeoutMs = 5000; + private const int ReadTimeoutMs = 30000; + + private readonly string _pipeName; + + /// Optional diagnostic sink; receives human-readable trace lines. + public Action? Trace { get; init; } + + public BrokerClient(string? pipeName = null) + { + _pipeName = pipeName ?? DefaultPipeName; + } + + /// Check whether the broker is reachable (pipe exists and answers the health check). + public async Task IsAvailableAsync(CancellationToken cancellationToken = default) + { + try + { + var response = await SendHttpRequestAsync("GET", "/v1/health", null, null, cancellationToken).ConfigureAwait(false); + return response.StatusCode == 200; + } + catch (Exception ex) + { + Trace?.Invoke($"Broker not available: {ex.GetType().Name}: {ex.Message}"); + return false; + } + } + + /// Evaluate a package operation against policy without executing it (dry-run). + public Task EvaluateAsync(PackageRequest request, CancellationToken cancellationToken = default) + => SendPackageOperationAsync(request, "/v1/package-operations/evaluate", cancellationToken); + + /// Submit a package operation for evaluation and (if allowed) elevated execution. + public Task ExecuteAsync(PackageRequest request, CancellationToken cancellationToken = default) + => SendPackageOperationAsync(request, "/v1/package-operations", cancellationToken); + + /// + /// Submit a package operation and poll until it reaches a terminal status + /// ( or ). + /// + public async Task ExecuteAndWaitAsync( + PackageRequest request, + CancellationToken cancellationToken = default, + int pollIntervalMs = 500) + { + var executeResponse = await SendPackageOperationAsync(request, "/v1/package-operations", cancellationToken).ConfigureAwait(false); + if (executeResponse is null) + { + Trace?.Invoke("Execute request failed, cannot poll for status."); + return null; + } + + if (executeResponse.Decision != Decision.Allow) + { + Trace?.Invoke($"Operation denied by policy: {executeResponse.Reason}"); + return new StatusResponse + { + RequestId = request.RequestId, + Status = OperationStatus.Failed, + Note = $"Denied by policy: {executeResponse.Reason}", + }; + } + + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(pollIntervalMs, cancellationToken).ConfigureAwait(false); + + var status = await QueryStatusAsync(request.RequestId, request.Broker, cancellationToken).ConfigureAwait(false); + if (status is null) + { + Trace?.Invoke("Status query returned null, retrying..."); + continue; + } + + if (status.Status is OperationStatus.Completed or OperationStatus.Failed) + { + return status; + } + } + + return new StatusResponse + { + RequestId = request.RequestId, + Status = OperationStatus.Failed, + Note = "Operation polling was cancelled.", + }; + } + + /// Query the status of a previously submitted package operation. + public async Task QueryStatusAsync( + string requestId, + BrokerContext brokerContext, + CancellationToken cancellationToken = default) + { + try + { + var statusRequest = new StatusRequest + { + RequestId = requestId, + Broker = brokerContext, + }; + + var body = JsonSerializer.Serialize(statusRequest, BrokerJson.Options); + + var headers = new Dictionary + { + ["Content-Type"] = "application/json", + ["Accept"] = "application/json", + ["UniGetUI-Protocol-Version"] = ProtocolVersion, + }; + + var response = await SendHttpRequestAsync("POST", "/v1/package-operations/status", body, headers, cancellationToken).ConfigureAwait(false); + + if (string.IsNullOrWhiteSpace(response.Body)) + { + Trace?.Invoke($"Empty status response body (status {response.StatusCode})."); + return null; + } + + return JsonSerializer.Deserialize(response.Body, BrokerJson.Options); + } + catch (Exception ex) + { + Trace?.Invoke($"Error querying operation status: {ex.Message}"); + return null; + } + } + + public void Dispose() + { + // No persistent resources to dispose. + } + + private async Task SendPackageOperationAsync(PackageRequest request, string endpoint, CancellationToken cancellationToken) + { + try + { + var body = JsonSerializer.Serialize(request, BrokerJson.Options); + + var headers = new Dictionary + { + ["Content-Type"] = RequestMediaType, + ["Accept"] = ResponseMediaType, + ["UniGetUI-Protocol-Version"] = ProtocolVersion, + ["UniGetUI-Request-Id"] = request.RequestId, + }; + + var response = await SendHttpRequestAsync("POST", endpoint, body, headers, cancellationToken).ConfigureAwait(false); + + if (string.IsNullOrWhiteSpace(response.Body)) + { + Trace?.Invoke($"Empty response body from broker (status {response.StatusCode})."); + return null; + } + + return JsonSerializer.Deserialize(response.Body, BrokerJson.Options); + } + catch (Exception ex) + { + Trace?.Invoke($"Error communicating with broker: {ex.Message}"); + return null; + } + } + + /// Send a raw HTTP/1.1 request over the named pipe and read the response. + private async Task SendHttpRequestAsync( + string method, + string path, + string? body, + Dictionary? extraHeaders, + CancellationToken cancellationToken) + { + using var pipe = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); + + using (var connectCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) + { + connectCts.CancelAfter(ConnectTimeoutMs); + await pipe.ConnectAsync(connectCts.Token).ConfigureAwait(false); + } + + var requestBuilder = new StringBuilder(); + requestBuilder.Append($"{method} {path} HTTP/1.1\r\n"); + requestBuilder.Append("Host: unigetui-broker\r\n"); + requestBuilder.Append("Connection: close\r\n"); + + if (extraHeaders is not null) + { + foreach (var (key, value) in extraHeaders) + { + if (!key.Equals("Host", StringComparison.OrdinalIgnoreCase)) + { + requestBuilder.Append($"{key}: {value}\r\n"); + } + } + } + + byte[]? bodyBytes = body is not null ? Encoding.UTF8.GetBytes(body) : null; + requestBuilder.Append($"Content-Length: {bodyBytes?.Length ?? 0}\r\n"); + requestBuilder.Append("\r\n"); + + var headerBytes = Encoding.ASCII.GetBytes(requestBuilder.ToString()); + await pipe.WriteAsync(headerBytes, cancellationToken).ConfigureAwait(false); + if (bodyBytes is not null) + { + await pipe.WriteAsync(bodyBytes, cancellationToken).ConfigureAwait(false); + } + await pipe.FlushAsync(cancellationToken).ConfigureAwait(false); + + using var readCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + readCts.CancelAfter(ReadTimeoutMs); + return await ReadHttpResponseAsync(pipe, readCts.Token).ConfigureAwait(false); + } + + /// Parse an HTTP/1.1 response from the pipe stream. + private static async Task ReadHttpResponseAsync(Stream stream, CancellationToken ct) + { + var buffer = new byte[65536]; + var totalRead = 0; + + while (totalRead < buffer.Length) + { + var bytesRead = await stream.ReadAsync(buffer.AsMemory(totalRead, buffer.Length - totalRead), ct).ConfigureAwait(false); + if (bytesRead == 0) + { + break; + } + totalRead += bytesRead; + + var currentText = Encoding.ASCII.GetString(buffer, 0, totalRead); + var headerEnd = currentText.IndexOf("\r\n\r\n", StringComparison.Ordinal); + if (headerEnd < 0) + { + continue; + } + + var headerText = currentText[..headerEnd]; + var bodyStart = headerEnd + 4; + + var lines = headerText.Split("\r\n"); + var statusCode = int.Parse(lines[0].Split(' ')[1]); + + var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + for (var i = 1; i < lines.Length; i++) + { + var colonIdx = lines[i].IndexOf(':'); + if (colonIdx > 0) + { + headers[lines[i][..colonIdx].Trim()] = lines[i][(colonIdx + 1)..].Trim(); + } + } + + var contentLength = 0; + if (headers.TryGetValue("Content-Length", out var clStr)) + { + contentLength = int.Parse(clStr); + } + + var bodyBytesRead = totalRead - bodyStart; + while (bodyBytesRead < contentLength) + { + if (bodyStart + contentLength > buffer.Length) + { + var newBuffer = new byte[bodyStart + contentLength]; + Buffer.BlockCopy(buffer, 0, newBuffer, 0, totalRead); + buffer = newBuffer; + } + + var read = await stream.ReadAsync(buffer.AsMemory(bodyStart + bodyBytesRead, contentLength - bodyBytesRead), ct).ConfigureAwait(false); + if (read == 0) + { + break; + } + bodyBytesRead += read; + totalRead += read; + } + + var bodyText = Encoding.UTF8.GetString(buffer, bodyStart, contentLength); + return new HttpPipeResponse(statusCode, bodyText); + } + + throw new InvalidOperationException("Failed to read a complete HTTP response from the pipe."); + } + + private readonly record struct HttpPipeResponse(int StatusCode, string Body); +} diff --git a/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client/BrokerJson.cs b/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client/BrokerJson.cs new file mode 100644 index 000000000..4f01a2ef8 --- /dev/null +++ b/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client/BrokerJson.cs @@ -0,0 +1,34 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Devolutions.UniGetUI.Broker.Client; + +/// Canonical schema URIs used in the $schema field of each document. +public static class SchemaUris +{ + public const string Request = "https://aka.ms/unigetui/package-request.schema.1.0.json"; + public const string Response = "https://aka.ms/unigetui/package-broker-response.schema.1.0.json"; + public const string Policy = "https://aka.ms/unigetui/package-policy.schema.1.0.json"; + public const string StatusRequest = "https://aka.ms/unigetui/package-operation-status-request.schema.1.0.json"; + public const string StatusResponse = "https://aka.ms/unigetui/package-operation-status-response.schema.1.0.json"; +} + +/// Shared for broker documents. +public static class BrokerJson +{ + /// + /// Serialization options matching the broker wire format: PascalCase property names + /// (via explicit [JsonPropertyName] attributes), PascalCase enum values, and + /// null optionals omitted (mirroring the Rust skip_serializing_if = "Option::is_none"). + /// + public static readonly JsonSerializerOptions Options = new() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = false, + }; + + public static readonly JsonSerializerOptions PrettyOptions = new(Options) + { + WriteIndented = true, + }; +} diff --git a/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client/Devolutions.UniGetUI.Broker.Client.csproj b/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client/Devolutions.UniGetUI.Broker.Client.csproj new file mode 100644 index 000000000..d312a7110 --- /dev/null +++ b/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client/Devolutions.UniGetUI.Broker.Client.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + latest + enable + enable + Devolutions.UniGetUI.Broker.Client + Devolutions.UniGetUI.Broker.Client + true + + + + + Devolutions.UniGetUI.Broker.Client + + 0.0.0.0 + Devolutions Agent UniGetUI package broker client + Client and DTOs for the Devolutions Agent UniGetUI package broker API. + Devolutions Inc. + © Devolutions Inc. All rights reserved. + MIT OR Apache-2.0 + https://github.com/Devolutions/devolutions-gateway.git + git + true + snupkg + false + + + diff --git a/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client/Enums.cs b/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client/Enums.cs new file mode 100644 index 000000000..5d493580b --- /dev/null +++ b/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client/Enums.cs @@ -0,0 +1,91 @@ +using System.Text.Json.Serialization; + +namespace Devolutions.UniGetUI.Broker.Client; + +// Enum members are spelled exactly as they appear on the wire (PascalCase), so the +// default JsonStringEnumConverter round-trips them without a naming policy. + +/// Package operation type. +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum Operation +{ + Install, + Update, + Uninstall, +} + +/// Supported package manager names. +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum ManagerName +{ + Winget, + PowerShell, + PowerShell7, +} + +/// Installation scope. +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum Scope +{ + User, + Machine, +} + +/// Target architecture. +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum Architecture +{ + X86, + X64, + Arm64, + Neutral, +} + +/// Requested elevation level. +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum Elevation +{ + Standard, + Elevated, +} + +/// Policy decision. +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum Decision +{ + Allow, + Deny, +} + +/// Broker transport type. +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum Transport +{ + HttpNamedPipe, + HttpLoopbackSimulator, +} + +/// Execution mode. +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum ExecutionMode +{ + SimulatedElevated, + Elevated, +} + +/// Status of an asynchronous package operation. +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum OperationStatus +{ + Starting, + Running, + Completed, + Failed, +} + +/// Rule precedence strategy. +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum RulePrecedence +{ + PriorityThenDeny, +} diff --git a/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client/PolicyModels.cs b/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client/PolicyModels.cs new file mode 100644 index 000000000..c3ab8ff7a --- /dev/null +++ b/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client/PolicyModels.cs @@ -0,0 +1,199 @@ +using System.Text.Json.Serialization; + +namespace Devolutions.UniGetUI.Broker.Client; + +/// A policy document governing which package operations are allowed or denied. +public sealed class PolicyDocument +{ + [JsonPropertyName("$schema")] + public string Schema { get; set; } = SchemaUris.Policy; + + [JsonPropertyName("PolicyVersion")] + public string PolicyVersion { get; set; } = "1.0.0"; + + [JsonPropertyName("PolicyType")] + public string PolicyType { get; set; } = "PackageBrokerPolicy"; + + [JsonPropertyName("Metadata")] + public PolicyMetadata Metadata { get; set; } = new(); + + [JsonPropertyName("Enforcement")] + public PolicyEnforcement Enforcement { get; set; } = new(); + + [JsonPropertyName("Rules")] + public List Rules { get; set; } = []; +} + +public sealed class PolicyMetadata +{ + [JsonPropertyName("Id")] + public string Id { get; set; } = ""; + + [JsonPropertyName("Publisher")] + public string Publisher { get; set; } = ""; + + [JsonPropertyName("Revision")] + public int Revision { get; set; } + + [JsonPropertyName("PublishedAt")] + public DateTimeOffset PublishedAt { get; set; } + + [JsonPropertyName("ValidFrom")] + public DateTimeOffset? ValidFrom { get; set; } + + [JsonPropertyName("ValidUntil")] + public DateTimeOffset? ValidUntil { get; set; } + + [JsonPropertyName("Description")] + public string? Description { get; set; } + + [JsonPropertyName("SupportUrl")] + public string? SupportUrl { get; set; } +} + +public sealed class PolicyEnforcement +{ + [JsonPropertyName("DefaultDecision")] + public Decision DefaultDecision { get; set; } + + [JsonPropertyName("RulePrecedence")] + public RulePrecedence RulePrecedence { get; set; } + + [JsonPropertyName("AuditMode")] + public bool? AuditMode { get; set; } +} + +public sealed class PolicyRule +{ + [JsonPropertyName("Id")] + public string Id { get; set; } = ""; + + [JsonPropertyName("Enabled")] + public bool Enabled { get; set; } = true; + + [JsonPropertyName("Priority")] + public int Priority { get; set; } + + [JsonPropertyName("Decision")] + public Decision Decision { get; set; } + + [JsonPropertyName("Reason")] + public string? Reason { get; set; } + + [JsonPropertyName("Match")] + public PolicyMatch Match { get; set; } = new(); + + [JsonPropertyName("Constraints")] + public PolicyConstraints? Constraints { get; set; } +} + +public sealed class PolicyMatch +{ + [JsonPropertyName("Operations")] + public List Operations { get; set; } = []; + + [JsonPropertyName("Managers")] + public List Managers { get; set; } = []; + + [JsonPropertyName("Sources")] + public List Sources { get; set; } = []; + + [JsonPropertyName("PackageIdentifiers")] + public List PackageIdentifiers { get; set; } = []; + + [JsonPropertyName("PackageNames")] + public List PackageNames { get; set; } = []; + + [JsonPropertyName("Versions")] + public List Versions { get; set; } = []; + + [JsonPropertyName("VersionRange")] + public VersionRange? VersionRange { get; set; } + + [JsonPropertyName("Scopes")] + public List Scopes { get; set; } = []; + + [JsonPropertyName("Architectures")] + public List Architectures { get; set; } = []; + + [JsonPropertyName("Elevation")] + public List Elevation { get; set; } = []; + + [JsonPropertyName("Interactive")] + public List Interactive { get; set; } = []; + + [JsonPropertyName("SkipHashCheck")] + public List SkipHashCheck { get; set; } = []; + + [JsonPropertyName("PreRelease")] + public List PreRelease { get; set; } = []; + + [JsonPropertyName("HasCustomParameters")] + public List HasCustomParameters { get; set; } = []; + + [JsonPropertyName("HasCustomInstallLocation")] + public List HasCustomInstallLocation { get; set; } = []; + + [JsonPropertyName("HasPrePostCommands")] + public List HasPrePostCommands { get; set; } = []; + + [JsonPropertyName("HasKillBeforeOperation")] + public List HasKillBeforeOperation { get; set; } = []; + + [JsonPropertyName("HasUninstallPrevious")] + public List HasUninstallPrevious { get; set; } = []; +} + +public sealed class VersionRange +{ + [JsonPropertyName("MinVersion")] + public string? MinVersion { get; set; } + + [JsonPropertyName("MaxVersion")] + public string? MaxVersion { get; set; } + + [JsonPropertyName("IncludePrerelease")] + public bool IncludePrerelease { get; set; } +} + +public sealed class PolicyConstraints +{ + [JsonPropertyName("AllowInteractive")] + public bool AllowInteractive { get; set; } = true; + + [JsonPropertyName("AllowSkipHashCheck")] + public bool AllowSkipHashCheck { get; set; } = true; + + [JsonPropertyName("AllowPreRelease")] + public bool AllowPreRelease { get; set; } = true; + + [JsonPropertyName("AllowCustomInstallLocation")] + public bool AllowCustomInstallLocation { get; set; } = true; + + [JsonPropertyName("AllowedInstallLocationPatterns")] + public List AllowedInstallLocationPatterns { get; set; } = []; + + [JsonPropertyName("AllowCustomParameters")] + public bool AllowCustomParameters { get; set; } = true; + + [JsonPropertyName("AllowedCustomParameters")] + public List AllowedCustomParameters { get; set; } = []; + + [JsonPropertyName("AllowedCustomParameterPatterns")] + public List AllowedCustomParameterPatterns { get; set; } = []; + + [JsonPropertyName("DeniedCustomParameters")] + public List DeniedCustomParameters { get; set; } = []; + + [JsonPropertyName("AllowPrePostCommands")] + public bool AllowPrePostCommands { get; set; } = true; + + [JsonPropertyName("AllowKillBeforeOperation")] + public bool AllowKillBeforeOperation { get; set; } = true; + + [JsonPropertyName("AllowUninstallPrevious")] + public bool AllowUninstallPrevious { get; set; } = true; + + [JsonPropertyName("AllowUpgrade")] + public bool AllowUpgrade { get; set; } = true; +} diff --git a/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client/RequestModels.cs b/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client/RequestModels.cs new file mode 100644 index 000000000..093be073d --- /dev/null +++ b/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client/RequestModels.cs @@ -0,0 +1,133 @@ +using System.Text.Json.Serialization; + +namespace Devolutions.UniGetUI.Broker.Client; + +/// Canonical request sent by an unelevated UniGetUI process to the elevated broker. +public sealed class PackageRequest +{ + [JsonPropertyName("$schema")] + public string Schema { get; set; } = SchemaUris.Request; + + [JsonPropertyName("RequestVersion")] + public string RequestVersion { get; set; } = "1.0.0"; + + [JsonPropertyName("RequestType")] + public string RequestType { get; set; } = "PackageOperation"; + + [JsonPropertyName("RequestId")] + public string RequestId { get; set; } = ""; + + [JsonPropertyName("CreatedAt")] + public DateTimeOffset CreatedAt { get; set; } + + [JsonPropertyName("Operation")] + public Operation Operation { get; set; } + + [JsonPropertyName("Manager")] + public RequestManager Manager { get; set; } = new(); + + [JsonPropertyName("Source")] + public RequestSource Source { get; set; } = new(); + + [JsonPropertyName("Package")] + public RequestPackage Package { get; set; } = new(); + + [JsonPropertyName("Options")] + public RequestOptions Options { get; set; } = new(); + + [JsonPropertyName("Broker")] + public BrokerContext Broker { get; set; } = new(); +} + +public sealed class RequestManager +{ + [JsonPropertyName("Name")] + public ManagerName Name { get; set; } + + [JsonPropertyName("DisplayName")] + public string DisplayName { get; set; } = ""; + + [JsonPropertyName("ExecutableFriendlyName")] + public string ExecutableFriendlyName { get; set; } = ""; +} + +public sealed class RequestSource +{ + [JsonPropertyName("Name")] + public string Name { get; set; } = ""; + + [JsonPropertyName("Url")] + public string? Url { get; set; } + + [JsonPropertyName("IsVirtualManager")] + public bool? IsVirtualManager { get; set; } +} + +public sealed class RequestPackage +{ + [JsonPropertyName("Id")] + public string Id { get; set; } = ""; + + [JsonPropertyName("Name")] + public string Name { get; set; } = ""; + + [JsonPropertyName("Version")] + public string? Version { get; set; } + + [JsonPropertyName("Architecture")] + public Architecture? Architecture { get; set; } + + [JsonPropertyName("Channel")] + public string? Channel { get; set; } +} + +public sealed class RequestOptions +{ + [JsonPropertyName("Scope")] + public Scope? Scope { get; set; } + + [JsonPropertyName("Interactive")] + public bool Interactive { get; set; } + + [JsonPropertyName("SkipHashCheck")] + public bool SkipHashCheck { get; set; } + + [JsonPropertyName("PreRelease")] + public bool PreRelease { get; set; } + + [JsonPropertyName("CustomInstallLocation")] + public string? CustomInstallLocation { get; set; } + + [JsonPropertyName("CustomParameters")] + public List CustomParameters { get; set; } = []; + + [JsonPropertyName("PreOperationCommand")] + public string? PreOperationCommand { get; set; } + + [JsonPropertyName("PostOperationCommand")] + public string? PostOperationCommand { get; set; } + + [JsonPropertyName("KillBeforeOperation")] + public List KillBeforeOperation { get; set; } = []; + + [JsonPropertyName("UninstallPrevious")] + public bool UninstallPrevious { get; set; } + + [JsonPropertyName("NoUpgrade")] + public bool NoUpgrade { get; set; } +} + +public sealed class BrokerContext +{ + [JsonPropertyName("RequestedElevation")] + public Elevation RequestedElevation { get; set; } + + [JsonPropertyName("EffectiveUser")] + public string EffectiveUser { get; set; } = ""; + + [JsonPropertyName("ClientVersion")] + public string? ClientVersion { get; set; } + + [JsonPropertyName("ClientProcessPath")] + public string? ClientProcessPath { get; set; } +} diff --git a/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client/ResponseModels.cs b/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client/ResponseModels.cs new file mode 100644 index 000000000..95fefb08a --- /dev/null +++ b/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client/ResponseModels.cs @@ -0,0 +1,103 @@ +using System.Text.Json.Serialization; + +namespace Devolutions.UniGetUI.Broker.Client; + +/// Canonical response returned by the broker after evaluating a request. +public sealed class BrokerResponse +{ + [JsonPropertyName("$schema")] + public string Schema { get; set; } = SchemaUris.Response; + + [JsonPropertyName("ResponseVersion")] + public string ResponseVersion { get; set; } = "1.0.0"; + + [JsonPropertyName("ResponseType")] + public string ResponseType { get; set; } = "PackageBrokerResponse"; + + [JsonPropertyName("Broker")] + public BrokerInfo Broker { get; set; } = new(); + + [JsonPropertyName("AuditId")] + public string AuditId { get; set; } = ""; + + [JsonPropertyName("RequestId")] + public string RequestId { get; set; } = ""; + + [JsonPropertyName("ReceivedAt")] + public DateTimeOffset ReceivedAt { get; set; } + + [JsonPropertyName("CompletedAt")] + public DateTimeOffset CompletedAt { get; set; } + + [JsonPropertyName("Manager")] + public string? Manager { get; set; } + + [JsonPropertyName("Source")] + public string? Source { get; set; } + + [JsonPropertyName("PackageId")] + public string? PackageId { get; set; } + + [JsonPropertyName("Operation")] + public Operation? Operation { get; set; } + + [JsonPropertyName("Decision")] + public Decision Decision { get; set; } + + [JsonPropertyName("RuleId")] + public string RuleId { get; set; } = ""; + + [JsonPropertyName("Reason")] + public string Reason { get; set; } = ""; + + [JsonPropertyName("WouldExecute")] + public bool WouldExecute { get; set; } + + [JsonPropertyName("Policy")] + public ResponsePolicyInfo Policy { get; set; } = new(); + + [JsonPropertyName("Execution")] + public ExecutionInfo Execution { get; set; } = new(); +} + +public sealed class BrokerInfo +{ + [JsonPropertyName("Name")] + public string Name { get; set; } = ""; + + [JsonPropertyName("ProtocolVersion")] + public string ProtocolVersion { get; set; } = "1.0"; + + [JsonPropertyName("Transport")] + public Transport Transport { get; set; } + + [JsonPropertyName("PipeName")] + public string? PipeName { get; set; } + + [JsonPropertyName("ElevatedSimulation")] + public bool ElevatedSimulation { get; set; } +} + +public sealed class ResponsePolicyInfo +{ + [JsonPropertyName("Id")] + public string Id { get; set; } = ""; + + [JsonPropertyName("Revision")] + public int Revision { get; set; } + + [JsonPropertyName("PolicyVersion")] + public string PolicyVersion { get; set; } = "1.0.0"; +} + +public sealed class ExecutionInfo +{ + [JsonPropertyName("Mode")] + public ExecutionMode Mode { get; set; } + + [JsonPropertyName("Command")] + public List Command { get; set; } = []; + + [JsonPropertyName("Note")] + public string Note { get; set; } = ""; +} diff --git a/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client/StatusModels.cs b/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client/StatusModels.cs new file mode 100644 index 000000000..c903b40b0 --- /dev/null +++ b/crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client/StatusModels.cs @@ -0,0 +1,56 @@ +using System.Text.Json.Serialization; + +namespace Devolutions.UniGetUI.Broker.Client; + +/// Request to query the status of a previously submitted package operation. +public sealed class StatusRequest +{ + [JsonPropertyName("$schema")] + public string Schema { get; set; } = SchemaUris.StatusRequest; + + [JsonPropertyName("RequestVersion")] + public string RequestVersion { get; set; } = "1.0.0"; + + [JsonPropertyName("RequestType")] + public string RequestType { get; set; } = "PackageOperationStatus"; + + [JsonPropertyName("RequestId")] + public string RequestId { get; set; } = ""; + + [JsonPropertyName("Broker")] + public BrokerContext Broker { get; set; } = new(); +} + +/// Response to a status query. +public sealed class StatusResponse +{ + [JsonPropertyName("$schema")] + public string Schema { get; set; } = SchemaUris.StatusResponse; + + [JsonPropertyName("ResponseVersion")] + public string ResponseVersion { get; set; } = "1.0.0"; + + [JsonPropertyName("ResponseType")] + public string ResponseType { get; set; } = "PackageOperationStatusResponse"; + + [JsonPropertyName("Broker")] + public BrokerInfo Broker { get; set; } = new(); + + [JsonPropertyName("RequestId")] + public string RequestId { get; set; } = ""; + + [JsonPropertyName("Status")] + public OperationStatus Status { get; set; } + + [JsonPropertyName("StartedAt")] + public DateTimeOffset? StartedAt { get; set; } + + [JsonPropertyName("CompletedAt")] + public DateTimeOffset? CompletedAt { get; set; } + + [JsonPropertyName("ExitCode")] + public int? ExitCode { get; set; } + + [JsonPropertyName("Note")] + public string? Note { get; set; } +} diff --git a/crates/unigetui-broker/dotnet/README.md b/crates/unigetui-broker/dotnet/README.md new file mode 100644 index 000000000..469438ac7 --- /dev/null +++ b/crates/unigetui-broker/dotnet/README.md @@ -0,0 +1,31 @@ +# Devolutions.UniGetUI.Broker.Client + +Hand-written C# client and DTOs for the Devolutions Agent UniGetUI package broker. + +This is a deliberate middle-ground: rather than generating the client from the +OpenAPI document, the DTOs and the named-pipe HTTP client are written by hand +(idiomatic, with strongly-typed enums), and **validated against the exact same JSON +Schemas and sample documents the Rust crate uses** — no copied fixtures, no drift. + +## Layout + +- `Devolutions.UniGetUI.Broker.Client/` — the library: + - DTOs for the request, response, status, and policy documents (PascalCase wire format). + - Enums (`Operation`, `Decision`, `OperationStatus`, …) that serialize to the exact + wire values, so policy/status comparisons are type-checked rather than stringly-typed. + - `BrokerClient` — HTTP/1.1 over a Windows named pipe (evaluate / execute / poll status). +- `Devolutions.UniGetUI.Broker.Client.Tests/` — xunit parity tests that load + `../../schema/*.json` and `../../assets/samples/**` directly and assert: + - every sample round-trips through the DTOs under strict unmapped-member handling + (so a missing DTO field fails the test — the mirror of Rust's `deny_unknown_fields`); + - re-serialized output validates against the JSON Schema (via `NJsonSchema`); + - intentionally-invalid fixtures are rejected. + +## Build & test + +```powershell +dotnet test crates/unigetui-broker/dotnet/Devolutions.UniGetUI.Broker.Client.slnx +``` + +The OpenAPI document (`../openapi/unigetui-broker-api.yaml`) is kept as an API reference +but is not used to generate this client. diff --git a/crates/unigetui-broker/dotnet/build.ps1 b/crates/unigetui-broker/dotnet/build.ps1 new file mode 100644 index 000000000..e1c97ebfe --- /dev/null +++ b/crates/unigetui-broker/dotnet/build.ps1 @@ -0,0 +1,45 @@ +#!/bin/env pwsh + +# Builds and packs the broker client, substituting a date-based version into the +# csproj before packaging (mirrors the now-proto NuGet release flow). The version +# can be overridden with -Version or the PACKAGE_VERSION environment variable. + +[CmdletBinding()] +param( + [string]$Version = $env:PACKAGE_VERSION +) + +$ErrorActionPreference = "Stop" + +Push-Location -Path $PSScriptRoot + +try { + $Csproj = "./Devolutions.UniGetUI.Broker.Client/Devolutions.UniGetUI.Broker.Client.csproj" + + if ([string]::IsNullOrEmpty($Version) -or $Version -eq 'latest') { + $Version = (Get-Date -Format "yyyy.MM.dd") + ".0" + } + + if ($Version -NotMatch '^\d+\.\d+\.\d+\.\d+$') { + throw "invalid version format: $Version, expected: 1.2.3.4" + } + + Write-Host "Packaging Devolutions.UniGetUI.Broker.Client $Version" + + # Substitute the placeholder before packing, then restore it so the + # working tree stays clean (CI checkouts are disposable; local runs are not). + $Original = Get-Content $Csproj -Raw + try { + $Patched = $Original -Replace '().*?()', "$Version" + Set-Content -Path $Csproj -Value $Patched -Encoding UTF8 -NoNewline + + dotnet build --configuration Release $Csproj + dotnet pack --configuration Release $Csproj + } + finally { + Set-Content -Path $Csproj -Value $Original -Encoding UTF8 -NoNewline + } +} +finally { + Pop-Location +} diff --git a/crates/unigetui-broker/openapi/README.md b/crates/unigetui-broker/openapi/README.md new file mode 100644 index 000000000..ffdc2dd45 --- /dev/null +++ b/crates/unigetui-broker/openapi/README.md @@ -0,0 +1,18 @@ +# UniGetUI Package Broker OpenAPI + +`unigetui-broker-api.yaml` is the OpenAPI 3.1 description of the broker's HTTP API +(served over a Windows named pipe). It is generated from the Rust types via +`aide` + `schemars` and also carries the `PolicyDocument` schema as a component. + +Regenerate it with the workspace OpenAPI tool: + +```powershell +../../../tools/generate-openapi/generate.ps1 +``` + +(or `cargo run -p generate-openapi -- unigetui-broker`). + +The document is kept as an API reference / schema artifact. The C# client is +hand-written in `../dotnet/` and validated against the same JSON Schemas +(`../schema/`) and sample documents (`../assets/samples/`) used by the Rust tests, +rather than generated from this spec. diff --git a/crates/unigetui-broker/openapi/unigetui-broker-api.yaml b/crates/unigetui-broker/openapi/unigetui-broker-api.yaml new file mode 100644 index 000000000..9217d1091 --- /dev/null +++ b/crates/unigetui-broker/openapi/unigetui-broker-api.yaml @@ -0,0 +1,1050 @@ +openapi: 3.1.0 +info: + title: UniGetUI Package Broker API + description: HTTP API exposed by the Devolutions Agent UniGetUI package broker over a Windows named pipe. + version: '1.0' +paths: + /v1/health: + get: + summary: Health check + description: Reports whether the broker is ready or paused (policy unavailable). + /v1/capabilities: + get: + summary: Broker capabilities + description: Lists supported transports, media types, managers, and operations. + /v1/package-operations/evaluate: + post: + summary: Evaluate a package operation (dry-run) + description: Evaluates a package request against the active policy without executing anything. + requestBody: + description: Canonical request sent by an unelevated UniGetUI process to the elevated broker. + content: + application/json: + schema: + $ref: '#/components/schemas/PackageRequest' + required: true + responses: + '200': + description: Canonical response returned by the broker after evaluating a request. + content: + application/json: + schema: + $ref: '#/components/schemas/BrokerResponse' + '403': + description: Canonical response returned by the broker after evaluating a request. + content: + application/json: + schema: + $ref: '#/components/schemas/BrokerResponse' + /v1/package-operations: + post: + summary: Evaluate and execute a package operation + description: Evaluates a package request and, if allowed, submits it for elevated background execution. Poll the status endpoint for the result. + requestBody: + description: Canonical request sent by an unelevated UniGetUI process to the elevated broker. + content: + application/json: + schema: + $ref: '#/components/schemas/PackageRequest' + required: true + responses: + '200': + description: Canonical response returned by the broker after evaluating a request. + content: + application/json: + schema: + $ref: '#/components/schemas/BrokerResponse' + '403': + description: Canonical response returned by the broker after evaluating a request. + content: + application/json: + schema: + $ref: '#/components/schemas/BrokerResponse' + /v1/package-operations/status: + post: + summary: Query operation status + description: Returns the current status of a previously submitted package operation. + requestBody: + description: Request to query the status of a previously submitted package operation. + content: + application/json: + schema: + $ref: '#/components/schemas/StatusRequest' + required: true + responses: + '200': + description: Response to a status query. + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '404': + description: Response to a status query. + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' +components: + schemas: + Architecture: + description: Target architecture. + type: string + enum: + - X86 + - X64 + - Arm64 + - Neutral + BrokerContext: + description: Broker context provided by the client. + type: object + required: + - EffectiveUser + - RequestedElevation + properties: + ClientProcessPath: + description: File path of the client process. + type: string + maxLength: 2048 + nullable: true + ClientVersion: + description: Version of the UniGetUI client. + type: string + maxLength: 128 + nullable: true + EffectiveUser: + description: Windows identity of the calling user. + type: string + maxLength: 256 + minLength: 1 + RequestedElevation: + description: Elevation level requested. + $ref: '#/components/schemas/Elevation' + additionalProperties: false + BrokerInfo: + description: Broker identity information in responses. + type: object + required: + - ElevatedSimulation + - Name + - ProtocolVersion + - Transport + properties: + ElevatedSimulation: + description: Whether the broker is running in simulated elevation mode. + type: boolean + Name: + description: Broker display name. + type: string + maxLength: 128 + minLength: 1 + PipeName: + description: Named pipe path (when transport is http-named-pipe). + type: string + maxLength: 256 + minLength: 1 + nullable: true + ProtocolVersion: + description: Protocol version (e.g., "1.0"). + $ref: '#/components/schemas/ProtocolVersion' + Transport: + description: Transport mechanism. + $ref: '#/components/schemas/Transport' + additionalProperties: false + BrokerResponse: + description: Canonical response returned by the broker after evaluating a request. + type: object + required: + - $schema + - AuditId + - Broker + - CompletedAt + - Decision + - Execution + - Policy + - Reason + - ReceivedAt + - RequestId + - ResponseType + - ResponseVersion + - RuleId + - WouldExecute + properties: + $schema: + description: Response schema URI constant. + $ref: '#/components/schemas/ResponseSchemaUri' + AuditId: + description: Server-generated audit identifier. + $ref: '#/components/schemas/ResourceId' + Broker: + description: Broker identity and capabilities. + $ref: '#/components/schemas/BrokerInfo' + CompletedAt: + description: UTC timestamp when broker completed evaluation (RFC 3339). + type: string + format: date-time + Decision: + description: The evaluation decision. + $ref: '#/components/schemas/Decision' + Execution: + description: Execution details. + $ref: '#/components/schemas/ExecutionInfo' + Manager: + description: Manager name from the request (null if not parsed). + type: string + maxLength: 256 + minLength: 1 + nullable: true + Operation: + description: Operation from the request (null if not parsed). + $ref: '#/components/schemas/Operation' + nullable: true + PackageId: + description: Package identifier from the request (null if not parsed). + $ref: '#/components/schemas/PackageIdentifier' + nullable: true + Policy: + description: Summary of the policy used. + $ref: '#/components/schemas/ResponsePolicyInfo' + Reason: + description: Human-readable reason for the decision. + type: string + maxLength: 2048 + minLength: 1 + ReceivedAt: + description: UTC timestamp when broker received the request (RFC 3339). + type: string + format: date-time + RequestId: + description: Echoed request id. + $ref: '#/components/schemas/ResourceId' + ResponseType: + description: Must be `"packageBrokerResponse"`. + $ref: '#/components/schemas/PackageBrokerResponse' + ResponseVersion: + description: Response syntax version (semver). + $ref: '#/components/schemas/SemanticVersion' + RuleId: + description: The rule that produced the decision. + $ref: '#/components/schemas/RuleId' + Source: + description: Source name from the request (null if not parsed). + type: string + maxLength: 256 + minLength: 1 + nullable: true + WouldExecute: + description: Whether the broker would execute a command for this decision. + type: boolean + additionalProperties: false + CommandString: + type: string + maxLength: 2048 + minLength: 1 + CustomParameterString: + type: string + maxLength: 512 + minLength: 1 + Decision: + description: Policy decision. + type: string + enum: + - Allow + - Deny + Elevation: + description: Requested elevation level. + type: string + enum: + - Standard + - Elevated + ExecutionInfo: + description: Execution outcome details. + type: object + required: + - Command + - Mode + - Note + properties: + Command: + description: Command that was or would be executed. + type: array + items: + $ref: '#/components/schemas/CommandString' + maxItems: 256 + Mode: + description: Execution mode. + $ref: '#/components/schemas/ExecutionMode' + Note: + description: Additional note about execution. + type: string + maxLength: 2048 + minLength: 1 + additionalProperties: false + ExecutionMode: + description: Execution mode. + type: string + enum: + - SimulatedElevated + - Elevated + ManagerName: + description: Supported package manager names. + type: string + enum: + - Winget + - PowerShell + - PowerShell7 + Operation: + description: Package operation type. + type: string + enum: + - Install + - Update + - Uninstall + OperationStatus: + description: Status of an asynchronous package operation. + oneOf: + - description: Process is being prepared/started. + type: string + enum: + - Starting + - description: Process is running. + type: string + enum: + - Running + - description: Process exited successfully (exit code 0). + type: string + enum: + - Completed + - description: Process failed (non-zero exit, timeout, or launch failure). + type: string + enum: + - Failed + PackageBrokerResponse: + type: string + enum: + - PackageBrokerResponse + PackageIdentifier: + type: string + maxLength: 256 + minLength: 1 + pattern: ^[^\/:*?"<>|\x01-\x1f]+$ + PackageOperation: + type: string + enum: + - PackageOperation + PackageOperationStatus: + type: string + enum: + - PackageOperationStatus + PackageOperationStatusResponse: + type: string + enum: + - PackageOperationStatusResponse + PackageRequest: + description: Canonical request sent by an unelevated UniGetUI process to the elevated broker. + type: object + required: + - $schema + - Broker + - CreatedAt + - Manager + - Operation + - Options + - Package + - RequestId + - RequestType + - RequestVersion + - Source + properties: + $schema: + description: Request schema URI constant. + $ref: '#/components/schemas/RequestSchemaUri' + Broker: + description: Broker context from the client. + $ref: '#/components/schemas/BrokerContext' + CreatedAt: + description: UTC timestamp when the client created the request (RFC 3339). + type: string + format: date-time + Manager: + description: Package manager information. + $ref: '#/components/schemas/RequestManager' + Operation: + description: The package operation to perform. + $ref: '#/components/schemas/Operation' + Options: + description: Operation options. + $ref: '#/components/schemas/RequestOptions' + Package: + description: Package information. + $ref: '#/components/schemas/RequestPackage' + RequestId: + description: Unique client-generated request id for audit correlation. + $ref: '#/components/schemas/ResourceId' + RequestType: + description: Must be `"packageOperation"`. + $ref: '#/components/schemas/PackageOperation' + RequestVersion: + description: The request syntax version (semver). + $ref: '#/components/schemas/SemanticVersion' + Source: + description: Source/repository information. + $ref: '#/components/schemas/RequestSource' + additionalProperties: false + ProcessName: + type: string + maxLength: 128 + minLength: 1 + ProtocolVersion: + type: string + pattern: ^[0-9]+\.[0-9]+$ + RequestManager: + description: Package manager metadata from the request. + type: object + required: + - DisplayName + - ExecutableFriendlyName + - Name + properties: + DisplayName: + description: Human-readable display name. + type: string + maxLength: 128 + minLength: 1 + ExecutableFriendlyName: + description: Friendly name of the executable. + type: string + maxLength: 128 + minLength: 1 + Name: + description: Package manager name. + $ref: '#/components/schemas/ManagerName' + additionalProperties: false + RequestOptions: + description: Options controlling the package operation. + type: object + required: + - Interactive + - PreRelease + - SkipHashCheck + properties: + CustomInstallLocation: + description: Custom install directory path. + type: string + maxLength: 2048 + nullable: true + CustomParameters: + description: Additional command-line parameters. + type: array + items: + $ref: '#/components/schemas/CustomParameterString' + maxItems: 64 + Interactive: + description: Run interactively (show installer UI). + type: boolean + KillBeforeOperation: + description: Processes to kill before running the operation. + type: array + items: + $ref: '#/components/schemas/ProcessName' + maxItems: 64 + NoUpgrade: + description: Whether to skip upgrade if an existing version is detected (for install operations). + default: false + type: boolean + PostOperationCommand: + description: Command to execute after the package operation. + type: string + maxLength: 2048 + nullable: true + PreOperationCommand: + description: Command to execute before the package operation. + type: string + maxLength: 2048 + nullable: true + PreRelease: + description: Allow pre-release versions. + type: boolean + Scope: + description: Installation scope. + $ref: '#/components/schemas/Scope' + nullable: true + SkipHashCheck: + description: Skip package hash verification. + type: boolean + UninstallPrevious: + description: Whether to uninstall previous version before installing update. + default: false + type: boolean + additionalProperties: false + RequestPackage: + description: Package information. + type: object + required: + - Id + - Name + properties: + Architecture: + description: Target architecture. + $ref: '#/components/schemas/Architecture' + nullable: true + Channel: + description: Release channel. + type: string + maxLength: 16 + minLength: 1 + nullable: true + Id: + description: Package identifier (e.g., "Publisher.Package" for WinGet). + $ref: '#/components/schemas/PackageIdentifier' + Name: + description: Human-readable package name. + type: string + maxLength: 256 + minLength: 1 + Version: + description: |- + Target version (for update/install operations). + + A lenient version string rather than strict SemVer: real package versions are frequently not SemVer (e.g. PowerShell modules use 4-part .NET versions like `5.6.0.0`, and some winget packages use 2-part or date-based versions). + $ref: '#/components/schemas/VersionString' + nullable: true + additionalProperties: false + RequestSchemaUri: + type: string + enum: + - https://aka.ms/unigetui/package-request.schema.1.0.json + RequestSource: + description: Package source/repository information. + type: object + required: + - Name + properties: + IsVirtualManager: + description: Whether this is a virtual manager (runs without a real CLI). + type: boolean + nullable: true + Name: + description: Source name. + type: string + maxLength: 128 + minLength: 1 + Url: + description: Optional source URL. + type: string + maxLength: 2048 + nullable: true + additionalProperties: false + ResourceId: + type: string + maxLength: 128 + pattern: ^[A-Za-z0-9][A-Za-z0-9._:\-]{0,127}$ + ResponsePolicyInfo: + description: Summary of policy used for the decision. + type: object + required: + - Id + - PolicyVersion + - Revision + properties: + Id: + description: Policy document identifier. + $ref: '#/components/schemas/ResourceId' + PolicyVersion: + description: Policy syntax version. + $ref: '#/components/schemas/SemanticVersion' + Revision: + description: Policy revision number. + type: integer + format: int32 + maximum: 2147483647.0 + minimum: 1.0 + additionalProperties: false + ResponseSchemaUri: + type: string + enum: + - https://aka.ms/unigetui/package-broker-response.schema.1.0.json + RuleId: + type: string + maxLength: 128 + pattern: ^(||[A-Za-z0-9][A-Za-z0-9._:\-]{0,127})$ + Scope: + description: Package installation scope. + type: string + enum: + - User + - Machine + SemanticVersion: + type: string + maxLength: 128 + pattern: ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$ + StatusRequest: + description: Request to query the status of a previously submitted package operation. + type: object + required: + - $schema + - Broker + - RequestId + - RequestType + - RequestVersion + properties: + $schema: + description: Status request schema URI constant. + $ref: '#/components/schemas/StatusRequestSchemaUri' + Broker: + description: Broker context from the client. + $ref: '#/components/schemas/BrokerContext' + RequestId: + description: The `requestId` of the original package operation to query. + $ref: '#/components/schemas/ResourceId' + RequestType: + description: Must be `"packageOperationStatus"`. + $ref: '#/components/schemas/PackageOperationStatus' + RequestVersion: + description: Request syntax version (semver). + $ref: '#/components/schemas/SemanticVersion' + additionalProperties: false + StatusRequestSchemaUri: + type: string + enum: + - https://aka.ms/unigetui/package-operation-status-request.schema.1.0.json + StatusResponse: + description: Response to a status query. + type: object + required: + - $schema + - Broker + - RequestId + - ResponseType + - ResponseVersion + - Status + properties: + $schema: + description: Status response schema URI constant. + $ref: '#/components/schemas/StatusResponseSchemaUri' + Broker: + description: Broker identity and capabilities. + $ref: '#/components/schemas/BrokerInfo' + CompletedAt: + description: UTC timestamp when the operation completed or failed (null if still running). + type: string + format: date-time + nullable: true + ExitCode: + description: Process exit code (present when status is `completed`, or `failed` due to non-zero exit). + type: integer + format: int32 + nullable: true + Note: + description: Human-readable note about the status. + type: string + maxLength: 2048 + nullable: true + RequestId: + description: The original request id being queried. + $ref: '#/components/schemas/ResourceId' + ResponseType: + description: Must be `"packageOperationStatusResponse"`. + $ref: '#/components/schemas/PackageOperationStatusResponse' + ResponseVersion: + description: Response syntax version (semver). + $ref: '#/components/schemas/SemanticVersion' + StartedAt: + description: UTC timestamp when the process was actually launched (null if not yet started). + type: string + format: date-time + nullable: true + Status: + description: Current status of the operation. + $ref: '#/components/schemas/OperationStatus' + additionalProperties: false + StatusResponseSchemaUri: + type: string + enum: + - https://aka.ms/unigetui/package-operation-status-response.schema.1.0.json + Transport: + description: Broker transport type. + type: string + enum: + - HttpNamedPipe + - HttpLoopbackSimulator + VersionString: + type: string + maxLength: 128 + minLength: 1 + PolicyDocument: + title: PolicyDocument + description: A policy document governing which package operations are allowed or denied. + type: object + required: + - $schema + - Enforcement + - Metadata + - PolicyType + - PolicyVersion + - Rules + properties: + $schema: + description: Policy schema URI constant. + allOf: + - $ref: '#/components/schemas/PolicySchemaUri' + Enforcement: + description: Enforcement configuration. + allOf: + - $ref: '#/components/schemas/PolicyEnforcement' + Metadata: + description: Policy metadata. + allOf: + - $ref: '#/components/schemas/PolicyMetadata' + PolicyType: + description: Must be `"packageBrokerPolicy"`. + allOf: + - $ref: '#/components/schemas/PackageBrokerPolicy' + PolicyVersion: + description: Policy syntax version (semver). + allOf: + - $ref: '#/components/schemas/SemanticVersion' + Rules: + description: Ordered list of policy rules (may be empty; enforcement defaults apply). + type: array + items: + $ref: '#/components/schemas/PolicyRule' + maxItems: 1024 + additionalProperties: false + HttpUrl: + type: string + maxLength: 2048 + pattern: ^([Hh][Tt][Tt][Pp][Ss]?)://.+$ + PackageBrokerPolicy: + type: string + enum: + - PackageBrokerPolicy + PolicyConstraints: + description: Constraints applied after a rule matches. + type: object + properties: + AllowCustomInstallLocation: + description: Allow custom install location. + type: boolean + AllowCustomParameters: + description: Allow custom parameters. + type: boolean + AllowInteractive: + description: Allow interactive mode. + type: boolean + AllowKillBeforeOperation: + description: Allow killing processes before operation. + type: boolean + AllowPrePostCommands: + description: Allow pre/post operation commands. + type: boolean + AllowPreRelease: + description: Allow pre-release versions. + type: boolean + AllowSkipHashCheck: + description: Allow skipping hash verification. + type: boolean + AllowUninstallPrevious: + description: Allow uninstalling previous version before installing update. + type: boolean + AllowUpgrade: + description: Allow skipping upgrade on install operations if an existing version is detected (for install operations). + type: boolean + AllowedCustomParameterPatterns: + description: Glob patterns for allowed custom parameters. + type: array + items: + $ref: '#/components/schemas/CustomParameterString' + maxItems: 128 + AllowedCustomParameters: + description: Exact allowed custom parameters. + type: array + items: + $ref: '#/components/schemas/CustomParameterString' + maxItems: 128 + AllowedInstallLocationPatterns: + description: Glob patterns for allowed install locations. + type: array + items: + $ref: '#/components/schemas/StringPattern' + maxItems: 64 + DeniedCustomParameters: + description: Denied custom parameters (deny takes precedence over allow). + type: array + items: + $ref: '#/components/schemas/CustomParameterString' + maxItems: 128 + additionalProperties: false + PolicyEnforcement: + description: Enforcement configuration. + type: object + required: + - DefaultDecision + - RulePrecedence + properties: + AuditMode: + description: When true, broker logs decisions but does not enforce. + type: boolean + nullable: true + DefaultDecision: + description: Decision when no rule matches. + allOf: + - $ref: '#/components/schemas/Decision' + RulePrecedence: + description: Rule precedence strategy (must be "PriorityThenDeny"). + allOf: + - $ref: '#/components/schemas/RulePrecedence' + additionalProperties: false + PolicyMatch: + description: Match criteria for a policy rule. All specified fields must match. At least one field must be present. + type: object + properties: + Architectures: + description: Allowed architectures. + type: array + items: + $ref: '#/components/schemas/Architecture' + maxItems: 5 + uniqueItems: true + Elevation: + description: Allowed elevation levels. + type: array + items: + $ref: '#/components/schemas/Elevation' + maxItems: 2 + uniqueItems: true + HasCustomInstallLocation: + description: Whether request has custom install location. + type: array + items: + type: boolean + maxItems: 2 + uniqueItems: true + HasCustomParameters: + description: Whether request has custom parameters. + type: array + items: + type: boolean + maxItems: 2 + uniqueItems: true + HasKillBeforeOperation: + description: Whether request has kill-before-operation entries. + type: array + items: + type: boolean + maxItems: 2 + uniqueItems: true + HasPrePostCommands: + description: Whether request has pre/post operation commands. + type: array + items: + type: boolean + maxItems: 2 + uniqueItems: true + HasUninstallPrevious: + description: Whether request has uninstall-previous flag set. + type: array + items: + type: boolean + uniqueItems: true + Interactive: + description: Allowed interactive values. + type: array + items: + type: boolean + maxItems: 2 + uniqueItems: true + Managers: + description: Allowed managers. + type: array + items: + $ref: '#/components/schemas/ManagerName' + maxItems: 16 + uniqueItems: true + Operations: + description: Allowed operations. + type: array + items: + $ref: '#/components/schemas/Operation' + maxItems: 3 + uniqueItems: true + PackageIdentifiers: + description: Package identifier patterns (wildcard). + type: array + items: + $ref: '#/components/schemas/StringPattern' + maxItems: 1024 + uniqueItems: true + PackageNames: + description: Package name patterns (wildcard). + type: array + items: + $ref: '#/components/schemas/StringPattern' + maxItems: 1024 + uniqueItems: true + PreRelease: + description: Allowed preRelease values. + type: array + items: + type: boolean + maxItems: 2 + uniqueItems: true + Scopes: + description: Allowed scopes. + type: array + items: + $ref: '#/components/schemas/Scope' + maxItems: 2 + uniqueItems: true + SkipHashCheck: + description: Allowed skipHashCheck values. + type: array + items: + type: boolean + maxItems: 2 + uniqueItems: true + Sources: + description: Source patterns (wildcard). + type: array + items: + $ref: '#/components/schemas/StringPattern' + maxItems: 128 + uniqueItems: true + VersionRange: + description: Semantic version range. + allOf: + - $ref: '#/components/schemas/VersionRange' + nullable: true + Versions: + description: Exact version list. + type: array + items: + $ref: '#/components/schemas/VersionString' + maxItems: 256 + uniqueItems: true + additionalProperties: false + PolicyMetadata: + description: Policy metadata. + type: object + required: + - Id + - PublishedAt + - Publisher + - Revision + properties: + Description: + description: Human-readable description. + type: string + maxLength: 512 + nullable: true + Id: + description: Unique policy identifier. + allOf: + - $ref: '#/components/schemas/ResourceId' + PublishedAt: + description: ISO 8601 publication timestamp (RFC 3339). + type: string + format: date-time + Publisher: + description: Organization that published the policy. + type: string + maxLength: 128 + minLength: 1 + Revision: + description: Monotonically increasing revision number. + type: integer + format: int32 + maximum: 2147483647.0 + minimum: 1.0 + SupportUrl: + description: URL for support or documentation. + allOf: + - $ref: '#/components/schemas/HttpUrl' + nullable: true + ValidFrom: + description: Policy becomes active at this time. + type: string + format: date-time + nullable: true + ValidUntil: + description: Policy expires at this time. + type: string + format: date-time + nullable: true + additionalProperties: false + PolicyRule: + description: A single policy rule. + type: object + required: + - Decision + - Id + - Match + - Priority + properties: + Constraints: + description: Additional constraints applied after matching. When absent, no constraints are enforced beyond the match criteria. + allOf: + - $ref: '#/components/schemas/PolicyConstraints' + nullable: true + Decision: + description: Decision if this rule matches. + allOf: + - $ref: '#/components/schemas/Decision' + Enabled: + description: Whether the rule is active. + default: true + type: boolean + Id: + description: Unique rule identifier. + allOf: + - $ref: '#/components/schemas/ResourceId' + Match: + description: Match criteria — request must satisfy all specified fields. At least one criterion must be present. + allOf: + - $ref: '#/components/schemas/PolicyMatch' + Priority: + description: Priority (lower = higher precedence). + type: integer + format: int32 + maximum: 2147483647.0 + minimum: 0.0 + Reason: + description: Reason reported to the client. + type: string + maxLength: 512 + nullable: true + additionalProperties: false + PolicySchemaUri: + type: string + enum: + - https://aka.ms/unigetui/package-policy.schema.1.0.json + RulePrecedence: + description: Rule precedence strategy — always PriorityThenDeny. + type: string + enum: + - PriorityThenDeny + StringPattern: + type: string + maxLength: 256 + minLength: 1 + VersionRange: + description: Semantic version range for matching. + type: object + properties: + IncludePrerelease: + description: Whether to include pre-release versions. + default: false + type: boolean + MaxVersion: + description: Maximum version (inclusive). + type: string + maxLength: 128 + minLength: 1 + nullable: true + MinVersion: + description: Minimum version (inclusive). + type: string + maxLength: 128 + minLength: 1 + nullable: true + additionalProperties: false + diff --git a/crates/unigetui-broker/schema/unigetui.package-broker-response.schema.json b/crates/unigetui-broker/schema/unigetui.package-broker-response.schema.json new file mode 100644 index 000000000..77aec20c1 --- /dev/null +++ b/crates/unigetui-broker/schema/unigetui.package-broker-response.schema.json @@ -0,0 +1,360 @@ +{ + "$id": "https://aka.ms/unigetui/package-broker-response.schema.1.0.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "definitions": { + "BrokerInfo": { + "additionalProperties": false, + "description": "Broker identity information in responses.", + "properties": { + "ElevatedSimulation": { + "description": "Whether the broker is running in simulated elevation mode.", + "type": "boolean" + }, + "Name": { + "description": "Broker display name.", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "PipeName": { + "description": "Named pipe path (when transport is http-named-pipe).", + "maxLength": 256, + "minLength": 1, + "type": [ + "string", + "null" + ] + }, + "ProtocolVersion": { + "allOf": [ + { + "$ref": "#/definitions/ProtocolVersion" + } + ], + "description": "Protocol version (e.g., \"1.0\")." + }, + "Transport": { + "allOf": [ + { + "$ref": "#/definitions/Transport" + } + ], + "description": "Transport mechanism." + } + }, + "required": [ + "ElevatedSimulation", + "Name", + "ProtocolVersion", + "Transport" + ], + "type": "object" + }, + "CommandString": { + "maxLength": 2048, + "minLength": 1, + "type": "string" + }, + "Decision": { + "description": "Policy decision.", + "enum": [ + "Allow", + "Deny" + ], + "type": "string" + }, + "ExecutionInfo": { + "additionalProperties": false, + "description": "Execution outcome details.", + "properties": { + "Command": { + "description": "Command that was or would be executed.", + "items": { + "$ref": "#/definitions/CommandString" + }, + "maxItems": 256, + "type": "array" + }, + "Mode": { + "allOf": [ + { + "$ref": "#/definitions/ExecutionMode" + } + ], + "description": "Execution mode." + }, + "Note": { + "description": "Additional note about execution.", + "maxLength": 2048, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "Command", + "Mode", + "Note" + ], + "type": "object" + }, + "ExecutionMode": { + "description": "Execution mode.", + "enum": [ + "SimulatedElevated", + "Elevated" + ], + "type": "string" + }, + "Operation": { + "description": "Package operation type.", + "enum": [ + "Install", + "Update", + "Uninstall" + ], + "type": "string" + }, + "PackageBrokerResponse": { + "enum": [ + "PackageBrokerResponse" + ], + "type": "string" + }, + "PackageIdentifier": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[^\\/:*?\"<>|\\x01-\\x1f]+$", + "type": "string" + }, + "ProtocolVersion": { + "pattern": "^[0-9]+\\.[0-9]+$", + "type": "string" + }, + "ResourceId": { + "maxLength": 128, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:\\-]{0,127}$", + "type": "string" + }, + "ResponsePolicyInfo": { + "additionalProperties": false, + "description": "Summary of policy used for the decision.", + "properties": { + "Id": { + "allOf": [ + { + "$ref": "#/definitions/ResourceId" + } + ], + "description": "Policy document identifier." + }, + "PolicyVersion": { + "allOf": [ + { + "$ref": "#/definitions/SemanticVersion" + } + ], + "description": "Policy syntax version." + }, + "Revision": { + "description": "Policy revision number.", + "format": "uint32", + "maximum": 2147483647.0, + "minimum": 1.0, + "type": "integer" + } + }, + "required": [ + "Id", + "PolicyVersion", + "Revision" + ], + "type": "object" + }, + "ResponseSchemaUri": { + "enum": [ + "https://aka.ms/unigetui/package-broker-response.schema.1.0.json" + ], + "type": "string" + }, + "RuleId": { + "maxLength": 128, + "pattern": "^(||[A-Za-z0-9][A-Za-z0-9._:\\-]{0,127})$", + "type": "string" + }, + "SemanticVersion": { + "maxLength": 128, + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$", + "type": "string" + }, + "Transport": { + "description": "Broker transport type.", + "enum": [ + "HttpNamedPipe", + "HttpLoopbackSimulator" + ], + "type": "string" + } + }, + "description": "Canonical response returned by the broker after evaluating a request.", + "properties": { + "$schema": { + "allOf": [ + { + "$ref": "#/definitions/ResponseSchemaUri" + } + ], + "description": "Response schema URI constant." + }, + "AuditId": { + "allOf": [ + { + "$ref": "#/definitions/ResourceId" + } + ], + "description": "Server-generated audit identifier." + }, + "Broker": { + "allOf": [ + { + "$ref": "#/definitions/BrokerInfo" + } + ], + "description": "Broker identity and capabilities." + }, + "CompletedAt": { + "description": "UTC timestamp when broker completed evaluation (RFC 3339).", + "format": "date-time", + "type": "string" + }, + "Decision": { + "allOf": [ + { + "$ref": "#/definitions/Decision" + } + ], + "description": "The evaluation decision." + }, + "Execution": { + "allOf": [ + { + "$ref": "#/definitions/ExecutionInfo" + } + ], + "description": "Execution details." + }, + "Manager": { + "description": "Manager name from the request (null if not parsed).", + "maxLength": 256, + "minLength": 1, + "type": [ + "string", + "null" + ] + }, + "Operation": { + "anyOf": [ + { + "$ref": "#/definitions/Operation" + }, + { + "type": "null" + } + ], + "description": "Operation from the request (null if not parsed)." + }, + "PackageId": { + "anyOf": [ + { + "$ref": "#/definitions/PackageIdentifier" + }, + { + "type": "null" + } + ], + "description": "Package identifier from the request (null if not parsed)." + }, + "Policy": { + "allOf": [ + { + "$ref": "#/definitions/ResponsePolicyInfo" + } + ], + "description": "Summary of the policy used." + }, + "Reason": { + "description": "Human-readable reason for the decision.", + "maxLength": 2048, + "minLength": 1, + "type": "string" + }, + "ReceivedAt": { + "description": "UTC timestamp when broker received the request (RFC 3339).", + "format": "date-time", + "type": "string" + }, + "RequestId": { + "allOf": [ + { + "$ref": "#/definitions/ResourceId" + } + ], + "description": "Echoed request id." + }, + "ResponseType": { + "allOf": [ + { + "$ref": "#/definitions/PackageBrokerResponse" + } + ], + "description": "Must be `\"packageBrokerResponse\"`." + }, + "ResponseVersion": { + "allOf": [ + { + "$ref": "#/definitions/SemanticVersion" + } + ], + "description": "Response syntax version (semver)." + }, + "RuleId": { + "allOf": [ + { + "$ref": "#/definitions/RuleId" + } + ], + "description": "The rule that produced the decision." + }, + "Source": { + "description": "Source name from the request (null if not parsed).", + "maxLength": 256, + "minLength": 1, + "type": [ + "string", + "null" + ] + }, + "WouldExecute": { + "description": "Whether the broker would execute a command for this decision.", + "type": "boolean" + } + }, + "required": [ + "$schema", + "AuditId", + "Broker", + "CompletedAt", + "Decision", + "Execution", + "Policy", + "Reason", + "ReceivedAt", + "RequestId", + "ResponseType", + "ResponseVersion", + "RuleId", + "WouldExecute" + ], + "title": "BrokerResponse", + "type": "object" +} \ No newline at end of file diff --git a/crates/unigetui-broker/schema/unigetui.package-operation-status-request.schema.json b/crates/unigetui-broker/schema/unigetui.package-operation-status-request.schema.json new file mode 100644 index 000000000..4f18a7de0 --- /dev/null +++ b/crates/unigetui-broker/schema/unigetui.package-operation-status-request.schema.json @@ -0,0 +1,130 @@ +{ + "$id": "https://aka.ms/unigetui/package-operation-status-request.schema.1.0.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "definitions": { + "BrokerContext": { + "additionalProperties": false, + "description": "Broker context provided by the client.", + "properties": { + "ClientProcessPath": { + "description": "File path of the client process.", + "maxLength": 2048, + "type": [ + "string", + "null" + ] + }, + "ClientVersion": { + "description": "Version of the UniGetUI client.", + "maxLength": 128, + "type": [ + "string", + "null" + ] + }, + "EffectiveUser": { + "description": "Windows identity of the calling user.", + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "RequestedElevation": { + "allOf": [ + { + "$ref": "#/definitions/Elevation" + } + ], + "description": "Elevation level requested." + } + }, + "required": [ + "EffectiveUser", + "RequestedElevation" + ], + "type": "object" + }, + "Elevation": { + "description": "Requested elevation level.", + "enum": [ + "Standard", + "Elevated" + ], + "type": "string" + }, + "PackageOperationStatus": { + "enum": [ + "PackageOperationStatus" + ], + "type": "string" + }, + "ResourceId": { + "maxLength": 128, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:\\-]{0,127}$", + "type": "string" + }, + "SemanticVersion": { + "maxLength": 128, + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$", + "type": "string" + }, + "StatusRequestSchemaUri": { + "enum": [ + "https://aka.ms/unigetui/package-operation-status-request.schema.1.0.json" + ], + "type": "string" + } + }, + "description": "Request to query the status of a previously submitted package operation.", + "properties": { + "$schema": { + "allOf": [ + { + "$ref": "#/definitions/StatusRequestSchemaUri" + } + ], + "description": "Status request schema URI constant." + }, + "Broker": { + "allOf": [ + { + "$ref": "#/definitions/BrokerContext" + } + ], + "description": "Broker context from the client." + }, + "RequestId": { + "allOf": [ + { + "$ref": "#/definitions/ResourceId" + } + ], + "description": "The `requestId` of the original package operation to query." + }, + "RequestType": { + "allOf": [ + { + "$ref": "#/definitions/PackageOperationStatus" + } + ], + "description": "Must be `\"packageOperationStatus\"`." + }, + "RequestVersion": { + "allOf": [ + { + "$ref": "#/definitions/SemanticVersion" + } + ], + "description": "Request syntax version (semver)." + } + }, + "required": [ + "$schema", + "Broker", + "RequestId", + "RequestType", + "RequestVersion" + ], + "title": "StatusRequest", + "type": "object" +} \ No newline at end of file diff --git a/crates/unigetui-broker/schema/unigetui.package-operation-status-response.schema.json b/crates/unigetui-broker/schema/unigetui.package-operation-status-response.schema.json new file mode 100644 index 000000000..3a7b0d5f5 --- /dev/null +++ b/crates/unigetui-broker/schema/unigetui.package-operation-status-response.schema.json @@ -0,0 +1,215 @@ +{ + "$id": "https://aka.ms/unigetui/package-operation-status-response.schema.1.0.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "definitions": { + "BrokerInfo": { + "additionalProperties": false, + "description": "Broker identity information in responses.", + "properties": { + "ElevatedSimulation": { + "description": "Whether the broker is running in simulated elevation mode.", + "type": "boolean" + }, + "Name": { + "description": "Broker display name.", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "PipeName": { + "description": "Named pipe path (when transport is http-named-pipe).", + "maxLength": 256, + "minLength": 1, + "type": [ + "string", + "null" + ] + }, + "ProtocolVersion": { + "allOf": [ + { + "$ref": "#/definitions/ProtocolVersion" + } + ], + "description": "Protocol version (e.g., \"1.0\")." + }, + "Transport": { + "allOf": [ + { + "$ref": "#/definitions/Transport" + } + ], + "description": "Transport mechanism." + } + }, + "required": [ + "ElevatedSimulation", + "Name", + "ProtocolVersion", + "Transport" + ], + "type": "object" + }, + "OperationStatus": { + "description": "Status of an asynchronous package operation.", + "oneOf": [ + { + "description": "Process is being prepared/started.", + "enum": [ + "Starting" + ], + "type": "string" + }, + { + "description": "Process is running.", + "enum": [ + "Running" + ], + "type": "string" + }, + { + "description": "Process exited successfully (exit code 0).", + "enum": [ + "Completed" + ], + "type": "string" + }, + { + "description": "Process failed (non-zero exit, timeout, or launch failure).", + "enum": [ + "Failed" + ], + "type": "string" + } + ] + }, + "PackageOperationStatusResponse": { + "enum": [ + "PackageOperationStatusResponse" + ], + "type": "string" + }, + "ProtocolVersion": { + "pattern": "^[0-9]+\\.[0-9]+$", + "type": "string" + }, + "ResourceId": { + "maxLength": 128, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:\\-]{0,127}$", + "type": "string" + }, + "SemanticVersion": { + "maxLength": 128, + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$", + "type": "string" + }, + "StatusResponseSchemaUri": { + "enum": [ + "https://aka.ms/unigetui/package-operation-status-response.schema.1.0.json" + ], + "type": "string" + }, + "Transport": { + "description": "Broker transport type.", + "enum": [ + "HttpNamedPipe", + "HttpLoopbackSimulator" + ], + "type": "string" + } + }, + "description": "Response to a status query.", + "properties": { + "$schema": { + "allOf": [ + { + "$ref": "#/definitions/StatusResponseSchemaUri" + } + ], + "description": "Status response schema URI constant." + }, + "Broker": { + "allOf": [ + { + "$ref": "#/definitions/BrokerInfo" + } + ], + "description": "Broker identity and capabilities." + }, + "CompletedAt": { + "description": "UTC timestamp when the operation completed or failed (null if still running).", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "ExitCode": { + "description": "Process exit code (present when status is `completed`, or `failed` due to non-zero exit).", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "Note": { + "description": "Human-readable note about the status.", + "maxLength": 2048, + "type": [ + "string", + "null" + ] + }, + "RequestId": { + "allOf": [ + { + "$ref": "#/definitions/ResourceId" + } + ], + "description": "The original request id being queried." + }, + "ResponseType": { + "allOf": [ + { + "$ref": "#/definitions/PackageOperationStatusResponse" + } + ], + "description": "Must be `\"packageOperationStatusResponse\"`." + }, + "ResponseVersion": { + "allOf": [ + { + "$ref": "#/definitions/SemanticVersion" + } + ], + "description": "Response syntax version (semver)." + }, + "StartedAt": { + "description": "UTC timestamp when the process was actually launched (null if not yet started).", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "Status": { + "allOf": [ + { + "$ref": "#/definitions/OperationStatus" + } + ], + "description": "Current status of the operation." + } + }, + "required": [ + "$schema", + "Broker", + "RequestId", + "ResponseType", + "ResponseVersion", + "Status" + ], + "title": "StatusResponse", + "type": "object" +} \ No newline at end of file diff --git a/crates/unigetui-broker/schema/unigetui.package-policy.schema.json b/crates/unigetui-broker/schema/unigetui.package-policy.schema.json new file mode 100644 index 000000000..0e687770d --- /dev/null +++ b/crates/unigetui-broker/schema/unigetui.package-policy.schema.json @@ -0,0 +1,620 @@ +{ + "$id": "https://aka.ms/unigetui/package-policy.schema.1.0.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "definitions": { + "Architecture": { + "description": "Target architecture.", + "enum": [ + "X86", + "X64", + "Arm64", + "Neutral" + ], + "type": "string" + }, + "CustomParameterString": { + "maxLength": 512, + "minLength": 1, + "type": "string" + }, + "Decision": { + "description": "Policy decision.", + "enum": [ + "Allow", + "Deny" + ], + "type": "string" + }, + "Elevation": { + "description": "Requested elevation level.", + "enum": [ + "Standard", + "Elevated" + ], + "type": "string" + }, + "HttpUrl": { + "maxLength": 2048, + "pattern": "^([Hh][Tt][Tt][Pp][Ss]?)://.+$", + "type": "string" + }, + "ManagerName": { + "description": "Supported package manager names.", + "enum": [ + "Winget", + "PowerShell", + "PowerShell7" + ], + "type": "string" + }, + "Operation": { + "description": "Package operation type.", + "enum": [ + "Install", + "Update", + "Uninstall" + ], + "type": "string" + }, + "PackageBrokerPolicy": { + "enum": [ + "PackageBrokerPolicy" + ], + "type": "string" + }, + "PolicyConstraints": { + "additionalProperties": false, + "description": "Constraints applied after a rule matches.", + "properties": { + "AllowCustomInstallLocation": { + "description": "Allow custom install location.", + "type": "boolean" + }, + "AllowCustomParameters": { + "description": "Allow custom parameters.", + "type": "boolean" + }, + "AllowInteractive": { + "description": "Allow interactive mode.", + "type": "boolean" + }, + "AllowKillBeforeOperation": { + "description": "Allow killing processes before operation.", + "type": "boolean" + }, + "AllowPrePostCommands": { + "description": "Allow pre/post operation commands.", + "type": "boolean" + }, + "AllowPreRelease": { + "description": "Allow pre-release versions.", + "type": "boolean" + }, + "AllowSkipHashCheck": { + "description": "Allow skipping hash verification.", + "type": "boolean" + }, + "AllowUninstallPrevious": { + "description": "Allow uninstalling previous version before installing update.", + "type": "boolean" + }, + "AllowUpgrade": { + "description": "Allow skipping upgrade on install operations if an existing version is detected (for install operations).", + "type": "boolean" + }, + "AllowedCustomParameterPatterns": { + "description": "Glob patterns for allowed custom parameters.", + "items": { + "$ref": "#/definitions/CustomParameterString" + }, + "maxItems": 128, + "type": "array" + }, + "AllowedCustomParameters": { + "description": "Exact allowed custom parameters.", + "items": { + "$ref": "#/definitions/CustomParameterString" + }, + "maxItems": 128, + "type": "array" + }, + "AllowedInstallLocationPatterns": { + "description": "Glob patterns for allowed install locations.", + "items": { + "$ref": "#/definitions/StringPattern" + }, + "maxItems": 64, + "type": "array" + }, + "DeniedCustomParameters": { + "description": "Denied custom parameters (deny takes precedence over allow).", + "items": { + "$ref": "#/definitions/CustomParameterString" + }, + "maxItems": 128, + "type": "array" + } + }, + "type": "object" + }, + "PolicyEnforcement": { + "additionalProperties": false, + "description": "Enforcement configuration.", + "properties": { + "AuditMode": { + "description": "When true, broker logs decisions but does not enforce.", + "type": [ + "boolean", + "null" + ] + }, + "DefaultDecision": { + "allOf": [ + { + "$ref": "#/definitions/Decision" + } + ], + "description": "Decision when no rule matches." + }, + "RulePrecedence": { + "allOf": [ + { + "$ref": "#/definitions/RulePrecedence" + } + ], + "description": "Rule precedence strategy (must be \"PriorityThenDeny\")." + } + }, + "required": [ + "DefaultDecision", + "RulePrecedence" + ], + "type": "object" + }, + "PolicyMatch": { + "additionalProperties": false, + "description": "Match criteria for a policy rule. All specified fields must match. At least one field must be present.", + "properties": { + "Architectures": { + "description": "Allowed architectures.", + "items": { + "$ref": "#/definitions/Architecture" + }, + "maxItems": 5, + "type": "array", + "uniqueItems": true + }, + "Elevation": { + "description": "Allowed elevation levels.", + "items": { + "$ref": "#/definitions/Elevation" + }, + "maxItems": 2, + "type": "array", + "uniqueItems": true + }, + "HasCustomInstallLocation": { + "description": "Whether request has custom install location.", + "items": { + "type": "boolean" + }, + "maxItems": 2, + "type": "array", + "uniqueItems": true + }, + "HasCustomParameters": { + "description": "Whether request has custom parameters.", + "items": { + "type": "boolean" + }, + "maxItems": 2, + "type": "array", + "uniqueItems": true + }, + "HasKillBeforeOperation": { + "description": "Whether request has kill-before-operation entries.", + "items": { + "type": "boolean" + }, + "maxItems": 2, + "type": "array", + "uniqueItems": true + }, + "HasPrePostCommands": { + "description": "Whether request has pre/post operation commands.", + "items": { + "type": "boolean" + }, + "maxItems": 2, + "type": "array", + "uniqueItems": true + }, + "HasUninstallPrevious": { + "description": "Whether request has uninstall-previous flag set.", + "items": { + "type": "boolean" + }, + "type": "array", + "uniqueItems": true + }, + "Interactive": { + "description": "Allowed interactive values.", + "items": { + "type": "boolean" + }, + "maxItems": 2, + "type": "array", + "uniqueItems": true + }, + "Managers": { + "description": "Allowed managers.", + "items": { + "$ref": "#/definitions/ManagerName" + }, + "maxItems": 16, + "type": "array", + "uniqueItems": true + }, + "Operations": { + "description": "Allowed operations.", + "items": { + "$ref": "#/definitions/Operation" + }, + "maxItems": 3, + "type": "array", + "uniqueItems": true + }, + "PackageIdentifiers": { + "description": "Package identifier patterns (wildcard).", + "items": { + "$ref": "#/definitions/StringPattern" + }, + "maxItems": 1024, + "type": "array", + "uniqueItems": true + }, + "PackageNames": { + "description": "Package name patterns (wildcard).", + "items": { + "$ref": "#/definitions/StringPattern" + }, + "maxItems": 1024, + "type": "array", + "uniqueItems": true + }, + "PreRelease": { + "description": "Allowed preRelease values.", + "items": { + "type": "boolean" + }, + "maxItems": 2, + "type": "array", + "uniqueItems": true + }, + "Scopes": { + "description": "Allowed scopes.", + "items": { + "$ref": "#/definitions/Scope" + }, + "maxItems": 2, + "type": "array", + "uniqueItems": true + }, + "SkipHashCheck": { + "description": "Allowed skipHashCheck values.", + "items": { + "type": "boolean" + }, + "maxItems": 2, + "type": "array", + "uniqueItems": true + }, + "Sources": { + "description": "Source patterns (wildcard).", + "items": { + "$ref": "#/definitions/StringPattern" + }, + "maxItems": 128, + "type": "array", + "uniqueItems": true + }, + "VersionRange": { + "anyOf": [ + { + "$ref": "#/definitions/VersionRange" + }, + { + "type": "null" + } + ], + "description": "Semantic version range." + }, + "Versions": { + "description": "Exact version list.", + "items": { + "$ref": "#/definitions/VersionString" + }, + "maxItems": 256, + "type": "array", + "uniqueItems": true + } + }, + "type": "object" + }, + "PolicyMetadata": { + "additionalProperties": false, + "description": "Policy metadata.", + "properties": { + "Description": { + "description": "Human-readable description.", + "maxLength": 512, + "type": [ + "string", + "null" + ] + }, + "Id": { + "allOf": [ + { + "$ref": "#/definitions/ResourceId" + } + ], + "description": "Unique policy identifier." + }, + "PublishedAt": { + "description": "ISO 8601 publication timestamp (RFC 3339).", + "format": "date-time", + "type": "string" + }, + "Publisher": { + "description": "Organization that published the policy.", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "Revision": { + "description": "Monotonically increasing revision number.", + "format": "uint32", + "maximum": 2147483647.0, + "minimum": 1.0, + "type": "integer" + }, + "SupportUrl": { + "anyOf": [ + { + "$ref": "#/definitions/HttpUrl" + }, + { + "type": "null" + } + ], + "description": "URL for support or documentation." + }, + "ValidFrom": { + "description": "Policy becomes active at this time.", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "ValidUntil": { + "description": "Policy expires at this time.", + "format": "date-time", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "Id", + "PublishedAt", + "Publisher", + "Revision" + ], + "type": "object" + }, + "PolicyRule": { + "additionalProperties": false, + "description": "A single policy rule.", + "properties": { + "Constraints": { + "anyOf": [ + { + "$ref": "#/definitions/PolicyConstraints" + }, + { + "type": "null" + } + ], + "description": "Additional constraints applied after matching. When absent, no constraints are enforced beyond the match criteria." + }, + "Decision": { + "allOf": [ + { + "$ref": "#/definitions/Decision" + } + ], + "description": "Decision if this rule matches." + }, + "Enabled": { + "default": true, + "description": "Whether the rule is active.", + "type": "boolean" + }, + "Id": { + "allOf": [ + { + "$ref": "#/definitions/ResourceId" + } + ], + "description": "Unique rule identifier." + }, + "Match": { + "allOf": [ + { + "$ref": "#/definitions/PolicyMatch" + } + ], + "description": "Match criteria — request must satisfy all specified fields. At least one criterion must be present." + }, + "Priority": { + "description": "Priority (lower = higher precedence).", + "format": "uint32", + "maximum": 2147483647.0, + "minimum": 0.0, + "type": "integer" + }, + "Reason": { + "description": "Reason reported to the client.", + "maxLength": 512, + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "Decision", + "Id", + "Match", + "Priority" + ], + "type": "object" + }, + "PolicySchemaUri": { + "enum": [ + "https://aka.ms/unigetui/package-policy.schema.1.0.json" + ], + "type": "string" + }, + "ResourceId": { + "maxLength": 128, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:\\-]{0,127}$", + "type": "string" + }, + "RulePrecedence": { + "description": "Rule precedence strategy — always PriorityThenDeny.", + "enum": [ + "PriorityThenDeny" + ], + "type": "string" + }, + "Scope": { + "description": "Package installation scope.", + "enum": [ + "User", + "Machine" + ], + "type": "string" + }, + "SemanticVersion": { + "maxLength": 128, + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$", + "type": "string" + }, + "StringPattern": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "VersionRange": { + "additionalProperties": false, + "description": "Semantic version range for matching.", + "properties": { + "IncludePrerelease": { + "default": false, + "description": "Whether to include pre-release versions.", + "type": "boolean" + }, + "MaxVersion": { + "description": "Maximum version (inclusive).", + "maxLength": 128, + "minLength": 1, + "type": [ + "string", + "null" + ] + }, + "MinVersion": { + "description": "Minimum version (inclusive).", + "maxLength": 128, + "minLength": 1, + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "VersionString": { + "maxLength": 128, + "minLength": 1, + "type": "string" + } + }, + "description": "A policy document governing which package operations are allowed or denied.", + "properties": { + "$schema": { + "allOf": [ + { + "$ref": "#/definitions/PolicySchemaUri" + } + ], + "description": "Policy schema URI constant." + }, + "Enforcement": { + "allOf": [ + { + "$ref": "#/definitions/PolicyEnforcement" + } + ], + "description": "Enforcement configuration." + }, + "Metadata": { + "allOf": [ + { + "$ref": "#/definitions/PolicyMetadata" + } + ], + "description": "Policy metadata." + }, + "PolicyType": { + "allOf": [ + { + "$ref": "#/definitions/PackageBrokerPolicy" + } + ], + "description": "Must be `\"packageBrokerPolicy\"`." + }, + "PolicyVersion": { + "allOf": [ + { + "$ref": "#/definitions/SemanticVersion" + } + ], + "description": "Policy syntax version (semver)." + }, + "Rules": { + "description": "Ordered list of policy rules (may be empty; enforcement defaults apply).", + "items": { + "$ref": "#/definitions/PolicyRule" + }, + "maxItems": 1024, + "type": "array" + } + }, + "required": [ + "$schema", + "Enforcement", + "Metadata", + "PolicyType", + "PolicyVersion", + "Rules" + ], + "title": "PolicyDocument", + "type": "object" +} \ No newline at end of file diff --git a/crates/unigetui-broker/schema/unigetui.package-request.schema.json b/crates/unigetui-broker/schema/unigetui.package-request.schema.json new file mode 100644 index 000000000..170a96b5f --- /dev/null +++ b/crates/unigetui-broker/schema/unigetui.package-request.schema.json @@ -0,0 +1,442 @@ +{ + "$id": "https://aka.ms/unigetui/package-request.schema.1.0.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "definitions": { + "Architecture": { + "description": "Target architecture.", + "enum": [ + "X86", + "X64", + "Arm64", + "Neutral" + ], + "type": "string" + }, + "BrokerContext": { + "additionalProperties": false, + "description": "Broker context provided by the client.", + "properties": { + "ClientProcessPath": { + "description": "File path of the client process.", + "maxLength": 2048, + "type": [ + "string", + "null" + ] + }, + "ClientVersion": { + "description": "Version of the UniGetUI client.", + "maxLength": 128, + "type": [ + "string", + "null" + ] + }, + "EffectiveUser": { + "description": "Windows identity of the calling user.", + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "RequestedElevation": { + "allOf": [ + { + "$ref": "#/definitions/Elevation" + } + ], + "description": "Elevation level requested." + } + }, + "required": [ + "EffectiveUser", + "RequestedElevation" + ], + "type": "object" + }, + "CustomParameterString": { + "maxLength": 512, + "minLength": 1, + "type": "string" + }, + "Elevation": { + "description": "Requested elevation level.", + "enum": [ + "Standard", + "Elevated" + ], + "type": "string" + }, + "ManagerName": { + "description": "Supported package manager names.", + "enum": [ + "Winget", + "PowerShell", + "PowerShell7" + ], + "type": "string" + }, + "Operation": { + "description": "Package operation type.", + "enum": [ + "Install", + "Update", + "Uninstall" + ], + "type": "string" + }, + "PackageIdentifier": { + "maxLength": 256, + "minLength": 1, + "pattern": "^[^\\/:*?\"<>|\\x01-\\x1f]+$", + "type": "string" + }, + "PackageOperation": { + "enum": [ + "PackageOperation" + ], + "type": "string" + }, + "ProcessName": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "RequestManager": { + "additionalProperties": false, + "description": "Package manager metadata from the request.", + "properties": { + "DisplayName": { + "description": "Human-readable display name.", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "ExecutableFriendlyName": { + "description": "Friendly name of the executable.", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "Name": { + "allOf": [ + { + "$ref": "#/definitions/ManagerName" + } + ], + "description": "Package manager name." + } + }, + "required": [ + "DisplayName", + "ExecutableFriendlyName", + "Name" + ], + "type": "object" + }, + "RequestOptions": { + "additionalProperties": false, + "description": "Options controlling the package operation.", + "properties": { + "CustomInstallLocation": { + "description": "Custom install directory path.", + "maxLength": 2048, + "type": [ + "string", + "null" + ] + }, + "CustomParameters": { + "description": "Additional command-line parameters.", + "items": { + "$ref": "#/definitions/CustomParameterString" + }, + "maxItems": 64, + "type": "array" + }, + "Interactive": { + "description": "Run interactively (show installer UI).", + "type": "boolean" + }, + "KillBeforeOperation": { + "description": "Processes to kill before running the operation.", + "items": { + "$ref": "#/definitions/ProcessName" + }, + "maxItems": 64, + "type": "array" + }, + "NoUpgrade": { + "default": false, + "description": "Whether to skip upgrade if an existing version is detected (for install operations).", + "type": "boolean" + }, + "PostOperationCommand": { + "description": "Command to execute after the package operation.", + "maxLength": 2048, + "type": [ + "string", + "null" + ] + }, + "PreOperationCommand": { + "description": "Command to execute before the package operation.", + "maxLength": 2048, + "type": [ + "string", + "null" + ] + }, + "PreRelease": { + "description": "Allow pre-release versions.", + "type": "boolean" + }, + "Scope": { + "anyOf": [ + { + "$ref": "#/definitions/Scope" + }, + { + "type": "null" + } + ], + "description": "Installation scope." + }, + "SkipHashCheck": { + "description": "Skip package hash verification.", + "type": "boolean" + }, + "UninstallPrevious": { + "default": false, + "description": "Whether to uninstall previous version before installing update.", + "type": "boolean" + } + }, + "required": [ + "Interactive", + "PreRelease", + "SkipHashCheck" + ], + "type": "object" + }, + "RequestPackage": { + "additionalProperties": false, + "description": "Package information.", + "properties": { + "Architecture": { + "anyOf": [ + { + "$ref": "#/definitions/Architecture" + }, + { + "type": "null" + } + ], + "description": "Target architecture." + }, + "Channel": { + "description": "Release channel.", + "maxLength": 16, + "minLength": 1, + "type": [ + "string", + "null" + ] + }, + "Id": { + "allOf": [ + { + "$ref": "#/definitions/PackageIdentifier" + } + ], + "description": "Package identifier (e.g., \"Publisher.Package\" for WinGet)." + }, + "Name": { + "description": "Human-readable package name.", + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "Version": { + "anyOf": [ + { + "$ref": "#/definitions/VersionString" + }, + { + "type": "null" + } + ], + "description": "Target version (for update/install operations).\n\nA lenient version string rather than strict SemVer: real package versions are frequently not SemVer (e.g. PowerShell modules use 4-part .NET versions like `5.6.0.0`, and some winget packages use 2-part or date-based versions)." + } + }, + "required": [ + "Id", + "Name" + ], + "type": "object" + }, + "RequestSchemaUri": { + "enum": [ + "https://aka.ms/unigetui/package-request.schema.1.0.json" + ], + "type": "string" + }, + "RequestSource": { + "additionalProperties": false, + "description": "Package source/repository information.", + "properties": { + "IsVirtualManager": { + "description": "Whether this is a virtual manager (runs without a real CLI).", + "type": [ + "boolean", + "null" + ] + }, + "Name": { + "description": "Source name.", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "Url": { + "description": "Optional source URL.", + "maxLength": 2048, + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "Name" + ], + "type": "object" + }, + "ResourceId": { + "maxLength": 128, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:\\-]{0,127}$", + "type": "string" + }, + "Scope": { + "description": "Package installation scope.", + "enum": [ + "User", + "Machine" + ], + "type": "string" + }, + "SemanticVersion": { + "maxLength": 128, + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$", + "type": "string" + }, + "VersionString": { + "maxLength": 128, + "minLength": 1, + "type": "string" + } + }, + "description": "Canonical request sent by an unelevated UniGetUI process to the elevated broker.", + "properties": { + "$schema": { + "allOf": [ + { + "$ref": "#/definitions/RequestSchemaUri" + } + ], + "description": "Request schema URI constant." + }, + "Broker": { + "allOf": [ + { + "$ref": "#/definitions/BrokerContext" + } + ], + "description": "Broker context from the client." + }, + "CreatedAt": { + "description": "UTC timestamp when the client created the request (RFC 3339).", + "format": "date-time", + "type": "string" + }, + "Manager": { + "allOf": [ + { + "$ref": "#/definitions/RequestManager" + } + ], + "description": "Package manager information." + }, + "Operation": { + "allOf": [ + { + "$ref": "#/definitions/Operation" + } + ], + "description": "The package operation to perform." + }, + "Options": { + "allOf": [ + { + "$ref": "#/definitions/RequestOptions" + } + ], + "description": "Operation options." + }, + "Package": { + "allOf": [ + { + "$ref": "#/definitions/RequestPackage" + } + ], + "description": "Package information." + }, + "RequestId": { + "allOf": [ + { + "$ref": "#/definitions/ResourceId" + } + ], + "description": "Unique client-generated request id for audit correlation." + }, + "RequestType": { + "allOf": [ + { + "$ref": "#/definitions/PackageOperation" + } + ], + "description": "Must be `\"packageOperation\"`." + }, + "RequestVersion": { + "allOf": [ + { + "$ref": "#/definitions/SemanticVersion" + } + ], + "description": "The request syntax version (semver)." + }, + "Source": { + "allOf": [ + { + "$ref": "#/definitions/RequestSource" + } + ], + "description": "Source/repository information." + } + }, + "required": [ + "$schema", + "Broker", + "CreatedAt", + "Manager", + "Operation", + "Options", + "Package", + "RequestId", + "RequestType", + "RequestVersion", + "Source" + ], + "title": "PackageRequest", + "type": "object" +} \ No newline at end of file diff --git a/crates/unigetui-broker/src/Refactoring.md b/crates/unigetui-broker/src/Refactoring.md new file mode 100644 index 000000000..35304931d --- /dev/null +++ b/crates/unigetui-broker/src/Refactoring.md @@ -0,0 +1,11 @@ +### Refactoring +- I updated model the way I like it - moved some fileds, some were removed etc. Please update code/fix errors after theese changes. +- Add logic to watch and live reload policy (watch both json and yml). + -- If file failed to load or corrupted, pause stop the actual broker logic and wait while file becomes available again. If file was changed and successfully loaded - restart the broker logic with new policy. +- command_builder.rs is generic name, bit it implementes only winget-related logic. + -- I added some changes to winget command building logic, please try to preserve it (extend if needed, if something missing) + -- Add new command_builder module folder, and add mod.rs with generic methods/structs and separate winget.rs. If asked non-winget managers - return "not supported" response with a proper description. +- "const" fields like `schema` in json structures should be prefixed with "_" as they are unused members, and only relevant for serialization. +- Split models.rs into separate files for better readability and maintainability. module folder should be named "model". +- Please review README.md file under Unigetui repo/policies folder - there are still some missing pieces of feature implementation, most notably - YML support; rust should also search for *yml as alternative to *.json files, and accept both formats for policy file. (deserialize). +- Update uniget client side after changes on server side. diff --git a/crates/unigetui-broker/src/bin/standalone.rs b/crates/unigetui-broker/src/bin/standalone.rs new file mode 100644 index 000000000..5fff709b0 --- /dev/null +++ b/crates/unigetui-broker/src/bin/standalone.rs @@ -0,0 +1,221 @@ +//! Standalone UniGetUI broker server for testing. +//! +//! Runs the broker HTTP server on a named pipe (Windows) or TCP loopback (development). +//! By default operates in dry-run mode — builds WinGet commands but only logs them. +//! +//! Usage: +//! unigetui-broker-standalone [OPTIONS] +//! +//! Options: +//! --policy Path to policy JSON file (default: %PROGRAMDATA%/Devolutions/Agent/unigetui-policy.json) +//! --pipe Named pipe name (default: \\.\pipe\UniGetUI.PackageBroker.v1) +//! --tcp Also listen on TCP for development (e.g., 127.0.0.1:8765) +//! --execute Enable real command execution (default: dry-run) +//! --help Show this help + +// CLI binary legitimately uses stderr for user-facing messages. +#![allow(clippy::print_stderr)] + +use std::net::SocketAddr; +use std::path::PathBuf; +use std::sync::Arc; + +use tokio::sync::Notify; +use unigetui_broker::executor::{CommandExecutor, DryRunExecutor}; +use unigetui_broker::pipe::DEFAULT_PIPE_NAME; +use unigetui_broker::policy_loader; +use unigetui_broker::server::BrokerState; + +#[derive(Debug)] +struct Args { + policy_path: Option, + pipe_name: String, + tcp_addr: Option, + execute: bool, +} + +fn parse_args() -> Args { + let mut args = Args { + policy_path: None, + pipe_name: DEFAULT_PIPE_NAME.to_owned(), + tcp_addr: None, + execute: false, + }; + + let raw: Vec = std::env::args().collect(); + let mut i = 1; + while i < raw.len() { + match raw[i].as_str() { + "--policy" => { + i += 1; + if i < raw.len() { + args.policy_path = Some(PathBuf::from(&raw[i])); + } + } + "--pipe" => { + i += 1; + if i < raw.len() { + args.pipe_name = raw[i].clone(); + } + } + "--tcp" => { + i += 1; + if i < raw.len() { + args.tcp_addr = Some(raw[i].parse().expect("invalid TCP address")); + } + } + "--execute" => { + args.execute = true; + } + "--help" | "-h" => { + print_help(); + std::process::exit(0); + } + other => { + eprintln!("Unknown argument: {other}"); + print_help(); + std::process::exit(1); + } + } + i += 1; + } + + args +} + +fn print_help() { + eprintln!( + r#"UniGetUI Package Broker — Standalone Test Server + +Usage: unigetui-broker-standalone [OPTIONS] + +Options: + --policy Path to policy JSON file + (default: %PROGRAMDATA%/Devolutions/Agent/unigetui-policy.json) + --pipe Named pipe name + (default: \\.\pipe\UniGetUI.PackageBroker.v1) + --tcp Also listen on TCP for development (e.g., 127.0.0.1:8765) + --execute Enable real command execution (default: dry-run) + --help Show this help + +In dry-run mode (default), the broker evaluates policies and builds commands +but only logs them without executing. Use --execute to actually run WinGet. +"# + ); +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Initialize tracing. + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .init(); + + let args = parse_args(); + + // Load policy. + let policy_path = match args.policy_path { + Some(p) => p, + None => policy_loader::find_default_policy()?, + }; + let policy = policy_loader::load_policy(&policy_path)?; + + let executor: Box = if args.execute { + tracing::warn!("Running in EXECUTE mode — commands will be run!"); + #[cfg(windows)] + { + Box::new(unigetui_broker::executor::WindowsExecutor::new()) + } + #[cfg(not(windows))] + { + anyhow::bail!("execute mode is only supported on Windows"); + } + } else { + tracing::info!("Running in DRY-RUN mode — commands will only be logged"); + Box::new(DryRunExecutor) + }; + + let state = Arc::new(BrokerState { + policy: std::sync::RwLock::new(Some(Arc::new(policy))), + executor, + pipe_name: args.pipe_name.clone(), + tracker: unigetui_broker::operation_tracker::OperationTracker::new(), + }); + + let shutdown = Arc::new(Notify::new()); + + // Set up Ctrl+C handler. + let shutdown_signal = Arc::clone(&shutdown); + tokio::spawn(async move { + tokio::signal::ctrl_c().await.ok(); + tracing::info!("Received Ctrl+C, shutting down"); + shutdown_signal.notify_waiters(); + }); + + // Start TCP listener for development if requested. + if let Some(tcp_addr) = args.tcp_addr { + let tcp_state = Arc::clone(&state); + let tcp_shutdown = Arc::clone(&shutdown); + tokio::spawn(async move { + if let Err(error) = run_tcp_server(tcp_state, tcp_addr, tcp_shutdown).await { + tracing::error!(%error, "TCP server error"); + } + }); + } + + // Start named pipe server (Windows only). + #[cfg(windows)] + { + tracing::info!(pipe = %args.pipe_name, "Listening on named pipe"); + if let Some(addr) = args.tcp_addr { + tracing::info!(%addr, "Also listening on TCP"); + } + unigetui_broker::pipe::run_pipe_server(state, shutdown).await?; + } + + #[cfg(not(windows))] + { + if args.tcp_addr.is_some() { + tracing::info!("Named pipe not available on this platform, using TCP only"); + // Wait for shutdown. + shutdown.notified().await; + } else { + anyhow::bail!("named pipe is not available on this platform; use --tcp to listen on TCP"); + } + } + + Ok(()) +} + +async fn run_tcp_server(state: Arc, addr: SocketAddr, shutdown: Arc) -> anyhow::Result<()> { + let listener = tokio::net::TcpListener::bind(addr).await?; + tracing::info!(%addr, "TCP server listening"); + + // Build the axum router once; it is cheaply cloned per connection. + let router = unigetui_broker::server::build_router(state); + + loop { + tokio::select! { + result = listener.accept() => { + match result { + Ok((stream, peer)) => { + tracing::debug!(%peer, "TCP connection accepted"); + let router = router.clone(); + tokio::spawn(async move { + unigetui_broker::server::serve_connection(stream, router).await; + }); + } + Err(error) => { + tracing::error!(%error, "TCP accept error"); + } + } + } + _ = shutdown.notified() => { + return Ok(()); + } + } + } +} 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..625d7093b --- /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 winget; +pub mod powershell; + +use crate::model::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.name { + crate::model::ManagerName::Winget => winget::build_winget_command(request), + crate::model::ManagerName::PowerShell => powershell::build_powershell5_command(request), + crate::model::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..cb0ffc2d0 --- /dev/null +++ b/crates/unigetui-broker/src/command_builder/powershell.rs @@ -0,0 +1,289 @@ +//! 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