diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index cbc95f5..117cfad 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -14,7 +14,7 @@ // ========================================================================= var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); - var SX_VERSION = "2026-03-15T14:09:57Z"; + var SX_VERSION = "2026-03-15T14:20:13Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -1835,8 +1835,10 @@ PRIMITIVES["step-continue"] = stepContinue; var contData = continuationData(f); return (function() { var captured = get(contData, "captured"); - var restK = get(contData, "rest-kont"); - return makeCekValue(arg, env, concat(captured, restK)); + return (function() { + var result = cekRun(makeCekValue(arg, env, captured)); + return makeCekValue(result, env, kont); +})(); })(); })() : (isSxTruthy((isSxTruthy(isCallable(f)) && isSxTruthy(!isSxTruthy(isLambda(f))) && isSxTruthy(!isSxTruthy(isComponent(f))) && !isSxTruthy(isIsland(f)))) ? makeCekValue(apply(f, args), env, kont) : (isSxTruthy(isLambda(f)) ? (function() { var params = lambdaParams(f); diff --git a/spec/evaluator.sx b/spec/evaluator.sx index 3c2ad89..3db8d8b 100644 --- a/spec/evaluator.sx +++ b/spec/evaluator.sx @@ -2101,13 +2101,17 @@ (define continue-with-call (fn (f args env raw-args kont) (cond - ;; Continuation — restore captured frames and inject value + ;; Continuation — run captured delimited continuation, return result to caller. + ;; Multi-shot: each invocation runs captured frames to completion via nested + ;; cek-run, then returns the result to the caller's kont. (continuation? f) (let ((arg (if (empty? args) nil (first args))) (cont-data (continuation-data f))) - (let ((captured (get cont-data "captured")) - (rest-k (get cont-data "rest-kont"))) - (make-cek-value arg env (concat captured rest-k)))) + (let ((captured (get cont-data "captured"))) + ;; Run ONLY the captured frames (delimited by reset). + ;; Empty kont after captured = the continuation terminates and returns. + (let ((result (cek-run (make-cek-value arg env captured)))) + (make-cek-value result env kont)))) ;; Native callable (and (callable? f) (not (lambda? f)) (not (component? f)) (not (island? f))) diff --git a/sx/sx/plans/mother-language.sx b/sx/sx/plans/mother-language.sx index 0704531..961a8fc 100644 --- a/sx/sx/plans/mother-language.sx +++ b/sx/sx/plans/mother-language.sx @@ -366,6 +366,131 @@ "The only interpreter in the system is the CPU.")) + ;; ----------------------------------------------------------------------- + ;; Security model + ;; ----------------------------------------------------------------------- + + (~docs/section :title "Security Model" :id "security" + + (p "Compiled SX running as WASM is " (em "more secure") " than plain JavaScript, " + "not less. JS has ambient access to the full browser API. " + "WASM + the platform layer means compiled SX code has " + (strong "zero ambient capabilities") " \u2014 every capability is explicitly granted.") + + (h4 :class "font-semibold mt-4 mb-2" "Five defence layers") + + (div :class "overflow-x-auto rounded border border-stone-200 mb-4" + (table :class "w-full text-left text-sm" + (thead (tr :class "border-b border-stone-200 bg-stone-100" + (th :class "px-3 py-2 font-medium text-stone-600" "Layer") + (th :class "px-3 py-2 font-medium text-stone-600" "Enforced by") + (th :class "px-3 py-2 font-medium text-stone-600" "What it prevents"))) + (tbody + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 text-stone-700" "1. WASM sandbox") + (td :class "px-3 py-2 text-stone-700" "Browser") + (td :class "px-3 py-2 text-stone-600" "Memory isolation, no system calls, no DOM access except via explicit imports. Validated before execution.")) + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 text-stone-700" "2. Platform capabilities") + (td :class "px-3 py-2 text-stone-700" (code "sx-platform.js")) + (td :class "px-3 py-2 text-stone-600" "Compiled code can only call functions you register. No fetch? Can't fetch. No localStorage? Can't read storage. The platform is a capability system.")) + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 text-stone-700" "3. Content-addressed verification") + (td :class "px-3 py-2 text-stone-700" "CID determinism") + (td :class "px-3 py-2 text-stone-600" "Compiler is deterministic: same source \u2192 same CID. Client can re-compile and verify. Tampered WASM produces wrong CID \u2192 reject.")) + (tr :class "border-b border-stone-100" + (td :class "px-3 py-2 text-stone-700" "4. Per-component attenuation") + (td :class "px-3 py-2 text-stone-700" "Platform scoping") + (td :class "px-3 py-2 text-stone-600" "Different components get different capability subsets. User-generated content gets a locked-down platform \u2014 can render DOM but can't fetch or listen to events.")) + (tr + (td :class "px-3 py-2 text-stone-700" "5. Source-first fallback") + (td :class "px-3 py-2 text-stone-700" "Client compiler") + (td :class "px-3 py-2 text-stone-600" "Don't trust precompiled WASM? Compile from source locally. The client has the compiler. Precompilation is an optimisation, not a trust requirement."))))) + + (h4 :class "font-semibold mt-4 mb-2" "Content-addressed tamper detection") + (p "The server sends both SX source and precompiled WASM CID. The client can verify:") + + (~docs/code :code (highlight ";; Server sends:\nContent-Type: application/wasm\nX-Sx-Source-Cid: bafyrei..source\nX-Sx-Compiled-Cid: bafyrei..compiled\n\n;; Client verifies (optional, configurable):\n1. Hash the WASM binary \u2192 matches X-Sx-Compiled-Cid?\n2. Compile source locally \u2192 produces same compiled CID?\n3. Check manifest of pinned CIDs \u2192 CID is expected?\n\n;; Any mismatch = tampered = reject" "text")) + + (h4 :class "font-semibold mt-4 mb-2" "Capability attenuation per component") + (p "The platform scopes capabilities per evaluator instance. " + "App shell gets full access. Third-party or user-generated content gets the minimum:") + + (~docs/code :code (highlight "// Full capabilities for the app shell\nplatform.registerAll(appShellCompiler);\n\n// Restricted for user-generated content\nplatform.registerSubset(userContentCompiler, {\n allow: [\"dom-create-element\", \"dom-set-attr\", \"dom-append\",\n \"dom-create-text-node\", \"dom-set-text\"],\n deny: [\"fetch\", \"localStorage\", \"dom-listen\",\n \"dom-set-inner-html\", \"eval\"]\n});\n\n// The restricted compiler's WASM module literally doesn't\n// have imports for the denied functions. Not just blocked\n// at runtime \u2014 absent from the binary." "javascript")) + + (h4 :class "font-semibold mt-4 mb-2" "Component manifests") + (p "The app ships with a manifest of expected CIDs for its core components. " + "Like subresource integrity (SRI) but for compiled code:") + + (~docs/code :code (highlight ";; Component manifest (shipped with the app, signed)\n{\n \"~card\": \"bafyrei..abc\"\n \"~header\": \"bafyrei..def\"\n \"~nav-item\": \"bafyrei..ghi\"\n}\n\n;; On navigation: server sends component update\n;; Client compiles \u2192 checks CID against manifest\n;; Match = trusted, execute\n;; Mismatch = tampered, reject and report" "text")) + + (p "The security model is " (em "structural") ", not bolt-on. " + "WASM isolation, platform capabilities, content-addressed verification, " + "and per-component attenuation all arise naturally from the architecture. " + "The platform layer that enables Rust/OCaml interop is the same layer " + "that enforces security boundaries.")) + + + ;; ----------------------------------------------------------------------- + ;; Isomorphic rendering + SEO + ;; ----------------------------------------------------------------------- + + (~docs/section :title "Isomorphic Rendering" :id "isomorphic" + + (p "WASM is invisible to search engines. But SX is already isomorphic \u2014 " + "the same spec, the same components, rendered to HTML on the server " + "and to DOM on the client. Compiled WASM doesn't change this. " + "It makes the client side faster without affecting what crawlers see.") + + (h4 :class "font-semibold mt-4 mb-2" "The rendering pipeline") + + (~docs/code :code (highlight "Crawler visits:\n GET /page\n \u2192 Server compiles SX (native OCaml)\n \u2192 render-to-html (adapter-html.sx)\n \u2192 Full static HTML with semantic markup\n \u2192 Google indexes it\n\nUser first visit:\n GET /page\n \u2192 Server renders HTML (same as crawler)\n \u2192 Browser displays immediately (no JS needed)\n \u2192 Client loads sx-compiler.wasm + sx-platform.js\n \u2192 Hydrates: attaches event handlers, activates islands\n \u2192 Page is interactive\n\nUser navigates (SPA):\n sx-get /next-page\n \u2192 Server sends SX wire format (aser)\n \u2192 Client compiles + renders via WASM\n \u2192 Morph engine patches the DOM" "text")) + + (p "The server and client have the " (em "same compiler") " from the " (em "same spec") ". " + (code "adapter-html.sx") " produces HTML strings. " + (code "adapter-dom.sx") " produces DOM nodes. " + "Two rendering modes of one evaluator. The compiled WASM version " + "makes hydration and SPA navigation faster, but the initial HTML " + "is always server-rendered.") + + (h4 :class "font-semibold mt-4 mb-2" "What crawlers see") + (ul :class "list-disc list-inside space-y-1 mt-2" + (li "Fully rendered HTML \u2014 no \"loading...\" skeleton, no JS-dependent content") + (li "Semantic markup \u2014 " (code "