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.
Summary
When a store entry's registry id differs from the manifest id inside it,
store_managerdeletes the existing install outside the rollback protection thatinstall_pluginexists 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-weather2.6.6 installed. Registry entryweatherhasplugin_path: plugins/ledmatrix-weather, whose manifest saysid: ledmatrix-weather.Returns
{"success": true, "message": "Plugin weather installed successfully"}.Journal:
1. The rollback net is bypassed
install_plugin(store_manager.py:1259-1294) stages a backup before installing:plugin_idhere is the registry id, so it checksplugin-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:The docstring on
install_pluginstates the invariant this violates:In the mismatch case the download has already succeeded, so the window is narrower than that — but
_safe_remove_directoryfollowed byshutil.movecan 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_plugincallsplugin_state_manager.set_plugin_installed(plugin_id)with the registry id. Result on this rig:47 state entries for 44 installed plugins. Three predate my testing. The genuine
ledmatrix-weatherentry meanwhile flipped tostatus: "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: weatherreturns HTTP 200 withsuccess: 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/installedreports manifest ids, soweathernever appears there, and/plugins/store/listkeeps offering it. Every click repeats the destructive rename.Suggested fix
plugins_dir / manifest_idunder the same backup discipline.set_plugin_installed,invalidate_cache,load_plugin, and the operation-history record — the mismatch branch already reassignsplugin_id = manifest_plugin_idlocally at line 1434, but that rebinding doesn't reachapi_v3.Environment
LEDMatrix
v3.3.0-4-g0730d952, 256x64 rig, plugins dirplugin-repos/. Found while installing every catalogued plugin from the store.