@termix-ssh/plugin-sdk 1.0.22 → 1.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/PLUGIN-GUIDE.md +69 -1
  3. package/dist/backend.d.ts +107 -0
  4. package/dist/capabilities.js +3 -0
  5. package/dist/frontend.d.ts +19 -0
  6. package/dist/host/{PluginsTab-CM2B33BD.js → PluginsTab-MNQU5OJ3.js} +4 -4
  7. package/dist/host/{chunk-HQ6NNFSX.js → chunk-5W6F6GJJ.js} +68 -3
  8. package/dist/host/{chunk-P26QKKU5.js → chunk-6WLECSNB.js} +35 -5
  9. package/dist/host/{chunk-ROB2SH5Q.js → chunk-C52NRTA6.js} +9 -5
  10. package/dist/host/{chunk-VKNGNEF3.js → chunk-DN6YKMFJ.js} +1 -1
  11. package/dist/host/{chunk-TK3Y437H.js → chunk-EKXGDM5P.js} +1 -1
  12. package/dist/host/{chunk-JRV2HIDK.js → chunk-GRH7GSKG.js} +1 -1
  13. package/dist/host/{chunk-7J4QNGFM.js → chunk-IMPLAAUM.js} +1 -1
  14. package/dist/host/{chunk-I5F2PSMR.js → chunk-RWFXGG4Q.js} +9 -9
  15. package/dist/host/core-classes.json +1 -1
  16. package/dist/host/frame-host.css +1 -1
  17. package/dist/host/frame-host.js +5 -5
  18. package/dist/host/{linked-server-J5G457OA.js → linked-server-YCAJ2NX3.js} +1 -1
  19. package/dist/host/{rbac-api-PY22W5UY.js → rbac-api-A64TXDL5.js} +1 -1
  20. package/dist/host/testing-host.js +10 -6
  21. package/dist/host/types/src/ui/lib/db-health-monitor.d.ts +5 -0
  22. package/dist/host/types/src/ui/lib/plugin-transport.d.ts +5 -0
  23. package/dist/host/types/src/ui/plugin-host/sdk-ui.d.ts +4 -0
  24. package/dist/host/types/src/ui/shell/HostTabEmbed.d.ts +15 -0
  25. package/dist/host/types/src/ui/shell/host-sections.d.ts +25 -0
  26. package/dist/host/ui.js +51 -9
  27. package/dist/host/{use-permissions-EWRI3WLT.js → use-permissions-FLQAN6I7.js} +1 -1
  28. package/dist/manifest.d.ts +6 -1
  29. package/dist/manifest.js +7 -1
  30. package/dist/operations.d.ts +222 -0
  31. package/dist/operations.js +650 -0
  32. package/dist/testing.d.ts +9 -1
  33. package/dist/testing.js +45 -0
  34. package/package.json +5 -1
  35. package/schema/manifest.schema.json +25 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.24
4
+
5
+ Plugin API 1.14.0.
6
+
7
+ ### Added
8
+
9
+ - `app.registerHostSection` adds a card to the host page
10
+ - `/ui` exports `HostTabEmbed`, which draws any tab type inside another view, the host section registry, and `operationsApi` for core's `/operations` routes
11
+
12
+ ## 1.0.23
13
+
14
+ Plugin API 1.14.0.
15
+
16
+ ### Added
17
+
18
+ - `contributes.operations` declares a plugin's actions, events, resources and signals, so Automations, Fleets, the assistant, the CLI, the phone app and the command palette can find them with no wiring
19
+ - `ctx.operations` registers handlers, runs other plugins' declared actions, emits events and provides approval rules, with the new `operations:provide`, `operations:invoke` and `operations:approve` capabilities
20
+ - `@termix-ssh/plugin-sdk/operations` has the types, input checks, redaction and JSON Schema for declared actions
21
+ - `contributes.hostSections` declares cards for the host page
22
+
3
23
  ## 1.0.22
4
24
 
5
25
  Plugin API 1.13.0.
package/PLUGIN-GUIDE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Termix plugin guide
2
2
 
3
- SDK 1.0.22 | Plugin API 1.13.0
3
+ SDK 1.0.24 | Plugin API 1.14.0
4
4
 
5
5
  This guide is for anyone, person or AI coding tool, building a Termix plugin. It ships inside
6
6
  `@termix-ssh/plugin-sdk`, so the copy in your `node_modules` always matches the SDK you have installed.
@@ -414,6 +414,70 @@ ctx.schedule.every(60_000, () => refresh(), { jitterMs: 5_000 });
414
414
  - Use a service: declare it in `requires`, then `ctx.services.get<T>("snippets.access")`. It returns
415
415
  nothing while no provider runs, so always handle that.
416
416
 
417
+ ### Operations
418
+
419
+ Declare what your plugin can do in `contributes.operations`, and it shows up in Automations, Fleets, the
420
+ assistant, the CLI, the phone app and the command palette with no other wiring. It also gets per host results,
421
+ dry runs, run history, approvals and drift checks.
422
+
423
+ ```json
424
+ "capabilities": ["operations:provide"],
425
+ "engine": { "termix": ">=26.11.1-0", "api": "^1.13" },
426
+ "contributes": {
427
+ "operations": {
428
+ "actions": [{
429
+ "id": "demo.restart",
430
+ "titleKey": "ops.restart.title",
431
+ "scope": "host",
432
+ "inputs": [{ "key": "name", "type": "string", "labelKey": "ops.restart.name", "required": true }],
433
+ "risk": "change",
434
+ "permission": "use",
435
+ "hostAccess": "edit",
436
+ "dryRun": "describe"
437
+ }]
438
+ }
439
+ }
440
+ ```
441
+
442
+ ```ts
443
+ ctx.operations.register("demo.restart", {
444
+ describe: (input) => `Would restart ${input.name}`,
445
+ run: async (input, { hostId, signal, log }) => {
446
+ return { summary: `Restarted ${input.name}`, changed: true };
447
+ },
448
+ });
449
+ ```
450
+
451
+ - Actions: `scope` is `host`, `resource` or `server`. `risk` is `read`, `change` or `destructive`. Host actions declare
452
+ `hostAccess`. Use `edit` for changes unless people with `connect` already do this today. A destructive action
453
+ must use `edit` or `manage` and needs a confirm.
454
+ - Inputs use the settings field types, plus `required`, `pattern`, `optionsFrom` (a read action fills the choices)
455
+ and type `resource`. Keys you did not declare are refused before your handler runs. `secret` values reach the handler
456
+ but are never stored or shown. Mark private outputs `sensitive`.
457
+ - `dryRun`: `native` calls `run` with `dryRun: true` and nothing may change. `describe` calls `describe` instead.
458
+ - `check` names a read action that proves the result, used for drift. `undo` names the reverse action, and `run` returns
459
+ `undoInput`. `idempotent` lets a rollout retry it. `exclusive` allows one at a time per host. `when` limits it to
460
+ hosts by setting or OS.
461
+ - `surfaces`: the assistant and MCP are off until you set them true. Only do that for actions safe for a model to
462
+ ask for. The rest are on unless you set them false.
463
+ - Resources describe a kind of thing, such as a container: a read action on one host returns `{ rows }` as `data`, and
464
+ core draws the All hosts table and host page section. Attach actions with `scope: "resource"`.
465
+ - Signals are status tiles: a read action returns `{ value, status, label }` as `data`, with status `ok`, `warn`, `fail` or
466
+ `unknown`. `fix` names the action offered when it is not ok. Keep these reads cheap, they run for every host.
467
+ - Events: declare them, then call `ctx.operations.emit(id, payload, { hostId })`.
468
+ - Handlers must watch `signal` and stop when it fires. Throw on failure; a failed run never counts against your
469
+ plugin's error budget. Throw an error with `code = "NEEDS_INTERACTION"` when a host needs a person to sign in first.
470
+ - Handlers have no request and no session. Take what you need from the input and `hostId`.
471
+ - Keep your REST routes. Inside one, call `ctx.operations.run(id, { hostId, input })` so a button press goes through
472
+ the same checks and lands in the same history.
473
+ - To run another plugin's action, add `operations:invoke`, list it in `operations.invokes` and call
474
+ `ctx.operations.invoke(id, { hostId, input, via: "automation" })`. It acts as the current user and rules still apply.
475
+ - `risk` is your plugin's claim. Plugins run in-process, so this is not a sandbox. Actions from plugins not signed as
476
+ official count as `change` when rules decide what needs a person, and stay off the assistant and MCP until an admin
477
+ allows them.
478
+ - Set `engine.termix` to at least `>=26.11.1-0`. Keep `engine.api` as it was unless you import something new from
479
+ `/ui`, or the phone app will ask people to update to see your plugin.
480
+
417
481
  ### Shared packages
418
482
 
419
483
  These come from Termix and are never bundled: `@termix-ssh/plugin-sdk`, `express`, `ssh2`, `ws`, `multer`,
@@ -962,6 +1026,7 @@ To run your own registry instead, see https://docs.termix.site/develop/registrie
962
1026
  | `@termix-ssh/plugin-sdk/db` | `defineTable` and column builders |
963
1027
  | `@termix-ssh/plugin-sdk/manifest` | `PluginManifest` type and manifest checks |
964
1028
  | `@termix-ssh/plugin-sdk/capabilities` | The capability catalog |
1029
+ | `@termix-ssh/plugin-sdk/operations` | Types, input checks and JSON Schema for declared actions |
965
1030
  | `@termix-ssh/plugin-sdk/settings` | Settings field validation |
966
1031
  | `@termix-ssh/plugin-sdk/testing` | `createMockCtx`, `createTestDb`, `renderWithApp`, `createFakeContext` |
967
1032
  | `@termix-ssh/plugin-sdk/vitest-preset` | `pluginVitestConfig` |
@@ -993,6 +1058,8 @@ To run your own registry instead, see https://docs.termix.site/develop/registrie
993
1058
  | `desktop:local` | high | Use the user's own computer, desktop app only |
994
1059
  | `hosts:export` | high | Export hosts with their saved logins |
995
1060
  | `notify:hub` | high | Receive every alert (the alerts hub only) |
1061
+ | `operations:invoke` | high | Run other plugins' declared actions, listed in `invokes` |
1062
+ | `operations:approve` | high | Decide which actions need a person |
996
1063
  | `ui:layout` | high | Replace the whole app layout |
997
1064
  | `hosts:write` | medium | Add and edit hosts |
998
1065
  | `hosts:status-provide` | medium | Be the source of host status dots |
@@ -1008,6 +1075,7 @@ To run your own registry instead, see https://docs.termix.site/develop/registrie
1008
1075
  | `users:read` | medium | Usernames, roles and UI language |
1009
1076
  | `events:core` | medium | Listen to core events |
1010
1077
  | `notify:send` | medium | Send alerts |
1078
+ | `operations:provide` | medium | Offer declared actions and events |
1011
1079
  | `audit:read` | medium | Read the audit log |
1012
1080
  | `desktop:window` | medium | Open windows in the desktop app |
1013
1081
  | `hosts:read` | low | Read hosts the user can see |
package/dist/backend.d.ts CHANGED
@@ -13,6 +13,7 @@ import type { HostSshOptions } from "./ssh-options.js";
13
13
  export type { HostSshOptions } from "./ssh-options.js";
14
14
  import type { PluginManifest, PluginSettingsField } from "./manifest.js";
15
15
  import type { PluginTableDefinition } from "./db.js";
16
+ import type { OperationResult, OperationRisk, OperationSurfaces } from "./operations.js";
16
17
  /**
17
18
  * Thrown when a plugin calls a guarded member without the capability.
18
19
  *
@@ -2680,6 +2681,110 @@ export interface PluginSummary {
2680
2681
  export interface PluginPlugins {
2681
2682
  list: () => Promise<PluginSummary[]>;
2682
2683
  }
2684
+ /** What a handler gets besides its input. */
2685
+ export interface PluginOperationContext {
2686
+ hostId?: number;
2687
+ /** The row a resource action targets. */
2688
+ resourceKey?: string;
2689
+ /** Null for a background read. */
2690
+ runId: string | null;
2691
+ /** Nothing may change when this is set. */
2692
+ dryRun: boolean;
2693
+ /** Fires when the run is cancelled, times out or the plugin stops. */
2694
+ signal: AbortSignal;
2695
+ log: (line: string) => void;
2696
+ }
2697
+ export interface PluginOperationHandler {
2698
+ run: (input: Record<string, unknown>, context: PluginOperationContext) => Promise<OperationResult> | OperationResult;
2699
+ /** The dry run text for an action declared with dryRun "describe". */
2700
+ describe?: (input: Record<string, unknown>, context: PluginOperationContext) => string | Promise<string>;
2701
+ }
2702
+ export interface PluginOperationInvokeOptions {
2703
+ hostId?: number;
2704
+ resourceKey?: string;
2705
+ input?: Record<string, unknown>;
2706
+ dryRun?: boolean;
2707
+ /** A person agreed to a destructive action. */
2708
+ confirmed?: boolean;
2709
+ idempotencyKey?: string;
2710
+ batchId?: string;
2711
+ /** What this plugin is doing: automation, assistant, mcp, rollout and so on. */
2712
+ via?: string;
2713
+ }
2714
+ export interface PluginOperationOutcome {
2715
+ runId: string | null;
2716
+ status: string;
2717
+ summary?: string;
2718
+ changed?: boolean;
2719
+ data?: unknown;
2720
+ error?: string;
2721
+ errorCode?: string;
2722
+ approvalId?: string;
2723
+ }
2724
+ export interface PluginOperationApprovalInput {
2725
+ actionId: string;
2726
+ pluginId: string;
2727
+ risk: OperationRisk;
2728
+ /** What rules should use. Plugins not signed as official count as change. */
2729
+ effectiveRisk: OperationRisk;
2730
+ origin: {
2731
+ kind: string;
2732
+ pluginId?: string;
2733
+ via?: string;
2734
+ };
2735
+ userId: string;
2736
+ hostId?: number;
2737
+ hostTags?: string[];
2738
+ batchId?: string;
2739
+ }
2740
+ export interface PluginOperationApprovalDecision {
2741
+ decision: "allow" | "approve" | "deny";
2742
+ reason?: string;
2743
+ allowSelf?: boolean;
2744
+ expiresInMinutes?: number;
2745
+ }
2746
+ export interface PluginOperationEvent {
2747
+ id: string;
2748
+ eventId: string;
2749
+ pluginId: string;
2750
+ hostId?: number;
2751
+ severity?: string;
2752
+ payload?: unknown;
2753
+ runId?: string;
2754
+ depth: number;
2755
+ at: string;
2756
+ }
2757
+ /**
2758
+ * Declared actions and events. See contributes.operations in the manifest.
2759
+ * register and emit need operations:provide. invoke, list and onEvent need
2760
+ * operations:invoke, and invoke only reaches ids in the manifest's invokes list.
2761
+ */
2762
+ export interface PluginOperations {
2763
+ register: (id: string, handler: PluginOperationHandler) => void;
2764
+ /**
2765
+ * Runs one of this plugin's own actions from its route, so a button press
2766
+ * goes through the same checks and lands in the same history.
2767
+ */
2768
+ run: (actionId: string, options?: PluginOperationInvokeOptions) => Promise<PluginOperationOutcome>;
2769
+ invoke: (actionId: string, options?: PluginOperationInvokeOptions) => Promise<PluginOperationOutcome>;
2770
+ list: (options?: {
2771
+ lang?: string;
2772
+ surface?: keyof OperationSurfaces;
2773
+ }) => Promise<unknown>;
2774
+ emit: (eventId: string, payload?: unknown, options?: {
2775
+ hostId?: number;
2776
+ }) => Promise<void>;
2777
+ onEvent: (subscription: {
2778
+ pattern: string;
2779
+ handler: (event: PluginOperationEvent) => void | Promise<void>;
2780
+ userId?: string;
2781
+ cooldownMs?: number;
2782
+ }) => () => void;
2783
+ /** Rules for what needs a person. Needs operations:approve. */
2784
+ approvals: {
2785
+ provide: (provider: (input: PluginOperationApprovalInput) => PluginOperationApprovalDecision | null | undefined | Promise<PluginOperationApprovalDecision | null | undefined>) => () => void;
2786
+ };
2787
+ }
2683
2788
  export interface PluginContext {
2684
2789
  readonly pluginId: string;
2685
2790
  readonly manifest: PluginManifest;
@@ -2720,6 +2825,8 @@ export interface PluginContext {
2720
2825
  readonly schedule: PluginSchedule;
2721
2826
  /** Core's notification channels. Needs notify:send. */
2722
2827
  readonly notify: PluginNotify;
2828
+ /** Declared actions and events, reachable from every surface. */
2829
+ readonly operations: PluginOperations;
2723
2830
  /** Outbound HTTP through core's SSRF guard. Needs network:outbound. */
2724
2831
  readonly fetch: PluginFetch;
2725
2832
  /** Plain reachability checks. Needs network:probe. */
@@ -38,6 +38,8 @@ export const CAPABILITY_CATALOG = [
38
38
  entry("ssh:transport", "high"),
39
39
  entry("device:serial", "high"),
40
40
  entry("notify:hub", "high"),
41
+ entry("operations:invoke", "high"),
42
+ entry("operations:approve", "high"),
41
43
  entry("ui:layout", "high"),
42
44
  entry("hosts:write", "medium"),
43
45
  entry("hosts:export", "high"),
@@ -53,6 +55,7 @@ export const CAPABILITY_CATALOG = [
53
55
  entry("users:read", "medium"),
54
56
  entry("events:core", "medium"),
55
57
  entry("notify:send", "medium"),
58
+ entry("operations:provide", "medium"),
56
59
  entry("audit:read", "medium"),
57
60
  entry("desktop:window", "medium"),
58
61
  entry("desktop:local", "high"),
@@ -643,6 +643,20 @@ export interface HostBadgeContribution {
643
643
  export type HostStatusIndicatorContribution = HostBadgeContribution;
644
644
  /** A control in the host editor's toolbar. */
645
645
  export type HostEditorToolbarItemContribution = HostBadgeContribution;
646
+ export interface HostSectionProps {
647
+ host: PluginHostRecord;
648
+ isVisible: boolean;
649
+ shell: ShellApi;
650
+ }
651
+ export interface HostSectionContribution {
652
+ /** Declare it in manifest contributes.hostSections too. */
653
+ id: string;
654
+ titleKey: string;
655
+ /** Lower sorts first. */
656
+ order?: number;
657
+ when?: (host: PluginHostRecord) => boolean;
658
+ component: ComponentType<HostSectionProps>;
659
+ }
646
660
  export interface HostContextMenuItemContribution {
647
661
  id: string;
648
662
  titleKey: string;
@@ -1085,6 +1099,11 @@ export interface TermixApp extends TermixAppInfo {
1085
1099
  /** Adds a control to the host editor's toolbar. */
1086
1100
  registerHostEditorToolbarItem: (item: HostEditorToolbarItemContribution) => Disposer;
1087
1101
  registerHostContextMenuItem: (item: HostContextMenuItemContribution) => Disposer;
1102
+ /**
1103
+ * A card on the host page. Core 26.11.1 and newer; call it as
1104
+ * app.registerHostSection?.(...) if your plugin allows older cores.
1105
+ */
1106
+ registerHostSection: (section: HostSectionContribution) => Disposer;
1088
1107
  registerPaletteEntry: (entry: PaletteEntryContribution) => Disposer;
1089
1108
  registerPaletteGroup: (group: PaletteGroupContribution) => Disposer;
1090
1109
  registerKeybindingAction: (action: KeybindingActionContribution) => Disposer;
@@ -29,7 +29,7 @@ import {
29
29
  sortPlugins,
30
30
  uploadEntry,
31
31
  youtubeEmbedUrl
32
- } from "./chunk-ROB2SH5Q.js";
32
+ } from "./chunk-C52NRTA6.js";
33
33
  import {
34
34
  Button,
35
35
  FakeSwitch,
@@ -37,7 +37,7 @@ import {
37
37
  PluginIcon,
38
38
  SectionCard,
39
39
  hasSettingsPage
40
- } from "./chunk-7J4QNGFM.js";
40
+ } from "./chunk-IMPLAAUM.js";
41
41
  import {
42
42
  ArrowDownWideNarrow,
43
43
  BookOpen,
@@ -109,8 +109,8 @@ import {
109
109
  updatePlugin,
110
110
  uploadPlugin,
111
111
  useConfirm
112
- } from "./chunk-VKNGNEF3.js";
113
- import "./chunk-P26QKKU5.js";
112
+ } from "./chunk-DN6YKMFJ.js";
113
+ import "./chunk-6WLECSNB.js";
114
114
  import {
115
115
  __toESM,
116
116
  define_import_meta_env_default,
@@ -3,7 +3,7 @@ import {
3
3
  hasPermission,
4
4
  resetPermissionsCache,
5
5
  usePermissions
6
- } from "./chunk-JRV2HIDK.js";
6
+ } from "./chunk-GRH7GSKG.js";
7
7
  import {
8
8
  Columns2,
9
9
  KeyRound,
@@ -51,7 +51,7 @@ import {
51
51
  withIconBoundary,
52
52
  withPluginContext,
53
53
  withPluginScope
54
- } from "./chunk-VKNGNEF3.js";
54
+ } from "./chunk-DN6YKMFJ.js";
55
55
  import {
56
56
  SYNC_CHANGED_EVENT,
57
57
  TRUST_TOKEN_KEY,
@@ -99,7 +99,7 @@ import {
99
99
  usePluginStore,
100
100
  useTabTypes,
101
101
  websocketAuthProtocols
102
- } from "./chunk-P26QKKU5.js";
102
+ } from "./chunk-6WLECSNB.js";
103
103
  import {
104
104
  init_define_import_meta_env
105
105
  } from "./chunk-YGYBM7SQ.js";
@@ -2010,6 +2010,18 @@ var en_default = {
2010
2010
  title: "Send alerts",
2011
2011
  consequence: "To you or other users, and through the channels they set up."
2012
2012
  },
2013
+ "operations:provide": {
2014
+ title: "Offer actions",
2015
+ consequence: "Its actions can be run from Automations, the CLI, the phone app and other places, by people allowed to use them."
2016
+ },
2017
+ "operations:invoke": {
2018
+ title: "Run other plugins' actions",
2019
+ consequence: "Acts for the signed-in user on the actions its manifest lists. Approval rules still apply."
2020
+ },
2021
+ "operations:approve": {
2022
+ title: "Decide what needs approval",
2023
+ consequence: "Can let actions run with no approval, or block them, for every user."
2024
+ },
2013
2025
  "notify:hub": {
2014
2026
  title: "Receive every alert",
2015
2027
  consequence: "Stores and delivers the alerts all plugins send, including their contents."
@@ -3048,6 +3060,23 @@ function withProtocolSettings(pluginSettings, flags, list = listHostProtocols())
3048
3060
  return next;
3049
3061
  }
3050
3062
 
3063
+ // src/ui/shell/host-sections.ts
3064
+ init_define_import_meta_env();
3065
+ var sections2 = createRegistry(byOrderThenId);
3066
+ var registerHostSection = sections2.register;
3067
+ var useHostSections = sections2.useList;
3068
+ var listHostSections = sections2.list;
3069
+ var resetHostSections = sections2.reset;
3070
+ function hostSectionsFor(all, host) {
3071
+ return all.filter((section) => {
3072
+ try {
3073
+ return section.when(host);
3074
+ } catch {
3075
+ return false;
3076
+ }
3077
+ });
3078
+ }
3079
+
3051
3080
  // src/ui/dashboard/dashboard-cards-registry.ts
3052
3081
  init_define_import_meta_env();
3053
3082
  var registry3 = createRegistry();
@@ -3232,6 +3261,22 @@ function createPluginApi(pluginId) {
3232
3261
  }
3233
3262
  });
3234
3263
  }
3264
+ function createOperationsApi() {
3265
+ return new Proxy(authApi, {
3266
+ get(target, property, receiver) {
3267
+ const value = Reflect.get(target, property, receiver);
3268
+ if (typeof value !== "function") return value;
3269
+ if (property === "get" || property === "delete" || property === "post" || property === "put" || property === "patch") {
3270
+ return (url, ...rest) => value.call(
3271
+ target,
3272
+ `/operations${url.startsWith("/") ? url : `/${url}`}`,
3273
+ ...rest
3274
+ );
3275
+ }
3276
+ return value.bind(target);
3277
+ }
3278
+ });
3279
+ }
3235
3280
  function pluginFetch(pluginId, path2, init = {}) {
3236
3281
  const base = (authApi.defaults.baseURL ?? "").replace(/\/+$/, "");
3237
3282
  const headers = new Headers(init.headers);
@@ -8676,6 +8721,20 @@ function createPluginApp(pluginId, manifest, contributes, options = {}) {
8676
8721
  })
8677
8722
  );
8678
8723
  },
8724
+ registerHostSection(section) {
8725
+ return track(
8726
+ registerHostSection({
8727
+ id: section.id,
8728
+ pluginId,
8729
+ titleKey: key(section.titleKey),
8730
+ order: section.order,
8731
+ when: hostCallback(section.when ?? (() => true)),
8732
+ component: scoped(
8733
+ section.component
8734
+ )
8735
+ })
8736
+ );
8737
+ },
8679
8738
  registerHostContextMenuItem(item) {
8680
8739
  return track(
8681
8740
  registerHostContextMenuItem({
@@ -9084,6 +9143,11 @@ export {
9084
9143
  hostProtocolFlags,
9085
9144
  enabledHostProtocols,
9086
9145
  withProtocolSettings,
9146
+ registerHostSection,
9147
+ useHostSections,
9148
+ listHostSections,
9149
+ resetHostSections,
9150
+ hostSectionsFor,
9087
9151
  registerDashboardCard,
9088
9152
  getRegisteredDashboardCard,
9089
9153
  registeredDashboardCardList,
@@ -9101,6 +9165,7 @@ export {
9101
9165
  registerSlotContribution,
9102
9166
  getSlotContributions,
9103
9167
  resetActionRegistry,
9168
+ createOperationsApi,
9104
9169
  pluginWsUrl,
9105
9170
  PluginComponent,
9106
9171
  onboardingSteps,
@@ -4691,6 +4691,8 @@ var DatabaseHealthMonitor = class _DatabaseHealthMonitor {
4691
4691
  static instance;
4692
4692
  listeners = /* @__PURE__ */ new Map();
4693
4693
  degradedActive = false;
4694
+ probe = null;
4695
+ probing = null;
4694
4696
  constructor() {
4695
4697
  }
4696
4698
  static getInstance() {
@@ -4729,10 +4731,28 @@ var DatabaseHealthMonitor = class _DatabaseHealthMonitor {
4729
4731
  const errorCode = errorLike.response?.data?.code || errorLike.code;
4730
4732
  const lowerMessage = errorMessage.toLowerCase();
4731
4733
  const isDatabaseError = lowerMessage.includes("database") || lowerMessage.includes("sqlite") || lowerMessage.includes("drizzle") || errorCode === "DATABASE_ERROR" || errorCode === "DB_CONNECTION_FAILED";
4732
- const isBackendUnreachable = errorCode === "ERR_NETWORK" || errorCode === "ECONNREFUSED" || errorCode === "ECONNABORTED" || errorCode === "ECONNRESET" || errorCode === "ETIMEDOUT" || errorCode === "ERR_CANCELED" || lowerMessage.includes("network error") && errorLike.response === void 0 || lowerMessage.includes("request aborted") || lowerMessage.includes("timeout");
4733
- if (!(isDatabaseError || isBackendUnreachable)) {
4734
- return;
4734
+ if (errorCode === "ERR_CANCELED" || lowerMessage === "canceled") return;
4735
+ const isBackendUnreachable = errorCode === "ERR_NETWORK" || errorCode === "ECONNREFUSED" || errorCode === "ECONNRESET" || lowerMessage.includes("network error") && errorLike.response === void 0;
4736
+ const isTimeout = errorCode === "ECONNABORTED" || errorCode === "ETIMEDOUT" || lowerMessage.includes("request aborted") || lowerMessage.includes("timeout");
4737
+ if (isDatabaseError || isBackendUnreachable) {
4738
+ this.markDegraded(errorMessage, errorCode);
4739
+ } else if (isTimeout) {
4740
+ if (!this.probe) {
4741
+ this.markDegraded(errorMessage, errorCode);
4742
+ return;
4743
+ }
4744
+ this.probing ??= this.probe().catch(() => false).then((reachable) => {
4745
+ if (!reachable) this.markDegraded(errorMessage, errorCode);
4746
+ }).finally(() => {
4747
+ this.probing = null;
4748
+ });
4735
4749
  }
4750
+ }
4751
+ /** Sets the quick check used after a request times out. */
4752
+ setReachabilityProbe(probe) {
4753
+ this.probe = probe;
4754
+ }
4755
+ markDegraded(errorMessage, errorCode) {
4736
4756
  if (!this.degradedActive) {
4737
4757
  this.degradedActive = true;
4738
4758
  this.emit("database-connection-degraded", {
@@ -6320,7 +6340,7 @@ async function updateRole(roleId, roleData) {
6320
6340
  try {
6321
6341
  const response = await rbacApi.put(`/rbac/roles/${roleId}`, roleData);
6322
6342
  if (roleData.permissions) {
6323
- const { notifyPermissionsChanged } = await import("./use-permissions-EWRI3WLT.js");
6343
+ const { notifyPermissionsChanged } = await import("./use-permissions-FLQAN6I7.js");
6324
6344
  notifyPermissionsChanged();
6325
6345
  }
6326
6346
  return response.data;
@@ -6927,7 +6947,7 @@ function createRemoteOriginApiInstance(path) {
6927
6947
  });
6928
6948
  instance.interceptors.request.use(
6929
6949
  async (config) => {
6930
- const { getLinkedSession: getLinkedSession2 } = await import("./linked-server-J5G457OA.js");
6950
+ const { getLinkedSession: getLinkedSession2 } = await import("./linked-server-YCAJ2NX3.js");
6931
6951
  const linked = await getLinkedSession2();
6932
6952
  config.baseURL = linked ? `${linked.serverUrl}${path}` : "http://no-server-configured";
6933
6953
  if (config.headers.set) {
@@ -6954,6 +6974,16 @@ function initializeApiInstances() {
6954
6974
  hostApi = createApiInstance(getApiUrl("/host", 30001), "HOST");
6955
6975
  sshHostApi = hostApi;
6956
6976
  authApi = createApiInstance(getApiUrl("", 30001), "AUTH");
6977
+ const healthApi = authApi;
6978
+ dbHealthMonitor.setReachabilityProbe(
6979
+ () => healthApi.get("/health", {
6980
+ timeout: 5e3,
6981
+ __silentRetry: true
6982
+ }).then(
6983
+ () => true,
6984
+ () => false
6985
+ )
6986
+ );
6957
6987
  rbacApi = createApiInstance(getApiUrl("", 30001), "RBAC");
6958
6988
  }
6959
6989
  var hostApi;
@@ -1,7 +1,7 @@
1
1
  const __termixNoGlob = () => ({});
2
2
  import {
3
3
  Button
4
- } from "./chunk-7J4QNGFM.js";
4
+ } from "./chunk-IMPLAAUM.js";
5
5
  import {
6
6
  ChevronRight,
7
7
  PANEL,
@@ -10,10 +10,10 @@ import {
10
10
  cn,
11
11
  gridSizeStyle,
12
12
  useAreaPreferences
13
- } from "./chunk-VKNGNEF3.js";
13
+ } from "./chunk-DN6YKMFJ.js";
14
14
  import {
15
15
  isElectron
16
- } from "./chunk-P26QKKU5.js";
16
+ } from "./chunk-6WLECSNB.js";
17
17
  import {
18
18
  __commonJS,
19
19
  __toESM,
@@ -2429,6 +2429,8 @@ var CAPABILITY_CATALOG = [
2429
2429
  entry("ssh:transport", "high"),
2430
2430
  entry("device:serial", "high"),
2431
2431
  entry("notify:hub", "high"),
2432
+ entry("operations:invoke", "high"),
2433
+ entry("operations:approve", "high"),
2432
2434
  entry("ui:layout", "high"),
2433
2435
  entry("hosts:write", "medium"),
2434
2436
  entry("hosts:export", "high"),
@@ -2444,6 +2446,7 @@ var CAPABILITY_CATALOG = [
2444
2446
  entry("users:read", "medium"),
2445
2447
  entry("events:core", "medium"),
2446
2448
  entry("notify:send", "medium"),
2449
+ entry("operations:provide", "medium"),
2447
2450
  entry("audit:read", "medium"),
2448
2451
  entry("desktop:window", "medium"),
2449
2452
  entry("desktop:local", "high"),
@@ -2540,8 +2543,9 @@ function mergePlugins(installed, registry) {
2540
2543
  latestBeta: entry2?.latestBeta ?? null,
2541
2544
  channel: summary.channel ?? entry2?.channel ?? "stable",
2542
2545
  isBeta: isPrerelease(summary.version),
2543
- // The server keeps its own copy up to date.
2544
- updateAvailable: summary.runsOnServer ? false : entry2?.updateAvailable ?? false,
2546
+ // The server keeps its own copy up to date, and a plugin that is off
2547
+ // should not ask for attention.
2548
+ updateAvailable: summary.runsOnServer || !summary.enabled ? false : entry2?.updateAvailable ?? false,
2545
2549
  addedCapabilities: entry2?.addedCapabilities ?? [],
2546
2550
  versions: entry2?.versions ?? [],
2547
2551
  capabilities: summary.capabilities ?? [],
@@ -4,7 +4,7 @@ import {
4
4
  getPluginRecord,
5
5
  rbacApi,
6
6
  usePluginStore
7
- } from "./chunk-P26QKKU5.js";
7
+ } from "./chunk-6WLECSNB.js";
8
8
  import {
9
9
  __export,
10
10
  init_define_import_meta_env
@@ -1,7 +1,7 @@
1
1
  const __termixNoGlob = () => ({});
2
2
  import {
3
3
  useTheme
4
- } from "./chunk-HQ6NNFSX.js";
4
+ } from "./chunk-5W6F6GJJ.js";
5
5
  import {
6
6
  init_define_import_meta_env
7
7
  } from "./chunk-YGYBM7SQ.js";
@@ -26,7 +26,7 @@ var inFlight = null;
26
26
  function load() {
27
27
  if (cache) return Promise.resolve(cache);
28
28
  if (inFlight) return inFlight;
29
- inFlight = import("./rbac-api-PY22W5UY.js").then(({ getMyPermissions }) => getMyPermissions()).then((result) => {
29
+ inFlight = import("./rbac-api-A64TXDL5.js").then(({ getMyPermissions }) => getMyPermissions()).then((result) => {
30
30
  cache = {
31
31
  permissions: result.permissions ?? [],
32
32
  isAdmin: !!result.isAdmin
@@ -9,7 +9,7 @@ import {
9
9
  icons_exports,
10
10
  isFieldActive,
11
11
  usePluginScope
12
- } from "./chunk-VKNGNEF3.js";
12
+ } from "./chunk-DN6YKMFJ.js";
13
13
  import {
14
14
  init_define_import_meta_env
15
15
  } from "./chunk-YGYBM7SQ.js";