Windows: official port -- UCRT static build, UTF-8 throughout, Low IL sandbox, ncurses TUI, installer; v0.6.0

Build
  * cross build switches to pkgsCross.ucrt64 (shell-win.nix); curl (Schannel),
    winpthreads and ncurses 6.6 are linked statically, so the release is
    cst-agent.exe + busybox.exe (busybox-w32 Unicode) with no extra DLLs
  * windres embeds a manifest with activeCodePage=UTF-8 and a version resource
  * ncurses 6.6 Windows console driver patches (mouse button state, A_DIM,
    wide-char declarations): scripts/patches/
  * scripts/bootstrap_msys2.sh builds the same release natively in MSYS2
    UCRT64; PR #1's MINGW64 notes now point there
  * vendor/tre-regex (musl's TRE) provides <regex.h> on MinGW

UTF-8
  * paths cross the system call boundary as UTF-16 (*W APIs); console code
    pages set to 65001 and restored on exit; files are read and written in
    binary mode
  * child output that is not valid UTF-8 is decoded from the console, OEM or
    ANSI system code page
  * read_file hides CR; edit_file, apply_patch and write_file keep CRLF line
    endings and a UTF-8 BOM

OS layer and processes
  * os_dir_* and cst_fnmatch replace the POSIX-only directory walks in
    subagent, plugins, skills, undo and fs_tools; realpath / path_under and
    symlink rejection work on Windows
  * every child goes through CreateProcessW with its own environment block
    inside a Job Object, so timeouts and Esc end the whole process tree

Sandbox (auto mode, "auto · lowil")
  * run_shell runs with a restricted Low integrity token; the workspace gets
    an inheritable Low label once, the sandbox TEMP is labeled Low, and the
    credentials file gets a no-read-up label
  * drive roots, the profile and system folders are never labeled
  * network commands still ask, since Low IL does not block the network;
    Access denied and MSYS2's 0xc0000142 offer a rerun outside the sandbox
  * /sandbox lists labeled folders, /sandbox reset-labels restores them

TUI
  * raster console fonts are switched to Consolas / NSimSun while running
  * the home logo falls back to text when the console font lacks U+2580
  * mouse drag copy goes to the Windows clipboard

Distribution
  * scripts/install_windows.ps1 installs a Gitea release into
    %LOCALAPPDATA%\Programs\cst-agent and adds it to the user PATH;
    cst-agent-update.cmd reruns it; scripts/release_windows.sh uploads the
    zip with the token taken from the environment
  * scripts/run_windows.bat is gone; docs/README-windows.md is rewritten

Tests
  * new test_lowil, test_win_utf8 and test_os_dir; tests made portable
    (cst_test_util.h)
  * scripts/win_test.sh and scripts/winvm/ run the suite on English and
    Chinese Windows Server VMs as Administrator, a standard user and a
    Chinese user name: 29/29 on all six
This commit is contained in:
WHC
2026-10-06 23:08:14 +08:00
parent 9862c29f58
commit 2a43ae6622
84 changed files with 9784 additions and 1083 deletions
+106
View File
@@ -0,0 +1,106 @@
# Install or update CST-Agent on Windows from a Gitea release.
#
# irm https://git.whcself.xyz/WHC/CST-agent/raw/branch/main/scripts/install_windows.ps1 | iex
# powershell -ExecutionPolicy Bypass -File install_windows.ps1 [-Version 0.6.0] [-Prefix DIR] [-Zip FILE] [-NoPath]
#
# Installs into %LOCALAPPDATA%\Programs\cst-agent (no admin rights needed),
# adds it to the user PATH and leaves cst-agent-update.cmd beside the exe,
# which reruns this script for that folder. Environment overrides:
# CST_VERSION, CST_PREFIX, CST_GITEA (server URL), CST_REPO (owner/name).
# Needs Windows 10 1903+ / Server 2022+ (UTF-8 process code page).
# Keep this file ASCII: Windows PowerShell 5.1 reads BOM-less scripts in the
# ANSI code page.
param(
[string]$Version = $env:CST_VERSION,
[string]$Prefix = $env:CST_PREFIX,
[string]$Zip = "",
[switch]$NoPath
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$Server = if ($env:CST_GITEA) { $env:CST_GITEA.TrimEnd("/") } else { "https://git.whcself.xyz" }
$Repo = if ($env:CST_REPO) { $env:CST_REPO } else { "WHC/CST-agent" }
if (-not $Prefix) { $Prefix = Join-Path $env:LOCALAPPDATA "Programs\cst-agent" }
$Prefix = [IO.Path]::GetFullPath($Prefix)
$build = [Environment]::OSVersion.Version.Build
if ($build -lt 18362) {
throw "CST-Agent needs Windows 10 1903 (build 18362) or newer; this is build $build."
}
$work = Join-Path ([IO.Path]::GetTempPath()) ("cst-agent-install-" + [Guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Force $work | Out-Null
try {
if (-not $Zip) {
$api = "$Server/api/v1/repos/$Repo/releases"
$url = if (-not $Version -or $Version -eq "latest") { "$api/latest" } else { "$api/tags/v$($Version.TrimStart('v'))" }
Write-Host "==> $url"
try {
$rel = Invoke-RestMethod -Uri $url -UseBasicParsing
} catch {
throw "no release found at $url ($($_.Exception.Message))"
}
$asset = $rel.assets | Where-Object { $_.name -like "*-windows-x86_64.zip" } | Select-Object -First 1
if (-not $asset) { throw "release $($rel.tag_name) has no *-windows-x86_64.zip" }
$Zip = Join-Path $work $asset.name
Write-Host "==> download $($asset.name) ($($rel.tag_name))"
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $Zip -UseBasicParsing
$sum = $rel.assets | Where-Object { $_.name -eq "$($asset.name).sha256" } | Select-Object -First 1
if ($sum) {
$sumFile = Join-Path $work $sum.name
Invoke-WebRequest -Uri $sum.browser_download_url -OutFile $sumFile -UseBasicParsing
$want = ((Get-Content $sumFile -Raw).Trim() -split "\s+")[0].ToLower()
$got = (Get-FileHash -Algorithm SHA256 $Zip).Hash.ToLower()
if ($want -ne $got) { throw "sha256 mismatch for $($asset.name): expected $want, got $got" }
Write-Host "==> sha256 ok"
}
}
Expand-Archive -Path $Zip -DestinationPath $work -Force
$src = Join-Path $work "windows-x86_64"
if (-not (Test-Path (Join-Path $src "cst-agent.exe"))) { throw "$Zip has no windows-x86_64\cst-agent.exe" }
New-Item -ItemType Directory -Force $Prefix | Out-Null
# A running exe cannot be overwritten but can be renamed out of the way.
foreach ($name in @("cst-agent.exe", "busybox.exe")) {
$dst = Join-Path $Prefix $name
$old = "$dst.old"
if (Test-Path $old) { Remove-Item -Force $old -ErrorAction SilentlyContinue }
if (Test-Path $dst) {
try { Remove-Item -Force $dst } catch { Rename-Item -Force $dst "$name.old" }
}
}
Copy-Item -Force -Recurse (Join-Path $src "*") $Prefix
$update = Join-Path $Prefix "cst-agent-update.cmd"
if (-not (Test-Path $update)) {
Set-Content -Encoding Ascii -Path $update -Value @(
"@echo off",
"rem Update CST-Agent in this folder from the latest release.",
"powershell -NoProfile -ExecutionPolicy Bypass -File `"%~dp0install_windows.ps1`" -Prefix `"%~dp0.`" %*"
)
}
if (-not (Test-Path (Join-Path $Prefix "install_windows.ps1")) -and $PSCommandPath) {
Copy-Item -Force $PSCommandPath (Join-Path $Prefix "install_windows.ps1")
}
if (-not $NoPath) {
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
$parts = @($userPath -split ";" | Where-Object { $_ })
if (-not ($parts | Where-Object { $_.TrimEnd("\") -ieq $Prefix.TrimEnd("\") })) {
[Environment]::SetEnvironmentVariable("Path", (($parts + $Prefix) -join ";"), "User")
Write-Host "==> added $Prefix to the user PATH (open a new terminal to use it)"
}
if (-not (($env:Path -split ";") | Where-Object { $_.TrimEnd("\") -ieq $Prefix.TrimEnd("\") })) {
$env:Path = "$env:Path;$Prefix"
}
}
$ver = (& (Join-Path $Prefix "cst-agent.exe") --version) 2>&1 | Select-Object -First 1
Write-Host "==> installed $ver in $Prefix"
Write-Host "Run 'cst-agent' inside a project folder; update later with 'cst-agent-update'."
} finally {
Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue
}