Files
one-os-dev-wayland-html5-co…/10-porting-to-odoo-js-python.md

34 KiB
Raw Permalink Blame History

10 — Porting Blueprint (JS Browser + Python Host + Odoo 20 OWL Shell)

This is the standalone porting chapter. It maps every Greenfield layer analyzed in chapters 02-09 to concrete guidance for building a Wayland-to-HTML compositor that integrates with an Odoo 20 addon shell and a JS + Python backend.

Target architecture

The recommended architecture:

┌──────────────────────────────────────────────────────────────────────────────┐
│                            BROWSER (Odoo 20 web client)                       │
│                                                                              │
│  ┌──────────────────────────────────────────────────────────────────────────┐ │
│  │ Odoo 20 OWL Shell addon (your code)                                      │ │
│  │  - Application launcher (lists apps from /api/compositor/apps)          │ │
│  │  - Window management UI (taskbar, decorations)                           │ │
│  │  - Keyboard layout picker (RMLVO)                                        │ │
│  │  - Per-output <canvas> element                                          │ │
│  │  - Triggers app launches via Odoo /api/compositor/launch                │ │
│  │  - Implements UserShellApi.events.* callbacks                            │ │
│  └──────────────────────────────────────────────────────────────────────────┘ │
│                                │                                              │
│                                │ userShell.events + userShell.actions         │
│                                ▼                                              │
│  ┌──────────────────────────────────────────────────────────────────────────┐ │
│  │ @gfld/compositor (browser-side compositor library)                      │ │
│  │  - Use as-is from npm: @gfld/compositor                                  │ │
│  │  - Replaces compositor-shell with createOdooUserShellApi(session)       │ │
│  │  - Renders surfaces to <canvas> via WebGL                                 │ │
│  │  - Manages keyboard/pointer/touch                                        │ │
│  └──────────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────┘
                                 │
                                 │ WebSocket (TCP, KCP-over-WS)
                                 │ 9 endpoint paths per session
                                 ▼
┌──────────────────────────────────────────────────────────────────────────────┐
│                       PYTHON HOST (Linux server)                              │
│                                                                              │
│  ┌──────────────────────────────────────────────────────────────────────────┐ │
│  │ Odoo 20 backend addon (your code)                                        │ │
│  │  - /api/compositor/apps → list of available apps                         │ │
│  │  - /api/compositor/launch → triggers proxy to spawn app                  │ │
│  │  - Session management (one Odoo session ↔ one proxy session)              │ │
│  │  - Per-tenant isolation, ACL, audit log                                  │ │
│  └──────────────────────────────────────────────────────────────────────────┘ │
│                                │                                              │
│                                │ spawn / IPC                                   │
│                                ▼                                              │
│  ┌──────────────────────────────────────────────────────────────────────────┐ │
│  │ Python compositor-proxy (your code — replaces @gfld/compositor-proxy)    │ │
│  │  - aiohttp/FastAPI WebSocket server (replaces ws + ws-server)            │ │
│  │  - pywayland (cffi bindings to libwayland-server)                        │ │
│  │  - python-xkbcommon for keymap compilation                              │ │
│  │  - GStreamer Python bindings (gi.repository.Gst) for H.264 encoding      │ │
│  │  - asyncio event loop integrating libwayland FD polling                  │ │
│  │  - Per-app child_process spawning (subprocess.Popen with env)            │ │
│  │  - Sandboxing via bubblewrap + seccomp (NEW — Greenfield has none)       │ │
│  └──────────────────────────────────────────────────────────────────────────┘ │
│                                │                                              │
│                                │ WAYLAND_DISPLAY=wayland-N                     │
│                                ▼                                              │
│  ┌──────────────────────────────────────────────────────────────────────────┐ │
│  │ Native Linux apps (gtk4-demo, xterm, kwrite, ...)                        │ │
│  │  - Use host system libraries (no sysroot)                                │ │
│  │  - Sandboxed via bubblewrap + seccomp                                    │ │
│  └──────────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────┘

Per-layer porting guidance

For each Greenfield layer analyzed in chapters 02-09:

Layer 1: Wayland Protocol XML

What to copy as-is: All four protocol XML files (wayland.xml, xdg-shell.xml, wayland-drm.xml, web-bitmapbuf-unstable-v1.xml). They are upstream standard + Greenfield's custom extension. No need to rewrite.

What to replace: Nothing. The XML is the source of truth.

Where Python fits: Python doesn't need to parse the XML directly — use pywayland which already ships with parsed bindings for the standard protocols. For Greenfield's custom web-bitmapbuf-unstable-v1, you can either skip it (native apps don't use it) or generate Python bindings via pywayland-protocol-scanner.

Where the Odoo 20 OWL shell hooks in: Not applicable. This layer is below the shell.

Layer 2: Protocol Generators

What to copy as-is: The TypeScript generators for the browser side (client-generator, compositor-generator). They produce stubs for the browser compositor and browser-resident JS apps.

What to replace: The compositor-proxy-generator. Greenfield's proxy generates CommonJS *_interface.js + *_interceptor.js files for the Node N-API addon. For Python, you don't need a generator — use pywayland's built-in protocol bindings.

Where Python fits: pywayland provides wl_interface structs and request/event dispatch in pure Python via cffi. You write Python classes that implement WlCompositorRequests, WlSeatRequests, etc.

Where the Odoo 20 OWL shell hooks in: Not applicable.

Layer 3: Wayland Runtime

What to copy as-is: @gfld/common (Connection.ts) and @gfld/compositor-protocol for the browser side. These implement the standard Wayland wire format and object lifecycle, and they're MIT-licensed.

What to replace: The proxy's wayland-server.ts (which wraps the wayland-server-addon.node N-API addon). Replace with pywayland's Display and Client classes.

Where Python fits: pywayland provides:

  • pywayland.server.Display — wraps wl_display
  • pywayland.server.Client — wraps wl_client
  • pywayland.server.Resource — wraps wl_resource
  • pywayland.server.Global — wraps wl_global
  • Per-resource event/request dispatch via Python method overrides

You write Python classes that subclass WlCompositor, WlSeat, etc. — similar to how Greenfield's TypeScript classes implement *Requests interfaces.

Where the Odoo 20 OWL shell hooks in: Not applicable. The runtime layer is below the shell.

Layer 4: Transport

What to copy as-is: The browser side of Channel.ts (ARQChannel

  • SimpleChannel + ReconnectingWebSocket) and kcp.ts. The TypeScript KCP implementation is solid and battle-tested.

What to replace: The native side of Channel.ts. Replace Node's ws library with aiohttp WebSocket or FastAPI WebSocket.

Where Python fits:

  • Use aiohttp.web.WebSocketResponse or starlette.websockets.WebSocket for the WebSocket server.
  • Port the KCP implementation from TypeScript to Python (or wrap a C implementation via cffi — pykcp exists).
  • Use asyncio event loop integration: pywayland exposes the libwayland event-loop FD, which you can integrate via asyncio.loop.add_reader().
  • For multiplexing 9 endpoints, use aiohttp.web.Application with 9 routes, or FastAPI with 9 WebSocket endpoints.

Where the Odoo 20 OWL shell hooks in: The shell connects to the proxy via the same WebSocket URLs. No change needed.

Layer 5: Rendering Pipeline (browser side)

What to copy as-is: The entire packages/compositor/src/ rendering pipeline. This is the heart of Greenfield and it's where most of the engineering effort went. Reuse it wholesale:

  • Surface.ts, View.ts, Output.ts, Loop.ts, Callback.ts, Region.ts, Shm.ts, BufferContents.ts, BufferImplementation.ts, ImageBitmapBuffer.ts
  • render/ directory (Renderer, Scene, SceneShader, Texture, RenderState, shaders)
  • remote/ directory (StreamingBuffer, BufferStream, EncodedFrame, EncoderFeedback, decoders)

What to replace: Nothing. This layer is pure browser TypeScript and works as-is.

Where Python fits: Not applicable. The browser does all rendering.

Where the Odoo 20 OWL shell hooks in: The shell owns the <canvas> element and passes it to userShell.actions.initScene(canvasCreator). The compositor creates a Scene per canvas.

Layer 6: Surface Lifecycle / Roles

What to copy as-is: All of XdgWmBase.ts, XdgSurface.ts, XdgToplevel.ts, XdgPopup.ts, XdgPositioner.ts, Shell.ts, ShellSurface.ts, Subcompositor.ts, Subsurface.ts, SurfaceRole.ts, SurfaceChild.ts. These implement Wayland's role state machines correctly.

What to replace: Nothing.

Where Python fits: Not applicable (this is browser-side).

Where the Odoo 20 OWL shell hooks in: The shell implements UserShellApi.events callbacks (see 08-app-lifecycle.md). Specifically:

  • surfaceCreated → add a window to the OWL taskbar
  • surfaceDestroyed → remove from taskbar
  • surfaceTitleUpdated → update window title
  • surfaceActivationUpdated → mark window as active
  • clientCreated / clientDestroyed → update app registry
  • clientUnresponsiveUpdated → show "not responding" indicator

Layer 7: Desktop / Layout Policy

What to copy as-is: desktop/Desktop.ts provides Floating and AlwaysFullScreen modes. Floating is what most users want (windows can be moved/resized freely).

What to replace: If your Odoo shell needs custom layout policies (e.g. tiling, tabbed), you'll need to extend DesktopSurface with new modes. But you can start with Floating.

Where Python fits: Not applicable.

Where the Odoo 20 OWL shell hooks in: The shell calls userShell.actions.activateSurface(compositorSurface) when the user clicks a taskbar entry.

Layer 8: User Shell (the integration seam)

What to copy as-is: UserShellApi.ts. The interface is clean and shell-agnostic.

What to replace: Replace compositor-shell (Preact) with your Odoo 20 OWL addon.

Where Python fits: Not applicable. The shell is browser-side.

Where the Odoo 20 OWL shell hooks in: This IS the integration point. Create an OWL component that:

// Pseudocode for Odoo 20 OWL shell
import { Component, useState, onMounted } from "@odoo/owl";
import { createCompositorSession } from "@gfld/compositor";

export class CompositorShell extends Component {
  setup() {
    this.state = useState({
      surfaces: new Map(),  // id → { title, appId, active }
      clients: new Map(),
    });

    onMounted(async () => {
      const session = createCompositorSession();
      this.session = session;

      // Wire UserShellApi events to OWL reactive state
      session.userShell.events.surfaceCreated = (surface) => {
        this.state.surfaces.set(surface.id, {
          title: "", appId: "", active: false
        });
      };
      session.userShell.events.surfaceDestroyed = (surface) => {
        this.state.surfaces.delete(surface.id);
      };
      session.userShell.events.surfaceTitleUpdated = (surface, title) => {
        const s = this.state.surfaces.get(surface.id);
        if (s) s.title = title;
      };
      session.userShell.events.surfaceActivationUpdated = (surface, active) => {
        const s = this.state.surfaces.get(surface.id);
        if (s) s.active = active;
      };
      // ... etc for clientCreated, clientDestroyed, etc.

      // Initialize the canvas
      const canvas = this.canvasRef.el;
      session.userShell.actions.initScene(() => ({
        canvas,
        width: canvas.clientWidth,
        height: canvas.clientHeight,
      }));
    });
  }

  // User clicks app in launcher
  async launchApp(appId) {
    const response = await fetch("/web/compositor/launch", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ app_id: appId }),
    });
    const { signal_url } = await response.json();
    // Browser JS opens signal_url WebSocket → compositor handles the rest
  }

  // User clicks taskbar entry
  activateSurface(surfaceId) {
    this.session.userShell.actions.activateSurface({ id: surfaceId });
  }
}

Odoo 20 backend addon

The Odoo 20 backend addon acts as a thin proxy between the OWL shell and the Python compositor-proxy. It handles:

  • App launcher config (per-user, per-tenant allowed apps list)
  • Auth (Odoo session → compositor-proxy session mapping)
  • Audit log (which user launched which app when)
  • Multi-tenant isolation (separate Python proxy processes per tenant, containerized)
  • Per-tenant resource limits (max concurrent apps, max memory)
# Pseudocode for Odoo 20 backend controller
from odoo import http
from odoo.http import request

class CompositorController(http.Controller):

    @http.route("/web/compositor/apps", type="json", auth="user")
    def list_apps(self):
        # Return user-allowed apps from applications.json
        user = request.env.user
        return request.env["compositor.app"].search_read(
            [("allowed_group_ids", "in", user.groups_id.ids)],
            ["name", "icon", "app_id"],
        )

    @http.route("/web/compositor/launch", type="json", auth="user")
    def launch_app(self, app_id):
        user = request.env.user
        # Look up the user's compositor-proxy host (per-tenant routing)
        proxy_host = user.compositor_proxy_host
        proxy_port = user.compositor_proxy_port

        # Tell the Python proxy to spawn the app
        # The proxy returns a signal_url + auth key
        import aiohttp
        async with aiohttp.ClientSession() as sess:
            async with sess.post(
                f"http://{proxy_host}:{proxy_port}/launch",
                json={
                    "app_id": app_id,
                    "odoo_session_id": request.session.sid,
                    "user_id": user.id,
                },
            ) as resp:
                result = await resp.json()

        # Audit log
        request.env["compositor.audit.log"].create({
            "user_id": user.id,
            "app_id": app_id,
            "timestamp": fields.Datetime.now(),
        })

        return {"signal_url": result["signal_url"]}

Python compositor-proxy (the big new code)

This is the bulk of the new code. Replace Node's compositor-proxy + compositor-proxy-cli with a Python equivalent. The key components:

# Pseudocode for Python compositor-proxy
import asyncio
import os
import signal
import subprocess
from aiohttp import web, WSMsgType
from pywayland.server import Display
from xkbcommon import xkb
import gi
gi.require_version("Gst", "1.0")
gi.require_version("GstGL", "1.0")
from gi.repository import Gst, GstGL, GLib

class CompositorProxy:
    def __init__(self, bind_ip="0.0.0.0", bind_port=8081, render_device="/dev/dri/renderD128"):
        self.bind_ip = bind_ip
        self.bind_port = bind_port
        self.render_device = render_device
        self.display = Display()
        self.sessions = {}  # session_id → SessionProcess

    async def start(self):
        # 1. Create wl_display + add socket
        self.display.create()
        socket_name = self.display.add_socket_auto()
        os.environ["WAYLAND_DISPLAY"] = socket_name

        # 2. Init SHM formats (YUV420, NV12, YUV444, NV21)
        self.init_shm()

        # 3. Init DRM (open /dev/dri/renderD128, register dmabuf globals)
        self.init_drm()

        # 4. Integrate wl_display FD into asyncio loop
        loop = asyncio.get_event_loop()
        loop.add_reader(self.display.get_fd(), self.dispatch_requests)

        # 5. Start GStreamer
        Gst.init(None)
        self.gst_loop = GLib.MainLoop()
        # Run GLib loop in a separate thread (GStreamer requires it)
        import threading
        threading.Thread(target=self.gst_loop.run, daemon=True).start()

        # 6. Start aiohttp server
        app = web.Application()
        app.add_routes([
            web.get("/signal", self.handle_signal),
            web.get("/channel", self.handle_channel),
            web.post("/launch", self.handle_launch),
            # ... 6 more endpoints
        ])
        runner = web.AppRunner(app)
        await runner.setup()
        site = web.TCPSite(runner, self.bind_ip, self.bind_port)
        await site.start()

        # Run forever
        await asyncio.Event().wait()

    def dispatch_requests(self):
        """Called by asyncio when wl_display FD is readable."""
        self.display.dispatch_events_pending()

    async def handle_launch(self, request):
        body = await request.json()
        app_id = body["app_id"]

        # Spawn the app, set WAYLAND_DISPLAY
        env = {**os.environ, "WAYLAND_DISPLAY": os.environ["WAYLAND_DISPLAY"]}
        proc = subprocess.Popen(
            self.applications[app_id]["executable"],
            env=env,
            # NEW vs Greenfield: sandbox with bubblewrap
            preexec_fn=lambda: self.setup_sandbox(),
        )

        return web.json_response({
            "signal_url": f"ws://{self.bind_ip}:{self.bind_port}/signal?app_id={app_id}",
            "pid": proc.pid,
        })

    def setup_sandbox(self):
        """NEW: bubblewrap + seccomp sandbox (Greenfield has none)."""
        import ctypes
        libc = ctypes.CDLL("libc.so.6")
        # ... bwrap / unshare / seccomp_filter setup

Native app bridge (Python equivalent)

For each Wayland client that connects (one per app), spawn a Python NativeWaylandClientSession equivalent that:

  1. Sets up the wire-message interceptor (using pywayland's resource dispatch).
  2. Creates a PROTOCOL ARQ channel (port kcp.ts to Python or use pykcp).
  3. Creates a FRAME ARQ channel for the surface's encoded content.
  4. Manages the app's lifecycle (10-min SIGHUP grace, SIGTERM escalation).

For XWayland, replace westfield-xwayland.c with a Python equivalent that:

  1. Creates /tmp/.X11-unix/X<N> listener socket.
  2. Sets DISPLAY=:N environment variable.
  3. Spawns Xwayland :N -ac -rootless -listen <fd> -displayfd <fd> -wm <fd>.
  4. Wraps the wm_fd as a Python file-like object.
  5. Uses xtsb (the existing TypeScript X11 bindings) on the browser side to handle XWM logic — the browser side stays the same.

Hardware acceleration (Python equivalent)

For the encoding pipeline:

  • Use GStreamer Python bindings (gi.repository.Gst):
    • appsrc for input buffers
    • glupload, glcolorconvert, glshader for GPU processing
    • x264enc / nvh264enc / vaapih264enc for H.264 encoding
    • appsink for output samples
  • Same encoder selection logic as Greenfield (PNG ≤ 256×256, H.264 above).
  • Run the GLib main loop in a separate thread, use asyncio.run_in_executor for cross-thread encoding requests.
  • For zero-copy dmabuf, use GstGLMemoryEGL (same as Greenfield).

For the browser decode side: no change. The browser uses WebCodecs preferred + FFmpeg Wasm fallback, same as Greenfield.

Key trade-offs vs staying on Node

Staying on Node (use Greenfield as-is, just replace compositor-shell with Odoo)

Pros:

  • ~90% of Greenfield's code reused as-is (browser compositor + native proxy)
  • Less code to write/maintain
  • Faster time-to-market
  • Battle-tested in Greenfield's rc1 cycle

Cons:

  • Native Node addons (.node files) are Linux-x64 only, ABI-locked to Node major
  • GStreamer + libwayland integration is via C N-API, harder to extend
  • Odoo team may have more Python expertise than Node expertise
  • AGPL-3.0 license on compositor-proxy — viral to your modifications

Switching to Python

Pros:

  • Aligns with Odoo's Python backend stack
  • pywayland (cffi) is easier to debug than N-API addons
  • Python GStreamer bindings are well-documented
  • Easier to integrate with Odoo's auth/session model
  • Can avoid AGPL by reimplementing from scratch (or use only MIT libs)

Cons:

  • Significant new code (rewrite the proxy + native addons in Python)
  • Python GIL may bottleneck encoding throughput (use multiprocessing or run GStreamer in separate thread)
  • pywayland is less mature than libwayland's native bindings
  • You take on the maintenance of a forked Greenfield-equivalent

Recommendation: hybrid approach

  1. Browser side: Use Greenfield's @gfld/compositor library as-is (MIT-licensed via @gfld/compositor-protocol + @gfld/common, AGPL only on @gfld/compositor itself — but the shell is yours so the AGPL virality is contained to the compositor library).
  2. Native side: Start with Greenfield's compositor-proxy (AGPL) to get to MVP fast. Then progressively port to Python as needs evolve (sandboxing, multi-tenant, custom auth, Odoo session integration).
  3. Shell: Build the Odoo 20 OWL addon from scratch (clean room, MIT/Apache license).

Implementation phases

Phase 1 (1-2 weeks): MVP — display a single Linux app

  1. Run Greenfield's compositor-proxy-cli as a Docker container.
  2. Build minimal Odoo 20 OWL addon that creates a <canvas> and userShell.events handlers.
  3. Embed the OWL addon in an Odoo action.
  4. Trigger app launch via HTTP fetch to the proxy.
  5. Verify the Linux app displays in the canvas.

Phase 2 (2-4 weeks): Multi-app, basic window management

  1. Implement surfaceCreated / surfaceDestroyed handlers.
  2. Build a taskbar OWL component listing active surfaces.
  3. Implement activateSurface action on taskbar click.
  4. Test with 2-3 concurrent apps.

Phase 3 (4-8 weeks): Python port of proxy

  1. Replace Node compositor-proxy-cli with a Python equivalent.
  2. Implement WebSocket endpoints in aiohttp or FastAPI.
  3. Port the KCP TypeScript implementation to Python (or use pykcp).
  4. Replace wayland-server-addon.node with pywayland.
  5. Replace proxy-encoding-addon.node with GStreamer Python bindings.
  6. Add bubblewrap sandboxing (NEW — Greenfield has none).

Phase 4 (8-16 weeks): Polish & scale

  1. Multi-tenant isolation (separate Python proxy processes per tenant).
  2. Per-tenant resource limits (cgroups, max memory, max CPU).
  3. Audit log integration with Odoo.
  4. Clipboard sync (native → DOM and reverse).
  5. Touch input implementation (Greenfield has stub only).
  6. Primary selection (middle-click paste).
  7. Adaptive bitrate (consume EncoderFeedback data, Greenfield doesn't).

Specific replacements for each Greenfield native component

Greenfield native component Python replacement Notes
wayland-server-addon.node (1231 lines C) pywayland (cffi bindings) Same libwayland-server, Python wrapper
proxy-poll-addon.node (127 lines C) asyncio.loop.add_reader() Built-in, no addon needed
proxy-encoding-addon.node + libproxy-encoding.so (~1700 lines C) gi.repository.Gst (GStreamer Python bindings) Same GStreamer, Python wrapper
Patched libwayland-server (vendored at native/wayland/src/wayland-server/) Use upstream libwayland-server + Greenfield's patches applied Apply changes.diff to a fork of libwayland, install via system package
westfield-*.c (EGL, dmabuf, xwayland helpers) Reimplement in Python via pyEGL or python-xkbcommon + pygobject's GstGL Less mature than Greenfield's C — be prepared to debug
xtsb (X11 TypeScript bindings) Use as-is on browser side, replace Python side with python-xlib Browser-side xtsb is MIT-licensed
Node ws library aiohttp.web.WebSocketResponse or starlette.websockets.WebSocket Mature, well-documented
Node pkg (single-binary packaging) PyInstaller or shiv Same end result
Node fork() per session multiprocessing.Process or os.fork() Direct equivalent
compositor-proxy-cli (CLI args) argparse Standard Python
ajv (JSON schema validation) jsonschema Standard Python

Final architecture diagram (target state)

┌──────────────────────────────────────────────────────────────────────────────────┐
│                           BROWSER (Odoo 20 web client)                            │
│                                                                                  │
│  ┌────────────────────────────────────────────────────────────────────────────┐  │
│  │ Odoo 20 OWL Shell addon (your code, MIT/Apache)                            │  │
│  │  - Application launcher, window manager, keyboard picker                  │  │
│  │  - Per-output <canvas>                                                    │  │
│  │  - Implements UserShellApi.events.* callbacks                             │  │
│  └────────────────────────────────────────────────────────────────────────────┘  │
│                                  │                                                │
│                                  │ userShell.events + actions                     │
│                                  ▼                                                │
│  ┌────────────────────────────────────────────────────────────────────────────┐  │
│  │ @gfld/compositor (browser-side, MIT libs + AGPL compositor)                │  │
│  │  - Surface, View, Output, Loop, Seat, Keyboard, Pointer, Touch             │  │
│  │  - render/ (WebGL shaders)                                                 │  │
│  │  - remote/ (Channel, KCP, decoders)                                       │  │
│  └────────────────────────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────────────────────┘
                                  │
                                  │ WebSocket + KCP-over-WS
                                  │ 9 endpoint paths per session
                                  ▼
┌──────────────────────────────────────────────────────────────────────────────────┐
│                              PYTHON HOST (Linux server)                            │
│                                                                                  │
│  ┌────────────────────────────────────────────────────────────────────────────┐  │
│  │ Odoo 20 backend addon (your code, MPL-2.0/LGPL)                            │  │
│  │  - /web/compositor/apps, /web/compositor/launch                            │  │
│  │  - Per-user, per-tenant allowed apps list                                  │  │
│  │  - Audit log                                                                │  │
│  └────────────────────────────────────────────────────────────────────────────┘  │
│                                  │                                                │
│                                  │ subprocess / IPC                               │
│                                  ▼                                                │
│  ┌────────────────────────────────────────────────────────────────────────────┐  │
│  │ Python compositor-proxy (your code, MIT/Apache)                            │  │
│  │  - aiohttp/FastAPI WebSocket server (9 endpoints)                         │  │
│  │  - pywayland (cffi bindings to libwayland-server)                          │  │
│  │  - python-xkbcommon for keymap compilation                                 │  │
│  │  - GStreamer Python bindings for H.264 encoding                            │  │
│  │  - asyncio event loop integrating libwayland FD polling                    │  │
│  │  - Per-app subprocess.Popen with bubblewrap sandbox                        │  │
│  │  - KCP port (pykcp or hand-rolled)                                        │  │
│  └────────────────────────────────────────────────────────────────────────────┘  │
│                                  │                                                │
│                                  │ WAYLAND_DISPLAY=wayland-N                      │
│                                  ▼                                                │
│  ┌────────────────────────────────────────────────────────────────────────────┐  │
│  │ Native Linux apps (gtk4-demo, xterm, kwrite, ...)                          │  │
│  │  - Use host system libraries                                               │  │
│  │  - Sandboxed via bubblewrap + seccomp                                      │  │
│  └────────────────────────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────────────────────┘