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:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user