feat: collapse plugin runtime to one tier behind a plugin SDK

This commit is contained in:
LukeGus committed 2026-09-22 14:11:50 -05:00
1 parent 6e058f6e9e
commit 4758016f41
96 files changed
+34631 -8551

No files matched your search

+18 -8
View File
@@ -11,20 +11,30 @@ export interface PluginContributions {
tabs?: PluginTabContribution[];
}
/**
* What GET /plugins returns.
*
* The admin-only fields are absent for a caller without
* admin.plugins.manage: what a plugin may do, what it has been granted and
* why it failed are operational details the shell does not need.
*/
export interface PluginSummary {
id: string;
name: string;
version: string;
tier: string;
source: string;
enabled: boolean;
runtimeState: string;
lastError: string | null;
/** enabled | disabled | blocked | failed, or the loader's live state. */
state: string;
contributes: PluginContributions | null;
/** Capabilities this plugin's manifest declares it may ask for. */
permissions: string[];
/** The subset of `permissions` an admin has actually granted. */
grantedCapabilities: string[];
tier?: string;
source?: string;
/** Capabilities this plugin's manifest declares. Admins only. */
capabilities?: string[];
/** The subset of `capabilities` actually granted. Admins only. */
grantedCapabilities?: string[];
/** Why the plugin is blocked or failed. Admins only. */
lastError?: string | null;
}
export async function getPlugins(): Promise<PluginSummary[]> {
+89 -25
View File
@@ -3988,31 +3988,7 @@
"pluginPermissionGranted": "Granted",
"pluginPermissionNotGranted": "Not granted",
"pluginGrantFailed": "Failed to grant the capability",
"pluginRevokeFailed": "Failed to revoke the capability",
"pluginPermissionHostsRead": "Read hosts",
"pluginPermissionHostsReadDesc": "See the list of your hosts and their non-secret details.",
"pluginPermissionHostsWrite": "Modify hosts",
"pluginPermissionHostsWriteDesc": "Create, edit or delete hosts on your behalf.",
"pluginPermissionCredentialsUse": "Use stored credentials",
"pluginPermissionCredentialsUseDesc": "Connect using a saved credential without you retyping it.",
"pluginPermissionSshExec": "Run commands over SSH",
"pluginPermissionSshExecDesc": "Open a connection to a host and run commands on it.",
"pluginPermissionSshSftp": "Transfer files over SFTP",
"pluginPermissionSshSftpDesc": "Read or write files on a host over SFTP.",
"pluginPermissionStorageOwn": "Store its own data",
"pluginPermissionStorageOwnDesc": "Save and read back its own settings and data.",
"pluginPermissionStorageSecrets": "Store secrets",
"pluginPermissionStorageSecretsDesc": "Save and read back sensitive values it manages itself.",
"pluginPermissionNetworkOutbound": "Make outbound network requests",
"pluginPermissionNetworkOutboundDesc": "Contact services outside this server.",
"pluginPermissionEventsRead": "Subscribe to server events",
"pluginPermissionEventsReadDesc": "React to things happening elsewhere in Termix.",
"pluginPermissionNotifySend": "Send notifications",
"pluginPermissionNotifySendDesc": "Deliver a notification through your configured channels.",
"pluginPermissionUsersRead": "Read user accounts",
"pluginPermissionUsersReadDesc": "See basic details about accounts on this server.",
"pluginPermissionProcessSidecar": "Run a background process",
"pluginPermissionProcessSidecarDesc": "Start and manage its own helper process."
"pluginRevokeFailed": "Failed to revoke the capability"
},
"newUi": {
"sidebar": {
@@ -5136,5 +5112,93 @@
"isolationUnavailable": "This browser cannot isolate web endpoints from your Termix session. Use a browser with credentialless iframe support.",
"isolationNotice": "Embedded pages use an isolated session. For pages requiring storage or popup sign-in, use an isolated window in the desktop app.",
"openIsolatedWindow": "Open isolated window"
},
"plugins": {
"capabilities": {
"credentials:read": {
"title": "See your passwords and keys",
"consequence": "It can read the stored secret for any host you can reach."
},
"ssh:connect": {
"title": "Run commands on your servers",
"consequence": "Any command, on hosts you connect it to, with your access."
},
"process:spawn": {
"title": "Run programs on this machine",
"consequence": "Programs run on the Termix server itself, not on your hosts."
},
"users:write": {
"title": "Create and change user accounts",
"consequence": "It can add users and change their roles."
},
"auth:provide": {
"title": "Handle sign in",
"consequence": "It can add a login method or a second factor."
},
"system:tls": {
"title": "Manage certificates",
"consequence": "It can request and replace the server certificate."
},
"hosts:write": {
"title": "Create and change hosts",
"consequence": "It can add hosts and edit the ones you already have."
},
"credentials:use": {
"title": "Connect to your servers",
"consequence": "It cannot see your passwords or keys."
},
"network:outbound": {
"title": "Reach the internet",
"consequence": "It can send requests to outside services."
},
"network:serve": {
"title": "Accept connections",
"consequence": "It opens a port on the Termix server."
},
"users:read": {
"title": "See usernames and roles",
"consequence": "Never password hashes, never two factor state."
},
"events:core": {
"title": "Watch everything happening",
"consequence": "Including activity from other users, not just yours."
},
"notify:send": {
"title": "Send notifications",
"consequence": "Through the channels you have already set up."
},
"audit:read": {
"title": "Read the audit log",
"consequence": "It can see who did what and when."
},
"desktop:window": {
"title": "Open desktop windows",
"consequence": "Only in the Termix desktop app."
},
"hosts:read": {
"title": "See your host list",
"consequence": "Names and addresses for hosts you can already see."
},
"db:own": {
"title": "Store its own data",
"consequence": "Kept in its own tables, separate from other plugins."
},
"kv:own": {
"title": "Remember small settings",
"consequence": "Kept separate from other plugins."
},
"secrets:own": {
"title": "Store its own secrets",
"consequence": "Encrypted by Termix. The plugin never holds the key."
},
"settings:read-core": {
"title": "Read Termix settings",
"consequence": "Server settings, not your hosts or credentials."
},
"ui:surface": {
"title": "Add its own screens",
"consequence": "Tabs, panels and settings you can hide later."
}
}
}
}
+3 -5
View File
@@ -81,18 +81,16 @@ export function AdminPluginsSection({
key={plugin.id}
label={plugin.name}
badge={
plugin.tier === "first-party"
? t("admin.pluginBuiltIn")
: undefined
plugin.tier === "bundled" ? t("admin.pluginBuiltIn") : undefined
}
description={
plugin.lastError
? plugin.lastError
: `v${plugin.version} · ${plugin.runtimeState}`
: `v${plugin.version} · ${plugin.state}`
}
>
<div className="flex items-center gap-2">
{plugin.permissions.length > 0 && (
{(plugin.capabilities?.length ?? 0) > 0 && (
<Button
variant="outline"
size="sm"
+12 -76
View File
@@ -15,83 +15,18 @@ import {
type PluginSummary,
} from "@/api/plugins-api";
import { AdminToggle } from "./AdminSettingsShared";
import { CAPABILITY_CATALOG } from "@termix/plugin-sdk/capabilities";
/**
* Every permission a plugin's manifest can declare, with the label and
* one-line description shown here. process:transport-owner is left out on
* purpose: it is reserved for first-party plugins and enforced by a hardcoded
* allowlist, not something an admin grants, so showing a toggle for it would
* be misleading. ui.* permissions describe what a plugin contributes to the
* shell rather than a capability the broker gates, but they are still shown
* here since the manifest can declare them and a plugin's grant list should
* not silently hide part of what it asked for.
* The catalog is the list. It is ordered worst first, so the riskiest thing a
* plugin asked for is the first row an admin reads rather than something they
* have to scroll for.
*/
const LABELED_PERMISSIONS: Array<{
capability: string;
labelKey: string;
descriptionKey: string;
}> = [
{
capability: "hosts.read",
labelKey: "admin.pluginPermissionHostsRead",
descriptionKey: "admin.pluginPermissionHostsReadDesc",
},
{
capability: "hosts.write",
labelKey: "admin.pluginPermissionHostsWrite",
descriptionKey: "admin.pluginPermissionHostsWriteDesc",
},
{
capability: "credentials.use",
labelKey: "admin.pluginPermissionCredentialsUse",
descriptionKey: "admin.pluginPermissionCredentialsUseDesc",
},
{
capability: "ssh.exec",
labelKey: "admin.pluginPermissionSshExec",
descriptionKey: "admin.pluginPermissionSshExecDesc",
},
{
capability: "ssh.sftp",
labelKey: "admin.pluginPermissionSshSftp",
descriptionKey: "admin.pluginPermissionSshSftpDesc",
},
{
capability: "storage.own",
labelKey: "admin.pluginPermissionStorageOwn",
descriptionKey: "admin.pluginPermissionStorageOwnDesc",
},
{
capability: "storage.secrets",
labelKey: "admin.pluginPermissionStorageSecrets",
descriptionKey: "admin.pluginPermissionStorageSecretsDesc",
},
{
capability: "network.outbound",
labelKey: "admin.pluginPermissionNetworkOutbound",
descriptionKey: "admin.pluginPermissionNetworkOutboundDesc",
},
{
capability: "events.read",
labelKey: "admin.pluginPermissionEventsRead",
descriptionKey: "admin.pluginPermissionEventsReadDesc",
},
{
capability: "notify.send",
labelKey: "admin.pluginPermissionNotifySend",
descriptionKey: "admin.pluginPermissionNotifySendDesc",
},
{
capability: "users.read",
labelKey: "admin.pluginPermissionUsersRead",
descriptionKey: "admin.pluginPermissionUsersReadDesc",
},
{
capability: "process.sidecar",
labelKey: "admin.pluginPermissionProcessSidecar",
descriptionKey: "admin.pluginPermissionProcessSidecarDesc",
},
];
const LABELED_PERMISSIONS = CAPABILITY_CATALOG.map((capability) => ({
capability: capability.id,
labelKey: capability.titleKey,
descriptionKey: capability.consequenceKey,
}));
export function PluginPermissionsDialog({
plugin,
@@ -109,10 +44,11 @@ export function PluginPermissionsDialog({
if (!plugin) return null;
const declared = plugin.capabilities ?? [];
const rows = LABELED_PERMISSIONS.filter((row) =>
plugin.permissions.includes(row.capability),
declared.includes(row.capability),
);
const granted = new Set(plugin.grantedCapabilities);
const granted = new Set(plugin.grantedCapabilities ?? []);
const toggle = async (capability: string, isGranted: boolean) => {
setBusy(capability);
+3 -3
View File
@@ -28,10 +28,10 @@ function plugin(overrides: Partial<PluginSummary> = {}): PluginSummary {
id: "ssh-terminal",
name: "SSH Terminal",
version: "1.0.0",
tier: "first-party",
tier: "bundled",
source: "bundled",
enabled: true,
runtimeState: "active",
state: "active",
lastError: null,
contributes: {
tabs: [
@@ -43,7 +43,7 @@ function plugin(overrides: Partial<PluginSummary> = {}): PluginSummary {
},
],
},
permissions: [],
capabilities: [],
grantedCapabilities: [],
...overrides,
};