feat(templating): (T1) Preferences UI — plugin-sandbox toggle + per-plugin elevated + mode indicator

- Scripting settings: new "Sandbox all plugin code (experimental)" toggle for pluginSandboxEnabled
  (data-testid toggle-plugin-sandbox), beside the existing template-tag toggle.
- Plugins settings: each user plugin card now shows its resolved execution mode
  (Sandboxed / Elevated / In-process, data-testid plugin-mode-<name>) and a "Full host access"
  checkbox (data-testid plugin-elevated-<name>) that writes pluginConfig.<name>.elevated. Mode +
  toggle read from live settings so they update immediately, before the plugin list reloads.
- Widened SerializablePlugin.config to carry the optional `elevated` flag through the bridge.
This commit is contained in:
jackkav
2026-07-31 08:23:49 +02:00
parent 01325daabc
commit fe87f7e5fb
3 changed files with 86 additions and 1 deletions

View File

@@ -76,7 +76,7 @@ export interface SerializablePlugin {
description: string;
version: string;
directory: string;
config: { disabled: boolean };
config: { disabled: boolean; elevated?: boolean };
/** Parsed `insomnia.permissions` manifest (sandbox plan C3), surfaced in Preferences → Plugins. */
permissions: { modules: string[]; capabilities: string[] };
/** Validation warnings from parsing `permissions`; shown on the plugin card. Empty when clean. */

View File

@@ -13,6 +13,7 @@ import {
} from 'react-aria-components';
import type { SerializablePlugin } from '~/common/plugins/bridge-types';
import { resolvePluginExecutionMode } from '~/common/plugins/sandbox-mode';
import { validatePluginName } from '~/common/utils/plugin-name';
import { useRootLoaderData } from '~/root';
import { plugins as pluginsBridge } from '~/ui/plugins/renderer-bridge';
@@ -498,6 +499,28 @@ export const Plugins: FC = () => {
? 'Declared empty permissions (baseline access)'
: 'No permissions declared (baseline access)';
// T1: this plugin's resolved execution mode + the per-plugin "elevated" escape hatch.
// Only user plugins reach this list (bundle plugins are filtered out above). Read
// `elevated` from live settings (not the load-time plugin.config snapshot) so the toggle
// and badge update immediately, before the plugin list reloads.
const isElevated = settings.pluginConfig?.[plugin.name]?.elevated === true;
const executionMode = resolvePluginExecutionMode(settings, {
directory: plugin.directory,
config: { elevated: isElevated },
});
const modeLabel =
executionMode === 'sandboxed'
? 'Sandboxed'
: executionMode === 'elevated'
? 'Elevated'
: 'In-process';
const modeTitle =
executionMode === 'sandboxed'
? 'Runs in the QuickJS sandbox (default-deny host access).'
: executionMode === 'elevated'
? 'Runs in the main process with full host access (you granted this).'
: 'Sandbox is off — runs in the main process with full host access.';
return (
<GridListItem
textValue={plugin.name}
@@ -540,6 +563,13 @@ export const Plugins: FC = () => {
>
{permissionLabel}
</span>
<span
data-testid={`plugin-mode-${plugin.name}`}
className="rounded-sm bg-(--hl-xs) px-1.5 text-xs whitespace-nowrap text-(--hl)"
title={modeTitle}
>
{modeLabel}
</span>
{plugin.permissionWarnings && plugin.permissionWarnings.length > 0 && (
<span
data-testid={`plugin-permission-warning-${plugin.name}`}
@@ -554,6 +584,37 @@ export const Plugins: FC = () => {
</div>
<div className="flex items-center gap-6">
<Checkbox
data-testid={`plugin-elevated-${plugin.name}`}
isSelected={isElevated}
isDisabled={isRefreshingPlugins}
className="group flex items-center gap-1.5 p-0 text-xs disabled:animate-pulse"
onChange={isSelected => {
patchSettings({
pluginConfig: {
...settings.pluginConfig,
[plugin.name]: {
...plugin.config,
...settings.pluginConfig?.[plugin.name],
elevated: isSelected,
},
},
});
}}
>
<div className="flex h-4 w-4 items-center justify-center rounded-sm ring-1 ring-(--hl-sm) transition-colors group-focus:ring-2 group-data-selected:bg-(--hl-xs)">
<Icon
icon="check"
className="h-3 w-3 opacity-0 group-data-selected:text-(--color-warning) group-data-selected:opacity-100"
/>
</div>
<span
className="whitespace-nowrap text-(--hl)"
title="Run this plugin in the main process with full host access instead of the sandbox."
>
Full host access
</span>
</Checkbox>
<div className="flex w-[8ch] items-center justify-center gap-2">
{plugin.version}
<a className="space-left" href={link} title={link}>

View File

@@ -144,6 +144,7 @@ export const ScriptingSettings = () => {
const sandboxEnabled = settings.scriptSandboxEnabled !== false;
const strictModeEnabled = settings.scriptStrictModeEnabled !== false;
const templateTagSandboxEnabled = settings.templateTagSandboxEnabled === true;
const pluginSandboxEnabled = settings.pluginSandboxEnabled === true;
const disabledRules = settings.disabledSecurityRules ?? [];
const disabledProperties = settings.disabledBlockedProperties ?? [];
const disabledRoots = settings.disabledBlockedRoots ?? [];
@@ -340,6 +341,29 @@ export const ScriptingSettings = () => {
</div>
</div>
<div className="rounded-md border border-solid border-(--hl-sm) bg-(--hl-xs) p-4">
<div className="flex items-center justify-between">
<div className="flex flex-col gap-1">
<span className="text-sm font-medium text-(--color-font)">Sandbox all plugin code (experimental)</span>
<p className="text-xs text-(--hl)">
Run every untrusted plugin surface template tags, request/response hooks, actions, and load-time code
inside the QuickJS-WASM sandbox. Installed plugins are default-deny; grant an individual plugin full host
access with "Run with full host access" in Preferences Plugins.
</p>
</div>
<Switch
data-testid="toggle-plugin-sandbox"
isSelected={pluginSandboxEnabled}
onChange={enabled => patchSettings({ pluginSandboxEnabled: enabled })}
className="group flex items-center gap-2"
>
<div className="flex h-6 w-11 cursor-pointer items-center rounded-full border-2 border-solid border-transparent bg-(--hl-md) transition-colors group-data-selected:bg-(--color-surprise)">
<span className="h-5 w-5 translate-x-0 rounded-full bg-white transition-transform group-data-selected:translate-x-5" />
</div>
</Switch>
</div>
</div>
<RuleCard
title="Mask Rules"
description="Overwrites specific global variables with undefined so scripts cannot access them."