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
+25 -10
View File
@@ -1,5 +1,13 @@
#include "demo_guard.h"
// Fault-injection demo. Under the wasm runtime every unit fault is a guest
// trap that the workspace turns into a clean 500 without harming the worker;
// these modes exercise the three distinct trap causes (mirrors tests/wasm-kill).
u64 error_reporting_recurse(volatile u64 depth)
{
volatile u64 next = depth + 1;
return(next + error_reporting_recurse(next));
}
RENDER(Request& context)
{
@@ -10,12 +18,19 @@ RENDER(Request& context)
}
String mode = context.get["mode"];
if(mode == "exception")
throw std::runtime_error("Intentional test exception from /test/error-reporting.uce");
if(mode == "abort")
raise(SIGABRT);
if(mode == "segfault")
raise(SIGSEGV);
if(mode == "trap")
__builtin_trap();
if(mode == "recurse")
{
volatile u64 sink = error_reporting_recurse(0);
(void)sink;
}
if(mode == "loop")
{
volatile u64 i = 0;
while(i >= 0)
i++;
}
<>
<link rel="stylesheet" href='style.css?v=<?= time() ?>'></link>
@@ -23,11 +38,11 @@ RENDER(Request& context)
<a href="index.uce">UCE Test</a>:
Error reporting
</h1>
<p>These actions intentionally trigger failures so you can verify that UCE returns a usable `500` response instead of dropping the upstream connection.</p>
<p>These actions intentionally trigger guest traps so you can verify that UCE returns a usable `500` response from a healthy worker instead of dropping the upstream connection.</p>
<ul>
<li><a href="?mode=exception">Trigger uncaught exception</a></li>
<li><a href="?mode=abort">Trigger SIGABRT</a></li>
<li><a href="?mode=segfault">Trigger SIGSEGV</a></li>
<li><a href="?mode=trap">Trigger an explicit trap (`__builtin_trap`)</a></li>
<li><a href="?mode=recurse">Trigger stack exhaustion (unbounded recursion)</a></li>
<li><a href="?mode=loop">Trigger a runaway loop (epoch interrupt)</a></li>
</ul>
</>
}
+5 -3
View File
@@ -1,7 +1,9 @@
RENDER(Request& context)
{
auto p = compiler_load_shared_unit(&context, "post.uce");
if(p)
print(to_string(p));
// The native SharedUnit loader has been retired. Unit metadata is now
// exposed through the wasm membrane via unit_info() (uce_host_units), which
// resolves and introspects another unit without dlopen-ing a native .so.
DValue info = unit_info("post.uce");
print(json_encode(info));
}
+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.");