Expose installed plugin toggles
Advertise the installed_plugins connector capability and add a protected API endpoint that lists already-installed Agent Zero plugins and toggles supported plugins only. The endpoint normalizes plugin metadata, preserves the installed-only safety boundary, and refuses changes to protected plugins such as _a0_connector so the CLI cannot disconnect itself.
Alessandro committed
May 26, 2026 at 20:05 UTC
d4dc83ba787d2bbb8c4413c0aac41e8bdd6cde84
2 files changed
+160
plugins/_a0_connector/api/v1/capabilities.py
+1
@@ -37,6 +37,7 @@ _OPTIONAL_FEATURES: dict[str, tuple[str, ...]] = {
37
"skills_list": ("helpers.skills", "helpers.files", "helpers.projects", "helpers.runtime"),
38
"skills_activate": ("helpers.skills", "helpers.persist_chat"),
39
"skills_delete": ("helpers.skills", "helpers.files", "helpers.projects", "helpers.runtime"),
40
+ "installed_plugins": ("helpers.plugins",),
41
"model_presets": ("plugins._model_config.helpers.model_config",),
42
"model_switcher": ("plugins._model_config.helpers.model_config",),
43
"browser_runtime_config": ("plugins._browser.helpers.config", "helpers.plugins"),
plugins/_a0_connector/api/v1/installed_plugins.py
new
+159
@@ -0,0 +1,159 @@
1
+"""POST /api/plugins/_a0_connector/v1/installed_plugins."""
2
+from __future__ import annotations
3
+
4
+from typing import Any, Mapping
5
+
6
+from helpers.api import Request, Response
7
+import plugins._a0_connector.api.v1.base as connector_base
8
+
9
+
10
+_PROTECTED_PLUGIN_REASONS = {
11
+ "_a0_connector": "The A0 Connector plugin keeps this CLI session connected.",
12
+}
13
+
14
+
15
+def _clean_text(value: object) -> str:
16
+ return str(value or "").strip()
17
+
18
+
19
+def _plugin_mapping(plugin: object) -> dict[str, Any]:
20
+ if isinstance(plugin, Mapping):
21
+ return dict(plugin)
22
+
23
+ model_dump = getattr(plugin, "model_dump", None)
24
+ if callable(model_dump):
25
+ data = model_dump(mode="json")
26
+ return data if isinstance(data, dict) else {}
27
+
28
+ result: dict[str, Any] = {}
29
+ for key in (
30
+ "name",
31
+ "display_name",
32
+ "description",
33
+ "version",
34
+ "author",
35
+ "repo",
36
+ "always_enabled",
37
+ "is_custom",
38
+ "has_main_screen",
39
+ "has_config_screen",
40
+ "toggle_state",
41
+ ):
42
+ if hasattr(plugin, key):
43
+ result[key] = getattr(plugin, key)
44
+ return result
45
+
46
+
47
+def _plugin_payload(plugin: object) -> dict[str, Any]:
48
+ data = _plugin_mapping(plugin)
49
+ name = _clean_text(data.get("name"))
50
+ display_name = _clean_text(data.get("display_name")) or name
51
+ toggle_state = _clean_text(data.get("toggle_state")).lower()
52
+ always_enabled = bool(data.get("always_enabled"))
53
+ enabled = always_enabled or toggle_state == "enabled"
54
+ protected_reason = _PROTECTED_PLUGIN_REASONS.get(name, "")
55
+ if always_enabled and not protected_reason:
56
+ protected_reason = "Agent Zero marks this plugin as always enabled."
57
+
58
+ return {
59
+ "name": name,
60
+ "display_name": display_name,
61
+ "description": _clean_text(data.get("description")),
62
+ "version": _clean_text(data.get("version")),
63
+ "author": _clean_text(data.get("author")),
64
+ "repo": _clean_text(data.get("repo")),
65
+ "source": "custom" if bool(data.get("is_custom")) else "builtin",
66
+ "is_custom": bool(data.get("is_custom")),
67
+ "always_enabled": always_enabled,
68
+ "enabled": enabled,
69
+ "toggle_state": "enabled" if enabled else "disabled",
70
+ "toggleable": not bool(protected_reason),
71
+ "protected_reason": protected_reason,
72
+ "has_main_screen": bool(data.get("has_main_screen")),
73
+ "has_config_screen": bool(data.get("has_config_screen")),
74
+ }
75
+
76
+
77
+def _installed_plugin_payloads() -> list[dict[str, Any]]:
78
+ from helpers import plugins
79
+
80
+ items = plugins.get_enhanced_plugins_list(custom=True, builtin=True)
81
+ payloads = [_plugin_payload(item) for item in items]
82
+ return [payload for payload in payloads if payload["name"]]
83
+
84
+
85
+def _find_installed_plugin(plugin_name: str) -> dict[str, Any] | None:
86
+ normalized = plugin_name.strip()
87
+ if not normalized:
88
+ return None
89
+ for payload in _installed_plugin_payloads():
90
+ if payload["name"] == normalized:
91
+ return payload
92
+ return None
93
+
94
+
95
+def _parse_enabled(value: object) -> bool | None:
96
+ if isinstance(value, bool):
97
+ return value
98
+ if isinstance(value, str):
99
+ normalized = value.strip().lower()
100
+ if normalized in {"1", "true", "yes", "on", "enable", "enabled"}:
101
+ return True
102
+ if normalized in {"0", "false", "no", "off", "disable", "disabled"}:
103
+ return False
104
+ return None
105
+
106
+
107
+class InstalledPlugins(connector_base.ProtectedConnectorApiHandler):
108
+ """List and toggle already-installed Agent Zero Core plugins only."""
109
+
110
+ async def process(self, input: dict, request: Request) -> dict | Response:
111
+ del request
112
+
113
+ action = _clean_text(input.get("action") or "list").lower()
114
+ if action == "list":
115
+ plugins = _installed_plugin_payloads()
116
+ enabled_count = sum(1 for plugin in plugins if plugin["enabled"])
117
+ return {
118
+ "ok": True,
119
+ "plugins": plugins,
120
+ "installed_count": len(plugins),
121
+ "enabled_count": enabled_count,
122
+ }
123
+
124
+ if action != "set_enabled":
125
+ return Response(status=400, response=f"Unknown action: {action}")
126
+
127
+ plugin_name = _clean_text(input.get("plugin_name"))
128
+ if not plugin_name:
129
+ return Response(status=400, response="Missing plugin_name")
130
+
131
+ enabled = _parse_enabled(input.get("enabled"))
132
+ if enabled is None:
133
+ return Response(status=400, response="Missing or invalid enabled state")
134
+
135
+ plugin = _find_installed_plugin(plugin_name)
136
+ if plugin is None:
137
+ return Response(status=404, response="Plugin not found")
138
+
139
+ if not plugin["toggleable"]:
140
+ return Response(
141
+ status=400,
142
+ response=plugin["protected_reason"] or "Plugin cannot be toggled.",
143
+ )
144
+
145
+ from helpers import plugins as plugin_helpers
146
+
147
+ plugin_helpers.toggle_plugin(
148
+ plugin_name,
149
+ enabled,
150
+ project_name="",
151
+ agent_profile="",
152
+ clear_overrides=False,
153
+ )
154
+ updated = _find_installed_plugin(plugin_name) or plugin
155
+ return {
156
+ "ok": True,
157
+ "plugin": updated,
158
+ "plugins": _installed_plugin_payloads(),
159
+ }