Point readers at WHC/Hermit as canonical; add MIGRATION.md and
migrate_remote_to_hermit.sh so existing checkouts can retarget origin.
Co-authored-by: Cursor <cursoragent@cursor.com>
Ship hermit binary (cst-agent symlink), ~/.config/hermit with legacy fallback,
Hermit color theme and mini crab frames, plus SVG→PUA font build pipeline.
Co-authored-by: Cursor <cursoragent@cursor.com>
Rewrite the homepage for day-to-day use, move paper/eval drafts under
docs/archive, and add a docs index so theory no longer leads the project.
Co-authored-by: Cursor <cursoragent@cursor.com>
Bring back Gitea CI + auto-publish; share release helpers; strengthen multi-turn
tool/job prompts with a unit contract; ccache/make help and --release bootstrap.
Co-authored-by: Cursor <cursoragent@cursor.com>
CLI/Docker assert AEAD sync; status shows sealed channel; headless denials no longer pretend the user refused.
Co-authored-by: Cursor <cursoragent@cursor.com>
Ignore/clean host objects, add linux-headers, and include sys/time.h
so discover builds under musl. Docker chair↔member handshake now passes.
Co-authored-by: Cursor <cursoragent@cursor.com>
Derive Realm keys from a user password (Argon2id, no private key on disk).
Login claims the sole chair with handover; peers share parliament context;
--new-project clears it. CLI peer/discover helpers and Docker alpine recipe
included; headless empty-answer retries raised for unattended runs.
Co-authored-by: Cursor <cursoragent@cursor.com>
Two strands land together: the window draws pictures now, and the whole front
end builds, installs and runs on the phone with the engine linked in-process.
Images (the "make a picture actually show up" part)
- Engine, additively: cst_agent_hooks_t.on_tool_images(call, list, n, ud) fires
when a call left attachments behind, and cst_agent_pending_rel() /
cst_agent_pending_is_image() read that list without the app touching the
store. tests/test_attach.c::test_agent_pending pins the three cases (an
image, a stored name that passes cst_attach_rel_ok, out-of-range NULL).
- App: image://cstattach/<stored name> (app/src/ui/SessionImages.cpp) serves
only the current session's attachment directory, checked with
cst_attach_rel_ok, decodes down to the requested sourceSize and never
upscales, and holds a mutex because requestImage() runs on the render
thread while the directory can change underneath it. AgentCore gained
attachmentDir(), pendingImages(), the toolImages()/promptImages() signals
and hookToolImages(); history() gives replayed rows their images too.
- QML: ImageStrip.qml (a thumb strip, <=150 px tall, shaped by the picture,
tooltip with name/size/origin, the file's name when it went missing),
wired into MessageCard and ToolCard. Qt 6's ListModel cannot hold a
list-valued role (append() reads back undefined, setProperty drops it), so
a row carries one image per line as <stored>|<name>|<origin>|<w>x<h> and
ImageStrip decodes that.
Android (the engine is already in the APK; this is the front end)
- app/src/main.cpp: on Android the workspace, config, tmp and HOME go under
the app's own data directory before the engine opens anything, because
QDir::currentPath() is "/" there and /tmp is not writable.
- app/android/: the manifest (QtApplication/QtActivity, INTERNET and
POST_NOTIFICATIONS, adjustResize, no storage permission -- the picker will
be the system one), strings, a vector icon; minSdk 24 and targetSdk 35 come
from app/CMakeLists.txt through QT_ANDROID_MIN/TARGET_SDK_VERSION.
- app/src/platform/android/AndroidTools.cpp: the Android file behind
PlatformInit(), owner "app:android" (notification, clipboard, share sheet,
file picker are stubs for now, so the tool list does not lie about them).
- app/tools/selftest.c decides its scratch workspace with os_temp_dir(), so
the C smoke test runs on the device too.
- scripts/build_apk_android.sh: sources to installed APK in one command
(environment checks, qt-cmake configure, --target apk, adb install), and
the host-side plumbing the NixOS + qt.io combination needs -- the NDK
sysroot include dirs for Qt's FindGLESv2/FindEGL (they only look under
<sysroot>/include), -L$PREFIX/lib on the shared/module link lines as well
as the executables', the Android debug keystore created on first use with
QT_ANDROID_SIGN_APK + QT_ANDROID_KEYSTORE_* (androiddeployqt copies the
unsigned gradle output and signs it itself), and the #!/bin/bash shebang of
the build-tools wrappers, which NixOS does not have.
- scripts/gradle-android-init.gradle (installed into ~/.gradle/init.d/): the
release lint task is switched off -- lintVitalAnalyzeRelease resolves its
own tool jars straight from dl.google.com, bypassing every repository the
build configures, and fails the build after all the real work is done.
- Icons: app/icons/*.svg (Breeze's 24@3x files, the 19 names the window asks
for) are compiled in as :/cst-icons and ThemeIcons.cpp falls back to them
when the platform has no icon theme. On the phone the Qt theme lookup found
nothing and the QML glyph fallback was an empty box in the phone's fonts.
- docs/android.md: "The APK (Qt front end)" -- the SDK assembled without
sdkmanager, the host Qt's missing libraries on NixOS, the Gradle mirrors,
lint, signing -- and a Status that says what runs where. app/README.md
gains the images and APK lines, and stops claiming Android platform tools
that are still stubs.
Verified: engine `make check` green; desktop `cst-app-selftest: OK` and
--uitest all checks pass; phone (NX789J, Android 16, arm64-v8a) -- the APK
installs, starts on the workspace under /data/user/0/xyz.whcself.cstagent/
files/, a turn reaches a configured provider over HTTPS from inside the ART
process and renders the reply, rotation keeps the session and docks the
sidebar, and the toolbar/card icons draw.
Version 0.9.0.
mode = coding
The engine can be embedded now instead of only driven from a terminal: the
same objects link into a Qt front end (app/) that owns a cst_agent_t inside
its own process -- no port, no token, no second process -- and that front end
now runs on the desktop and, engine-side, on the phone.
Capability metadata (include/cst_tools.h, src/tools/registry.c):
- cst_tool_def_t gains perm_name, platforms and available (a probe that
returns a reason), so a permission is addressable by name and a tool can
say why it cannot run here.
- cst_platform_name(), cst_tool_platform_ok(), cst_tool_available(),
cst_tool_meta_reload(), cst_tool_perm_name() and CST_PLAT_DESKTOP[].
- cst_tool_register() may take over a built-in name that is not runnable on
this platform, so a front end replaces it with its own implementation.
- the registry entries carry their class (delete_file -> "delete"), the
desktop clipboard tools carry a clipboard probe; find/active/run gate on it.
- /api/settings.tools[] exposes perm_name, platforms, available, why and the
declared parameters.
Engine as a library (Makefile, CMakeLists.txt):
- libcstcore.a from the CORE_OBJ set, and a CMake cstcore target shared by
the CLI and by app/ (-DCST_BUILD_APP=ON): the engine is compiled once.
- Android branch for the vendored sashidown: a per-ABI archive built with the
NDK's aarch64-linux-android24-clang wrapper, because CMake's compiler is
plain clang and keeps --target/--sysroot in its own variables.
Qt front end (app/, new):
- AgentCore wraps one cst_agent_t: turns run on a worker thread, every
cst_agent_hooks_t callback becomes a Qt signal, and the two hooks that need
an answer (on_permission, on_ask_user) block the worker until the GUI
replies; mode, approval and plan mode go through the engine's config.
- ToolProvider hands the front end's tools to the engine (owner "app:*"); the
Linux one is notification_send with a probe for notify-send/D-Bus.
- Main.qml: transcript, tool blocks, consent dialog (Deny/Once/Always ->
CST_PERM_*), ask_user, mode switch, Esc to cancel; --selftest stays
headless and runs the embedding checks without QML.
Android (scripts/, docs/android.md, new):
- scripts/build_third_party_android.sh builds static mbedTLS + libcurl for an
ABI (CA path /system/etc/security/cacerts); build_android.sh builds
cst-agent, libcstcore.a and the test binaries for the phone, can push them
and run the whole suite there.
- sandbox_exec.c keeps the plain exec path on Android: the seccomp supervisor
compiled in and the probe reported "seccomp", but writes outside the
workspace were not denied on the device -- claiming a sandbox that does not
hold is worse than reporting none.
- tests/test_diff.c compares layouts only against real GNU diffutils: toybox's
diff prints the same hunk header with its own hunk/context rules.
Tests: test_tool_meta (metadata, platform gate, probe reasons, takeover of a
built-in name) and test_embed (in-process embedding contract) are new; the
existing registry/llm_body assertions now follow availability. 38 test
binaries pass on the host, 36/36 on arm64-v8a Android 16, cst-app-selftest
passes in both the standalone app/ build and the repo build.
Version 0.8.7.
Sessions: rename, delete and search; /api/retry regenerates the last
answer, editing a prompt resends from there; /api/export downloads the
transcript as Markdown (cst_session_export_md). Session messages carry
note/tool_result markers, so a reload rebuilds the tool steps with
their arguments, output and exit status; subagent progress is shown.
Diff and settings: /api/diff shows the real diff; /api/settings covers
protocol, max_steps, tool switches and confirm; /api/permissions edits
the rules; sandbox status is reported. Session rename no longer reads
past a short title.
Commands: slash commands with completion in the composer (/model,
/provider, /new, /retry, /undo, /compact, /export, /diff, /init,
/skill and more), a Ctrl+P palette, Shift+Tab cycles the approval
mode, F1 or ? shows the keys. "Thinking expanded" is saved in the
settings (show_thinking), "expand all tool output" in the browser.
Extensions: GET /api/extensions lists MCP servers, plugins, skills,
agent roles and subagent runs; POST /api/extensions/reload and
/api/extensions/trust; a workspace switch reloads them. /api/init and
/api/skill run on the server (cst_init_prompt shared with the TUI).
Errors, statuses and [cst] notes are shown in Chinese with a readable
reason instead of rc=; a failed turn's error survives the reload.
Tests: scripts/e2e_webui_sessions.py, e2e_webui_workspace.py and
e2e_webui_ext.py, run from e2e_webui.sh.
Streaming: Chinese reasoning showed up garbled in the WebUI. The agent
loop now feeds visible/thinking text in runs instead of byte by byte,
and the WebUI holds an incomplete UTF-8 tail until the next chunk
(cst_utf8_hold_feed), so every think_delta/assistant_delta is whole.
src/provider_switch.c: model list fetch/cache, key verify-and-save and
provider switching moved out of tui.c and shared by TUI and WebUI.
WebUI API:
- GET /api/providers, GET /api/models[?refresh=1] (401 on a rejected
key, still listing built-ins), POST /api/provider, POST /api/key
(key checked against /v1/models, saved to credentials 0600, never
echoed in replies, events or logs)
- /api/config validates everything before applying, persists model,
effort and approval (not yolo), warns when effort does not apply;
plan_mode no longer forces read-only
- state: turn_usage, api_base, key_set/key_from, model_info,
effort_applies, sees_images, max_steps, tool_profile, perm_timeout;
permission events carry their timeout
Frontend: searchable model picker with refresh and manual entry,
provider picker and key dialog, effort/vision hints, toasts instead of
silent failures, Chinese error text, real protocol in the settings and
usage panels, approval countdown from the server's timeout.
Tests: utf8 hold unit test, scripts/e2e_webui_settings.py (fake
/v1/models, wrong key, switching, persistence, Chinese thinking) run
from e2e_webui.sh, scripts/tui_keys/provider.py.
Build: <fcntl.h> in tui.c, format-truncation in http_srv.c and /web.
WebUI:
- refuse cross-site requests (Origin must be the page itself) in the
default no-token mode: any page could POST approval=yolo and a prompt
- session/workspace/undo/compact/attach/model changes are 409 while a
turn runs (use-after-free, messages landing in the wrong session)
- ask_user returned "dismissed" at once instead of waiting
- cancel and shutdown wake a turn parked on a permission or question;
SIGTERM mid-turn no longer hangs or crashes
- a page opened while a question is pending still gets it;
Last-Event-ID resumes the stream; event seq matches the SSE id
- token mode: page assets load without the token, images via cookie
- --web-open works, --web-port/--web-token are validated
- frontend: gate keys ignored while typing, clear-images button,
reasoning no longer wiped while streaming
HTTP server: auth and size cap before reading the body, strict
Content-Length, NUL in url_decode, 413 on keep-alive, idle timeout,
absolute-form only without a leading '/', case-insensitive Connection,
header deadline, 400 for long request lines, unlink before close,
poll() instead of select(), SO_EXCLUSIVEADDRUSE on Windows.
Markdown sanitizer escapes quotes in attribute values.
/web: passes this session's provider/model/base/effort/approval (key
via the environment), keeps output off the TUI, shows the real URL and
keeps CURRENT on the TUI's session.
Tests: scripts/e2e_webui_ask.py, scripts/e2e_webui_http.py (run from
e2e_webui.sh), sanitizer quote test.
A second implementation of cst_agent_hooks_t next to the TUI: the turn runs on
a worker thread, the hooks publish events into a ring buffer that SSE clients
read, so the engine itself needs no changes.
- src/net.c, include/cst_net.h: POSIX/Winsock sockets, non-blocking connect
- src/http_srv.c, include/cst_http.h: HTTP/1.1, keep-alive, chunked streaming,
body caps (413), Expect: 100-continue, connection cap, exact routing
- src/web_md.c, include/cst_web_md.h: streaming markdown -> sanitized HTML
(tag/attribute allowlist, javascript: URLs and off-site images dropped)
- src/webui.c, include/cst_webui.h: event ring + SSE broadcast, all routes,
token (Bearer/Cookie/query) and loopback Host checks, 3 s auto-deny when no
client is attached
- src/webui_stub.c, CST_WEBUI=0 / CST_BUILD_WEBUI=OFF: build without the WebUI
- web/: no-build front end (transcript, approvals, todo, sessions, image
upload, status bar)
- docs/webui.md: engine API v1 - events, requests, security model
- tests/test_webui.c, scripts/e2e_webui.sh: unit tests plus an end-to-end run
driven by a recorded SSE stream (no API key needed)
- src/main.c: --web --web-port --web-host --web-token --web-dev --web-open
and CST_WEB_PORT/HOST/TOKEN/ROOT
An empty POST body is now accepted without Content-Length (RFC 9112 6.3) so
curl-driven clients can reach cancel/undo/compact/quit; CMakeList's core tests
learned about sashidown and test_webui, and CST_WEBUI=0 defines CST_NO_WEBUI.
TUI keys: register both CSI and SS3 forms of the cursor/Home/End keys and
the xterm modifier sequences when terminfo lacks them, so an arrow sent in
the "other" form no longer turns into Esc + a typed letter. Up/Down move by
visual (soft-wrapped) row, and the draft survives a trip through history.
scripts/tui_keys drives the TUI in a pty to check all of this.
Images: text inside a multimodal content array is now a {"type":"text"}
part; DeepSeek rejected the bare string with HTTP 422. deepseek-flash
(and its deepseek-chat / deepseek-reasoner aliases) read images, so the
registry marks them vision; deepseek-v4-pro stays text only.
CI: models.c lost _DEFAULT_SOURCE in 0.7.0 and gmtime_r broke the musl
build; tests sleep with poll() instead of usleep for the same reason.
After a successful windows-cross build on main or v* tags, upload the zip
and .sha256 via scripts/release_windows.sh (creates/updates v$VERSION).
Prefer secrets.RELEASE_TOKEN; fall back to GITHUB_TOKEN. Skip on PRs.
os_win.c needs cst_buf_* / cst_utf8_valid from util.c after os_compat was
added to the small replay CMake targets. Release packaging only needs
cst-agent.exe, so bootstrap_windows.sh builds that target alone.
windows-cross died cloning busybox-w32 through a global gitclone.com
insteadOf (HTTP 502). Try ghfast/ghproxy/gitclone/github per repo instead
of forcing one proxy for every fetch.
gcc -O0 keeps unused cst_test_util.h helpers that need os_*; clang GC'd
them so only the gcc CMake step failed. Prefer ghfast/ghproxy.net for the
llvm-mingw tarball (gitclone 500 / github unreachable on the runner).
clang under strict C11 left strcasestr undeclared (returns int → pointer
truncation → SIGSEGV). Parse Content-Length portably; keep -Werror on
make check. Drop Nix for windows-cross (root /nix fails) and use pinned
llvm-mingw UCRT instead; Alpine musl gets linux-headers for audit.h.
- Replace actions/checkout (pulls from github.com) with scripts/ci_checkout.sh
fetched from this Gitea over raw/commit.
- Remove the macOS job (no runner).
- Disable sanitizers until the Docker runner has SYS_PTRACE; it was wedging
the only slot with AddressSanitizer:DEADLYSIGNAL.
- Add windows-cross: Nix UCRT mingw via TUNA, bootstrap_windows.sh, verify
dist/cst-agent-*-windows-x86_64.zip. Third-party git clones go through
gitclone.com.
Install was pulling bookworm packages from deb.debian.org; the domestic
Gitea runner crawls that for minutes. Rewrite sources to TUNA before
apt-get/apk, with CST_CI_MIRROR to override.
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
- bootstrap_termux.sh installs pkg-config/binutils, builds with make,
runs the tests and installs into Termux's $PREFIX/bin.
- Makefile drops -lpthread on Android (pthreads are in bionic libc).
- TUI assumes UTF-8 on Android: nl_langinfo needs API 26, Termux
targets 24. All sources compile for aarch64-linux-android24 (NDK 27).
Tools
- Table-driven tool registry with permission classes and default/minimal
profiles; new tools: edit_file, list_dir, glob, grep, delete_file,
move_file, todo_write, web_fetch, ask_user (plus read_file paging).
- Built-in Myers diff; write tools return unified diffs.
- Persistent permission rules (.agent/permissions.json), per-turn undo
snapshots (.agent/undo), AGENTS.md project notes, auto-compact at 80%
of the model context, cancellable run_shell with process-group kill.
TUI
- Table-driven slash commands: /provider /key /effort /status /tools
/permissions /yolo /undo /retry /diff /export /copy /rename /delete
/init /max-steps /todo, with / and @ completion.
- Permission modal with diff and prefix rules, ask_user modal, todo
panel, header with model/effort/context, bracketed paste, line editing,
wide-char cursor, NO_COLOR and ASCII fallback; Esc never quits.
Startup and configuration
- API keys are entered in the TUI (hidden) and saved per provider in
<config>/credentials (0600); env variables are only a fallback.
- Last provider, per-provider model and reasoning effort are remembered
in <config>/settings.json; --effort/-e and /effort send
reasoning_effort (low|medium|high|max) to models that accept it.
- DeepSeek preset uses deepseek-flash / deepseek-v4-pro.
- Session flags --new/--continue/--resume/--list-sessions, --yolo,
--confirm.
Compatibility and tests
- ncurses detection, macOS/Homebrew support, make install, XDG config
dir, POSIX cleanups; Gitea CI (gcc/clang/asan/musl/macOS).
- Unit tests for diff, fs tools, registry, undo/perms and config;
e2e fixtures multi_file_refactor and ask_or_todo; vendored cJSON.