Something went wrong. Try again.
Personal fork of prime agent. Mirror of: https://mnemosyne.sigilzero.dev/sigilzero/prime-agent
Something went wrong. Try again.
weave install.ps1
51 kB · 928 lines
at atom/factory
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929# install.ps1 — the Windows-native installer for Prime Agent (Rust build).## THE ONE-LINER (the production endpoint serves the stable installer):## irm https://app.primeintellect.ai/prime-agent/install.ps1 | iex## The release pipeline's publish step renders this file to <base>/install.ps1# (the stable channel) and <base>/install-beta.ps1 (the beta channel), the# build's download base + channel stamped in.## The Git Bash route is the same channel through install-rust.sh (the sh# one-liner `curl -fsSL .../install.sh | sh` under Git Bash/MSYS2/Cygwin);# this script is the PowerShell-native form. Both routes publish the SAME# layout — $PREFIX\share\prime-agent\ (the payload) and $PREFIX\bin\# (the launcher pair) — so either route can update the other's install.## THE CHANNEL (install-rust.sh parity): the channel pointer (<base>/stable# or <base>/beta) gives the version, the channel manifest (<base>/latest.json# or <base>/beta.json) gives this platform's artifact row, and the versioned# release prefix serves the tarball plus its SHA256SUMS; the checksum is# verified before anything is published. Every file this installer reads# comes from the download base. CI may fetch this script from a GitHub ref# to verify changes before they reach the production installer endpoint.## CHANNEL SELECTION: the production installer uses stable. A missing# platform artifact is a release error, not permission to switch channels.# Beta remains available through an explicit channel selection.## NO TYPESCRIPT TAKEOVER STEPS: the TypeScript product never shipped a# Windows build, so there is no TS daemon, native install, or npm package# to retire here — this installer publishes the Rust payload and stops# this product's own daemon for the swap (a Windows process holds its# binary open; the payload swap needs the daemon down first).## Configuration (environment, install-rust.sh's own knobs):# PRIME_AGENT_DOWNLOAD_BASE_URL the R2-backed download base# PRIME_AGENT_RELEASE_CHANNEL stable | beta# PRIME_AGENT_VERSION pin an exact version# PRIME_AGENT_RUST_PREFIX install prefix (default: $HOME\.local)# PRIME_AGENT_UV_BIN_DIR uv install dir (default: $HOME\.local\bin)## Prerequisites: Windows 10+ (tar.exe and Get-FileHash ship with the OS;# the kernel's Python runtime bootstraps through uv on first session — the# pre-warm below installs the venv when uv is available).
# The whole body runs in a child scope: under `irm | iex` the script shares# the caller's session, so the Stop preference and the installer's own# variables must not stay behind in it, and a refusal must return to the# prompt (Fail throws) instead of closing the user's PowerShell. The body# stays unindented so the publish render's line-anchored sed still matches.# Any failure reaches the catch at the bottom: run as a file it exits 1 (in# PowerShell 7 and Windows PowerShell 5.1 alike, never relying on how an# uncaught error or a trap's break maps to the exit code); under `irm | iex`# ($PSCommandPath is empty) it rethrows to the caller instead, so the# caller's session stays open.try {& {$ErrorActionPreference = 'Stop'
# The publish-rendered defaults: the release pipeline copies this file to# <base>/install.ps1 with $DownloadBaseUrlDefault set to the R2 public base# and $ReleaseChannelDefault set to the channel; the repo-file default is# the same bucket-root base install-rust.sh carries, so the raw repo copy# installs out of the box too.$DownloadBaseUrlDefault = 'https://pub-728493de92a943e2a9b2d17b4719f318.r2.dev'$ReleaseChannelDefault = 'stable'
# The installer-owned bookkeeping, initialized BEFORE any Fail can run# (under `irm | iex` the script scope is the caller's session: ambient# variables must never be read as this installer's state, so the names are# unique to this script and all start cleared - the bots' finding).$script:primeAgentInstallScratch = $null$script:primeAgentInstallStage = $null$script:primeAgentInstallLock = $null
# Fail only throws: the catch at the bottom sweeps the installer's own# state on every failure path (a Fail and any other terminating error alike).function Fail($message) { throw "install.ps1: $message"}
# --- TLS floor (PowerShell 5.1 defaults can sit below TLS 1.2) ---------------try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12} catch { Fail "could not enable TLS 1.2: $($_.Exception.Message)"}
# --- the install ledger (install-rust.sh's step_ok / step_fail lines, in# --- the ASCII the console's default codepage renders cleanly) --------------function Step-Ok([string]$Text, [string]$Detail = '') { if ($Detail) { Write-Host "ok: $Text ($Detail)" } else { Write-Host "ok: $Text" }}function Step-Fail([string]$Text, [string]$Detail = '') { if ($Detail) { Write-Warning "fail: $Text ($Detail)" } else { Write-Warning "fail: $Text" }}
# --- the whole-entry PATH compare -------------------------------------------------# A substring check against the joined PATH string ("$bin*") lets a sibling# entry like C:\Tools\bin-old mask C:\Tools\bin: the add is skipped and# the command stays unavailable. Membership compares WHOLE `;`-delimited# entries, case-insensitively (Windows paths are case-insensitive).function Test-PathEntry([string]$PathValue, [string]$Entry) { foreach ($candidate in ($PathValue -split ';')) { if ($candidate -and ($candidate.Trim() -ieq $Entry)) { return $true } } return $false}
# --- the streaming download (install-rust.sh's fetch() discipline: three# --- attempts, and the installer's own progress, never the web cmdlet's# --- transfer records - a HttpClient copy loop repaints one short line in# --- place with the file's name, the landed bytes, and the percent) ---------if (-not ('System.Net.Http.HttpClient' -as [type])) { Add-Type -AssemblyName System.Net.Http}function Save-Release { param([string]$Uri, [string]$OutFile, [string]$Name) $attempts = 0 while ($true) { $attempts += 1 $client = [System.Net.Http.HttpClient]::new() $response = $null try { $client.Timeout = [TimeSpan]::FromMinutes(30) $response = $client.GetAsync($Uri, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead).GetAwaiter().GetResult() if (-not $response.IsSuccessStatusCode) { throw "HTTP $([int]$response.StatusCode) for $Uri" } $total = $response.Content.Headers.ContentLength $stream = $response.Content.ReadAsStreamAsync().GetAwaiter().GetResult() $file = $null $done = 0L try { $file = [System.IO.File]::Create($OutFile) $buffer = New-Object byte[] (256 * 1024) # THE STALL WATCHDOG (the reviewer's finding): the # HttpClient timeout above bounds only the wait for the # headers under ResponseHeadersRead - a server that answers # them and then parks the body would leave the synchronous # read below waiting forever, and the three attempts would # never run. Each chunk read is a bounded task: a body # silent for the whole window throws and takes the retry # path; a slow body never trips it (a read returns as data # arrives, the window restarts per chunk). $readTimeout = [TimeSpan]::FromSeconds(60) while ($true) { $readTask = $stream.ReadAsync($buffer, 0, $buffer.Length) if (-not $readTask.Wait($readTimeout)) { throw "the body of $Name stalled (no data for $([int]$readTimeout.TotalSeconds) seconds)" } $read = $readTask.Result if ($read -le 0) { break } $file.Write($buffer, 0, $read) $done += $read if ($total) { $pct = [Math]::Floor($done * 100 / $total) Write-Host ("`r {0}: {1:N1} of {2:N1} MB ({3}%)" -f $Name, ($done / 1MB), ($total / 1MB), $pct) -NoNewline } else { Write-Host ("`r {0}: {1:N1} MB" -f $Name, ($done / 1MB)) -NoNewline } } $file.Flush() } finally { if ($file) { $file.Dispose() } $stream.Dispose() } Write-Host '' return } catch { Write-Host '' Remove-Item -LiteralPath $OutFile -Force -ErrorAction SilentlyContinue if ($attempts -ge 3) { throw "could not download $Uri (3 attempts): $($_.Exception.Message)" } Write-Host "retrying $Name (attempt $($attempts + 1) of 3): $($_.Exception.Message)" Start-Sleep -Seconds 1 } finally { if ($response) { $response.Dispose() } $client.Dispose() } }}
# --- the knobs ----------------------------------------------------------------$baseUrl = if ($env:PRIME_AGENT_DOWNLOAD_BASE_URL) { $env:PRIME_AGENT_DOWNLOAD_BASE_URL } else { $DownloadBaseUrlDefault }$channelRequested = $env:PRIME_AGENT_RELEASE_CHANNEL$channel = if ($channelRequested) { $channelRequested } else { $ReleaseChannelDefault }$versionPin = $env:PRIME_AGENT_VERSION$prefix = if ($env:PRIME_AGENT_RUST_PREFIX) { $env:PRIME_AGENT_RUST_PREFIX } else { Join-Path $HOME '.local' }
# THE HTTPS RULE: the channel is served over the R2-backed domain, and a# plaintext download base would let a network attacker swap the payload# the checksum then "verifies" into place. The one escape hatch is the# explicitly-named knob for local-channel/e2e use (install-rust.sh carries# the same rule + knob); it prints a loud warning when it is active.$allowHttp = $env:PRIME_AGENT_ALLOW_HTTP -eq '1'if ($baseUrl -notmatch '^https://') { if ($allowHttp) { Write-Warning "PRIME_AGENT_ALLOW_HTTP=1: the download base $baseUrl is NOT https - the download is plaintext; use this only for a local channel you control" } else { Fail "the download base URL must be an https URL: $baseUrl" }}$baseUrl = $baseUrl.TrimEnd('/')if (@('stable', 'beta') -notcontains $channel) { Fail "unknown release channel: $channel (stable or beta)" }# The prefix does not need to exist (a fresh Windows profile has no# $HOME\.local; the share/bin creation below makes it), but it must name# a directory path, not an existing FILE.if (Test-Path $prefix -PathType Leaf) { Fail "the install prefix names an existing file: $prefix" }
# THE UV TARGET KNOB VALIDATES EARLY (the reviewer's finding): a bad value# must fail BEFORE any side effect - the daemon stop, the payload publish,# and the launcher swap all happen below, and a late refusal would leave# a half-applied install. The target must be FULLY QUALIFIED (a# drive-based or UNC path WITH its server and share): a rooted-only check# accepts drive-relative# spellings like C:uv-bin or \uv-bin, which resolve against the CURRENT# directory in later shells - the exact break this check exists to# prevent.if ($env:PRIME_AGENT_UV_BIN_DIR -and (($env:PRIME_AGENT_UV_BIN_DIR -notmatch '^[a-zA-Z]:[\\/]') -and ($env:PRIME_AGENT_UV_BIN_DIR -notmatch '^\\\\[^\\\\]+\\[^\\\\]+'))) { Fail "PRIME_AGENT_UV_BIN_DIR must be a fully qualified absolute path (a drive-based or UNC path): $($env:PRIME_AGENT_UV_BIN_DIR)"}if ($env:PRIME_AGENT_UV_BIN_DIR -and $env:PRIME_AGENT_UV_BIN_DIR.Contains(';')) { Fail "PRIME_AGENT_UV_BIN_DIR must not contain a semicolon (the Windows PATH separator): $($env:PRIME_AGENT_UV_BIN_DIR)"}
# The channel files: the pointer (<base>/stable or <base>/beta) names the# version, the channel manifest (<base>/latest.json or <base>/beta.json)# the rows; the version resolution below reads the pair per channel.
# The platform this script serves: the channel manifest's win32-x64 row# (the TS NATIVE_PLATFORMS spelling pa-core::update::install reads) — a# fixed target, checked here so an ARM64 Windows machine fails loudly# instead of downloading a payload it cannot start.$platform = 'win32-x64'# The OS architecture, not the process's: 32-bit PowerShell on x64 Windows# (WOW64) reports PROCESSOR_ARCHITECTURE=x86 and the OS arm rides# PROCESSOR_ARCHITEW6432 — the 64-bit OS must not be refused (Macroscope:# the WOW64 shape).$effectiveArchitecture = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE }if ($effectiveArchitecture -ne 'AMD64') { Fail "this machine reports architecture '$effectiveArchitecture'; the Windows channel ships the x86_64 (win32-x64) build only"}
# The channel naming contract gives the artifact row's file name: the row# the channel manifest must carry for this platform (the reader's own rule,# pa-core::update::release::parse_channel_manifest).function ExpectedFileName($version, $platform) { return "prime-agent-$version-$platform.tar.gz"}
# --- resolve the version (the channel pointer, or the pinned version) --------# A PINNED version skips the channel manifest entirely (install-rust.sh# parity): the manifest describes the channel's CURRENT release, and a# historical pin must install from its own versioned prefix — the row is# the channel naming contract, and the release prefix's SHA256SUMS verifies# the artifact. An unpinned install reads the pointer + the manifest as a# PAIR, retrying once on a version mismatch (the publish writes the# manifest first and the pointer second: a read between the two writes sees# the old pointer with the new manifest — a transient window, not a broken# channel — install-rust.sh's consistency retry). Both explicitly selected# channels use the same pair reader and retry discipline.function Read-ChannelPair($channelName) { $pairPointer = $channelName $pairManifestName = if ($channelName -eq 'beta') { 'beta.json' } else { 'latest.json' } $attempt = 0 # The same quiet discipline as the download block: these reads are web # cmdlets too (their progress records flash the console); pinned here, # restored on every exit - the quiet zone is the read, never the # session. -UseBasicParsing keeps a stock PowerShell 5.1 (Internet # Explorer never initialized) off the parsing COM path. $callerProgressPreference = $ProgressPreference $callerVerbosePreference = $VerbosePreference $ProgressPreference = 'SilentlyContinue' $VerbosePreference = 'SilentlyContinue' try { while ($true) { # The pointer is published bare ("1.2.3") but a `v`-prefixed # spelling is a valid historical form - normalize it (the manifest's # version is bare; the release prefix and the artifact names carry # no extra `v` - the bots' finding). $pairVersion = (Invoke-RestMethod -Uri "$baseUrl/$pairPointer" -UseBasicParsing).ToString().Trim().TrimStart('v') if (-not $pairVersion) { Fail "could not resolve the latest $channelName version from $baseUrl/$pairPointer" } $pairManifest = Invoke-RestMethod -Uri "$baseUrl/$pairManifestName" -UseBasicParsing $pairManifestVersion = ($pairManifest.version).ToString().TrimStart('v') if ($pairManifestVersion -eq $pairVersion) { break } $attempt += 1 if ($attempt -gt 2) { Fail "the $channelName manifest's version $pairManifestVersion does not match the channel pointer $pairVersion (re-read twice; the channel looks inconsistent)" } Write-Host "the $channelName pointer and manifest disagree (a publish's consistency window); re-reading the pair..." Start-Sleep -Seconds 1 } } finally { $ProgressPreference = $callerProgressPreference $VerbosePreference = $callerVerbosePreference } return [pscustomobject]@{ Version = $pairVersion; Manifest = $pairManifest }}
# The manifest's row for this platform: the channel naming contract gives# the file name the row must carry (the reader's own rule,# pa-core::update::release::parse_channel_manifest).function Find-PlatformRow($rowManifest, $rowVersion) { $expected = ExpectedFileName $rowVersion $platform $found = $null foreach ($candidate in @($rowManifest.binaries_v2) + @($rowManifest.binaries)) { if ($candidate -and $candidate.platform -eq $platform) { if ($candidate.file -ne $expected) { Fail "the manifest's $platform row names '$($candidate.file)' instead of the channel naming '$expected'" } $found = $candidate break } } return $found}
$manifest = $null$row = $nullif ($versionPin) { $version = $versionPin.Trim().TrimStart('v') Write-Host "installing prime-agent $version (pinned) from the $channel channel ($platform)"} else { $pair = Read-ChannelPair $channel $version = $pair.Version $manifest = $pair.Manifest $row = Find-PlatformRow $manifest $version if (-not $row) { Fail "no artifact row for platform $platform in the $channel manifest; the installer will not switch release channels automatically" } if ($row.sha256 -notmatch '^[0-9a-f]{64}$') { Fail "the channel manifest's sha256 for $(ExpectedFileName $version $platform) is malformed" } Write-Host "installing prime-agent $version from the $channel channel ($platform)"}
# --- the channel manifest row (unpinned) / the naming contract (pinned) ------$expectedFile = ExpectedFileName $version $platformif ($versionPin) { Write-Host "pinned ${version}: installing from the versioned release prefix (the channel naming contract names the row)"}
# --- the tarball + SHA256SUMS from the versioned release prefix ----------------$releasePrefix = "releases/v$version"$download = Join-Path ([IO.Path]::GetTempPath()) ("prime-agent-download-{0}" -f [Guid]::NewGuid().ToString('N'))New-Item -ItemType Directory -Path $download | Out-Null$script:primeAgentInstallScratch = $download# THE DOWNLOAD SWEEP: the scratch (the tarball + the sums) is removed when# this script exits - success below, a Fail or a terminating error in the# catch at the bottom (install-rust.sh's `rm -rf "$dl"` discipline; the# bots' finding: accumulated release tarballs in the temp folder).$tarball = Join-Path $download $expectedFile$sumsPath = Join-Path $download 'SHA256SUMS'# THE QUIET DOWNLOAD (the operator's real-machine report, 2026-10-06):# PowerShell's web cmdlets under the default preferences leak the .NET# stack's transfer records onto the caller's console - the progress bar# and the "Writing web request / Writing request stream..." verbose# chatter - and under `irm | iex` this script shares the caller's# session, so the caller's preferences ride along. The installer pins# both to SilentlyContinue for its web work only and restores the# caller's values after (the quiet zone is the download, never the# session); the payload streams through Save-Release (its own in-place# progress line, no transfer records at all), and the checksums file's# small fetch stays silent under the pin.$callerProgressPreference = $ProgressPreference$callerVerbosePreference = $VerbosePreference$ProgressPreference = 'SilentlyContinue'$VerbosePreference = 'SilentlyContinue'Write-Host "downloading $expectedFile"try { Save-Release -Uri "$baseUrl/$releasePrefix/$expectedFile" -OutFile $tarball -Name $expectedFile Invoke-WebRequest -Uri "$baseUrl/$releasePrefix/SHA256SUMS" -OutFile $sumsPath -UseBasicParsing Write-Host "downloaded: $expectedFile ($([Math]::Round((Get-Item -LiteralPath $tarball).Length / 1MB, 1)) MB)"} catch { Fail "could not download $expectedFile from $baseUrl/$releasePrefix/: $($_.Exception.Message)"} finally { $ProgressPreference = $callerProgressPreference $VerbosePreference = $callerVerbosePreference}
# --- verify the checksum (the release prefix's sums, cross-checked with the# --- manifest's row: two independent reads of the same digest) ------------------$sumsLine = Get-Content -LiteralPath $sumsPath | Where-Object { $_ -match " $expectedFile$" } | Select-Object -First 1if (-not $sumsLine) { Fail "SHA256SUMS in $releasePrefix has no line for $expectedFile" }$sumsSha = ($sumsLine -split '\s+')[0]if (-not $versionPin -and $sumsSha -ne $row.sha256) { Fail "checksum mismatch between the channel manifest and SHA256SUMS for ${expectedFile}: the channel is inconsistent" }$actualSha = (Get-FileHash -LiteralPath $tarball -Algorithm SHA256).Hash.ToLower()if ($actualSha -ne $sumsSha) { Fail "checksum mismatch for ${expectedFile}: the download is corrupt" }Step-Ok 'checksum verified' "$expectedFile ($version, the $channel channel)"
# --- extract to a staging dir inside the prefix (same volume: the final swap# --- is a rename, not a copy) -----------------------------------------------------$share = Join-Path $prefix 'share\prime-agent'$bin = Join-Path $prefix 'bin'New-Item -ItemType Directory -Path (Join-Path $prefix 'share') -Force | Out-NullNew-Item -ItemType Directory -Path $bin -Force | Out-Null$stage = Join-Path (Join-Path $prefix 'share') ("prime-agent.stage-{0}" -f [Guid]::NewGuid().ToString('N'))New-Item -ItemType Directory -Path $stage | Out-Null# The stage rides every exit path once created (the failure sweep at the bottom:# a failed extraction/validation must not leave the extracted payload# behind - the bots' finding).$script:primeAgentInstallStage = $stage& tar -xzf $tarball -C $stageif ($LASTEXITCODE -ne 0) { Fail "could not extract $expectedFile (tar exited $LASTEXITCODE)" }$payloadExe = Join-Path $stage 'prime-agent.exe'if (-not (Test-Path $payloadExe -PathType Leaf)) { Fail "the tarball did not contain a prime-agent.exe payload" }Step-Ok 'extracted' $expectedFile
# The ownership marker (install-rust.sh's exact write shape — the update# funnel's channel-stickiness read keys on it).$channelLine = "install-rust.sh channel $channel"$marker = "$channelLine`nversion $version"# -Encoding Ascii keeps the marker byte-exact: Set-Content's default# encoding follows $PSDefaultParameterValues['*:Encoding'] (UTF-16 or a# BOM), and the update funnel's channel-stickiness read + the rollback's# marker validation grep the raw ASCII line.Set-Content -LiteralPath (Join-Path $stage '.prime-agent-install') -Value $marker -Encoding Ascii -NoNewline
# The launcher paths (the section below the publish writes both).$launcher = Join-Path $bin 'prime-agent.cmd'$shLauncher = Join-Path $bin 'prime-agent'
# --- the publication lock (one writer): a lock DIRECTORY is the atomic# --- claim on Windows (mkdir wins exactly once); the holder's pid rides a# --- file inside, and a stale lock is never auto-stolen. THE LOCK COMES# --- FIRST: a failed lock claim must never have stopped a daemon (the# --- bots' finding - the stop-for-nothing downtime).$lockDir = Join-Path (Join-Path $prefix 'share') '.prime-agent-install.lock.d'try { New-Item -ItemType Directory -Path $lockDir -ErrorAction Stop | Out-Null} catch { $heldBy = '' $pidPath = Join-Path $lockDir 'pid' if (Test-Path $pidPath) { $heldBy = (Get-Content -LiteralPath $pidPath -ErrorAction SilentlyContinue) } Fail "another installer (pid $heldBy) or a stale publication lock owns $lockDir; if no installer is running, remove it and re-run"}try { Set-Content -LiteralPath (Join-Path $lockDir 'pid') -Value $PID} catch { # A failed PID write releases the freshly claimed lock (the bots' # finding: the leaked lock otherwise blocks every later install). Remove-Item -Recurse -Force $lockDir -ErrorAction SilentlyContinue Fail "could not write the publication lock PID: $($_.Exception.Message)"}# The lock rides every exit path from here: Fail releases it, and the# publish's finally releases + clears it.$script:primeAgentInstallLock = $lockDir
$published = $false$rollback = $null$script:daemonStopped = $false
# THE PREFLIGHT: every ownership refusal that can happen runs BEFORE the# daemon stop (a refused install must never have stopped a daemon - the# bots' finding: the stop-then-refuse order was the avoidable downtime).$rollback = Join-Path (Join-Path $prefix 'share') 'prime-agent.old'if (Test-Path $share -PathType Container) { $markerPath = Join-Path $share '.prime-agent-install' if (-not (Test-Path $markerPath -PathType Leaf)) { Fail "refusing to take ownership of ${share}: it is not a marked prime-agent payload tree (.prime-agent-install); move it aside and re-run" } if (Test-Path $rollback) { $rollbackMarker = Join-Path $rollback '.prime-agent-install' if (-not (Test-Path $rollbackMarker -PathType Leaf)) { Fail "refusing to delete the unmarked rollback directory: $rollback (move it aside and re-run)" } }}
try { # THE DAEMON STOP, inside the lock's try (a terminating error in the # stop must still release the lock - the bots' finding: the stale lock # otherwise left behind). THE TRUSTED STOP: the previous payload's own # binary (the marked share tree's prime-agent.exe), never the launcher - # a launcher this installer has not verified is the unowned-execution # shape (an attacker-placed bin\prime-agent.cmd would otherwise run # with the installer's inherited environment); the share's ownership # marker gates the stop, and a fresh install (no marked payload yet) # has no daemon the stop could reach. $payloadStopExe = Join-Path $share 'prime-agent.exe' $shareMarker = Join-Path $share '.prime-agent-install' if ((Test-Path $shareMarker -PathType Leaf) -and (Test-Path $payloadStopExe -PathType Leaf)) { Write-Host 'stopping the running Rust daemon before the publish (a Windows process holds its binary open)' # THE SCRIPTED FORM: the CLI's bare `shutdown` prompts for # confirmation in an interactive terminal and REFUSES a # non-interactive one ("Shutdown requires confirmation in an # interactive terminal. Use prime-agent shutdown --force") - the # installer's context is the scripted one, so `--force` is the # documented non-interactive stop (the daemon's own forced # shutdown drains its workers with its internal budgets). & $payloadStopExe shutdown --force *> $null if ($LASTEXITCODE -eq 0) { $script:daemonStopped = $true } else { Write-Warning "no daemon answered the shutdown request; if a daemon is running, stop it by hand (prime-agent shutdown --force) and re-run" } }
# The one-generation rollback (the ownership checks already ran in the # preflight): the previous payload moves aside, the fresh stage swaps # in, and the next install sweeps the rollback. if (Test-Path $share -PathType Container) { if (Test-Path $rollback) { Remove-Item -Recurse -Force $rollback } [IO.Directory]::Move($share, $rollback) Write-Host "rollback: $rollback (the previous payload, one generation)" } [IO.Directory]::Move($stage, $share) $script:primeAgentInstallStage = $null # The payload is live: no later launcher failure rolls it back (the new # tree is valid; the restored launchers point at ../share/prime-agent/ # prime-agent.exe - the new payload - and keep working). $published = $true Step-Ok 'published' "prime-agent $version to $prefix"
# --- the launcher pair: the cmd shim (cmd.exe + PowerShell) and the sh # --- launcher (Git Bash) - the same payload, both shells, the same # --- content install-rust.sh writes. THE LOCK STAYS HELD through the # --- launcher transaction (the bots' finding: a second installer must # --- not slip between the payload publish and the launcher write, or # --- its launcher would pair with this payload and a later failure of # --- EITHER installer would restore the other's .pre-takeover over # --- it). The unowned-file discipline is the sh installer's own: a # --- launcher this script did not write is preserved aside, never # --- overwritten (an unrelated command is never destroyed) AND a # --- failure after a preserve puts the user's file back. An # --- INSTALLER-OWNED launcher is snapshotted first (an I/O failure at # --- Set-Content can truncate a live, working launcher). # Script scope, the scope the helpers below append to: the reset must # hit the same arrays, or an earlier `irm | iex` run's entries in the # same session would be replayed by this run's recovery. $script:preservedLaunchers = @() $script:ownedLauncherBackups = @() function Backup-ExistingLauncher($path) { if (-not (Test-Path $path -PathType Leaf)) { return } $backup = "$path.install-backup" while (Test-Path $backup) { $backup = "$backup.$([Guid]::NewGuid().ToString('N').Substring(0,4))" } Copy-Item -LiteralPath $path -Destination $backup -Force $script:ownedLauncherBackups += ,@($backup, $path) } function Preserve-UnownedLauncher($path) { if (-not (Test-Path $path -PathType Leaf)) { return } $firstLine = (Get-Content -LiteralPath $path -TotalCount 2) -join ' ' if ($firstLine -match 'launcher written by install-rust\.sh') { Backup-ExistingLauncher $path return } $preserved = "$path.pre-takeover" while (Test-Path $preserved) { $preserved = "$preserved.$([Guid]::NewGuid().ToString('N').Substring(0,4))" } [IO.File]::Move($path, $preserved) $script:preservedLaunchers += ,@($preserved, $path) Write-Host "note: an unrelated $(Split-Path -Leaf $path) existed at $path; it was preserved at $preserved" } Preserve-UnownedLauncher $launcher Preserve-UnownedLauncher $shLauncher
try { $cmdShim = @'@echo offrem prime-agent - launcher written by install-rust.sh.if not defined PRIME_AGENT_CODING_AGENT_DIR set "PRIME_AGENT_CODING_AGENT_DIR=%USERPROFILE%\.prime\agent""%~dp0..\share\prime-agent\prime-agent.exe" %*'@ Set-Content -LiteralPath $launcher -Value $cmdShim
$shBody = @'#!/bin/sh# prime-agent — launcher written by install-rust.sh.export PRIME_AGENT_CODING_AGENT_DIR="${PRIME_AGENT_CODING_AGENT_DIR:-$HOME/.prime/agent}"exec "$(dirname "$0")/../share/prime-agent/prime-agent.exe" "$@"'@ # LF endings + no BOM: the sh launcher must stay a POSIX file. [IO.File]::WriteAllText($shLauncher, $shBody.Replace("`r`n", "`n"))
# The launchers are live: the owned-launcher snapshots (the failure # restore points) sweep away - nothing lingers after a success. foreach ($pair in $script:ownedLauncherBackups) { Remove-Item -Force $pair[0] -ErrorAction SilentlyContinue } } catch { # A failed launcher write never leaves the machine without its # command: every preserved file goes home, and every OWNED # launcher's pre-write snapshot is restored over its truncated # form, before the failure surfaces. The destination's PARTIAL # file (Set-Content/WriteAllText can create or truncate before # throwing) is removed first - the broken new file otherwise # blocks the restore. foreach ($pair in $script:preservedLaunchers) { if (Test-Path $pair[1]) { Remove-Item -Force $pair[1] -ErrorAction SilentlyContinue } [IO.File]::Move($pair[0], $pair[1]) } foreach ($pair in $script:ownedLauncherBackups) { Copy-Item -LiteralPath $pair[0] -Destination $pair[1] -Force Remove-Item -Force $pair[0] -ErrorAction SilentlyContinue } throw }} finally { # A failed publish never leaves the machine without its previous payload: # the old tree returns to the live name before the lock releases (the sh # installer's restore-on-exit discipline). $rollback is pre-initialized: # a terminating error before its assignment must not turn the finally's # own check into the leaked-lock failure. if (-not $published) { if ($rollback -and (Test-Path $rollback -PathType Container) -and -not (Test-Path $share)) { # The restore's own failure must not swallow the lock release # (an error thrown here would skip the Remove-Item below - the # stale-lock manual recovery class): the restore reports the # manual recovery instead of throwing. try { [IO.Directory]::Move($rollback, $share) Write-Warning "the publish failed; the previous payload was restored to $share" } catch { Write-Warning "the publish failed AND the previous payload could not be restored: it waits at $rollback - recover with: Move-Item '$rollback' '$share'" } } # A failed install sweeps its stage tree (the bots' finding: the # accumulated prime-agent.stage-* directories). if ($stage -and (Test-Path $stage -PathType Container)) { Remove-Item -Recurse -Force $stage -ErrorAction SilentlyContinue } # A daemon stopped for a FAILED install stays down (its sessions' # state is on disk; nothing here restarts it): name the recovery so # the user never guesses - the next invocation boots the old # payload's daemon again. if ($script:daemonStopped) { Write-Warning "the daemon stopped for this failed install stays down until the next prime-agent invocation boots it from the restored payload" } } Remove-Item -Recurse -Force $lockDir -ErrorAction SilentlyContinue $script:primeAgentInstallLock = $null}
# --- the kernel pre-warm: uv + the Python venv (install-rust.sh# --- parity — the sh installer INSTALLS uv when it is missing and then# --- pre-warms the kernel; the ps1 mirrors the whole flow). THE SEPARATE# --- CHECKS (install-rust.sh's own discipline): the fetch and the script# --- run are verified one after the other, so a dead network cannot# --- masquerade as success — the astral script's own verdict is never# --- trusted, the uv.exe FILE decides. The destination rides the env# --- EXPLICITLY (UV_INSTALL_DIR, install-rust.sh's UV_INSTALL_DIR=# --- "$uv_bin_dir"): the computed target wins over any inherited value,# --- and the caller's values come back. Offline the step degrades to the# --- honest note plus the exact manual command (install-rust.sh's# --- note/todo shape); the install itself still succeeds.# THE UV TARGET KNOB (install-rust.sh's own PRIME_AGENT_UV_BIN_DIR): an# operator-set dir wins (a packaged install keeps uv inside its own tree,# the e2e harnesses point it at their scratch dir so a run never touches# the shared ~/.local/bin); without the knob the canonical ~/.local/bin is# the target (where the product's ensure_uv also looks).$uvInstallDir = Join-Path $HOME '.local\bin'# The knob's fully-qualified validation ran early with the other knobs# (before any side effect - see the validation block in the knobs section).if ($env:PRIME_AGENT_UV_BIN_DIR) { $uvInstallDir = $env:PRIME_AGENT_UV_BIN_DIR }$uvExe = Join-Path $uvInstallDir 'uv.exe'$uvDefaultExe = Join-Path (Join-Path $HOME '.local\bin') 'uv.exe'# THE THREE-WAY LOOKUP (the reviewer's finding): a usable uv is one on# PATH (Get-Command), at the install target, or at the canonical# ~/.local/bin - the product's own ensure_uv searches PATH and# ~/.local/bin, so both pre-existing spellings are honored (no duplicate# install), and the pre-warm gate below follows the record.# THE REAL-APPLICATION CHECK (the reviewer's finding): a bare Get-Command# also finds a PowerShell ALIAS or FUNCTION named uv in the caller's# session - which no child process can resolve - so only an Application# command (a real executable on PATH) counts.$uvOnPath = [bool](Get-Command uv -CommandType Application -ErrorAction SilentlyContinue)$uvAtTarget = Test-Path $uvExe -PathType Leaf$uvAtDefault = Test-Path $uvDefaultExe -PathType Leaf$uvKnown = $uvOnPath -or $uvAtTarget -or $uvAtDefaultif (-not $uvKnown) { Write-Host "installing uv (the kernel venv's package manager)" $uvInstallerPath = Join-Path $download 'uv-install.ps1' $callerProgressPreference = $ProgressPreference $callerVerbosePreference = $VerbosePreference $ProgressPreference = 'SilentlyContinue' $VerbosePreference = 'SilentlyContinue' try { Invoke-WebRequest -Uri 'https://astral.sh/uv/install.ps1' -OutFile $uvInstallerPath -UseBasicParsing -TimeoutSec 60 if ((Test-Path $uvInstallerPath -PathType Leaf) -and ((Get-Item -LiteralPath $uvInstallerPath).Length -gt 0)) { $callerUvInstallDir = $env:UV_INSTALL_DIR $callerUvUnmanaged = $env:UV_UNMANAGED_INSTALL $callerUvInstaller = $env:PRIME_AGENT_UV_INSTALLER try { $env:UV_INSTALL_DIR = $uvInstallDir $env:UV_UNMANAGED_INSTALL = $null # THE CHILD PROCESS (the reviewer's finding): the astral # script exits 1 on its own failures (a Restricted execution # policy among them - a stock Windows PowerShell 5.1 setting), # and under Invoke-Expression that exit would leave THIS # installer's control flow before the catch could degrade it # to the honest note. The fetched script runs in its own # shell with a process-scoped execution policy and its own # silenced progress; the parent reads the child's exit as # DATA, and the uv.exe FILE decides. $childShell = if (Get-Command pwsh -CommandType Application -ErrorAction SilentlyContinue) { (Get-Command pwsh -CommandType Application).Source } elseif (Get-Command powershell -CommandType Application -ErrorAction SilentlyContinue) { (Get-Command powershell -CommandType Application).Source } else { # The process we are already running under is a PowerShell: # its own executable is the honest absolute fallback (a # bare 'powershell' could be intercepted by a caller alias # or function). (Get-Process -Id $PID).Path } # THE PATH RIDES AS DATA (the reviewers' finding): the # download dir sits under the caller's TEMP, and a profile # path with an apostrophe in it would break the single- # quoted literal in the child script - the child parsed # dead, the install skipped, the pre-warm left undone. So # the path never rides the command line OR the script text: # it rides the environment (the same handoff this block # already runs UV_INSTALL_DIR through), and the child reads # the variable - environment inheritance is OS semantics, # no PowerShell parsing of the path happens at any version. $env:PRIME_AGENT_UV_INSTALLER = $uvInstallerPath $childScript = @"`$ProgressPreference = 'SilentlyContinue'`$VerbosePreference = 'SilentlyContinue'& `$env:PRIME_AGENT_UV_INSTALLER"@ $childEncoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($childScript)) & $childShell -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $childEncoded *> $null $uvRunExit = $LASTEXITCODE } finally { $env:UV_INSTALL_DIR = $callerUvInstallDir $env:UV_UNMANAGED_INSTALL = $callerUvUnmanaged $env:PRIME_AGENT_UV_INSTALLER = $callerUvInstaller } if (-not (Test-Path $uvExe -PathType Leaf)) { Step-Fail 'installing uv' "the uv installer exited $uvRunExit; uv.exe is missing" } elseif ($uvRunExit -ne 0) { Step-Fail 'installing uv' "the uv installer exited $uvRunExit (uv.exe landed; keeping it)" } else { Step-Ok 'uv installed' $uvInstallDir } } else { Step-Fail 'installing uv' 'the fetched uv installer is empty' } } catch { Step-Fail 'installing uv' $_.Exception.Message } finally { $ProgressPreference = $callerProgressPreference $VerbosePreference = $callerVerbosePreference Remove-Item -LiteralPath $uvInstallerPath -Force -ErrorAction SilentlyContinue } if (-not (Test-Path $uvExe -PathType Leaf)) { Write-Host 'note: could not install uv; the kernel pre-warm was skipped. The first session needs uv. Install it with:' Write-Host ' irm https://astral.sh/uv/install.ps1 | iex' }}# THE PRE-WARM'S PATH FIX (install-rust.sh's own): the product's ensure_uv# searches PATH and ~/.local/bin/uv(.exe) - it does NOT know the# PRIME_AGENT_UV_BIN_DIR target - so the prepend serves the uv THIS RUN# installed or found at the target when nothing usable answers on PATH# (the reviewer's finding: a pre-warm whose uv is known but unfindable# fails). THE GUARD (the macroscope finding): a working PATH uv is never# shadowed - a stale target file cannot displace it - and a uv at the# canonical ~/.local/bin needs no prepend (ensure_uv finds it by itself).# The record is recomputed first: the install above may have landed the# target uv.$uvAtTarget = Test-Path $uvExe -PathType Leaf$uvKnown = $uvOnPath -or $uvAtTarget -or $uvAtDefaultif (-not $uvOnPath -and $uvAtTarget -and -not (Test-PathEntry $env:PATH $uvInstallDir)) { $env:PATH = "$uvInstallDir;$env:PATH"}if ($uvKnown) { Write-Host 'kernel pre-warm: provisioning the Python kernel runtime' & $launcher --prime-agent-bootstrap if ($LASTEXITCODE -ne 0) { Step-Fail 'preparing the Python kernel' 'the first session retries it online' } else { Step-Ok 'kernel ready' }} else { Write-Host 'note: uv was not found; the first session bootstraps the kernel itself and needs the network once'}
# --- the PATH add (the operator's smoothness ask, 2026-10-06; the# --- windows-native shape of the sh installer's profile note) --------------------# Linux boxes usually carry ~/.local/bin on PATH already, so install-rust.sh# only PRINTS a profile note (it never edits a shell profile); a Windows# box has no %USERPROFILE%\.local\bin, and a printed-only note leaves the# fresh install unusable until the user edits their environment by hand.# Windows has a canonical user-PATH setting (the registry's User scope —# no admin needed), so the installer ADDS the launcher's bin dir there# itself: idempotent (appended only when the entry is absent, never# duplicated), mirrored into the CURRENT session so prime-agent works in# this console immediately, and a new terminal picks it up (the# WM_SETTINGCHANGE broadcast tells the running shells the environment# changed, the same way setx does — best-effort; the registry write is# what lasts).# THE REGISTRY-KIND DISCIPLINE (the reviewer's finding): the user Path is# often REG_EXPAND_SZ (the %VAR%-carrying kind Windows template profiles# write), and [Environment]::GetEnvironmentVariable returns it EXPANDED# while SetEnvironmentVariable writes back plain REG_SZ# (dotnet/runtime#1442) - %JAVA_HOME%-style entries would freeze at THIS# run's expansion and stop following their variables. The add reads the# RAW value with its kind through the registry API and writes the append# back with the SAME kind: the %VAR% spellings survive verbatim.# The durable add is BEST-EFFORT (the reviewer's finding): the session# PATH above already works and the payload is published at this point, so# a registry that will not open or write must degrade to the honest# manual note - never abort a finished install over the nicety.# THE PATH-ADD TARGETS (the reviewer's finding: a documented install# target must stay discoverable once the installer exits - the product's# own ensure_uv searches PATH and ~/.local/bin only): the launcher's bin# dir always, and the custom uv target when the knob placed one outside# the canonical default (the prefix's own bin is already the launcher's# bin dir then; the e2e harnesses strip their scratch entries again).$pathDirs = @($bin)if ($uvInstallDir -ne (Join-Path $HOME '.local\bin') -and $uvInstallDir -ne $bin) { $pathDirs += $uvInstallDir }# THE ANY-APPEND FLAG (the reviewer's finding): a write on ANY iteration# must broadcast - a later no-op iteration must never suppress an earlier# append's WM_SETTINGCHANGE (Explorer would hand new terminals a stale# environment). The flag initializes OUTSIDE the loop and never resets.$pathAdded = $falseforeach ($binDir in $pathDirs) { if (-not (Test-PathEntry $env:PATH $binDir)) { $env:PATH = "$env:PATH;$binDir" Write-Host "PATH: added $binDir to this session - prime-agent works in this console now" } $envKey = $null try { $envKey = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $true) if (-not $envKey) { # A stripped profile can lack HKCU\Environment entirely. $envKey = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey('Environment') } $rawUserPath = $envKey.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) $rawUserKind = [Microsoft.Win32.RegistryValueKind]::ExpandString if ($envKey.GetValueNames() -contains 'Path') { $rawUserKind = $envKey.GetValueKind('Path') } # THE DUAL-SPELLING DEDUP (the reviewer's finding): a raw entry like # %USERPROFILE%\.local\bin spells the SAME directory as the expanded # $binDir, and a raw-text compare alone would miss it - the add would # append the directory a second time in its expanded spelling. So the # membership check runs against BOTH spellings while the append and # the kind-preserving write-back keep the raw text untouched. $expandedUserPath = [Environment]::ExpandEnvironmentVariables($rawUserPath) if (-not (Test-PathEntry $rawUserPath $binDir) -and -not (Test-PathEntry $expandedUserPath $binDir)) { $newUserPath = if ($rawUserPath) { $rawUserPath.TrimEnd(';') + ";$binDir" } else { $binDir } $envKey.SetValue('Path', $newUserPath, $rawUserKind) $pathAdded = $true } } catch { Write-Host "note: could not persist the PATH entry to the user environment: $($_.Exception.Message)" Write-Host ' this session works; to add it by hand, put this entry on the user PATH:' Write-Host " $binDir" } finally { if ($envKey) { $envKey.Close() } }}if ($pathAdded) { try { if (-not ('prime_agent_install.Win32SendMessage' -as [type])) { Add-Type -Namespace 'prime_agent_install' -Name 'Win32SendMessage' -MemberDefinition @'[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam, uint fuFlags, uint uTimeout, out UIntPtr lpdwResult);'@ } [prime_agent_install.Win32SendMessage]::SendMessageTimeout([IntPtr]0xffff, 0x001A, [UIntPtr]::Zero, 'Environment', 2, 5000, [ref]([UIntPtr]::Zero)) } catch { # The registry write stands; the broadcast is a courtesy (a re-login # reloads the environment too). } Write-Host "PATH: added the missing entries to the user PATH (a new terminal picks them up automatically)"}
# --- verify: the launcher must answer --version -----------------------------------try { $versionOut = (& $launcher --version) 2>$null | Select-Object -First 1 if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -eq -1073741515) { # 0xC0000135, STATUS_DLL_NOT_FOUND: the launcher's process never # started - a runtime DLL the binary links is missing. The shipped # build links the STATIC VC runtime, so this class should not # occur; the honest message replaces the venv-bootstrap claim (a # DLL failure has nothing to do with the kernel venv, and # re-running cannot fix it). Write-Warning "the installed launcher failed --version with exit code -1073741515 (0xC0000135: the launcher could not start - a runtime DLL it needs is missing; the shipped build links the static runtime and should not need one - please report this install)" } else { Write-Warning "the installed launcher failed --version (exit code $LASTEXITCODE; the first run bootstraps the kernel venv - re-run it)" } } else { Write-Host "installed: $versionOut" }} catch { Write-Warning "the first --version run failed (the first run bootstraps the kernel venv - re-run it): $($_.Exception.Message)"}Write-Host "launcher: $launcher"Write-Host "payload: $share"Write-Host "source: the $channel channel at $baseUrl (prime-agent $version)"Write-Host 'next steps: the README''s Install section ships inside the payload:'Write-Host " $share\README.md"
$script:primeAgentInstallScratch = $null$script:preservedLaunchers = @()$script:ownedLauncherBackups = @()if ($download) { Remove-Item -Recurse -Force $download -ErrorAction SilentlyContinue }}} catch { # THE FAILURE SWEEP, the one cleanup path for every failure: exactly # what THIS script created - the download scratch, the stage tree, and # the publication lock it claimed (a refused install never leaks its # lock; the publish's finally has already released it when the failure # came from there). The names were cleared at the top, so an ambient # caller's variables never enter the cleanup. if ($script:primeAgentInstallScratch) { Remove-Item -Recurse -Force $script:primeAgentInstallScratch -ErrorAction SilentlyContinue } if ($script:primeAgentInstallStage) { Remove-Item -Recurse -Force $script:primeAgentInstallStage -ErrorAction SilentlyContinue } if ($script:primeAgentInstallLock) { Remove-Item -Recurse -Force $script:primeAgentInstallLock -ErrorAction SilentlyContinue } $script:primeAgentInstallScratch = $null $script:primeAgentInstallStage = $null $script:primeAgentInstallLock = $null if ($PSCommandPath) { $Host.UI.WriteErrorLine($_.Exception.Message) exit 1 } throw}