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:
WHC
2026-10-09 13:51:34 +08:00
parent adf3e9f024
commit e238f5f37d
15 changed files with 1001 additions and 144 deletions
+26 -2
View File
@@ -47,6 +47,19 @@ BASE="http://127.0.0.1:$PORT"
echo " port $PORT"
fail() { echo "FAIL: $1"; echo "--- events"; cat "$EVENTS" 2>/dev/null | head -40; exit 1; }
# SIGTERM must shut the server down cleanly, even mid-turn (128+ = a crash)
stop_server() {
kill "$1" 2>/dev/null || return 0
n=0
while kill -0 "$1" 2>/dev/null && [ $n -lt 200 ]; do n=$((n + 1)); sleep 0.2; done
if kill -0 "$1" 2>/dev/null; then
kill -9 "$1" 2>/dev/null || true
fail "$2 hung on SIGTERM"
fi
st=0
wait "$1" 2>/dev/null || st=$?
[ "$st" -lt 128 ] || fail "$2 died with status $st on SIGTERM"
}
# 1. the shell page and the state endpoint
curl -fsS --max-time 5 "$BASE/" | grep -q "<title>CST-Agent" || fail "GET / is not the shell page"
@@ -160,7 +173,7 @@ fi
# 7. the interactive approval path: a fresh server in ask mode must raise a
# permission event, and answering it must let the turn continue.
echo "== permission round trip (-A ask)"
kill "$SRV_PID" 2>/dev/null || true # the first server's replay is spent
stop_server "$SRV_PID" "the first server" # its replay is spent
rm -rf "$WORK/pw"; mkdir -p "$WORK/pw"
P_EVENTS="$WORK/permission-events.txt"
P_LOG="$WORK/permission-server.log" # its own log: the port parse must not
@@ -206,7 +219,7 @@ else
i=$((i + 1)); sleep 0.2
done
grep -q "event: permission_done" "$P_EVENTS" || fail "the answer was not acknowledged"
kill "$P_PID" 2>/dev/null || true
stop_server "$P_PID" "the ask-mode server"
kill "$P_CURL" 2>/dev/null || true
echo " answered request $PID_REQ with 'allow once'"
fi
@@ -260,4 +273,15 @@ done
kill -0 "$T_PID" 2> /dev/null && fail "the server ignored /api/quit"
wait "$T_PID" 2>/dev/null || true
# 9. ask_user waits for the answer, and the HTTP hardening checks (both
# need a scripted model: python3)
if command -v python3 > /dev/null; then
echo "== ask_user round trip"
CST_AGENT="$BIN" python3 "$ROOT/scripts/e2e_webui_ask.py" > "$WORK/ask.txt" 2>&1 ||
{ cat "$WORK/ask.txt"; fail "the ask_user round trip"; }
echo "== HTTP hardening"
CST_AGENT="$BIN" python3 "$ROOT/scripts/e2e_webui_http.py" > "$WORK/http.txt" 2>&1 ||
{ cat "$WORK/http.txt"; fail "the HTTP hardening checks"; }
fi
echo "== OK: (events: $(grep -c '^event: ' "$EVENTS"))"