From e87f1aea4959dc441c4469f56d432550b448d22e Mon Sep 17 00:00:00 2001 From: sunasrd-byte Date: Mon, 3 Aug 2026 11:44:29 +0200 Subject: [PATCH] Run child scripts under a Process-scoped Bypass execution policy On a fresh Windows machine the execution policy is Restricted. Piping this installer to iex sidesteps that, but scripts it invokes in turn do not get the same free pass: - uv's installer checks Get-ExecutionPolicy and refuses to run unless it reports Unrestricted, RemoteSigned or Bypass, so the install fails with "PowerShell requires an execution policy in [Unrestricted, RemoteSigned, Bypass] to run uv". - Invoke-SubScript executes walkthrough.ps1 and examples.ps1 from disk with & $path, which is blocked outright. - npm ships npx.ps1, which PowerShell prefers over npx.cmd, so `npx plain-forge install` is subject to the policy too. Add Invoke-WithBypassedExecutionPolicy, which sets Bypass for the Process scope only, runs the given script block, and restores the previous value in a finally block. Process scope applies to this process and its children, is discarded when the process exits, and leaves the user's CurrentUser and LocalMachine policies untouched, so the workaround of running Set-ExecutionPolicy -Scope CurrentUser RemoteSigned by hand is no longer needed. The helper is a no-op on non-Windows, where Set-ExecutionPolicy throws, and tolerates Group Policy lockdowns: when MachinePolicy or UserPolicy override the Process scope it reports the problem and still runs the block, letting the child script surface its own error. & $editor.Cmd is deliberately left unwrapped: cursor and code resolve to .cmd shims, which the execution policy does not apply to. Fixes #263 --- install/powershell/install.ps1 | 62 +++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/install/powershell/install.ps1 b/install/powershell/install.ps1 index 0ac18e80..a830d691 100644 --- a/install/powershell/install.ps1 +++ b/install/powershell/install.ps1 @@ -49,11 +49,61 @@ $env:NC = $NC if (-not $nonInteractive) { Clear-Host } Write-Host "Started ${YELLOW}${BOLD}*codeplain CLI${NC} installation..." +# Run a script block under a Process-scoped Bypass execution policy. +# +# On a fresh Windows machine the execution policy is Restricted. Piping this +# installer to iex sidesteps that, but scripts we invoke in turn do not get the +# same free pass: uv's installer refuses to run unless Get-ExecutionPolicy +# reports Unrestricted/RemoteSigned/Bypass, and any .ps1 we execute from disk +# (walkthrough, examples, the npx shim) is blocked outright. +# +# Process scope is the narrowest thing that works: it applies only to this +# PowerShell process and its children, is discarded when the process exits, and +# leaves the user's CurrentUser/LocalMachine policy untouched. We restore the +# previous process value afterwards so the relaxed policy lasts no longer than +# the call that needs it. +function Invoke-WithBypassedExecutionPolicy { + param([Parameter(Mandatory = $true)][scriptblock]$ScriptBlock) + + if (-not ($IsWindows -or ($env:OS -eq "Windows_NT"))) { + # Execution policy is a Windows-only concept; Set-ExecutionPolicy throws + # on other platforms. + return & $ScriptBlock + } + + $previousPolicy = $null + $policyChanged = $false + try { + $previousPolicy = Get-ExecutionPolicy -Scope Process + if ($previousPolicy -ne 'Bypass') { + Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force -ErrorAction Stop + $policyChanged = $true + } + } catch { + # Typically a Group Policy lockdown: the MachinePolicy/UserPolicy scopes + # win over Process, so there is nothing we can do here. Run anyway and + # let the child script surface its own error. + Write-Host "${GRAY}Could not relax the execution policy for this process: $($_.Exception.Message)${NC}" + } + + try { + & $ScriptBlock + } finally { + if ($policyChanged) { + try { + Set-ExecutionPolicy -Scope Process -ExecutionPolicy $previousPolicy -Force -ErrorAction Stop + } catch { + Write-Host "${GRAY}Could not restore this process's execution policy to '${previousPolicy}'.${NC}" + } + } + } +} + # Install uv if not present function Install-Uv { Write-Host "Installing uv package manager..." if ($IsWindows -or ($env:OS -eq "Windows_NT")) { - irm https://astral.sh/uv/install.ps1 | iex + Invoke-WithBypassedExecutionPolicy { irm https://astral.sh/uv/install.ps1 | iex } $env:Path = [Environment]::GetEnvironmentVariable('Path', 'User') + ';' + [Environment]::GetEnvironmentVariable('Path', 'Machine') } else { bash -c "curl -LsSf https://astral.sh/uv/install.sh | sh" @@ -305,15 +355,17 @@ function Invoke-SubScript { $scriptPath = ".\$ScriptName" } + # Both branches execute a .ps1 from disk, which the execution policy blocks + # under the default Restricted setting. if ($scriptPath) { # Run locally - & $scriptPath + Invoke-WithBypassedExecutionPolicy { & $scriptPath } } else { # Download and run $tempFile = Join-Path ([System.IO.Path]::GetTempPath()) $ScriptName Invoke-WebRequest -Uri "${env:CODEPLAIN_SCRIPTS_BASE_URL}/${ScriptName}" -OutFile $tempFile -UseBasicParsing try { - & $tempFile + Invoke-WithBypassedExecutionPolicy { & $tempFile } } finally { Remove-Item $tempFile -ErrorAction SilentlyContinue } @@ -348,7 +400,9 @@ if ($nonInteractive) { $plainForgeInstalled = $false if ($installPlainForge -notmatch '^[Nn]$') { if (Get-Command npx -ErrorAction SilentlyContinue) { - npx plain-forge install + # npm ships an npx.ps1 shim that PowerShell prefers over npx.cmd, so this + # call is subject to the execution policy too. + Invoke-WithBypassedExecutionPolicy { npx plain-forge install } $plainForgeInstalled = $true Write-Host "" } else {