feat: finish the docker plugin (B15)

This commit is contained in:
LukeGus committed 2026-09-24 11:58:20 -05:00
1 parent 0a3f05d99d
commit 993a91c899
167 files changed
+26299 -5136

No files matched your search

+48 -11
View File
@@ -248,10 +248,18 @@ not merely unrouted: a terminal session must not outlive the plugin that owns it
authenticated it, for a library that insists on owning its own WebSocketServer.
guacamole-lite is why it exists. Three routes are public because they
authenticate themselves in a way core cannot: the terminal (a share-link guest
arrives with a share token), the Docker console (per-message auth), and the
arrives with a share token), the Docker console (it refuses a socket without a signed-in user holding
`docker.use`, and re-checks the data key per message), and the
Guacamole display (a single-use encrypted connection token in the query, minted
by an authenticated route).
**B15** moved the Docker console off `ctx.ws.upgrade` and its own
`WebSocketServer` onto `ctx.ws.route` with `public` and `optionalAuth`: core
resolves the caller's token, the handler closes a socket without a user or
without `docker.use`, and it checks `isDataUnlocked()` on every connect
message. Nothing is left at module scope, so disable, enable while a console
is open closes it and the next enable serves new ones.
**B9** added `optionalAuth` for a public route: core still verifies a token
when the client sent one and hands the handler that user, else an empty id.
The terminal uses it, because one socket serves both signed-in users and
@@ -348,7 +356,9 @@ plugin id in `src/ui` outside tests, apart from the entries in
`scripts/shell-plugin-id-allowlist.json`, each with a reason (host data like
the `docker` runtime name, which Phase B moves). **B14** removed the `rdp`,
`vnc` and `telnet` entries: the shell learns those protocols from
`app.registerHostProtocol`.
`app.registerHostProtocol`. **B15** removed both `docker` entries: the Hosts
panel's Docker filter and the bulk Docker toggles went with the column, and
the host editor no longer carries a Proxmox default that spelled `docker`.
#### The loader
@@ -581,7 +591,10 @@ stopgap in the right place: D1 turns them into typed bridge members.
**B16** added `LineChart`, `useAdaptivePolling`, `useAreaPreferences` (the
Appearance density and chart options), and the homepage widget pieces
`WidgetTitle` and `runVisibleInterval`, for host-metrics's cards and its
metrics chart widget. The frontend SDK gained `useHostStatus(hostId)`: core's
metrics chart widget. **B15** added `useUiPreferencesContext`, whose `setOverride` the
Docker manager uses to remember the card or table layout the user picked.
The `docker` and `hostMetrics` areas are still declared in core's
`types/ui-preferences.ts`; a plugin cannot contribute its own area yet. The frontend SDK gained `useHostStatus(hostId)`: core's
status for a host, from the shell's own polling, or null outside the shell.
**B14** added `buildOriginWsUrl`, `getBasePath`, `resolveRemoteHostId` and the
`ConnectionStage` type, for remote desktop's display socket and its remote
@@ -672,6 +685,17 @@ user. Recording files live
under `ctx.files.dataDir()/session_logs/<user>/<session>.cast`. Retention
(`retentionDays`, an admin setting) runs a sweep at boot and every 24 hours.
**B15**'s docker plugin provides `docker.containers` (`listContainers`,
`action`) and `docker.events` (`subscribe(hostId, listener)`), both gated on
`docker.use`. The service name needs a dot, so the plain `docker` the step
was sketched with became `docker.containers`. `docker.events` polls each
subscribed host about once a minute as the subscribing user and diffs the
snapshots; a subscription is idempotent, which is how automations picks it
back up after docker is disabled and enabled again (it re-subscribes every
watched host each minute). Automations uses both as optional services: its
Docker step goes through `action`, and its `docker_event` trigger through
`subscribe`.
A service call runs a permission check for its actor, so a share-link guest,
who has no user, cannot make one. Guest link resolution is therefore
published on `ctx.registry` as `sessions.sharing.guests` instead: the token is
@@ -802,6 +826,11 @@ missing service with an empty handle, like the runtime, instead of throwing. **B
before), so `provide`/`consume`/`revoke` round-trip the way the real registry
does. `plugins/workspaces/tests/backend/helpers.ts` is the worked example.
The doubles' `wsRoutes` (**B15**) record each route's `handler` (or the
`upgrade` callback) and `options`, so a test can hand a route a fake socket;
docker's `tests/backend/console.test.ts` opens a console, disposes the plugin
and activates it again that way.
`renderWithApp` also takes `api` (a stub for `app.api` and `usePluginApi()`),
`layout` (what `app.tabs.getLayout()` returns) and `ready` (fire
`app.tabs.onReady`). `app.tabs.applyLayout` is recorded in `shellCalls` and runs
@@ -1366,8 +1395,14 @@ connect pipeline instead of importing ssh2 helpers from core:
a chain has no single resolved host of its own.
- `prepare(host, { client, purpose })`, `openTransport`,
`classifyKeyboardInteractive` and `autoResponses` are the lower level for a
transport with its own prompt flow (docker's console, host metrics, **B6**'s
interactive file-manager connect with its TOTP/Warpgate parking flow).
transport with its own prompt flow (**B6**'s interactive file-manager
connect with its TOTP/Warpgate parking flow).
- **B15** showed the higher level covers a multi-request HTTP handshake too:
the Docker panel passes `connect` a `prompt` channel whose `ask` answers the
waiting request with what to ask (a code, a Warpgate sign-in, credentials)
and resolves when the next request (`connect-totp`, `connect-warpgate`)
brings the answer. A retry TOTP round re-asks instead of failing. docker
has no auth code of its own left.
- `requiresSecret(authType)` and `supportsBackground(authType)` ask the
provider.
- **B6** added `"file-manager"` and `"file-transfer"` to `PluginSshPurpose`
@@ -2050,8 +2085,8 @@ Then, with the app running:
## Legacy core imports: the debt D1 removes
The bundled plugins predate the SDK, apart from workspaces (A9), snippets
(B2), remote-desktop (B14) and host-metrics (B16), which import nothing from
core. The others still reach core by relative
(B2), remote-desktop (B14), docker (B15) and host-metrics (B16), which import
nothing from core. The others still reach core by relative
path (`../../../../src/backend/...`), which an esbuild plugin,
`packages/plugin-sdk/cli/lib/legacy-core-imports.mjs`, keeps out of the bundle
and rewrites to the compiled output path (`../../../backend/backend/...`, or
@@ -2078,8 +2113,8 @@ What the lint fence enforces today, in `eslint.config.mjs`:
| Core importing a plugin backend | **Error** | 0 | - |
| A plugin backend importing frontend code or `@/` | **Error** | 0 | - |
| The shell importing plugin code | **Error** | 0 | - |
| A plugin frontend importing core through `@/` | Warning | 84 files | D1 |
| A plugin importing core by relative path | Warning | 29 files | D1 |
| A plugin frontend importing core through `@/` | Warning | 70 files | D1 |
| A plugin importing core by relative path | Warning | 24 files | D1 |
| A plugin importing another plugin's source | Warning | 1 file | B18 |
A warning does not fail a build, so the counts are held by
@@ -2119,8 +2154,10 @@ Known specifics:
command history panel into ssh-terminal, which imports nothing from core.
The file manager's terminal window renders the `terminal.view` component
instead of importing the core terminal.
- Host data columns (`enableDocker` and so on) stay in core types until
Phase B moves them. Only UI branches on them left the shell. **B14** moved
- Host data columns (`enableFileManager` and so on) stay in core types until
Phase B moves them. Only UI branches on them left the shell. **B15** moved
docker's `enableDocker` and `docker_config` (now `containerRuntime`) into
its host settings. **B14** moved
remote desktop's (`enableRdp`, `enableVnc`, `enableTelnet`, the three ports,
`rdpSecurity`, `rdpIgnoreCert`, `guacamoleConfig`) into its host settings;
the protocol logins stay core host fields.
+38 -5
View File
@@ -80,7 +80,8 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite
columns, so none of them can see a plugin's host settings. For tunnels the
"Tunnel" filter and the bulk enable/disable entries were removed, and the
export's "Tunnels" group became "Proxy" (SOCKS5 fields only) since
`tunnelConnections` there is now stale. Bringing these back needs a way for
`tunnelConnections` there is now stale. **B15** removed the "Docker" filter
and the bulk Docker toggles the same way. Bringing these back needs a way for
core to read and write host-scope plugin settings generically (keyed off
`contributes.settings.host` or `hostCapability`) rather than per column.
Owner: D1.
@@ -88,9 +89,9 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite
`logActivity` is a core `@/main-axios` call and the plugin imports nothing
from `@/`. Old "tunnel" entries still reopen the tab through the plugin's
`activityTypes`. An `app.logActivity` (or equivalent bridge member) would
restore it for this plugin and let docker and file-manager drop their own
`@/main-axios` import for it too (host-metrics uses the `logActivity` the
ui entry exports). Owner: D1.
restore it for this plugin and let file-manager drop its own
`@/main-axios` import for it too (host-metrics and docker use the
`logActivity` the ui entry exports). Owner: D1.
- **B7 (tunnels):** on the desktop app, tunnel statuses from a connected
remote server are no longer merged into the tab: the old code polled the
remote tunnel service through `getRemoteTunnelApi()`, and the SDK has no
@@ -289,7 +290,8 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite
Their drizzle migrations are `SELECT 1;` on purpose (the copy runs after
drizzle on Postgres and MySQL). Drop them physically in 3.0.0, once every
install has booted 2.9.0. The legacy `guac_enabled` and `guac_url` rows in
`settings` are left too. Owner: 3.0.0.
`settings` are left too. **B15** did the same with `ssh_data.enable_docker`
and `docker_config` (copied by `docker-settings-migration.ts`). Owner: 3.0.0.
- **B14, found in B5 (proxmox):** `drizzle/postgres/0036_redundant_ender_wiggin.sql`
and `drizzle/mysql/0035_clumsy_leech.sql` really `DROP COLUMN` the four
proxmox host columns. On Postgres and MySQL that runs at boot before
@@ -371,6 +373,31 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite
gets "tunnels.start is not a function". Same cause as the tunnels line
above: since B12 `ctx.services.get` hands back an empty handle instead of
throwing, so the step has to check `typeof start`. Owner: B17 or D0.
- **B15 (docker), for D2:** the console socket `/plugin-ws/docker/console`
is a public route with `optionalAuth`. The handler refuses a socket with no
signed-in user or without `docker.use` and checks the data key per connect
message, but review it with the rest of the public routes.
- **B15 (docker):** `ssh_data.show_docker_in_sidebar` (and its siblings for
terminal, files, tunnels and server stats) are dead columns nothing reads;
only the raw database export still copies them. Drop them the B14 way.
Owner: D0.
- **B15 (docker):** the Docker manager's card or table layout is still an
override on core's Appearance presets (`types/ui-preferences.ts` declares a
`docker` area, as it does `hostMetrics`), reached through the ui entry's
`useAreaPreferences` and `useUiPreferencesContext`. A plugin cannot declare
its own area yet. Owner: D1.
- **B15 (docker):** the raw SQLite database import (`database.ts`) no longer
brings `enable_docker` across, the same gap every earlier column move left
there: it writes host rows only and never plugin settings. Owner: D1, with
the generic host-settings export and import.
- **B15 (docker), for B18:** the AI assistant's `list_hosts`/`get_host`
tools still report `enableDocker` from the host row, which no longer has
the column, so it always reads null. Read `pluginSettings.docker` or ask the
`docker.containers` service instead. Owner: B18.
- **B15 (docker), for B17:** automations' `docker-watcher.ts` still reads its
own rules through core's automation repository and types by relative
import; only the Docker side is on the SDK (`docker.containers` for the
step, `docker.events` for the trigger). Owner: B17.
## Manual checks after 2.9.0
@@ -411,3 +438,9 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite
that only had `connection_type = rdp` comes up as RDP with SSH off; RDP user
defaults apply to a saved host; Quick Connect over RDP and VNC; the desktop
app with a host on the remote server and one on This device.
- Docker: a 2.8 host with Docker on (and one on Podman) keeps its switch and
runtime; list, start, stop, logs, stats and the console on a Linux host;
TOTP (including a wrong code first) and Warpgate on the connect; a host
with no stored password asks for credentials; the homepage Docker widget;
an automation Docker step and a `docker_event` trigger; disable Docker with
a console open, then enable it and open another.
-1
View File
@@ -487,7 +487,6 @@ export interface PluginHostRecord {
enableTerminal?: boolean | null;
enableFileManager?: boolean | null;
enableTunnel?: boolean | null;
enableDocker?: boolean | null;
createdAt?: string | null;
updatedAt?: string | null;
[key: string]: unknown;
+33 -8
View File
@@ -17,7 +17,24 @@ import type {
import type { TermixApp } from "./frontend.js";
import type { PluginManifest } from "./manifest.js";
import type { PluginTableDefinition } from "./db.js";
import type { PluginMiddleware, PluginRouterOptions } from "./backend.js";
import type {
PluginMiddleware,
PluginRouterOptions,
PluginWebSocketHandler,
PluginWebSockets,
PluginWebSocketOptions,
} from "./backend.js";
/** A ctx.ws registration as the test doubles record it. */
export interface FakeWsRoute {
path: string;
raw: boolean;
/** For ctx.ws.route. */
handler?: PluginWebSocketHandler;
/** For ctx.ws.upgrade. */
upgrade?: Parameters<PluginWebSockets["upgrade"]>[1];
options?: PluginWebSocketOptions;
}
import type {
PluginLoginMethod,
PluginSecondFactor,
@@ -115,8 +132,11 @@ export interface FakePluginContext {
syncEntities: SyncEntityRegistration[];
/** Tombstones recorded through ctx.sync.recordTombstone, in order. */
tombstones: Array<{ userId: string; entityType: string; syncId: string }>;
/** WebSocket routes registered through ctx.ws, in order. */
wsRoutes: Array<{ path: string; raw: boolean }>;
/**
* WebSocket routes registered through ctx.ws, in order, with the handler
* and options, so a test can hand a route a fake socket.
*/
wsRoutes: FakeWsRoute[];
/** Router options passed to ctx.http.router, in order. */
httpRouters: Array<PluginRouterOptions | undefined>;
/** Backing store behind ctx.settings, keyed "<scope>:<scopeId>:<key>". */
@@ -237,7 +257,7 @@ export function createFakeContext(
entityType: string;
syncId: string;
}> = [];
const wsRoutes: Array<{ path: string; raw: boolean }> = [];
const wsRoutes: FakeWsRoute[] = [];
const httpRouters: Array<PluginRouterOptions | undefined> = [];
const listeners = new Map<string, Set<(payload: unknown) => void>>();
const settings = new Map<string, unknown>();
@@ -466,11 +486,16 @@ export function createFakeContext(
},
ws: {
route: (path) => {
wsRoutes.push({ path, raw: false });
route: (path, handler, wsOptions) => {
wsRoutes.push({ path, raw: false, handler, options: wsOptions });
},
upgrade: (path) => {
wsRoutes.push({ path, raw: true });
upgrade: (path, handler, wsOptions) => {
wsRoutes.push({
path,
raw: true,
upgrade: handler,
options: wsOptions,
});
},
},