From 8f1c764614233a9a26be5d7f5e8041015abea86b Mon Sep 17 00:00:00 2001 From: Pine Date: Fri, 14 Aug 2026 18:44:56 +0800 Subject: [PATCH] feat(plugin-inventory): blacklist/whitelist toggle guard with a default-open default Splits the enable/disable guard into two code-editable lists: REQUIRED_PLUGINS (the blacklist of load-bearing core that must never be disabled) and USER_TOGGLEABLE_PLUGINS (the whitelist, which overrides the blacklist for an explicitly toggleable plugin). A plugin on neither list is toggleable by default. isRequiredPlugin and isUserToggleable derive from the two lists. Co-Authored-By: Claude --- .../host/plugin-inventory/src/required.ts | 58 +++++++++++++++---- .../plugin-inventory/tests/required.spec.ts | 34 +++++++++++ 2 files changed, 80 insertions(+), 12 deletions(-) create mode 100644 packages/host/plugin-inventory/tests/required.spec.ts diff --git a/packages/host/plugin-inventory/src/required.ts b/packages/host/plugin-inventory/src/required.ts index 02052d12f5..0014f3db33 100644 --- a/packages/host/plugin-inventory/src/required.ts +++ b/packages/host/plugin-inventory/src/required.ts @@ -1,19 +1,25 @@ /** - * Which Loader plugins the running application requires and must not be toggled. + * Which Loader plugins the running application requires and must not be toggled, + * and which plugins a deployment explicitly allows the user to toggle. * - * The guard is default-open: every plugin is toggleable unless it is explicitly - * listed as required. Only the few load-bearing core plugins that other plugins - * inject and whose disable would tear the process or the management surface - * down are surfaced as `protected`. Add a module name to - * {@link REQUIRED_PLUGINS} when a plugin must never be disabled. + * The guard is default-open with two code-editable lists: + * - {@link REQUIRED_PLUGINS} is the blacklist of load-bearing core plugins that + * must never be disabled (disabling one tears the process or the management + * surface down). + * - {@link USER_TOGGLEABLE_PLUGINS} is the whitelist of plugins a deployment + * explicitly permits the user to toggle; a whitelisted name overrides the + * blacklist for that plugin. + * A plugin on neither list is toggleable by default. Edit these lists (they are + * plain constants) to change what the plugin-inventory `protected` flag reports + * and what `setEnabled` refuses to disable. * @module @deepseek-ai/dsh-plugin-inventory/required */ /** - * Plugin module names that are required by the application and must not be - * toggled. Everything else is toggleable. Keep this to the true core: the entry - * tree, the Remote RPC spine every Remote depends on, and the session/agent - * spines. `cordis:required` is a test seam for the unit tests. + * Blacklist: plugin module names required by the application and must not be + * toggled. Keep this to the true core: the entry tree, the Remote RPC spine + * every Remote depends on, and the session/agent spines. `cordis:required` is a + * test seam for the unit tests. */ const REQUIRED_PLUGINS = new Set([ '@deepseek-ai/cordis-plugin-loader', @@ -25,7 +31,35 @@ const REQUIRED_PLUGINS = new Set([ 'cordis:required', ]) -/** Whether a Loader module is required by the application and must not be toggled. */ +/** + * Whitelist: plugin module names a deployment explicitly permits the user to + * toggle. A name here overrides the blacklist, so a whitelisted plugin is never + * reported `protected` even if it is also required. Everything else is + * toggleable by default, so this is the place to force a specific plugin open. + */ +const USER_TOGGLEABLE_PLUGINS = new Set([ + '@deepseek-ai/dsh-image-recognition', + '@deepseek-ai/dsh-image-recognition-http', + '@deepseek-ai/dsh-tool-image-recognition', +]) + +/** + * Whether a Loader module is required by the application and must not be toggled. + * The blacklist wins over the default-open stance unless the name is explicitly + * whitelisted. + * @param moduleName - the Loader module specifier. + * @returns true when the plugin is `protected` (cannot be disabled). + */ export function isRequiredPlugin(moduleName: string): boolean { - return REQUIRED_PLUGINS.has(moduleName) + return REQUIRED_PLUGINS.has(moduleName) && !USER_TOGGLEABLE_PLUGINS.has(moduleName) +} + +/** + * Whether a Loader module may be toggled by the user. Default-open: a plugin is + * toggleable unless the blacklist marks it required and it is not whitelisted. + * @param moduleName - the Loader module specifier. + * @returns true when the plugin can be enabled or disabled. + */ +export function isUserToggleable(moduleName: string): boolean { + return !isRequiredPlugin(moduleName) } diff --git a/packages/host/plugin-inventory/tests/required.spec.ts b/packages/host/plugin-inventory/tests/required.spec.ts new file mode 100644 index 0000000000..d593859ce9 --- /dev/null +++ b/packages/host/plugin-inventory/tests/required.spec.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest' +import { AVAILABLE_BUNDLES } from '../src/bundles.ts' +import { isRequiredPlugin, isUserToggleable } from '../src/required.ts' + +describe('isRequiredPlugin (blacklist/whitelist, default-open)', () => { + it('marks the blacklist core as required', () => { + for (const name of ['@deepseek-ai/cordis-plugin-loader', '@deepseek-ai/dsh-session', 'cordis:required']) { + expect(isRequiredPlugin(name)).toBe(true) + expect(isUserToggleable(name)).toBe(false) + } + }) + + it('defaults everything else to toggleable', () => { + for (const name of ['@deepseek-ai/dsh-hmr', '@deepseek-ai/dsh-tool-todo', 'cordis:user-toggleable']) { + expect(isRequiredPlugin(name)).toBe(false) + expect(isUserToggleable(name)).toBe(true) + } + }) + + it('whitelist overrides the blacklist for an explicitly toggleable plugin', () => { + // image-recognition is on the whitelist, so it is never required. + for (const name of ['@deepseek-ai/dsh-image-recognition', '@deepseek-ai/dsh-image-recognition-http']) { + expect(isRequiredPlugin(name)).toBe(false) + expect(isUserToggleable(name)).toBe(true) + } + }) +}) + +describe('AVAILABLE_BUNDLES', () => { + it('lists the offline-installable optional bundles', () => { + expect(AVAILABLE_BUNDLES).toContain('@deepseek-ai/dsh-image-recognition-bundle') + expect(new Set(AVAILABLE_BUNDLES).size).toBe(AVAILABLE_BUNDLES.length) + }) +})