From 1077b281c9bde7e7b89ca027ecbe3e86cb862f3a Mon Sep 17 00:00:00 2001 From: a-lberto Date: Sun, 18 Oct 2026 16:40:25 +0200 Subject: [PATCH 1/2] Add installBehavior.disableLicenseDisclaimer setting Adds a boolean setting, modeled on disableInstallNotes, that hides the license disclaimer shown before a package is installed. Defaults to false, so behavior is unchanged unless enabled. Related: #6598. --- doc/ReleaseNotes.md | 4 +++ doc/Settings.md | 10 +++++++ .../JSON/settings/settings.schema.0.2.json | 5 ++++ .../Workflows/InstallFlow.cpp | 2 +- src/AppInstallerCLITests/InstallFlow.cpp | 27 +++++++++++++++++++ src/AppInstallerCLITests/UserSettings.cpp | 21 +++++++++++++++ .../Public/winget/UserSettings.h | 2 ++ src/AppInstallerCommonCore/UserSettings.cpp | 1 + 8 files changed, 71 insertions(+), 1 deletion(-) diff --git a/doc/ReleaseNotes.md b/doc/ReleaseNotes.md index 151f337e5b..853b1b8c46 100644 --- a/doc/ReleaseNotes.md +++ b/doc/ReleaseNotes.md @@ -35,6 +35,10 @@ Added a new `--ignore-unavailable` flag to the `install` command. When installin Added the contract version 30 `InstallOptions.InstallDependenciesOnly` property for COM callers that need to install a package's dependencies without installing the requested package. +### `installBehavior.disableLicenseDisclaimer` setting + +Added a new `installBehavior.disableLicenseDisclaimer` setting that hides the license disclaimer ("This application is licensed to you by its owner. Microsoft is not responsible for, nor does it grant any licenses to, third-party packages.") shown before a package is installed. Defaults to `false`. + ## Bug Fixes ### Portable installer alias handling diff --git a/doc/Settings.md b/doc/Settings.md index 269a3d029e..267e351f03 100644 --- a/doc/Settings.md +++ b/doc/Settings.md @@ -88,6 +88,16 @@ The `disableInstallNotes` behavior affects whether installation notes are shown }, ``` +### Disable License Disclaimer + +The `disableLicenseDisclaimer` behavior affects whether the license disclaimer ("This application is licensed to you by its owner. Microsoft is not responsible for, nor does it grant any licenses to, third-party packages.") is shown before a package is installed. The notice shown for packages provided through Microsoft Store is not affected. Defaults to `false` if value is not set or is invalid. + +```json + "installBehavior": { + "disableLicenseDisclaimer": true + }, +``` + ### Portable Package User Root The `portablePackageUserRoot` setting affects the default root directory where packages are installed to under `User` scope. This setting only applies to packages with the `portable` installer type. Defaults to `%LOCALAPPDATA%/Microsoft/WinGet/Packages/` if value is not set or is invalid. diff --git a/schemas/JSON/settings/settings.schema.0.2.json b/schemas/JSON/settings/settings.schema.0.2.json index e162aaac12..8b1fa62416 100644 --- a/schemas/JSON/settings/settings.schema.0.2.json +++ b/schemas/JSON/settings/settings.schema.0.2.json @@ -204,6 +204,11 @@ "type": "boolean", "default": false }, + "disableLicenseDisclaimer": { + "description": "Controls whether the license disclaimer is shown before a package is installed", + "type": "boolean", + "default": false + }, "portablePackageUserRoot": { "description": "The default root directory where packages are installed to under User scope. Applies to the portable installer type.", "type": "string", diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp index f0eb5731a1..59b0f3ad00 100644 --- a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -364,7 +364,7 @@ namespace AppInstaller::CLI::Workflow { context.Reporter.Info() << Execution::PromptEmphasis << Resource::String::InstallationDisclaimerMSStore << std::endl; } - else + else if (!Settings::User().Get()) { context.Reporter.Info() << Resource::String::InstallationDisclaimer1 << std::endl << diff --git a/src/AppInstallerCLITests/InstallFlow.cpp b/src/AppInstallerCLITests/InstallFlow.cpp index f5eff1290c..32789b65e6 100644 --- a/src/AppInstallerCLITests/InstallFlow.cpp +++ b/src/AppInstallerCLITests/InstallFlow.cpp @@ -176,6 +176,33 @@ TEST_CASE("InstallFlowNonZeroExitCode", "[InstallFlow][workflow]") REQUIRE(installResultStr.find("/silentwithprogress") != std::string::npos); } +TEST_CASE("InstallFlow_LicenseDisclaimer", "[InstallFlow][workflow]") +{ + bool disableDisclaimer = GENERATE(false, true); + + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + OverrideForShellExecute(context); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_Exe.yaml").GetPath().u8string()); + + TestUserSettings settings; + settings.Set(bool{ disableDisclaimer }); + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + // The install must succeed either way; only the disclaimer changes. + REQUIRE(context.GetTerminationHR() == S_OK); + REQUIRE(std::filesystem::exists(installResultPath.GetPath())); + + bool disclaimerShown = installOutput.str().find(Resource::LocString(Resource::String::InstallationDisclaimer1).get()) != std::string::npos; + REQUIRE(disclaimerShown == !disableDisclaimer); +} + TEST_CASE("InstallFlow_InstallationNotes", "[InstallFlow][workflow]") { TestCommon::TempFile installResultPath("TestExeInstalled.txt"); diff --git a/src/AppInstallerCLITests/UserSettings.cpp b/src/AppInstallerCLITests/UserSettings.cpp index 8f9e31e150..37111ef4ee 100644 --- a/src/AppInstallerCLITests/UserSettings.cpp +++ b/src/AppInstallerCLITests/UserSettings.cpp @@ -681,6 +681,27 @@ TEST_CASE("SettingsMaxResumes", "[settings]") } } +TEST_CASE("SettingsDisableLicenseDisclaimer", "[settings]") +{ + auto again = DeleteUserSettingsFiles(); + + SECTION("Default value") + { + UserSettingsTest userSettingTest; + + REQUIRE_FALSE(userSettingTest.Get()); + } + + SECTION("Enabled") + { + std::string_view json = R"({ "installBehavior": { "disableLicenseDisclaimer": true } })"; + SetSetting(Stream::PrimaryUserSettings, json); + UserSettingsTest userSettingTest; + + REQUIRE(userSettingTest.Get()); + } +} + TEST_CASE("LoggingChannels", "[settings]") { auto again = DeleteUserSettingsFiles(); diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h index e000c6adda..2bb35ef06d 100644 --- a/src/AppInstallerCommonCore/Public/winget/UserSettings.h +++ b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -118,6 +118,7 @@ namespace AppInstaller::Settings InstallSkipDependencies, ArchiveExtractionMethod, DisableInstallNotes, + DisableLicenseDisclaimer, PortablePackageUserRoot, PortablePackageMachineRoot, MaxResumes, @@ -210,6 +211,7 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::InstallSkipDependencies, bool, bool, false, ".installBehavior.skipDependencies"sv); SETTINGMAPPING_SPECIALIZATION(Setting::ArchiveExtractionMethod, std::string, Archive::ExtractionMethod, Archive::ExtractionMethod::ShellApi, ".installBehavior.archiveExtractionMethod"sv); SETTINGMAPPING_SPECIALIZATION(Setting::DisableInstallNotes, bool, bool, false, ".installBehavior.disableInstallNotes"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::DisableLicenseDisclaimer, bool, bool, false, ".installBehavior.disableLicenseDisclaimer"sv); SETTINGMAPPING_SPECIALIZATION(Setting::PortablePackageUserRoot, std::string, std::filesystem::path, {}, ".installBehavior.portablePackageUserRoot"sv); SETTINGMAPPING_SPECIALIZATION(Setting::PortablePackageMachineRoot, std::string, std::filesystem::path, {}, ".installBehavior.portablePackageMachineRoot"sv); SETTINGMAPPING_SPECIALIZATION(Setting::InstallDefaultRoot, std::string, std::filesystem::path, {}, ".installBehavior.defaultInstallRoot"sv); diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp index 5f3074f0e8..0d47fa8fcd 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -293,6 +293,7 @@ namespace AppInstaller::Settings WINGET_VALIDATE_PASS_THROUGH(InteractivityDisable) WINGET_VALIDATE_PASS_THROUGH(InstallSkipDependencies) WINGET_VALIDATE_PASS_THROUGH(DisableInstallNotes) + WINGET_VALIDATE_PASS_THROUGH(DisableLicenseDisclaimer) WINGET_VALIDATE_PASS_THROUGH(UninstallPurgePortablePackage) WINGET_VALIDATE_PASS_THROUGH(NetworkWingetAlternateSourceURL) WINGET_VALIDATE_PASS_THROUGH(MaxResumes) From 04cf8f1f7f52ea22742177d3347e9e5d144b0752 Mon Sep 17 00:00:00 2001 From: a-lberto Date: Wed, 21 Oct 2026 23:51:17 +0200 Subject: [PATCH 2/2] Added the `EnableForceLicenseDisclaimer` Group Policy, which lets administrators require that the license disclaimer is always shown, overriding the `installBehavior.disableLicenseDisclaimer` user setting. --- doc/ReleaseNotes.md | 2 ++ doc/Settings.md | 2 ++ doc/admx/DesktopAppInstaller.admx | 10 ++++++++++ doc/admx/en-US/DesktopAppInstaller.adml | 8 ++++++++ .../Workflows/InstallFlow.cpp | 3 ++- .../Shared/Strings/en-us/winget.resw | 3 +++ src/AppInstallerCLITests/GroupPolicy.cpp | 16 ++++++++++++++++ src/AppInstallerCLITests/InstallFlow.cpp | 8 +++++++- src/AppInstallerCLITests/TestSettings.h | 1 + src/AppInstallerSharedLib/GroupPolicy.cpp | 2 ++ .../Public/winget/GroupPolicy.h | 1 + .../Public/winget/Resources.h | 1 + 12 files changed, 55 insertions(+), 2 deletions(-) diff --git a/doc/ReleaseNotes.md b/doc/ReleaseNotes.md index 853b1b8c46..c06acffd1e 100644 --- a/doc/ReleaseNotes.md +++ b/doc/ReleaseNotes.md @@ -39,6 +39,8 @@ Added the contract version 30 `InstallOptions.InstallDependenciesOnly` property Added a new `installBehavior.disableLicenseDisclaimer` setting that hides the license disclaimer ("This application is licensed to you by its owner. Microsoft is not responsible for, nor does it grant any licenses to, third-party packages.") shown before a package is installed. Defaults to `false`. +Added the `EnableForceLicenseDisclaimer` Group Policy, which lets administrators require that the license disclaimer is always shown, overriding the `installBehavior.disableLicenseDisclaimer` user setting. + ## Bug Fixes ### Portable installer alias handling diff --git a/doc/Settings.md b/doc/Settings.md index 267e351f03..7f1d75202a 100644 --- a/doc/Settings.md +++ b/doc/Settings.md @@ -98,6 +98,8 @@ The `disableLicenseDisclaimer` behavior affects whether the license disclaimer ( }, ``` +Administrators can prevent users from hiding the disclaimer by enabling the **Always show the Windows Package Manager license disclaimer** Group Policy (`EnableForceLicenseDisclaimer`). When this policy is enabled, the disclaimer is always shown and this setting is ignored. + ### Portable Package User Root The `portablePackageUserRoot` setting affects the default root directory where packages are installed to under `User` scope. This setting only applies to packages with the `portable` installer type. Defaults to `%LOCALAPPDATA%/Microsoft/WinGet/Packages/` if value is not set or is invalid. diff --git a/doc/admx/DesktopAppInstaller.admx b/doc/admx/DesktopAppInstaller.admx index b62d5bde1b..83d1db712c 100644 --- a/doc/admx/DesktopAppInstaller.admx +++ b/doc/admx/DesktopAppInstaller.admx @@ -73,6 +73,16 @@ + + + + + + + + + + diff --git a/doc/admx/en-US/DesktopAppInstaller.adml b/doc/admx/en-US/DesktopAppInstaller.adml index 2f8dff5150..ba0369c571 100644 --- a/doc/admx/en-US/DesktopAppInstaller.adml +++ b/doc/admx/en-US/DesktopAppInstaller.adml @@ -38,6 +38,14 @@ If you enable this policy, the Windows Package Manager will bypass the Microsoft If you disable this policy, the Windows Package Manager will validate the Microsoft Store certificate used is valid and belongs to the Microsoft Store before communicating with the Microsoft Store source. If you do not configure this policy, the Windows Package Manager administrator settings will be adhered to. + Always show the Windows Package Manager license disclaimer + This policy controls whether users can hide the license disclaimer that the Windows Package Manager shows before installing a package ("This application is licensed to you by its owner. Microsoft is not responsible for, nor does it grant any licenses to, third-party packages."). + +If you enable this policy, the license disclaimer is always shown before a package is installed. The installBehavior.disableLicenseDisclaimer setting in the user's settings file is ignored. + +If you disable or do not configure this policy, users can hide the license disclaimer by setting installBehavior.disableLicenseDisclaimer to true in their settings file. By default, the disclaimer is shown. + +This policy does not affect the notice shown for packages from the Microsoft Store. Enable Windows Package Manager Hash Override This policy controls whether or not the Windows Package Manager can be configured to enable the ability override the SHA256 security validation in settings. diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp index 59b0f3ad00..f644b94bdc 100644 --- a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -364,7 +364,8 @@ namespace AppInstaller::CLI::Workflow { context.Reporter.Info() << Execution::PromptEmphasis << Resource::String::InstallationDisclaimerMSStore << std::endl; } - else if (!Settings::User().Get()) + else if (!Settings::User().Get() || + Settings::GroupPolicies().IsEnabled(Settings::TogglePolicy::Policy::ForceLicenseDisclaimer)) { context.Reporter.Info() << Resource::String::InstallationDisclaimer1 << std::endl << diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index af04b9f319..3ee293b583 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -1026,6 +1026,9 @@ Configuration is disabled due to Group Policy. Enable Windows App Installer Hash Override + + Always show the Windows App Installer license disclaimer + Export current sources as JSON for Group Policy. diff --git a/src/AppInstallerCLITests/GroupPolicy.cpp b/src/AppInstallerCLITests/GroupPolicy.cpp index 0d4301d363..86df41715a 100644 --- a/src/AppInstallerCLITests/GroupPolicy.cpp +++ b/src/AppInstallerCLITests/GroupPolicy.cpp @@ -428,6 +428,21 @@ TEST_CASE("GroupPolicy_Toggle", "[groupPolicy]") REQUIRE(groupPolicy.GetState(TogglePolicy::Policy::DefaultSource) == PolicyState::NotConfigured); REQUIRE(groupPolicy.IsEnabled(TogglePolicy::Policy::DefaultSource)); } + + SECTION("Force license disclaimer is disabled when not configured") + { + GroupPolicy groupPolicy{ policiesKey.get() }; + REQUIRE(groupPolicy.GetState(TogglePolicy::Policy::ForceLicenseDisclaimer) == PolicyState::NotConfigured); + REQUIRE_FALSE(groupPolicy.IsEnabled(TogglePolicy::Policy::ForceLicenseDisclaimer)); + } + + SECTION("Force license disclaimer enabled") + { + SetRegistryValue(policiesKey.get(), ForceLicenseDisclaimerPolicyValueName, 1); + GroupPolicy groupPolicy{ policiesKey.get() }; + REQUIRE(groupPolicy.GetState(TogglePolicy::Policy::ForceLicenseDisclaimer) == PolicyState::Enabled); + REQUIRE(groupPolicy.IsEnabled(TogglePolicy::Policy::ForceLicenseDisclaimer)); + } } TEST_CASE("GroupPolicy_AllEnabled", "[groupPolicy]") @@ -450,6 +465,7 @@ TEST_CASE("GroupPolicy_AllEnabled", "[groupPolicy]") SetRegistryValue(policiesKey.get(), ProxyCommandLineOptionsPolicyValueName, 1); SetRegistryValue(policiesKey.get(), McpServerValueName, 1); SetRegistryValue(policiesKey.get(), ConfigurationProcessorPathValueName, 1); + SetRegistryValue(policiesKey.get(), ForceLicenseDisclaimerPolicyValueName, 1); GroupPolicy groupPolicy{ policiesKey.get() }; for (const auto& policy : TogglePolicy::GetAllPolicies()) diff --git a/src/AppInstallerCLITests/InstallFlow.cpp b/src/AppInstallerCLITests/InstallFlow.cpp index 32789b65e6..e51750bb57 100644 --- a/src/AppInstallerCLITests/InstallFlow.cpp +++ b/src/AppInstallerCLITests/InstallFlow.cpp @@ -179,6 +179,7 @@ TEST_CASE("InstallFlowNonZeroExitCode", "[InstallFlow][workflow]") TEST_CASE("InstallFlow_LicenseDisclaimer", "[InstallFlow][workflow]") { bool disableDisclaimer = GENERATE(false, true); + PolicyState forcePolicy = GENERATE(PolicyState::NotConfigured, PolicyState::Disabled, PolicyState::Enabled); TestCommon::TempFile installResultPath("TestExeInstalled.txt"); @@ -191,6 +192,9 @@ TEST_CASE("InstallFlow_LicenseDisclaimer", "[InstallFlow][workflow]") TestUserSettings settings; settings.Set(bool{ disableDisclaimer }); + GroupPolicyTestOverride policies; + policies.SetState(TogglePolicy::Policy::ForceLicenseDisclaimer, forcePolicy); + InstallCommand install({}); install.Execute(context); INFO(installOutput.str()); @@ -199,8 +203,10 @@ TEST_CASE("InstallFlow_LicenseDisclaimer", "[InstallFlow][workflow]") REQUIRE(context.GetTerminationHR() == S_OK); REQUIRE(std::filesystem::exists(installResultPath.GetPath())); + // Hidden only when the user hid it and the policy does not force it. + bool forced = forcePolicy == PolicyState::Enabled; bool disclaimerShown = installOutput.str().find(Resource::LocString(Resource::String::InstallationDisclaimer1).get()) != std::string::npos; - REQUIRE(disclaimerShown == !disableDisclaimer); + REQUIRE(disclaimerShown == (!disableDisclaimer || forced)); } TEST_CASE("InstallFlow_InstallationNotes", "[InstallFlow][workflow]") diff --git a/src/AppInstallerCLITests/TestSettings.h b/src/AppInstallerCLITests/TestSettings.h index 3ac150db05..c5098fdc37 100644 --- a/src/AppInstallerCLITests/TestSettings.h +++ b/src/AppInstallerCLITests/TestSettings.h @@ -27,6 +27,7 @@ namespace TestCommon const std::wstring ProxyCommandLineOptionsPolicyValueName = L"EnableWindowsPackageManagerProxyCommandLineOptions"; const std::wstring McpServerValueName = L"EnableWindowsPackageManagerMcpServer"; const std::wstring ConfigurationProcessorPathValueName = L"EnableWindowsPackageManagerConfigurationProcessorPath"; + const std::wstring ForceLicenseDisclaimerPolicyValueName = L"EnableForceLicenseDisclaimer"; const std::wstring SourceUpdateIntervalPolicyValueName = L"SourceAutoUpdateInterval"; const std::wstring SourceUpdateIntervalPolicyOldValueName = L"SourceAutoUpdateIntervalInMinutes"; diff --git a/src/AppInstallerSharedLib/GroupPolicy.cpp b/src/AppInstallerSharedLib/GroupPolicy.cpp index 19cf9321e4..872f2c1864 100644 --- a/src/AppInstallerSharedLib/GroupPolicy.cpp +++ b/src/AppInstallerSharedLib/GroupPolicy.cpp @@ -338,6 +338,8 @@ namespace AppInstaller::Settings return TogglePolicy(policy, "EnableWindowsPackageManagerMcpServer"sv, String::PolicyEnableMcpServer); case TogglePolicy::Policy::ConfigurationProcessorPath: return TogglePolicy(policy, "EnableWindowsPackageManagerConfigurationProcessorPath"sv, String::PolicyEnableConfigurationProcessorPath); + case TogglePolicy::Policy::ForceLicenseDisclaimer: + return TogglePolicy(policy, "EnableForceLicenseDisclaimer"sv, String::PolicyForceLicenseDisclaimer, false); default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerSharedLib/Public/winget/GroupPolicy.h b/src/AppInstallerSharedLib/Public/winget/GroupPolicy.h index 57d2315b9d..4f21a4a3e2 100644 --- a/src/AppInstallerSharedLib/Public/winget/GroupPolicy.h +++ b/src/AppInstallerSharedLib/Public/winget/GroupPolicy.h @@ -51,6 +51,7 @@ namespace AppInstaller::Settings ProxyCommandLineOptions, McpServer, ConfigurationProcessorPath, + ForceLicenseDisclaimer, Max, }; diff --git a/src/AppInstallerSharedLib/Public/winget/Resources.h b/src/AppInstallerSharedLib/Public/winget/Resources.h index 800ce9b399..a2d688be02 100644 --- a/src/AppInstallerSharedLib/Public/winget/Resources.h +++ b/src/AppInstallerSharedLib/Public/winget/Resources.h @@ -50,6 +50,7 @@ namespace AppInstaller WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableExperimentalFeatures); WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableLocalManifests); WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableHashOverride); + WINGET_DEFINE_RESOURCE_STRINGID(PolicyForceLicenseDisclaimer); WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableLocalArchiveMalwareScanOverride); WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableDefaultSource); WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableMSStoreSource);