diff --git a/.github/workflows/rust-check.yml b/.github/workflows/rust-check.yml index 62c62e28f..22e91fc4f 100644 --- a/.github/workflows/rust-check.yml +++ b/.github/workflows/rust-check.yml @@ -10,6 +10,7 @@ on: - 'rustfmt.toml' - 'deny.toml' - 'tools/check-jian-boundaries.sh' + - 'tools/check-web-server-headless.sh' - 'tools/check-widget-boundary.sh' - 'tools/dump-diagnostics-golden.ts' - 'packages/pen-ai-skills/**' @@ -27,6 +28,7 @@ on: - 'rust-toolchain.toml' - 'rustfmt.toml' - 'tools/check-jian-boundaries.sh' + - 'tools/check-web-server-headless.sh' - 'tools/check-widget-boundary.sh' - 'tools/dump-diagnostics-golden.ts' - 'packages/pen-ai-skills/**' @@ -92,6 +94,10 @@ jobs: if: runner.os == 'Linux' run: bash tools/check-widget-boundary.sh + - name: Verify op-host-web-server headless boundary (no winit/glutin/GL) + if: runner.os == 'Linux' + run: bash tools/check-web-server-headless.sh + - name: Install Bun (retry on transient infra flake) if: runner.os == 'Linux' run: | diff --git a/.github/workflows/wasm-bundle-build.yml b/.github/workflows/wasm-bundle-build.yml index bcc2646a0..2cf0f63a5 100644 --- a/.github/workflows/wasm-bundle-build.yml +++ b/.github/workflows/wasm-bundle-build.yml @@ -113,7 +113,7 @@ jobs: # wasm-bindgen `pkg/` output PLUS the vendored CanvasKit artifact under a # `canvaskit/` subdir, all inside a single `web-bundle/` directory. This # mirrors the `/web-bundle` + `web-bundle/canvaskit` resolution - # order in `crates/op-host-desktop/src/web_static.rs`. + # order in `crates/op-web-daemon/src/web_static.rs`. - name: Assemble deployable web-bundle/ run: | set -euo pipefail diff --git a/Dockerfile.web-rust b/Dockerfile.web-rust index a7a8e80e3..74846eb9c 100644 --- a/Dockerfile.web-rust +++ b/Dockerfile.web-rust @@ -47,18 +47,16 @@ ARG WEB_BUNDLE_SOURCE=build ARG SERVE_PORT=3100 # binaryen -> wasm-opt -Oz ; nodejs -> the 0-env-import assert in the gate -# script ; the rest are op-host-desktop's Linux link-time deps (winit/skia GL -# stack) so the daemon binary links. fonts-noto-cjk gives the daemon real CJK -# glyphs at runtime (matches the rust-check.yml Linux prereqs). +# script. op-host-web-server is the headless raster daemon (links op-web-daemon +# only — no winit/glutin/skia-GL under Approach Y), so NONE of the GL/X11 +# link-time libs the desktop binary needed are required here; only +# freetype/fontconfig (+ CJK fonts) for skia's raster text shaping at export. RUN apt-get update && apt-get install -y --no-install-recommends \ binaryen \ nodejs \ gzip \ ca-certificates \ pkg-config \ - libxkbcommon-dev libxkbcommon-x11-dev \ - libwayland-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev \ - libegl1-mesa-dev libgles2-mesa-dev libgbm-dev \ libfreetype-dev libfontconfig1-dev fonts-noto-cjk \ && rm -rf /var/lib/apt/lists/* @@ -69,9 +67,10 @@ WORKDIR /src # the CI checkout uses `submodules: recursive`. COPY . . -# Build the desktop daemon binary (the `--serve-web` host). Always built; this -# is the runtime binary regardless of how the web bundle is produced. -RUN cargo build -p op-host-desktop --release +# Build the headless web/MCP server binary (the `--serve-web` host) — links +# op-web-daemon only, no winit/skia-GL. Always built; this is the runtime +# binary regardless of how the web bundle is produced. +RUN cargo build -p op-host-web-server --release # Build the canvaskit wasm bundle in-image, UNLESS WEB_BUNDLE_SOURCE=copy. # Mirrors `tools/check-wasm-bundle.sh` exactly (cargo build --features canvaskit @@ -111,10 +110,6 @@ ENV OPENPENCIL_SERVE_PORT=${SERVE_PORT} RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ - libegl1 libgles2 libgbm1 \ - libxkbcommon0 libxkbcommon-x11-0 \ - libwayland-client0 libwayland-egl1 \ - libxcb-render0 libxcb-shape0 libxcb-xfixes0 \ libfreetype6 libfontconfig1 fonts-noto-cjk \ && rm -rf /var/lib/apt/lists/* @@ -124,7 +119,7 @@ WORKDIR /app # bundle as `/web-bundle` (and CanvasKit as `/web-bundle/ # canvaskit`). Keep the binary + the bundle dir as siblings under /app so that # ` == /app`. -COPY --from=builder /src/target/release/openpencil-desktop /app/openpencil-desktop +COPY --from=builder /src/target/release/op-host-web-server /app/op-host-web-server COPY --from=builder /out/web-bundle /app/web-bundle EXPOSE ${SERVE_PORT} @@ -134,4 +129,4 @@ EXPOSE ${SERVE_PORT} # reverse proxy for anything beyond a trusted network. The port is taken from # the build-time SERVE_PORT (baked into OPENPENCIL_SERVE_PORT); `sh -c` lets the # env var expand at container start. -CMD ["sh", "-c", "exec /app/openpencil-desktop --serve-web \"${OPENPENCIL_SERVE_PORT}\" --host 0.0.0.0"] +CMD ["sh", "-c", "exec /app/op-host-web-server --serve-web \"${OPENPENCIL_SERVE_PORT}\" --host 0.0.0.0"] diff --git a/tools/check-web-server-headless.sh b/tools/check-web-server-headless.sh new file mode 100755 index 000000000..52783f8d1 --- /dev/null +++ b/tools/check-web-server-headless.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Headless-boundary guard for the web / MCP server binary. +# +# The whole point of `op-host-web-server` (Approach Y) is that it links the +# extracted `op-web-daemon` and NOTHING from the desktop GUI stack: no winit / +# glutin / casement (windowing), no muda (native menus), no accesskit platform +# ADAPTERS (a11y bridges), and skia-safe WITHOUT the `gl` feature (raster only). +# This guard fails the build if any of those leak into the isolated dep graph, +# so the web image stays GUI-free. +# +# Codex Issue 1: the bare `accesskit` CORE crate is allowed — `op-editor-ui` +# pulls it unconditionally for the platform-free `Node` / `TreeUpdate` types +# (no GUI runtime). We grep the platform ADAPTER crates only. +set -euo pipefail + +cd "$(dirname "$0")/.." + +fail=0 + +# 1. No windowing / menu / a11y-adapter crates in the feature-resolved tree. +gui=$(cargo tree -p op-host-web-server -e features 2>/dev/null \ + | grep -E 'winit|glutin|casement|muda|accesskit_(macos|unix|windows|winit)' || true) +if [ -n "${gui}" ]; then + printf 'FAIL: op-host-web-server links a desktop GUI crate:\n%s\n\n' "${gui}" >&2 + fail=1 +fi + +# 2. skia-safe must be raster — NO `gl` feature. Under Approach Y, gl arrives +# only via the desktop-only `gl-host` edge; its presence in this isolated +# graph means a consumer leaked gl-host into the headless server. +skia_feats=$(cargo tree -p op-host-web-server -f '{p} {f}' 2>/dev/null \ + | grep 'skia-safe v' | sed 's/.*skia-safe v[^ ]*//' | head -1) +if printf '%s' "${skia_feats}" | grep -qE '(^| |,)gl(,| |$)'; then + printf 'FAIL: op-host-web-server skia-safe carries the `gl` feature (must be raster):\n features:%s\n\n' "${skia_feats}" >&2 + fail=1 +fi + +if [ "${fail}" -ne 0 ]; then + printf 'FAIL: op-host-web-server headless boundary check\n' >&2 + exit 1 +fi + +echo "PASS: op-host-web-server headless boundary check (no winit/glutin/casement/muda/accesskit-adapter; skia raster, no gl)"