feat: finish the tailscale plugin, including SSH auth (B3)

This commit is contained in:
LukeGus committed 2026-09-23 09:41:20 -05:00
1 parent c77301577f
commit d2ff5fc78f
35 files changed
+1251 -526

No files matched your search

+21 -4
View File
@@ -417,13 +417,21 @@ boundary and Suspense.
Hooks: `useTranslation` (the plugin's namespace), `usePermission` (a short
name resolves to `<id>.<name>`), `useSettings`, `useHost`, `useHosts`,
`useCurrentUser`, `useTheme`, `useToast`, `usePluginApi`, `useTabs`,
`useSshAuthTypes` (every SSH auth type the server knows, with its plugin). The SDK
has no runtime dependencies: the hooks delegate to a host bridge core installs.
`useSshAuthTypes` (every SSH auth type the server knows, with its plugin),
`useSlotContributions` (the visible contributions to a slot another plugin
owns, with their metadata, for a slot owner that builds a catalog from what
was contributed rather than just rendering it in place - host-metrics uses it
to list manager cards plugins added to `host-metrics.managers` next to its
own). The SDK has no runtime dependencies: the hooks delegate to a host
bridge core installs.
Slots core owns: `terminal.toolbar`, `terminal.dock`, `terminal.overlay`
(declared by ssh-terminal), `onboarding.steps`, `onboarding.features`,
`onboarding.workflow`, `hosts.importMenu`, `hosts.panel`, `proxmox.hostEditor`
and `session.remoteDisplay`. Cross-plugin frontend calls go through actions:
and `session.remoteDisplay`. `host-metrics.managers` (component slot,
**B3**) is host-metrics's own: tailscale contributes its manager card there
instead of host-metrics knowing tailscale exists, and host-metrics works with
or without tailscale enabled. Cross-plugin frontend calls go through actions:
`automations.list`, `fleets.list`, `session.remoteDisplay.token`.
**B2** added a live-terminal-session surface, for a plugin that needs to push
@@ -610,6 +618,15 @@ ctx.auth.registerSshAuthProvider({
},
onKeyboardInteractive: (round, host) => null, // claim a prompt style
onAuthFailed: (host, env, context) => undefined, // clear caches, retry once
onBanner: (banner, host, env) => undefined,
// claim an ssh2 "banner" event during the handshake, for a server that
// holds the connection open pending an out-of-band step (Tailscale SSH
// check mode). Return { action: "hold", timeoutMs, message, details? } to
// extend the connect timeout and have the terminal send
// "<type>_check_required" with `details` merged in, or
// { action: "release", details? } once the banner says it is done, which
// sends "<type>_check_completed". Undefined leaves the banner unhandled.
// B3 (tailscale) is the only caller today.
startInteraction: async (request) => {}, // start the browser step
});
```
@@ -1539,7 +1556,7 @@ 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 | 107 files | D1 |
| A plugin frontend importing core through `@/` | Warning | 108 files | D1 |
| A plugin importing core by relative path | Warning | 67 files | D1 |
| A plugin importing another plugin's source | Warning | 3 files | B18 |
+19
View File
@@ -0,0 +1,19 @@
# Finish list
Running list of things intentionally left unfinished by a Phase B/C step,
so D0 (or an earlier owner named below) can pick them up. Remove a line once
it's done.
- **B3 (tailscale):** `ManagerCardShell`/`ManagerSearch`-equivalent presentation
is duplicated as `TailscaleManagerShell`/`TailscaleManagerSearch` in
`plugins/tailscale/src/frontend/TailscaleManagerShell.tsx` rather than
shared with host-metrics's originals. Promote a shared version into
`@termix/plugin-sdk/ui` once a second manager-card plugin needs one, per the
contract's "add to the SDK when a second plugin needs it" rule. Owner: D1.
- **B3 (tailscale):** `plugins/tailscale/src/backend/host-metrics-manager.ts`
and `routes.ts` still resolve hosts and access through core's
`PermissionManager`/`resolveHostById`/`DataCrypto` by relative import
instead of a `ctx.hosts`-style SDK API, because `ctx.hosts` is not built yet
(status **B** in the ctx surface table). Same debt every other host-metrics
manager already carries; not new, but tailscale now carries it too. Owner:
whichever step builds `ctx.hosts`.
+26
View File
@@ -448,6 +448,20 @@ export type PluginSshAuthOutcome =
message: string;
};
export type PluginSshBannerDecision =
| {
/** Hold the connect timeout open and wait for the out-of-band step. */
action: "hold";
timeoutMs: number;
message: string;
details?: Record<string, unknown>;
}
| {
/** The out-of-band step finished; resume the normal connect timeout. */
action: "release";
details?: Record<string, unknown>;
};
export interface PluginKeyboardInteractivePrompt {
prompt: string;
echo?: boolean;
@@ -644,6 +658,18 @@ export interface PluginSshAuthProvider {
methodNotAvailable: boolean;
},
) => PluginSshAuthOutcome | undefined;
/**
* Claims an ssh2 "banner" event during the handshake, for a server that
* holds the connection open pending an out-of-band step (Tailscale SSH
* check mode). A transport that can show this sends `<type>_check_required`
* / `<type>_check_completed` over its socket, with `details` merged in.
* Return nothing to leave the banner unhandled.
*/
onBanner?: (
banner: string,
host: PluginSshHost,
env: PluginSshAuthEnv,
) => PluginSshBannerDecision | undefined;
/** Starts the browser step behind an interaction-required outcome. */
startInteraction?: (request: {
userId: string;
+12
View File
@@ -610,6 +610,7 @@ export interface PluginHostBridge {
useTabs: () => TabsApi;
invokeAction: (id: string, ...args: unknown[]) => Promise<unknown>;
useSshAuthTypes: () => { types: SshAuthTypeInfo[]; loaded: boolean };
useSlotContributions: (slotId: string) => SlotContribution[];
}
let host: PluginHostBridge | null = null;
@@ -712,6 +713,17 @@ export function useSshAuthTypes(): {
return requireHost().useSshAuthTypes();
}
/**
* The visible contributions to a slot another plugin owns, with their
* metadata (id, titleKey, component). For an owner that needs to build a
* catalog from what was contributed rather than just render it in place, e.g.
* host-metrics listing manager cards plugins added to "host-metrics.managers"
* next to its own. Same permission and `when` filtering as ComponentSlot.
*/
export function useSlotContributions(slotId: string): SlotContribution[] {
return requireHost().useSlotContributions(slotId);
}
/**
* Runs an action another plugin registered, from anywhere in a plugin's code.
* Resolves undefined when nobody registered it, so an optional dependency
+1
View File
@@ -111,6 +111,7 @@ const offline = () =>
})),
loaded: true,
}),
useSlotContributions: () => [],
};
afterEach(() => {