update docs, skill and enhance activation model

- Updated documentation to reflect the new plugin structure and activation rules. - Refactored activation rules related code in the API and frontend to accommodate the new plugin settings and activation logic. - Added support for clearing per-scope overrides when toggling plugins.

Alessandro committed Feb 24, 2026 at 15:02 UTC 8639069104a4fa791abeea8d7569c43247107e76
13 files changed +230 -47
AGENTS.md
+2 -1
@@ -126,9 +126,10 @@ Key Files:
126
127 ### Plugin Architecture
128 - Location: Always develop new plugins in usr/plugins/.
129 -- Manifest: Every plugin requires a plugin.json with name, description, version, and optionally settings_sections.
129 +- Manifest: Every plugin requires a plugin.yaml with name, description, version, and optionally settings_sections, per_project_config, per_agent_config, and always_enabled.
130 - Discovery: Conventions based on folder names (api/, tools/, webui/, extensions/).
131 - Settings: Use get_plugin_config(plugin_name, agent=agent) to retrieve settings. Plugins can expose a UI for settings via webui/config.html. For plugins wrapping core settings, set $store.pluginSettings.saveMode = 'core' in x-init.
132 +- Activation: Global and scoped activation rules are stored as .toggle-1 (ON) and .toggle-0 (OFF). Scoped rules are handled via the plugin "Switch" modal.
133
134 ### Lifecycle Synchronization
135 | Action | Backend Extension | Frontend Lifecycle |
AGENTS.plugins.md
+24 -12
@@ -23,12 +23,12 @@ Each plugin lives in usr/plugins/<plugin_name>/.
23
24 ```text
25 usr/plugins/<plugin_name>/
26 -├── plugin.json # Required: Name, version, settings config
26 +├── plugin.yaml # Required: Name, version, settings + activation metadata
27 ├── api/ # API handlers (ApiHandler subclasses)
28 ├── tools/ # Agent tools (Tool subclasses)
29 ├── helpers/ # Shared Python logic
30 ├── prompts/ # Prompt templates
31 -├── agents/ # Agent profiles
31 +├── agents/ # Agent profiles (agents/<profile>/agent.yaml)
32 ├── extensions/
33 │ ├── python/<point>/ # Backend lifecycle hooks
34 │ └── webui/<point>/ # UI HTML/JS contributions
@@ -37,14 +37,17 @@ usr/plugins/<plugin_name>/
37 └── ... # Full plugin pages/components
38 ```
39
40 -### plugin.json format
41 -```json
42 -{
43 - "name": "My Plugin",
44 - "description": "What this plugin does.",
45 - "version": "1.0.0",
46 - "settings_sections": ["agent"]
47 -}
40 +### plugin.yaml format
41 +```yaml
42 +name: My Plugin
43 +description: What this plugin does.
44 +version: 1.0.0
45 +settings_sections:
46 + - agent
47 +per_project_config: false
48 +per_agent_config: false
49 +# Optional: lock plugin permanently ON in UI/back-end
50 +always_enabled: false
51 ```
52 settings_sections values: agent, external, mcp, developer, backup.
53
@@ -75,16 +78,25 @@ Place *.js files in extensions/webui/<extension_point>/ and export a default asy
78 2. project/.a0proj/plugins/<name>/config.json
79 3. usr/agents/<profile>/plugins/<name>/config.json
80 4. usr/plugins/<name>/config.json
81 +5. plugins/<name>/default_config.yaml (fallback defaults)
82 +
83 +## 5. Plugin Activation Model
84 +
85 +- Global and scoped activation are independent, with no inheritance between scopes.
86 +- Activation flags are files: `.toggle-1` (ON) and `.toggle-0` (OFF).
87 +- UI states are `ON`, `OFF`, and `Advanced` (shown when any project/profile-specific override exists).
88 +- `always_enabled: true` in `plugin.yaml` forces ON and disables toggle controls in the UI.
89 +- The "Switch" modal is the canonical per-scope activation surface, and "Configure Plugin" keeps scope synchronized with the settings modal.
90
91 ---
92
81 -## 5. Routes
93 +## 6. Routes
94
95 | Route | Purpose |
96 |---|---|
97 | GET /plugins/<name>/<path> | Serve static assets |
98 | POST /api/plugins/<name>/<handler> | Call plugin API |
87 -| POST /api/plugins | Management (action: get_config, save_config) |
99 +| POST /api/plugins | Management (actions: get_config, save_config, list_configs, delete_config, toggle_plugin) |
100
101 ---
102
docs/README.md
+2
@@ -22,6 +22,7 @@ Welcome to the Agent Zero documentation hub. Whether you're getting started or d
22 ## Developer Documentation
23
24 - **[Architecture Overview](developer/architecture.md):** Understand Agent Zero's internal structure and components.
25 +- **[Plugins](developer/plugins.md):** Build plugins with `plugin.yaml`, scoped settings, and activation toggles.
26 - **[Extensions](developer/extensions.md):** Create custom extensions to extend functionality.
27 - **[Connectivity](developer/connectivity.md):** Connect to Agent Zero from external applications.
28 - **[WebSockets](developer/websockets.md):** Real-time communication infrastructure.
@@ -105,6 +106,7 @@ Welcome to the Agent Zero documentation hub. Whether you're getting started or d
106 - [Knowledge](developer/architecture.md#5-knowledge)
107 - [Skills](developer/architecture.md#6-skills)
108 - [Extensions](developer/architecture.md#7-extensions)
109 + - [Plugins](developer/plugins.md)
110 - [Extensions](developer/extensions.md)
111 - [Connectivity](developer/connectivity.md)
112 - [WebSockets](developer/websockets.md)
docs/developer/plugins.md new
+129
@@ -0,0 +1,129 @@
1 +# Plugins
2 +
3 +This page documents the current Agent Zero plugin system, including manifest format, discovery rules, scoped configuration, and activation behavior.
4 +
5 +## Overview
6 +
7 +Plugins extend Agent Zero through convention-based folders. A plugin can provide:
8 +
9 +- Backend: API handlers, tools, helpers, Python lifecycle extensions
10 +- Frontend: WebUI components and extension-point injections
11 +- Agent profiles: plugin-scoped subagent definitions
12 +- Settings: scoped plugin configuration loaded through the plugin settings store
13 +- Activation control: global and per-scope ON/OFF rules
14 +
15 +Primary roots (priority order):
16 +
17 +1. `usr/plugins/` (user/custom plugins)
18 +2. `plugins/` (core/built-in plugins)
19 +
20 +On name collisions, user plugins take precedence.
21 +
22 +## Manifest (`plugin.yaml`)
23 +
24 +Every plugin must contain `plugin.yaml`:
25 +
26 +```yaml
27 +name: My Plugin
28 +description: What this plugin does.
29 +version: 1.0.0
30 +settings_sections:
31 + - agent
32 +per_project_config: false
33 +per_agent_config: false
34 +always_enabled: false
35 +```
36 +
37 +Field reference:
38 +
39 +- `name`: UI display name
40 +- `description`: short plugin summary
41 +- `version`: plugin version string
42 +- `settings_sections`: where plugin settings appear (`agent`, `external`, `mcp`, `developer`, `backup`)
43 +- `per_project_config`: enables project-scoped settings/toggles
44 +- `per_agent_config`: enables agent-profile-scoped settings/toggles
45 +- `always_enabled`: forces ON state and disables toggle controls
46 +
47 +## Recommended Structure
48 +
49 +```text
50 +usr/plugins/<plugin_name>/
51 +├── plugin.yaml
52 +├── default_config.yaml # optional defaults
53 +├── api/ # ApiHandler implementations
54 +├── tools/ # Tool implementations
55 +├── helpers/ # shared Python logic
56 +├── prompts/
57 +├── agents/
58 +│ └── <profile>/agent.yaml # optional plugin-distributed agent profile
59 +├── extensions/
60 +│ ├── python/<extension_point>/
61 +│ └── webui/<extension_point>/
62 +└── webui/
63 + ├── config.html # optional settings UI
64 + └── ...
65 +```
66 +
67 +## Settings Resolution
68 +
69 +Plugin settings are resolved by scope. Higher priority overrides lower priority:
70 +
71 +1. `project/.a0proj/agents/<profile>/plugins/<name>/config.json`
72 +2. `project/.a0proj/plugins/<name>/config.json`
73 +3. `usr/agents/<profile>/plugins/<name>/config.json`
74 +4. `usr/plugins/<name>/config.json`
75 +5. `plugins/<name>/default_config.yaml` (fallback defaults)
76 +
77 +Notes:
78 +
79 +- Runtime reads support JSON and YAML fallback files.
80 +- Save path is scope-specific and persisted through plugin settings APIs.
81 +
82 +## Activation Model
83 +
84 +Activation is independent per scope and file-based:
85 +
86 +- `.toggle-1` means ON
87 +- `.toggle-0` means OFF
88 +- no explicit rule means ON by default
89 +
90 +WebUI activation states:
91 +
92 +- `ON`: explicit ON or implicit default
93 +- `OFF`: explicit OFF rule at selected scope
94 +- `Advanced`: at least one project/agent-profile override exists
95 +
96 +`always_enabled: true` bypasses OFF state and keeps the plugin ON in both backend and UI.
97 +
98 +## UI Flow
99 +
100 +Current plugin UX surfaces activation in two places:
101 +
102 +- Plugin list: simple ON/OFF selector, with `Advanced` option when scoped overrides are enabled
103 +- Plugin switch modal: scope-aware ON/OFF controls per project/profile, with direct handoff to settings
104 +
105 +Scope synchronization behavior:
106 +
107 +- Opening "Configure Plugin" from the switch modal propagates current scope into settings store
108 +- Switching scope in settings also mirrors into toggle store so activation status stays aligned
109 +
110 +## API Surface
111 +
112 +Core plugin management endpoint: `POST /api/plugins`
113 +
114 +Supported actions:
115 +
116 +- `get_config`
117 +- `save_config`
118 +- `list_configs`
119 +- `delete_config`
120 +- `toggle_plugin`
121 +
122 +## Migration Notes
123 +
124 +Current plugin format is YAML-based (`plugin.yaml`, `default_config.yaml`, `agent.yaml` for agent profiles). Legacy JSON manifests should be migrated.
125 +
126 +## See Also
127 +
128 +- `AGENTS.plugins.md` for full architecture details
129 +- `skills/a0-create-plugin/SKILL.md` for plugin authoring workflow
plugins/README.md
+2 -1
@@ -16,7 +16,8 @@ For detailed guides on how to create, extend, or configure plugins, please refer
16
17 ## Usage
18
19 -Plugins are automatically discovered based on the presence of a plugin.json file. Each plugin can contribute:
19 +Plugins are automatically discovered based on the presence of a plugin.yaml file. Each plugin can contribute:
20 - Backend: APIs, Tools, Helpers, and Lifecycle Extensions.
21 - Frontend: HTML/JS UI contributions via core breakpoints.
22 - Config: Isolated settings scoped per-project and per-agent profile.
23 +- Activation: Global/scoped ON-OFF rules via `.toggle-1` and `.toggle-0` files, including advanced per-scope switching in WebUI.
plugins/memory/plugin.yaml
+1 -1
@@ -4,4 +4,4 @@ version: 1.0.0
4 settings_sections:
5 - agent
6 per_project_config: true
7 -per_agent_config: false
\ No newline at end of file
7 +per_agent_config: true
\ No newline at end of file
python/api/plugins.py
+2 -1
@@ -120,6 +120,7 @@ class Plugins(ApiHandler):
120 enabled = input.get("enabled")
121 project_name = input.get("project_name", "")
122 agent_profile = input.get("agent_profile", "")
123 + clear_overrides = bool(input.get("clear_overrides", False))
124
125 if not plugin_name:
126 return Response(status=400, response="Missing plugin_name")
@@ -127,7 +128,7 @@ class Plugins(ApiHandler):
128 return Response(status=400, response="Missing enabled state")
129
130 plugins.toggle_plugin(
130 - plugin_name, bool(enabled), project_name, agent_profile
131 + plugin_name, bool(enabled), project_name, agent_profile, clear_overrides
132 )
133 return {"ok": True}
134
skills/a0-create-plugin/SKILL.md
+18 -13
@@ -1,6 +1,6 @@
1 ---
2 name: a0-create-plugin
3 -description: Create, extend, or modify Agent Zero plugins. Follows strict full-stack conventions (usr/plugins, plugin.json, Store Gating, AgentContext, plugin settings). Use for UI hooks, API handlers, lifecycle extensions, or plugin settings UI.
3 +description: Create, extend, or modify Agent Zero plugins. Follows strict full-stack conventions (usr/plugins, plugin.yaml, Store Gating, AgentContext, plugin settings). Use for UI hooks, API handlers, lifecycle extensions, or plugin settings UI.
4 ---
5
6 # Agent Zero Plugin Development
@@ -12,22 +12,24 @@ Primary references:
12 - /a0/AGENTS.md (Full-stack architecture & AgentContext)
13 - /a0/docs/agents/AGENTS.components.md (Component system deep dive)
14 - /a0/docs/agents/AGENTS.modals.md (Modal system & CSS conventions)
15 -- /a0/AGENTS.plugins.md (Extension points, plugin.json, settings system)
15 +- /a0/AGENTS.plugins.md (Extension points, plugin.yaml, settings system)
16
17 -## Plugin Manifest (plugin.json)
17 +## Plugin Manifest (plugin.yaml)
18
19 -Every plugin must have a plugin.json or it will not be discovered:
19 +Every plugin must have a plugin.yaml or it will not be discovered:
20
21 -```json
22 -{
23 - "name": "My Plugin",
24 - "description": "What this plugin does.",
25 - "version": "1.0.0",
26 - "settings_sections": ["agent"]
27 -}
21 +```yaml
22 +name: My Plugin
23 +description: What this plugin does.
24 +version: 1.0.0
25 +settings_sections:
26 + - agent
27 +per_project_config: false
28 +per_agent_config: false
29 ```
30
31 settings_sections controls which Settings tabs show a subsection for this plugin. Valid values: agent, external, mcp, developer, backup. Use [] for no subsection.
32 +Activation defaults to ON when no toggle rule exists. Set `per_project_config` and/or `per_agent_config` to enable advanced per-scope switching. Core system plugins may also use `always_enabled: true` to lock the plugin permanently ON (reserved for framework use).
33
34 ## Mandatory Frontend Patterns
35
@@ -64,7 +66,7 @@ Import it in the HTML <head>:
66
67 ## Plugin Settings
68
67 -If your plugin needs user-configurable settings, add webui/config.html. The system detects it automatically and shows a Settings button in the relevant tabs (per settings_sections in plugin.json).
69 +If your plugin needs user-configurable settings, add webui/config.html. The system detects it automatically and shows a Settings button in the relevant tabs (per settings_sections in plugin.yaml).
70
71 ### Settings modal contract
72
@@ -143,7 +145,10 @@ save_plugin_config(
145 ## Directory Layout
146 ```
147 usr/plugins/<name>/
146 - plugin.json # Required manifest
148 + plugin.yaml # Required manifest
149 + default_config.yaml # Optional default settings fallback
150 + agents/
151 + <profile>/agent.yaml # Optional plugin-distributed agent profile
152 api/ # API Handlers (ApiHandler base class)
153 tools/ # Tool subclasses
154 extensions/
webui/components/plugins/list/pluginListStore.js
+14 -5
@@ -65,11 +65,14 @@ const model = {
65 if (!pluginSettingsStore?.open) {
66 throw new Error("Plugin settings store is unavailable.");
67 }
68 - // Set saveMode before open() so loadSettings picks up the right mode
68 + await pluginSettingsStore.open(plugin.name, {
69 + perProjectConfig: !!plugin.per_project_config,
70 + perAgentConfig: !!plugin.per_agent_config,
71 + });
72 + // Set saveMode after open() (open resets it to 'plugin')
73 if (plugin.settings_sections?.includes('core')) {
74 pluginSettingsStore.saveMode = 'core';
75 }
72 - await pluginSettingsStore.open(plugin.name);
76 window.openModal?.("components/plugins/plugin-settings.html");
77 } catch (e) {
78 showErrorNotification(e, "Failed to open plugin config");
@@ -100,14 +103,20 @@ const model = {
103 }
104
105 const enabled = value === 'enabled';
103 - this.loading = true; // Show loading state
106 + const clearOverrides = plugin.toggle_state === 'advanced';
107 + if (clearOverrides && !window.confirm(
108 + `"${plugin.display_name || plugin.name}" has per-scope activation rules that will be removed. Set globally to ${enabled ? 'ON' : 'OFF'}?`
109 + )) return;
110 +
111 + this.loading = true;
112 try {
113 const response = await api.callJsonApi("plugins", {
114 action: "toggle_plugin",
115 plugin_name: plugin.name,
116 enabled: enabled,
109 - project_name: "", // Global
110 - agent_profile: "" // Global
117 + project_name: "",
118 + agent_profile: "",
119 + clear_overrides: clearOverrides,
120 });
121 if (response?.error) throw new Error(response.error);
122 await this.refresh();
webui/components/plugins/plugin-settings-store.js
+8 -1
@@ -169,6 +169,9 @@ const model = {
169 // 'core' = save via $store.settings.saveSettings() (for plugins that surface core settings)
170 saveMode: 'plugin',
171
172 + perProjectConfig: true,
173 + perAgentConfig: true,
174 +
175 isLoading: false,
176 isSaving: false,
177 error: null,
@@ -176,13 +179,15 @@ const model = {
179 // Called by the subsection button before openModal()
180 // Optional scope: { projectName, agentProfileKey } — skips redundant global loadSettings()
181 // when the caller already knows which scope to open at.
179 - async open(pluginName, { projectName = "", agentProfileKey = "" } = {}) {
182 + async open(pluginName, { projectName = "", agentProfileKey = "", perProjectConfig = true, perAgentConfig = true } = {}) {
183 this.pluginName = pluginName;
184 this.pluginMeta = null;
185 this.settings = {};
186 this.settingsSnapshotJson = "";
187 this.error = null;
188 this.saveMode = 'plugin';
189 + this.perProjectConfig = perProjectConfig;
190 + this.perAgentConfig = perAgentConfig;
191 this.projectName = projectName;
192 this.agentProfileKey = agentProfileKey;
193 this.previousProjectName = projectName;
@@ -316,6 +321,8 @@ const model = {
321 this.isListingConfigs = false;
322 this.configsError = null;
323 this.configs = [];
324 + this.perProjectConfig = true;
325 + this.perAgentConfig = true;
326 },
327
328 // Reactive URL for the plugin's settings component (used with x-html injection)
webui/components/plugins/plugin-settings.html
+8 -10
@@ -11,8 +11,9 @@
11 <div x-create="$store.pluginSettings.onModalOpen()"
12 x-destroy="$store.pluginSettings.cleanup()">
13
14 - <!-- Context toolbar: Project + Agent profile (mirrors skills list) -->
15 - <div class="plugin-settings-scope-section">
14 + <!-- Context toolbar: Project + Agent profile (only when at least one scope is configurable) -->
15 + <div class="plugin-settings-scope-section"
16 + x-show="$store.pluginSettings.perProjectConfig || $store.pluginSettings.perAgentConfig">
17 <div class="plugin-settings-scope-header">
18 <div class="plugin-settings-scope-title">Settings scope</div>
19 <div class="plugin-settings-scope-desc">This plugin supports settings per project or agent profile.</div>
@@ -24,7 +25,8 @@
25 <span class="plugin-settings-toolbar-label">Project</span>
26 <select x-model="$store.pluginSettings.projectName"
27 x-init="$nextTick(() => $el.value = $store.pluginSettings.projectName)"
27 - @change="$store.pluginSettings.onScopeChanged()">
28 + @change="$store.pluginSettings.onScopeChanged()"
29 + :disabled="!$store.pluginSettings.perProjectConfig">
30 <option value="">Global</option>
31 <template x-for="project in $store.pluginSettings.projects" :key="project.key">
32 <option :value="project.key" x-text="project.label"></option>
@@ -36,7 +38,8 @@
38 <span class="plugin-settings-toolbar-label">Agent profile</span>
39 <select x-model="$store.pluginSettings.agentProfileKey"
40 x-init="$nextTick(() => $el.value = $store.pluginSettings.agentProfileKey)"
39 - @change="$store.pluginSettings.onScopeChanged()">
41 + @change="$store.pluginSettings.onScopeChanged()"
42 + :disabled="!$store.pluginSettings.perAgentConfig">
43 <option value="">All profiles</option>
44 <template x-for="profile in $store.pluginSettings.agentProfiles" :key="profile.key">
45 <option :value="profile.key" x-text="profile.label"></option>
@@ -50,7 +53,7 @@
53
54 </div>
55
53 - <!-- Activation row: ON/OFF toggle + link to Advanced per-scope modal -->
56 + <!-- Activation row: ON/OFF toggle -->
57 <div class="plugin-settings-toolbar plugin-activation-toolbar" x-show="$store.pluginToggle">
58 <div class="plugin-settings-toolbar-row">
59 <div class="plugin-activation-toggle-group">
@@ -63,11 +66,6 @@
66 </label>
67 <span class="plugin-toggle-status-text" x-text="$store.pluginToggle?.statusLabel"></span>
68 </div>
66 - <button type="button" class="button plugin-settings-toolbar-button"
67 - @click="window.openModal?.('components/plugins/toggle/plugin-toggle-advanced.html')">
68 - <span class="icon material-symbols-outlined">toggle_on</span>
69 - Switch
70 - </button>
69 </div>
70 </div>
71 </div>
webui/components/plugins/toggle/plugin-toggle-advanced.html
+4 -2
@@ -38,7 +38,8 @@
38 <label class="plugin-settings-toolbar-item">
39 <span class="plugin-settings-toolbar-label">Project</span>
40 <select x-model="$store.pluginToggle.projectName"
41 - @change="$store.pluginToggle.onScopeChanged()">
41 + @change="$store.pluginToggle.onScopeChanged()"
42 + :disabled="!$store.pluginToggle.perProjectConfig">
43 <option value="">Global</option>
44 <template x-for="p in $store.pluginToggle.projects" :key="p.key">
45 <option :value="p.key" x-text="p.label"></option>
@@ -49,7 +50,8 @@
50 <label class="plugin-settings-toolbar-item">
51 <span class="plugin-settings-toolbar-label">Agent profile</span>
52 <select x-model="$store.pluginToggle.agentProfileKey"
52 - @change="$store.pluginToggle.onScopeChanged()">
53 + @change="$store.pluginToggle.onScopeChanged()"
54 + :disabled="!$store.pluginToggle.perAgentConfig">
55 <option value="">All profiles</option>
56 <template x-for="a in $store.pluginToggle.agentProfiles" :key="a.key">
57 <option :value="a.key" x-text="a.label"></option>
webui/components/plugins/toggle/plugin-toggle-store.js
+16
@@ -20,6 +20,8 @@ const model = {
20 // Status: 'enabled' | 'disabled'
21 status: 'enabled',
22 alwaysEnabled: false,
23 + perProjectConfig: true,
24 + perAgentConfig: true,
25 explicitPath: null,
26 configs: [],
27
@@ -35,10 +37,16 @@ const model = {
37 const pluginName = typeof plugin === 'string' ? plugin : plugin?.name;
38 this.pluginName = pluginName;
39 this.alwaysEnabled = typeof plugin === 'object' ? !!plugin.always_enabled : false;
40 + this.perProjectConfig = typeof plugin === 'object' ? !!plugin.per_project_config : true;
41 + this.perAgentConfig = typeof plugin === 'object' ? !!plugin.per_agent_config : true;
42
43 try {
44 await Promise.all([this.loadProjects(), this.loadAgentProfiles()]);
45 await this.loadConfigs();
46 + // Auto-save ON for the default scope (Global + All profiles)
47 + if (!this.explicitPath && !this.alwaysEnabled && this.pluginName) {
48 + await this.setEnabled(true);
49 + }
50 } finally {
51 this.isLoading = false;
52 }
@@ -50,6 +58,9 @@ const model = {
58 this.agentProfileKey = "";
59 this.error = null;
60 this.configs = [];
61 + this.perProjectConfig = true;
62 + this.perAgentConfig = true;
63 + this.alwaysEnabled = false;
64 },
65
66 async loadProjects() {
@@ -212,6 +223,11 @@ const model = {
223 async onScopeChanged() {
224 this.calculateStatus();
225
226 + // Auto-save immediately so the displayed default state is persisted
227 + if (!this.explicitPath && !this.alwaysEnabled && this.pluginName) {
228 + await this.setEnabled(true);
229 + }
230 +
231 // Sync scope with settings store so its loadSettings picks up the right context
232 settingsStore.projectName = this.projectName || "";
233 settingsStore.agentProfileKey = this.agentProfileKey || "";