Adds a hardcoded process:transport-owner tier so the terminal can own its WebSocket server and ssh2 clients, which cannot cross the worker boundary.
SSH Terminal
The interactive SSH terminal, packaged as a first-party plugin.
Why this plugin runs in-process
Every other plugin runs in a worker_threads worker and reaches the server
through a structured-clone postMessage boundary. That boundary is what makes
the capability gate and the audit trail mean anything: a worker plugin cannot
hold a db handle, an ssh2 Client or a live socket, so every privileged action
has to go through the broker.
This plugin cannot work that way. It is the SSH transport rather than a consumer of one:
- it owns a
WebSocketServeron port 30002, - it holds long-lived
ssh2.Clientobjects and PTY streams, - it needs plaintext host credentials to connect at all.
None of those can cross a structured-clone boundary, and ctx.hosts returns a
13-field allowlist that deliberately excludes password, key and
keyPassword. So ssh-terminal runs on the main thread instead, under the
process:transport-owner tier.
Membership in that tier is a hardcoded allowlist in
src/backend/plugins/first-party.ts. A manifest cannot ask for it: declaring
process:transport-owner without being on the list is a validation error. The
tier has no capability gate and no audit interception, which is only defensible
because nothing installable can enter it.
What lives where
The terminal's implementation stays in src/backend/hosts/terminal/. Only the
lifecycle lives here. Two reasons it was not copied in:
session-manager.tshas six consumers outside the terminal (terminal routes, open-tabs, user-image-storage, collab, and session-sharing twice). It is shared infrastructure, not plugin-private code.- host key verification (
host-key-verifier.ts), jump host chaining (jump-host-chain.ts) and host resolution (host-resolver.ts) each have around eight consumers already. They keep working by not moving.
The SSH connection pool also stays in core. The terminal never used it: it
calls new Client() directly. The pool's consumers are metrics, tmux, tunnel,
fleet routes, AI tools, automations and ctx.ssh itself, so moving it here
would have made all of them depend on the terminal being enabled.
Only SSHAuthManager is terminal-exclusive.
WebSocket: still port 30002, not /plugin-api/
The terminal WS was deliberately left on its own port and path.
/plugin-api has no auth middleware, so routing terminal auth through it would
be a regression. Beyond that, /ssh/websocket/ is mapped in both nginx configs,
three frontend call sites build that URL (Terminal.tsx, SharedSessionView,
CollabRoomTab), Electron's buildOriginWsUrl pins localPort: 30002, and the
8-attempt reconnect plus detach/reattach buffering all depend on the current
path.
The plugin owns the port's lifecycle instead: enabling it calls
startTerminalServer(), disabling it calls stopTerminalServer(), which closes
live sessions and frees the port.
Disabling
Disabling behaves like any other plugin. No confirmation dialog, no special casing. The terminal tab stops being offered from the host list, command palette and quick connect, the port stops listening, and every other feature (metrics, docker, tunnels, file manager) keeps working off its own connections.