refactor(web): read plugins through a PluginCatalog; only the display runs them (#688)

The web process built its own PluginManager and loaded plugins into itself:
store installs and updates loaded or reloaded a web-side copy, and config
saves and enable/disable called on_config_change, on_enable and on_disable
on it. None of that reached the panel, and /plugins/installed reported
runtime state from those copies.

- Add PluginCatalog (src/plugin_system/plugin_catalog.py): manifests,
  directories, display modes, installed version, schema and config reads,
  with no way to run a plugin. app.py and both blueprints use it; the
  plugin_manager blueprint attribute is gone.
- Remove every lifecycle call from the web routes. Config changes already
  reach the display through ConfigService (on_config_change) and the
  enabled-set reconcile.
- Health and metrics readers move to api_v3.health_tracker /
  resource_monitor. /plugins/installed reports loaded/state/error_info as
  null (the display does not publish them) and enabled by the display's
  rule.
- Store install, update and uninstall answer restart_required when the
  running display will not pick the change up by itself
  (display_restart_required). The restart banner follows the flag via
  window.noteRestartRequired instead of the /config/main URL heuristic;
  /config/main now sends restart_required: true.
- The one remaining in-process import of plugin code (Starlark helper
  modules, oauth_flow action scripts) goes through
  _import_plugin_code_in_web_process() until a web-entry contract.
- /plugins/installed reports vegas_participation (from #682) from the
  user's setting or the manifest, with vegas_participation_source; when
  only the plugin's code decides it, null with source 'runtime', since the
  web process no longer has plugin instances to ask.
- Check & Update All keeps its restart flags when the final list refresh
  fails, and asks for a restart when an enabled plugin's first request got
  no answer and the re-sent one found it up to date.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-30 10:39:44 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent ba6eccb489
commit 7ab6fb1aff
74 changed files with 1759 additions and 676 deletions
+27 -19
View File
@@ -34,7 +34,7 @@ from src.common.path_safety import (
)
from werkzeug.exceptions import HTTPException
from src.exceptions import ConfigError
from src.plugin_system.plugin_manager import PluginManager
from src.plugin_system.plugin_catalog import PluginCatalog
from src.plugin_system.store_manager import PluginStoreManager
from src.plugin_system.saved_repositories import SavedRepositoriesManager
from src.plugin_system.schema_manager import SchemaManager
@@ -117,12 +117,6 @@ else:
# If relative, resolve relative to the project root
plugins_dir = project_root / plugins_dir_name
plugin_manager = PluginManager(
plugins_dir=str(plugins_dir),
config_manager=config_manager,
display_manager=None, # Not needed for web interface
cache_manager=None # Not needed for web interface
)
plugin_store_manager = PluginStoreManager(plugins_dir=str(plugins_dir))
# A core `git pull` update (or any checkout) restores built-in plugins
# committed under plugin-repos/, even ones the user uninstalled. Re-remove any
@@ -149,6 +143,18 @@ schema_manager = SchemaManager(
config_manager=config_manager
)
# The web process reads plugins as files and never runs them: no plugin module
# is imported, no plugin class instantiated, no lifecycle hook called here.
# Only the display process (src/display_controller.py) does that. Config
# saves reach the running plugins through the display's config watcher; what
# the display knows at run time (health, metrics, errors, current mode) it
# publishes to the shared cache. See docs/ARCHITECTURE.md.
plugin_catalog = PluginCatalog(
plugins_dir=plugins_dir,
config_manager=config_manager,
schema_manager=schema_manager,
)
# Initialize operation queue for plugin operations
operation_queue = PluginOperationQueue(max_history=500)
@@ -169,7 +175,7 @@ operation_history = OperationHistory(
)
# Plugin discovery is deferred until first API request that needs it
# This improves startup time - endpoints will call discover_plugins() when needed
# This improves startup time - endpoints call plugin_catalog.discover_plugins() when needed
# Register blueprints
from web_interface.blueprints.pages_v3 import pages_v3
@@ -177,13 +183,13 @@ from web_interface.blueprints.api_v3 import api_v3
# Initialize managers in blueprints
pages_v3.config_manager = config_manager
pages_v3.plugin_manager = plugin_manager
pages_v3.plugin_catalog = plugin_catalog
pages_v3.plugin_store_manager = plugin_store_manager
pages_v3.saved_repositories_manager = saved_repositories_manager
pages_v3.schema_manager = schema_manager
api_v3.config_manager = config_manager
api_v3.plugin_manager = plugin_manager
api_v3.plugin_catalog = plugin_catalog
api_v3.plugin_store_manager = plugin_store_manager
api_v3.saved_repositories_manager = saved_repositories_manager
api_v3.schema_manager = schema_manager
@@ -194,17 +200,19 @@ api_v3.operation_history = operation_history
from src.cache_manager import CacheManager
api_v3.cache_manager = CacheManager()
# Wire plugin health/metrics for the web process. The display service records
# health and execution-time metrics to the shared on-disk cache; giving the web
# process its own tracker/monitor backed by that same cache lets the health API
# routes (/api/v3/plugins/health, /plugins/metrics) read that persisted data.
# Plugin health and metrics as the display publishes them. The display service
# records health and execution-time metrics to the shared on-disk cache; a
# tracker/monitor backed by that same cache lets the health API routes
# (/api/v3/plugins/health, /plugins/metrics) read what it wrote.
# Guarded so any init failure degrades to "not available" rather than breaking
# the web server.
api_v3.health_tracker = None
api_v3.resource_monitor = None
try:
from src.plugin_system.plugin_health import PluginHealthTracker
from src.plugin_system.resource_monitor import PluginResourceMonitor
plugin_manager.health_tracker = PluginHealthTracker(api_v3.cache_manager)
plugin_manager.resource_monitor = PluginResourceMonitor(api_v3.cache_manager)
api_v3.health_tracker = PluginHealthTracker(api_v3.cache_manager)
api_v3.resource_monitor = PluginResourceMonitor(api_v3.cache_manager)
except Exception as _hm_err: # pragma: no cover - defensive startup guard
logging.getLogger(__name__).warning(
"Could not enable plugin health/metrics for web UI: %s", _hm_err
@@ -960,7 +968,7 @@ def _run_startup_reconciliation() -> None:
reconciler = StateReconciliation(
state_manager=plugin_state_manager,
config_manager=config_manager,
plugin_manager=plugin_manager,
plugin_manager=plugin_catalog,
plugins_dir=plugins_dir,
store_manager=plugin_store_manager
)
@@ -968,7 +976,7 @@ def _run_startup_reconciliation() -> None:
if result.inconsistencies_found:
_logger.info("[Reconciliation] %s", result.message)
if result.inconsistencies_fixed:
plugin_manager.discover_plugins()
plugin_catalog.discover_plugins()
if not result.reconciliation_successful:
_logger.warning(
"[Reconciliation] Finished with %d unresolved issue(s); "
@@ -1043,7 +1051,7 @@ def start_auto_update_scheduler():
config_manager=config_manager,
core_update=perform_core_update,
store_manager=plugin_store_manager,
plugin_manager=plugin_manager,
plugin_catalog=plugin_catalog,
schema_manager=schema_manager,
operation_history=operation_history,
)