rename init to execute.py; update docs + create plugin skill
Alessandro committed
Mar 13, 2026 at 11:25 UTC
7b1f3fbbacd4b5e812ad020120e802437bd62104
15 files changed
+98
-635
api/plugins.py
+2
-2
@@ -284,9 +284,9 @@ class Plugins(ApiHandler):
284
if not plugin_dir:
285
return Response(status=404, response="Plugin not found")
286
287
- init_script = files.get_abs_path(plugin_dir, "initialize.py")
287
+ init_script = files.get_abs_path(plugin_dir, "execute.py")
288
if not files.exists(init_script):
289
- return Response(status=404, response="initialize.py not found")
289
+ return Response(status=404, response="execute.py not found")
290
291
executed_at = datetime.now(timezone.utc).isoformat()
292
try:
docs/README.md
+1
@@ -70,6 +70,7 @@ Welcome to the Agent Zero documentation hub. Whether you're getting started or d
70
- [User Guides](#user-guides)
71
- [Usage Guide](guides/usage.md)
72
- [Basic Operations](guides/usage.md#basic-operations)
73
+ - [Plugins and Marketplace](guides/usage.md#plugins-and-marketplace)
74
- [Tool Usage](guides/usage.md#tool-usage)
75
- [Projects](guides/usage.md#projects)
76
- [What Projects Provide](guides/usage.md#what-projects-provide)
docs/agents/AGENTS.plugins.md
+17
-3
@@ -24,7 +24,7 @@ Each plugin lives in usr/plugins/<plugin_name>/.
24
```text
25
usr/plugins/<plugin_name>/
26
├── plugin.yaml # Required: Title, version, settings + activation metadata
27
-├── initialize.py # Optional: one-time setup script (dependencies, models, etc.)
27
+├── execute.py # Optional: user-triggered plugin script
28
├── hooks.py # Optional: runtime hook functions callable by the framework
29
├── default_config.yaml # Optional: fallback settings defaults
30
├── README.md # Optional: shown in Plugin List UI
@@ -69,6 +69,15 @@ Field reference:
69
- `per_agent_config`: Enables agent-profile-scoped settings and toggle rules
70
- `always_enabled`: Forces ON and disables toggle controls in the UI (reserved for framework use)
71
72
+### execute.py (plugin script)
73
+
74
+Plugins can include an optional `execute.py` file at the plugin root for user-triggered work such as setup, post-install steps, maintenance, migrations, repair flows, or resource refreshes. It is started manually from the Plugins UI, never automatically, and should print progress while returning `0` on success.
75
+
76
+Design guidance:
77
+- use `execute.py` for manual operations the user may need to run again later
78
+- prefer making it rerunnable or state-aware
79
+- avoid placing framework-internal automatic behavior here; that belongs in `hooks.py` or lifecycle extensions
80
+
81
### hooks.py (framework runtime hooks)
82
83
Plugins can include an optional `hooks.py` file at the plugin root. Agent Zero loads this module on demand and calls exported functions by name through `helpers.plugins.call_plugin_hook(...)`.
@@ -253,9 +262,14 @@ Index submission rules:
262
- `title` max 50 characters, `description` max 500 characters
263
- `tags`: optional, up to 5, use recommended tags from https://github.com/agent0ai/a0-plugins/blob/main/TAGS.md
264
256
-### Plugin Marketplace (Coming Soon)
265
+### Plugin Marketplace
266
+
267
+The marketplace is provided by the always-enabled `_plugin_installer` plugin. Users can reach it from the **Plugins** dialog in two ways:
268
+
269
+- the **Browse** tab in `webui/components/plugins/list/plugin-list.html`
270
+- the **Install** toolbar action injected by `plugins/_plugin_installer/extensions/webui/plugins-list-header-buttons/install-buttons.html`, which opens `plugins/_plugin_installer/webui/main.html` on its own **Browse** tab
271
258
-A built-in **Plugin Marketplace** plugin (always active) will allow users to browse the Plugin Index and install or update community plugins directly from the Agent Zero UI. This section will be updated once the marketplace plugin is released.
272
+Both routes surface Plugin Index entries inside Agent Zero. The marketplace supports search, filtering, sorting, and a detail view with README content and installation actions.
273
274
---
275
docs/developer/architecture.md
+1
-1
@@ -65,7 +65,7 @@ This architecture ensures:
65
| `usr/secrets.env` | Secrets store (managed via Settings -> Secrets) |
66
| `conf/model_providers.yaml` | Model provider defaults and settings |
67
| `agent.py` | Core agent implementation |
68
-| `initialize.py` | Framework initialization |
68
+| `execute.py` | Framework initialization |
69
| `models.py` | Model providers and configs |
70
| `preload.py` | Pre-initialization routines |
71
| `prepare.py` | Environment preparation |
docs/developer/plugins.md
+19
-8
@@ -49,7 +49,7 @@ Field reference:
49
```text
50
usr/plugins/<plugin_name>/
51
├── plugin.yaml
52
-├── initialize.py # optional one-time setup script
52
+├── execute.py # optional user-triggered plugin script
53
├── hooks.py # optional runtime hook functions callable by the framework
54
├── default_config.yaml # optional defaults
55
├── README.md # optional, shown in Plugin List UI
@@ -68,12 +68,13 @@ usr/plugins/<plugin_name>/
68
└── ...
69
```
70
71
-## Plugin Initialization (`initialize.py`)
71
+## Plugin Script (`execute.py`)
72
73
-Plugins can include an optional `initialize.py` at the plugin root for one-time setup such as installing dependencies, downloading models, or preparing databases.
73
+Plugins can include an optional `execute.py` at the plugin root for user-triggered operations such as setup, post-install actions, maintenance, repair steps, or other manual tasks that should run only when explicitly requested.
74
75
-- Triggered manually via the **Init** button in the Plugin List UI — never runs automatically
76
-- Execution is tracked in `usr/plugins/<plugin_name>/init_exec.json` (timestamp + exit code)
75
+- Triggered manually from the Plugin List UI — never runs automatically
76
+- Suitable for rerunnable operations such as refreshing caches, rebuilding generated files, running migrations, or syncing plugin-managed resources
77
+- Execution state is recorded per plugin with timestamp and exit code metadata
78
- The modal streams output in real time and shows success/failure on completion
79
80
```python
@@ -89,6 +90,7 @@ def main():
90
if result.returncode != 0:
91
print("ERROR: Installation failed")
92
return result.returncode
93
+ print("Refreshing plugin resources...")
94
print("Done.")
95
return 0
96
@@ -96,7 +98,7 @@ if __name__ == "__main__":
98
sys.exit(main())
99
```
100
99
-Return `0` on success, non-zero on failure. Print progress for user feedback. Use `sys.executable` for pip commands.
101
+Return `0` on success, non-zero on failure. Print progress for user feedback. Use `sys.executable` for pip commands. Prefer making the script safe to run more than once; if reruns are not safe, detect the current state and print a clear explanation.
102
103
## Runtime Hooks (`hooks.py`)
104
@@ -107,6 +109,8 @@ Plugins can also include an optional `hooks.py` at the plugin root. Agent Zero l
109
- Hook functions may be synchronous or async.
110
- Hook modules are cached, so edits may require a plugin refresh or cache clear before changes are picked up.
111
112
+Use `execute.py` when the user should explicitly decide when the operation runs. Use `hooks.py` or lifecycle extensions when the work belongs to framework-managed behavior.
113
+
114
Current built-in usage: the plugin installer calls `install()` from `hooks.py` after copying a plugin into place.
115
116
### Dependency and environment behavior
@@ -244,9 +248,16 @@ Submission rules:
248
- `description`: max 500 characters
249
- `tags`: optional, up to 5, see https://github.com/agent0ai/a0-plugins/blob/main/TAGS.md
250
247
-### Plugin Marketplace (Coming Soon)
251
+### Plugin Marketplace
252
+
253
+Agent Zero now exposes the community **Plugin Marketplace** through the always-enabled **Plugin Installer** plugin. Users can browse Plugin Index entries directly from the Plugins UI without leaving the application.
254
+
255
+Users can open the marketplace from the **Plugins** dialog in two ways:
256
+
257
+- click the **Browse** tab after **Custom** and **Builtin**
258
+- click **Install** in the plugin list toolbar to open the installer modal, which starts on its own **Browse** tab
259
249
-A built-in **Plugin Marketplace** (always-active plugin) will allow users to browse the Plugin Index and install or update community plugins directly from the Agent Zero UI without leaving the application. This section will be updated once the marketplace plugin is released.
260
+The marketplace supports search, filtering, sorting, and a plugin detail view with README content and the install action.
261
262
## User Feedback in Plugin UI (Notifications)
263
docs/guides/usage.md
+21
@@ -74,6 +74,27 @@ Access the chat history in JSON format
74
> [!TIP]
75
> Use the Context and History buttons to understand how the agent interprets your instructions and debug any unexpected behavior.
76
77
+### Plugins and Marketplace
78
+Open the **Plugins** dialog from the sidebar quick actions by clicking the plugin icon.
79
+
80
+
81
+
82
+From this view you can manage installed plugins, review plugin details, open plugin settings, and change activation state.
83
+
84
+#### Open the Plugin Marketplace
85
+There are two ways to reach the marketplace from the Plugins dialog:
86
+
87
+1. Click the **Browse** tab after **Custom** and **Builtin** to switch the current dialog into marketplace mode.
88
+2. Click the **Install** button in the top-right toolbar to open the installer modal, which starts on its own **Browse** tab.
89
+
90
+
91
+
92
+The marketplace lets you search community plugins, filter by tags, sort the listing, and open a plugin card for more context before installing.
93
+
94
+
95
+
96
+Opening a plugin shows its author, tags, README content, and install action. Once you are in the detail view, the next steps are intentionally straightforward.
97
+
98
### File Attachments
99
Agent Zero supports direct file attachments in the chat interface for seamless file operations:
100
docs/res/usage/plugins/marketplace-main-view.png
Binary files /dev/null and b/docs/res/usage/plugins/marketplace-main-view.png differ
docs/res/usage/plugins/marketplace-plugin-detail.png
Binary files /dev/null and b/docs/res/usage/plugins/marketplace-plugin-detail.png differ
docs/res/usage/plugins/plugins-list.png
Binary files /dev/null and b/docs/res/usage/plugins/plugins-list.png differ
helpers/plugins.py
+1
-1
@@ -162,7 +162,7 @@ def get_enhanced_plugins_list(
162
has_config_screen = files.exists(str(d / "webui" / "config.html"))
163
has_readme = files.exists(str(d / "README.md"))
164
has_license = files.exists(str(d / "LICENSE"))
165
- has_init_script = files.exists(str(d / "initialize.py"))
165
+ has_init_script = files.exists(str(d / "execute.py"))
166
toggle_state = get_toggle_state(d.name)
167
current_commit = ""
168
current_commit_timestamp = ""
plugins/README.md
+12
-4
@@ -41,9 +41,15 @@ per_agent_config: false
41
always_enabled: false
42
```
43
44
-## Plugin Initialization (`initialize.py`)
44
+## Plugin Script (`execute.py`)
45
46
-Plugins can include an optional `initialize.py` at the plugin root for one-time setup such as installing dependencies or downloading models. Users trigger it via the **Init** button in the Plugin List UI. The script should return `0` on success and print progress messages for user feedback.
46
+Plugins can include an optional `execute.py` at the plugin root for user-triggered operations such as setup, post-install steps, maintenance, repairs, migrations, or resource refreshes. Users trigger it from the Plugin List UI.
47
+
48
+Guidelines:
49
+- Treat it as a manual plugin script, not as the primary way to use the plugin
50
+- Prefer making it safe to rerun, or detect state and explain why a rerun is not appropriate
51
+- Return `0` on success and print progress messages for user feedback
52
+- Use `hooks.py` instead when the behavior is framework-internal or should happen automatically
53
54
## Runtime Hooks (`hooks.py`)
55
@@ -77,6 +83,8 @@ tags:
83
84
Note: The index `plugin.yaml` is a **different schema** from the runtime manifest — it contains only `title`, `description`, `github`, and optional `tags`. Do not mix them up.
85
80
-## Plugin Marketplace (Coming Soon)
86
+## Plugin Marketplace
87
+
88
+Agent Zero now includes a built-in marketplace flow through the always-enabled **Plugin Installer** plugin. From the **Plugins** dialog, users can either open the **Browse** tab or click **Install**, which opens the installer modal on its own **Browse** tab.
89
82
-A built-in **Plugin Marketplace** (always-active plugin) is planned and will allow users to browse the Plugin Index and install community plugins directly from the Agent Zero UI.
90
+The marketplace surfaces Plugin Index entries directly in the UI and lets users search, filter, inspect, and install community plugins without leaving Agent Zero.
plugins/_chat_branching/extensions/webui/set_messages_after_loop/plugins.py
deleted
-606
@@ -1,606 +0,0 @@
1
-from __future__ import annotations
2
-
3
-import asyncio
4
-import re, json, glob
5
-import time
6
-from pathlib import Path
7
-from typing import (
8
- Any,
9
- Dict,
10
- Iterator,
11
- List,
12
- Literal,
13
- Optional,
14
- TYPE_CHECKING,
15
- TypedDict,
16
-)
17
-
18
-from helpers import files, notification, print_style, yaml as yaml_helper, cache
19
-from pydantic import BaseModel, Field
20
-
21
-from helpers.defer import DeferredTask
22
-
23
-if TYPE_CHECKING:
24
- from agent import Agent
25
-
26
-# Extracts target selector from <meta name="plugin-target" content="...">
27
-_META_TARGET_RE = re.compile(
28
- r'<meta\s+name=["\']plugin-target["\']\s+content=["\']([^"\']+)["\']',
29
- re.IGNORECASE,
30
-)
31
-
32
-type ToggleState = Literal["enabled", "disabled", "advanced"]
33
-
34
-
35
-class PluginAssetFile(TypedDict):
36
- path: str
37
- project_name: str
38
- agent_profile: str
39
-
40
-
41
-META_FILE_NAME = "plugin.yaml"
42
-CONFIG_FILE_NAME = "config.json"
43
-CONFIG_DEFAULT_FILE_NAME = "default_config.yaml"
44
-DISABLED_FILE_NAME = ".toggle-0"
45
-ENABLED_FILE_NAME = ".toggle-1"
46
-TOGGLE_FILE_PATTERN = ".toggle-[01]"
47
-_last_frontend_reload_notification_at = 0.0
48
-
49
-
50
-class PluginMetadata(BaseModel):
51
- name: str = ""
52
- title: str = ""
53
- description: str = ""
54
- version: str = ""
55
- settings_sections: List[str] = Field(default_factory=list)
56
- per_project_config: bool = False
57
- per_agent_config: bool = False
58
- always_enabled: bool = False
59
-
60
-
61
-class PluginListItem(BaseModel):
62
- name: str
63
- path: str
64
- display_name: str = ""
65
- description: str = ""
66
- version: str = ""
67
- settings_sections: List[str] = Field(default_factory=list)
68
- per_project_config: bool = False
69
- per_agent_config: bool = False
70
- always_enabled: bool = False
71
- is_custom: bool = False
72
- has_main_screen: bool = False
73
- has_config_screen: bool = False
74
- has_readme: bool = False
75
- has_license: bool = False
76
- has_init_script: bool = False
77
- toggle_state: ToggleState = "disabled"
78
-
79
-
80
-def after_plugin_change(plugin_names: list[str] | None = None):
81
- clear_plugin_cache()
82
- send_frontend_reload_notification(plugin_names)
83
-
84
-
85
-def clear_plugin_cache():
86
- cache.clear("*(plugins)*")
87
-
88
-
89
-def get_plugin_roots(plugin_name: str = "") -> List[str]:
90
- """Plugin root directories, ordered by priority (user first)."""
91
- return [
92
- files.get_abs_path(files.USER_DIR, files.PLUGINS_DIR, plugin_name),
93
- files.get_abs_path(files.PLUGINS_DIR, plugin_name),
94
- ]
95
-
96
-
97
-def get_plugins_list():
98
- result: list[str] = []
99
- seen_names: set[str] = set()
100
- for root in get_plugin_roots():
101
- for dir in Path(root).iterdir():
102
- if not dir.is_dir() or dir.name.startswith("."):
103
- continue
104
- if dir.name in seen_names:
105
- continue
106
- if files.exists(str(dir), META_FILE_NAME):
107
- seen_names.add(dir.name)
108
- result.append(dir.name)
109
- result.sort(key=lambda p: Path(p).name)
110
- return result
111
-
112
-
113
-def get_enhanced_plugins_list(
114
- custom: bool = True, builtin: bool = True
115
-) -> List[PluginListItem]:
116
- """Discover plugins by directory convention. First root wins on ID conflict."""
117
- results = []
118
-
119
- def load_plugins(root_path: str, is_custom: bool):
120
- for d in sorted(Path(root_path).iterdir(), key=lambda p: p.name):
121
- try:
122
- if not d.is_dir() or d.name.startswith("."):
123
- continue
124
- meta_file = str(d / META_FILE_NAME)
125
- if not files.exists(meta_file):
126
- continue
127
- meta = PluginMetadata.model_validate(files.read_file_yaml(meta_file))
128
- has_main_screen = files.exists(str(d / "webui" / "main.html"))
129
- has_config_screen = files.exists(str(d / "webui" / "config.html"))
130
- has_readme = files.exists(str(d / "README.md"))
131
- has_license = files.exists(str(d / "LICENSE"))
132
- has_init_script = files.exists(str(d / "initialize.py"))
133
- toggle_state = get_toggle_state(d.name)
134
- results.append(
135
- PluginListItem(
136
- name=d.name,
137
- path=str(d),
138
- display_name=meta.title or d.name,
139
- description=meta.description,
140
- version=meta.version,
141
- settings_sections=meta.settings_sections,
142
- per_project_config=meta.per_project_config,
143
- per_agent_config=meta.per_agent_config,
144
- always_enabled=meta.always_enabled,
145
- is_custom=is_custom,
146
- has_main_screen=has_main_screen,
147
- has_config_screen=has_config_screen,
148
- has_readme=has_readme,
149
- has_license=has_license,
150
- has_init_script=has_init_script,
151
- toggle_state=toggle_state,
152
- )
153
- )
154
- except Exception as e:
155
- print_style.PrintStyle.error(f"Failed to load plugin {d.name}: {e}")
156
- continue
157
-
158
- if custom:
159
- load_plugins(files.get_abs_path(files.USER_DIR, files.PLUGINS_DIR), True)
160
- if builtin:
161
- load_plugins(files.get_abs_path(files.PLUGINS_DIR), False)
162
- return results
163
-
164
-
165
-def get_plugin_meta(plugin_name: str):
166
- plugin_dir = find_plugin_dir(plugin_name)
167
- if not plugin_dir:
168
- return None
169
- return PluginMetadata.model_validate(
170
- files.read_file_yaml(files.get_abs_path(plugin_dir, META_FILE_NAME))
171
- )
172
-
173
-
174
-def find_plugin_dir(plugin_name: str):
175
- if not plugin_name:
176
- return None
177
-
178
- # check if the plugin is in the user directory
179
- user_plugin_path = files.get_abs_path(
180
- files.USER_DIR, files.PLUGINS_DIR, plugin_name, META_FILE_NAME
181
- )
182
- if files.exists(user_plugin_path):
183
- return files.get_abs_path(files.USER_DIR, files.PLUGINS_DIR, plugin_name)
184
-
185
- # check if the plugin is in the default directory
186
- default_plugin_path = files.get_abs_path(
187
- files.PLUGINS_DIR, plugin_name, META_FILE_NAME
188
- )
189
- if files.exists(default_plugin_path):
190
- return files.get_abs_path(files.PLUGINS_DIR, plugin_name)
191
-
192
- return None
193
-
194
-
195
-def delete_plugin(plugin_name: str):
196
- plugin_dir = find_plugin_dir(plugin_name)
197
- if not plugin_dir:
198
- raise FileNotFoundError(f"Plugin '{plugin_name}' not found")
199
- custom_plugins_dir = files.get_abs_path(files.USER_DIR, files.PLUGINS_DIR)
200
- if not files.is_in_dir(plugin_dir, custom_plugins_dir):
201
- raise ValueError("Only custom plugins can be deleted")
202
- send_frontend_reload_notification([plugin_name]) # send before deletion to properly check the extensions, second notification will be skipped automatically
203
- files.delete_dir(plugin_dir)
204
- after_plugin_change([plugin_name])
205
-
206
-
207
-def get_plugin_paths(*subpaths: str) -> List[str]:
208
- sub = "*/" + "/".join(subpaths) if subpaths else "*"
209
- paths: List[str] = []
210
- for root in get_plugin_roots():
211
- paths.extend(
212
- files.find_existing_paths_by_pattern(files.get_abs_path(root, sub))
213
- )
214
- return paths
215
-
216
-
217
-def get_enabled_plugin_paths(agent: Agent | None, *subpaths: str) -> List[str]:
218
- enabled = get_enabled_plugins(agent)
219
- paths: list[str] = []
220
-
221
- for plugin in enabled:
222
- base_dir = find_plugin_dir(plugin)
223
- if not base_dir:
224
- continue
225
-
226
- if not subpaths:
227
- if files.exists(base_dir):
228
- paths.append(base_dir)
229
- continue
230
-
231
- path_pattern = files.get_abs_path(base_dir, *subpaths)
232
- paths.extend(files.find_existing_paths_by_pattern(path_pattern))
233
-
234
- return paths
235
-
236
-
237
-def get_enabled_plugins(agent: Agent | None):
238
- plugins = get_plugins_list()
239
- active = []
240
-
241
- for plugin in plugins:
242
- # plugins are toggled via .enabled / .disabled files
243
- # every plugin is on by default, unless disabled in usr dir
244
- enabled = True
245
-
246
- # root plugin paths
247
- plugin_paths = get_plugin_roots(plugin)
248
-
249
- # + agent paths
250
- if agent:
251
- from helpers import subagents
252
-
253
- agent_paths = subagents.get_paths(
254
- agent,
255
- files.PLUGINS_DIR,
256
- plugin,
257
- must_exist_completely=True,
258
- include_default=False,
259
- include_user=False,
260
- include_plugins=False,
261
- include_project=True,
262
- )
263
- plugin_paths = agent_paths + plugin_paths
264
-
265
- # go through paths in reverse order and determine the state
266
- enabled = determined_toggle_from_paths(enabled, reversed(plugin_paths))
267
-
268
- if enabled:
269
- active.append(plugin)
270
-
271
- return active
272
-
273
-
274
-def determined_toggle_from_paths(default: bool, paths: Iterator[str]):
275
- enabled = default
276
- for plugin_path in paths:
277
- if enabled:
278
- enabled = not files.exists(
279
- files.get_abs_path(plugin_path, DISABLED_FILE_NAME)
280
- )
281
- else:
282
- enabled = files.exists(files.get_abs_path(plugin_path, ENABLED_FILE_NAME))
283
- return enabled
284
-
285
-
286
-def get_toggle_state(plugin_name: str) -> ToggleState:
287
- meta = get_plugin_meta(plugin_name)
288
- if not meta:
289
- return "disabled"
290
- if meta.always_enabled:
291
- return "enabled"
292
-
293
- # root plugin paths
294
- plugin_paths = get_plugin_roots(plugin_name)
295
- state = (
296
- "enabled"
297
- if determined_toggle_from_paths(True, reversed(plugin_paths))
298
- else "disabled"
299
- )
300
-
301
- # global toggles
302
- usr_toggles = [
303
- files.find_existing_paths_by_pattern(
304
- files.get_abs_path(files.PLUGINS_DIR, plugin_name, TOGGLE_FILE_PATTERN)
305
- ),
306
- files.find_existing_paths_by_pattern(
307
- files.get_abs_path(
308
- files.USER_DIR, files.PLUGINS_DIR, plugin_name, TOGGLE_FILE_PATTERN
309
- )
310
- ),
311
- ]
312
-
313
- # additional toggles in project/agent directories, return advanced
314
- if meta.per_agent_config or meta.per_project_config:
315
- configs = find_plugin_assets(
316
- TOGGLE_FILE_PATTERN,
317
- plugin_name=plugin_name,
318
- project_name="*" if meta.per_project_config else "",
319
- agent_profile="*" if meta.per_agent_config else "",
320
- only_first=False,
321
- )
322
-
323
- # Advanced if there are specific overrides (project or agent specific)
324
- if any(c.get("project_name") or c.get("agent_profile") for c in configs):
325
- state = "advanced"
326
-
327
- return state
328
-
329
-
330
-def toggle_plugin(
331
- plugin_name: str,
332
- enabled: bool,
333
- project_name: str = "",
334
- agent_profile: str = "",
335
- clear_overrides: bool = False,
336
-):
337
- if clear_overrides:
338
- all_toggles = find_plugin_assets(
339
- TOGGLE_FILE_PATTERN,
340
- plugin_name=plugin_name,
341
- project_name="*",
342
- agent_profile="*",
343
- only_first=False,
344
- )
345
- for toggle in all_toggles:
346
- files.delete_file(toggle["path"])
347
-
348
- enabled_file = determine_plugin_asset_path(
349
- plugin_name, project_name, agent_profile, ENABLED_FILE_NAME
350
- )
351
- disabled_file = determine_plugin_asset_path(
352
- plugin_name, project_name, agent_profile, DISABLED_FILE_NAME
353
- )
354
-
355
- # ensure clean state by deleting both potential files first
356
- files.delete_file(enabled_file)
357
- files.delete_file(disabled_file)
358
-
359
- if enabled:
360
- files.write_file(enabled_file, "")
361
- else:
362
- files.write_file(disabled_file, "")
363
- after_plugin_change([plugin_name])
364
-
365
-
366
-def get_plugin_config(
367
- plugin_name: str,
368
- agent: Agent | None = None,
369
- project_name: str | None = None,
370
- agent_profile: str | None = None,
371
-):
372
-
373
- if project_name is None and agent is not None:
374
- from helpers import projects
375
-
376
- project_name = projects.get_context_project_name(agent.context)
377
- if agent_profile is None and agent is not None:
378
- agent_profile = agent.config.profile
379
-
380
- # find config.json in all possible places
381
- file = find_plugin_asset(
382
- plugin_name,
383
- CONFIG_FILE_NAME,
384
- project_name=project_name or "",
385
- agent_profile=agent_profile or "",
386
- )
387
- file_path = file.get("path", "") if file else ""
388
-
389
- # use default config if not found
390
- if not file_path:
391
- file_path = files.get_abs_path(
392
- find_plugin_dir(plugin_name), CONFIG_DEFAULT_FILE_NAME
393
- )
394
- if file_path and files.exists(file_path):
395
- return (
396
- json.loads if file_path.lower().endswith(".json") else yaml_helper.loads
397
- )(files.read_file(file_path))
398
- return None
399
-
400
-
401
-def get_default_plugin_config(plugin_name: str):
402
- file_path = files.get_abs_path(
403
- find_plugin_dir(plugin_name), CONFIG_DEFAULT_FILE_NAME
404
- )
405
- if file_path and files.exists(file_path):
406
- return (
407
- json.loads if file_path.lower().endswith(".json") else yaml_helper.loads
408
- )(files.read_file(file_path))
409
- return None
410
-
411
-
412
-def save_plugin_config(
413
- plugin_name: str, project_name: str, agent_profile: str, settings: dict
414
-):
415
- file_path = determine_plugin_asset_path(
416
- plugin_name, project_name, agent_profile, CONFIG_FILE_NAME
417
- )
418
- if file_path:
419
- files.write_file(file_path, json.dumps(settings))
420
- after_plugin_change([plugin_name])
421
-
422
-
423
-def find_plugin_asset(
424
- plugin_name: str, *subpaths: str, project_name="", agent_profile=""
425
-):
426
- result = find_plugin_assets(
427
- *subpaths,
428
- plugin_name=plugin_name,
429
- project_name=project_name,
430
- agent_profile=agent_profile,
431
- only_first=True,
432
- )
433
- return result[0] if result else None
434
-
435
-
436
-def find_plugin_assets(
437
- *subpaths: str,
438
- plugin_name: str = "*",
439
- project_name: str = "*",
440
- agent_profile: str = "*",
441
- only_first: bool = False,
442
-) -> list[PluginAssetFile]:
443
- from helpers import projects, subagents
444
-
445
- results: list[PluginAssetFile] = []
446
-
447
- def _collect(path: str, proj: str, profile: str) -> bool:
448
- is_glob = glob.has_magic(path)
449
- matched_paths = (
450
- files.find_existing_paths_by_pattern(path)
451
- if is_glob
452
- else ([path] if files.exists(path) else [])
453
- )
454
-
455
- need_proj = proj == "*"
456
- need_prof = profile == "*"
457
-
458
- def _after(s: str, marker: str, last: bool = False) -> str:
459
- i = s.rfind(marker) if last else s.find(marker)
460
- if i == -1:
461
- return ""
462
- start = i + len(marker)
463
- end = s.find("/", start)
464
- return s[start:] if end == -1 else s[start:end]
465
-
466
- for matched in matched_paths:
467
- inferred_proj = _after(matched, "/projects/") if need_proj else proj
468
- inferred_prof = (
469
- _after(matched, "/agents/", last=True) if need_prof else profile
470
- )
471
- results.append(
472
- {
473
- "project_name": inferred_proj,
474
- "agent_profile": inferred_prof,
475
- "path": matched,
476
- }
477
- )
478
- if only_first:
479
- return True
480
- return False
481
-
482
- # project/.a0proj/agents/<profile>/plugins/<plugin_name>/...
483
- if project_name:
484
- if agent_profile:
485
- path = projects.get_project_meta(
486
- project_name,
487
- files.AGENTS_DIR,
488
- agent_profile,
489
- files.PLUGINS_DIR,
490
- plugin_name,
491
- *subpaths,
492
- )
493
- if _collect(path, project_name, agent_profile):
494
- return results
495
- if not agent_profile or agent_profile == "*":
496
- # project/.a0proj/plugins/<plugin_name>/...
497
- path = projects.get_project_meta(
498
- project_name, files.PLUGINS_DIR, plugin_name, *subpaths
499
- )
500
- if _collect(path, project_name, ""):
501
- return results
502
-
503
- # usr/agents/<profile>/plugins/<plugin_name>/...
504
- if agent_profile:
505
- path = files.get_abs_path(
506
- subagents.USER_AGENTS_DIR,
507
- agent_profile,
508
- files.PLUGINS_DIR,
509
- plugin_name,
510
- *subpaths,
511
- )
512
- if _collect(path, "", agent_profile):
513
- return results
514
-
515
- # usr?/plugins/<any_plugin>/agents/<profile>/plugins/<plugin_name>/...
516
- for plugin_base in get_enabled_plugin_paths(None):
517
- path = files.get_abs_path(
518
- plugin_base,
519
- files.AGENTS_DIR,
520
- agent_profile,
521
- files.PLUGINS_DIR,
522
- plugin_name,
523
- *subpaths,
524
- )
525
- if _collect(path, "", agent_profile):
526
- return results
527
-
528
- # agents/<profile>/plugins/<plugin_name>/...
529
- path = files.get_abs_path(
530
- subagents.DEFAULT_AGENTS_DIR,
531
- agent_profile,
532
- files.PLUGINS_DIR,
533
- plugin_name,
534
- *subpaths,
535
- )
536
- if _collect(path, "", agent_profile):
537
- return results
538
-
539
- # usr/plugins/<plugin_name>/...
540
- path = files.get_abs_path(files.USER_DIR, files.PLUGINS_DIR, plugin_name, *subpaths)
541
- if _collect(path, "", ""):
542
- return results
543
-
544
- # plugins/<plugin_name>/...
545
- path = files.get_abs_path(files.PLUGINS_DIR, plugin_name, *subpaths)
546
- _collect(path, "", "")
547
-
548
- return results
549
-
550
-
551
-def determine_plugin_asset_path(
552
- plugin_name: str, project_name: str, agent_profile: str, *subpaths: str
553
-):
554
- base_path = files.get_abs_path(files.USER_DIR)
555
-
556
- if project_name:
557
- from helpers import projects
558
-
559
- base_path = projects.get_project_meta(project_name)
560
-
561
- if agent_profile:
562
- base_path = files.get_abs_path(base_path, files.AGENTS_DIR, agent_profile)
563
-
564
- return files.get_abs_path(base_path, files.PLUGINS_DIR, plugin_name, *subpaths)
565
-
566
-
567
-def send_frontend_reload_notification(plugin_names: list[str] | None = None):
568
- """If the plugin changed has webui extensions, notify frontend to reload the page"""
569
- global _last_frontend_reload_notification_at
570
-
571
- display_time = 5
572
- now = time.monotonic()
573
- if now - _last_frontend_reload_notification_at < display_time:
574
- return
575
-
576
- if plugin_names:
577
- has_webui_extension = False
578
- for plugin_name in plugin_names:
579
- plugin_dir = find_plugin_dir(plugin_name)
580
- if plugin_dir and files.exists(
581
- files.get_abs_path(plugin_dir, "extensions", "webui")
582
- ):
583
- has_webui_extension = True
584
- break
585
- if not has_webui_extension:
586
- return
587
-
588
- async def _send_later():
589
- global _last_frontend_reload_notification_at
590
-
591
- await asyncio.sleep(1)
592
-
593
- _last_frontend_reload_notification_at = time.monotonic()
594
-
595
- notification.NotificationManager.send_notification(
596
- type=notification.NotificationType.INFO,
597
- priority=notification.NotificationPriority.NORMAL,
598
- title="Plugins with frontend extensions updated, page plugins/_plugin_scanmended",
599
- message="""<button type="button" class="button confirm" onclick="window.location.reload()"><span class="icon material-symbols-outlined">refresh</span>Reload page</button>""",
600
- detail="",
601
- display_time=display_time,
602
- group="plugins_changed",
603
- id="plugins_frontend_reload",
604
- )
605
-
606
- DeferredTask().start_task(_send_later)
plugins/_plugin_installer/webui/install-detail.html
+4
-3
@@ -88,7 +88,7 @@
88
<template x-if="$store.pluginInstallStore.installedPluginInfo.has_init_script">
89
<button type="button" class="button"
90
@click="$store.pluginInstallStore.handleOpenInit()">
91
- <span class="icon material-symbols-outlined">terminal</span> Init
91
+ <span class="icon material-symbols-outlined">terminal</span> Script
92
</button>
93
</template>
94
<button type="button" class="button"
@@ -508,10 +508,10 @@
508
flex: 1;
509
min-width: 200px;
510
padding: 0.85rem 1.5rem;
511
- border: 2px solid var(--color-highlight);
511
+ border: 2px solid var(--color-primary);
512
border-radius: 8px;
513
background: transparent;
514
- color: var(--color-highlight);
514
+ color: var(--color-text);
515
font-size: 1rem;
516
font-weight: 600;
517
cursor: pointer;
@@ -526,6 +526,7 @@
526
.pi-btn-discussion:hover {
527
background: var(--color-highlight);
528
color: #fff;
529
+ border: 2px solid var(--color-highlight);
530
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
531
}
532
skills/a0-create-plugin/SKILL.md
+18
-5
@@ -177,7 +177,7 @@ save_plugin_config(
177
```
178
/a0/usr/plugins/<name>/
179
plugin.yaml # Required manifest
180
- initialize.py # Optional one-time setup script
180
+ execute.py # Optional user-triggered setup, post-install, or maintenance script
181
hooks.py # Optional framework runtime hook functions
182
default_config.yaml # Optional default settings fallback
183
README.md # Optional, shown in Plugin List UI
@@ -195,8 +195,17 @@ save_plugin_config(
195
my-store.js # Alpine stores
196
```
197
198
-## Plugin Initialization Script (`initialize.py`)
199
-If your plugin requires one-time setup (e.g., installing dependencies, downloading models), add an `initialize.py` at the plugin root:
198
+## Plugin Execution Script (`execute.py`)
199
+If your plugin needs a user-triggered script for setup, post-install work, maintenance, or other manual operations, add an `execute.py` at the plugin root.
200
+
201
+Good uses for `execute.py` include:
202
+- installing dependencies or downloading models/assets
203
+- running post-install steps after the plugin is copied into place
204
+- rebuilding caches, indexes, or generated files
205
+- applying migrations, repair steps, or sync jobs that the user may need to run again later
206
+- performing periodic maintenance tasks that should happen only when explicitly requested by the user
207
+
208
+Use `execute.py` for **user-initiated** work. If the behavior is framework-internal or should happen automatically as part of plugin lifecycle handling, use `hooks.py` or lifecycle extensions instead.
209
210
```python
211
import subprocess
@@ -211,6 +220,10 @@ def main():
220
if result.returncode != 0:
221
print("ERROR: Installation failed")
222
return result.returncode
223
+
224
+ print("Refreshing plugin resources...")
225
+ # Add post-install, repair, migration, or maintenance logic here.
226
+
227
print("Done.")
228
return 0
229
@@ -218,7 +231,7 @@ if __name__ == "__main__":
231
sys.exit(main())
232
```
233
221
-Users trigger it via the **Init** button in the Plugin List UI. Return `0` on success, non-zero on failure.
234
+Users trigger it from the Plugins UI. Treat it as a manual, rerunnable operation: return `0` on success, non-zero on failure, and print progress so the user can understand what happened. When possible, make it safe to run more than once; if reruns are not safe, detect the state and print a clear message.
235
236
## Runtime Hooks (`hooks.py`)
237
If your plugin needs framework-internal hook points, add a `hooks.py` file at the plugin root. The framework can call exported functions by name via `helpers.plugins.call_plugin_hook(...)`.
@@ -300,4 +313,4 @@ Help the user prepare the fork, the index manifest, and draft the PR.
313
314
The **Plugin Index** is the community hub at https://github.com/agent0ai/a0-plugins.
315
303
-A **Plugin Marketplace** (a built-in always-active plugin) is planned and will allow users to browse, install, and update indexed plugins directly from the Agent Zero UI. When available, this skill will be updated to guide users through marketplace-based installation as well.
316
+Agent Zero now exposes indexed plugins through the built-in **Plugin Marketplace**. Users can open it from the **Plugins** dialog either through the **Browse** tab or through the **Install** button, then inspect plugin details and install directly from the UI.
webui/components/plugins/list/plugin-init-modal.html
+2
-2
@@ -17,7 +17,7 @@
17
<span class="material-symbols-outlined idle-icon">terminal</span>
18
<div class="idle-text">
19
<span class="idle-headline">Press <strong>Run</strong> to initialize <strong x-text="$store.pluginInitStore.pluginDisplayName"></strong></span>
20
- <span class="idle-sub">This will execute the plugin's <code>initialize.py</code> setup script.</span>
20
+ <span class="idle-sub">This will execute the plugin's <code>execute.py</code> setup script.</span>
21
<template x-if="$store.pluginInitStore.lastExec">
22
<span class="last-exec-info">
23
Last run:
@@ -36,7 +36,7 @@
36
37
<div class="plugin-init-status" x-show="$store.pluginInitStore.running">
38
<span class="material-symbols-outlined spin">progress_activity</span>
39
- <span>Running initialize.py...</span>
39
+ <span>Running execute.py...</span>
40
</div>
41
42
<div class="plugin-init-exit" x-show="!$store.pluginInitStore.running && $store.pluginInitStore.exitCode !== null">