Windows: official port -- UCRT static build, UTF-8 throughout, Low IL sandbox, ncurses TUI, installer; v0.6.0
ci / linux (clang) (push) Canceled after 0s
ci / linux (gcc) (push) Canceled after 0s
ci / sanitizers (push) Canceled after 0s
ci / musl (push) Canceled after 0s
ci / macos (push) Canceled after 0s

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
+23 -40
View File
@@ -14,9 +14,6 @@
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#ifndef _WIN32
#include <dirent.h>
#endif
#include "cJSON.h"
#include "cst_agent.h"
@@ -79,6 +76,7 @@ static char *slurp(const char *path, size_t max) {
fclose(fp);
if (!b.buf)
cst_buf_puts(&b, "");
cst_strip_bom(b.buf, &b.len);
return cst_buf_steal(&b);
}
@@ -118,25 +116,24 @@ static int truthy(const char *v) {
static void load_dir(const char *dir, const char *source, cst_agent_role_t *v,
int *n, int max) {
#ifndef _WIN32
DIR *d = opendir(dir);
os_dir_t *d = os_dir_open(dir);
if (!d)
return;
struct dirent *de;
while ((de = readdir(d)) != NULL && *n < max) {
size_t len = strlen(de->d_name);
if (de->d_name[0] == '.' || len < 4 ||
strcmp(de->d_name + len - 3, ".md") != 0)
const os_dirent_t *de;
while ((de = os_dir_next(d)) != NULL && *n < max) {
size_t len = strlen(de->name);
if (de->name[0] == '.' || len < 4 ||
strcmp(de->name + len - 3, ".md") != 0)
continue;
char p[OS_PATH_MAX];
if (snprintf(p, sizeof(p), "%s/%s", dir, de->d_name) >= (int)sizeof(p))
if (snprintf(p, sizeof(p), "%s/%s", dir, de->name) >= (int)sizeof(p))
continue;
char *text = slurp(p, ROLE_FILE_MAX);
if (!text)
continue;
char name[64], val[1024];
if (!cst_fm_get(text, "name", name, sizeof(name)) || !name[0])
snprintf(name, sizeof(name), "%.*s", (int)(len - 3), de->d_name);
snprintf(name, sizeof(name), "%.*s", (int)(len - 3), de->name);
char clean[64];
cst_tool_sanitize_name(name, clean, sizeof(clean));
int at = find_role(v, *n, clean);
@@ -166,14 +163,7 @@ static void load_dir(const char *dir, const char *source, cst_agent_role_t *v,
r->prompt = strdup(body);
free(text);
}
closedir(d);
#else
(void)dir;
(void)source;
(void)v;
(void)n;
(void)max;
#endif
os_dir_close(d);
}
int cst_roles_list(const char *ws, cst_agent_role_t *out, int max) {
@@ -255,18 +245,16 @@ int cst_subagent_path(const char *ws, const char *parent_id, const char *file,
static int next_file(const char *dir, char *file, size_t fsz) {
pthread_mutex_lock(&g_seq_mu);
int best = 0;
#ifndef _WIN32
DIR *d = opendir(dir);
os_dir_t *d = os_dir_open(dir);
if (d) {
struct dirent *de;
while ((de = readdir(d)) != NULL) {
const os_dirent_t *de;
while ((de = os_dir_next(d)) != NULL) {
int k = 0;
if (sscanf(de->d_name, "sub-%d.jsonl", &k) == 1 && k > best)
if (sscanf(de->name, "sub-%d.jsonl", &k) == 1 && k > best)
best = k;
}
closedir(d);
os_dir_close(d);
}
#endif
int rc = -1;
for (int k = best + 1; k < best + 100; k++) {
char p[OS_PATH_MAX];
@@ -276,7 +264,7 @@ static int next_file(const char *dir, char *file, size_t fsz) {
struct stat st;
if (stat(p, &st) == 0)
continue;
FILE *fp = fopen(p, "a");
FILE *fp = fopen(p, "ab");
if (fp) {
fclose(fp);
rc = 0;
@@ -302,7 +290,7 @@ static void write_meta(const char *dir, const char *file, const char *role,
cJSON_AddNumberToObject(o, "ms", ms);
char *txt = cJSON_PrintUnformatted(o);
cJSON_Delete(o);
FILE *fp = txt ? fopen(p, "w") : NULL;
FILE *fp = txt ? fopen(p, "wb") : NULL;
if (fp) {
fputs(txt, fp);
fputc('\n', fp);
@@ -317,19 +305,18 @@ int cst_subagent_list(const char *ws, const char *parent_id,
if (sub_dir(ws, parent_id, dir, sizeof(dir)) != 0)
return 0;
int n = 0;
#ifndef _WIN32
DIR *d = opendir(dir);
os_dir_t *d = os_dir_open(dir);
if (!d)
return 0;
struct dirent *de;
while ((de = readdir(d)) != NULL && n < max) {
const os_dirent_t *de;
while ((de = os_dir_next(d)) != NULL && n < max) {
int k = 0;
char tail[8] = "";
if (sscanf(de->d_name, "sub-%d.%7s", &k, tail) != 2 ||
if (sscanf(de->name, "sub-%d.%7s", &k, tail) != 2 ||
strcmp(tail, "json") != 0)
continue;
char p[OS_PATH_MAX];
if (snprintf(p, sizeof(p), "%s/%s", dir, de->d_name) >= (int)sizeof(p))
if (snprintf(p, sizeof(p), "%s/%s", dir, de->name) >= (int)sizeof(p))
continue;
char *txt = slurp(p, 64 * 1024);
cJSON *o = txt ? cJSON_Parse(txt) : NULL;
@@ -358,11 +345,7 @@ int cst_subagent_list(const char *ws, const char *parent_id,
r->mtime = (long long)st.st_mtime;
n++;
}
closedir(d);
#else
(void)out;
(void)max;
#endif
os_dir_close(d);
/* newest first: higher number */
for (int i = 1; i < n; i++)
for (int j = i; j > 0 && atoi(out[j - 1].file + 4) < atoi(out[j].file + 4);