Fix pipe desync: send_ok_raw escapes newlines, expand-components? in env

- send_ok_raw: when SX wire format contains newlines (string literals),
  fall back to (ok "...escaped...") instead of (ok-raw ...) to keep
  the pipe single-line. Prevents multi-line responses from desyncing
  subsequent requests.
- expand-components? flag set in kernel env (not just VM adapter globals)
  so aser-list's env-has? check finds it during component expansion.
- SX_STANDALONE: restore no_oauth but generate CSRF via session cookie
  so mutation handlers (DELETE etc.) still work without account service.
- Shell statics injection: only inject small values (hashes, URLs) as
  kernel vars. Large blobs (CSS, component_defs) use placeholder tokens.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-20 12:32:03 +00:00
parent ae0e87fbf8
commit 373a4f0134
3 changed files with 43 additions and 23 deletions

View File

@@ -170,23 +170,20 @@ class OcamlBridge:
static = _get_shell_static()
except Exception:
return # not ready yet (no app context)
# Define small values as kernel variables.
# Large blobs (component_defs, pages_sx, init_sx) use placeholders
# at render time — NOT injected here.
for key in ("sx_css", "component_hash", "sx_css_classes", "asset_url",
# Only inject small, safe values as kernel variables.
# Large/complex blobs use placeholder tokens at render time.
for key in ("component_hash", "sx_css_classes", "asset_url",
"sx_js_hash", "body_js_hash"):
val = static.get(key, "")
if val is None:
val = ""
val = static.get(key) or ""
var = f"__shell-{key.replace('_', '-')}"
defn = f'(define {var} "{_escape(str(val))}")'
try:
await self._send(f'(load-source "{_escape(defn)}")')
await self._read_until_ok(ctx=None)
except OcamlBridgeError:
pass
# Also inject list/nil values
for key in ("head_scripts", "inline_css", "inline_head_js", "body_scripts"):
except OcamlBridgeError as e:
_logger.warning("Shell static inject failed for %s: %s", key, e)
# List/nil values
for key in ("head_scripts", "body_scripts"):
val = static.get(key)
var = f"__shell-{key.replace('_', '-')}"
if val is None:
@@ -199,8 +196,8 @@ class OcamlBridge:
try:
await self._send(f'(load-source "{_escape(defn)}")')
await self._read_until_ok(ctx=None)
except OcamlBridgeError:
pass
except OcamlBridgeError as e:
_logger.warning("Shell static inject failed for %s: %s", key, e)
self._shell_statics_injected = True
_logger.info("Injected shell statics into OCaml kernel")
@@ -220,14 +217,16 @@ class OcamlBridge:
async with self._lock:
await self._inject_helpers_locked()
await self._inject_shell_statics_locked()
# Large blobs (component_defs, pages_sx, init_sx) use placeholders.
# OCaml renders the shell with short tokens; Python splices in
# the real values. This avoids piping ~1MB through stdin/stdout.
PLACEHOLDER_KEYS = {"component_defs", "pages_sx", "init_sx"}
# Large/complex blobs use placeholders — OCaml renders the shell
# with short tokens; Python splices in the real values post-render.
# This avoids piping large strings or strings with special chars
# through the SX parser.
PLACEHOLDER_KEYS = {"component_defs", "pages_sx", "init_sx",
"sx_css", "inline_css", "inline_head_js"}
placeholders = {}
static_keys = {"component_hash", "sx_css_classes", "asset_url",
"sx_js_hash", "body_js_hash", "sx_css",
"head_scripts", "inline_css", "inline_head_js", "body_scripts"}
"sx_js_hash", "body_js_hash",
"head_scripts", "body_scripts"}
parts = [f'(sx-page-full "{_escape(page_source)}"']
for key, val in shell_kwargs.items():
k = key.replace("_", "-")