<# DeepSeek Harness (dsh) bootstrap for the Scan VM ------------------------------------------------ Sets up dsh on a Windows workstation and points it at the shared inference box. Safe to re-run. Usage: irm https://ai.mindprobe.xyz/go | iex With options (the scriptblock form is how you pass arguments through irm): & ([scriptblock]::Create((irm https://ai.mindprobe.xyz/go))) -Reasoning xhigh Nothing here is secret: the Scan VM endpoint is keyless on our LAN. #> param( [string] $Endpoint = 'http://10.2.2.7:48765/v1', [string] $ModelId = 'qwen3.8-27b', [string] $ModelName = 'Qwen3.8 27B FP8', [string] $RouteId = 'scan', [string] $DisplayName = 'Scan VM (Qwen3.8 27B FP8)', # Pinned to a version that actually exists on npm. The repo's package.json # carries an unreleased number (rc.5), which npm rejects with ETARGET. [string] $DshVersion = '0.1.0-rc.6', [ValidateSet('off', 'low', 'medium', 'xhigh')] [string] $Reasoning = 'medium', # Configure settings.yaml but skip the npm install step. [switch] $ConfigOnly, # Overwrite an existing llm-pi-ai block instead of writing a sidecar file. [switch] $Force, # Answer yes to every prompt. Needed for unattended runs, since a # non-interactive host cannot be asked. [switch] $Yes, # Never install anything. Report what is missing and stop. [switch] $NoInstall, # Reinstall dsh even when it already reports the pinned version. For # repairing a broken install; an ordinary update does not need it. [switch] $Reinstall ) $ErrorActionPreference = 'Stop' # ---------------------------------------------------------------- output ---- $script:Warnings = @() # Set when .ps1 shims stay blocked, so the closing instructions can say # 'dsh.cmd' rather than a 'dsh' that would not run. $script:PolicyBlocked = $false # Sentinel for a handled, already-reported failure. We must never call exit: # `irm | iex` runs in the caller's scope, so `exit` would close the user's # console window instead of just stopping the script. $script:AbortToken = 'dsh-setup-abort' function Write-Step { param([string] $m) Write-Host "==> $m" -ForegroundColor Cyan } function Write-Ok { param([string] $m) Write-Host " [ok] $m" -ForegroundColor Green } function Write-Note { param([string] $m) Write-Host " $m" -ForegroundColor DarkGray } function Write-Warn { param([string] $m) $script:Warnings += $m Write-Host " [!] $m" -ForegroundColor Yellow } function Write-Fail { param([string] $m, [string[]] $hints = @()) Write-Host "" Write-Host " [x] $m" -ForegroundColor Red foreach ($h in $hints) { Write-Host " $h" -ForegroundColor DarkGray } Write-Host "" throw $script:AbortToken } # ------------------------------------------------------------ prerequisites - function Test-Interactive { # Read-Host would hang forever in a non-interactive host, so only prompt # where there is genuinely someone to answer. try { return ([Environment]::UserInteractive -and -not $env:CI) } catch { return $false } } function Confirm-Action { param([string] $Question) if ($Yes) { return $true } if ($NoInstall) { return $false } if (-not (Test-Interactive)) { Write-Note "Not an interactive session -- not prompting. Re-run with -Yes to auto-install." return $false } while ($true) { $answer = Read-Host " $Question [Y/n]" if ($answer -eq '' -or $answer -match '^\s*[Yy]') { return $true } if ($answer -match '^\s*[Nn]') { return $false } } } function Install-WithWinget { param([string] $Id, [string] $Label) if (-not (Get-Command winget -ErrorAction SilentlyContinue)) { Write-Note "winget is not available on this machine, so it cannot install $Label." return $false } Write-Step "Installing $Label" Write-Note "winget may ask for elevation." # winget is chatty on stderr; same 5.1 NativeCommandError trap as npm. $prevEap = $ErrorActionPreference $ErrorActionPreference = 'Continue' try { & winget install -e --id $Id --accept-source-agreements --accept-package-agreements 2>&1 | ForEach-Object { Write-Note "$_" } } finally { $ErrorActionPreference = $prevEap } # winget reports "already installed, nothing to upgrade" as a failure code. # That is a success for our purposes: the package is present and current. $code = $LASTEXITCODE if ($code -eq 0) { return $true } $benign = @( -1978335189, # 0x8A15002B UPDATE_NOT_APPLICABLE - already at latest -1978335135, # 0x8A150061 PACKAGE_ALREADY_INSTALLED -1978334972 # 0x8A150104 NO_APPLICABLE_UPDATE ) if ($benign -contains $code) { Write-Note "$Label is already installed and current." return $true } Write-Warn "winget exited with code $code while installing $Label." return $false } function Update-PathFromRegistry { # winget writes the machine/user PATH, but this process keeps the copy it # started with. Re-read both so a freshly installed tool is usable without # opening a new terminal. try { $machine = [System.Environment]::GetEnvironmentVariable('Path', 'Machine') $user = [System.Environment]::GetEnvironmentVariable('Path', 'User') $env:Path = (@($machine, $user) | Where-Object { $_ }) -join ';' } catch { Write-Note "Could not refresh PATH from the registry: $($_.Exception.Message)" } } function Resolve-Cli { param([string] $Name) # PowerShell resolves a bare command name to the .ps1 shim ahead of the # .cmd one, and ExecutionPolicy blocks .ps1 files on a default Windows # install. The .cmd shim is a batch file and is never subject to script # policy, so prefer it and fall back to the bare name elsewhere. foreach ($candidate in @("$Name.cmd", $Name)) { $cmd = Get-Command $candidate -ErrorAction SilentlyContinue if ($cmd) { return $cmd.Source } } return $null } function Get-DshVersion { # Same rule as the Node probe: read the output, never $LASTEXITCODE. $cli = Resolve-Cli 'dsh' if ($null -eq $cli) { return $null } try { $out = (& $cli --version 2>&1) | Out-String if ($out -match '(\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?)') { return $Matches[1] } } catch { } return $null } function Get-BlockingExecutionPolicy { # Returns the effective policy when it would block a locally-created .ps1, # otherwise $null. On a default Windows client this is 'Restricted'. try { $policy = Get-ExecutionPolicy if ("$policy" -in @('Restricted', 'AllSigned', 'Undefined')) { return "$policy" } } catch { } return $null } function Get-NodeVersionString { # Probe by invoking rather than via Get-Command: after a PATH refresh the # command-discovery cache can still be stale, but an actual spawn is truth. # # Deliberately does NOT consult $LASTEXITCODE. Piping a native command into # `Select-Object -First` stops the pipeline before PowerShell records the # exit code, so $LASTEXITCODE keeps whatever value it already held -- a # stale non-zero from an earlier failure then reads as "node is missing". # The output itself is the only trustworthy signal here. try { $out = (& node -v 2>&1) | Out-String if ($out -match 'v?(\d+\.\d+\.\d+)') { return "v$($Matches[1])" } } catch { } return $null } function Test-NodeVersionOk { param([string] $Raw) if ($Raw -notmatch '^v?(\d+)\.(\d+)\.(\d+)') { return $false } $major = [int] $Matches[1] $minor = [int] $Matches[2] # ^22.19.0 || >=24.0.0 -- note that 23.x does NOT qualify. return (($major -ge 24) -or ($major -eq 22 -and $minor -ge 19)) } # ------------------------------------------------------------------ setup --- function Invoke-DshSetup { Write-Host "" Write-Host " DeepSeek Harness -> Scan VM" -ForegroundColor White Write-Host " ---------------------------" -ForegroundColor DarkGray Write-Host "" # -- PowerShell ---------------------------------------------------------- Write-Step "Checking PowerShell" if ($PSVersionTable.PSVersion.Major -lt 7) { Write-Warn "Running Windows PowerShell $($PSVersionTable.PSVersion). dsh prefers PowerShell 7." Write-Note "dsh gives the agent a 'pwsh' tool on Windows and falls back to 5.1 only as" Write-Note "a last resort, where text encoding and language mode have rough edges." if (Get-Command pwsh -ErrorAction SilentlyContinue) { Write-Note "PowerShell 7 is already installed -- just use 'pwsh' instead of 'powershell'." } elseif (Confirm-Action "Install PowerShell 7 now with winget?") { if (Install-WithWinget 'Microsoft.PowerShell' 'PowerShell 7') { Update-PathFromRegistry Write-Ok "PowerShell 7 installed" # Nothing can move a running session to a different engine, so # this run continues on 5.1. That is fine -- 5.1 is a supported # fallback, and everything below works on it. Write-Note "This session is still 5.1. Use 'pwsh' for dsh work from now on." } } else { Write-Note "Skipped. Install later with: winget install Microsoft.PowerShell" } } else { Write-Ok "PowerShell $($PSVersionTable.PSVersion)" } # -- execution policy ---------------------------------------------------- # npm and dsh both install .ps1 shims alongside their .cmd ones, and # PowerShell prefers the .ps1. Under the default Restricted policy those # refuse to load -- so `dsh web` would fail even after a clean install. # This script itself is exempt: `irm | iex` evaluates a string, never a file. Write-Step "Checking execution policy" $blocking = Get-BlockingExecutionPolicy if ($null -eq $blocking) { Write-Ok "Execution policy allows local scripts ($(Get-ExecutionPolicy))" } else { Write-Warn "Execution policy is '$blocking', which blocks .ps1 files." Write-Note "npm and dsh both ship .ps1 shims, so typing 'dsh web' would fail" Write-Note "with 'running scripts is disabled on this system'." if (Confirm-Action "Set your user's policy to RemoteSigned? (no admin needed)") { try { Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned -Force Write-Ok "Execution policy for your user is now RemoteSigned" Write-Note "Locally-created scripts run; downloaded ones still need a signature." $script:PolicyBlocked = $false } catch { Write-Warn "Could not change the policy: $($_.Exception.Message)" $script:PolicyBlocked = $true } } else { Write-Note "Skipped. Install will still work -- this script calls npm.cmd directly." Write-Note "But you will need to type 'dsh.cmd web' rather than 'dsh web'." $script:PolicyBlocked = $true } } # -- Node ---------------------------------------------------------------- Write-Step "Checking Node.js" $nodeRaw = Get-NodeVersionString if ($null -eq $nodeRaw) { Write-Warn "Node.js is not installed (or not on PATH)." } elseif (-not (Test-NodeVersionOk $nodeRaw)) { Write-Warn "Node $nodeRaw is too old, or an unsupported major." Write-Note "dsh requires ^22.19.0 or >=24.0.0 -- note that Node 23 does NOT qualify." } if ($null -eq $nodeRaw -or -not (Test-NodeVersionOk $nodeRaw)) { $fixed = $false if (Confirm-Action "Install Node.js LTS now with winget?") { $null = Install-WithWinget 'OpenJS.NodeJS.LTS' 'Node.js LTS' # Judge by outcome, not by winget's verdict. The package may have # been installed all along (a stale probe brought us here), or have # installed correctly despite an unhelpful exit code. Update-PathFromRegistry $nodeRaw = Get-NodeVersionString $fixed = ($null -ne $nodeRaw) -and (Test-NodeVersionOk $nodeRaw) if (-not $fixed) { if ($null -ne $nodeRaw) { Write-Warn "Node is now $nodeRaw, which still does not satisfy the requirement." } else { # Installed, but this process cannot see it. A new terminal # picks up the PATH the installer wrote. Write-Warn "Node is still not visible to this session." Write-Note "Open a NEW terminal and re-run the one-liner -- it will pick up from here." } } } if (-not $fixed) { Write-Fail "Node.js ^22.19.0 or >=24.0.0 is required and not available." @( "Install it, open a NEW terminal, then re-run this script:", "", " winget install OpenJS.NodeJS.LTS", "", "or download from https://nodejs.org" ) } } Write-Ok "Node $nodeRaw" $npm = Resolve-Cli 'npm' if ($null -eq $npm) { Write-Fail "npm is not on PATH even though node is." @( "Repair your Node install, or open a new terminal so PATH refreshes." ) } # -- reachability -------------------------------------------------------- Write-Step "Checking the Scan VM" $modelsUrl = "$($Endpoint.TrimEnd('/'))/models" $served = @() try { $resp = Invoke-RestMethod -Uri $modelsUrl -TimeoutSec 8 $served = @($resp.data | ForEach-Object { $_.id }) Write-Ok "Reachable at $Endpoint" } catch { Write-Fail "Cannot reach the Scan VM at $Endpoint" @( "$($_.Exception.Message)", "", "Check in order:", " 1. Are you on the office network / VPN?", " 2. Test-NetConnection 10.2.2.7 -Port 48765", " 3. If TCP succeeds but this fails, the VM is up but vLLM is not --", " ask whoever owns the box to restart it." ) } if ($served -notcontains $ModelId) { Write-Warn "The endpoint does not currently serve '$ModelId'." Write-Note "It is serving: $($served -join ', ')" Write-Note "Continuing anyway -- settings.yaml will still be written for '$ModelId'." } else { Write-Ok "Model '$ModelId' is loaded" } # -- install ------------------------------------------------------------- $skipInstall = $false if ($ConfigOnly) { Write-Step "Skipping dsh install (-ConfigOnly)" } else { # The pinned version is the team's single source of truth: bump it on # the site, everyone re-runs the one-liner. Skipping a no-op reinstall # makes that re-run cheap enough that people actually do it. $installedVersion = Get-DshVersion if ($installedVersion -eq $DshVersion -and -not $Reinstall) { Write-Step "dsh is already at $DshVersion" Write-Note "Nothing to install. Re-run with -Reinstall to force it." $skipInstall = $true } else { $skipInstall = $false if ($null -eq $installedVersion) { Write-Step "Installing dsh@$DshVersion" Write-Note "This takes a minute or two on a first run." } else { Write-Step "Updating dsh $installedVersion -> $DshVersion" } } } if (-not $ConfigOnly -and -not $skipInstall) { # npm writes progress and warnings to stderr as a matter of course. # Under $ErrorActionPreference='Stop', Windows PowerShell 5.1 promotes # redirected stderr to a terminating NativeCommandError, so a perfectly # normal install would abort here. Relax the preference for the call # and judge success by the exit code instead. # npm 11 blocks dependency install scripts unless named. dsh needs # them: koffi builds the Windows ACL sandbox, node-pty the terminal, # and dsh-subprocess-local its spawn helper. Without these the agent # fails closed at the sandbox with SANDBOX_UNAVAILABLE. $allow = '@deepseek-ai/dsh-subprocess-local,koffi,node-pty,@google/genai,protobufjs' $prevEap = $ErrorActionPreference $ErrorActionPreference = 'Continue' try { # $npm is the .cmd shim, so this runs regardless of execution policy. & $npm install -g "--allow-scripts=$allow" "@deepseek-ai/dsh@$DshVersion" 2>&1 | ForEach-Object { Write-Note "$_" } } finally { $ErrorActionPreference = $prevEap } if ($LASTEXITCODE -ne 0) { Write-Fail "npm exited with code $LASTEXITCODE." @( "Run the command yourself to see the full error:", "", " npm.cmd install -g `"--allow-scripts=$allow`" `"@deepseek-ai/dsh@$DshVersion`"" ) } $verb = if ($null -eq $installedVersion) { 'installed' } else { 'updated to' } Write-Ok "dsh $verb $DshVersion" } # -- configuration ------------------------------------------------------- Write-Step "Writing model configuration" $dshHome = if ($env:DSH_HOME) { $env:DSH_HOME } else { Join-Path $env:USERPROFILE '.dsh' } if (-not (Test-Path $dshHome)) { New-Item -ItemType Directory -Path $dshHome -Force | Out-Null Write-Note "Created $dshHome" } $settingsPath = Join-Path $dshHome 'settings.yaml' $block = @" llm-pi-ai: providers: ${RouteId}: displayName: $DisplayName api: openai-completions baseURL: $Endpoint # The Scan VM ignores this key, but pi-ai's openai-completions protocol # refuses to dispatch without one ("No API key for provider"). The value # lives in $DSH_HOME/.credentials.yaml; this is only a reference to it. apiKeyEnv: SCAN_API_KEY # A private IP tells pi-ai nothing about the reasoning dialect, so # state it outright. 'openai' means a plain reasoning_effort field. compat: thinkingFormat: openai supportsReasoningEffort: true # Deployment default. The server's own default is xhigh, which burns # a lot of thinking tokens on a box the whole team shares. reasoning: $Reasoning # A 27B reasoning model can think for a while before its first token. streamIdleTimeoutMs: 600000 models: - id: $ModelId name: $ModelName contextWindow: 262144 maxTokens: 32768 input: [text, image] # This server accepts ONLY these levels. Do not add high/max/minimal # -- dsh would offer them and every request using one would fail. # 'off' maps to the wire value 'none'; sending nothing would instead # let the server apply its own default of xhigh. reasoningEfforts: off: none low: low medium: medium xhigh: xhigh "@ # UTF-8 with no BOM. Set-Content -Encoding UTF8 writes a BOM on PS 5.1, # and a BOM landing mid-file would corrupt an append. $utf8NoBom = New-Object System.Text.UTF8Encoding $false if (-not (Test-Path $settingsPath)) { [System.IO.File]::WriteAllText($settingsPath, $block, $utf8NoBom) Write-Ok "Created $settingsPath" } else { $existing = Get-Content -Path $settingsPath -Raw -ErrorAction SilentlyContinue if ($null -eq $existing) { $existing = '' } # empty file reads as $null if ($existing.Trim() -eq '') { [System.IO.File]::WriteAllText($settingsPath, $block, $utf8NoBom) Write-Ok "Wrote $settingsPath (was empty)" } elseif ($existing -notmatch '(?m)^\s*llm-pi-ai\s*:') { # No LLM section yet -- a new top-level YAML key appends cleanly. $sep = if ($existing.EndsWith("`n")) { "" } else { "`r`n" } [System.IO.File]::WriteAllText($settingsPath, $existing + $sep + "`r`n" + $block, $utf8NoBom) Write-Ok "Appended the '$RouteId' route to $settingsPath" } elseif ($Force) { $backup = "$settingsPath.bak-$(Get-Date -Format 'yyyyMMdd-HHmmss')" Copy-Item $settingsPath $backup [System.IO.File]::WriteAllText($settingsPath, $block, $utf8NoBom) Write-Warn "Replaced settings.yaml (-Force). Previous file: $backup" } else { # Merging YAML properly needs a parser we do not want to depend on, # so hand over an exact block rather than guessing at their file. $sidecar = Join-Path $dshHome 'settings.scan-route.yaml' [System.IO.File]::WriteAllText($sidecar, $block, $utf8NoBom) Write-Warn "settings.yaml already has an llm-pi-ai section -- not touching it." Write-Note "Wrote the route to: $sidecar" Write-Note "Merge the 'providers:' entry into your existing block by hand," Write-Note "or re-run with -Force to replace the section outright." } } # -- credential ---------------------------------------------------------- # pi-ai will not dispatch an openai-completions request without a key, even # to an endpoint that ignores it. settings.yaml holds only a reference # (apiKeyEnv), so the value has to exist somewhere: this file is it. # The document is a flat "REFERENCE: value" mapping and nothing else. Write-Step "Storing the placeholder credential" $credPath = Join-Path $dshHome '.credentials.yaml' $credLine = 'SCAN_API_KEY: local' if (-not (Test-Path $credPath)) { $header = "# The Scan VM ignores this value entirely -- it exists only because`r`n" + "# pi-ai will not dispatch a request without a key.`r`n" [System.IO.File]::WriteAllText($credPath, $header + $credLine + "`r`n", $utf8NoBom) Write-Ok "Created $credPath" } else { $cred = Get-Content -Path $credPath -Raw -ErrorAction SilentlyContinue if ($null -eq $cred) { $cred = '' } if ($cred -match '(?m)^\s*SCAN_API_KEY\s*:') { Write-Note "SCAN_API_KEY already present -- left as is." } else { $sep = if ($cred -eq '' -or $cred.EndsWith("`n")) { "" } else { "`r`n" } [System.IO.File]::WriteAllText($credPath, $cred + $sep + $credLine + "`r`n", $utf8NoBom) Write-Ok "Added SCAN_API_KEY to $credPath" } } # -- default model ------------------------------------------------------- # The base bundle defaults to DeepSeek's hosted API, which would demand a # DEEPSEEK_API_KEY. Repoint it so `dsh web` opens on the right model and # `dsh --profile headless` works with no further setup. Write-Step "Setting the default model" $patchPath = Join-Path $dshHome 'cordis.patch.yml' $patch = @" # Home-level patch layer. Applied after every profile's own patch, so it wins # for both ``dsh web`` and ``dsh --profile headless``. - id: agent-default-model config: provider: $RouteId model: $ModelId "@ if (-not (Test-Path $patchPath)) { [System.IO.File]::WriteAllText($patchPath, $patch, $utf8NoBom) Write-Ok "Created $patchPath" } else { $existingPatch = Get-Content -Path $patchPath -Raw -ErrorAction SilentlyContinue if ($null -eq $existingPatch) { $existingPatch = '' } if ($existingPatch -match '(?m)^\s*-\s*id:\s*agent-default-model\s*$') { Write-Note "cordis.patch.yml already sets a default model -- left as is." } else { $sep = if ($existingPatch.EndsWith("`n")) { "" } else { "`r`n" } [System.IO.File]::WriteAllText($patchPath, $existingPatch + $sep + "`r`n" + $patch, $utf8NoBom) Write-Ok "Appended the default-model row to $patchPath" } } # -- verify -------------------------------------------------------------- if (-not $ConfigOnly) { Write-Step "Verifying" Update-PathFromRegistry $dshCli = Resolve-Cli 'dsh' if ($null -ne $dshCli) { $prevEap = $ErrorActionPreference $ErrorActionPreference = 'Continue' try { $v = (& $dshCli --version 2>&1 | Select-Object -First 1) } finally { $ErrorActionPreference = $prevEap } Write-Ok "dsh on PATH ($v)" } else { Write-Warn "dsh is installed but not on PATH in THIS shell." Write-Note "Open a new terminal, or use 'npx @deepseek-ai/dsh web'." } } # -- done ---------------------------------------------------------------- Write-Host "" Write-Host " Done." -ForegroundColor Green Write-Host "" # With .ps1 shims blocked, a bare 'dsh' would not launch. Name the shim # that does rather than printing an instruction that fails. $cli = if ($script:PolicyBlocked) { 'dsh.cmd' } else { 'dsh' } Write-Host " Next:" -ForegroundColor White Write-Host " cd C:\path\to\your\project" Write-Host " $cli web" Write-Host "" Write-Host " Then open http://127.0.0.1:3080 and click 'Choose workspace' to pick your" Write-Host " project directory. '$ModelName' is already the default model." Write-Host "" Write-Host " One-shot tasks work too, no browser needed:" -ForegroundColor DarkGray Write-Host " $cli --profile headless `"summarize this repo`"" -ForegroundColor DarkGray Write-Host "" if ($script:PolicyBlocked) { Write-Host " Note: you declined the execution-policy change, so use '$cli'." -ForegroundColor Yellow Write-Host " To switch to plain 'dsh' later:" -ForegroundColor Yellow Write-Host " Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned" -ForegroundColor Yellow Write-Host "" } Write-Host " Reasoning is set to '$Reasoning'. Use xhigh when you are stuck, off for" -ForegroundColor DarkGray Write-Host " mechanical work. high/max/minimal are NOT valid on this server." -ForegroundColor DarkGray Write-Host "" if ($script:Warnings.Count -gt 0) { Write-Host " $($script:Warnings.Count) warning(s) above are worth a look." -ForegroundColor Yellow Write-Host "" } } # ------------------------------------------------------------------- entry --- try { Invoke-DshSetup } catch { # A Write-Fail has already printed a formatted explanation; anything else # is unexpected and worth surfacing raw. if ("$($_.Exception.Message)" -ne $script:AbortToken) { Write-Host "" Write-Host " [x] Unexpected error: $($_.Exception.Message)" -ForegroundColor Red Write-Host " $($_.InvocationInfo.PositionMessage)" -ForegroundColor DarkGray Write-Host "" } Write-Host " Setup did not complete." -ForegroundColor Red Write-Host "" }