Initial commit: Greenfield architectural analysis (14 files, ~80 pages, 10 parallel research agents)

This commit is contained in:
1b-agent committed 2026-10-08 08:48:25 +00:00
commit c4b45cf218
15 files changed
+5444

No files matched your search

+644
View File
@@ -0,0 +1,644 @@
# 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 │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────┘
```