From c5deb4083865659914bb123a624a5db739860c2a Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 19 Aug 2026 03:46:05 +0200 Subject: [PATCH 1/4] =?UTF-8?q?feat(cef):=20best-effort=20Wayland=20launch?= =?UTF-8?q?=20smoke=20(roadmap=20=C2=A744.2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Roadmap §44.2 explicitly rejects "CEF uses Chromium" as proof of Wayland/X11 correctness — the existing mandatory harness only ever runs worldscript_host under Xvfb/X11. This is a genuine feasibility attempt, grounded in real evidence rather than a guess: - Chromium's own upstream GN default (build/config/ozone.gni, the is_linux branch) compiles BOTH the x11 and wayland Ozone platforms into every standard Linux build (ozone_platform_wayland = true). - CEF's own tools/gn_args.py has zero ozone/wayland overrides — confirmed by reading it directly — so the pinned binary distribution very likely inherits that upstream default. - --ozone-platform=wayland is a real, verified Chromium switch (ui/ozone/public/ozone_switches.cc: kOzonePlatform). What's still genuinely unverified — whether the fetched "linux64 minimal" distribution carries Wayland support through in practice, and whether a headless Weston compositor is a viable launch target on a stock GitHub Actions runner — is exactly what this new CI step answers empirically, the same way the accessibility and crash-reporting proofs were resolved by real CI attempts rather than paper research alone. Deliberately additive and non-blocking: reuses the exact already-built worldscript_host binary and the already-proven X11 harness untouched; the new step is continue-on-error so a real "doesn't work yet" finding is informative, not a regression gate (this whole workflow is already advisory-only, per its own header comment). XDG_RUNTIME_DIR added to turbo.json's globalEnv — required by systemd/Wayland (0700-mode runtime dir) and read directly by the new script, per Biome's noUndeclaredEnvVars rule. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/cef-learning-harness.yml | 17 ++- scripts/cef/run-wayland-smoke.mjs | 149 +++++++++++++++++++++ turbo.json | 3 +- 3 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 scripts/cef/run-wayland-smoke.mjs diff --git a/.github/workflows/cef-learning-harness.yml b/.github/workflows/cef-learning-harness.yml index 8b432c87a..100a5c4fd 100644 --- a/.github/workflows/cef-learning-harness.yml +++ b/.github/workflows/cef-learning-harness.yml @@ -118,11 +118,26 @@ jobs: "http://localhost:8080/" --cycles 3 kill "$SERVER_PID" + - name: Best-effort Wayland launch smoke (roadmap §44.2) + id: wayland-smoke + continue-on-error: true + run: | + sudo apt-get install -y weston + python3 -m http.server 8081 --directory dist & + SERVER_PID=$! + sleep 1 + node scripts/cef/run-wayland-smoke.mjs \ + "$(pwd)/build/worldscript_host/worldscript_host" \ + "http://localhost:8081/" + kill "$SERVER_PID" + - name: Summary + if: always() run: | echo "## 🧪 CEF Learning Harness" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" echo "- Pinned SDK: \`$(node -e "console.log(require('./scripts/cef/cef-version.json').cefVersion)")\`" >> "$GITHUB_STEP_SUMMARY" echo "- Cache hit: \`${{ steps.cef-cache.outputs.cache-hit }}\`" >> "$GITHUB_STEP_SUMMARY" echo "- worldscript_host built and repeated launch/close cycles proven against the real production bundle (dist/), under Xvfb." >> "$GITHUB_STEP_SUMMARY" - echo "- Not yet in scope: X11/Wayland matrix beyond this one runner, sandbox posture, accessibility smoke, crash-reporting proof." >> "$GITHUB_STEP_SUMMARY" + echo "- Wayland smoke (best-effort, roadmap §44.2): \`${{ steps.wayland-smoke.outcome }}\`" >> "$GITHUB_STEP_SUMMARY" + echo "- Not yet in scope: X11/Wayland matrix beyond this one runner, sandbox posture, accessibility smoke." >> "$GITHUB_STEP_SUMMARY" diff --git a/scripts/cef/run-wayland-smoke.mjs b/scripts/cef/run-wayland-smoke.mjs new file mode 100644 index 000000000..cc773fb27 --- /dev/null +++ b/scripts/cef/run-wayland-smoke.mjs @@ -0,0 +1,149 @@ +#!/usr/bin/env node +/** + * Best-effort Wayland launch smoke test for the Wave 2 CEF host (roadmap §44.2: + * "'CEF uses Chromium' is not accepted as proof of Wayland/X11 correctness" — the + * mandatory run-launch-cycle-proof.mjs only ever runs worldscript_host under Xvfb/X11). + * + * This is a genuine feasibility attempt, not a guess: Chromium's own upstream GN + * default (build/config/ozone.gni, `is_linux` branch) compiles BOTH the x11 and + * wayland Ozone platforms into every standard Linux build (`ozone_platform_wayland = + * true`), and CEF's own tools/gn_args.py has no override disabling it — confirmed by + * reading both files directly, not assumed. What's still genuinely unverified before + * this script runs is whether the fetched "linux64 minimal" binary distribution + * actually carries that support through, and whether a headless Weston compositor + * (the Wayland-side equivalent of Xvfb — no real display/GPU needed) is a viable + * launch target for it on a stock GitHub Actions runner. + * + * Deliberately does NOT touch worldscript_host's build or the already-proven X11 + * proofs — this launches the exact same already-built binary, just under + * --ozone-platform=wayland against a headless Weston socket instead of Xvfb. The + * calling CI step is marked continue-on-error so a real "Wayland doesn't work with + * this CEF distribution/runner" finding is informative, not a regression gate. + * + * Run: node scripts/cef/run-wayland-smoke.mjs + */ +import { execFileSync, spawn } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; + +const [binaryPath, url] = process.argv.slice(2); + +if (!binaryPath || !url) { + console.error( + '[wayland-smoke] Usage: node scripts/cef/run-wayland-smoke.mjs ', + ); + process.exit(1); +} + +// QNBS-v3: same grace period rationale as run-launch-cycle-proof.mjs's STARTUP_GRACE_MS — CI-runner-speed variance, not a code concern this script has any control over. +const LAUNCH_GRACE_MS = 10000; +const COMPOSITOR_SOCKET_GRACE_MS = 5000; +const FFI_PROOF_LINE = 'rust_core ping = 424242'; +const EXPECTED_TITLE_LINE = 'title = WorldScript Studio'; +const WAYLAND_SOCKET_NAME = 'wayland-smoke-0'; + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +// QNBS-v3: systemd/Wayland both refuse to operate against a XDG_RUNTIME_DIR that isn't mode 0700 and owned by the current user — a real, well-documented requirement, not optional hardening. GitHub Actions runners don't set this by default (no logind session), so it's created explicitly here. +function ensureXdgRuntimeDir() { + const dir = process.env.XDG_RUNTIME_DIR || '/tmp/wayland-smoke-xdg-runtime'; + fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); + fs.chmodSync(dir, 0o700); + return dir; +} + +async function main() { + const xdgRuntimeDir = ensureXdgRuntimeDir(); + const env = { ...process.env, XDG_RUNTIME_DIR: xdgRuntimeDir }; + + console.log('[wayland-smoke] Starting headless Weston compositor…'); + // QNBS-v3: Weston's headless backend needs no real display/GPU — the Wayland-side equivalent of Xvfb, same reasoning as run-launch-cycle-proof.mjs uses xvfb-run for X11. + const weston = spawn( + 'weston', + ['--backend=headless-backend.so', `--socket=${WAYLAND_SOCKET_NAME}`], + { + env, + stdio: ['ignore', 'pipe', 'pipe'], + }, + ); + let westonStderr = ''; + weston.stderr.on('data', (chunk) => { + westonStderr += chunk.toString(); + }); + + const socketPath = path.join(xdgRuntimeDir, WAYLAND_SOCKET_NAME); + const socketDeadline = Date.now() + COMPOSITOR_SOCKET_GRACE_MS; + while (!fs.existsSync(socketPath) && Date.now() < socketDeadline) { + await sleep(200); + } + if (!fs.existsSync(socketPath)) { + console.error(`[wayland-smoke] FAIL — Weston did not create ${socketPath} in time.`); + if (westonStderr) console.error(`[wayland-smoke] Weston stderr:\n${westonStderr}`); + weston.kill('SIGKILL'); + process.exit(1); + } + console.log(`[wayland-smoke] Weston compositor socket ready: ${socketPath}`); + + console.log(`[wayland-smoke] Launching worldscript_host with --ozone-platform=wayland…`); + const child = spawn( + binaryPath, + [`--url=${url}`, '--ozone-platform=wayland', '--enable-logging=stderr', '--v=1'], + { + cwd: path.dirname(binaryPath), + stdio: ['ignore', 'pipe', 'pipe'], + env: { ...env, WAYLAND_DISPLAY: WAYLAND_SOCKET_NAME }, + }, + ); + + let stdout = ''; + let stderr = ''; + child.stdout.on('data', (chunk) => { + stdout += chunk.toString(); + }); + child.stderr.on('data', (chunk) => { + stderr += chunk.toString(); + }); + + const exited = new Promise((resolve) => + child.once('exit', (code, signal) => resolve({ code, signal })), + ); + + const rendered = await Promise.race([ + (async () => { + while (!(stdout.includes(FFI_PROOF_LINE) && stdout.includes(EXPECTED_TITLE_LINE))) { + await sleep(200); + } + return true; + })(), + exited.then(() => false), + sleep(LAUNCH_GRACE_MS).then(() => false), + ]); + + child.kill('SIGKILL'); + weston.kill('SIGKILL'); + try { + execFileSync('pkill', ['-9', '-f', WAYLAND_SOCKET_NAME], { stdio: 'ignore' }); + } catch { + // Nothing left matching — fine. + } + + if (!rendered) { + console.error( + `[wayland-smoke] FAIL — did not observe both "${FFI_PROOF_LINE}" and "${EXPECTED_TITLE_LINE}" within ${LAUNCH_GRACE_MS}ms under --ozone-platform=wayland.`, + ); + console.error(`[wayland-smoke] stdout:\n${stdout || '(empty)'}`); + console.error(`[wayland-smoke] stderr:\n${stderr || '(empty)'}`); + process.exit(1); + } + + console.log( + '[wayland-smoke] OK — worldscript_host rendered the real production bundle under a headless Weston Wayland compositor (--ozone-platform=wayland), FFI boundary and title both proven.', + ); +} + +main().catch((err) => { + console.error(`[wayland-smoke] FAIL — unexpected error: ${err.message}`); + process.exit(1); +}); diff --git a/turbo.json b/turbo.json index d96041cbf..6aad271dc 100644 --- a/turbo.json +++ b/turbo.json @@ -16,7 +16,8 @@ "RUN_DEEP_E2E", "RUN_MOBILE_E2E", "RUN_REAL_VOICE_E2E", - "SMOKE_PORT" + "SMOKE_PORT", + "XDG_RUNTIME_DIR" ], "tasks": { "build": { From 9c1f9d4ba6b0ca6df54b20ca01b34f740fb80fd9 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 19 Aug 2026 03:53:02 +0200 Subject: [PATCH 2/4] docs(cef): record real Wayland smoke evidence in competency docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #393's headless-Weston Wayland smoke passed on the first attempt — worldscript_host reached both proof lines (FFI ping + exact title) within ~1.2s under --ozone-platform=wayland. Checks Appendix A.1's "X11/Wayland initial smoke complete" item with linked evidence, adds a PASS row (single-runner-smoke caveat) to native-readiness.md, and a new "Display server" section to cef-architecture-primer.md explaining the real GN-args evidence gathered before attempting it and what the smoke does/doesn't prove (not the roadmap §44.2/§44.5 real-hardware matrix). Co-Authored-By: Claude Sonnet 5 --- docs/architecture/native-readiness.md | 5 +++-- docs/cef/CEF-RUST-COMPETENCY-MATRIX.md | 4 ++-- docs/cef/OWNERSHIP.yaml | 4 ++-- docs/cef/knowledge/cef-architecture-primer.md | 12 +++++++++++- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/docs/architecture/native-readiness.md b/docs/architecture/native-readiness.md index 47e6f3437..564b9ef37 100644 --- a/docs/architecture/native-readiness.md +++ b/docs/architecture/native-readiness.md @@ -57,7 +57,8 @@ Wave 2's first deliverable — the CEF binding/C++ decision — is now backed by | Check | Result | Owner | Notes | |---|---|---|---| | CEF binding/integration approach decided | **PASS** | cef-runtime | Option B (thin C++ CEF host + Rust core) chosen and spiked with real evidence: builds against CEF's own binary-distribution CMake macros, launches under Xvfb, survives 3 repeated start/close cycles with a clean process tree, and its core premise (a working Rust↔C++ FFI boundary) is proven in isolation. See ADR-0020. | -| CEF renders reliably on Linux dev systems | DEBT — partial | cef-runtime, Wave 2 (in progress) | Two data points now (dev-machine spike + CI-run PR #388 on GitHub Actions `ubuntu-latest`), the second of which renders the **real production bundle** (not `about:blank`) with a verified exact title, not just a blank page. Still one GPU config per machine, no sandbox, X11/Xvfb only. Exit condition (unchanged from roadmap §2's Wave 2 exit criteria): broader Linux/GPU/display-server matrix coverage (Appendix A.3) before this can flip to PASS. | +| CEF renders reliably on Linux dev systems | DEBT — partial | cef-runtime, Wave 2 (in progress) | Two data points now (dev-machine spike + CI-run PR #388 on GitHub Actions `ubuntu-latest`), the second of which renders the **real production bundle** (not `about:blank`) with a verified exact title, not just a blank page. Still one GPU config per machine, no sandbox, mainly X11/Xvfb (Wayland now also smoke-proven, see the dedicated row below). Exit condition (unchanged from roadmap §2's Wave 2 exit criteria): broader Linux/GPU/display-server matrix coverage (Appendix A.3) before this can flip to PASS. | +| Wayland display-server smoke | **PASS** — single-runner smoke only | cef-runtime, Wave 2 | PR #393: `worldscript_host` (the exact binary already proven under X11) also renders the real production bundle under a headless Weston Wayland compositor (`--ozone-platform=wayland`), same FFI-boundary + exact-title checks as the X11 harness, CI-run and non-blocking (roadmap §44.2). Grounded in real evidence before attempting: Chromium's own upstream GN default compiles Wayland Ozone support into every standard Linux build, and CEF's `tools/gn_args.py` has no override disabling it. Does **not** satisfy roadmap §44.2/§44.5's real-hardware/compositor matrix (NVIDIA/AMD/Intel × KDE/GNOME) — one virtual CI runner, one compositor implementation (Weston headless), no real GPU. | | CEF lifecycle assumptions documented | **PASS** | cef-runtime | `docs/cef/knowledge/subprocess-and-shutdown.md`'s core Wave 2 claim (SIGTERM → graceful `TryCloseBrowser`/`OnBeforeClose`/`CefQuitMessageLoop`/`CefShutdown`, repeated clean start/close cycles) now has a real linked chain: test (`scripts/cef/run-launch-cycle-proof.mjs`) → CI job (`🧪 CEF Learning Harness`) → doc, exactly what §61.1.4 requires. Save-coordinator/window-state persistence remain explicitly Wave 5+ scope (not a Wave 2 gap); Windows/macOS and a real packaged layout remain open, tracked in the doc's own "Outline" section. | | Early Accessibility Gate | Not yet attempted (real blocker found) | cef-runtime, Wave 2 | PR #391 attempted `CefAccessibilityHandler` — does not compile against CEF 151.3.18 (`CefClient::GetAccessibilityHandler()` doesn't exist in this version). Reverted rather than left half-working, after a fallback attempt (enable-only, no observability) regressed the previously-reliable FFI/rendering proofs. Real CEF-151 API research needed before the next attempt — see `docs/cef/knowledge/cef-architecture-primer.md`. | | Sandbox posture | Not yet attempted | desktop-security, Wave 2/3 (roadmap §12) | Every run so far used `no_sandbox=true`; zero evidence either way on this row. | @@ -67,4 +68,4 @@ Wave 2's first deliverable — the CEF binding/C++ decision — is now backed by | CEF host build + repeated launch/close cycle proof, in CI | **PASS** | cef-runtime | PR #388: `apps/desktop-cef/`'s `worldscript_host` (real, repo-committed C++/Rust source, not spike code) builds against the fetched CEF SDK and runs 3 independently-verified clean start/close cycles under Xvfb in CI — the roadmap's literal "isolated learning harness" / "safe repeated startup/shutdown" deliverables (§3142), not just the fetch/diagnostics increment. | | Rust FFI boundary proven inside the real host | **PASS** | cef-runtime, rust-core | `worldscript_rust_ping()` (rust-core, linked via Corrosion) is called from `OnAfterCreated` on every cycle and its exact sentinel value observed in CI output — stronger than the ADR-0020 spike's decoupled isolation test, since this proves the boundary works inside the actual multi-process CEF host, not a standalone C++ program. | -**Overall for this snapshot**: 6 PASS (one — crash reporting — explicitly PASS for its reporting half only, not symbolization), 2 explicit DEBT-in-progress rows (each with a concrete exit condition, not open-ended), 2 not-yet-attempted rows correctly left blank rather than assumed. No row is marked PASS without the evidence cited above. +**Overall for this snapshot**: 7 PASS (crash reporting explicitly PASS for its reporting half only, not symbolization; Wayland explicitly PASS for a single-runner smoke only, not the real-hardware/compositor matrix), 2 explicit DEBT-in-progress rows (each with a concrete exit condition, not open-ended), 2 not-yet-attempted rows correctly left blank rather than assumed. No row is marked PASS without the evidence cited above. diff --git a/docs/cef/CEF-RUST-COMPETENCY-MATRIX.md b/docs/cef/CEF-RUST-COMPETENCY-MATRIX.md index f918d252d..d936c0f9e 100644 --- a/docs/cef/CEF-RUST-COMPETENCY-MATRIX.md +++ b/docs/cef/CEF-RUST-COMPETENCY-MATRIX.md @@ -1,7 +1,7 @@ # CEF/Rust Competency Matrix **Companion to:** [`ROADMAP-CEF-DESKTOP-MIGRATION.md`](ROADMAP-CEF-DESKTOP-MIGRATION.md) §4.11, §61.1, Appendix A.1 · [ADR-0019](../adr/0019-cef-desktop-runtime-strategy.md) -**Established:** Wave 0, 2026-08-18. **Baseline was: nothing done yet.** Updated in place, 2026-08-18/19 (Wave 2, ADR-0020 spike + PR #386/#387/#388/#391/#392), per this doc's own "Update discipline" below — items flip to `true` only with a linked evidence commit, in the same commit as the flip. This file exists so future waves have a live, gradeable target instead of re-deriving the checklist from the roadmap prose each time. +**Established:** Wave 0, 2026-08-18. **Baseline was: nothing done yet.** Updated in place, 2026-08-18/19 (Wave 2, ADR-0020 spike + PR #386/#387/#388/#391/#392/#393), per this doc's own "Update discipline" below — items flip to `true` only with a linked evidence commit, in the same commit as the flip. This file exists so future waves have a live, gradeable target instead of re-deriving the checklist from the roadmap prose each time. This is an engineering gate (roadmap §4.11.6), not a training checklist. `WS-CEF-IPC` (Wave 4) and any production storage capability exposing privileged native operations may not proceed until the relevant items below are `true` with linked evidence. @@ -45,7 +45,7 @@ CI validation of this block ("fail CI when a required item for the active progra [ ] Accessibility smoke green (attempted, real blocker — see cef-architecture-primer.md's "Accessibility API" section) [ ] Crash-reporting/symbolization smoke green (crash-reporting half proven — PR #392, real Crashpad dump produced in CI; symbolization/decoding the dump not attempted, needs a full Chromium source checkout — see cef-architecture-primer.md) [ ] Linux dependency inventory complete (inventoried, not yet proven sufficient — see native-readiness.md) -[ ] X11/Wayland initial smoke complete (X11 only; Wayland zero evidence) +[x] X11/Wayland initial smoke complete — PR #393: X11 proven since PR #388 (Xvfb); Wayland now also proven (headless Weston compositor, --ozone-platform=wayland, same FFI+title checks, cef-learning-harness CI job). Real-hardware/compositor matrix (roadmap §44.2/§44.5 — NVIDIA/AMD/Intel × KDE/GNOME, real graphics hardware) remains unproven; this is one virtual-CI runner only. [ ] Upgrade playbook written [ ] External-expertise escalation path documented ``` diff --git a/docs/cef/OWNERSHIP.yaml b/docs/cef/OWNERSHIP.yaml index a922bdd90..269ce2e94 100644 --- a/docs/cef/OWNERSHIP.yaml +++ b/docs/cef/OWNERSHIP.yaml @@ -56,7 +56,7 @@ documents: - cef-learning-harness # cef-competency-gate: no such CI workflow/job exists yet — planned, not implemented (CodeRabbit review finding on PR #389). Re-add once it's a real job. driftCheckTool: "planned — not implemented, see Wave 1" - note: "Updated in place for Wave 2 (PR #386/#387/#388/#391/#392) — 3 of 7 cef_competency items now true with linked evidence (renderer_crash_ci added, PR #392); competency gate still not satisfied (6/12)." + note: "Updated in place for Wave 2 (PR #386/#387/#388/#391/#392/#393) — 3 of 7 cef_competency items now true with linked evidence (renderer_crash_ci added, PR #392); Wayland smoke checked in Appendix A.1 (PR #393, no dedicated cef_competency field for it per the roadmap's own §61.1.3 shape); competency gate still not satisfied (6/12)." - path: docs/cef/TAURI-COUPLING-INVENTORY.md tier: B @@ -104,7 +104,7 @@ documents: related_ci: - cef-learning-harness driftCheckTool: "planned — not implemented, see Wave 1" - note: "Real evidence from PR #388 for process model, message loop, subprocess packaging; crash reporting proven in CI (PR #392). Sandbox config, symbolization, and a directly-observed process-tree snapshot remain open." + note: "Real evidence from PR #388 for process model, message loop, subprocess packaging; crash reporting proven in CI (PR #392); Wayland display-server smoke proven in CI (PR #393). Sandbox config, symbolization, a real GPU/compositor matrix, and a directly-observed process-tree snapshot remain open." - path: docs/cef/knowledge/cef-rust-binding-cookbook.md tier: A diff --git a/docs/cef/knowledge/cef-architecture-primer.md b/docs/cef/knowledge/cef-architecture-primer.md index 4f9ae4022..91e993feb 100644 --- a/docs/cef/knowledge/cef-architecture-primer.md +++ b/docs/cef/knowledge/cef-architecture-primer.md @@ -1,6 +1,6 @@ # CEF Architecture Primer -**Status:** Real evidence from `apps/desktop-cef/` (PR #388) for process model, message loop, and subprocess packaging; crash reporting and renderer-crash resilience proven in CI (PR #392). Sandbox configuration, dump symbolization, and a directly-observed full process-tree snapshot remain open. +**Status:** Real evidence from `apps/desktop-cef/` (PR #388) for process model, message loop, and subprocess packaging; crash reporting and renderer-crash resilience proven in CI (PR #392); Wayland display-server smoke also proven in CI (PR #393), alongside X11. Sandbox configuration, dump symbolization, a real GPU/compositor matrix, and a directly-observed full process-tree snapshot remain open. **Scope:** How CEF's multi-process architecture (browser process, renderer process, GPU/utility processes; browser/frame/client ownership; message-loop integration; subprocess launch and packaging; sandbox model) maps onto WorldScript Studio's specific host and build, written from our actual integration — not a generic CEF tutorial. **Tier:** A (release/security-critical) — see [`../OWNERSHIP.yaml`](../OWNERSHIP.yaml). **Roadmap context:** [`../ROADMAP-CEF-DESKTOP-MIGRATION.md`](../ROADMAP-CEF-DESKTOP-MIGRATION.md) §4.11.1 ("CEF architecture" domain), §4.11.2, Wave 2. @@ -49,6 +49,16 @@ Unlike the accessibility attempt above, every mechanism here was verified agains **What this does NOT prove**: symbolization — decoding the `.dmp` file into a human-readable stack trace — needs `dump_syms` and `minidump_stackwalk`, which CEF's own docs say must be built from a *complete Chromium source checkout* (`gn`/`ninja`, hours of build time, tens of GB of disk). That is out of reach of this project's minimal-CEF-SDK-only CI setup (and of the local dev machine's own constrained RAM/disk, per this repo's own low-end-hardware guidance) and was not attempted. `crash_symbolization_smoke` in `docs/cef/CEF-RUST-COMPETENCY-MATRIX.md` stays `false` for that reason — the crash-*reporting* half is proven; symbolization is a separate, still-open item. +## Display server — X11 proven since PR #388, Wayland now also proven + +Roadmap §44.2 is explicit: *"'CEF uses Chromium' is not accepted as proof of Wayland/X11 correctness."* Until PR #393 this host had only ever been exercised under X11 (`xvfb-run`). + +**Real evidence gathered before attempting anything**, matching the discipline the accessibility attempt's own "what this means for the next attempt" note called for: Chromium's own upstream GN default (`build/config/ozone.gni`, the `is_linux` branch) compiles **both** the `x11` and `wayland` Ozone platforms into every standard Linux build (`ozone_platform_wayland = true`), and CEF's own `tools/gn_args.py` has zero ozone/wayland overrides — confirmed by reading both files directly. `--ozone-platform=wayland` is a real, verified Chromium switch (`ui/ozone/public/ozone_switches.cc`'s `kOzonePlatform`). + +**Directly observed evidence, PR #393**: `scripts/cef/run-wayland-smoke.mjs` launches a headless Weston compositor (`weston --backend=headless-backend.so` — the Wayland-side equivalent of Xvfb, no real display/GPU needed) and the *exact same already-built* `worldscript_host` binary under `--ozone-platform=wayland`, `WAYLAND_DISPLAY` pointed at Weston's socket. The CI log shows the compositor socket created, then `worldscript_host` reaching both `rust_core ping = 424242` and `title = WorldScript Studio` within about 1.2 seconds — the same two proofs the X11 harness uses, now also true under Wayland, on a stock GitHub Actions runner, first attempt. + +**What this does NOT prove**: roadmap §44.2/§44.5's real matrix — NVIDIA/AMD/Intel GPUs × KDE/GNOME compositors × real graphics hardware. This is one virtual CI runner, one compositor implementation (Weston, headless, no GPU), non-blocking (`continue-on-error`) in CI. It answers "does the fetched CEF binary distribution and this host even support Wayland at all" (yes), not "does WorldScript Studio work correctly under every real-world Wayland desktop" (unproven). + ## Sandbox configuration, as shipped `chrome-sandbox` is present in the output directory (copied automatically as part of `CEF_BINARY_FILES`) but is **not used** — `main.cpp` sets `CefSettings.no_sandbox = true` unconditionally. Zero evidence exists on real sandbox posture; this is explicitly tracked as "Not yet attempted" in `docs/architecture/native-readiness.md` and `false` in the competency manifest. From 371b30e759bd060feeb166007a61914504166370 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 19 Aug 2026 04:10:41 +0200 Subject: [PATCH 3/4] =?UTF-8?q?fix(cef):=20harden=20Wayland=20smoke=20?= =?UTF-8?q?=E2=80=94=20CodeAnt/CodeRabbit=20review=20findings=20on=20PR=20?= =?UTF-8?q?#393?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3 real findings, all fixed: - Cleanup only killed the tracked child PID and pkill'd by WAYLAND_SOCKET_NAME — but CEF re-execs the same binary for renderer/GPU/crashpad-handler subprocesses, none of which carry that socket name in their own argv (it's passed via env var). Now sweeps every process matching ^binaryPath, same pattern run-launch-cycle-proof.mjs already uses for the identical problem. - ensureXdgRuntimeDir() chmod'd a fixed, predictable /tmp path, and would chmod an already-set XDG_RUNTIME_DIR it doesn't own (real on GitHub Actions: the runner already provides /run/user/). Now trusts an already-set, existing XDG_RUNTIME_DIR as-is, and only creates (and owns) a fresh one via mkdtempSync as a fallback. - Neither spawned process (weston, worldscript_host) had an 'error' listener — spawn() reports a missing/non-executable program via an async error event, not a thrown exception, so a real "binary not found" case (exactly the kind of failure this probe exists to report) would have crashed with a raw ENOENT instead of this script's own FAIL diagnostic. Co-Authored-By: Claude Sonnet 5 --- scripts/cef/run-wayland-smoke.mjs | 36 +++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/scripts/cef/run-wayland-smoke.mjs b/scripts/cef/run-wayland-smoke.mjs index cc773fb27..8cafbc6fb 100644 --- a/scripts/cef/run-wayland-smoke.mjs +++ b/scripts/cef/run-wayland-smoke.mjs @@ -24,6 +24,7 @@ */ import { execFileSync, spawn } from 'node:child_process'; import fs from 'node:fs'; +import os from 'node:os'; import path from 'node:path'; const [binaryPath, url] = process.argv.slice(2); @@ -46,10 +47,13 @@ function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } -// QNBS-v3: systemd/Wayland both refuse to operate against a XDG_RUNTIME_DIR that isn't mode 0700 and owned by the current user — a real, well-documented requirement, not optional hardening. GitHub Actions runners don't set this by default (no logind session), so it's created explicitly here. +// QNBS-v3: trusts an already-set XDG_RUNTIME_DIR as-is (GitHub Actions runners provide a real one, e.g. /run/user/) rather than chmod'ing a directory this script doesn't own — CodeRabbit review finding on PR #393. Only falls back to creating (and owning) its own via mkdtempSync, never a fixed predictable /tmp path. function ensureXdgRuntimeDir() { - const dir = process.env.XDG_RUNTIME_DIR || '/tmp/wayland-smoke-xdg-runtime'; - fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); + const existing = process.env.XDG_RUNTIME_DIR; + if (existing && fs.existsSync(existing)) { + return existing; + } + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'wayland-smoke-xdg-runtime-')); fs.chmodSync(dir, 0o700); return dir; } @@ -69,6 +73,10 @@ async function main() { }, ); let westonStderr = ''; + // QNBS-v3: spawn() reports a missing/non-executable program via an async 'error' event, not a thrown exception — without a listener Node crashes with a raw ENOENT stack trace instead of this script's own FAIL diagnostic, exactly the failure mode this probe exists to report cleanly — CodeRabbit review finding on PR #393. + weston.on('error', (err) => { + westonStderr += `spawn error: ${err.message}\n`; + }); weston.stderr.on('data', (chunk) => { westonStderr += chunk.toString(); }); @@ -99,6 +107,10 @@ async function main() { let stdout = ''; let stderr = ''; + // QNBS-v3: same rationale as weston's 'error' listener above — a missing/non-executable binaryPath must surface through this script's own FAIL diagnostic, not an unhandled ENOENT crash. + child.on('error', (err) => { + stderr += `spawn error: ${err.message}\n`; + }); child.stdout.on('data', (chunk) => { stdout += chunk.toString(); }); @@ -121,12 +133,24 @@ async function main() { sleep(LAUNCH_GRACE_MS).then(() => false), ]); - child.kill('SIGKILL'); weston.kill('SIGKILL'); + // QNBS-v3: sweeps every process matching the binary path, not just the one tracked child PID — CodeAnt review finding on PR #393 (CEF re-execs the same binary for renderer/GPU/crashpad-handler subprocesses; none of them carry WAYLAND_SOCKET_NAME in their own command line since it's passed via env var, not argv, so the previous pkill -f pattern never matched them). Same pattern as run-launch-cycle-proof.mjs's killAllMatchingProcesses(). try { - execFileSync('pkill', ['-9', '-f', WAYLAND_SOCKET_NAME], { stdio: 'ignore' }); + const out = execFileSync('pgrep', ['-f', `^${binaryPath}`], { + stdio: ['ignore', 'pipe', 'ignore'], + }) + .toString() + .trim(); + for (const pid of out.split('\n').filter(Boolean).map(Number)) { + if (pid === process.pid) continue; + try { + process.kill(pid, 'SIGKILL'); + } catch { + // Already gone — fine. + } + } } catch { - // Nothing left matching — fine. + // pgrep exits 1 when nothing matches — nothing to kill. } if (!rendered) { From a5b2e388f2c336a02c31a92e3f96164707694b3c Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 19 Aug 2026 04:36:40 +0200 Subject: [PATCH 4/4] =?UTF-8?q?fix(cef):=20stale-socket=20race=20+=20one-s?= =?UTF-8?q?hot=20cleanup=20gap=20=E2=80=94=20CodeAnt=20findings=20on=20PR?= =?UTF-8?q?=20#393?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2 more real findings on the Wayland smoke script: - A stale socket left by a previous crashed/killed run would satisfy the existsSync readiness check immediately without this Weston instance ever actually starting, and worldscript_host could then connect to a dead/unrelated compositor. Now unlinks any pre-existing socket before spawning Weston, and also bails out early if Weston itself exits before creating it (rather than polling the full grace period for a socket a dead process will never create). - Cleanup took a single pgrep snapshot right after SIGKILL — a CEF subprocess created in the gap, or one still mid-exit, would survive undetected. sweepMatchingProcesses() is now called twice: once immediately, then again after a short grace period, with a warning logged if anything still matches on the second pass. Co-Authored-By: Claude Sonnet 5 --- scripts/cef/run-wayland-smoke.mjs | 71 ++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/scripts/cef/run-wayland-smoke.mjs b/scripts/cef/run-wayland-smoke.mjs index 8cafbc6fb..6a8bcbb8e 100644 --- a/scripts/cef/run-wayland-smoke.mjs +++ b/scripts/cef/run-wayland-smoke.mjs @@ -39,6 +39,8 @@ if (!binaryPath || !url) { // QNBS-v3: same grace period rationale as run-launch-cycle-proof.mjs's STARTUP_GRACE_MS — CI-runner-speed variance, not a code concern this script has any control over. const LAUNCH_GRACE_MS = 10000; const COMPOSITOR_SOCKET_GRACE_MS = 5000; +// QNBS-v3: a process created in the gap between the first pgrep snapshot and its SIGKILL (or one still exiting) wouldn't be caught by a single one-shot sweep — CodeAnt review finding on PR #393. This is how long the second, verifying pass waits before re-sweeping. +const CLEANUP_RECHECK_GRACE_MS = 2000; const FFI_PROOF_LINE = 'rust_core ping = 424242'; const EXPECTED_TITLE_LINE = 'title = WorldScript Studio'; const WAYLAND_SOCKET_NAME = 'wayland-smoke-0'; @@ -47,6 +49,29 @@ function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } +// QNBS-v3: shared by the two cleanup passes below — CEF re-execs the same binary for renderer/GPU/crashpad-handler subprocesses, none of which carry WAYLAND_SOCKET_NAME in their own command line (it's passed via env var, not argv), so a name-based pkill never matched them — CodeAnt review finding on PR #393. Same pattern run-launch-cycle-proof.mjs uses for the identical problem. +function sweepMatchingProcesses() { + try { + const out = execFileSync('pgrep', ['-f', `^${binaryPath}`], { + stdio: ['ignore', 'pipe', 'ignore'], + }) + .toString() + .trim(); + const pids = out.split('\n').filter(Boolean).map(Number); + for (const pid of pids) { + if (pid === process.pid) continue; + try { + process.kill(pid, 'SIGKILL'); + } catch { + // Already gone — fine. + } + } + return pids.filter((pid) => pid !== process.pid).length > 0; + } catch { + return false; // pgrep exits 1 when nothing matches — nothing to kill. + } +} + // QNBS-v3: trusts an already-set XDG_RUNTIME_DIR as-is (GitHub Actions runners provide a real one, e.g. /run/user/) rather than chmod'ing a directory this script doesn't own — CodeRabbit review finding on PR #393. Only falls back to creating (and owning) its own via mkdtempSync, never a fixed predictable /tmp path. function ensureXdgRuntimeDir() { const existing = process.env.XDG_RUNTIME_DIR; @@ -62,6 +87,14 @@ async function main() { const xdgRuntimeDir = ensureXdgRuntimeDir(); const env = { ...process.env, XDG_RUNTIME_DIR: xdgRuntimeDir }; + // QNBS-v3: a stale socket left by a previous crashed/killed run would otherwise satisfy the existsSync check below immediately, without this Weston instance ever actually starting — CodeAnt review finding on PR #393. Removing it first means "socket exists" can only mean "this Weston instance created it". + const socketPath = path.join(xdgRuntimeDir, WAYLAND_SOCKET_NAME); + try { + fs.unlinkSync(socketPath); + } catch { + // Nothing there — the expected common case. + } + console.log('[wayland-smoke] Starting headless Weston compositor…'); // QNBS-v3: Weston's headless backend needs no real display/GPU — the Wayland-side equivalent of Xvfb, same reasoning as run-launch-cycle-proof.mjs uses xvfb-run for X11. const weston = spawn( @@ -73,6 +106,10 @@ async function main() { }, ); let westonStderr = ''; + let westonExited = false; + weston.once('exit', () => { + westonExited = true; + }); // QNBS-v3: spawn() reports a missing/non-executable program via an async 'error' event, not a thrown exception — without a listener Node crashes with a raw ENOENT stack trace instead of this script's own FAIL diagnostic, exactly the failure mode this probe exists to report cleanly — CodeRabbit review finding on PR #393. weston.on('error', (err) => { westonStderr += `spawn error: ${err.message}\n`; @@ -81,13 +118,15 @@ async function main() { westonStderr += chunk.toString(); }); - const socketPath = path.join(xdgRuntimeDir, WAYLAND_SOCKET_NAME); const socketDeadline = Date.now() + COMPOSITOR_SOCKET_GRACE_MS; - while (!fs.existsSync(socketPath) && Date.now() < socketDeadline) { + // QNBS-v3: also bails out early if Weston itself already exited — otherwise this loop would keep polling for a socket a dead process will never create, wasting the whole grace period on a doomed wait — CodeAnt review finding on PR #393 (the same "verify the process is actually alive" half of the finding). + while (!fs.existsSync(socketPath) && !westonExited && Date.now() < socketDeadline) { await sleep(200); } - if (!fs.existsSync(socketPath)) { - console.error(`[wayland-smoke] FAIL — Weston did not create ${socketPath} in time.`); + if (westonExited || !fs.existsSync(socketPath)) { + console.error( + `[wayland-smoke] FAIL — Weston ${westonExited ? 'exited before creating' : 'did not create'} ${socketPath} in time.`, + ); if (westonStderr) console.error(`[wayland-smoke] Weston stderr:\n${westonStderr}`); weston.kill('SIGKILL'); process.exit(1); @@ -134,23 +173,13 @@ async function main() { ]); weston.kill('SIGKILL'); - // QNBS-v3: sweeps every process matching the binary path, not just the one tracked child PID — CodeAnt review finding on PR #393 (CEF re-execs the same binary for renderer/GPU/crashpad-handler subprocesses; none of them carry WAYLAND_SOCKET_NAME in their own command line since it's passed via env var, not argv, so the previous pkill -f pattern never matched them). Same pattern as run-launch-cycle-proof.mjs's killAllMatchingProcesses(). - try { - const out = execFileSync('pgrep', ['-f', `^${binaryPath}`], { - stdio: ['ignore', 'pipe', 'ignore'], - }) - .toString() - .trim(); - for (const pid of out.split('\n').filter(Boolean).map(Number)) { - if (pid === process.pid) continue; - try { - process.kill(pid, 'SIGKILL'); - } catch { - // Already gone — fine. - } - } - } catch { - // pgrep exits 1 when nothing matches — nothing to kill. + sweepMatchingProcesses(); + // QNBS-v3: verify-and-resweep, not a single one-shot pass — CodeAnt review finding on PR #393 (a subprocess created between the first pgrep snapshot and its kill, or one still in the middle of exiting, would otherwise survive undetected). + await sleep(CLEANUP_RECHECK_GRACE_MS); + if (sweepMatchingProcesses()) { + console.error( + '[wayland-smoke] WARNING — worldscript_host process(es) still matched after cleanup; sent a second SIGKILL sweep.', + ); } if (!rendered) {