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

645 lines
34 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```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<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 │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────┘
```