Skip to content

store_manager: registry/manifest id mismatch deletes the existing install outside the rollback net and records phantom state #526

Description

@ChuckBuilds

Summary

When a store entry's registry id differs from the manifest id inside it, store_manager deletes the existing install outside the rollback protection that install_plugin exists to provide, records plugin state under an id that has no directory, and returns success to the caller. The store then keeps offering the same plugin for install indefinitely, so the cycle repeats on every click.

The registry data that triggers this is being fixed separately (ChuckBuilds/ledmatrix-plugins#455) — this issue is about the core handling the case safely regardless of what a registry says, including third-party registries the project doesn't control.

Reproduction

Rig with ledmatrix-weather 2.6.6 installed. Registry entry weather has plugin_path: plugins/ledmatrix-weather, whose manifest says id: ledmatrix-weather.

curl -X POST http://localhost:5000/api/v3/plugins/install \
-H 'Content-Type: application/json' -d '{"plugin_id":"weather"}'

Returns {"success": true, "message": "Plugin weather installed successfully"}.

Journal:

store_manager - INFO - Installing plugin: weather (latest branch head)
store_manager - INFO - Installing from monorepo subdirectory: plugins/ledmatrix-weather
store_manager - INFO - Downloading 259 files for plugins/ledmatrix-weather via API
store_manager - WARNING - Manifest ID 'ledmatrix-weather' doesn't match registry ID 'weather'. Renaming directory to match manifest ID.
store_manager - WARNING - Target directory ledmatrix-weather already exists, removing it
store_manager - INFO - Successfully installed plugin: ledmatrix-weather (branch main)
plugin_manager - ERROR - No manifest found for plugin: weather
api_v3 - WARNING - [PluginVersion] Could not read manifest for weather at .../plugin-repos/weather/manifest.json: No such file or directory

1. The rollback net is bypassed

install_plugin (store_manager.py:1259-1294) stages a backup before installing:

plugin_path=self.plugins_dir/plugin_idifnotplugin_path.exists():
returnself._install_plugin_impl(plugin_id, branch)
backup_path=plugin_path.with_name(f"{plugin_path.name}.standalone-backup-preinstall")
...
plugin_path.rename(backup_path)

plugin_id here is the registry id, so it checks plugin-repos/weather — which doesn't exist — and takes the no-backup fast path. The install then reaches the mismatch branch (store_manager.py:1418-1434), which removes the actual install with no net:

ifmanifest_plugin_id!=plugin_id:
correct_path=self.plugins_dir/manifest_plugin_idifcorrect_path.exists():
self.logger.warning(f"Target directory {manifest_plugin_id} already exists, removing it")
ifnotself._safe_remove_directory(correct_path):
self.logger.error(...)
returnFalseshutil.move(str(plugin_path), str(correct_path))

The docstring on install_plugin states the invariant this violates:

"_install_plugin_impl deletes the existing directory before downloading, so every failure after that point — a dropped connection, a malformed manifest, or the compatibility gate refusing the new version — left the user with no plugin at all."

In the mismatch case the download has already succeeded, so the window is narrower than that — but _safe_remove_directory followed by shutil.move can still fail partway (permissions, ENOSPC, a concurrent reader), and there is nothing to restore from. Everything the rollback machinery was written for applies here.

2. Phantom state under a nonexistent id

api_v3.install_plugin calls plugin_state_manager.set_plugin_installed(plugin_id) with the registry id. Result on this rig:

$ curl -s localhost:5000/api/v3/plugins/state | ...
weather status=installed installed_at=2026-09-04T17:26:54 <- no plugin-repos/weather
leaderboard status=installed installed_at=2026-09-03T09:47:57 <- no plugin-repos/leaderboard
music status=installed installed_at=2026-09-03T09:47:49 <- no plugin-repos/music
stocks status=installed installed_at=2026-09-03T09:48:17 <- no plugin-repos/stocks

47 state entries for 44 installed plugins. Three predate my testing. The genuine ledmatrix-weather entry meanwhile flipped to status: "unknown".

3. Success is reported for a plugin that isn't there

The same request that logs plugin_manager - ERROR - No manifest found for plugin: weather returns HTTP 200 with success: true. discover_plugins() / load_plugin(plugin_id) are called with the registry id and fail silently as far as the caller is concerned.

4. It never converges

/api/v3/plugins/installed reports manifest ids, so weather never appears there, and /plugins/store/list keeps offering it. Every click repeats the destructive rename.

Suggested fix

  • Detect the manifest id before deciding whether an existing install needs staging, and stage plugins_dir / manifest_id under the same backup discipline.
  • Switch to the manifest id for set_plugin_installed, invalidate_cache, load_plugin, and the operation-history record — the mismatch branch already reassigns plugin_id = manifest_plugin_id locally at line 1434, but that rebinding doesn't reach api_v3.
  • Return the effective (manifest) id in the API response so the caller can tell what was actually installed.
  • Consider reconciling stale state entries whose directory is absent, so existing phantoms get cleaned up.

Environment

LEDMatrix v3.3.0-4-g0730d952, 256x64 rig, plugins dir plugin-repos/. Found while installing every catalogued plugin from the store.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions