Private/Start-ApplyWatcher.ps1

# Internal helper -- spawn the apply-watcher background process from
# @kagdaci/repo-switcher. Writes the watcher PID to disk for later health-check.

function Start-ApplyWatcher {
    param(
        [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$ApplyWatcherScript,
        [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$WatcherPidFile,
        [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$LogFile,
        [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$PwshPath,
        [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$RepoSwitcherRoot
    )
    if (-not (Test-Path $ApplyWatcherScript)) {
        Write-LogLine -Message "apply-watcher script missing at $ApplyWatcherScript -- Telegram apply will be disabled" -Level 'WARN' -LogFile $LogFile
        return
    }

    # [G4] security F5: defense in depth -- confirm the watcher script resolves
    # INSIDE the declared RepoSwitcherRoot (catches TOCTOU between Test-Path
    # and Start-Process where an attacker could replace apply-watcher.ps1 with
    # a malicious file outside the trusted dir via a symlink/junction).
    try {
        $canonScript = [System.IO.Path]::GetFullPath($ApplyWatcherScript)
        $canonRoot   = [System.IO.Path]::GetFullPath($RepoSwitcherRoot.TrimEnd('\').TrimEnd('/'))
    } catch {
        Write-LogLine -Message "apply-watcher path canonicalization failed: $($_.Exception.Message) -- aborting watcher spawn" -Level 'WARN' -LogFile $LogFile
        return
    }
    if (-not $canonScript.StartsWith($canonRoot + [IO.Path]::DirectorySeparatorChar, [System.StringComparison]::OrdinalIgnoreCase)) {
        Write-LogLine -Message "apply-watcher path '$canonScript' escapes RepoSwitcherRoot '$canonRoot' -- aborting watcher spawn" -Level 'WARN' -LogFile $LogFile
        return
    }

    # Use the explicit pwsh executable path inherited from the supervisor's own
    # process ([G4] devops T2-2). Falling back to PATH-resolved 'pwsh' would
    # break on multi-install systems where the watcher could pick up a different
    # PS7 (e.g. user-scoped vs system-scoped install).
    $proc = Start-Process $PwshPath `
        -ArgumentList '-NoProfile', '-WindowStyle', 'Hidden', '-File', $ApplyWatcherScript `
        -WindowStyle Hidden -PassThru
    Write-FileAtomic -Path $WatcherPidFile -Content $proc.Id
    Write-LogLine -Message "Spawned apply-watcher PID=$($proc.Id) via $PwshPath" -Level 'INFO' -LogFile $LogFile
}