main
md 406 lines 16.6 KB
Rendered Raw
1 ---
2 name: a0-create-plugin
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 version: 1.0.0
5 tags: ["plugins", "create", "build", "develop", "extend"]
6 trigger_patterns:
7 - "create plugin"
8 - "build plugin"
9 - "new plugin"
10 - "develop plugin"
11 - "write plugin"
12 - "plugin template"
13 ---
14
15 # Agent Zero Plugin Development
16
17 > [!IMPORTANT]
18 > Always create new plugins in `/a0/usr/plugins/<plugin_name>/`. The `/a0/plugins/` directory is reserved for core system plugins.
19
20 Related skills: `/a0/skills/a0-review-plugin/SKILL.md` | `/a0/skills/a0-contribute-plugin/SKILL.md` | `/a0/skills/a0-manage-plugin/SKILL.md`
21
22 Primary references:
23 - /a0/AGENTS.md (Full-stack architecture & AgentContext)
24 - /a0/plugins/AGENTS.md (Plugin contract, plugin.yaml, settings, banners, extension contracts, Plugin Index)
25 - /a0/webui/components/AGENTS.md (Component system and modal component conventions)
26 - /a0/webui/js/AGENTS.md (Modal stack, API helpers, extension loader)
27 - /a0/webui/css/AGENTS.md (Modal CSS and shared visual primitives)
28 - /a0/docs/developer/plugins.md (Developer lifecycle and publishing)
29
30 ---
31
32 ## Step 0: Ask First — Local or Community Plugin?
33
34 Before starting, ask the user one question:
35
36 > "Should this plugin be **local only** (stays in your Agent Zero installation) or a **community plugin** (published to the Plugin Index so others can install it)?"
37
38 - **Local plugin**: Create it in `/a0/usr/plugins/<plugin_name>/`. No repository needed. Skip to the manifest section below.
39 - **Community plugin**: The plugin must live in its own GitHub repository (runtime manifest at the repo root), and then a separate index submission PR is made to https://github.com/agent0ai/a0-plugins. Guide the user through both steps.
40
41 ---
42
43 ## Plugin Manifest (plugin.yaml)
44
45 Every plugin must have a `plugin.yaml` or it will not be discovered.
46
47 ```yaml
48 name: my_plugin # required for community plugins; must match dir name (^[a-z0-9_]+$)
49 title: My Plugin
50 description: What this plugin does.
51 version: 1.0.0
52 settings_sections:
53 - agent
54 per_project_config: false
55 per_agent_config: false
56 ```
57
58 `name`: lowercase, numbers, underscores only (`^[a-z0-9_]+$`). Required by CI when submitting to the Plugin Index - must exactly match the index folder name.
59
60 `settings_sections` controls which Settings tabs show a subsection for this plugin. Valid values: `agent`, `external`, `mcp`, `developer`, `backup`. Use `[]` for no subsection.
61
62 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).
63
64 ---
65
66 ## Mandatory Frontend Patterns
67
68 ### 1. The "Store Gate" Template
69 To avoid race conditions and undefined errors, every component must use this wrapper:
70 ```html
71 <div x-data>
72 <template x-if="$store.myPluginStore">
73 <div x-init="$store.myPluginStore.onOpen()" x-destroy="$store.myPluginStore.cleanup()">
74 <!-- Content goes here -->
75 </div>
76 </template>
77 </div>
78 ```
79
80 ### 2. Separate Store Module
81 Place store logic in a separate .js file. Do NOT use alpine:init listeners inside HTML.
82 ```javascript
83 // webui/my-store.js
84 import { createStore } from "/js/AlpineStore.js";
85 export const store = createStore("myPluginStore", {
86 status: 'idle',
87 init() { ... },
88 onOpen() { ... },
89 cleanup() { ... }
90 });
91 ```
92 Import it in the HTML <head>:
93 ```html
94 <head>
95 <script type="module" src="/plugins/<plugin_name>/webui/my-store.js"></script>
96 </head>
97 ```
98
99 ### 3. User Feedback: A0 Notifications Only
100 Do **not** show errors or success via inline boxes (e.g. a red `<div>` bound to `store.error`). Use the project notification system so toasts and history stay consistent.
101
102 - **Errors**: `toastFrontendError(message, "My Plugin")` (or `$store.notificationStore.frontendError(...)`)
103 - **Success**: `toastFrontendSuccess(message, "My Plugin")`
104 - **Warnings/Info**: `toastFrontendWarning`, `toastFrontendInfo` from `/components/notifications/notification-store.js`
105
106 Import and call from your store; do not render a dedicated error/success block in the template. See [Notifications](/a0/docs/developer/notifications.md) for the full API.
107
108 ---
109
110 ## Plugin Settings
111
112 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`).
113
114 ### Settings modal contract
115
116 The modal provides Project + Agent profile context selectors. The plugin settings wrapper instantiates a local modal context from `$store.pluginSettingsPrototype`. Inside `config.html`, bind plugin fields to `config.*` and use `context.*` for modal-level state and actions:
117
118 ```html
119 <html>
120 <head>
121 <title>My Plugin Settings</title>
122 <script type="module">
123 import { store } from "/components/plugins/plugin-settings-store.js";
124 </script>
125 </head>
126 <body>
127 <div x-data>
128 <input x-model="config.my_key" />
129 <input type="checkbox" x-model="config.feature_enabled" />
130 </div>
131 </body>
132 </html>
133 ```
134
135 The modal's Save button persists `config` to `config.json` in the correct scope (project/agent/global).
136
137 ### Sidebar Button (sidebar entry point)
138 - Extension point: `sidebar-quick-actions-main-start`
139 - Class: `class="config-button"`
140 - Placement: `x-move-after=".config-button#dashboard"`
141 - Action: `@click="openModal('/plugins/<plugin_name>/webui/my-modal.html')"`
142
143 ---
144
145 ## Backend API & Context
146
147 ### Import Paths
148 - Correct: `from agent import AgentContext, AgentContextType`
149 - Correct: `from initialize import initialize_agent`
150 - Correct for plugin-local Python modules under `usr/plugins/<name>/`: `from usr.plugins.<name>.helpers.module import ...`
151 - Avoid `sys.path` hacks for plugin-local imports
152 - Avoid symlink-dependent imports like `from plugins.<name>...` for user/community plugins in `usr/plugins/`
153
154 ### Sending Messages Proactively
155 ```python
156 from agent import AgentContext
157 from helpers.messages import UserMessage
158
159 context = AgentContext.use(context_id)
160 task = context.communicate(UserMessage("Message text"))
161 response = await task.result()
162 ```
163
164 ### Reading Plugin Settings (backend)
165 ```python
166 from helpers.plugins import get_plugin_config, save_plugin_config
167
168 # Runtime (with running agent - resolves project/profile from context)
169 settings = get_plugin_config("my-plugin", agent=agent) or {}
170
171 # Explicit write target (project/profile scope)
172 save_plugin_config(
173 "my-plugin",
174 project_name="my-project",
175 agent_profile="default",
176 settings=settings,
177 )
178 ```
179
180 ### Configuration Hook Caller Context
181
182 Use caller context only when the same settings need different behavior for a
183 known origin. An unlabeled call remains compatible and uses `"api"`; a
184 plugin-controlled runtime path can opt in explicitly:
185
186 ```python
187 settings = get_plugin_config("my-plugin", agent=agent, caller="agent") or {}
188 ```
189
190 Its `hooks.py` receives `hook_context={"caller": ...}`. For example, a plugin
191 can redact a stored credential for a UI-specific path while preserving its
192 normal runtime configuration:
193
194 ```python
195 def get_plugin_config(default=None, hook_context=None, **kwargs):
196 caller = (hook_context or {}).get("caller", "api")
197 return redact_for_display(default) if caller == "ui" else default
198 ```
199
200 The available values are `"ui"`, `"agent"`, and `"api"`. Existing hooks do
201 not need to change: the framework safely ignores this new argument for hooks
202 that do not accept it. This is behavior metadata, never authorization; do not
203 use it to grant or deny access to secrets or other protected data. A
204 `config.html` alone does not set the caller; its backend load/save path must
205 pass it explicitly.
206
207 ---
208
209 ## Directory Layout
210 ```
211 /a0/usr/plugins/<name>/
212 plugin.yaml # Required manifest
213 execute.py # Optional user-triggered setup, post-install, or maintenance script
214 hooks.py # Optional framework runtime hook functions
215 default_config.yaml # Optional default settings fallback
216 README.md # Optional locally; strongly recommended for community plugins
217 LICENSE # Optional locally (shown in Plugin List UI when present); required at repo root for Plugin Index submission
218 agents/
219 <profile>/agent.yaml # Optional plugin-distributed agent profile
220 api/ # API Handlers (ApiHandler base class)
221 tools/ # Tool subclasses
222 helpers/ # Shared Python logic
223 prompts/ # Prompt templates
224 conf/
225 model_providers.yaml # Optional: add or override model providers
226 extensions/
227 python/<extension_point>/ # Named Python lifecycle extensions
228 python/_functions/<module>/<qualname>/<start|end>/ # Implicit @extensible hooks
229 webui/<point>/ # HTML/JS hook extensions
230 webui/
231 config.html # Optional: plugin settings UI
232 my-modal.html # Full plugin pages
233 my-store.js # Alpine stores
234 ```
235
236 Do not create the retired flattened extensible path form `extensions/python/<module>_<qualname>_<start|end>/`. The current runtime only resolves the deep `_functions/<module>/<qualname>/<start|end>` layout for implicit `@extensible` hooks.
237
238 ### Import rule for plugin-local Python code
239
240 Use the fully qualified `usr.plugins.<plugin_name>...` path for plugin-local
241 imports. This lets plugins keep a normal `helpers/` directory without renaming
242 it to `<name>_helpers`, and it avoids both `sys.path` mutation and symlink
243 installation steps.
244
245 Good:
246
247 ```python
248 from usr.plugins.my_plugin.helpers.runtime import do_work
249 import usr.plugins.my_plugin.helpers.state as state
250 ```
251
252 Avoid:
253
254 ```python
255 sys.path.insert(0, ...)
256 from helpers.runtime import do_work
257
258 from plugins.my_plugin.helpers.runtime import do_work
259 ```
260
261 ## Plugin Execution Script (`execute.py`)
262 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.
263
264 Good uses for `execute.py` include:
265 - installing dependencies or downloading models/assets
266 - running post-install steps after the plugin is copied into place
267 - rebuilding caches, indexes, or generated files
268 - applying migrations, repair steps, or sync jobs that the user may need to run again later
269 - performing periodic maintenance tasks that should happen only when explicitly requested by the user
270
271 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.
272
273 First rule of plugin side effects: do not modify the system permanently in ways
274 that outlive the plugin. When a plugin is deleted, there should be no leftover
275 symlinks, unmanaged services, or stray files outside plugin-owned paths unless
276 the user explicitly requested that behavior and the plugin documents how to
277 clean it up.
278
279 ```python
280 import subprocess
281 import sys
282
283 def main():
284 print("Installing plugin dependencies...")
285 result = subprocess.run(
286 [sys.executable, "-m", "pip", "install", "requests==2.31.0"],
287 text=True,
288 )
289 if result.returncode != 0:
290 print("ERROR: Installation failed")
291 return result.returncode
292
293 print("Refreshing plugin resources...")
294 # Add post-install, repair, migration, or maintenance logic here.
295
296 print("Done.")
297 return 0
298
299 if __name__ == "__main__":
300 sys.exit(main())
301 ```
302
303 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.
304
305 ## Runtime Hooks (`hooks.py`)
306 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(...)`.
307
308 - `hooks.py` runs inside the **Agent Zero framework runtime**, not the separate agent execution environment.
309 - Use it for things like install hooks, pre-update hooks, plugin registration work, cache setup, file preparation, or other internal framework operations.
310 - Current built-in usage:
311 - the plugin installer calls `install()` in `hooks.py` after placing a plugin in `usr/plugins/`
312 - the plugin updater calls `pre_update()` in `hooks.py` immediately before pulling new plugin code into place
313 - the plugin uninstaller calls `uninstall()` in `hooks.py` before deleting the plugin directory — use this to clean up any dependencies or state created by `install()`
314 - Hook functions may be sync or async.
315 - Hooks should be reversible and cleanup-safe. Prefer framework-managed state and plugin-owned paths over permanent system modifications.
316
317 ### Environment targeting rules
318 - If `hooks.py` runs `sys.executable -m pip install ...`, it installs into the same Python environment that is running Agent Zero.
319 - That is correct for dependencies needed by the plugin inside the framework runtime.
320 - If the dependency is meant for the separate agent runtime or for OS-level tools, do **not** assume the current environment is correct.
321
322 Instead, explicitly switch targets in a subprocess:
323 - invoke the exact Python interpreter for the target runtime
324 - activate the target virtualenv in the subprocess before running `pip`
325 - run the relevant OS package manager from a subprocess configured for the intended environment
326
327 In Docker, this usually means `hooks.py` affects `/opt/venv-a0` unless you intentionally target `/opt/venv` or another environment.
328
329 ---
330
331 ## Community Plugin: GitHub Repo + Plugin Index Submission
332
333 If the user chose a **community plugin**, follow these additional steps after building and testing the plugin locally.
334
335 ### 1. Repository Structure
336
337 The plugin must live in its own GitHub repository with the plugin contents at the **repository root** (not inside a subfolder):
338
339 ```text
340 your-plugin-repo/ ← GitHub repository root
341 ├── plugin.yaml ← runtime manifest (must include name field!)
342 ├── default_config.yaml
343 ├── README.md
344 ├── LICENSE ← required at repo root before Plugin Index submission
345 ├── api/
346 ├── tools/
347 ├── extensions/
348 └── webui/
349 ```
350
351 The runtime `plugin.yaml` at the repo root **must include a `name` field** matching the index folder name:
352
353 ```yaml
354 name: my_plugin # REQUIRED - must match index folder name exactly
355 title: My Plugin
356 description: What this plugin does.
357 version: 1.0.0
358 ```
359
360 Help the user create this repository and push the plugin files to it.
361
362 ### 2. Index manifest (different from runtime manifest)
363
364 The Plugin Index (`https://github.com/agent0ai/a0-plugins`) uses a **separate `index.yaml`** file that only describes discoverability — it is NOT the same as the runtime `plugin.yaml` and has a different schema:
365
366 ```yaml
367 title: My Plugin
368 description: What this plugin does.
369 github: https://github.com/yourname/your-plugin-repo
370 tags:
371 - tools
372 - example
373 screenshots: # optional, up to 5 full image URLs
374 - https://raw.githubusercontent.com/yourname/your-plugin-repo/main/docs/screen1.png
375 ```
376
377 Required fields: `title`, `description`, `github`. Optional: `tags` (up to 5), `screenshots` (up to 5 URLs).
378 See the recommended tag list at https://github.com/agent0ai/a0-plugins/blob/main/TAGS.md.
379
380 > Important: CI also checks that your remote `plugin.yaml` contains a `name` field matching the index folder name exactly.
381
382 ### 3. Submission steps
383
384 1. Fork `https://github.com/agent0ai/a0-plugins`.
385 2. Create the folder `plugins/<your_plugin_name>/` in the fork.
386 - Folder name: lowercase letters, numbers, underscores only (`^[a-z0-9_]+$`) - no hyphens
387 - Must exactly match the `name` field in your remote `plugin.yaml`
388 3. Add `index.yaml` inside it (and optionally a square thumbnail ≤ 20 KB named `thumbnail.png`, `thumbnail.jpg`, or `thumbnail.webp`).
389 4. Open a Pull Request. The PR must add exactly one new plugin folder.
390 5. CI validates automatically. A maintainer reviews and merges.
391
392 Submission constraints:
393 - Folder name: unique, stable, `^[a-z0-9_]+$`
394 - Folders starting with `_` are reserved for internal use
395 - `title` max 50 characters, `description` max 500 characters
396 - `index.yaml` max 2000 characters total
397
398 For a fully guided contribution flow (including git operations), read `/a0/skills/a0-contribute-plugin/SKILL.md`.
399
400 ---
401
402 ## Plugin Index & Plugin Hub
403
404 The **Plugin Index** is the community hub at https://github.com/agent0ai/a0-plugins.
405
406 Agent Zero now exposes indexed plugins through the built-in **Plugin Hub**. 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.