Files
Termix/plugins/ssh-terminal/README.md
T
LukeGus c94a453f1e feat: ship the ssh terminal as a first-party in-process plugin
Adds a hardcoded process:transport-owner tier so the terminal can own its
WebSocket server and ssh2 clients, which cannot cross the worker boundary.
2026-09-21 08:28:50 -05:00

71 lines
3.2 KiB
Markdown

# 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 `WebSocketServer` on port 30002,
- it holds long-lived `ssh2.Client` objects 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.ts` has 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.