From 0a9ce8c2cee62d6236054eee9cac5c41f512dc57 Mon Sep 17 00:00:00 2001 From: xiami762 <> Date: Tue, 12 May 2026 10:14:21 +0800 Subject: [PATCH] fix(windows-installer): require elevation for installer shortcuts Route installer-created desktop and Start menu launchers through an elevated PowerShell helper so Windows prompts for UAC before starting Flocks. This keeps the shared flocks.cmd wrapper as the real entrypoint while preventing updater failures caused by insufficient permissions. Co-authored-by: Cursor --- packaging/windows/flocks-setup.iss | 11 +++--- packaging/windows/start-flocks-elevated.ps1 | 16 +++++++++ .../test_browser_runtime_configuration.py | 36 ++++++++++++------- 3 files changed, 46 insertions(+), 17 deletions(-) create mode 100644 packaging/windows/start-flocks-elevated.ps1 diff --git a/packaging/windows/flocks-setup.iss b/packaging/windows/flocks-setup.iss index bccc5977..aa1643c6 100644 --- a/packaging/windows/flocks-setup.iss +++ b/packaging/windows/flocks-setup.iss @@ -49,13 +49,14 @@ Root: HKCU; Subkey: "Environment"; ValueType: string; ValueName: "FLOCKS_INSTALL Root: HKCU; Subkey: "Environment"; ValueType: string; ValueName: "FLOCKS_REPO_ROOT"; ValueData: "{app}\flocks"; Flags: uninsdeletevalue Root: HKCU; Subkey: "Environment"; ValueType: string; ValueName: "FLOCKS_NODE_HOME"; ValueData: "{app}\tools\node"; Flags: uninsdeletevalue -; Shortcuts intentionally target the same wrapper path that `scripts\install.ps1` -; writes, so the Start menu / desktop icon and `flocks start` typed in a new -; terminal are strictly equivalent across all install flows. +; Installer-created launch shortcuts intentionally go through a tiny elevation +; helper first, then invoke the same `%USERPROFILE%\.local\bin\flocks.cmd` +; wrapper that `scripts\install.ps1` writes. This keeps the app entrypoint +; consistent while letting Windows prompt for UAC on shortcut launches. [Icons] -Name: "{autoprograms}\{#MyAppName}\Start Flocks"; Filename: "{%USERPROFILE}\.local\bin\flocks.cmd"; Parameters: "start"; WorkingDir: "{%USERPROFILE}" +Name: "{autoprograms}\{#MyAppName}\Start Flocks"; Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File ""{app}\flocks\packaging\windows\start-flocks-elevated.ps1"""; WorkingDir: "{%USERPROFILE}" Name: "{autoprograms}\{#MyAppName}\Flocks repository"; Filename: "{app}\flocks"; WorkingDir: "{app}\flocks" -Name: "{userdesktop}\{#MyAppName}"; Filename: "{%USERPROFILE}\.local\bin\flocks.cmd"; Parameters: "start"; WorkingDir: "{%USERPROFILE}"; Tasks: desktopicon +Name: "{userdesktop}\{#MyAppName}"; Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File ""{app}\flocks\packaging\windows\start-flocks-elevated.ps1"""; WorkingDir: "{%USERPROFILE}"; Tasks: desktopicon [Run] Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\flocks\packaging\windows\bootstrap-windows.ps1"" -InstallRoot ""{app}"""; StatusMsg: "Setting up Python and JavaScript dependencies..."; Flags: runascurrentuser waituntilterminated diff --git a/packaging/windows/start-flocks-elevated.ps1 b/packaging/windows/start-flocks-elevated.ps1 new file mode 100644 index 00000000..2a7d5c14 --- /dev/null +++ b/packaging/windows/start-flocks-elevated.ps1 @@ -0,0 +1,16 @@ +[CmdletBinding()] +param() + +$wrapperPath = Join-Path $HOME ".local\bin\flocks.cmd" +if (-not (Test-Path -LiteralPath $wrapperPath)) { + throw "Flocks launcher not found: $wrapperPath" +} + +$cmdPath = $env:ComSpec +if ([string]::IsNullOrWhiteSpace($cmdPath)) { + $cmdPath = "cmd.exe" +} + +# Route installer-created shortcuts through UAC, but keep the real app entrypoint +# on the shared flocks.cmd wrapper so shortcut launches match terminal launches. +Start-Process -FilePath $cmdPath -ArgumentList @("/c", "`"$wrapperPath`" start") -WorkingDirectory $HOME -WindowStyle Hidden -Verb RunAs diff --git a/tests/scripts/test_browser_runtime_configuration.py b/tests/scripts/test_browser_runtime_configuration.py index 24fe4756..9b275fde 100644 --- a/tests/scripts/test_browser_runtime_configuration.py +++ b/tests/scripts/test_browser_runtime_configuration.py @@ -87,11 +87,9 @@ def test_inno_setup_points_to_packaging_bootstrap() -> None: assert "scripts\\bootstrap-windows.ps1" not in iss -def test_inno_shortcuts_point_to_user_local_bin_wrapper() -> None: - """Start-menu and desktop shortcuts must match the CLI wrapper location that - `scripts/install.ps1` writes, so `flocks start` triggered from the shortcut - and from a freshly opened terminal are strictly equivalent across all - install flows (source, one-liner, bundled installer).""" +def test_inno_shortcuts_point_to_elevated_launcher() -> None: + """Installer-created launch shortcuts should route through the elevated + launcher so clicking them triggers UAC before running the shared wrapper.""" iss = (PACKAGING_WINDOWS_DIR / "flocks-setup.iss").read_text(encoding="utf-8") icons_section_idx = iss.find("[Icons]") @@ -99,7 +97,8 @@ def test_inno_shortcuts_point_to_user_local_bin_wrapper() -> None: assert icons_section_idx != -1 and run_section_idx != -1 icons_block = iss[icons_section_idx:run_section_idx] - expected_target = "{%USERPROFILE}\\.local\\bin\\flocks.cmd" + expected_target = 'Filename: "powershell.exe"' + expected_script = 'start-flocks-elevated.ps1' start_menu_lines = [ line for line in icons_block.splitlines() @@ -108,14 +107,27 @@ def test_inno_shortcuts_point_to_user_local_bin_wrapper() -> None: assert start_menu_lines, "expected Start Flocks + desktop shortcut entries" for line in start_menu_lines: assert expected_target in line, ( - f"shortcut must target the shared wrapper path; got: {line}" + f"shortcut must target PowerShell launcher; got: {line}" ) - assert 'Parameters: "start"' in line + assert expected_script in line + assert "-WindowStyle Hidden" in line - # Guard against accidentally re-introducing a shortcut to {app}\bin, which - # would point to a non-existent file because install.ps1 writes the wrapper - # under %USERPROFILE%\.local\bin. - assert "{app}\\bin\\flocks.cmd" not in icons_block + # Guard against accidentally re-introducing direct shortcut launches that + # bypass the UAC prompt. + assert "{%USERPROFILE}\\.local\\bin\\flocks.cmd" not in icons_block + + +def test_windows_elevated_launcher_runs_shared_wrapper_as_admin() -> None: + """The elevation helper should re-use the shared CLI wrapper and request + Administrator rights via Start-Process.""" + script = (PACKAGING_WINDOWS_DIR / "start-flocks-elevated.ps1").read_text( + encoding="utf-8-sig" + ) + + assert 'Join-Path $HOME ".local\\bin\\flocks.cmd"' in script + assert "Start-Process" in script + assert "-Verb RunAs" in script + assert "`\"$wrapperPath`\" start" in script def test_inno_finish_page_reminds_user_to_reopen_terminal() -> None: