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
+3
View File
@@ -26,6 +26,9 @@ void cst_perms_suggest_prefix(const char *cmd, char *out, size_t out_sz);
/* 1 when every segment of cmd (split on | || && ;) is a read-only command
* from a fixed whitelist, with no output redirection or substitution. */
int cst_shell_is_safe(const char *cmd);
/* 1 when cmd looks like it talks to the network (curl, ssh, git push,
* npm install, ...). Errs on the side of yes. */
int cst_shell_uses_network(const char *cmd);
#ifdef __cplusplus
}
+19 -3
View File
@@ -12,6 +12,10 @@
* against mistakes, not hostile code (the path is checked before
* the kernel reads it again).
* macOS sandbox_init (Seatbelt profile)
* Windows Low integrity level: a restricted token (no admin privileges,
* Low IL) can only write objects labeled Low, so the workspace and
* a temp dir get that label; the credentials file is labeled
* no-read-up. The network stays open (callers ask first).
* other none: callers fall back to rule-based approval
*/
@@ -27,15 +31,16 @@ typedef enum {
CST_SANDBOX_NONE = 0,
CST_SANDBOX_LANDLOCK,
CST_SANDBOX_SEATBELT,
CST_SANDBOX_SECCOMP
CST_SANDBOX_SECCOMP,
CST_SANDBOX_LOWIL
} cst_sandbox_kind_t;
/* What this process can use; cached. CST_SANDBOX=off|0|none forces NONE,
* CST_SANDBOX=landlock|seccomp allows only that kind. */
* CST_SANDBOX=landlock|seccomp|lowil allows only that kind. */
int cst_sandbox_probe(void);
/* Drops the cache and probes again (tests that change CST_SANDBOX). */
int cst_sandbox_reprobe(void);
/* "landlock" | "seatbelt" | "seccomp" | "none" */
/* "landlock" | "seatbelt" | "seccomp" | "lowil" | "none" */
const char *cst_sandbox_name(int kind);
/* Landlock ABI version (0 when unavailable); ≥4 also blocks TCP. */
int cst_sandbox_landlock_abi(void);
@@ -45,6 +50,9 @@ typedef struct {
int ruleset_fd; /* Landlock, built in the parent */
char *profile; /* Seatbelt */
void *sc; /* seccomp: filter program and supervisor thread */
void *token; /* lowil: primary token for CreateProcessAsUserW */
char *tmpdir; /* lowil: Low-labeled TEMP for the child */
char *note; /* lowil: shown once when a directory was just labeled */
} cst_sandbox_plan_t;
/* Parent side: build the policy. Returns 0, or -1 (plan->kind = NONE). */
@@ -64,6 +72,14 @@ int cst_sandbox_run_shell(const char *workspace, const char *cmd,
* way (EACCES / EPERM / EROFS / blocked network). */
int cst_sandbox_blocked(const char *out, size_t n, int exit_code);
/* lowil: directories this user's cst-agent labeled Low (recorded in the
* config dir), one per line; malloc'd, NULL when there are none. */
char *cst_sandbox_labels(void);
/* Gives every recorded directory its old label back. Returns how many were
* restored (-1 when some failed; those stay recorded); *report gets a
* malloc'd summary. */
int cst_sandbox_reset_labels(char **report);
#ifdef __cplusplus
}
#endif
+21
View File
@@ -25,6 +25,27 @@ char *cst_buf_steal(cst_buf_t *b);
size_t cst_utf8_floor(const char *s, size_t n);
/* Copy s[0..n) replacing invalid UTF-8 with '?'. malloc'd, NUL-terminated. */
char *cst_utf8_sanitize(const char *s, size_t n, size_t *out_len);
int cst_utf8_valid(const char *s, size_t n);
/* Text files may start with a UTF-8 BOM (Windows editors): its length (0/3),
* and an in-place strip of the NUL-terminated s (*len updated when given). */
size_t cst_bom_len(const char *s, size_t n);
void cst_strip_bom(char *s, size_t *len);
/* Line endings: "\r\n" -> "\n" in place (returns the new length); whether
* every line of s ends in "\r\n" (and at least one does); and a malloc'd
* copy of s with bare "\n" turned into "\r\n" when crlf, its BOM dropped and
* a BOM added when bom. Edits keep a Windows file's style with these. */
size_t cst_crlf_to_lf(char *s, size_t n);
int cst_text_is_crlf(const char *s, size_t n);
char *cst_text_restyle(const char *s, size_t n, int crlf, int bom,
size_t *out_len);
/* fnmatch(3) without FNM_PATHNAME ('*' also crosses '/'): * ? [..] [!..]
* [[:class:]] and backslash escapes over UTF-8 code points. Returns 0 on a
* match. Always case-insensitive on Windows. */
#define CST_FNM_PERIOD 1 /* a leading '.' must be matched literally */
#define CST_FNM_CASEFOLD 2
int cst_fnmatch(const char *pat, const char *str, int flags);
uint64_t cst_fnv1a64(uint64_t h, const void *data, size_t n);
#define CST_FNV_INIT 1469598103934665603ULL
+9 -1
View File
@@ -1,6 +1,14 @@
#ifndef CST_VERSION_H
#define CST_VERSION_H
#define CST_VERSION "0.5.0"
#define CST_VERSION_MAJOR 0
#define CST_VERSION_MINOR 6
#define CST_VERSION_PATCH 0
#define CST_VERSION_STR_(x) #x
#define CST_VERSION_STR(x) CST_VERSION_STR_(x)
#define CST_VERSION \
CST_VERSION_STR(CST_VERSION_MAJOR) \
"." CST_VERSION_STR(CST_VERSION_MINOR) "." CST_VERSION_STR(CST_VERSION_PATCH)
#endif
+66 -2
View File
@@ -22,6 +22,28 @@ int os_realpath(const char *path, char *out, size_t out_sz);
/* True if path is under root (after realpath/norm). Trailing slash rules apply. */
int os_path_under(const char *root, const char *path);
/* --- directory listing / file type --- */
typedef struct os_dir os_dir_t;
typedef struct {
const char *name; /* UTF-8; valid until the next os_dir_next */
int is_dir;
int is_link; /* symlink, or a junction on Windows (not followed) */
} os_dirent_t;
os_dir_t *os_dir_open(const char *path);
/* Next entry, skipping "." and ".."; NULL at the end. */
const os_dirent_t *os_dir_next(os_dir_t *d);
void os_dir_close(os_dir_t *d);
typedef struct {
int is_dir, is_reg, is_link;
long long size;
double mtime; /* seconds since the epoch */
} os_stat_t;
/* Like lstat(2): a link is reported as a link, not followed. */
int os_lstat(const char *path, os_stat_t *st);
/* Replace `to` if it exists (atomic where the OS allows). */
int os_rename_replace(const char *from, const char *to);
/* --- dirs / config --- */
int os_mkdir(const char *path, int mode);
int os_mkdir_p(const char *path, int mode);
@@ -67,6 +89,28 @@ int os_run_shell_ex(const char *cwd, const char *cmd, size_t max_out,
os_run_result_t *r, os_child_hook_fn child_hook,
void *ud);
#ifdef _WIN32
/* Windows: run under `token` (a primary token, e.g. the Low-IL sandbox
* token; NULL = ours) with env_add "KEY=VALUE" entries layered on the
* inherited environment. Every child runs in a kill-on-close Job Object, so
* timeouts and cancels end the whole process tree. */
int os_run_capture_token(const char *cwd, const char *const argv[],
size_t max_out, os_run_result_t *r, void *token,
const char *const env_add[]);
/* os_run_shell_c under token, with env_add. */
int os_run_shell_token(const char *cwd, const char *cmd, size_t max_out,
os_run_result_t *r, void *token,
const char *const env_add[]);
/* "Windows 10.0.26100 x86_64" (the real version, not the compat shim's). */
void os_windows_version(char *out, size_t n);
/* Sets the mandatory-label SACL, e.g. "S:(ML;OICI;NW;;;LW)"; 0 / -1. */
int os_win_set_label(const char *path, const char *sddl);
/* The explicit (not inherited) label as SDDL, "S:" when there is none. */
int os_win_get_label(const char *path, char *out, size_t n);
/* CF_UNICODE_TEXT clipboard (the console may not do OSC 52). */
int os_clipboard_set(const char *utf8, size_t n);
#endif
/* Child processes get stdin=/dev/null, their own process group, and are
* killed after the timeout (CST_SHELL_TIMEOUT seconds, default 120) or when
* *flag becomes non-zero. Exit code 124 = timeout, 130 = cancelled. */
@@ -80,7 +124,7 @@ int os_get_run_timeout(void);
* the inherited environment (NULL-terminated, may be NULL). */
typedef struct {
#ifdef _WIN32
void *proc, *in_w, *out_r;
void *proc, *in_w, *out_r, *job;
#else
int pid, in_fd, out_fd;
#endif
@@ -106,10 +150,30 @@ void os_run_parallel(os_task_fn fn, void *const args[], int n);
/* Monotonic milliseconds (arbitrary epoch). */
double os_now_ms(void);
/* --- text encoding ---
* cst-agent is UTF-8 throughout. On Windows the manifest makes UTF-8 the
* process code page; os_console_init() also switches the console to UTF-8
* (restored at exit), sets a UTF-8 C locale and asks Python children for
* UTF-8. Returns -1 when the process code page is not UTF-8 (pre-1903
* Windows); no-op returning 0 elsewhere. */
int os_console_init(void);
/* Child output that is not valid UTF-8, decoded from the console/OEM/ANSI
* code page: malloc'd UTF-8, or NULL when s is already UTF-8 (or not on
* Windows). */
char *os_output_to_utf8(const char *s, size_t n, size_t *out_len);
#ifdef _WIN32
#include <wchar.h>
/* malloc'd conversions; NULL on invalid input or allocation failure */
wchar_t *os_utf8_to_wide(const char *s);
char *os_wide_to_utf8(const wchar_t *w);
#endif
/* --- misc --- */
void os_localtime(const time_t *t, struct tm *out);
int os_getpid(void);
int os_chmod_private(const char *path); /* best-effort 0600; no-op/ok on Win */
/* 0600; on Windows a Medium no-read-up label, so Low-IL children (the
* shell sandbox) cannot read the file. */
int os_chmod_private(const char *path);
/* --- UTF-8 --------------------------------------------------------------
* Encode one code point as UTF-8, independent of the C locale.