minesweeper leaves the platform; plugin sockets relay ws-to-ws

Minesweeper is now plugins/minesweeper (gitea.officer.dev/plugins/minesweeper).
Removes /api/minesweeper, the Dashboard screen, the officerdev panel, the
permission registry entry, the App.tsx route and the hand-written websocket
provider in server.tsx. Registry entries: 22 -> 21, execution 4 -> 3.

The relay changed transport, and that is the part worth reading.

A sidecar announces exactly one port. That single announcement is the whole
provenance argument in target.ts — the platform watched the port arrive at
registration, which is the only thing separating it from a number a manifest
asserted. But a raw-TCP relay requires that port to BE a raw TCP listener, so
the sidecar cannot also serve HTTP on it, and the plugin's ordinary routes
have to fall back to the typed command vocabulary over the registration
socket. That is the mechanism the fleet has spent months retiring, and
Minesweeper would have been the first thing to go back to it.

Relaying websocket-to-websocket dissolves the conflict. The sidecar serves
HTTP and /ws on the one port it announced, exactly as the pty sidecar already
does; createSidecarProxy handles the HTTP half; and the conversion from frames
to whatever the plugin actually speaks happens inside the plugin, which is the
only process that knows. The relay is still ignorant of the payload — the
ignorance sits one layer up.

Also: the caller's JWT is no longer forwarded upstream. A sidecar has no use
for it and forwarding one puts a bearer token into whatever that process logs.
X-Officer-User carries the only part it needs. (The terminal relay still
forwards its token to the pty sidecar; not touched here.)

Verified: bunx tsgo clean, bun test 1403 pass / 0 fail, bun run docs:check
green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-23 17:43:43 +00:00
co-authored by Claude Opus 5
parent 6490dbc368
commit fbdfd3fb89
26 changed files with 96 additions and 1034 deletions
+11 -5
View File
@@ -285,10 +285,10 @@ export type PluginManifest = {
/**
* A websocket this plugin serves, relayed by the platform.
*
* Declaring this makes `/api/plugin-ws/<appName>` a byte relay onto the address below. It exists because
* Bun's route table is fixed at `serve()`, so a plugin installed afterwards could never add a socket route
* — which is why `/minesweeper` had to live in the platform and `plugins/desktop` could not stream a
* display at all.
* Declaring this makes `/api/plugin-ws/<appName>` a frame relay onto `ws://127.0.0.1:<port>/ws`, where the
* port is the one the named sidecar announced at registration. It exists because Bun's route table is
* fixed at `serve()`, so a plugin installed afterwards could never add a socket route — which is why
* `/minesweeper` had to live in the platform and `plugins/desktop` could not stream a display at all.
*
* Authorisation is the plugin's OWN capability, asked as if the socket were a GET against its HTTP prefix.
* There is deliberately no separate socket permission: granting the plugin grants its socket, and nothing
@@ -308,7 +308,13 @@ export type PluginManifest = {
* manifest asserted. Provenance, not a claim.
*
* The cost is that a plugin wanting a socket needs a sidecar. That is not ceremony — it is the thing
* that makes the port mean anything.
* that makes the port mean anything. It costs nothing else: the socket lives at `/ws` on the SAME
* listener the sidecar already serves its routes from, so a plugin does not choose between having a
* socket and having ordinary HTTP.
*
* **The sidecar serves a websocket, not a raw stream.** Whatever it is really talking to — a VNC display,
* a line protocol, another process entirely — the conversion happens inside the plugin, because the
* plugin is the only thing that knows. The platform moves frames it does not read.
*
* The host is always loopback and cannot be set. A plugin naming an arbitrary host would be a
* request-forgery primitive reachable by anyone the plugin is granted to.