# 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 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 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 `` 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: ```typescript // 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) ```python # 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: ```python # 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` listener socket. 2. Sets `DISPLAY=:N` environment variable. 3. Spawns `Xwayland :N -ac -rootless -listen -displayfd -wm `. 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 `` 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 │ │ │ │ - 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 │ │ │ └────────────────────────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────────────────────────┘ ```