<# qorin-agent installer -- Windows. irm https://dl.qorin.dev/install.ps1 | iex Mirrors install.sh step for step so the two read the same: download, verify, place, set it to start with you, sign in. Where it differs from UNIX it is because Windows differs, and each of those places says so. THE ONE THING THIS SCRIPT MUST GET RIGHT (ADR-013, amendment 2026-08-05): the agent runs under YOUR account, never LocalSystem. On UNIX the agent runs as root and drops to the target user with setuid. Windows has no setuid -- a token there carries network identity too, so "become this user without proof" does not exist by design. Rather than impersonate, the agent simply IS you: your profile, your PATH, your %USERPROFILE%, and the credentials claude/codex/cursor/gemini/opencode already stored for you. LocalSystem would install perfectly and then look for those credentials in C:\Windows\System32\config\systemprofile. The agent would enrol, report healthy, and fail every spawn. `qorin-agent service install` refuses it. B-394 — E DA QUI NON SERVE PIU' L'AMMINISTRATORE. Non c'e' un servizio Windows da creare (e' un'attivita' pianificata sotto il tuo account, quindi niente password) e il binario non va piu' in Program Files ma nella tua cartella, cosi' l'agent puo' aggiornarsi da solo. Prima non poteva: girava non elevato e il suo binario stava dove serve l'amministratore per scrivere. #> #Requires -Version 5.1 [CmdletBinding()] param( [string] $DownloadBase = $(if ($env:QORIN_DOWNLOAD_BASE) { $env:QORIN_DOWNLOAD_BASE } else { 'https://dl.qorin.dev' }), # B-394 — NELLA TUA CARTELLA, non in Program Files. # # Program Files vuole l'amministratore per essere scritta, ma l'agent gira # come TE e non elevato (ADR-013, e la task e' registrata `-RunLevel # Limited`). Il risultato era che l'auto-aggiornamento non poteva riuscire: # il pulsante «Aggiorna» in app rispondeva sempre «permission denied». # Misurato dal founder sulla VM Parallels il 2026-09-07. # # `%LOCALAPPDATA%\Programs` e' dove si installano VS Code, Discord e la # maggior parte delle app moderne senza chiedere privilegi: l'agent scrive # nella propria cartella, e l'aggiornamento funziona come su Linux e Mac. [string] $InstallDir = $(if ($env:QORIN_INSTALL_DIR) { $env:QORIN_INSTALL_DIR } else { "$env:LOCALAPPDATA\Programs\Qorin" }), # The account the service will run as. Defaults to whoever is installing, # which is right almost always -- and wrong loudly, not quietly, when it # is not. [string] $Account = "$env:USERDOMAIN\$env:USERNAME" ) $ErrorActionPreference = 'Stop' function Say { param($m) Write-Host $m } function Dim { param($m) Write-Host $m -ForegroundColor DarkGray } function Step { param($n, $of, $m) Write-Host "[$n/$of] $m" -ForegroundColor Cyan } function Die { param([string[]] $Lines) Write-Host '' Write-Host $Lines[0] -ForegroundColor Red $Lines | Select-Object -Skip 1 | ForEach-Object { Dim " $_" } exit 1 } # ---------------------------------------------------------------- preflight if ([Environment]::OSVersion.Version.Build -lt 17763) { Die @( 'This build of Windows is too old for Qorin.', 'The agent gives each workspace a real terminal, and Windows only grew', 'the API for that (ConPTY) in Windows 10 1809 / build 17763.', "This machine reports build $([Environment]::OSVersion.Version.Build)." ) } # B-394 — QUI SI PRETENDEVA L'AMMINISTRATORE, e non serve piu'. # # La frase diceva «Creating a Windows service always needs them». Vero, ma # Qorin non crea piu' un servizio: registra un'attivita' pianificata sotto il # tuo account (`internal/svc/svc_windows.go`, `Register-ScheduledTask` con # `-LogonType Interactive`). Una task tua non chiede privilegi, e da B-394 # nemmeno la cartella di installazione. # # Restava quindi un'elevazione richiesta per una ragione che non esisteva piu', # e che in cambio rendeva impossibile l'auto-aggiornamento. # B-394b — E NON SI INSTALLA COME SYSTEM. # # (nota: questa guardia nasce da una prova vera. `prlctl exec` sulla VM # Parallels gira come NT AUTHORITY\SYSTEM, e lanciando l'installer da li' # l'installazione sarebbe finita in # C:\Windows\System32\config\systemprofile\AppData\Local — il profilo di # sistema, cioe' esattamente il posto che ADR-013 vieta — senza un errore.) # # Finche' l'installazione era in Program Files, l'account che eseguiva lo # script non cambiava DOVE finiva il binario. Ora sì: `%LOCALAPPDATA%` è il # profilo di CHI ESEGUE. Un'installazione fatta da SYSTEM, o da un account di # servizio, andrebbe in un profilo che nessuna persona apre mai — e la task # verrebbe registrata per quell'account, che non ha le credenziali dei CLI. # # Elevare con UAC il PROPRIO utente resta corretto: il profilo e' lo stesso. $io = [Security.Principal.WindowsIdentity]::GetCurrent() if ($io.IsSystem -or $io.Name -match '^NT AUTHORITY\\') { Die @( "This installer must run as the person who will use Qorin, not as $($io.Name).", 'The agent runs under your account and uses the logins that claude, codex,', 'cursor, gemini and opencode already stored in your profile. Installed from a', 'system account it would land in a profile nobody opens, and every workspace', 'would fail.', '', 'Open PowerShell as yourself and run it again.' ) } $arch = if ([Environment]::Is64BitOperatingSystem) { 'amd64' } else { '386' } if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { $arch = 'arm64' } $binaryName = "qorin-agent-windows-$arch.exe" $binaryUrl = "$DownloadBase/$binaryName" $installPath = Join-Path $InstallDir 'qorin-agent.exe' Say '' Say 'Qorin agent -- Windows' Dim " account $Account" Dim " install $installPath" Dim " startup scheduled task QorinAgent (starts when you log in)" Say '' $TOTAL = 4 # ------------------------------------------------------------------ 1. get Step 1 $TOTAL "Downloading $binaryName" $tmp = Join-Path ([IO.Path]::GetTempPath()) "qorin-agent-$([guid]::NewGuid()).exe" try { Invoke-WebRequest -Uri $binaryUrl -OutFile $tmp -UseBasicParsing -TimeoutSec 300 } catch { Die @( "Could not download $binaryUrl", $_.Exception.Message, '', 'If your network inspects TLS, the proxy may be rejecting the download.' ) } # Same posture as install.sh: verify when we can, say so plainly when we # cannot, and never claim a check that did not happen. Step 2 $TOTAL 'Verifying checksum' try { $sums = (Invoke-WebRequest -Uri "$DownloadBase/SHA256SUMS" -UseBasicParsing -TimeoutSec 60).Content $want = ($sums -split "`n" | Where-Object { $_ -match [regex]::Escape($binaryName) } | Select-Object -First 1) -split '\s+' | Select-Object -First 1 if ($want) { $got = (Get-FileHash -Path $tmp -Algorithm SHA256).Hash.ToLower() if ($got -ne $want.ToLower()) { Remove-Item $tmp -Force Die @( 'Checksum mismatch -- the download does not match what we published.', "expected $want", "got $got", 'Not installing. Try again; if it repeats, tell us at support@qorin.dev.' ) } Dim ' ok' } else { Dim " no entry for $binaryName in SHA256SUMS -- skipped" } } catch { Dim ' SHA256SUMS unreachable -- skipped (the download itself was over TLS)' } # ---------------------------------------------------------------- 3. place # B-394 — CHI ERA GIA' INSTALLATO IN PROGRAM FILES. # # Non la si tocca: rimuoverla vorrebbe dire l'amministratore, che questo script # non chiede piu'. Si dice che c'e' e come toglierla, e si va avanti — la task # viene riscritta e puntera' alla nuova, quindi la vecchia resta solo come file # fermo su disco. $vecchia = Join-Path "$env:ProgramFiles\Qorin" 'qorin-agent.exe' if ((Test-Path $vecchia) -and ($vecchia -ne $installPath)) { Say '' Say ' There is an older Qorin installed for all users:' -ForegroundColor Yellow Dim " $vecchia" Dim ' Qorin now installs in your own folder, so it can update itself' Dim ' without asking for Administrator. This new install replaces it.' Dim '' Dim ' To remove the old one, from PowerShell as Administrator:' Dim " Remove-Item -Recurse -Force '$env:ProgramFiles\Qorin'" Say '' } # B-394b — E LA VECCHIA ATTIVITA' NELLA RADICE. # # Fino a oggi la task si registrava in `\`, e li' Windows non lascia # rimuoverla senza un prompt elevato («Accesso negato», misurato). Quella # nuova vive in `\Qorin\` e convive: al prossimo accesso partirebbero DUE # agent, con la stessa identita' — che e' il conflitto che `svc.go` descrive, # «they share one identity and evict each other from the control plane in an # endless connect/disconnect loop». # # Non la si puo' togliere da qui, quindi si dice come, e si dice perche'. $vecchiaTask = Get-ScheduledTask -TaskName 'Qorin Agent' -TaskPath '\' -ErrorAction SilentlyContinue if ($vecchiaTask) { Say '' Say ' An older Qorin startup entry is still registered:' -ForegroundColor Yellow Dim ' Task Scheduler \ Qorin Agent' Dim ' Leave it and two agents will start at your next sign-in, fighting over' Dim ' the same identity. Removing it needs Administrator once:' Dim '' Dim " Unregister-ScheduledTask -TaskName 'Qorin Agent' -TaskPath '\' -Confirm:`$false" Say '' } Step 3 $TOTAL "Installing to $installPath" New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null # Stop first: Windows will not overwrite a running binary, and the failure it # gives ("being used by another process") non nomina cio' che lo tiene aperto. # B-394 — e cio' che lo tiene aperto e' un'attivita' pianificata, non un # servizio: `sc.exe stop QorinAgent` non fermava niente da quando l'agent ha # smesso di essere un servizio, e falliva in silenzio dentro un `*> $null`. # B-394b — il nome ha uno SPAZIO e la task vive in `\Qorin\`: cercarla come # 'QorinAgent' nella radice non trovava niente, e il comando falliva in # silenzio come faceva `sc.exe` prima di lui. Misurato sulla VM il 2026-09-08. Stop-ScheduledTask -TaskName 'Qorin Agent' -TaskPath '\Qorin\' -ErrorAction SilentlyContinue Get-Process -Name 'qorin-agent' -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue Start-Sleep -Milliseconds 500 Move-Item -Path $tmp -Destination $installPath -Force # B-394 — il PATH dell'UTENTE. Quello di macchina vuole l'amministratore, e # l'installazione ora e' tua: mettere la cartella nel PATH di tutti sarebbe # anche sbagliato, perche' il binario sta nel tuo profilo e gli altri account # non lo vedrebbero comunque. $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') if ($userPath -notlike "*$InstallDir*") { $nuovo = if ($userPath) { "$userPath;$InstallDir" } else { $InstallDir } [Environment]::SetEnvironmentVariable('Path', $nuovo, 'User') Dim ' added to your PATH (new terminals will see it)' } $version = (& $installPath --version 2>$null | Select-Object -First 1) if ($version) { Dim " $version" } # -------------------------------------------------------------- 4. service Step 4 $TOTAL 'Setting it to start when you log in' # B-394 — QUI SI CHIEDEVA LA PASSWORD DI WINDOWS, e non serviva piu'. # # Il passo precedente creava un servizio, e un servizio sotto il tuo account # Windows lo pretende una volta, alla creazione. Da quando l'agent registra # invece un'attivita' pianificata con `-LogonType Interactive`, quella password # non la chiede piu' nessuno: la variabile `QORIN_SERVICE_PASSWORD` che lo # script riempiva non e' letta da UNA SOLA riga di codice Go — verificato # cercandola in tutto il repo. # # Restavano venti righe che spiegavano con cura perche' fosse necessario dare # una password a un programma. Erano scritte bene e non erano piu' vere, il che # le rendeva peggio che inutili: chiedevano fiducia per una cosa che non # accadeva. # # L'attivita' parte al tuo accesso e gira finche' sei connesso. Non gira # quando sei disconnesso: e' il prezzo del non chiedere una password, ed e' # quello che il founder ha scelto in B-164 — «Qorin si installa sempre con # l'utente corrente e resta per lui». & $installPath service install if ($LASTEXITCODE -ne 0) { Die @( 'Could not register the scheduled task.', 'Run this to see what Windows says:', '', " $installPath service install" ) } Say '' Say 'Installed.' -ForegroundColor Green # B-394 — diceva «starts at boot and keeps running when you log out»: era vero # del servizio, non lo e' dell'attivita' pianificata che lo ha sostituito. Dim ' The agent starts when you log in, and runs while you are logged in.' Say '' Say ' Next, connect it to your Qorin account:' Say " qorin-agent login" -ForegroundColor Cyan Say '' Dim ' Status and logs:' Dim ' qorin-agent service status' Dim " $env:ProgramData\Qorin\agent.log" Say ''