fix(starlark): show installed apps with the other plugins, and let them toggle

An app installed from the store appeared nowhere and could not be enabled
or disabled. Same cause as the 404s: #253 surfaced installed apps in
/plugins/installed as `starlark:<app_id>` entries and routed `starlark:`
toggles to the Starlark manifest, and #330 removed both along with the
routes.

Without the first, the app store installs successfully into a list
nothing renders. Without the second, toggling one falls through to the
plugin_manager lookup and answers "Plugin not found" -- a Starlark app is
an entry in starlark-apps' own manifest, not a plugin in that sense.

Restored as two named helpers rather than the original inline blocks:

  _starlark_virtual_plugins()  reads the loaded plugin when there is one
                               and the on-disk manifest otherwise, so the
                               list is right before starlark-apps loads
  _toggle_starlark_app()       updates through the plugin's own
                               _update_manifest_safe when loaded, or the
                               manifest directly when not

Two changes on the original. The toggle now runs app_id through
_validate_and_sanitize_app_id first -- it reaches a filesystem manifest
and had no validation where the other Starlark routes all have it. And
_starlark_virtual_plugins swallows its own failures: the entries are
appended to the real plugin list, and a broken Starlark manifest should
cost the Starlark rows, not empty the plugins page.

6 new tests covering listing, the fields the UI keys on, toggle
persistence, the unknown-app 404, traversal rejection, and that a
Starlark failure leaves the rest of the list intact.

Full core suite: 3979 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9
This commit is contained in:
ChuckBuilds
2026-09-07 14:31:40 -04:00
co-authored by Claude Opus 5
parent 2538690435
commit 2a92f1ab3b
2 changed files with 155 additions and 0 deletions
+84
View File
@@ -2813,6 +2813,7 @@ def get_installed_plugins():
with ThreadPoolExecutor(max_workers=8) as executor:
results = list(executor.map(_build_plugin_entry, all_plugin_info))
plugins = [r for r in results if r is not None]
plugins.extend(_starlark_virtual_plugins())
return jsonify({'status': 'success', 'data': {'plugins': plugins}})
except Exception as e:
@@ -3119,6 +3120,13 @@ def toggle_plugin():
current_enabled = config.get(plugin_id, {}).get('enabled', False)
enabled = not current_enabled
# A Starlark app is not a plugin in plugin_manager's sense -- it is an
# entry in starlark-apps' own manifest -- so its enable/disable is
# handled here rather than falling through to the check below, which
# would answer "Plugin not found".
if plugin_id.startswith('starlark:'):
return _toggle_starlark_app(plugin_id[len('starlark:'):], enabled)
# Check if plugin exists in manifests (discovered but may not be loaded)
if plugin_id not in api_v3.plugin_manager.plugin_manifests:
return jsonify({'status': 'error', 'message': 'Plugin not found'}), 404
@@ -9667,3 +9675,79 @@ def get_tronbyte_categories():
except Exception as e:
logger.exception("[Starlark] get_tronbyte_categories failed")
return jsonify({'status': 'error', 'message': 'Failed to fetch categories'}), 500
def _starlark_virtual_plugins() -> list:
"""Installed Starlark apps, shaped like plugin entries.
#253 surfaced these alongside real plugins so an installed .star app can
be seen, enabled and disabled from the same list as everything else; #330
dropped it with the rest of the Starlark code, which is why an app
installs successfully and then appears nowhere.
Reads the loaded plugin when there is one and the on-disk manifest
otherwise, so the list is right before starlark-apps has been loaded too.
"""
entries = []
base = {
'version': 'starlark', 'category': 'Starlark App', 'tags': ['starlark'],
'verified': False, 'last_updated': None, 'last_commit': None,
'last_commit_message': None, 'branch': None, 'web_ui_actions': [],
'vegas_mode': 'fixed', 'vegas_content_type': 'multi',
'is_starlark_app': True,
}
try:
plugin = _get_starlark_plugin()
if plugin is not None and hasattr(plugin, 'apps'):
for app_id, app in plugin.apps.items():
m = getattr(app, 'manifest', {}) or {}
entries.append({**base,
'id': f'starlark:{app_id}',
'name': m.get('name', app_id),
'author': m.get('author', 'Tronbyte Community'),
'description': m.get('summary', 'Starlark app'),
'enabled': app.is_enabled(),
'loaded': True})
return entries
for app_id, data in (_read_starlark_manifest().get('apps', {}) or {}).items():
entries.append({**base,
'id': f'starlark:{app_id}',
'name': data.get('name', app_id),
'author': data.get('author', 'Tronbyte Community'),
'description': data.get('summary', 'Starlark app'),
'enabled': data.get('enabled', True),
'loaded': False})
except Exception:
# Never let a Starlark problem empty the whole plugins list.
logger.exception('Could not build Starlark virtual plugin entries')
return entries
def _toggle_starlark_app(app_id: str, enabled: bool):
"""Enable or disable one Starlark app, loaded or not."""
safe_id, err = _validate_and_sanitize_app_id(app_id)
if err:
return jsonify({'status': 'error', 'message': f'Invalid app_id: {err}'}), 400
plugin = _get_starlark_plugin()
if plugin is not None and safe_id in getattr(plugin, 'apps', {}):
plugin.apps[safe_id].manifest['enabled'] = enabled
def _update(manifest):
manifest['apps'][safe_id]['enabled'] = enabled
plugin._update_manifest_safe(_update)
else:
manifest = _read_starlark_manifest()
app_data = manifest.get('apps', {}).get(safe_id)
if not app_data:
return jsonify({'status': 'error',
'message': f'Starlark app not found: {safe_id}'}), 404
app_data['enabled'] = enabled
if not _write_starlark_manifest(manifest):
return jsonify({'status': 'error', 'message': 'Failed to save manifest'}), 500
return jsonify({'status': 'success',
'message': f"Starlark app {'enabled' if enabled else 'disabled'}",
'enabled': enabled})