OpenCode-style TUI, model probing, sandboxed auto approval

- TUI: centered home screen with logo, boxed input with mode line, sidebar
  (>=120 cols), Ctrl+P palette, Tab Build/Plan, Shift+Tab approval cycle,
  mouse (cards, cursor, pickers, wheel), /sidebar /mouse /approval
- /models probing: key check on entry (401 re-asks), API models in /model
- Shell sandbox in C: Linux Landlock (writes limited to workspace/tmp,
  ABI>=4 blocks TCP), macOS sandbox_init, rules fallback elsewhere
- Approval modes ask/auto/yolo (default auto); read-only command whitelist;
  ask to rerun outside the sandbox when a command looks blocked
- Upgradable bootstrap scripts (git pull --ff-only, rebuild, reinstall) and
  cst-agent-update; cst-agent --version
- tests: test_sandbox, test_models
This commit is contained in:
WHC
2026-10-05 12:34:46 +08:00
parent a77f27ed59
commit ef63f6d612
25 changed files with 3021 additions and 405 deletions
+112
View File
@@ -698,3 +698,115 @@ finish:
free(body);
return ret;
}
/* --- model listing -------------------------------------------------------- */
#define MODELS_BODY_MAX (2 * 1024 * 1024)
static size_t models_write_cb(char *ptr, size_t size, size_t nmemb, void *ud) {
cst_buf_t *b = ud;
size_t n = size * nmemb;
if (b->len + n > MODELS_BODY_MAX || cst_buf_append(b, ptr, n) != 0)
return 0;
return n;
}
static int add_model(char out[][128], int n, int max_out, const char *id) {
if (!id || !id[0] || n >= max_out || strlen(id) >= 128)
return n;
for (int i = 0; i < n; i++)
if (strcmp(out[i], id) == 0)
return n;
snprintf(out[n], 128, "%s", id);
return n + 1;
}
/* {"data":[{"id":..}]}, {"models":[{"id"|"name":..}]} or a bare array. */
static int parse_models(const char *json, char out[][128], int max_out) {
cJSON *root = cJSON_Parse(json);
if (!root)
return -1;
const cJSON *arr = root;
if (!cJSON_IsArray(arr))
arr = cJSON_GetObjectItemCaseSensitive(root, "data");
if (!cJSON_IsArray(arr))
arr = cJSON_GetObjectItemCaseSensitive(root, "models");
int n = -1;
if (cJSON_IsArray(arr)) {
n = 0;
const cJSON *it;
cJSON_ArrayForEach(it, arr) {
const cJSON *id = cJSON_GetObjectItemCaseSensitive(it, "id");
if (!cJSON_IsString(id))
id = cJSON_GetObjectItemCaseSensitive(it, "name");
if (cJSON_IsString(id))
n = add_model(out, n, max_out, id->valuestring);
else if (cJSON_IsString(it))
n = add_model(out, n, max_out, it->valuestring);
}
}
cJSON_Delete(root);
return n;
}
int cst_llm_list_models(const char *api_base, const char *api_key,
char out[][128], int max_out, char *err,
size_t err_sz) {
char dummy[8];
if (!err || !err_sz) {
err = dummy;
err_sz = sizeof(dummy);
}
err[0] = 0;
if (!api_base || !api_base[0] || !out || max_out <= 0) {
snprintf(err, err_sz, "bad request");
return -1;
}
CURL *curl = curl_easy_init();
if (!curl) {
snprintf(err, err_sz, "curl init failed");
return -1;
}
char url[1024];
size_t bl = strlen(api_base);
while (bl && api_base[bl - 1] == '/')
bl--;
snprintf(url, sizeof(url), "%.*s/models", (int)bl, api_base);
struct curl_slist *hdrs = curl_slist_append(NULL, "Accept: application/json");
char auth[700];
if (api_key && api_key[0]) {
snprintf(auth, sizeof(auth), "Authorization: Bearer %s", api_key);
hdrs = curl_slist_append(hdrs, auth);
}
cst_buf_t body = {0};
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hdrs);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, models_write_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
CURLcode rc = curl_easy_perform(curl);
long http = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http);
curl_slist_free_all(hdrs);
curl_easy_cleanup(curl);
memset(auth, 0, sizeof(auth));
int n = -1;
if (rc != CURLE_OK) {
snprintf(err, err_sz, "curl: %s", curl_easy_strerror(rc));
} else if (http == 401 || http == 403) {
snprintf(err, err_sz, "HTTP %ld: the API key was rejected", http);
n = CST_MODELS_AUTH;
} else if (http >= 400) {
snprintf(err, err_sz, "HTTP %ld from %s", http, url);
} else {
n = parse_models(body.buf ? body.buf : "", out, max_out);
if (n < 0)
snprintf(err, err_sz, "unexpected /models response");
}
cst_buf_free(&body);
return n;
}