forked from WHC/Hermit
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:
+223
-43
@@ -72,11 +72,14 @@ typedef struct wui {
|
||||
wui_sse_t *subs;
|
||||
|
||||
int busy;
|
||||
int op_busy; /* a session/workspace/undo/compact/attach call runs */
|
||||
int turn_live; /* the turn thread still uses w; cleared as it exits */
|
||||
uint64_t req_id; /* permission/ask id counter */
|
||||
uint64_t perm_id;
|
||||
int perm_active, perm_answer;
|
||||
uint64_t ask_id;
|
||||
int ask_active, ask_rc;
|
||||
int ask_active, ask_rc; /* ask_rc: 0 pending, 1 answered, -1 dismissed */
|
||||
uint64_t pending_seq; /* event of the open permission/ask_user, 0 = none */
|
||||
char ask_ans[WUI_ASK_MAX];
|
||||
|
||||
volatile int stopping;
|
||||
@@ -88,36 +91,44 @@ static uint64_t wui_first_seq(const wui_t *w) {
|
||||
return w->seq > WUI_RING ? w->seq - WUI_RING + 1 : 1;
|
||||
}
|
||||
|
||||
/* Owns data (a cJSON object). */
|
||||
static void wui_emit(wui_t *w, const char *type, cJSON *data) {
|
||||
/* Owns data (a cJSON object). Returns the event's seq, 0 when dropped. */
|
||||
static uint64_t wui_emit(wui_t *w, const char *type, cJSON *data) {
|
||||
if (!data)
|
||||
data = cJSON_CreateObject();
|
||||
if (!data)
|
||||
return;
|
||||
return 0;
|
||||
cJSON_AddStringToObject(data, "type", type);
|
||||
/* the seq in the payload must match the SSE id, so it is taken under mu */
|
||||
pthread_mutex_lock(&w->mu);
|
||||
cJSON_AddNumberToObject(data, "seq", (double)(w->seq + 1));
|
||||
char *json = cJSON_PrintUnformatted(data);
|
||||
cJSON_Delete(data);
|
||||
if (!json)
|
||||
return;
|
||||
cst_buf_t f = {0};
|
||||
cst_buf_printf(&f, "event: %s\ndata: %s\n\n", type, json);
|
||||
if (json)
|
||||
cst_buf_printf(&f, "event: %s\ndata: %s\n\n", type, json);
|
||||
free(json);
|
||||
if (!f.buf) {
|
||||
pthread_mutex_unlock(&w->mu);
|
||||
cst_buf_free(&f);
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
pthread_mutex_lock(&w->mu);
|
||||
w->seq++;
|
||||
int slot = (int)(w->seq % WUI_RING);
|
||||
uint64_t seq = ++w->seq;
|
||||
int slot = (int)(seq % WUI_RING);
|
||||
free(w->ring[slot].frame);
|
||||
w->ring[slot].frame = f.buf; /* stolen */
|
||||
w->ring[slot].seq = w->seq;
|
||||
w->ring[slot].seq = seq;
|
||||
f.buf = NULL;
|
||||
for (wui_sse_t *s = w->subs; s; s = s->next)
|
||||
pthread_cond_broadcast(&s->cv);
|
||||
pthread_mutex_unlock(&w->mu);
|
||||
cst_buf_free(&f);
|
||||
return seq;
|
||||
}
|
||||
|
||||
static void wui_set_pending(wui_t *w, uint64_t seq) {
|
||||
pthread_mutex_lock(&w->mu);
|
||||
w->pending_seq = seq;
|
||||
pthread_mutex_unlock(&w->mu);
|
||||
}
|
||||
|
||||
static void wui_emit_str(wui_t *w, const char *type, const char *key,
|
||||
@@ -379,7 +390,7 @@ static int wui_wait_permission(wui_t *w, const char *tool, const char *detail,
|
||||
strncmp(detail, CST_PERM_SANDBOX,
|
||||
strlen(CST_PERM_SANDBOX)) ==
|
||||
0);
|
||||
wui_emit(w, "permission", o);
|
||||
wui_set_pending(w, wui_emit(w, "permission", o));
|
||||
}
|
||||
int timeout = w->perm_timeout_s > 0 ? w->perm_timeout_s : 600;
|
||||
/* Nobody is watching: do not hang the turn for ten minutes. */
|
||||
@@ -404,6 +415,8 @@ static int wui_wait_permission(wui_t *w, const char *tool, const char *detail,
|
||||
break;
|
||||
}
|
||||
}
|
||||
w->perm_active = 0;
|
||||
w->pending_seq = 0;
|
||||
pthread_mutex_unlock(&w->mu);
|
||||
cJSON *e = cJSON_CreateObject();
|
||||
if (e) {
|
||||
@@ -427,7 +440,7 @@ static int on_ask_user(const char *question, const char *options, char *ans,
|
||||
uint64_t id = w->req_id;
|
||||
w->ask_id = id;
|
||||
w->ask_active = 1;
|
||||
w->ask_rc = -1;
|
||||
w->ask_rc = 0;
|
||||
w->ask_ans[0] = 0;
|
||||
int has_client = w->subs != NULL;
|
||||
pthread_mutex_unlock(&w->mu);
|
||||
@@ -449,7 +462,7 @@ static int on_ask_user(const char *question, const char *options, char *ans,
|
||||
}
|
||||
p = nl ? nl + 1 : NULL;
|
||||
}
|
||||
wui_emit(w, "ask_user", o);
|
||||
wui_set_pending(w, wui_emit(w, "ask_user", o));
|
||||
}
|
||||
int timeout = has_client ? 600 : 3;
|
||||
int rc = -1;
|
||||
@@ -472,6 +485,8 @@ static int on_ask_user(const char *question, const char *options, char *ans,
|
||||
break;
|
||||
}
|
||||
}
|
||||
w->ask_active = 0;
|
||||
w->pending_seq = 0;
|
||||
pthread_mutex_unlock(&w->mu);
|
||||
return rc;
|
||||
}
|
||||
@@ -544,6 +559,8 @@ static int wui_serve_asset(cst_http_conn_t *c, wui_t *w, const char *path) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int wui_auth(const cst_http_req_t *req, void *ud);
|
||||
|
||||
static int h_asset(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
const cst_http_req_t *req = cst_http_request(c);
|
||||
@@ -552,6 +569,9 @@ static int h_asset(cst_http_conn_t *c, void *ud) {
|
||||
* reachable. */
|
||||
static const char prefix[] = "/api/attach/";
|
||||
if (strncmp(req->path, prefix, sizeof(prefix) - 1) == 0) {
|
||||
/* the fallback route is public (the page's own assets); images are not */
|
||||
if (wui_auth(req, w) != 0)
|
||||
return cst_http_send_err(c, 401, "unauthorized");
|
||||
const char *rel = req->path + sizeof(prefix) - 1;
|
||||
if (!cst_attach_rel_ok(rel))
|
||||
return -1;
|
||||
@@ -569,8 +589,8 @@ static int h_asset(cst_http_conn_t *c, void *ud) {
|
||||
size_t n = strlen(rel);
|
||||
if (n > 4 && strcmp(rel + n - 4, ".png") == 0)
|
||||
ctype = "image/png";
|
||||
else if (n > 4 && (strcmp(rel + n - 4, ".jpg") == 0 ||
|
||||
strcmp(rel + n - 4, ".jpeg") == 0))
|
||||
else if ((n > 4 && strcmp(rel + n - 4, ".jpg") == 0) ||
|
||||
(n > 5 && strcmp(rel + n - 5, ".jpeg") == 0))
|
||||
ctype = "image/jpeg";
|
||||
else if (n > 4 && strcmp(rel + n - 4, ".gif") == 0)
|
||||
ctype = "image/gif";
|
||||
@@ -630,11 +650,19 @@ static int h_events(cst_http_conn_t *c, void *ud) {
|
||||
char buf[32];
|
||||
if (cst_http_query(req, "since", buf, sizeof(buf)) == 0)
|
||||
s.cursor = (uint64_t)strtoull(buf, NULL, 10);
|
||||
else if (req->last_event_id)
|
||||
s.cursor = (uint64_t)strtoull(req->last_event_id, NULL, 10);
|
||||
pthread_mutex_init(&s.mu, NULL);
|
||||
pthread_cond_init(&s.cv, NULL);
|
||||
pthread_mutex_lock(&w->mu);
|
||||
if (s.cursor == 0)
|
||||
s.cursor = w->seq; /* a fresh client wants new events only */
|
||||
/* 0 = a fresh client wants new events only; past the head = ids from an
|
||||
* earlier server process */
|
||||
if (s.cursor == 0 || s.cursor > w->seq) {
|
||||
s.cursor = w->seq;
|
||||
/* a page opened while a question waits must still get to answer it */
|
||||
if (w->pending_seq && w->pending_seq <= w->seq)
|
||||
s.cursor = w->pending_seq - 1;
|
||||
}
|
||||
if (s.cursor < wui_first_seq(w) - 1)
|
||||
s.cursor = wui_first_seq(w) - 1; /* too far behind: resume at the head */
|
||||
s.next = w->subs;
|
||||
@@ -741,14 +769,19 @@ static void *wui_turn_main(void *arg) {
|
||||
wui_end_turn(w, rc, err);
|
||||
free(t->prompt);
|
||||
free(t);
|
||||
pthread_mutex_lock(&w->mu);
|
||||
w->turn_live = 0;
|
||||
pthread_cond_broadcast(&w->cv);
|
||||
pthread_mutex_unlock(&w->mu);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int wui_start_turn(wui_t *w, const char *text, char *err, size_t err_sz) {
|
||||
pthread_mutex_lock(&w->mu);
|
||||
if (w->busy) {
|
||||
if (w->busy || w->op_busy) {
|
||||
pthread_mutex_unlock(&w->mu);
|
||||
snprintf(err, err_sz, "a turn is already running");
|
||||
snprintf(err, err_sz, w->busy ? "a turn is already running"
|
||||
: "another change is running");
|
||||
return -1;
|
||||
}
|
||||
w->busy = 1;
|
||||
@@ -776,11 +809,15 @@ static int wui_start_turn(wui_t *w, const char *text, char *err, size_t err_sz)
|
||||
return -1;
|
||||
}
|
||||
pthread_t th;
|
||||
pthread_mutex_lock(&w->mu);
|
||||
w->turn_live = 1;
|
||||
pthread_mutex_unlock(&w->mu);
|
||||
if (pthread_create(&th, NULL, wui_turn_main, t) != 0) {
|
||||
free(t->prompt);
|
||||
free(t);
|
||||
pthread_mutex_lock(&w->mu);
|
||||
w->busy = 0;
|
||||
w->turn_live = 0;
|
||||
pthread_mutex_unlock(&w->mu);
|
||||
snprintf(err, err_sz, "cannot start the turn thread");
|
||||
return -1;
|
||||
@@ -808,9 +845,45 @@ static int h_turn(cst_http_conn_t *c, void *ud) {
|
||||
"{\"ok\":true}");
|
||||
}
|
||||
|
||||
/* The agent is not locked: switching sessions, the workspace, undo, compact
|
||||
* and the pending images all change what a running turn reads. These run
|
||||
* one at a time and never during a turn. */
|
||||
static int wui_claim(wui_t *w) {
|
||||
pthread_mutex_lock(&w->mu);
|
||||
int ok = !w->busy && !w->op_busy;
|
||||
if (ok)
|
||||
w->op_busy = 1;
|
||||
pthread_mutex_unlock(&w->mu);
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
static void wui_release(wui_t *w) {
|
||||
pthread_mutex_lock(&w->mu);
|
||||
w->op_busy = 0;
|
||||
pthread_mutex_unlock(&w->mu);
|
||||
}
|
||||
|
||||
#define WUI_EXCLUSIVE(name) \
|
||||
static int name(cst_http_conn_t *c, void *ud) { \
|
||||
wui_t *w = ud; \
|
||||
if (wui_claim(w) != 0) \
|
||||
return cst_http_send_err(c, 409, "busy: wait for the running turn"); \
|
||||
int rc = name##_x(c, ud); \
|
||||
wui_release(w); \
|
||||
return rc; \
|
||||
}
|
||||
|
||||
static int h_cancel(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
cst_agent_request_cancel(w->agent);
|
||||
/* a turn parked on the gate or a question would not see the flag */
|
||||
pthread_mutex_lock(&w->mu);
|
||||
if (w->perm_active && w->perm_answer < 0)
|
||||
w->perm_answer = CST_PERM_DENY;
|
||||
if (w->ask_active && w->ask_rc == 0)
|
||||
w->ask_rc = -1;
|
||||
pthread_cond_broadcast(&w->cv);
|
||||
pthread_mutex_unlock(&w->mu);
|
||||
return cst_http_send_str(c, 200, "application/json; charset=utf-8",
|
||||
"{\"ok\":true}");
|
||||
}
|
||||
@@ -870,11 +943,15 @@ static int h_ask_user(cst_http_conn_t *c, void *ud) {
|
||||
static int h_sessions(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
const cst_agent_config_t *cfg = cst_agent_config(w->agent);
|
||||
static char ids[200][128];
|
||||
char (*ids)[128] = malloc(200 * sizeof(*ids)); /* handlers run in parallel */
|
||||
if (!ids)
|
||||
return cst_http_send_err(c, 500, "out of memory");
|
||||
int n = cst_agent_store_list(cfg->workspace_root, ids, 200);
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
if (!root)
|
||||
if (!root) {
|
||||
free(ids);
|
||||
return cst_http_send_err(c, 500, "out of memory");
|
||||
}
|
||||
char cur[128] = "";
|
||||
cst_agent_store_get_current(cfg->workspace_root, cur, sizeof(cur));
|
||||
cJSON_AddStringToObject(root, "current", cur);
|
||||
@@ -893,6 +970,7 @@ static int h_sessions(cst_http_conn_t *c, void *ud) {
|
||||
cJSON_AddNumberToObject(o, "mtime", (double)in.mtime);
|
||||
cJSON_AddItemToArray(arr, o);
|
||||
}
|
||||
free(ids);
|
||||
char *json = cJSON_PrintUnformatted(root);
|
||||
cJSON_Delete(root);
|
||||
int rc = cst_http_send_json(c, 200, json);
|
||||
@@ -900,7 +978,7 @@ static int h_sessions(cst_http_conn_t *c, void *ud) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int h_session_new(cst_http_conn_t *c, void *ud) {
|
||||
static int h_session_new_x(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
char id[128] = "";
|
||||
if (cst_agent_new_session(w->agent, id, sizeof(id)) != 0)
|
||||
@@ -920,7 +998,7 @@ static int h_session_new(cst_http_conn_t *c, void *ud) {
|
||||
|
||||
/* POST /api/workspace {"path":"/abs/dir"} - re-root the agent. The session
|
||||
* store is per directory, so this always lands in a fresh session there. */
|
||||
static int h_workspace(cst_http_conn_t *c, void *ud) {
|
||||
static int h_workspace_x(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
const cst_http_req_t *req = cst_http_request(c);
|
||||
cJSON *j = req->body_len ? cJSON_Parse(req->body) : NULL;
|
||||
@@ -950,7 +1028,7 @@ static int h_workspace(cst_http_conn_t *c, void *ud) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int h_session_switch(cst_http_conn_t *c, void *ud) {
|
||||
static int h_session_switch_x(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
const cst_http_req_t *req = cst_http_request(c);
|
||||
cJSON *j = req->body_len ? cJSON_Parse(req->body) : NULL;
|
||||
@@ -975,6 +1053,15 @@ static int h_config(cst_http_conn_t *c, void *ud) {
|
||||
cJSON_Delete(j);
|
||||
return cst_http_send_err(c, 400, "expected a JSON object");
|
||||
}
|
||||
/* the strings are read by the turn as it builds each request; approval
|
||||
* and plan mode may change mid-turn (say, to yolo at a pending gate) */
|
||||
int claimed = cJSON_GetObjectItem(j, "model") ||
|
||||
cJSON_GetObjectItem(j, "provider") ||
|
||||
cJSON_GetObjectItem(j, "effort");
|
||||
if (claimed && wui_claim(w) != 0) {
|
||||
cJSON_Delete(j);
|
||||
return cst_http_send_err(c, 409, "busy: wait for the running turn");
|
||||
}
|
||||
const cJSON *item;
|
||||
if ((item = cJSON_GetObjectItem(j, "model")) && cJSON_IsString(item))
|
||||
snprintf(cfg->model, sizeof(cfg->model), "%s", item->valuestring);
|
||||
@@ -995,12 +1082,14 @@ static int h_config(cst_http_conn_t *c, void *ud) {
|
||||
cfg->read_only = cfg->plan_mode;
|
||||
}
|
||||
cJSON_Delete(j);
|
||||
if (claimed)
|
||||
wui_release(w);
|
||||
wui_emit_state(w);
|
||||
return cst_http_send_str(c, 200, "application/json; charset=utf-8",
|
||||
"{\"ok\":true}");
|
||||
}
|
||||
|
||||
static int h_undo(cst_http_conn_t *c, void *ud) {
|
||||
static int h_undo_x(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
char report[2048] = "";
|
||||
if (cst_agent_undo(w->agent, report, sizeof(report)) != 0)
|
||||
@@ -1010,7 +1099,7 @@ static int h_undo(cst_http_conn_t *c, void *ud) {
|
||||
return cst_http_send_str(c, 200, "text/plain; charset=utf-8", report);
|
||||
}
|
||||
|
||||
static int h_compact(cst_http_conn_t *c, void *ud) {
|
||||
static int h_compact_x(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
char err[256] = "";
|
||||
if (cst_agent_compact(w->agent, err, sizeof(err)) != 0)
|
||||
@@ -1020,7 +1109,7 @@ static int h_compact(cst_http_conn_t *c, void *ud) {
|
||||
"{\"ok\":true}");
|
||||
}
|
||||
|
||||
static int h_attach_upload(cst_http_conn_t *c, void *ud) {
|
||||
static int h_attach_upload_x(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
const cst_http_req_t *req = cst_http_request(c);
|
||||
char name[256] = "pasted.png";
|
||||
@@ -1036,7 +1125,7 @@ static int h_attach_upload(cst_http_conn_t *c, void *ud) {
|
||||
"{\"ok\":true}");
|
||||
}
|
||||
|
||||
static int h_attach_clear(cst_http_conn_t *c, void *ud) {
|
||||
static int h_attach_clear_x(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
cst_agent_pending_clear(w->agent);
|
||||
wui_emit_state(w);
|
||||
@@ -1044,6 +1133,14 @@ static int h_attach_clear(cst_http_conn_t *c, void *ud) {
|
||||
"{\"ok\":true}");
|
||||
}
|
||||
|
||||
WUI_EXCLUSIVE(h_session_new)
|
||||
WUI_EXCLUSIVE(h_workspace)
|
||||
WUI_EXCLUSIVE(h_session_switch)
|
||||
WUI_EXCLUSIVE(h_undo)
|
||||
WUI_EXCLUSIVE(h_compact)
|
||||
WUI_EXCLUSIVE(h_attach_upload)
|
||||
WUI_EXCLUSIVE(h_attach_clear)
|
||||
|
||||
static int h_quit(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
w->stopping = 1;
|
||||
@@ -1107,10 +1204,51 @@ static int host_is_loopback(const char *host) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int strncasecmp_ascii(const char *a, const char *b, size_t n) {
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
char x = a[i], y = b[i];
|
||||
if (x >= 'A' && x <= 'Z')
|
||||
x = (char)(x - 'A' + 'a');
|
||||
if (y >= 'A' && y <= 'Z')
|
||||
y = (char)(y - 'A' + 'a');
|
||||
if (x != y)
|
||||
return x < y ? -1 : 1;
|
||||
if (!x)
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* A browser names the page that sent a request in Origin. Without a token
|
||||
* that is the only thing that tells our own page from any site the user
|
||||
* visits: a no-cors POST with a text/plain body needs no preflight and
|
||||
* carries Host: 127.0.0.1. So it must be this very origin. */
|
||||
static int origin_is_self(const char *origin, const char *host) {
|
||||
if (!origin)
|
||||
return 1; /* curl and friends; browsers always send it on POST */
|
||||
while (*origin == ' ')
|
||||
origin++;
|
||||
const char *rest = NULL;
|
||||
if (strncmp(origin, "http://", 7) == 0)
|
||||
rest = origin + 7;
|
||||
else if (strncmp(origin, "https://", 8) == 0)
|
||||
rest = origin + 8;
|
||||
if (!rest || !host)
|
||||
return 0;
|
||||
while (*host == ' ')
|
||||
host++;
|
||||
size_t n = strlen(host);
|
||||
while (n && (host[n - 1] == ' ' || host[n - 1] == '\t'))
|
||||
n--;
|
||||
return strlen(rest) == n && strncasecmp_ascii(rest, host, n) == 0;
|
||||
}
|
||||
|
||||
static int wui_auth(const cst_http_req_t *req, void *ud) {
|
||||
wui_t *w = ud;
|
||||
if (!w->need_auth)
|
||||
return host_is_loopback(req->host) ? 0 : -1;
|
||||
return host_is_loopback(req->host) && origin_is_self(req->origin, req->host)
|
||||
? 0
|
||||
: -1;
|
||||
char buf[128];
|
||||
if (req->authorization && strncmp(req->authorization, "Bearer ", 7) == 0 &&
|
||||
token_matches(w, req->authorization + 7))
|
||||
@@ -1158,23 +1296,21 @@ static void wui_make_token(char *out, size_t out_sz) {
|
||||
}
|
||||
|
||||
static void open_browser(const char *url) {
|
||||
char cmd[600];
|
||||
#if defined(_WIN32)
|
||||
char cmd[600];
|
||||
snprintf(cmd, sizeof(cmd), "start \"\" \"%s\"", url);
|
||||
os_run_shell_c(".", cmd, 0, NULL);
|
||||
#elif defined(__APPLE__)
|
||||
char cmd[600];
|
||||
snprintf(cmd, sizeof(cmd), "open '%s'", url);
|
||||
os_run_shell_c(".", cmd, 0, NULL);
|
||||
#else
|
||||
char cmd[600];
|
||||
/* Termux has no xdg-open; termux-open-url hands the URL to Android. */
|
||||
const char *opener =
|
||||
getenv("TERMUX_VERSION") ? "termux-open-url" : "xdg-open";
|
||||
snprintf(cmd, sizeof(cmd), "%s '%s' >/dev/null 2>&1 &", opener, url);
|
||||
os_run_shell_c(".", cmd, 0, NULL);
|
||||
#endif
|
||||
(void)url;
|
||||
os_run_result_t r;
|
||||
memset(&r, 0, sizeof(r)); /* a NULL result makes the runner refuse */
|
||||
os_run_shell_c(".", cmd, 0, &r);
|
||||
os_run_result_free(&r);
|
||||
}
|
||||
|
||||
int cst_webui_run(cst_agent_t *agent, const cst_webui_opts_t *opts) {
|
||||
@@ -1194,6 +1330,14 @@ int cst_webui_run(cst_agent_t *agent, const cst_webui_opts_t *opts) {
|
||||
snprintf(w->host, sizeof(w->host), "%s",
|
||||
opts->host && opts->host[0] ? opts->host : "127.0.0.1");
|
||||
if (opts->token && opts->token[0]) {
|
||||
if (strlen(opts->token) >= sizeof(w->token)) {
|
||||
fprintf(stderr, "webui: the token is longer than %d characters\n",
|
||||
(int)sizeof(w->token) - 1);
|
||||
pthread_cond_destroy(&w->cv);
|
||||
pthread_mutex_destroy(&w->mu);
|
||||
free(w);
|
||||
return 1;
|
||||
}
|
||||
snprintf(w->token, sizeof(w->token), "%s", opts->token);
|
||||
w->need_auth = 1;
|
||||
} else if (strcmp(w->host, "127.0.0.1") != 0 &&
|
||||
@@ -1227,8 +1371,10 @@ int cst_webui_run(cst_agent_t *agent, const cst_webui_opts_t *opts) {
|
||||
routes[i].ud = w;
|
||||
routes[i].auth = w->need_auth || strcmp(routes[i].path, "/") != 0;
|
||||
}
|
||||
routes[0].auth = 0; /* the shell page itself sets the cookie */
|
||||
routes[sizeof(routes) / sizeof(routes[0]) - 1].auth = w->need_auth;
|
||||
/* the shell page and its assets carry no token (app.js reads it from the
|
||||
* URL and sets the cookie); h_asset checks /api/attach/ itself */
|
||||
routes[0].auth = 0;
|
||||
routes[sizeof(routes) / sizeof(routes[0]) - 1].auth = 0;
|
||||
|
||||
cst_http_opts_t ho;
|
||||
memset(&ho, 0, sizeof(ho));
|
||||
@@ -1262,8 +1408,12 @@ int cst_webui_run(cst_agent_t *agent, const cst_webui_opts_t *opts) {
|
||||
w->token);
|
||||
fflush(stderr);
|
||||
wui_emit_state(w);
|
||||
if (opts->open_browser)
|
||||
open_browser(url);
|
||||
if (opts->open_browser) {
|
||||
char full[sizeof(url) + 80];
|
||||
snprintf(full, sizeof(full), "%s%s%s", url, w->need_auth ? "#token=" : "",
|
||||
w->need_auth ? w->token : "");
|
||||
open_browser(full);
|
||||
}
|
||||
|
||||
if (opts->prompt && opts->prompt[0]) {
|
||||
char err2[256] = "";
|
||||
@@ -1281,8 +1431,38 @@ int cst_webui_run(cst_agent_t *agent, const cst_webui_opts_t *opts) {
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
fprintf(stderr, "webui: shutting down\n");
|
||||
/* Wake a turn parked on a permission/ask_user answer, cancel it and wait
|
||||
* for its thread: it emits through w, and the agent is destroyed next. */
|
||||
pthread_mutex_lock(&w->mu);
|
||||
w->stopping = 1;
|
||||
cst_http_stop(w->srv);
|
||||
pthread_cond_broadcast(&w->cv);
|
||||
int live = w->turn_live;
|
||||
pthread_mutex_unlock(&w->mu);
|
||||
cst_agent_request_cancel(w->agent); /* also a /api/compact in flight */
|
||||
if (live) {
|
||||
struct timespec deadline;
|
||||
clock_gettime(CLOCK_REALTIME, &deadline);
|
||||
deadline.tv_sec += 30;
|
||||
pthread_mutex_lock(&w->mu);
|
||||
while (w->turn_live &&
|
||||
pthread_cond_timedwait(&w->cv, &w->mu, &deadline) == 0)
|
||||
;
|
||||
live = w->turn_live;
|
||||
pthread_mutex_unlock(&w->mu);
|
||||
}
|
||||
if (live) {
|
||||
/* still running (a command ignoring cancel): the caller would destroy
|
||||
* the agent under it, so leave without tearing anything down */
|
||||
fprintf(stderr, "webui: the running turn did not stop in 30 s\n");
|
||||
fflush(NULL);
|
||||
_Exit(1);
|
||||
}
|
||||
if (cst_http_stop(w->srv) > 0) {
|
||||
/* a handler still runs and uses w and the agent: same as above */
|
||||
fprintf(stderr, "webui: a request did not finish in 30 s\n");
|
||||
fflush(NULL);
|
||||
_Exit(1);
|
||||
}
|
||||
cst_http_free(w->srv);
|
||||
for (int i = 0; i < WUI_RING; i++)
|
||||
free(w->ring[i].frame);
|
||||
|
||||
Reference in New Issue
Block a user