a plugin websocket targets a sidecar, never a port a manifest picked

The fixed-port form shipped this morning and lasted about an hour. The owner asked
whether declaring a port was safe, and it is not: nothing stopped a manifest naming
a port it does not own, so `websocket: { port: 5432 }` plus a grant would have been
a raw TCP relay onto Postgres — authenticated by the platform, available to anyone
that plugin was granted to. Port 22 and officer's own port are the same sentence.

The relay checked that the HOST was loopback and I mistook that for having checked
anything about the port. They are different questions and only the second one
matters: loopback is exactly where everything worth reaching already is.

So the only form is `{ sidecar }`. A sidecar's port is trustworthy for a reason a
declaration can never be — the platform watched it arrive at registration, so the
association between a port and a plugin is observed rather than asserted. The same
distinction that decides the delegation problem in todo/agent-coordination: a claim
is not evidence, whoever is making it.

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. It was added as a shortcut for
Minesweeper, whose x11vnc listens on a known port — which is precisely the case
that looked harmless and generalised into a hole.

Nothing had used it. Minesweeper is still whole in the platform and its extraction
had not started.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-23 16:09:03 +00:00
co-authored by Claude Opus 5
parent 452029a6d3
commit f2f3ca12ba
2 changed files with 22 additions and 12 deletions
+6 -8
View File
@@ -9,13 +9,13 @@ import { snapshotPlugins } from '@@/plugins/mount';
line-oriented sidecar without knowing which is which. The moment this returned a scheme or a path, the
relay would have to care what it was carrying.
Two forms, because two things genuinely differ:
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.
{ port: 5900 } a fixed port a plugin's own service listens on — `x11vnc` for a desktop, or
anything the plugin starts itself and knows the number of in advance.
{ sidecar: 'name' } resolved at connect time, because a sidecar binds an EPHEMERAL port and announces
it. A manifest cannot know that number, which is why the indirection exists rather
than being a convenience.
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.
**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
@@ -32,8 +32,6 @@ export async function pluginWsTargetFor(appName: string): Promise<PluginWsTarget
const socket = state?.plugin.manifest.websocket;
if (!socket) return null;
if ('port' in socket) return { host: '127.0.0.1', port: socket.port };
// 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.
+16 -4
View File
@@ -294,14 +294,26 @@ export type PluginManifest = {
* There is deliberately no separate socket permission: granting the plugin grants its socket, and nothing
* can drift out of step because there is only one decision.
*
* `{ port }` for a fixed loopback port the plugin's own service listens on. `{ sidecar }` when the target
* is a sidecar, whose port is ephemeral and announced at registration — a manifest cannot know that
* number, which is the whole reason the indirection exists.
* The target is a SIDECAR, always, and never a port number a manifest picked.
*
* A fixed-port form existed for about an hour and was removed before it shipped anywhere. The hole is
* total: nothing stops a manifest naming a port it does not own, so `{ port: 5432 }` plus a grant is a
* raw TCP relay onto Postgres, authenticated by the platform, for anyone that plugin is granted to. `:22`
* and officer's own port are the same sentence. Checking that the HOST was loopback — which the relay
* does — is not the same as checking the PORT belongs to the plugin, and only the second one matters.
*
* A sidecar's port is trustworthy for a reason a declaration can never be: the platform watched it
* arrive. The sidecar binds an ephemeral port and announces it at registration, so the association
* between "this port" and "this plugin" is something the platform observed rather than something a
* 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.
*
* 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.
*/
websocket?: { port: number } | { sidecar: string };
websocket?: { sidecar: string };
osDependencies?: OsDependency[];
/**