{"record":{"id":"4f099bec9a31191a","repo":"stablyai/orca","slug":"cannot-remove-protected-or-non-installed-plugin","errorCode":null,"errorMessage":"cannot remove protected or non-installed plugin ${parsed.pluginKey}","messagePattern":"cannot remove protected or non-installed plugin (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ipc/plugins.ts","lineNumber":220,"sourceCode":"            pluginsDir,\n            url: parsed.url,\n            ref: parsed.ref,\n            hostVersion,\n            blockedPluginReason\n          })\n    if (result.ok) {\n      await pluginService.refresh()\n    }\n    return result\n  })\n\n  ipcMain.handle('plugins:remove', async (event, args: unknown) => {\n    await pluginService.whenReady()\n    const parsed = removeArgsSchema.parse(args)\n    const pluginsDir = getUserPluginsDir(pluginService.options.userDataPath)\n    const lock = await readPluginLockfile(pluginsDir)\n    if (!canRemoveInstalledPlugin(pluginService, parsed.pluginKey, lock)) {\n      throw new Error(`cannot remove protected or non-installed plugin ${parsed.pluginKey}`)\n    }\n    await pluginService.deactivatePlugin(parsed.pluginKey)\n    await removeInstalledPlugin({\n      pluginsDir,\n      pluginsDataDir: getPluginsDataDir(pluginService.options.userDataPath),\n      pluginKey: parsed.pluginKey\n    })\n    // Drop the stale consent so a later reinstall re-prompts from scratch.\n    const settings = store.getSettings()\n    const consents = { ...settings.pluginConsents }\n    delete consents[parsed.pluginKey]\n    const disabledPlugins = normalizePluginIdList(settings.disabledPlugins).filter(\n      (pluginKey) => pluginKey !== parsed.pluginKey\n    )\n    store.updateSettings(\n      { pluginConsents: consents, disabledPlugins },\n      { notifyListeners: true, originWebContentsId: event.sender.id }\n    )","sourceCodeStart":202,"sourceCodeEnd":238,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/src/main/ipc/plugins.ts#L202-L238","documentation":"Thrown at plugins.ts:219-220 when canRemoveInstalledPlugin(pluginService, pluginKey, lock) returns false. That function (plugins.ts:79-88) returns true only if BOTH conditions hold: the plugin is not sourced as 'bundled' in the lockfile, AND the plugin is discovered and not marked isDev. So the error covers three distinct cases: the plugin is bundled/protected, it is a dev plugin, or it is not installed at all.","triggerScenarios":"Calling the plugins:remove IPC handler with a pluginKey that (a) is shipped/bundled with the app (lock.plugins[key].source.kind === 'bundled'), (b) is loaded from a dev path (plugin.isDev true), or (c) is not in the discovered plugin list (never installed or already removed).","commonSituations":"UI tries to show a remove button for a built-in plugin; a plugin was uninstalled out-of-band so the discovered list no longer has it; attempting to remove a dev plugin via the UI instead of editing devPluginPaths; typo in the pluginKey.","solutions":["Confirm the pluginKey exactly matches a discovered, non-dev, non-bundled plugin (check the plugins:list output).","If it is a dev plugin, remove it by editing the devPluginPaths setting instead of calling plugins:remove.","If it was already removed, no action is needed — refresh the plugin list.","Bundled plugins cannot be removed by design; do not expose a remove affordance for them in the UI."],"exampleFix":null,"handlingStrategy":"type-guard","validationCode":"// Before calling plugins:remove, check removability the same way the handler does.\nconst lock = await readPluginLockfile(getUserPluginsDir(userDataPath))\nconst removable =\n  lock?.plugins[pluginKey]?.source.kind !== 'bundled' &&\n  pluginService.getDiscovered().some((p) => p.pluginKey === pluginKey && !p.isDev)\nif (!removable) { /* hide/disable the remove button */ }","typeGuard":"function isRemovablePlugin(plugin: { isDev: boolean }, sourceKind?: string): boolean {\n  return !plugin.isDev && sourceKind !== 'bundled'\n}","tryCatchPattern":"try { await window.api.invoke('plugins:remove', { pluginKey }) }\ncatch (e) { if (e instanceof Error && e.message.startsWith('cannot remove protected or non-installed plugin')) { /* refresh list; it may already be gone */ } else throw e }","preventionTips":["Do not show a remove affordance for bundled or dev plugins in the UI.","Refresh the discovered plugin list before offering remove, so the button reflects the current state.","For dev plugins, guide users to edit devPluginPaths instead of removing."],"tags":["plugins","ipc","validation","permissions"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}