wasm runtime: central WS broker, unified handlers, W7d holdouts, membrane completeness

- WS: a dedicated broker process owns HTTP_PORT + every connection; it forwards
  renders to the worker pool over uce.sock (non-blocking) and applies ws_*
  command batches flushed back at workspace teardown. Removes the now-dead
  per-worker websocket executor (-509 lines).
- Dispatch: unify CLI / WebSocket / serve_http / page render through one
  serve_via_wasm(entry_unit, handler) path; handler string -> __uce_<handler>
  export symbol.
- W7d: rewrite zip.uce to the membrane return-value error contract (no C++
  try/catch), error-reporting.uce to genuine wasm traps instead of throw, and
  sharedunit.uce to unit_info(); empty the native-only token gate.
- Membrane: wire ls / mkdir / file_mtime through new uce_host_file_list /
  uce_host_file_mkdir / uce_host_file_mtime hostcalls (resolve_guest_file gains
  directory support). Fixes /doc/index.uce listing nothing; adds a regression
  assertion that the index enumerates items.
- Docs: add docs/wasm-runtime-architecture.md; record the W7e staged native-
  deletion plan in WASM-PROPOSAL.md.

Verified: scripts/run_cli_tests.sh --include-wasm-kill -> 87 passed, 0 failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
root
2026-06-14 17:51:51 +00:00
co-authored by Claude Opus 4.8
parent 15e8d092bc
commit 8587fbc5aa
14 changed files with 1219 additions and 769 deletions
+4
View File
@@ -155,6 +155,10 @@ void cli_run_demo_smoke()
void cli_run_http_smoke()
{
cli_expect_http("uce_http_smoke:doc index", "/doc/index.uce", 200, "<html>");
// Regression guard: the index enumerates pages via ls("pages/"). When that
// returned empty (wasm ls() was a stub), the page still rendered <html> but
// listed nothing — so assert the function grid is actually populated.
cli_expect_http("uce_http_smoke:doc index lists functions", "/doc/index.uce", 200, "func-item");
cli_expect_http("uce_http_smoke:doc singlepage", "/doc/singlepage.uce", 200, "<html>");
cli_expect_http("uce_http_smoke:doc component page", "/doc/index.uce?p=component", 200, "component()");
cli_expect_http("uce_http_smoke:doc regex page", "/doc/index.uce?p=regex_search", 200, "regex_search");
+14 -34
View File
@@ -49,33 +49,19 @@ RENDER(Request& context)
check("zip_read()", hello == "Hello ZIP", hello);
check("zip_extract()", extracted && nested == "Nested file" && value == "42", nested + " / " + value);
bool unsafe_rejected = false;
try
{
DValue unsafe_entries;
unsafe_entries["/absolute.txt"] = "bad";
zip_create(path_join(base, "unsafe.zip"), unsafe_entries);
}
catch(std::exception& e)
{
unsafe_rejected = contains(e.what(), "unsafe");
}
// Unsafe member names are rejected host-side; the membrane reports the
// failure as a false return (no thrown exception crosses into the unit).
DValue unsafe_entries;
unsafe_entries["/absolute.txt"] = "bad";
bool unsafe_rejected = !zip_create(path_join(base, "unsafe.zip"), unsafe_entries);
check("zip_create() rejects unsafe names", unsafe_rejected, "absolute member name rejected");
bool nul_name_rejected = false;
try
{
DValue unsafe_entries;
String nul_name = "prefix";
nul_name.push_back((char)0x00);
nul_name += "suffix.txt";
unsafe_entries[nul_name] = "bad";
zip_create(path_join(base, "nul-name.zip"), unsafe_entries);
}
catch(std::exception& e)
{
nul_name_rejected = contains(e.what(), "unsafe");
}
DValue nul_entries;
String nul_name = "prefix";
nul_name.push_back((char)0x00);
nul_name += "suffix.txt";
nul_entries[nul_name] = "bad";
bool nul_name_rejected = !zip_create(path_join(base, "nul-name.zip"), nul_entries);
check("zip_create() rejects NUL entry names", nul_name_rejected, "embedded NUL member name rejected");
String binary_source("UCE", 3);
@@ -98,15 +84,9 @@ RENDER(Request& context)
check("gz_compress()", gz_body.size() > gz_source.size() && (u8)gz_body[0] == 0x1f && (u8)gz_body[1] == 0x8b, "bytes=" + std::to_string((u64)gz_body.size()));
check("gz_uncompress()", gz_roundtrip == gz_source && gz_binary_roundtrip == binary_source && gz_binary_roundtrip.size() == binary_source.size(), "text=" + gz_roundtrip + ", binary bytes=" + std::to_string((u64)gz_binary_roundtrip.size()));
bool bad_gz_rejected = false;
try
{
gz_uncompress("not gzip data");
}
catch(std::exception& e)
{
bad_gz_rejected = contains(e.what(), "gz_uncompress");
}
// Invalid gzip input fails host-side; the membrane returns an empty result
// rather than throwing into the unit.
bool bad_gz_rejected = gz_uncompress("not gzip data") == "";
check("gz_uncompress() rejects invalid data", bad_gz_rejected, "invalid stream rejected");
site_tests_summary(passed, failed, skipped, "ZIP tests write only under /tmp/uce-site-tests-zip.");