@termix-ssh/plugin-sdk 1.0.22 → 1.0.23

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 (30) hide show
  1. package/CHANGELOG.md +11 -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/host/{PluginsTab-CM2B33BD.js → PluginsTab-MNQU5OJ3.js} +4 -4
  6. package/dist/host/{chunk-P26QKKU5.js → chunk-6WLECSNB.js} +35 -5
  7. package/dist/host/{chunk-TK3Y437H.js → chunk-BP42N374.js} +1 -1
  8. package/dist/host/{chunk-ROB2SH5Q.js → chunk-C52NRTA6.js} +9 -5
  9. package/dist/host/{chunk-VKNGNEF3.js → chunk-DN6YKMFJ.js} +1 -1
  10. package/dist/host/{chunk-I5F2PSMR.js → chunk-GBBKHJRQ.js} +9 -9
  11. package/dist/host/{chunk-JRV2HIDK.js → chunk-GRH7GSKG.js} +1 -1
  12. package/dist/host/{chunk-7J4QNGFM.js → chunk-IMPLAAUM.js} +1 -1
  13. package/dist/host/{chunk-HQ6NNFSX.js → chunk-X65V7ZRW.js} +15 -3
  14. package/dist/host/core-classes.json +1 -1
  15. package/dist/host/frame-host.css +1 -1
  16. package/dist/host/frame-host.js +5 -5
  17. package/dist/host/{linked-server-J5G457OA.js → linked-server-YCAJ2NX3.js} +1 -1
  18. package/dist/host/{rbac-api-PY22W5UY.js → rbac-api-A64TXDL5.js} +1 -1
  19. package/dist/host/testing-host.js +6 -6
  20. package/dist/host/types/src/ui/lib/db-health-monitor.d.ts +5 -0
  21. package/dist/host/ui.js +9 -9
  22. package/dist/host/{use-permissions-EWRI3WLT.js → use-permissions-FLQAN6I7.js} +1 -1
  23. package/dist/manifest.d.ts +6 -1
  24. package/dist/manifest.js +7 -1
  25. package/dist/operations.d.ts +222 -0
  26. package/dist/operations.js +650 -0
  27. package/dist/testing.d.ts +9 -1
  28. package/dist/testing.js +45 -0
  29. package/package.json +5 -1
  30. package/schema/manifest.schema.json +25 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.23
4
+
5
+ Plugin API 1.14.0.
6
+
7
+ ### Added
8
+
9
+ - `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
10
+ - `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
11
+ - `@termix-ssh/plugin-sdk/operations` has the types, input checks, redaction and JSON Schema for declared actions
12
+ - `contributes.hostSections` declares cards for the host page
13
+
3
14
  ## 1.0.22
4
15
 
5
16
  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.23 | 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"),
@@ -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,
@@ -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
  useTheme
4
- } from "./chunk-HQ6NNFSX.js";
4
+ } from "./chunk-X65V7ZRW.js";
5
5
  import {
6
6
  init_define_import_meta_env
7
7
  } from "./chunk-YGYBM7SQ.js";
@@ -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
@@ -15,10 +15,10 @@ import {
15
15
  useCompanionState,
16
16
  useIsMobile,
17
17
  useSettingsComponents
18
- } from "./chunk-HQ6NNFSX.js";
18
+ } from "./chunk-X65V7ZRW.js";
19
19
  import {
20
20
  usePermissions
21
- } from "./chunk-JRV2HIDK.js";
21
+ } from "./chunk-GRH7GSKG.js";
22
22
  import {
23
23
  Button,
24
24
  DefaultsOnly,
@@ -30,7 +30,7 @@ import {
30
30
  SettingRow,
31
31
  hasVisibleFields,
32
32
  isFieldShown
33
- } from "./chunk-7J4QNGFM.js";
33
+ } from "./chunk-IMPLAAUM.js";
34
34
  import {
35
35
  ArrowDownToLine,
36
36
  ArrowLeftToLine,
@@ -61,12 +61,12 @@ import {
61
61
  pluginKey,
62
62
  pluginName,
63
63
  usePluginScope
64
- } from "./chunk-VKNGNEF3.js";
64
+ } from "./chunk-DN6YKMFJ.js";
65
65
  import {
66
66
  getTabType,
67
67
  unregisteredViewStatus,
68
68
  usePluginStore
69
- } from "./chunk-P26QKKU5.js";
69
+ } from "./chunk-6WLECSNB.js";
70
70
  import {
71
71
  init_define_import_meta_env
72
72
  } from "./chunk-YGYBM7SQ.js";
@@ -111,7 +111,7 @@ var PasswordInput = React.forwardRef(({ className, ...props }, ref) => {
111
111
  {
112
112
  ref,
113
113
  type: showPassword ? "text" : "password",
114
- className: cn("h-11 text-base pr-12", className),
114
+ className: cn("h-8 pr-8", className),
115
115
  ...props
116
116
  }
117
117
  ),
@@ -120,9 +120,9 @@ var PasswordInput = React.forwardRef(({ className, ...props }, ref) => {
120
120
  {
121
121
  type: "button",
122
122
  onClick: () => setShowPassword((prev) => !prev),
123
- className: "absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition",
123
+ className: "absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition",
124
124
  "aria-label": showPassword ? "Hide password" : "Show password",
125
- children: showPassword ? /* @__PURE__ */ jsx2(EyeOff, { className: "h-5 w-5" }) : /* @__PURE__ */ jsx2(Eye, { className: "h-5 w-5" })
125
+ children: showPassword ? /* @__PURE__ */ jsx2(EyeOff, { className: "size-4" }) : /* @__PURE__ */ jsx2(Eye, { className: "size-4" })
126
126
  }
127
127
  )
128
128
  ] });
@@ -1321,7 +1321,7 @@ function CompanionLayout({
1321
1321
  // src/ui/shell/tabUtils.tsx
1322
1322
  import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
1323
1323
  var PluginsTab = lazy(
1324
- () => import("./PluginsTab-CM2B33BD.js").then((m) => ({ default: m.PluginsTab }))
1324
+ () => import("./PluginsTab-MNQU5OJ3.js").then((m) => ({ default: m.PluginsTab }))
1325
1325
  );
1326
1326
  function preloadTabSurface(type) {
1327
1327
  const loader = getTabType(type)?.preload;
@@ -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";
@@ -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."