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 || "";