Skip to content

fix(plugins): let an operator disable a plugin whose code is missing - #1037

Merged
rmyndharis merged 2 commits into
mainfrom
fix/disable-unloaded-plugin
Aug 1, 2026
Merged

fix(plugins): let an operator disable a plugin whose code is missing#1037
rmyndharis merged 2 commits into
mainfrom
fix/disable-unloaded-plugin

Conversation

@rmyndharis

Copy link
Copy Markdown
Owner

Problem

A plugin can exist in the registry — entry, config, per-plugin storage, and the enabledByOperator decision that boot reads — while its package directory is gone. An interrupted update leaves that state, and so does a directory that was never on the data volume in the first place.

In that state POST /plugins/:id/disable returned 404:

const plugin = this.pluginLoader.getPlugin(id);
if (!plugin) {
  throw new NotFoundException(`Plugin ${id} not found`);
}
if (plugin.status !== PluginStatus.ENABLED) {
  // Clear the decision here too: a plugin sitting in ERROR after a failed restore is not ENABLED,
  // and disabling it must stop the gateway retrying it on every boot.
  this.pluginLoader.setOperatorEnabled(id, false);
  ...

The branch immediately below already recognises that clearing the boot decision matters even when there is nothing running. The !plugin guard returns before reaching it, so the operator had no route at all to withdraw a standing instruction to enable the plugin on every boot. Reinstalling the code later brought it straight back up. The only workarounds were uninstalling — which also destroys the plugin's stored data — or hand-editing registry.json.

Observed on a 0.12.1 host: two registry entries left at enabledByOperator: true after their code was lost, both answering 404 to every disable attempt.

Change

disable expresses intent; it is not a runtime operation, and the registry is where intent lives. When the plugin is not loaded but a registry entry exists, the decision is now cleared against the registry and the call succeeds. A 404 is reserved for an id with no entry at all — which is what a 404 should mean.

PluginLoaderService.getRegistryEntry is added to make that distinction available; it reads through to the storage service's existing accessor.

Nothing else changes: there is no runtime to tear down in this state, so no lifecycle hook is skipped and no worker is left behind.

Tests

Two cases in plugins.service.spec.ts, using the existing real-loader-and-disk harness: a plugin is installed and marked enabled-by-operator, its directory is removed, and a fresh loader and service are built over the same registry — reproducing a restart that re-reads the registry and loads nothing. Disabling it then succeeds and clears the flag. The second case asserts an unknown id still throws NotFound.

Reverting the change fails the first and leaves the other 46 green.

src/modules/plugins + src/core/plugins: 443 passed. tsc --noEmit and eslint clean.

A plugin can be installed — registry entry, config, storage, and the
enabledByOperator decision that boot reads — while its package directory is
gone: an interrupted update, or a directory that was never on the data volume.
disable() threw NotFound in that state, because the loader had nothing to tear
down, so the operator had no route at all to withdraw the standing instruction
to enable it on every boot. Reinstalling the code brought the plugin back up.

disable expresses intent rather than performing a runtime operation, and the
registry is where intent lives. It now clears the decision against the registry
when the plugin is not loaded, and 404s only for an id with no entry at all —
which is what a 404 should mean. This is the same reasoning the not-ENABLED
branch below it already applies to a plugin sitting in ERROR.
The route's 404 was operator-visible behaviour and the fix changes what an
operator can do, so it belongs in the release notes rather than only in the PR.
@rmyndharis
rmyndharis force-pushed the fix/disable-unloaded-plugin branch from a5649e3 to d1956e4 Compare August 1, 2026 08:16
@rmyndharis
rmyndharis merged commit aa89fbf into main Aug 1, 2026
15 checks passed
@rmyndharis
rmyndharis deleted the fix/disable-unloaded-plugin branch August 1, 2026 08:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant