Two things the live install found. The manifest was refused at discovery. `apt: 'wine wine32:i386'` is two packages and an architecture suffix, and a package name may be neither — nor should it be, since `dpkg --add-architecture i386` has to happen first and no package name can express that. And Fedora's Xvfb package is `xorg-x11-server-Xvfb`, with a capital X, which a manifest package name may not have; the dnf entry is dropped rather than guessed, which turns into an instruction to the owner instead of a wrong install. More importantly: confining the prefix before wine starts is a confinement wine can undo. Observed directly — after a run, every com* link had come back with a fresh timestamp, because wine rebuilds dosdevices when it boots a prefix it thinks needs updating, and a wine package upgrade is enough to make it think so. Z: did not come back that time, but "the dangerous one happened not to regenerate" is not a property to rely on. confineAndVerify now runs again once the game is up and says so if Z: returned. Verified on the live server: Z: and the three home symlinks are gone from a prefix that had them, run/ is mode 700, nothing listens on 5907, and the RFB handshake arrives through the platform relay with a valid owner token and through nothing else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
133 lines
7.3 KiB
TypeScript
133 lines
7.3 KiB
TypeScript
import type { PluginManifest } from '@@/plugins/manifest';
|
|
|
|
/**
|
|
* Minesweeper X — a 1990s Windows game, running under wine on a virtual display, streamed to the browser.
|
|
*
|
|
* ── Why this is worth having at all ──
|
|
*
|
|
* It is a game, and it is also the smallest honest proof that Officer can put an arbitrary X11 program in a
|
|
* panel. Everything the Desktop plugin wants to do is here at one-tenth the size: a headless framebuffer, a
|
|
* process on it, a VNC server reading it, and a client in the browser. If this works, "run a Linux app in
|
|
* Officer" is a solved shape rather than a plan.
|
|
*
|
|
* ── It was in the platform until this extraction ──
|
|
*
|
|
* `/api/minesweeper`, `Screens/Dashboard/Minesweeper`, an `officerdev` panel, a permission-registry entry
|
|
* and a hand-written websocket provider in `server.tsx`. The socket is why it had to live there: Bun's route
|
|
* table is fixed at `serve()`, so nothing installed later could add one. That ceiling is gone — the platform
|
|
* now serves one wildcard socket door for every plugin — and this plugin is the first thing through it.
|
|
*
|
|
* ── The session is the sidecar's, not the platform's ──
|
|
*
|
|
* `officer-minesweeper` owns the whole lifecycle: it starts Xvfb, launches the game under wine, runs x11vnc
|
|
* against that display, and tears all three down. The platform's router forwards three routes to it and
|
|
* knows nothing else.
|
|
*
|
|
* That is not tidiness. The platform used to spawn `bin/session.sh` detached, which meant the game outlived
|
|
* every restart, nothing supervised it, and a crashed run left wine processes holding the prefix so the next
|
|
* start hung rather than failed. PM2 supervises a sidecar; a sidecar can clean up after itself on SIGTERM.
|
|
*
|
|
* ── x11vnc listens on a unix socket, and has no password ──
|
|
*
|
|
* `-nopw` is correct here and the reasoning is not "it is only a game". x11vnc binds a socket inside the
|
|
* sidecar's private run directory, mode 700, and NOTHING else can reach it — not another local account, not
|
|
* a member with a Linux user, not a process that guessed a port. There is no network listener to
|
|
* authenticate against, so a password would protect nothing and would be one more secret to store.
|
|
*
|
|
* The version this replaced listened on `127.0.0.1:5907`, which is a different claim: every local account
|
|
* could open it and take the display. That was survivable on a single-user machine and stopped being so the
|
|
* moment members got Linux accounts. A fixed port is also squattable — anything can bind 5907 first and be
|
|
* relayed to by the platform.
|
|
*
|
|
* ── It is grantable, and that took a fix rather than a decision ──
|
|
*
|
|
* In the platform this was `kind: 'execution'` — never grantable to anyone, at any level — because the wine
|
|
* process runs as the server owner and Minesweeper X has Open and Save dialogs for its replays. A fresh wine
|
|
* prefix maps the whole filesystem to `Z:` and symlinks the profile into the owner's real home, so "granted
|
|
* Minesweeper" quietly meant "granted a file picker as the owner".
|
|
*
|
|
* A plugin permission has no `kind` and is always grantable, so extraction alone would have turned a
|
|
* structurally sealed capability into a hole. `sidecar/prefix.ts` closes it instead: the prefix is confined
|
|
* to `C:` on every start, and the game can see nothing outside it. That is what makes the entry below
|
|
* honest rather than optimistic.
|
|
*
|
|
* One thing that is NOT solved: there is one display and one game, so two people granted this share a board
|
|
* and fight over the mouse. Per-viewer sessions would be a display and a prefix each — cheap, but a design
|
|
* decision rather than an implementation detail.
|
|
*
|
|
* ── `osDependencies` ──
|
|
*
|
|
* Six binaries, all `owner` scope: the session runs as the server owner, on the owner's display, in the
|
|
* owner's wine prefix. `wine` is the awkward one — the game is a 32-bit VB6 executable, so the host needs
|
|
* i386 multiarch and a `win32` prefix, and neither is something a package name can express. The sidecar
|
|
* builds the prefix on first start and says what it needs if it cannot; see `sidecar/prefix.ts`.
|
|
*/
|
|
export const manifest: PluginManifest = {
|
|
publisher: 'officerdev',
|
|
version: '1.0.0',
|
|
platform: '>=1.0.0',
|
|
|
|
label: 'Minesweeper',
|
|
summary: 'Minesweeper X under wine, on a headless display, streamed to the browser',
|
|
icon: 'Bomb',
|
|
color: '#64748b',
|
|
|
|
// The relay target. Not a port: a sidecar name, so the port is one the platform watched arrive at
|
|
// registration rather than one this file asserted. See `servers/api/plugin-ws/target.ts`.
|
|
websocket: { sidecar: 'minesweeper' },
|
|
|
|
permissions: [
|
|
{
|
|
key: 'minesweeper',
|
|
label: 'Minesweeper',
|
|
description: 'Play Minesweeper on the server display. The game is confined to its own wine prefix and can reach nothing else on the machine.',
|
|
},
|
|
],
|
|
|
|
osDependencies: [
|
|
{
|
|
binary: 'wine',
|
|
reason: 'Runs the game. Minesweeper X is a 32-bit Windows executable, so this host also needs i386 multiarch (`dpkg --add-architecture i386`) and the 32-bit wine package — a 64-bit-only wine cannot load it.',
|
|
// `apt: 'wine'` alone, deliberately. The 32-bit half is `wine32:i386`, and a package name may not
|
|
// carry an architecture suffix or a second package — nor should it, since `dpkg --add-architecture
|
|
// i386` has to happen first and no package name can express that. The `reason` above carries the
|
|
// whole command, which is what the owner is shown when this is missing.
|
|
packages: { apt: 'wine', dnf: 'wine', pacman: 'wine', brew: 'wine-stable' },
|
|
scope: 'owner',
|
|
},
|
|
{
|
|
binary: 'Xvfb',
|
|
reason: 'The virtual display the game draws on. A framebuffer with no hardware behind it, so nothing appears on any real monitor.',
|
|
// No `dnf` entry, and not by omission. Fedora's package is `xorg-x11-server-Xvfb` — capital X —
|
|
// and a manifest package name must be lowercase. Left out means "cannot be installed automatically
|
|
// here", which turns into an instruction to the owner rather than a wrong guess.
|
|
packages: { apt: 'xvfb', pacman: 'xorg-server-xvfb' },
|
|
scope: 'owner',
|
|
},
|
|
{
|
|
binary: 'x11vnc',
|
|
reason: 'Reads that display and serves it, so the browser has something to draw. Bound to a private unix socket, never a port.',
|
|
packages: { apt: 'x11vnc', dnf: 'x11vnc', pacman: 'x11vnc' },
|
|
scope: 'owner',
|
|
},
|
|
{
|
|
binary: 'xdotool',
|
|
reason: 'Finds the game window and reads its size, so the panel can fit its canvas to the board instead of the whole framebuffer.',
|
|
packages: { apt: 'xdotool', dnf: 'xdotool', pacman: 'xdotool' },
|
|
scope: 'owner',
|
|
},
|
|
{
|
|
binary: 'xdpyinfo',
|
|
reason: 'Waits for the display to be usable. A live Xvfb process is not the same as a display that answers, and starting the game too early fails silently.',
|
|
packages: { apt: 'x11-utils', dnf: 'xorg-x11-utils', pacman: 'xorg-xdpyinfo' },
|
|
scope: 'owner',
|
|
},
|
|
{
|
|
binary: 'winetricks',
|
|
reason: 'Installs the Visual Basic 6 runtime into the wine prefix on first start. Minesweeper X needs MSVBVM60.DLL, which wine does not ship; without it the process exits immediately with nothing but an unresolved-import line.',
|
|
packages: { apt: 'winetricks', dnf: 'winetricks', pacman: 'winetricks' },
|
|
scope: 'owner',
|
|
},
|
|
],
|
|
};
|