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
9.4 KiB
Platform support: Linux, WSL, macOS
CST-Agent targets POSIX systems: Linux (glibc and musl), WSL 2 and macOS.
Native Windows builds are covered separately in README-windows.md.
Build
| Platform | Dependencies | Build |
|---|---|---|
| Debian / Ubuntu / WSL | build-essential pkg-config libcurl4-openssl-dev libncurses-dev |
make |
| Fedora | gcc make pkgconf libcurl-devel ncurses-devel |
make |
| Arch | base-devel curl ncurses |
make |
| Alpine (musl) | build-base pkgconf curl-dev ncurses-dev |
make |
| NixOS | nix-shell (uses shell.nix) |
make |
| macOS | xcode-select --install, brew install ncurses curl pkg-config |
make |
make install PREFIX=~/.local installs the binary; CMake works too
(cmake -B build && cmake --build build && ctest --test-dir build).
The Makefile picks ncursesw, then ncurses, through pkg-config. On macOS it
adds Homebrew's keg-only ncurses/curl to PKG_CONFIG_PATH; without
Homebrew it falls back to the system -lncurses (wide-character capable).
Verified so far: NixOS x86_64 with gcc and clang (-Werror, make check,
make asan, ctest), and every source compiled -fsyntax-only with
x86_64-unknown-linux-musl-gcc without warnings. Full musl, macOS and WSL
builds run in CI (.gitea/workflows/ci.yml; the macOS job needs a runner and
the HAVE_MACOS_RUNNER variable) or by hand with the checklist below.
Files
| What | Where |
|---|---|
API keys (one per provider, entered in the TUI or with --ask-key) |
$XDG_CONFIG_HOME/cst-agent/credentials (default ~/.config/cst-agent/), mode 0600 |
| Last provider, models, effort and approval mode | $XDG_CONFIG_HOME/cst-agent/settings.json |
| Sessions, undo snapshots, exports | <workspace>/.agent/ (git-ignored automatically) |
| Saved permission rules | <workspace>/.agent/permissions.json |
| Project notes injected into the prompt | <workspace>/AGENTS.md (or CST.md) |
Shell sandbox
In the default auto approval mode, run_shell runs inside an OS sandbox
when one is available. Commands may read and execute anything but may only
write beneath the workspace, the temp directory, /dev/null and /dev/tty.
| Platform | Sandbox | Notes |
|---|---|---|
| Linux 5.13+ | Landlock (raw syscalls, no extra library) | ABI ≥ 4 (6.7+) also blocks TCP connect/bind |
| Linux 5.5+ without Landlock (Android / Termux, WSL, older distros), x86_64 / aarch64 | seccomp user-notify | refuses IPv4/IPv6 sockets; see below |
| macOS | sandbox_init profile |
also denies network |
| Windows 10 1903+ | Low integrity restricted token + Job Object | writes need a Low label (given to the workspace once); network is not blocked, so network commands still ask; see README-windows.md |
| Older kernels | none | falls back to the read-only rules |
The status bar and sidebar show auto · landlock, auto · seccomp,
auto · seatbelt, auto · lowil or auto · rules. CST_SANDBOX=off
disables the sandbox; CST_SANDBOX=landlock, seccomp or lowil allows
only that kind. When a sandboxed command looks blocked, the TUI asks whether to rerun
it outside the sandbox. The policy is built in the parent; the forked child
only makes a few async-signal-safe syscalls before exec, so the agent
itself is never confined.
seccomp. Used when Landlock is missing, e.g. Android GKI kernels. The
child installs a seccomp filter with a user-notify listener and passes the
listener to a supervisor thread in the agent. Opens with write flags and
every create / remove / rename / link / chmod / chown / utimes / xattr call
stop until the supervisor has read the path from the child's memory,
resolved it (cwd, dirfd, .., symlinks, /proc/self, /dev/stdout) and
answered: continue when it lands in the allowed places, EACCES otherwise.
Reads, exec and writes to already-open files are not checked. Background
jobs that outlive their shell are no longer the agent's descendants: with
Yama ptrace_scope ≥ 1 (common on desktop distros, absent on Android) the
supervisor cannot read their memory and their checked writes fail with
EACCES; once the run has finished they get ENOSYS. This keeps
mistakes out of the rest of the disk but is not a security boundary: the
kernel reads the path again after the check, so a hostile multi-threaded
program can swap it in between. The availability check runs in a
throwaway child, so a platform seccomp policy that kills the caller cannot
take the agent down.
Terminal notes
- UTF-8 locale gives the full UI. Under a non-UTF-8 locale, or with
CST_ASCII=1, box and icon glyphs fall back to ASCII. NO_COLOR=1turns colours off./copyand mouse selection use OSC 52. iTerm2 needs "Applications in terminal may access clipboard"; tmux needsset -g set-clipboard on; Windows Terminal (WSL), kitty, WezTerm, foot and WHC Shell support it out of the box.- Mouse selection: the TUI turns on button-motion reporting (
?1002h) on top of what ncurses enables, so dragging over the transcript selects text (double-click a word, triple-click a line) and releasing copies it. Terminals without 1002 still get clicks and the wheel. Shift+drag bypasses the TUI and uses the terminal's own selection. - Bracketed paste needs ncurses (all platforms above).
- macOS Terminal.app sends Option+Enter as Esc+Enter only with "Use Option as Meta key"; Ctrl+N always inserts a newline.
- Kitty keyboard protocol: at startup the TUI sends
CSI ? u; if the terminal answers (kitty, WezTerm, foot, Ghostty, WHC Shell, ...) it pushes the "disambiguate" flag (CSI > 1 u) on the alternate screen and pops it on exit. That makes Shift+Enter / Ctrl+Enter (newline), Ctrl+Backspace (delete word) and an instant Esc work. Other terminals (and tmux) don't answer and keep the legacy keys. If the TUI is killed, the main screen's keyboard mode is untouched because the flags live on the alternate screen's stack.
Acceptance checklist
Run on each platform before a release. Use a scratch git repository as the workspace and a real API key (entered when the TUI asks for it).
Build and tests:
makehas no warnings;make checkpassesmake asanpasses (Linux; macOS:make CFLAGS="-fsanitize=address,undefined -g" check)cmake -B build && cmake --build build && ctest --test-dir buildpassesmake install PREFIX=/tmp/cst && /tmp/cst/bin/cst-agent --help
CLI:
--list-sessions,--new,--resume <prefix>,--continuebehave as documented--headless --prompt "list the files"prints a transcript and exits 0- First start asks for a provider and key; the credentials file is mode 0600
- Second start reuses provider, model and effort without asking
TUI:
- Typing Chinese/emoji keeps the cursor in the right column; ←→ Home End work
- Pasting a multi-line block does not send it; Enter sends
/shows the command popup, Tab completes,@completes file paths- Esc cancels a running turn; Esc on an empty prompt does nothing; Ctrl+C twice quits
- A long
run_shell(e.g.sleep 30) stops promptly on Esc - Write permission shows the diff;
asaves a rule;psaves a shell prefix rule ask_usermodal accepts a number, an arrow selection and typed text- Todo panel appears while tasks are open; Ctrl+T hides it
/undorestores files changed by the last turn;/retryre-asks/diff,/export,/status,/tools off grep,/yolowork- Mouse: click a tool card to expand it, click in the input to move the
cursor, click a picker row;
/mouseturns it off - Drag over a wrapped reply: it highlights, the status row says "Copied", and pasting gives the paragraph without extra line breaks; double-click copies a word
- Tab toggles Build/Plan, Shift+Tab cycles ask/auto/yolo, Ctrl+P opens the palette
/modellists the provider's/models(marked(api)); a wrong key is re-askedautomode:echo x > ~/outsidein run_shell asks "Rerun outside the sandbox?" (Linux/Termux/macOS/Windows); withCST_SANDBOX=off,lsruns andtouch xasks- Resize the terminal during streaming: layout recovers
NO_COLOR=1andCST_ASCII=1render readable screens
WSL specifics:
- Workspace under
/mnt/c/...works (slower IO is expected);~/projectis recommended - Windows Terminal: mouse wheel scroll and OSC 52
/copy
Windows specifics (scripts/win_test.sh runs the tests on English and
Chinese VMs as Administrator, a standard user and a Chinese user name):
install_windows.ps1 -Zip dist\cst-agent-*.zipinstalls, adds the user PATH;cst-agent-update -Zip ...works whilecst-agent.exeruns- conhost with a raster font: Chinese renders (font switched), and the old font is back after exit; Windows Terminal renders the same
- Chinese input via IME, paste, mouse click / drag-copy / wheel
- Workspace under
C:\Users\<中文名>\项目: read/write/edit/apply_patch keep CRLF and BOM;run_shelloutput in Chinese is readable on a 936 system auto · lowil: first command reports the label;echo x > C:\outsideasks to rerun;curl ...asks before running;/sandbox reset-labelsrestores the label- Esc during
sleep 30 & sleep 30returns at once and leaves nobusybox.exebehind
macOS specifics:
- Builds with Homebrew ncurses and with the system ncurses (
brew unlink ncurses) - Terminal.app and iTerm2: arrows, Home/End, PgUp/PgDn, bracketed paste