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
+319
View File
@@ -0,0 +1,319 @@
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include "cst_sandbox.h"
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if defined(__linux__) && !defined(__ANDROID__)
#define CST_HAVE_LANDLOCK 1
#include <fcntl.h>
#include <stdint.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <unistd.h>
#elif defined(__APPLE__)
#define CST_HAVE_SEATBELT 1
#include <stdint.h>
#endif
static int g_kind = -1;
static int g_abi;
/* --- Linux: Landlock ------------------------------------------------------ */
#ifdef CST_HAVE_LANDLOCK
/* Defined here so older kernel headers still build. */
#ifndef __NR_landlock_create_ruleset
#define __NR_landlock_create_ruleset 444
#endif
#ifndef __NR_landlock_add_rule
#define __NR_landlock_add_rule 445
#endif
#ifndef __NR_landlock_restrict_self
#define __NR_landlock_restrict_self 446
#endif
#ifndef PR_SET_NO_NEW_PRIVS
#define PR_SET_NO_NEW_PRIVS 38
#endif
#define LL_CREATE_RULESET_VERSION (1U << 0)
#define LL_RULE_PATH_BENEATH 1
#define LL_FS_EXECUTE (1ULL << 0)
#define LL_FS_WRITE_FILE (1ULL << 1)
#define LL_FS_READ_FILE (1ULL << 2)
#define LL_FS_READ_DIR (1ULL << 3)
#define LL_FS_REMOVE_DIR (1ULL << 4)
#define LL_FS_REMOVE_FILE (1ULL << 5)
#define LL_FS_MAKE_CHAR (1ULL << 6)
#define LL_FS_MAKE_DIR (1ULL << 7)
#define LL_FS_MAKE_REG (1ULL << 8)
#define LL_FS_MAKE_SOCK (1ULL << 9)
#define LL_FS_MAKE_FIFO (1ULL << 10)
#define LL_FS_MAKE_BLOCK (1ULL << 11)
#define LL_FS_MAKE_SYM (1ULL << 12)
#define LL_FS_REFER (1ULL << 13) /* ABI 2 */
#define LL_FS_TRUNCATE (1ULL << 14) /* ABI 3 */
#define LL_NET_BIND_TCP (1ULL << 0) /* ABI 4 */
#define LL_NET_CONNECT_TCP (1ULL << 1) /* ABI 4 */
struct ll_ruleset_attr {
uint64_t handled_access_fs;
uint64_t handled_access_net;
};
struct ll_path_beneath_attr {
uint64_t allowed_access;
int32_t parent_fd;
} __attribute__((packed));
static int ll_abi(void) {
long v = syscall(__NR_landlock_create_ruleset, NULL, 0,
LL_CREATE_RULESET_VERSION);
return v > 0 ? (int)v : 0;
}
/* Only write-type rights are handled, so reading and exec stay allowed
* everywhere; the rules grant them back beneath the writable roots. */
static uint64_t ll_write_rights(int abi) {
uint64_t r = LL_FS_WRITE_FILE | LL_FS_REMOVE_DIR | LL_FS_REMOVE_FILE |
LL_FS_MAKE_CHAR | LL_FS_MAKE_DIR | LL_FS_MAKE_REG |
LL_FS_MAKE_SOCK | LL_FS_MAKE_FIFO | LL_FS_MAKE_BLOCK |
LL_FS_MAKE_SYM;
if (abi >= 2)
r |= LL_FS_REFER;
if (abi >= 3)
r |= LL_FS_TRUNCATE;
return r;
}
static int ll_allow(int ruleset, const char *path, uint64_t rights,
int is_dir) {
int fd = open(path, O_PATH | O_CLOEXEC | (is_dir ? O_DIRECTORY : 0));
if (fd < 0)
return -1;
struct ll_path_beneath_attr pb;
pb.allowed_access = rights;
pb.parent_fd = fd;
long rc = syscall(__NR_landlock_add_rule, ruleset, LL_RULE_PATH_BENEATH, &pb,
0);
close(fd);
return rc == 0 ? 0 : -1;
}
static int ll_prepare(const char *ws, const char *tmp, cst_sandbox_plan_t *p) {
int abi = g_abi;
struct ll_ruleset_attr attr;
memset(&attr, 0, sizeof(attr));
attr.handled_access_fs = ll_write_rights(abi);
if (abi >= 4)
attr.handled_access_net = LL_NET_BIND_TCP | LL_NET_CONNECT_TCP;
int fd = (int)syscall(__NR_landlock_create_ruleset, &attr, sizeof(attr), 0);
if (fd < 0)
return -1;
uint64_t dir_rights = attr.handled_access_fs;
uint64_t dev_rights = LL_FS_WRITE_FILE | (abi >= 3 ? LL_FS_TRUNCATE : 0);
if (ll_allow(fd, ws, dir_rights, 1) != 0) {
close(fd);
return -1;
}
if (tmp && tmp[0])
ll_allow(fd, tmp, dir_rights, 1); /* best effort: may not exist */
ll_allow(fd, "/dev/null", dev_rights, 0);
ll_allow(fd, "/dev/tty", dev_rights, 0);
p->ruleset_fd = fd;
return 0;
}
#endif /* CST_HAVE_LANDLOCK */
/* --- macOS: Seatbelt ------------------------------------------------------ */
#ifdef CST_HAVE_SEATBELT
/* Deprecated but still shipped; declared here to avoid the warning. */
extern int sandbox_init(const char *profile, uint64_t flags, char **errorbuf);
extern void sandbox_free_error(char *errorbuf);
static void sb_quote(char *out, size_t sz, const char *s) {
size_t o = 0;
for (; *s && o + 3 < sz; s++) {
if (*s == '"' || *s == '\\')
out[o++] = '\\';
out[o++] = *s;
}
out[o] = 0;
}
static int sb_prepare(const char *ws, const char *tmp, cst_sandbox_plan_t *p) {
char rws[OS_PATH_MAX], rtmp[OS_PATH_MAX];
char qws[2 * OS_PATH_MAX], qtmp[2 * OS_PATH_MAX];
if (os_realpath(ws, rws, sizeof(rws)) != 0)
return -1;
if (!tmp || os_realpath(tmp, rtmp, sizeof(rtmp)) != 0)
snprintf(rtmp, sizeof(rtmp), "/private/tmp");
sb_quote(qws, sizeof(qws), rws);
sb_quote(qtmp, sizeof(qtmp), rtmp);
size_t n = strlen(qws) + strlen(qtmp) + 512;
p->profile = malloc(n);
if (!p->profile)
return -1;
snprintf(p->profile, n,
"(version 1)(allow default)(deny file-write*)"
"(allow file-write* (subpath \"%s\") (subpath \"%s\") "
"(literal \"/dev/null\") (literal \"/dev/tty\") "
"(literal \"/dev/dtracehelper\"))"
"(deny network*)",
qws, qtmp);
return 0;
}
#endif /* CST_HAVE_SEATBELT */
/* --- common --------------------------------------------------------------- */
int cst_sandbox_probe(void) {
if (g_kind >= 0)
return g_kind;
g_kind = CST_SANDBOX_NONE;
const char *e = getenv("CST_SANDBOX");
if (e && (strcmp(e, "0") == 0 || strcmp(e, "off") == 0 ||
strcmp(e, "none") == 0))
return g_kind;
#ifdef CST_HAVE_LANDLOCK
g_abi = ll_abi();
if (g_abi > 0)
g_kind = CST_SANDBOX_LANDLOCK;
#elif defined(CST_HAVE_SEATBELT)
g_kind = CST_SANDBOX_SEATBELT;
#endif
return g_kind;
}
int cst_sandbox_landlock_abi(void) {
cst_sandbox_probe();
return g_abi;
}
const char *cst_sandbox_name(int kind) {
switch (kind) {
case CST_SANDBOX_LANDLOCK:
return "landlock";
case CST_SANDBOX_SEATBELT:
return "seatbelt";
default:
return "none";
}
}
int cst_sandbox_prepare(const char *workspace, const char *tmpdir,
cst_sandbox_plan_t *plan) {
memset(plan, 0, sizeof(*plan));
plan->ruleset_fd = -1;
int kind = cst_sandbox_probe();
int rc = -1;
#ifdef CST_HAVE_LANDLOCK
if (kind == CST_SANDBOX_LANDLOCK)
rc = ll_prepare(workspace, tmpdir, plan);
#endif
#ifdef CST_HAVE_SEATBELT
if (kind == CST_SANDBOX_SEATBELT)
rc = sb_prepare(workspace, tmpdir, plan);
#endif
(void)workspace;
(void)tmpdir;
plan->kind = rc == 0 ? kind : CST_SANDBOX_NONE;
return rc;
}
int cst_sandbox_enter(void *arg) {
const cst_sandbox_plan_t *p = arg;
if (!p)
return -1;
#ifdef CST_HAVE_LANDLOCK
if (p->kind == CST_SANDBOX_LANDLOCK) {
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0)
return -1;
return syscall(__NR_landlock_restrict_self, p->ruleset_fd, 0) == 0 ? 0
: -1;
}
#endif
#ifdef CST_HAVE_SEATBELT
if (p->kind == CST_SANDBOX_SEATBELT) {
char *err = NULL;
if (sandbox_init(p->profile, 0, &err) != 0) {
if (err)
sandbox_free_error(err);
return -1;
}
return 0;
}
#endif
return -1;
}
void cst_sandbox_release(cst_sandbox_plan_t *plan) {
if (!plan)
return;
#ifdef CST_HAVE_LANDLOCK
if (plan->ruleset_fd >= 0)
close(plan->ruleset_fd);
#endif
free(plan->profile);
plan->profile = NULL;
plan->ruleset_fd = -1;
plan->kind = CST_SANDBOX_NONE;
}
int cst_sandbox_run_shell(const char *workspace, const char *cmd,
size_t max_out, os_run_result_t *r) {
char tmp[OS_PATH_MAX];
if (os_temp_dir(tmp, sizeof(tmp)) != 0)
tmp[0] = 0;
cst_sandbox_plan_t plan;
if (cst_sandbox_prepare(workspace, tmp, &plan) != 0) {
memset(r, 0, sizeof(*r));
const char *msg = "[cst: sandbox unavailable; command not run]\n";
r->out = malloc(strlen(msg) + 1);
if (r->out) {
memcpy(r->out, msg, strlen(msg) + 1);
r->out_len = strlen(msg);
}
r->exit_code = 126;
return 0;
}
int rc = os_run_shell_ex(workspace, cmd, max_out, r, cst_sandbox_enter,
&plan);
cst_sandbox_release(&plan);
return rc;
}
static int has(const char *out, size_t n, const char *needle) {
size_t k = strlen(needle);
for (size_t i = 0; i + k <= n; i++)
if (memcmp(out + i, needle, k) == 0)
return 1;
return 0;
}
int cst_sandbox_blocked(const char *out, size_t n, int exit_code) {
if (exit_code == 0 || !out)
return 0;
static const char *const SIGNS[] = {
"Permission denied", "Read-only file system", "Operation not permitted",
"EACCES", "EPERM", "EROFS",
"[cst: sandbox", "Network is unreachable",
/* localized strerror(EACCES/EPERM/EROFS) */
"权限不够", "不允许的操作", "只读文件系统", "權限不足",
"拒絕不符權限的操作", "許可がありません", "Keine Berechtigung",
"Permission non accord", "Permiso denegado", "Отказано в доступе",
};
for (size_t i = 0; i < sizeof(SIGNS) / sizeof(SIGNS[0]); i++)
if (has(out, n, SIGNS[i]))
return 1;
return 0;
}