Files
platform/src/servers/api/plugin-ws/target.ts
T
pastilhasandClaude Opus 5 fbdfd3fb89 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
2026-08-23 17:43:43 +00:00

43 lines
2.4 KiB
TypeScript

import { sidecarPort } from '@@/sidecar-registry';
import { snapshotPlugins } from '@@/plugins/mount';
/*
Where a plugin's socket actually goes.
A target is an ADDRESS — a host and a port — never a URL a manifest wrote. The relay turns it into
`ws://<host>:<port>/ws`, and `/ws` is a convention rather than a setting for the same reason the host is
not configurable: every value here has to be one the platform derived, not one a plugin asserted.
The target is always a SIDECAR. A fixed-port form was written and removed the same day: nothing stops a
manifest naming a port it does not own, so `{ port: 5432 }` plus a grant would be a raw TCP relay onto
Postgres for anyone that plugin reaches. Verifying the host is loopback is not verifying the port belongs
to the plugin, and only the second question matters.
A sidecar's port is different in kind, not degree: the platform watched it arrive at registration, so the
association is observed rather than asserted. It is also the SAME port the sidecar serves its HTTP on —
one announcement, one listener, `/ws` alongside the routes. That is what the relay's move to
websocket-to-websocket bought, and why a plugin wanting a socket no longer has to choose between having
one and having ordinary routes.
**Host is always loopback and is not configurable.** A plugin socket that could name an arbitrary host
would be a request-forgery primitive handed to every plugin author and reachable by anyone the plugin is
granted to. There is no case for it that a sidecar does not already serve.
*/
export type PluginWsTarget = { host: string; port: number };
export async function pluginWsTargetFor(appName: string): Promise<PluginWsTarget | null> {
const { states } = await snapshotPlugins();
// Installed, not merely present on disk. A plugin directory that exists but is not installed has no
// routes and no permission, so it must have no socket either.
const state = states.find((st) => st.plugin.appName === appName && st.install);
const socket = state?.plugin.manifest.websocket;
if (!socket) return null;
// A sidecar that has not announced its port yet is indistinguishable from one that never will, and both
// are correctly a refusal — there is nothing to relay onto. The caller says "not accepting connections",
// which is true either way.
const port = sidecarPort(socket.sidecar);
return port ? { host: '127.0.0.1', port } : null;
}