v0.8.2: build fixes, WebUI security and correctness
Build: <fcntl.h> in tui.c, format-truncation in http_srv.c and /web. WebUI: - refuse cross-site requests (Origin must be the page itself) in the default no-token mode: any page could POST approval=yolo and a prompt - session/workspace/undo/compact/attach/model changes are 409 while a turn runs (use-after-free, messages landing in the wrong session) - ask_user returned "dismissed" at once instead of waiting - cancel and shutdown wake a turn parked on a permission or question; SIGTERM mid-turn no longer hangs or crashes - a page opened while a question is pending still gets it; Last-Event-ID resumes the stream; event seq matches the SSE id - token mode: page assets load without the token, images via cookie - --web-open works, --web-port/--web-token are validated - frontend: gate keys ignored while typing, clear-images button, reasoning no longer wiped while streaming HTTP server: auth and size cap before reading the body, strict Content-Length, NUL in url_decode, 413 on keep-alive, idle timeout, absolute-form only without a leading '/', case-insensitive Connection, header deadline, 400 for long request lines, unlink before close, poll() instead of select(), SO_EXCLUSIVEADDRUSE on Windows. Markdown sanitizer escapes quotes in attribute values. /web: passes this session's provider/model/base/effort/approval (key via the environment), keeps output off the TUI, shows the real URL and keeps CURRENT on the TUI's session. Tests: scripts/e2e_webui_ask.py, scripts/e2e_webui_http.py (run from e2e_webui.sh), sanitizer quote test.
This commit is contained in:
@@ -5369,6 +5369,7 @@ static void cmd_quit(tui_t *t, char *arg, int *quit) {
|
||||
* the TUI holding the terminal. The child gets a fresh session: two writers on
|
||||
* one session file is the one thing that would corrupt it. */
|
||||
#ifndef _WIN32
|
||||
#include <fcntl.h>
|
||||
#include <sys/wait.h> /* waitpid: collect the WebUI started by the last /web */
|
||||
#endif
|
||||
#ifndef CST_WEBUI_DEFAULT_PORT
|
||||
@@ -5393,33 +5394,117 @@ static void cmd_web(tui_t *t, char *arg, int *quit) {
|
||||
snprintf(exe, sizeof(exe), "%s/cst-agent", dir);
|
||||
const cst_agent_config_t *cfg = cst_agent_config(t->agent);
|
||||
const char *ws = (cfg && cfg->workspace_root[0]) ? cfg->workspace_root : ".";
|
||||
char logf[4096];
|
||||
char logf[4096 + 32];
|
||||
snprintf(logf, sizeof(logf), "%s/.agent", ws);
|
||||
os_mkdir_p(logf, 0755);
|
||||
snprintf(logf, sizeof(logf), "%s/.agent/webui.log", ws);
|
||||
/* The WebUI talks to what this session talks to (-b, -m, a /provider
|
||||
* switch...). The key goes through the environment, not argv (ps). */
|
||||
const char *argv[20];
|
||||
int na = 0;
|
||||
argv[na++] = "cst-agent";
|
||||
argv[na++] = "--web";
|
||||
argv[na++] = "--workspace";
|
||||
argv[na++] = ws;
|
||||
argv[na++] = "--new";
|
||||
if (cfg && cfg->provider[0])
|
||||
argv[na++] = "--provider", argv[na++] = cfg->provider;
|
||||
if (cfg && cfg->model[0])
|
||||
argv[na++] = "--model", argv[na++] = cfg->model;
|
||||
if (cfg && cfg->api_base[0])
|
||||
argv[na++] = "--api-base", argv[na++] = cfg->api_base;
|
||||
if (cfg && cfg->effort[0])
|
||||
argv[na++] = "--effort", argv[na++] = cfg->effort;
|
||||
if (cfg)
|
||||
argv[na++] = "--approval", argv[na++] = cst_approval_name(cfg->approval);
|
||||
argv[na] = NULL;
|
||||
size_t nenv = 0;
|
||||
while (environ[nenv])
|
||||
nenv++;
|
||||
char **envp = calloc(nenv + 2, sizeof(char *));
|
||||
char keyvar[600] = "";
|
||||
if (!envp) {
|
||||
push_system_line(t, "out of memory");
|
||||
return;
|
||||
}
|
||||
size_t ne = 0;
|
||||
for (size_t i = 0; i < nenv; i++)
|
||||
if (strncmp(environ[i], "CST_API_KEY=", 12) != 0)
|
||||
envp[ne++] = environ[i];
|
||||
if (cfg && cfg->api_key[0]) {
|
||||
snprintf(keyvar, sizeof(keyvar), "CST_API_KEY=%s", cfg->api_key);
|
||||
envp[ne++] = keyvar;
|
||||
}
|
||||
envp[ne] = NULL;
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
free(envp);
|
||||
push_system_line(t, "cannot start a second process");
|
||||
return;
|
||||
}
|
||||
if (pid == 0) {
|
||||
setsid();
|
||||
/* never inherit the TUI's terminal: its output would land on our screen */
|
||||
int in = open("/dev/null", O_RDONLY);
|
||||
if (in >= 0 && in != STDIN_FILENO) {
|
||||
dup2(in, STDIN_FILENO);
|
||||
close(in);
|
||||
}
|
||||
int fd = open(logf, O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
||||
if (fd < 0)
|
||||
fd = open("/dev/null", O_WRONLY);
|
||||
if (fd >= 0) {
|
||||
dup2(fd, STDOUT_FILENO);
|
||||
dup2(fd, STDERR_FILENO);
|
||||
if (fd > 2)
|
||||
close(fd);
|
||||
}
|
||||
execl(exe, "cst-agent", "--web", "--workspace", ws, "--new", (char *)NULL);
|
||||
execve(exe, (char *const *)argv, envp);
|
||||
_exit(127);
|
||||
}
|
||||
free(envp);
|
||||
spawned = pid;
|
||||
/* Wait (2 s at most) for the URL it prints once it listens: the port may
|
||||
* come from CST_WEB_PORT, or it may not start at all. */
|
||||
char url[512] = "";
|
||||
int died = 0;
|
||||
for (int i = 0; i < 40 && !url[0] && !died; i++) {
|
||||
usleep(50000);
|
||||
FILE *fp = fopen(logf, "r");
|
||||
char line[512];
|
||||
while (fp && fgets(line, sizeof(line), fp)) {
|
||||
if (strncmp(line, "webui: http", 11) == 0) {
|
||||
line[strcspn(line, "\r\n")] = 0;
|
||||
snprintf(url, sizeof(url), "%s", line + 7);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (fp)
|
||||
fclose(fp);
|
||||
if (!url[0] && waitpid(pid, NULL, WNOHANG) == pid) {
|
||||
died = 1;
|
||||
spawned = 0;
|
||||
}
|
||||
}
|
||||
/* its --new made the WebUI's session CURRENT; a plain `cst-agent` later
|
||||
* should still resume this one */
|
||||
char sid[128] = "";
|
||||
cst_agent_session_id(t->agent, sid, sizeof(sid));
|
||||
if (sid[0])
|
||||
cst_agent_store_set_current(ws, sid);
|
||||
char msg[4200];
|
||||
snprintf(msg, sizeof(msg),
|
||||
"WebUI started in the background: http://127.0.0.1:%d/ (new session)",
|
||||
CST_WEBUI_DEFAULT_PORT);
|
||||
if (died)
|
||||
snprintf(msg, sizeof(msg), "the WebUI did not start (port busy?): see %s",
|
||||
logf);
|
||||
else if (url[0])
|
||||
snprintf(msg, sizeof(msg), "WebUI started in the background: %s (new session)",
|
||||
url);
|
||||
else
|
||||
snprintf(msg, sizeof(msg), "WebUI starting in the background "
|
||||
"(http://127.0.0.1:%d/ unless CST_WEB_PORT says "
|
||||
"otherwise)", CST_WEBUI_DEFAULT_PORT);
|
||||
push_system_line(t, msg);
|
||||
snprintf(msg, sizeof(msg), "its log: %s (port busy? see the log, or pass "
|
||||
"--web-port)", logf);
|
||||
snprintf(msg, sizeof(msg), "its log: %s", logf);
|
||||
push_system_line(t, msg);
|
||||
#endif
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user