Initial CST-Agent: portable Linux/Termux/Windows coding agent.

Vendored sashidown, OpenAI-compatible env (OPENAI_*), and bootstrap scripts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WHC
2026-09-07 22:42:32 +08:00
co-authored by Cursor
commit 1f1fcba7ad
75 changed files with 14940 additions and 0 deletions
+784
View File
@@ -0,0 +1,784 @@
#define _DEFAULT_SOURCE
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cst_agent.h"
#include "os_compat.h"
#ifndef _WIN32
#include <unistd.h>
#else
#include <io.h>
#include <fcntl.h>
#endif
void cst_tool_result_free(cst_tool_result_t *r) {
if (!r)
return;
free(r->out);
r->out = NULL;
r->out_len = 0;
}
static const char *arg_get(const ctl_tool_call_t *call, const char *key) {
for (int i = 0; i < call->nargs; i++) {
if (strcmp(call->args[i].key, key) == 0)
return call->args[i].value ? call->args[i].value : "";
}
return NULL;
}
static int set_result(cst_tool_result_t *r, const char *s, int code) {
cst_tool_result_free(r);
size_t n = s ? strlen(s) : 0;
r->out = malloc(n + 1);
if (!r->out)
return -1;
if (n)
memcpy(r->out, s, n);
r->out[n] = 0;
r->out_len = n;
r->exit_code = code;
return 0;
}
/* Read whole file into malloc'd buffer; *out_len set. Returns NULL if missing. */
static char *slurp_file(const char *full, size_t *out_len) {
*out_len = 0;
FILE *fp = fopen(full, "rb");
if (!fp)
return NULL;
if (fseek(fp, 0, SEEK_END) != 0) {
fclose(fp);
return NULL;
}
long sz = ftell(fp);
if (sz < 0) {
fclose(fp);
return NULL;
}
rewind(fp);
char *buf = malloc((size_t)sz + 1);
if (!buf) {
fclose(fp);
return NULL;
}
size_t n = fread(buf, 1, (size_t)sz, fp);
fclose(fp);
buf[n] = 0;
*out_len = n;
return buf;
}
#define DIFF_OUT_CAP (48 * 1024)
/* OpenCode-style: always surface a unified diff after write/patch. */
static char *make_unified_diff(const char *relpath, const char *oldc, size_t olen,
const char *newc, size_t nlen) {
char t0[OS_PATH_MAX], t1[OS_PATH_MAX];
int fd0 = os_mkstemp(t0, sizeof(t0), "cstda");
int fd1 = os_mkstemp(t1, sizeof(t1), "cstdb");
if (fd0 < 0 || fd1 < 0) {
if (fd0 >= 0) {
#ifdef _WIN32
_close(fd0);
_unlink(t0);
#else
close(fd0);
unlink(t0);
#endif
}
if (fd1 >= 0) {
#ifdef _WIN32
_close(fd1);
_unlink(t1);
#else
close(fd1);
unlink(t1);
#endif
}
return NULL;
}
if (olen) {
#ifdef _WIN32
(void)_write(fd0, oldc, (unsigned)olen);
#else
(void)write(fd0, oldc, olen);
#endif
}
if (nlen) {
#ifdef _WIN32
(void)_write(fd1, newc, (unsigned)nlen);
#else
(void)write(fd1, newc, nlen);
#endif
}
#ifdef _WIN32
_close(fd0);
_close(fd1);
#else
close(fd0);
close(fd1);
#endif
const char *label = relpath && relpath[0] ? relpath : "file";
char cmd[OS_PATH_MAX * 2 + 256];
snprintf(cmd, sizeof(cmd),
"diff -u --label a/%s --label b/%s \"%s\" \"%s\"", label, label, t0,
t1);
os_run_result_t rr = {0};
int rc = os_run_shell_c(NULL, cmd, DIFF_OUT_CAP, &rr);
#ifdef _WIN32
_unlink(t0);
_unlink(t1);
#else
unlink(t0);
unlink(t1);
#endif
if (rc != 0) {
os_run_result_free(&rr);
return strdup("(no textual diff — diff unavailable)\n");
}
/* diff returns 1 when files differ — still success for our purposes */
if (!rr.out || rr.out_len == 0) {
os_run_result_free(&rr);
return strdup("(no textual diff — identical or binary)\n");
}
char *out = rr.out;
rr.out = NULL;
size_t len = rr.out_len;
os_run_result_free(&rr);
if (len >= DIFF_OUT_CAP - 64) {
snprintf(out + DIFF_OUT_CAP - 64, 64, "\n…(diff truncated)\n");
}
return out;
}
static int set_result_with_diff(cst_tool_result_t *r, const char *relpath,
const char *oldc, size_t olen, const char *newc,
size_t nlen, int code) {
char *diff = make_unified_diff(relpath, oldc ? oldc : "", olen, newc ? newc : "",
nlen);
char hdr[256];
snprintf(hdr, sizeof(hdr), "wrote %s (%zu bytes)\n",
relpath ? relpath : "?", nlen);
size_t hlen = strlen(hdr);
size_t dlen = diff ? strlen(diff) : 0;
char *buf = malloc(hlen + dlen + 1);
if (!buf) {
free(diff);
return set_result(r, "ok", code);
}
memcpy(buf, hdr, hlen);
if (diff && dlen)
memcpy(buf + hlen, diff, dlen);
buf[hlen + dlen] = 0;
free(diff);
cst_tool_result_free(r);
r->out = buf;
r->out_len = hlen + dlen;
r->exit_code = code;
return 0;
}
int cst_tool_read_file(const char *workspace_root, const ctl_tool_call_t *call,
cst_tool_result_t *r) {
const char *path = arg_get(call, "path");
if (!path)
return set_result(r, "error: missing path", 1);
char full[4096];
if (cst_sandbox_resolve(workspace_root, path, full, sizeof(full)) != 0)
return set_result(r, "error: path outside workspace", 1);
FILE *fp = fopen(full, "rb");
if (!fp)
return set_result(r, "error: cannot open file", 1);
if (fseek(fp, 0, SEEK_END) != 0) {
fclose(fp);
return set_result(r, "error: seek failed", 1);
}
long sz = ftell(fp);
if (sz < 0 || sz > 2 * 1024 * 1024) {
fclose(fp);
return set_result(r, "error: file too large or empty seek", 1);
}
rewind(fp);
char *buf = malloc((size_t)sz + 1);
if (!buf) {
fclose(fp);
return set_result(r, "error: oom", 1);
}
size_t n = fread(buf, 1, (size_t)sz, fp);
fclose(fp);
buf[n] = 0;
cst_tool_result_free(r);
r->out = buf;
r->out_len = n;
r->exit_code = 0;
return 0;
}
int cst_tool_write_file(const char *workspace_root, const ctl_tool_call_t *call,
cst_tool_result_t *r) {
const char *path = arg_get(call, "path");
const char *content = arg_get(call, "content");
if (!path || !content)
return set_result(r, "error: missing path or content", 1);
char full[4096];
if (cst_sandbox_resolve(workspace_root, path, full, sizeof(full)) != 0)
return set_result(r, "error: path outside workspace", 1);
size_t olen = 0;
char *oldc = slurp_file(full, &olen);
FILE *fp = fopen(full, "wb");
if (!fp) {
free(oldc);
return set_result(r, "error: cannot write file", 1);
}
size_t n = strlen(content);
if (fwrite(content, 1, n, fp) != n) {
fclose(fp);
free(oldc);
return set_result(r, "error: write failed", 1);
}
fclose(fp);
int rc = set_result_with_diff(r, path, oldc, olen, content, n, 0);
free(oldc);
return rc;
}
int cst_tool_run_shell(const char *workspace_root, const ctl_tool_call_t *call,
cst_tool_result_t *r) {
const char *cmd = arg_get(call, "cmd");
if (!cmd)
cmd = arg_get(call, "command");
if (!cmd)
return set_result(r, "error: missing cmd", 1);
char root_real[OS_PATH_MAX];
if (os_realpath(workspace_root, root_real, sizeof(root_real)) != 0)
return set_result(r, "error: bad workspace", 1);
os_run_result_t rr = {0};
if (os_run_shell_c(root_real, cmd, 512 * 1024, &rr) != 0) {
os_run_result_free(&rr);
return set_result(r, "error: shell exec failed", 1);
}
cst_tool_result_free(r);
r->out = rr.out;
r->out_len = rr.out_len;
r->exit_code = rr.exit_code;
rr.out = NULL;
os_run_result_free(&rr);
return 0;
}
/* Minimal unified diff apply: only support *** Update File: / write via
* apply_patch tool with arg path + content for MVP simplicity is write_file.
* For apply_patch: expect arg "patch" with a simple format:
* *** Update File: <relpath>
* <full new file content>
* Or classic unified diff for single file with ---/+++ and @@ hunks.
*/
static int apply_full_replace(const char *workspace_root, const char *path,
const char *content, cst_tool_result_t *r) {
ctl_tool_call_t fake = {0};
snprintf(fake.name, sizeof(fake.name), "write_file");
fake.nargs = 2;
snprintf(fake.args[0].key, sizeof(fake.args[0].key), "path");
fake.args[0].value = (char *)path;
fake.args[0].value_len = strlen(path);
snprintf(fake.args[1].key, sizeof(fake.args[1].key), "content");
fake.args[1].value = (char *)content;
fake.args[1].value_len = strlen(content);
return cst_tool_write_file(workspace_root, &fake, r);
}
/* Apply one complete unified-diff hunk to a malloc'd file buffer.
* Returns 0 on success. Very small subset: context/add/remove lines. */
static int apply_hunk(char **file, size_t *flen, const char *hunk,
size_t hunk_len) {
/* Find @@ line */
const char *p = hunk;
const char *end = hunk + hunk_len;
while (p < end && !(*p == '@' && p + 1 < end && p[1] == '@')) {
while (p < end && *p != '\n')
p++;
if (p < end)
p++;
}
if (p >= end)
return -1;
while (p < end && *p != '\n')
p++;
if (p < end)
p++;
/* Parse old file into lines */
size_t nlines = 0;
for (size_t i = 0; i < *flen; i++)
if ((*file)[i] == '\n')
nlines++;
if (*flen > 0 && (*file)[*flen - 1] != '\n')
nlines++;
char **lines = calloc(nlines + 64, sizeof(char *));
size_t *llens = calloc(nlines + 64, sizeof(size_t));
if (!lines || !llens) {
free(lines);
free(llens);
return -1;
}
size_t li = 0, start = 0;
for (size_t i = 0; i <= *flen; i++) {
if (i == *flen || (*file)[i] == '\n') {
size_t L = i - start;
lines[li] = malloc(L + 1);
if (!lines[li]) {
for (size_t k = 0; k < li; k++)
free(lines[k]);
free(lines);
free(llens);
return -1;
}
memcpy(lines[li], *file + start, L);
lines[li][L] = 0;
llens[li] = L;
li++;
start = i + 1;
}
}
size_t cur = 0;
/* Walk hunk body */
while (p < end) {
char tag = *p;
const char *ls = p + 1;
const char *le = ls;
while (le < end && *le != '\n')
le++;
size_t L = (size_t)(le - ls);
if (tag == ' ' || tag == '-') {
if (cur >= li || llens[cur] != L || memcmp(lines[cur], ls, L) != 0) {
for (size_t k = 0; k < li; k++)
free(lines[k]);
free(lines);
free(llens);
return -1;
}
if (tag == '-') {
free(lines[cur]);
memmove(&lines[cur], &lines[cur + 1], (li - cur - 1) * sizeof(char *));
memmove(&llens[cur], &llens[cur + 1], (li - cur - 1) * sizeof(size_t));
li--;
} else {
cur++;
}
} else if (tag == '+') {
char *nl = malloc(L + 1);
if (!nl) {
for (size_t k = 0; k < li; k++)
free(lines[k]);
free(lines);
free(llens);
return -1;
}
memcpy(nl, ls, L);
nl[L] = 0;
lines = realloc(lines, (li + 2) * sizeof(char *));
llens = realloc(llens, (li + 2) * sizeof(size_t));
memmove(&lines[cur + 1], &lines[cur], (li - cur) * sizeof(char *));
memmove(&llens[cur + 1], &llens[cur], (li - cur) * sizeof(size_t));
lines[cur] = nl;
llens[cur] = L;
li++;
cur++;
} else if (tag == '\\') {
/* "\ No newline at end of file" — ignore */
} else {
for (size_t k = 0; k < li; k++)
free(lines[k]);
free(lines);
free(llens);
return -1;
}
p = (le < end) ? le + 1 : end;
}
/* rebuild */
size_t total = 0;
for (size_t k = 0; k < li; k++)
total += llens[k] + 1;
char *out = malloc(total + 1);
if (!out) {
for (size_t k = 0; k < li; k++)
free(lines[k]);
free(lines);
free(llens);
return -1;
}
size_t o = 0;
for (size_t k = 0; k < li; k++) {
memcpy(out + o, lines[k], llens[k]);
o += llens[k];
out[o++] = '\n';
free(lines[k]);
}
free(lines);
free(llens);
free(*file);
*file = out;
*flen = o;
return 0;
}
int cst_tool_apply_patch(const char *workspace_root, const ctl_tool_call_t *call,
cst_tool_result_t *r) {
const char *patch = arg_get(call, "patch");
if (!patch)
return set_result(r, "error: missing patch", 1);
/* Full-file replace shorthand */
if (strncmp(patch, "*** Update File: ", 17) == 0) {
const char *nl = strchr(patch, '\n');
if (!nl)
return set_result(r, "error: incomplete Update File header", 1);
char path[1024];
size_t plen = (size_t)(nl - (patch + 17));
if (plen >= sizeof(path))
return set_result(r, "error: path too long", 1);
memcpy(path, patch + 17, plen);
path[plen] = 0;
char full[4096];
if (cst_sandbox_resolve(workspace_root, path, full, sizeof(full)) != 0)
return set_result(r, "error: path outside workspace", 1);
size_t olen = 0;
char *oldc = slurp_file(full, &olen);
const char *neu = nl + 1;
size_t nlen = strlen(neu);
ctl_tool_call_t fake = {0};
snprintf(fake.name, sizeof(fake.name), "write_file");
fake.nargs = 2;
snprintf(fake.args[0].key, sizeof(fake.args[0].key), "path");
fake.args[0].value = path;
fake.args[0].value_len = strlen(path);
snprintf(fake.args[1].key, sizeof(fake.args[1].key), "content");
fake.args[1].value = (char *)neu;
fake.args[1].value_len = nlen;
/* write_file already emits diff; free unused oldc */
free(oldc);
return cst_tool_write_file(workspace_root, &fake, r);
}
/* Unified diff: require complete hunks (each @@ ... until next @@ or EOF) */
const char *p = patch;
char *rel = NULL;
/* find +++ b/path */
const char *plus = strstr(p, "\n+++ ");
if (!plus)
plus = (strncmp(p, "+++ ", 4) == 0) ? p : NULL;
if (!plus)
return set_result(r, "error: missing +++ line (incomplete patch)", 1);
const char *ps = plus + (plus[0] == '\n' ? 5 : 4);
if (ps[0] == 'b' && ps[1] == '/')
ps += 2;
const char *pe = strchr(ps, '\n');
if (!pe)
return set_result(r, "error: incomplete +++ line", 1);
size_t rlen = (size_t)(pe - ps);
rel = malloc(rlen + 1);
if (!rel)
return set_result(r, "error: oom", 1);
memcpy(rel, ps, rlen);
rel[rlen] = 0;
char full[4096];
if (cst_sandbox_resolve(workspace_root, rel, full, sizeof(full)) != 0) {
free(rel);
return set_result(r, "error: path outside workspace", 1);
}
char *relpath_for_diff = rel; /* owned until return */
FILE *fp = fopen(full, "rb");
char *file = NULL;
size_t flen = 0;
if (fp) {
fseek(fp, 0, SEEK_END);
long sz = ftell(fp);
rewind(fp);
if (sz < 0) {
fclose(fp);
free(relpath_for_diff);
return set_result(r, "error: ftell", 1);
}
file = malloc((size_t)sz + 1);
if (!file) {
fclose(fp);
free(relpath_for_diff);
return set_result(r, "error: oom", 1);
}
flen = fread(file, 1, (size_t)sz, fp);
fclose(fp);
file[flen] = 0;
} else {
file = calloc(1, 1);
flen = 0;
}
/* Snapshot before mutating hunks. */
char *old_snapshot = NULL;
size_t old_len = flen;
if (flen) {
old_snapshot = malloc(flen + 1);
if (old_snapshot) {
memcpy(old_snapshot, file, flen);
old_snapshot[flen] = 0;
}
} else {
old_snapshot = calloc(1, 1);
old_len = 0;
}
/* Split into hunks starting at @@ */
const char *h = strstr(pe, "\n@@");
if (!h)
h = strstr(pe, "@@");
if (!h) {
free(file);
free(old_snapshot);
free(relpath_for_diff);
return set_result(r, "error: no complete hunk", 1);
}
if (*h == '\n')
h++;
while (h && *h == '@') {
const char *next = strstr(h + 2, "\n@@");
size_t hlen;
if (next) {
hlen = (size_t)(next + 1 - h);
} else {
hlen = strlen(h);
}
const char *hnl = strchr(h, '\n');
if (!hnl || hnl >= h + hlen - 1) {
free(file);
free(old_snapshot);
free(relpath_for_diff);
return set_result(r, "error: incomplete hunk (not confirmed)", 1);
}
if (apply_hunk(&file, &flen, h, hlen) != 0) {
free(file);
free(old_snapshot);
free(relpath_for_diff);
return set_result(r, "error: hunk apply failed", 1);
}
h = next ? next + 1 : NULL;
}
fp = fopen(full, "wb");
if (!fp) {
free(file);
free(old_snapshot);
free(relpath_for_diff);
return set_result(r, "error: cannot write", 1);
}
if (fwrite(file, 1, flen, fp) != flen) {
fclose(fp);
free(file);
free(old_snapshot);
free(relpath_for_diff);
return set_result(r, "error: write failed", 1);
}
fclose(fp);
/* Patch text is already a diff — show it when short. */
size_t plen = strlen(patch);
if (plen > 0 && plen < DIFF_OUT_CAP) {
char hdr[256];
snprintf(hdr, sizeof(hdr), "applied patch → %s\n", relpath_for_diff);
size_t hlen = strlen(hdr);
char *buf = malloc(hlen + plen + 1);
if (buf) {
memcpy(buf, hdr, hlen);
memcpy(buf + hlen, patch, plen);
buf[hlen + plen] = 0;
cst_tool_result_free(r);
r->out = buf;
r->out_len = hlen + plen;
r->exit_code = 0;
free(file);
free(old_snapshot);
free(relpath_for_diff);
return 0;
}
}
int rc = set_result_with_diff(r, relpath_for_diff, old_snapshot, old_len, file,
flen, 0);
free(file);
free(old_snapshot);
free(relpath_for_diff);
return rc;
}
int cst_tool_dispatch(const char *workspace_root, const ctl_tool_call_t *call,
cst_tool_result_t *r) {
if (!call || !r)
return -1;
memset(r, 0, sizeof(*r));
if (strcmp(call->name, "read_file") == 0)
return cst_tool_read_file(workspace_root, call, r);
if (strcmp(call->name, "run_shell") == 0)
return cst_tool_run_shell(workspace_root, call, r);
if (strcmp(call->name, "apply_patch") == 0)
return cst_tool_apply_patch(workspace_root, call, r);
if (strcmp(call->name, "write_file") == 0)
return cst_tool_write_file(workspace_root, call, r);
return set_result(r, "error: unknown tool", 1);
}
/* Parse JSON string at *pp (must point at '"'); advances *pp past closing quote.
* Returns malloc'd decoded value. */
static char *parse_json_string(const char **pp, size_t *out_len) {
const char *p = *pp;
if (!p || *p != '"')
return NULL;
p++;
size_t cap = 64, len = 0;
char *out = malloc(cap);
if (!out)
return NULL;
while (*p) {
char ch;
if (*p == '\\' && p[1]) {
p++;
if (*p == 'n')
ch = '\n';
else if (*p == 'r')
ch = '\r';
else if (*p == 't')
ch = '\t';
else
ch = *p;
p++;
} else if (*p == '"') {
p++;
out[len] = 0;
*pp = p;
if (out_len)
*out_len = len;
return out;
} else {
ch = *p++;
}
if (len + 2 > cap) {
cap *= 2;
char *nb = realloc(out, cap);
if (!nb) {
free(out);
return NULL;
}
out = nb;
}
out[len++] = ch;
}
free(out);
return NULL;
}
int cst_tool_call_from_native(ctl_tool_call_t *out, const char *id,
const char *name, const char *args_json) {
if (!out || !name || !name[0])
return -1;
memset(out, 0, sizeof(*out));
if (id)
snprintf(out->id, sizeof(out->id), "%s", id);
snprintf(out->name, sizeof(out->name), "%s", name);
if (!args_json)
return 0;
const char *p = args_json;
while (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t')
p++;
if (*p != '{')
return 0;
p++;
while (*p) {
while (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t' || *p == ',')
p++;
if (*p == '}' || !*p)
break;
if (*p != '"')
break;
size_t klen = 0;
char *key = parse_json_string(&p, &klen);
if (!key)
break;
while (*p == ' ' || *p == '\t')
p++;
if (*p != ':') {
free(key);
break;
}
p++;
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')
p++;
if (*p == '"') {
size_t vlen = 0;
char *val = parse_json_string(&p, &vlen);
if (!val) {
free(key);
break;
}
if (out->nargs < CTL_MAX_ARGS && klen < CTL_MAX_KEY) {
snprintf(out->args[out->nargs].key, CTL_MAX_KEY, "%s", key);
out->args[out->nargs].value = val;
out->args[out->nargs].value_len = vlen;
out->nargs++;
} else {
free(val);
}
free(key);
} else if (*p == '{' || *p == '[') {
free(key);
char open = *p;
char close = (open == '{') ? '}' : ']';
int depth = 0;
int in_str = 0;
for (; *p; p++) {
if (in_str) {
if (*p == '\\' && p[1]) {
p++;
continue;
}
if (*p == '"')
in_str = 0;
continue;
}
if (*p == '"') {
in_str = 1;
continue;
}
if (*p == open)
depth++;
else if (*p == close) {
depth--;
if (depth == 0) {
p++;
break;
}
}
}
} else {
free(key);
while (*p && *p != ',' && *p != '}')
p++;
}
}
return 0;
}