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:
+82
-1
@@ -6,6 +6,7 @@
|
||||
#include "cst_agent.h"
|
||||
#include "cst_metrics.h"
|
||||
#include "cst_perms.h"
|
||||
#include "cst_sandbox.h"
|
||||
#include "cst_tools.h"
|
||||
#include "cst_undo.h"
|
||||
#include "cst_util.h"
|
||||
@@ -240,6 +241,12 @@ char *cst_agent_build_prompt(const cst_agent_config_t *cfg, cst_protocol_t p,
|
||||
}
|
||||
if (!minimal && cst_protocol_is_ctl(p))
|
||||
cst_buf_puts(&b, SEC_CTL_NUMBERS);
|
||||
if (cfg && cfg->plan_mode)
|
||||
cst_buf_puts(&b, "\nPLAN MODE: you may only read and research (no file "
|
||||
"changes, no shell). Investigate, then answer with a "
|
||||
"concrete step-by-step plan: files to touch, what to "
|
||||
"change, how to verify. The user switches to Build mode "
|
||||
"to carry it out.\n");
|
||||
if (project_notes && project_notes[0]) {
|
||||
size_t n = strlen(project_notes);
|
||||
if (n > PROJECT_NOTES_MAX)
|
||||
@@ -550,6 +557,62 @@ static int needs_confirm(const cst_agent_config_t *cfg, cst_tool_perm_t p) {
|
||||
}
|
||||
}
|
||||
|
||||
/* A sandboxed run_shell looked blocked: ask whether to rerun it outside the
|
||||
* sandbox. On yes, *tr is replaced by the unsandboxed result; on no, a note
|
||||
* tells the model why it failed. */
|
||||
static void sandbox_escalate(turn_t *t, cst_tool_ctx_t *ctx,
|
||||
const ctl_tool_call_t *c, const char *cmd,
|
||||
cst_tool_result_t *tr) {
|
||||
cst_agent_t *a = t->a;
|
||||
int ans = CST_PERM_DENY;
|
||||
if (t->hooks && t->hooks->on_permission && !a->cancel) {
|
||||
size_t tail = tr->out_len > 1500 ? 1500 : tr->out_len;
|
||||
cst_buf_t d = {0};
|
||||
cst_buf_puts(&d, CST_PERM_SANDBOX);
|
||||
if (tr->out)
|
||||
cst_buf_append(&d, tr->out + tr->out_len - tail, tail);
|
||||
char *detail = cst_buf_steal(&d);
|
||||
hook_status(t, "blocked by the sandbox");
|
||||
ans = t->hooks->on_permission(c->name, detail ? detail : CST_PERM_SANDBOX,
|
||||
cmd, t->hooks->userdata);
|
||||
free(detail);
|
||||
}
|
||||
if (ans == CST_PERM_DENY) {
|
||||
const char *note = "\n[cst: blocked by the sandbox (writes are limited to "
|
||||
"the workspace and temp dir, no network); the user did "
|
||||
"not allow running it outside]\n";
|
||||
cst_buf_t b = {0};
|
||||
if (tr->out)
|
||||
cst_buf_append(&b, tr->out, tr->out_len);
|
||||
cst_buf_puts(&b, note);
|
||||
free(tr->out);
|
||||
tr->out_len = b.len;
|
||||
tr->out = cst_buf_steal(&b);
|
||||
return;
|
||||
}
|
||||
if (ans == CST_PERM_ALWAYS_PREFIX) {
|
||||
char pre[128];
|
||||
cst_perms_suggest_prefix(cmd, pre, sizeof(pre));
|
||||
if (pre[0])
|
||||
cst_perms_allow_prefix(a->perms, pre);
|
||||
} else if (ans == CST_PERM_ALWAYS_TOOL) {
|
||||
cst_perms_allow_tool(a->perms, c->name);
|
||||
}
|
||||
hook_status(t, "rerunning outside the sandbox");
|
||||
cst_tool_result_free(tr);
|
||||
memset(tr, 0, sizeof(*tr));
|
||||
ctx->sandbox = 0;
|
||||
cst_tool_run(ctx, c, tr);
|
||||
cst_buf_t b = {0};
|
||||
cst_buf_puts(&b, "[cst: the sandbox blocked this command; the user allowed "
|
||||
"rerunning it outside the sandbox. Output of that rerun:]\n");
|
||||
if (tr->out)
|
||||
cst_buf_append(&b, tr->out, tr->out_len);
|
||||
free(tr->out);
|
||||
tr->out_len = b.len;
|
||||
tr->out = cst_buf_steal(&b);
|
||||
}
|
||||
|
||||
/* Execute (or refuse) one confirmed call. Irreversible once started. */
|
||||
static void exec_call(turn_t *t, pcall_t *pc) {
|
||||
if (pc->done)
|
||||
@@ -602,11 +665,26 @@ static void exec_call(turn_t *t, pcall_t *pc) {
|
||||
}
|
||||
}
|
||||
}
|
||||
int allow = 1;
|
||||
int allow = 1, ask = 0;
|
||||
const char *cmd = def->perm == CST_TP_SHELL ? tool_cmd(c) : NULL;
|
||||
if (!dup && needs_confirm(&a->cfg, def->perm) && !a->cfg.auto_approve &&
|
||||
!cst_perms_tool_allowed(a->perms, c->name) &&
|
||||
!(cmd && cst_perms_shell_allowed(a->perms, cmd))) {
|
||||
ask = 1;
|
||||
if (a->cfg.approval == CST_APPROVAL_AUTO) {
|
||||
if (def->perm == CST_TP_WRITE) {
|
||||
ask = 0;
|
||||
} else if (def->perm == CST_TP_SHELL) {
|
||||
if (cst_sandbox_probe() != CST_SANDBOX_NONE) {
|
||||
ctx.sandbox = 1;
|
||||
ask = 0;
|
||||
} else if (cmd && cst_shell_is_safe(cmd)) {
|
||||
ask = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ask) {
|
||||
if (t->hooks && t->hooks->on_permission) {
|
||||
char *preview = NULL;
|
||||
if (def->preview)
|
||||
@@ -650,6 +728,9 @@ static void exec_call(turn_t *t, pcall_t *pc) {
|
||||
undo_snapshot(a, c);
|
||||
cst_tool_result_t tr = {0};
|
||||
cst_tool_run(&ctx, c, &tr);
|
||||
if (ctx.sandbox && cmd &&
|
||||
cst_sandbox_blocked(tr.out, tr.out_len, tr.exit_code))
|
||||
sandbox_escalate(t, &ctx, c, cmd, &tr);
|
||||
set_output(pc, tr.out ? tr.out : "", tr.out_len, tr.exit_code);
|
||||
cst_tool_result_free(&tr);
|
||||
if (mutating)
|
||||
|
||||
Reference in New Issue
Block a user