feat: A0 plugins skillset; update docs; add agent plugin scan endpoint

skills: offload debug from a0-manage-plugin; add a0-debug-plugin add endpoint for agent-facing plugin security scan Combine queue and start calls into a synchronous operation, works as the frontend modal version with the agent installing the plugin that gets returned scan results. For the helper, if the agent had to build the prompt itself, it would need to: Fetch plugin-scan-checks.json from the server Fetch plugin-scan-prompt.md from the server Interpolate all 8 template variables Send the resulting ~3KB prompt as part of its own context That's ~3KB of prompt template burning context window on every scan call, plus two extra HTTP requests. With helpers/prompt.py doing it server-side, the agent just sends {"git_url": "...", "checks": [...]} - a handful of tokens - and the server assembles the full prompt internally.

Alessandro committed Mar 13, 2026 at 13:26 UTC 2df1a3e43eea91cd0f32f9bbc6e01ea3bec97f61
14 files changed +1479 -32
docs/agents/AGENTS.plugins.md
+18 -10
@@ -46,9 +46,10 @@ usr/plugins/<plugin_name>/
46
47 ### plugin.yaml (runtime manifest)
48
49 -This is the manifest file that lives inside your plugin directory and drives runtime behavior. It is distinct from the index manifest used when publishing to the Plugin Index (see Section 7).
49 +This is the manifest file that lives inside your plugin directory and drives runtime behavior. It is distinct from the index manifest (`index.yaml`) used when publishing to the Plugin Index (see Section 7).
50
51 ```yaml
52 +name: my_plugin # required for community plugins (^[a-z0-9_]+$, must match dir name)
53 title: My Plugin
54 description: What this plugin does.
55 version: 1.0.0
@@ -61,6 +62,7 @@ always_enabled: false
62 ```
63
64 Field reference:
65 +- `name`: Plugin identifier. Required by CI when submitting to the Plugin Index. Must be `^[a-z0-9_]+$` and match the index folder name exactly.
66 - `title`: UI display name
67 - `description`: Short plugin summary
68 - `version`: Plugin version string
@@ -200,12 +202,13 @@ embedding:
202
203 The **Plugin Index** is a community-maintained repository at https://github.com/agent0ai/a0-plugins that lists plugins available to the Agent Zero community. Plugins listed there can be discovered and installed by other users.
204
203 -### Two Distinct plugin.yaml Files
205 +### Two Distinct Manifest Files
206
205 -There are two completely different `plugin.yaml` schemas used at different stages. They must not be confused:
207 +There are two completely different manifest files used at different stages. They must not be confused:
208
207 -**Runtime manifest** (inside your plugin repo/directory, drives Agent Zero behavior):
209 +**Runtime manifest** (`plugin.yaml`, inside your plugin repo/directory — drives Agent Zero behavior):
210 ```yaml
211 +name: my_plugin # REQUIRED for index submission; must match index folder name
212 title: My Plugin
213 description: What this plugin does.
214 version: 1.0.0
@@ -216,7 +219,7 @@ per_agent_config: false
219 always_enabled: false
220 ```
221
219 -**Index manifest** (submitted to the `a0-plugins` repo under `plugins/<your-plugin-name>/`, drives discoverability only):
222 +**Index manifest** (`index.yaml`, submitted to the `a0-plugins` repo under `plugins/<your_plugin_name>/` — drives discoverability only):
223 ```yaml
224 title: My Plugin
225 description: What this plugin does.
@@ -224,9 +227,11 @@ github: https://github.com/yourname/your-plugin-repo
227 tags:
228 - tools
229 - example
230 +screenshots: # optional, up to 5 full image URLs
231 + - https://raw.githubusercontent.com/yourname/your-plugin-repo/main/docs/screen.png
232 ```
233
229 -The index manifest contains only four fields (`title`, `description`, `github`, `tags`) and must not include runtime fields. The `github` field must point to the root of a GitHub repository that itself contains a runtime `plugin.yaml` at the repository root.
234 +The index manifest is named `index.yaml` (not `plugin.yaml`). Required fields: `title`, `description`, `github`. Optional: `tags` (up to 5), `screenshots` (up to 5 URLs). The `github` field must point to the root of a GitHub repository that contains a runtime `plugin.yaml` at the repository root, and that `plugin.yaml` must include a `name` field matching the index folder name exactly.
235
236 ### Repository Structure for Community Plugins
237
@@ -248,19 +253,22 @@ Users install it locally by cloning (or downloading) the repo contents into `/a0
253
254 ### Submitting to the Plugin Index
255
251 -1. Create a GitHub repository for your plugin with the runtime `plugin.yaml` at the repo root.
256 +1. Create a GitHub repository for your plugin with the runtime `plugin.yaml` (including the `name` field) at the repo root.
257 2. Fork `https://github.com/agent0ai/a0-plugins`.
253 -3. Create a folder `plugins/<your-plugin-name>/` containing only an index `plugin.yaml` (and optionally a square thumbnail image ≤ 20 KB).
258 +3. Create a folder `plugins/<your_plugin_name>/` containing only an `index.yaml` (and optionally a square thumbnail image ≤ 20 KB).
259 4. Open a Pull Request with exactly one new plugin folder.
260 5. CI validates the submission automatically. A maintainer reviews and merges.
261
262 Index submission rules:
263 - One plugin per PR
259 -- Folder name must be unique, stable, lowercase, kebab-case
264 +- Folder name: unique, stable, `^[a-z0-9_]+$` (lowercase, numbers, underscores — no hyphens)
265 +- Folder name must exactly match the `name` field in your remote `plugin.yaml`
266 - Folders starting with `_` are reserved for internal use
261 -- `github` must point to a public repo that contains `plugin.yaml` at its root
267 +- `github` must point to a public repo that contains `plugin.yaml` at its root with a matching `name` field
268 - `title` max 50 characters, `description` max 500 characters
269 +- `index.yaml` total max 2000 characters
270 - `tags`: optional, up to 5, use recommended tags from https://github.com/agent0ai/a0-plugins/blob/main/TAGS.md
271 +- `screenshots`: optional, up to 5 full image URLs (png/jpg/webp, each ≤ 2 MB)
272
273 ### Plugin Marketplace
274
docs/developer/plugins.md
+17 -9
@@ -24,6 +24,7 @@ On name collisions, user plugins take precedence.
24 Every plugin must contain `plugin.yaml`. This is the **runtime manifest** — it drives Agent Zero behavior. It is distinct from the index manifest used when publishing to the Plugin Index (see [Publishing to the Plugin Index](#publishing-to-the-plugin-index) below).
25
26 ```yaml
27 +name: my_plugin # required for community plugins (^[a-z0-9_]+$, must match dir name)
28 title: My Plugin
29 description: What this plugin does.
30 version: 1.0.0
@@ -36,6 +37,7 @@ always_enabled: false
37
38 Field reference:
39
40 +- `name`: plugin identifier; required by CI for index submission; must be `^[a-z0-9_]+$` and match the index folder name exactly
41 - `title`: UI display name
42 - `description`: short plugin summary
43 - `version`: plugin version string
@@ -189,12 +191,13 @@ Supported actions:
191
192 The **Plugin Index** is a community-maintained repository at https://github.com/agent0ai/a0-plugins. Plugins listed there are discoverable by all Agent Zero users.
193
192 -### Two Distinct plugin.yaml Files
194 +### Two Distinct Manifest Files
195
194 -There are two completely different `plugin.yaml` schemas — they must not be confused:
196 +There are two completely different manifest files — they must not be confused:
197
196 -**Runtime manifest** (inside your plugin's own repo, drives Agent Zero behavior):
198 +**Runtime manifest** (`plugin.yaml`, inside your plugin's own repo — drives Agent Zero behavior):
199 ```yaml
200 +name: my_plugin # REQUIRED for index submission; must match index folder name
201 title: My Plugin
202 description: What this plugin does.
203 version: 1.0.0
@@ -205,7 +208,7 @@ per_agent_config: false
208 always_enabled: false
209 ```
210
208 -**Index manifest** (submitted to `a0-plugins` under `plugins/<your-plugin-name>/`, drives discoverability only):
211 +**Index manifest** (`index.yaml`, submitted to `a0-plugins` under `plugins/<your_plugin_name>/` — drives discoverability only):
212 ```yaml
213 title: My Plugin
214 description: What this plugin does.
@@ -213,9 +216,11 @@ github: https://github.com/yourname/your-plugin-repo
216 tags:
217 - tools
218 - example
219 +screenshots: # optional, up to 5 full image URLs
220 + - https://raw.githubusercontent.com/yourname/your-plugin-repo/main/docs/screen.png
221 ```
222
218 -The index manifest has only four fields (`title`, `description`, `github`, `tags`). The `github` URL must point to a public GitHub repository that contains a runtime `plugin.yaml` at the **repository root**.
223 +The index manifest file is named `index.yaml` (not `plugin.yaml`). Required fields: `title`, `description`, `github`. Optional: `tags` (up to 5), `screenshots` (up to 5 URLs). The `github` URL must point to a public GitHub repository that contains a runtime `plugin.yaml` at the **repository root**, and that `plugin.yaml` must include a `name` field matching the index folder name exactly.
224
225 ### Repository Structure for Community Plugins
226
@@ -223,7 +228,7 @@ Plugin repos should expose the plugin contents at the repo root, so they can be
228
229 ```text
230 your-plugin-repo/ ← GitHub repository root
226 -├── plugin.yaml ← runtime manifest
231 +├── plugin.yaml ← runtime manifest (must include name field)
232 ├── default_config.yaml
233 ├── README.md
234 ├── LICENSE
@@ -235,18 +240,21 @@ your-plugin-repo/ ← GitHub repository root
240
241 ### Submission Process
242
238 -1. Create a GitHub repository with the runtime `plugin.yaml` at the repo root.
243 +1. Create a GitHub repository with the runtime `plugin.yaml` (including the `name` field) at the repo root.
244 2. Fork `https://github.com/agent0ai/a0-plugins`.
240 -3. Add `plugins/<your-plugin-name>/plugin.yaml` (index manifest) to your fork, and optionally a square thumbnail image (≤ 20 KB, named `thumbnail.png|jpg|webp`).
245 +3. Create folder `plugins/<your_plugin_name>/` and add `index.yaml` (the index manifest, not `plugin.yaml`). Optionally add a square thumbnail image (≤ 20 KB, named `thumbnail.png|jpg|webp`).
246 4. Open a Pull Request. One PR must add exactly one new plugin folder.
247 5. CI validates automatically. A maintainer reviews and merges.
248
249 Submission rules:
245 -- Folder name: unique, stable, lowercase, kebab-case
250 +- Folder name: unique, stable, `^[a-z0-9_]+$` (lowercase, numbers, underscores — no hyphens)
251 +- Folder name must exactly match the `name` field in your remote `plugin.yaml`
252 - Folders starting with `_` are reserved for internal use
253 - `title`: max 50 characters
254 - `description`: max 500 characters
255 +- `index.yaml` total: max 2000 characters
256 - `tags`: optional, up to 5, see https://github.com/agent0ai/a0-plugins/blob/main/TAGS.md
257 +- `screenshots`: optional, up to 5 full image URLs (png/jpg/webp, each ≤ 2 MB)
258
259 ### Plugin Marketplace
260
plugins/README.md
+5 -3
@@ -68,8 +68,8 @@ The **Plugin Index** at https://github.com/agent0ai/a0-plugins is the community-
68
69 To share a plugin with the community:
70
71 -1. Create a standalone GitHub repository with the plugin contents at the repo root and the runtime `plugin.yaml` there.
72 -2. Fork `https://github.com/agent0ai/a0-plugins` and add a folder `plugins/<your-plugin-name>/` containing a separate index `plugin.yaml`:
71 +1. Create a standalone GitHub repository with the plugin contents at the repo root. The runtime `plugin.yaml` must include a `name` field matching the intended index folder name.
72 +2. Fork `https://github.com/agent0ai/a0-plugins` and add a folder `plugins/<your_plugin_name>/` containing a separate index manifest named `index.yaml` (not `plugin.yaml`):
73
74 ```yaml
75 title: My Plugin
@@ -79,9 +79,11 @@ tags:
79 - tools
80 ```
81
82 +Optional additional fields: `screenshots` (up to 5 image URLs).
83 +
84 3. Open a Pull Request. CI validates the submission; a maintainer reviews and merges.
85
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.
86 +Note: The index `index.yaml` is a **different file with a different schema** from the runtime `plugin.yaml`. Folder names use `^[a-z0-9_]+$` (underscores, no hyphens) and must match the `name` field in the remote `plugin.yaml` exactly.
87
88 ## Plugin Marketplace
89
plugins/_plugin_scan/api/plugin_scan_run.py new
+41
@@ -0,0 +1,41 @@
1 +from agent import AgentContext, UserMessage
2 +from helpers.api import ApiHandler, Input, Output, Request, Response
3 +from helpers import guids, message_queue as mq
4 +from plugins._plugin_scan.helpers.prompt import build_prompt
5 +
6 +
7 +class PluginScanRun(ApiHandler):
8 + """
9 + POST /api/plugins/_plugin_scan/plugin_scan_run
10 + Body: { "git_url": "https://github.com/...", "checks": [...] } # checks optional, defaults to all
11 + Returns: { "ok": true, "verdict": "safe|caution|dangerous|unknown", "report": "<markdown>" }
12 +
13 + Combines plugin_scan_queue + plugin_scan_start into one synchronous call and awaits the result.
14 + No server-side timeout - set an appropriate client-side timeout (repos can take 5+ min to scan).
15 + """
16 +
17 + async def process(self, input: Input, request: Request) -> Output:
18 + git_url: str = input.get("git_url", "").strip()
19 + if not git_url:
20 + return Response("Missing 'git_url'.", 400)
21 +
22 + ctxid = guids.generate_id()
23 + try:
24 + context = self.use_context(ctxid)
25 + prompt = build_prompt(git_url, input.get("checks"))
26 + mq.log_user_message(context, prompt, [])
27 + task = context.communicate(UserMessage(prompt, []))
28 + report: str = await task.result()
29 + except Exception as e:
30 + return Response(f"Scan failed: {e}", 500)
31 + finally:
32 + try:
33 + AgentContext.remove(ctxid)
34 + except Exception:
35 + pass
36 +
37 + return {
38 + "ok": True,
39 + "git_url": git_url,
40 + "report": report or "",
41 + }
plugins/_plugin_scan/helpers/__init__.py
plugins/_plugin_scan/helpers/prompt.py new
+30
@@ -0,0 +1,30 @@
1 +import json
2 +from pathlib import Path
3 +
4 +_DIR = Path(__file__).parent.parent
5 +_CFG = json.loads((_DIR / "webui" / "plugin-scan-checks.json").read_text())
6 +_TMPL = (_DIR / "webui" / "plugin-scan-prompt.md").read_text()
7 +
8 +
9 +def build_prompt(git_url: str, checks: list | None = None) -> str:
10 + ratings, all_checks = _CFG["ratings"], _CFG["checks"]
11 + keys = [k for k in (checks or all_checks) if k in all_checks]
12 +
13 + subs = {
14 + "GIT_URL": git_url,
15 + "SELECTED_CHECKS": "\n".join(f"- **{all_checks[k]['label']}**" for k in keys),
16 + "CHECK_DETAILS": "\n\n".join(
17 + f"#### {c['label']}\n{c['detail']}\n\nCriteria:\n"
18 + + "\n".join(f" - {ratings[l]['icon']} {d}" for l, d in c["criteria"].items())
19 + for c in (all_checks[k] for k in keys)
20 + ),
21 + "STATUS_LEGEND": "\n".join(f"- {r['icon']} **{r['label']}**" for r in ratings.values()),
22 + "RATING_ICONS": "/".join(r["icon"] for r in ratings.values()),
23 + "RATING_PASS": ratings["pass"]["icon"],
24 + "RATING_WARNING": ratings["warning"]["icon"],
25 + "RATING_FAIL": ratings["fail"]["icon"],
26 + }
27 + prompt = _TMPL
28 + for key, val in subs.items():
29 + prompt = prompt.replace(f"{{{{{key}}}}}", val)
30 + return prompt
plugins/_plugin_scan/webui/plugin-scan-prompt.md
+2 -2
@@ -41,7 +41,7 @@ Verify all of the following. If any is false, go back and fix it:
41
42 ## Output Format
43
44 -Your ENTIRE response must be a single markdown document with EXACTLY this structure. No preamble, no commentary, no extra sections. Start your response directly with the `#` heading.
44 +Submit your final report using the **`response` tool**. The `text` argument must be a single markdown document with EXACTLY this structure. No preamble, no commentary, no extra sections. Start your response directly with the `#` heading.
45
46 **Section 1** — Title line: `# 🛡️ Security Scan Report: {plugin title}`
47
@@ -65,7 +65,7 @@ Status icons: {{STATUS_LEGEND}}
65
66 ## Constraints
67
68 -- Do NOT output any text before the `#` title heading
68 +- The `text` argument of the `response` tool must start directly with the `#` title heading — no text before it
69 - Do NOT include your internal analysis process in the report
70 - Do NOT add checks beyond the list above
71 - Do NOT summarize multiple files into one finding
skills/a0-contribute-plugin/SKILL.md new
+229
@@ -0,0 +1,229 @@
1 +---
2 +name: a0-contribute-plugin
3 +description: Guide for publishing an Agent Zero plugin to the community Plugin Index (a0-plugins repo). Covers GitHub repo setup, index.yaml creation, CI validation rules, and PR submission. Use when the user wants to share, publish, submit, or contribute a plugin to the marketplace so other Agent Zero users can find and install it.
4 +version: 1.0.0
5 +tags: ["plugins", "contribute", "publish", "marketplace", "community", "index", "PR"]
6 +trigger_patterns:
7 + - "contribute plugin"
8 + - "publish plugin"
9 + - "share plugin"
10 + - "submit plugin"
11 + - "plugin marketplace"
12 + - "plugin index"
13 + - "community plugin"
14 + - "open source plugin"
15 +---
16 +
17 +# Agent Zero Plugin Contribution
18 +
19 +This skill guides publishing a plugin to the [Plugin Index](https://github.com/agent0ai/a0-plugins), making it discoverable and installable by all Agent Zero users.
20 +
21 +---
22 +
23 +## Prerequisites
24 +
25 +Before starting, verify:
26 +
27 +1. Plugin exists and works locally in `/a0/usr/plugins/<name>/`
28 +2. Plugin has been reviewed - if not, offer to run `a0-review-plugin` first:
29 + > "I recommend running a full review before contributing. Should I do that now?"
30 +3. User has a GitHub account and `git` / `gh` CLI available
31 +
32 +---
33 +
34 +## Step 0: Ask Automation Preference
35 +
36 +Before doing any git work, ask:
37 +
38 +> "Do you want me to handle the git operations (fork, branch, commit, PR) automatically, or would you prefer I give you the steps to run manually?"
39 +
40 +- **Automatic**: proceed using `gh` and `git` commands via the code execution tool
41 +- **Manual**: provide exact commands at each step for the user to run
42 +
43 +---
44 +
45 +## Step 1: Prepare the Plugin GitHub Repository
46 +
47 +The plugin must live in its **own standalone GitHub repository** with plugin contents at the **repo root** (not inside a subfolder).
48 +
49 +### Required repo structure
50 +
51 +```text
52 +your-plugin-repo/ <- GitHub repository root
53 +├── plugin.yaml <- runtime manifest (REQUIRED)
54 +├── README.md <- strongly recommended (shown in marketplace detail view)
55 +├── LICENSE <- strongly recommended
56 +├── default_config.yaml <- optional
57 +├── api/
58 +├── tools/
59 +├── extensions/
60 +└── webui/
61 +```
62 +
63 +### Runtime `plugin.yaml` requirements
64 +
65 +The remote `plugin.yaml` must include a **`name` field** - this is validated by CI and must exactly match the index folder name:
66 +
67 +```yaml
68 +name: my_plugin # REQUIRED - must match index folder name (^[a-z0-9_]+$)
69 +title: My Plugin
70 +description: What this plugin does.
71 +version: 1.0.0
72 +settings_sections: []
73 +per_project_config: false
74 +per_agent_config: false
75 +always_enabled: false
76 +```
77 +
78 +If the plugin was built locally, help the user create the GitHub repo and push it:
79 +
80 +```bash
81 +# Create repo (automatic mode - using gh CLI)
82 +gh repo create <repo-name> --public --description "Agent Zero plugin: <title>"
83 +git init
84 +git add .
85 +git commit -m "feat: initial plugin commit"
86 +git remote add origin https://github.com/<user>/<repo-name>.git
87 +git push -u origin main
88 +```
89 +
90 +---
91 +
92 +## Step 2: Choose the Index Folder Name
93 +
94 +The folder name in the index must:
95 +- Match the `name` field in your remote `plugin.yaml` **exactly**
96 +- Follow `^[a-z0-9_]+$` (lowercase letters, numbers, underscores - **no hyphens**)
97 +- Be unique in the index
98 +- Not start with `_` (reserved for internal use)
99 +
100 +Verify uniqueness by fetching the current index:
101 +```
102 +https://github.com/agent0ai/a0-plugins/releases/download/generated-index/index.json
103 +```
104 +
105 +Check that the intended name does not appear as a key in `plugins`.
106 +
107 +---
108 +
109 +## Step 3: Create the Index Submission
110 +
111 +### Fork and set up
112 +
113 +```bash
114 +# Automatic mode
115 +gh repo fork https://github.com/agent0ai/a0-plugins --clone --remote
116 +cd a0-plugins
117 +git checkout -b add-<plugin_name>
118 +```
119 +
120 +### Create the plugin folder
121 +
122 +```bash
123 +mkdir -p plugins/<plugin_name>
124 +```
125 +
126 +### Create `index.yaml`
127 +
128 +The index uses **`index.yaml`** (not `plugin.yaml`). These are different schemas:
129 +
130 +```yaml
131 +title: My Plugin
132 +description: One-sentence description of what the plugin does for the user.
133 +github: https://github.com/<user>/<repo-name>
134 +tags:
135 + - tools
136 + - example
137 +```
138 +
139 +Optional additional fields:
140 +```yaml
141 +screenshots:
142 + - https://raw.githubusercontent.com/<user>/<repo>/main/docs/screenshot1.png
143 + - https://raw.githubusercontent.com/<user>/<repo>/main/docs/screenshot2.webp
144 +```
145 +
146 +### Recommended tags
147 +
148 +Use tags from https://github.com/agent0ai/a0-plugins/blob/main/TAGS.md (up to 5).
149 +Common tags: `tools`, `automation`, `workflow`, `api`, `web`, `database`, `memory`, `integration`, `security`, `development`, `llm`, `agents`
150 +
151 +### Optional thumbnail
152 +
153 +Add a square image named `thumbnail.png`, `thumbnail.jpg`, or `thumbnail.webp` (max 20 KB, must be square aspect ratio) to `plugins/<plugin_name>/`.
154 +
155 +---
156 +
157 +## Step 4: Pre-validate Before PR
158 +
159 +Run these checks locally before opening the PR (mirrors what CI will verify):
160 +
161 +| Check | Rule |
162 +|---|---|
163 +| `index.yaml` exists in `plugins/<name>/` | Required |
164 +| Only `index.yaml` + optional thumbnail in the folder | No other files/subdirs |
165 +| `title` length | Max 50 characters |
166 +| `description` length | Max 500 characters |
167 +| `index.yaml` total length | Max 2000 characters |
168 +| `tags` count | Max 5 |
169 +| `screenshots` count | Max 5, each URL must be reachable |
170 +| `github` URL | Points to existing public repo |
171 +| Remote `plugin.yaml` | Exists at repo root |
172 +| Remote `plugin.yaml` `name` field | Matches index folder name exactly |
173 +| Folder name pattern | `^[a-z0-9_]+$`, no leading `_` |
174 +| `github` URL uniqueness | Not already in the index for another plugin |
175 +
176 +Verify the remote `plugin.yaml` name match:
177 +```bash
178 +curl -s https://raw.githubusercontent.com/<user>/<repo>/main/plugin.yaml | grep "^name:"
179 +# Expected output: name: <plugin_name>
180 +```
181 +
182 +---
183 +
184 +## Step 5: Commit and Open PR
185 +
186 +```bash
187 +# Add and commit
188 +git add plugins/<plugin_name>/
189 +git commit -m "feat: add <plugin_name> plugin"
190 +
191 +# Push and open PR
192 +git push origin add-<plugin_name>
193 +gh pr create \
194 + --repo agent0ai/a0-plugins \
195 + --title "feat: add <plugin_name>" \
196 + --body "## Plugin: <title>
197 +
198 +<description>
199 +
200 +- GitHub: <github_url>
201 +- Tags: <tags>"
202 +```
203 +
204 +### PR rules
205 +
206 +- One plugin per PR (adding exactly one new folder under `plugins/`)
207 +- CI validates automatically on open/sync/reopen
208 +- A human maintainer reviews after CI passes
209 +- If PR has no activity for 7+ days after CI failure it may be auto-closed
210 +
211 +---
212 +
213 +## Two Schemas at a Glance
214 +
215 +| File | Location | Purpose | Key fields |
216 +|---|---|---|---|
217 +| `plugin.yaml` | Your plugin's GitHub repo root | Runtime manifest (drives Agent Zero behavior) | `name` (required!), `title`, `description`, `version`, `settings_sections`, `per_project_config`, `per_agent_config`, `always_enabled` |
218 +| `index.yaml` | `a0-plugins/plugins/<name>/` | Index manifest (drives discoverability) | `title`, `description`, `github`, `tags`, `screenshots` |
219 +
220 +**Never mix these up.** They have different schemas and different purposes.
221 +
222 +---
223 +
224 +## References
225 +
226 +- Plugin Index repo: https://github.com/agent0ai/a0-plugins
227 +- Recommended tags: https://github.com/agent0ai/a0-plugins/blob/main/TAGS.md
228 +- Review before contributing: read `/a0/skills/a0-review-plugin/SKILL.md`
229 +- Build the plugin first: read `/a0/skills/a0-create-plugin/SKILL.md`
skills/a0-create-plugin/SKILL.md
+39 -8
@@ -1,6 +1,15 @@
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
@@ -8,6 +17,8 @@ description: Create, extend, or modify Agent Zero plugins. Follows strict full-s
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/docs/agents/AGENTS.components.md (Component system deep dive)
@@ -32,6 +43,7 @@ Before starting, ask the user one question:
43 Every plugin must have a `plugin.yaml` or it will not be discovered.
44
45 ```yaml
46 +name: my_plugin # required for community plugins; must match dir name (^[a-z0-9_]+$)
47 title: My Plugin
48 description: What this plugin does.
49 version: 1.0.0
@@ -41,6 +53,8 @@ per_project_config: false
53 per_agent_config: false
54 ```
55
56 +`name`: lowercase, numbers, underscores only (`^[a-z0-9_]+$`). Required by CI when submitting to the Plugin Index - must exactly match the index folder name.
57 +
58 `settings_sections` controls which Settings tabs show a subsection for this plugin. Valid values: `agent`, `external`, `mcp`, `developer`, `backup`. Use `[]` for no subsection.
59
60 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).
@@ -265,7 +279,7 @@ The plugin must live in its own GitHub repository with the plugin contents at th
279
280 ```text
281 your-plugin-repo/ ← GitHub repository root
268 -├── plugin.yaml ← runtime manifest (title, description, version, ...)
282 +├── plugin.yaml ← runtime manifest (must include name field!)
283 ├── default_config.yaml
284 ├── README.md
285 ├── LICENSE
@@ -275,11 +289,20 @@ your-plugin-repo/ ← GitHub repository root
289 └── webui/
290 ```
291
292 +The runtime `plugin.yaml` at the repo root **must include a `name` field** matching the index folder name:
293 +
294 +```yaml
295 +name: my_plugin # REQUIRED - must match index folder name exactly
296 +title: My Plugin
297 +description: What this plugin does.
298 +version: 1.0.0
299 +```
300 +
301 Help the user create this repository and push the plugin files to it.
302
303 ### 2. Index manifest (different from runtime manifest)
304
282 -The Plugin Index (`https://github.com/agent0ai/a0-plugins`) uses a **separate, simpler `plugin.yaml`** that only describes discoverability — it is NOT the same as the runtime manifest:
305 +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:
306
307 ```yaml
308 title: My Plugin
@@ -288,24 +311,32 @@ github: https://github.com/yourname/your-plugin-repo
311 tags:
312 - tools
313 - example
314 +screenshots: # optional, up to 5 full image URLs
315 + - https://raw.githubusercontent.com/yourname/your-plugin-repo/main/docs/screen1.png
316 ```
317
293 -Only four fields: `title`, `description`, `github` (required), and `tags` (optional, up to 5). See the recommended tag list at https://github.com/agent0ai/a0-plugins/blob/main/TAGS.md.
318 +Required fields: `title`, `description`, `github`. Optional: `tags` (up to 5), `screenshots` (up to 5 URLs).
319 +See the recommended tag list at https://github.com/agent0ai/a0-plugins/blob/main/TAGS.md.
320 +
321 +> Important: CI also checks that your remote `plugin.yaml` contains a `name` field matching the index folder name exactly.
322
323 ### 3. Submission steps
324
325 1. Fork `https://github.com/agent0ai/a0-plugins`.
298 -2. Create the folder `plugins/<your-plugin-name>/` in the fork.
299 -3. Add the index `plugin.yaml` inside it (and optionally a square thumbnail ≤ 20 KB named `thumbnail.png`, `thumbnail.jpg`, or `thumbnail.webp`).
326 +2. Create the folder `plugins/<your_plugin_name>/` in the fork.
327 + - Folder name: lowercase letters, numbers, underscores only (`^[a-z0-9_]+$`) - no hyphens
328 + - Must exactly match the `name` field in your remote `plugin.yaml`
329 +3. Add `index.yaml` inside it (and optionally a square thumbnail ≤ 20 KB named `thumbnail.png`, `thumbnail.jpg`, or `thumbnail.webp`).
330 4. Open a Pull Request. The PR must add exactly one new plugin folder.
301 -5. CI will validate automatically. A maintainer reviews and merges.
331 +5. CI validates automatically. A maintainer reviews and merges.
332
333 Submission constraints:
304 -- Folder name: unique, stable, lowercase, kebab-case
334 +- Folder name: unique, stable, `^[a-z0-9_]+$`
335 - Folders starting with `_` are reserved for internal use
336 - `title` max 50 characters, `description` max 500 characters
337 +- `index.yaml` max 2000 characters total
338
308 -Help the user prepare the fork, the index manifest, and draft the PR.
339 +For a fully guided contribution flow (including git operations), read `/a0/skills/a0-contribute-plugin/SKILL.md`.
340
341 ---
342
skills/a0-debug-plugin/SKILL.md new
+153
@@ -0,0 +1,153 @@
1 +---
2 +name: a0-debug-plugin
3 +description: Diagnose and fix Agent Zero plugin problems. Covers plugin not appearing, won't enable, API endpoints not responding, frontend store errors, extension point injection, settings resolution, hooks.py issues, and log inspection. Use when a plugin is not working, not loading, crashing, missing from the list, or behaving unexpectedly.
4 +version: 1.0.0
5 +tags: ["plugins", "debug", "troubleshoot", "fix", "diagnose", "error", "broken"]
6 +trigger_patterns:
7 + - "plugin not working"
8 + - "plugin not loading"
9 + - "plugin not showing"
10 + - "plugin broken"
11 + - "plugin error"
12 + - "plugin missing"
13 + - "plugin crash"
14 + - "debug plugin"
15 + - "troubleshoot plugin"
16 + - "fix plugin"
17 +---
18 +
19 +# Agent Zero Plugin Debugger
20 +
21 +Work through these checks in order. Stop at the first failure and fix it before continuing.
22 +
23 +---
24 +
25 +## 1. Plugin not appearing in the Plugins list
26 +
27 +```bash
28 +# Check plugin.yaml exists
29 +ls /a0/usr/plugins/<name>/plugin.yaml
30 +
31 +# Validate YAML
32 +python3 -c "import yaml; yaml.safe_load(open('/a0/usr/plugins/<name>/plugin.yaml'))"
33 +
34 +# Check directory name doesn't start with '.'
35 +ls /a0/usr/plugins/
36 +```
37 +
38 +Common causes:
39 +- Missing `plugin.yaml` - plugin not discovered
40 +- Invalid YAML syntax - plugin skipped silently
41 +- Directory name starts with `.` - skipped by discovery
42 +
43 +---
44 +
45 +## 2. Plugin appears but won't enable
46 +
47 +```bash
48 +# Check toggle state
49 +ls -la /a0/usr/plugins/<name>/.toggle-*
50 +
51 +# Check for conflicting scoped toggles
52 +ls -la project/.a0proj/plugins/<name>/.toggle-* 2>/dev/null
53 +ls -la /a0/usr/agents/default/plugins/<name>/.toggle-* 2>/dev/null
54 +```
55 +
56 +---
57 +
58 +## 3. API endpoint not responding
59 +
60 +- Verify the handler file is in `api/` and subclasses `ApiHandler`
61 +- Route format: `POST /api/plugins/<plugin_name>/<handler_filename_without_.py>`
62 +- Check for Python import errors on startup (check Agent Zero logs)
63 +- Verify correct import paths: `from agent import AgentContext` not `from helpers.context import AgentContext`
64 +
65 +```bash
66 +# Check for syntax errors in handler
67 +python3 -m py_compile /a0/usr/plugins/<name>/api/my_handler.py
68 +```
69 +
70 +---
71 +
72 +## 4. Frontend component not rendering / store errors
73 +
74 +- Check browser console for Alpine.js errors
75 +- Verify the store file is imported in HTML `<head>` via `<script type="module">`
76 +- Confirm Store Gate pattern is used (missing gate = undefined error if store not yet loaded)
77 +- Verify store name in `createStore(...)` matches `$store.<name>` in templates
78 +
79 +---
80 +
81 +## 5. Extension point not injecting
82 +
83 +- Check the HTML file is in `extensions/webui/<correct_breakpoint_name>/`
84 +- Verify the breakpoint name exists in core UI (check `webui/index.html` or component files for `<x-extension id="...">`)
85 +- Common breakpoints: `sidebar-quick-actions-main-start`, `plugins-list-header-buttons`, `chat-input-bottom-actions-end`
86 +- Confirm the HTML file has a root element with `x-data` and an `x-move-*` directive
87 +
88 +---
89 +
90 +## 6. Settings not saving / loading wrong values
91 +
92 +Config resolution order (highest priority first):
93 +1. `project/.a0proj/agents/<profile>/plugins/<name>/config.json`
94 +2. `project/.a0proj/plugins/<name>/config.json`
95 +3. `usr/agents/<profile>/plugins/<name>/config.json`
96 +4. `usr/plugins/<name>/config.json`
97 +5. `plugins/<name>/default_config.yaml`
98 +
99 +```bash
100 +# Find which config file is actually being loaded
101 +find /a0 -path "*/plugins/<name>/config.json" 2>/dev/null
102 +```
103 +
104 +---
105 +
106 +## 7. hooks.py install hook not running
107 +
108 +The `install()` hook is called automatically by the plugin installer after placement. If it didn't run:
109 +- Check the function is named exactly `install` (not `on_install` or similar)
110 +- Check for exceptions in the function (add try/except with print for debugging)
111 +- Manually trigger in the **framework runtime** (not via `code_execution_tool` python - that uses `/opt/venv`, not `/opt/venv-a0`):
112 +
113 +```bash
114 +cd /a0 && /opt/venv-a0/bin/python -c "
115 +import asyncio
116 +from helpers.plugins import call_plugin_hook
117 +asyncio.run(call_plugin_hook('<plugin_name>', 'install'))
118 +print('Done')
119 +"
120 +```
121 +
122 +---
123 +
124 +## 8. Check Agent Zero logs
125 +
126 +```bash
127 +# Find recent log files
128 +ls -lt /a0/logs/*.html | head -5
129 +```
130 +
131 +Plugin-related errors appear as Python tracebacks mentioning the plugin path.
132 +
133 +---
134 +
135 +## How Plugin Discovery Works
136 +
137 +1. Agent Zero walks `usr/plugins/` then `plugins/` at startup
138 +2. Any directory containing `plugin.yaml` is treated as a plugin
139 +3. `usr/plugins/<name>` takes priority over `plugins/<name>` when both exist (user overrides core)
140 +4. Toggle state is evaluated: `.toggle-0` disables, `.toggle-1` enables, no file = enabled by default
141 +5. Enabled plugins have their `extensions/`, `api/`, `tools/`, etc. registered into the runtime
142 +
143 +Plugins are re-scanned when:
144 +- Agent Zero restarts
145 +- A plugin is installed/removed via the installer
146 +- The "Refresh" action is triggered in the Plugins UI
147 +
148 +---
149 +
150 +## References
151 +
152 +- Plugin architecture: `/a0/docs/agents/AGENTS.plugins.md`
153 +- Manage (install/update/uninstall): read `/a0/skills/a0-manage-plugin/SKILL.md`
skills/a0-manage-plugin/SKILL.md new
+363
@@ -0,0 +1,363 @@
1 +---
2 +name: a0-manage-plugin
3 +description: Manage Agent Zero plugins lifecycle: browse the marketplace, scan for security, install from Git/ZIP/marketplace, update, uninstall, enable, disable, debug, and troubleshoot. Use when asked to install, update, uninstall, remove, scan, find, search, enable, disable, debug, or troubleshoot a plugin.
4 +version: 1.2.0
5 +tags: ["plugins", "install", "uninstall", "update", "scan", "security", "debug", "troubleshoot", "marketplace", "manage"]
6 +trigger_patterns:
7 + - "install plugin"
8 + - "uninstall plugin"
9 + - "remove plugin"
10 + - "delete plugin"
11 + - "update plugin"
12 + - "scan plugin"
13 + - "debug plugin"
14 + - "troubleshoot plugin"
15 + - "browse plugins"
16 + - "search plugins"
17 + - "plugin not working"
18 + - "enable plugin"
19 + - "disable plugin"
20 + - "plugin marketplace"
21 + - "list plugins"
22 +---
23 +
24 +# Agent Zero Plugin Management
25 +
26 +## Action Routing
27 +
28 +Identify what the user needs and jump to the relevant section:
29 +
30 +| User need | Section |
31 +|---|---|
32 +| Find / search / browse available plugins | [Browse Marketplace](#browse-marketplace) |
33 +| Scan a plugin for security issues | [Security Scan](#security-scan) |
34 +| Install a plugin | [Install a Plugin](#install-a-plugin) |
35 +| Update an installed plugin | [Update a Plugin](#update-a-plugin) |
36 +| Uninstall / remove a plugin | [Uninstall a Plugin](#uninstall-a-plugin) |
37 +| Enable or disable a plugin | [Activation](#activation) |
38 +| Plugin not loading / crashing / missing | Read `/a0/skills/a0-debug-plugin/SKILL.md` |
39 +| Explain how plugin discovery works | Read `/a0/skills/a0-debug-plugin/SKILL.md` |
40 +
41 +---
42 +
43 +## Browse Marketplace
44 +
45 +Fetch the current community index:
46 +
47 +```python
48 +import urllib.request, json
49 +
50 +url = "https://github.com/agent0ai/a0-plugins/releases/download/generated-index/index.json"
51 +with urllib.request.urlopen(url, timeout=30) as resp:
52 + index = json.loads(resp.read())
53 +
54 +plugins = index.get("plugins", {})
55 +```
56 +
57 +Each entry in `plugins` is keyed by plugin name with fields: `title`, `description`, `github`, `tags`, `thumbnail` (URL if available).
58 +
59 +**To search**: filter by keyword in `title`, `description`, or `tags`.
60 +
61 +**To list installed plugins** locally:
62 +```bash
63 +ls /a0/usr/plugins/
64 +```
65 +
66 +**Alternatively via UI**: Open the Plugins dialog in Agent Zero and switch to the **Browse** tab (or click **Install** in the toolbar to open the marketplace).
67 +
68 +---
69 +
70 +## Security Scan
71 +
72 +The `_plugin_scan` plugin provides an LLM-driven security scanner that clones the repository, reads all files, and produces a structured markdown report covering 6 checks: structure match, static code review, agent manipulation detection, remote communication, secrets access, and obfuscation.
73 +
74 +### Pre-install scan protocol (follow this for every install)
75 +
76 +**Always offer to scan before installing.** If the user hasn't explicitly declined, say:
77 +
78 +> "Before installing, I strongly recommend running a security scan on this plugin. Third-party plugins execute code inside your Agent Zero environment. Should I scan it first? The scan typically takes 2-4 minutes."
79 +
80 +If the user declines, acknowledge but warn once:
81 +
82 +> "Understood, skipping the scan. Note that installing unscanned third-party code carries security risks. Proceeding with installation."
83 +
84 +Then proceed to install. **Do not ask again** after the user declines.
85 +
86 +### Running a scan
87 +
88 +```python
89 +# (after authentication setup - see Install section)
90 +resp = s.post(
91 + f"{BASE}/api/plugins/_plugin_scan/plugin_scan_run",
92 + json={
93 + "git_url": "https://github.com/<user>/<plugin-repo>",
94 + "checks": ["structure", "codeReview", "agentManipulation", "remoteComms", "secrets", "obfuscation"],
95 + },
96 + headers={"X-CSRF-Token": token, "Origin": ORIGIN},
97 + timeout=600, # set generously - the scan clones the repo, reads all files, runs 6 LLM checks
98 +)
99 +data = resp.json()
100 +```
101 +
102 +Always pass the full `checks` list explicitly - omitting it has been observed to cause intermittent failures. To run a subset, remove unwanted keys from the list.
103 +
104 +### Interpreting the result
105 +
106 +```python
107 +report = data["report"] # full markdown security report
108 +```
109 +
110 +Present the full `report` to the user. Read the Summary section to determine the overall verdict (**Safe** / **Caution** / **Dangerous**) and act accordingly:
111 +
112 +| Verdict | Action |
113 +|---|---|
114 +| **Safe** | Offer to proceed with installation |
115 +| **Caution** | Show findings, warn that issues were found, ask for explicit confirmation: "Some warnings were found. Do you still want to install?" |
116 +| **Dangerous** | Show findings, **strongly advise against installing**: "The scanner flagged serious security issues. I strongly recommend NOT installing this plugin. Do you want to proceed anyway?" Only install if user explicitly confirms. |
117 +
118 +If the scan times out or errors (500), inform the user and ask whether to proceed without a scan.
119 +
120 +---
121 +
122 +## Install a Plugin
123 +
124 +> **Always use the HTTP API or UI.** Never import Agent Zero framework modules directly from `code_execution_tool` - the agent runs in a separate Python runtime (`/opt/venv`) that does not have the framework's dependencies. All programmatic installs must go through HTTP.
125 +
126 +### How installed state works
127 +
128 +The marketplace marks a plugin as **Installed** by cross-referencing marketplace keys against `usr/plugins/` directory names at request time. To appear installed:
129 +- The plugin directory must exist at `usr/plugins/<name>/` with a valid `plugin.yaml`
130 +- The framework plugin cache must be cleared (the API handles this automatically)
131 +- Re-fetching the marketplace index will then show it as installed
132 +
133 +### API authentication (required for all HTTP calls)
134 +
135 +The Agent Zero API uses CSRF protection. The `Origin` header is **always required** - without it the CSRF endpoint returns `ok: false` even when login is disabled.
136 +
137 +**Step 1: Set the base URL.** Agent Zero listens on port 80 inside Docker (the standard deployment):
138 +
139 +```python
140 +import requests
141 +
142 +BASE = "http://localhost" # port 80 inside Docker
143 +# If running outside Docker (dev mode), check: os.environ.get("WEB_UI_PORT", "5000")
144 +```
145 +
146 +**Step 2: Bootstrap the session and get the CSRF token:**
147 +
148 +```python
149 +s = requests.Session()
150 +ORIGIN = BASE # Origin must match a localhost pattern
151 +
152 +r = s.get(f"{BASE}/api/csrf_token", headers={"Origin": ORIGIN}, timeout=10)
153 +data = r.json()
154 +
155 +if not data.get("ok"):
156 + raise RuntimeError(f"CSRF bootstrap failed: {data.get('error')}")
157 +
158 +token = data["token"]
159 +runtime_id = data["runtime_id"]
160 +
161 +# Set the CSRF cookie (required alongside the header)
162 +s.cookies.set(f"csrf_token_{runtime_id}", token)
163 +```
164 +
165 +Reuse `s`, `BASE`, `ORIGIN`, and `token` for all subsequent API calls. Always include `headers={"X-CSRF-Token": token, "Origin": ORIGIN}` on every request.
166 +
167 +### Method 1: From a Git URL (via HTTP API) - preferred for programmatic use
168 +
169 +```python
170 +# (after authentication setup above)
171 +resp = s.post(
172 + f"{BASE}/api/plugins/_plugin_installer/plugin_install",
173 + json={
174 + "action": "install_git",
175 + "git_url": "https://github.com/<user>/<plugin-repo>",
176 + # "git_token": "<token>", # optional, for private repos
177 + # "plugin_name": "override" # optional, override directory name
178 + },
179 + headers={"X-CSRF-Token": token, "Origin": ORIGIN},
180 + timeout=120,
181 +)
182 +print(resp.json())
183 +```
184 +
185 +This runs the full pipeline in the framework runtime: clone → validate → place in `usr/plugins/` → run `install` hook → clear plugin cache → notify frontend. The marketplace will show the plugin as installed on the next index fetch.
186 +
187 +### Method 2: From the Marketplace (UI) - preferred for interactive use
188 +
189 +1. Open the Plugins dialog
190 +2. Go to the **Browse** tab (or click **Install**)
191 +3. Find the plugin, click it, click **Install**
192 +
193 +The UI handles everything including marking the plugin as installed in the marketplace view.
194 +
195 +### Method 3: From a ZIP file (via HTTP API)
196 +
197 +```python
198 +# (after authentication setup above)
199 +with open("plugin.zip", "rb") as f:
200 + resp = s.post(
201 + f"{BASE}/api/plugins/_plugin_installer/plugin_install",
202 + data={"action": "install_zip"},
203 + files={"plugin_file": f},
204 + headers={"X-CSRF-Token": token, "Origin": ORIGIN},
205 + )
206 +print(resp.json())
207 +```
208 +
209 +Or via UI: Plugins dialog -> Install -> ZIP tab -> upload file.
210 +
211 +### Manual install (last resort only)
212 +
213 +Only use this if the HTTP API is genuinely unavailable (not because of import errors - those mean you must use the HTTP API instead).
214 +
215 +```bash
216 +git clone https://github.com/<user>/<repo> /a0/usr/plugins/<plugin_name>
217 +```
218 +
219 +After cloning, the plugin is on disk but the framework doesn't know about it. Clear the cache and notify the frontend by calling the toggle API (off then on) which triggers `after_plugin_change()` internally:
220 +
221 +```python
222 +# (after authentication setup above)
223 +for state in [False, True]:
224 + s.post(
225 + f"{BASE}/api/plugins",
226 + json={"action": "toggle_plugin", "plugin_name": "<plugin_name>", "enabled": state},
227 + headers={"X-CSRF-Token": token, "Origin": ORIGIN},
228 + )
229 +```
230 +
231 +Or simply restart Agent Zero - on startup it re-scans `usr/plugins/` fresh.
232 +
233 +---
234 +
235 +## Update a Plugin
236 +
237 +> A dedicated update endpoint is being added to the framework. Until it lands, use the flow below.
238 +
239 +### Checking for updates
240 +
241 +**Do not compare version strings.** Contributors often forget to bump the version, so a matching version does not mean the plugin is current. Check for new commits instead:
242 +
243 +```bash
244 +# Is the plugin a git repo?
245 +git -C /a0/usr/plugins/<name> rev-parse --is-inside-work-tree 2>/dev/null
246 +
247 +# Compare local HEAD with remote HEAD (no fetch required)
248 +LOCAL=$(git -C /a0/usr/plugins/<name> rev-parse HEAD)
249 +REMOTE=$(git -C /a0/usr/plugins/<name> ls-remote origin HEAD | awk '{print $1}')
250 +echo "Local: $LOCAL"
251 +echo "Remote: $REMOTE"
252 +[ "$LOCAL" = "$REMOTE" ] && echo "Up to date" || echo "Update available"
253 +```
254 +
255 +If they differ, new commits exist on the remote - report this to the user as "update available" regardless of whether the version field changed.
256 +
257 +### Applying the update
258 +
259 +If installed via Git:
260 +
261 +```bash
262 +cd /a0/usr/plugins/<name>
263 +git pull origin main
264 +```
265 +
266 +Then refresh the framework cache via the toggle API (see [API authentication](#api-authentication-required-for-all-http-calls) for session setup):
267 +
268 +```python
269 +# (after authentication setup)
270 +for state in [False, True]:
271 + s.post(
272 + f"{BASE}/api/plugins",
273 + json={"action": "toggle_plugin", "plugin_name": "<name>", "enabled": state},
274 + headers={"X-CSRF-Token": token, "Origin": ORIGIN},
275 + )
276 +```
277 +
278 +If not a git repo: uninstall via the API (see [Uninstall a Plugin](#uninstall-a-plugin)), then reinstall via the Git method above.
279 +
280 +---
281 +
282 +## Uninstall a Plugin
283 +
284 +> **Safety rules - read before proceeding**:
285 +> - **Core plugins** (in `plugins/`, not `usr/plugins/`) cannot be uninstalled via the API - the framework blocks it. Disable them instead (see [Activation](#activation)).
286 +> - **Always ask for explicit user confirmation** before uninstalling: "Are you sure you want to uninstall `<name>`? This will delete all plugin files and cannot be undone."
287 +> - Uninstalling does NOT delete plugin config files stored in `usr/agents/` or project scopes.
288 +
289 +### Standard uninstall (via API)
290 +
291 +Uses the framework's `uninstall_plugin` which calls the plugin's `uninstall` hook (if defined) before deleting. Requires an authenticated session (see [API authentication](#api-authentication-required-for-all-http-calls) in the Install section):
292 +
293 +```python
294 +# (after authentication setup from the Install section)
295 +resp = s.post(
296 + f"{BASE}/api/plugins",
297 + json={
298 + "action": "delete_plugin",
299 + "plugin_name": "<name>",
300 + },
301 + headers={"X-CSRF-Token": token, "Origin": ORIGIN},
302 +)
303 +print(resp.json())
304 +```
305 +
306 +This is the preferred method. The framework will:
307 +1. Call `uninstall()` from `hooks.py` (if present) - runs cleanup
308 +2. Delete the `usr/plugins/<name>/` directory
309 +3. Notify the frontend to refresh the plugin list
310 +
311 +**Via UI**: Plugins dialog -> find the plugin -> click the delete (trash) icon -> confirm.
312 +
313 +### Fallback: direct folder removal
314 +
315 +Use this only if the standard uninstall fails (e.g., broken `uninstall` hook that crashes or hangs):
316 +
317 +```bash
318 +# Confirm the plugin is a custom one (usr/plugins/) - NEVER delete from plugins/
319 +ls /a0/usr/plugins/<name>/
320 +
321 +# Remove it
322 +rm -rf /a0/usr/plugins/<name>/
323 +```
324 +
325 +After manual removal, refresh the plugin list via the UI or restart Agent Zero.
326 +
327 +---
328 +
329 +## Activation
330 +
331 +Plugins are enabled/disabled via toggle files:
332 +- `.toggle-1` = explicitly ON
333 +- `.toggle-0` = explicitly OFF
334 +- No file = default (enabled for most plugins)
335 +
336 +**Enable a plugin**:
337 +```bash
338 +rm -f /a0/usr/plugins/<name>/.toggle-0
339 +touch /a0/usr/plugins/<name>/.toggle-1
340 +```
341 +
342 +**Disable a plugin**:
343 +```bash
344 +rm -f /a0/usr/plugins/<name>/.toggle-1
345 +touch /a0/usr/plugins/<name>/.toggle-0
346 +```
347 +
348 +Via UI: Plugins dialog -> find the plugin -> use the toggle switch.
349 +
350 +Plugins with `always_enabled: true` in `plugin.yaml` cannot be toggled (framework core plugins only).
351 +
352 +**Scoped toggles** (when `per_project_config` or `per_agent_config` is true): use the "Switch" modal in the UI, or place toggle files in the appropriate scoped path:
353 +- Project scope: `project/.a0proj/plugins/<name>/.toggle-1`
354 +- Agent profile scope: `usr/agents/<profile>/plugins/<name>/.toggle-1`
355 +
356 +---
357 +
358 +## References
359 +
360 +- Plugin architecture: `/a0/docs/agents/AGENTS.plugins.md`
361 +- Debug a broken plugin: read `/a0/skills/a0-debug-plugin/SKILL.md`
362 +- Create a new plugin: read `/a0/skills/a0-create-plugin/SKILL.md`
363 +- Review a plugin: read `/a0/skills/a0-review-plugin/SKILL.md`
skills/a0-plugin-router/SKILL.md new
+113
@@ -0,0 +1,113 @@
1 +---
2 +name: a0-plugin-router
3 +description: Main entry point for all Agent Zero plugin tasks. Routes to specialist skills for creating, reviewing, contributing, managing, or debugging plugins. Use when the user mentions plugins, asks how the plugin system works, wants to build/install/uninstall/publish/debug a plugin, or asks about the Plugin Marketplace.
4 +version: 1.0.0
5 +tags: ["plugins", "router", "meta", "create", "review", "contribute", "manage", "marketplace"]
6 +trigger_patterns:
7 + - "plugin"
8 + - "create plugin"
9 + - "build plugin"
10 + - "review plugin"
11 + - "contribute plugin"
12 + - "publish plugin"
13 + - "install plugin"
14 + - "manage plugin"
15 + - "marketplace"
16 + - "plugin index"
17 + - "how does the plugin system work"
18 +---
19 +
20 +# Agent Zero Plugin Router
21 +
22 +## Routing Decision
23 +
24 +Classify the user's request and read the appropriate specialist skill immediately.
25 +
26 +| User intent | Skill to read |
27 +|---|---|
28 +| Create / build / develop / write a new plugin | Read `/a0/skills/a0-create-plugin/SKILL.md` |
29 +| Review / audit / validate / check a plugin | Read `/a0/skills/a0-review-plugin/SKILL.md` |
30 +| Contribute / publish / submit / share to community | Read `/a0/skills/a0-contribute-plugin/SKILL.md` |
31 +| Install / update / uninstall / remove / browse / scan | Read `/a0/skills/a0-manage-plugin/SKILL.md` |
32 +| Plugin not working / crashing / missing / debug / troubleshoot | Read `/a0/skills/a0-debug-plugin/SKILL.md` |
33 +| Explain / how does it work / architecture | Answer inline using the overview below |
34 +
35 +If intent is ambiguous, ask one question before routing:
36 +> "Are you trying to **create** a new plugin, **review** one, **contribute** it to the community, **manage** (install/update/uninstall) plugins, or **debug** a plugin that isn't working?"
37 +
38 +If the user says "make a plugin for the community" - start with `a0-create-plugin`, then note that `a0-contribute-plugin` handles the publishing step after the plugin is built and tested.
39 +
40 +---
41 +
42 +## Plugin System Overview (for explain/explore queries)
43 +
44 +### Roots and Discovery
45 +
46 +Agent Zero discovers plugins from two roots, in priority order:
47 +1. `usr/plugins/<name>/` - user plugins (your custom plugins go here)
48 +2. `plugins/<name>/` - core system plugins (framework-bundled, do not modify)
49 +
50 +A plugin is valid when its directory contains a `plugin.yaml`. Directories starting with `.` are skipped.
51 +
52 +### Runtime Manifest (`plugin.yaml`)
53 +
54 +Every plugin requires a `plugin.yaml` at its root:
55 +
56 +```yaml
57 +name: my_plugin # required by CI for community plugins (^[a-z0-9_]+$)
58 +title: My Plugin # UI display name
59 +description: What it does.
60 +version: 1.0.0
61 +settings_sections: [agent] # which Settings tabs show a subsection
62 +per_project_config: false # enables project-scoped settings/toggle
63 +per_agent_config: false # enables agent-profile-scoped settings/toggle
64 +always_enabled: false # forces ON, disables toggle (framework use only)
65 +```
66 +
67 +`settings_sections` valid values: `agent`, `external`, `mcp`, `developer`, `backup`. Use `[]` for none.
68 +
69 +### What a Plugin Can Provide
70 +
71 +| Directory/File | Purpose |
72 +|---|---|
73 +| `api/` | API handlers (`ApiHandler` subclasses) |
74 +| `tools/` | Agent tools (`Tool` subclasses) |
75 +| `extensions/python/<point>/` | Backend lifecycle hooks |
76 +| `extensions/webui/<point>/` | HTML/JS injected into UI breakpoints |
77 +| `webui/config.html` | Plugin settings UI |
78 +| `webui/*.html`, `webui/*.js` | Full plugin pages and Alpine stores |
79 +| `hooks.py` | Framework runtime hooks (install, cache, registration) |
80 +| `execute.py` | User-triggered script (setup, maintenance, repair) |
81 +| `default_config.yaml` | Settings defaults |
82 +| `agents/<profile>/agent.yaml` | Plugin-distributed agent profiles |
83 +| `conf/model_providers.yaml` | Add/override model providers |
84 +
85 +### Activation
86 +
87 +- Global toggle: `.toggle-1` (ON) / `.toggle-0` (OFF) files in the plugin dir
88 +- Scoped toggles (project/agent) available when `per_project_config` or `per_agent_config` is true
89 +- Default: enabled when no toggle file exists
90 +- `always_enabled: true` forces ON and hides controls (reserved for framework)
91 +
92 +### Settings Resolution (highest priority first)
93 +
94 +1. `project/.a0proj/agents/<profile>/plugins/<name>/config.json`
95 +2. `project/.a0proj/plugins/<name>/config.json`
96 +3. `usr/agents/<profile>/plugins/<name>/config.json`
97 +4. `usr/plugins/<name>/config.json`
98 +5. `plugins/<name>/default_config.yaml`
99 +
100 +### Key API Routes
101 +
102 +| Route | Purpose |
103 +|---|---|
104 +| `GET /plugins/<name>/<path>` | Serve static plugin assets |
105 +| `POST /api/plugins/<name>/<handler>` | Call plugin API endpoint |
106 +| `POST /api/plugins` | Management (toggle, config, docs) |
107 +
108 +### Deep-Dive References
109 +
110 +- Architecture + extension points: `/a0/docs/agents/AGENTS.plugins.md`
111 +- Developer guide: `/a0/docs/developer/plugins.md`
112 +- Component system: `/a0/docs/agents/AGENTS.components.md`
113 +- Modal system: `/a0/docs/agents/AGENTS.modals.md`
skills/a0-review-plugin/SKILL.md new
+166
@@ -0,0 +1,166 @@
1 +---
2 +name: a0-review-plugin
3 +description: Full audit of Agent Zero plugins in usr/plugins/. Reviews manifest validity, directory structure, code patterns (Store Gating, notifications, imports), security, and duplicate detection against the community index. Use when asked to review, audit, validate, or check an existing plugin before using or contributing it.
4 +version: 1.0.0
5 +tags: ["plugins", "review", "audit", "validate", "security", "checklist"]
6 +trigger_patterns:
7 + - "review plugin"
8 + - "audit plugin"
9 + - "validate plugin"
10 + - "check plugin"
11 + - "plugin review"
12 + - "is my plugin correct"
13 + - "plugin checklist"
14 +---
15 +
16 +# Agent Zero Plugin Review
17 +
18 +Full-audit workflow for plugins in `/a0/usr/plugins/<name>/`. Run all 4 phases in order and report findings grouped by phase. Mark each item PASS, FAIL, or WARN.
19 +
20 +For detailed checklists and code pattern references, read `checklists.md` in this skill directory when needed.
21 +
22 +---
23 +
24 +## Phase 1: Manifest Validation
25 +
26 +Read `usr/plugins/<name>/plugin.yaml`. Check:
27 +
28 +- [ ] File exists at plugin root
29 +- [ ] Valid YAML (parseable, mapping at top level)
30 +- [ ] `name` field present, non-empty, matches `^[a-z0-9_]+$` and matches the directory name
31 +- [ ] `title` present and non-empty
32 +- [ ] `description` present and non-empty
33 +- [ ] `version` present, follows semver or simple `x.y.z` format
34 +- [ ] `settings_sections` is a list; each value is one of: `agent`, `external`, `mcp`, `developer`, `backup`
35 +- [ ] `per_project_config` and `per_agent_config` are booleans (if present)
36 +- [ ] `always_enabled` is `false` or absent (only framework core plugins should use `true`)
37 +- [ ] No unknown fields (warn on extra keys not in the schema)
38 +
39 +---
40 +
41 +## Phase 2: Structure Validation
42 +
43 +Inspect the plugin directory layout:
44 +
45 +- [ ] Directory is under `usr/plugins/` (not `plugins/` - that is reserved for core)
46 +- [ ] Directory name matches `^[a-z0-9_]+$`
47 +- [ ] If `api/` exists: contains Python files only; each should subclass `ApiHandler`
48 +- [ ] If `tools/` exists: contains Python files only; each should subclass `Tool`
49 +- [ ] If `extensions/` exists: check subdirs follow `python/<point>/` or `webui/<point>/` pattern
50 +- [ ] If `webui/config.html` exists: plugin must declare at least one `settings_sections` entry
51 +- [ ] If `hooks.py` exists: warn if it does NOT contain an `install` function (common oversight)
52 +- [ ] If `execute.py` exists: check it has a `main()` function and `if __name__ == "__main__": sys.exit(main())`
53 +- [ ] `default_config.yaml` (if present): valid YAML
54 +- [ ] No unexpected top-level files (anything not in the standard layout is a WARN)
55 +
56 +---
57 +
58 +## Phase 3: Code Pattern Review
59 +
60 +Read source files and check for violations of Agent Zero conventions.
61 +
62 +### Frontend (HTML/JS)
63 +
64 +- [ ] Every component that accesses a store uses the Store Gate pattern:
65 + ```html
66 + <div x-data>
67 + <template x-if="$store.myStore">
68 + <div x-init="$store.myStore.onOpen()" x-destroy="$store.myStore.cleanup()">
69 + ...
70 + </div>
71 + </template>
72 + </div>
73 + ```
74 +- [ ] No `alpine:init` event listeners inside HTML files (store logic must be in `.js` files)
75 +- [ ] Alpine stores use `createStore` imported from `/js/AlpineStore.js`
76 +- [ ] No inline error/success `<div>` blocks bound to `store.error` or similar - must use notification system:
77 + - `toastFrontendError(msg, "Plugin Name")` / `toastFrontendSuccess(...)` etc.
78 + - Import from `/components/notifications/notification-store.js`
79 +- [ ] Static assets served via `GET /plugins/<name>/...` (not hardcoded absolute paths)
80 +- [ ] Store module imported in HTML `<head>` via `<script type="module" src="/plugins/<name>/webui/store.js">`
81 +
82 +### Backend (Python)
83 +
84 +- [ ] Correct import paths:
85 + - `from agent import AgentContext, AgentContextType` (not `helpers.context`)
86 + - `from initialize import initialize_agent` (not a local reimport)
87 +- [ ] API handlers subclass `ApiHandler` from `python/helpers/api.py`
88 +- [ ] Tools subclass `Tool` from `helpers.tool`
89 +- [ ] Plugin settings read via `get_plugin_config("plugin-name", agent=agent)` from `helpers.plugins`
90 +- [ ] User messages sent via `context.communicate(UserMessage(...))`, not direct socket writes
91 +- [ ] `hooks.py` environment targeting: if installing packages for the agent runtime (not framework), subprocess must explicitly target the correct interpreter (e.g., `/opt/venv/bin/python`)
92 +- [ ] No `sys.executable -m pip install` for agent-runtime deps (that installs into framework runtime instead)
93 +
94 +---
95 +
96 +## Phase 4: Security + Index Review
97 +
98 +### Security checks
99 +
100 +- [ ] No hardcoded secrets, API keys, tokens, or passwords in any file
101 +- [ ] No `eval()` or `exec()` on user-supplied input
102 +- [ ] File path operations use safe joins (no concatenation with user input that could escape the sandbox)
103 +- [ ] Subprocess calls do not pass unsanitized user input as shell strings
104 +- [ ] ZIP extraction (if any): path traversal protection in place
105 +- [ ] No outbound network calls to third-party endpoints without user awareness (WARN if present, not automatic FAIL)
106 +
107 +### Duplicate detection against the community index
108 +
109 +Fetch the current index:
110 +```
111 +https://github.com/agent0ai/a0-plugins/releases/download/generated-index/index.json
112 +```
113 +
114 +Check:
115 +- [ ] Plugin `name` does not already exist as a folder in the index
116 +- [ ] No other index entry points to the same `github` URL
117 +- [ ] Plugin purpose is not already covered by an existing index entry (WARN for semantic overlap, not FAIL)
118 +
119 +### Community readiness assessment
120 +
121 +Summarize whether the plugin is ready for contribution:
122 +- READY: all FAIL items resolved, no WARN items blocking
123 +- NEEDS WORK: list specific FAIL items to fix
124 +- OPTIONAL IMPROVEMENTS: list WARN items
125 +
126 +---
127 +
128 +## Reporting Format
129 +
130 +```
131 +## Plugin Review: <plugin_name>
132 +
133 +### Phase 1: Manifest
134 +PASS name: my_plugin
135 +PASS title: My Plugin
136 +FAIL version: missing
137 +...
138 +
139 +### Phase 2: Structure
140 +PASS plugin.yaml present
141 +WARN Unexpected file at root: notes.txt
142 +...
143 +
144 +### Phase 3: Code Patterns
145 +PASS Store Gating: found in webui/main.html
146 +FAIL Inline error box found in webui/settings.html (use toastFrontendError instead)
147 +...
148 +
149 +### Phase 4: Security + Index
150 +PASS No hardcoded secrets found
151 +PASS No duplicate in community index
152 +WARN Outbound HTTP call to external service in api/handler.py:42
153 +
154 +### Summary
155 +Status: NEEDS WORK
156 +Fix required: version missing in plugin.yaml, inline error box in webui/settings.html
157 +```
158 +
159 +---
160 +
161 +## References
162 +
163 +- Detailed pattern checklists: read `checklists.md` in this skill directory
164 +- Plugin architecture: `/a0/docs/agents/AGENTS.plugins.md`
165 +- Component system: `/a0/docs/agents/AGENTS.components.md`
166 +- If review passes and user wants to publish: read `/a0/skills/a0-contribute-plugin/SKILL.md`
skills/a0-review-plugin/checklists.md new
+303
@@ -0,0 +1,303 @@
1 +# Plugin Review Checklists
2 +
3 +Reference material for `a0-review-plugin`. Read specific sections as needed during the review.
4 +
5 +---
6 +
7 +## Store Gating Pattern (required)
8 +
9 +Every Alpine component that accesses a store MUST wrap content in a `x-if` gate:
10 +
11 +```html
12 +<!-- CORRECT -->
13 +<div x-data>
14 + <template x-if="$store.myPluginStore">
15 + <div x-init="$store.myPluginStore.onOpen()" x-destroy="$store.myPluginStore.cleanup()">
16 + <!-- content -->
17 + </div>
18 + </template>
19 +</div>
20 +
21 +<!-- WRONG - will throw if store not yet initialized -->
22 +<div x-data x-init="$store.myPluginStore.onOpen()">
23 + <!-- content -->
24 +</div>
25 +```
26 +
27 +**Why**: Alpine stores are registered asynchronously. Without the gate, components referencing a store that hasn't loaded yet will throw undefined errors.
28 +
29 +---
30 +
31 +## Store Definition Pattern (required)
32 +
33 +```javascript
34 +// webui/my-store.js
35 +import { createStore } from "/js/AlpineStore.js";
36 +
37 +export const store = createStore("myPluginStore", {
38 + myData: null,
39 + init() {
40 + // called once globally on registration
41 + },
42 + onOpen() {
43 + // called when the component mounts
44 + },
45 + cleanup() {
46 + // called on x-destroy
47 + }
48 +});
49 +```
50 +
51 +**Import in HTML `<head>`**:
52 +```html
53 +<script type="module" src="/plugins/my_plugin/webui/my-store.js"></script>
54 +```
55 +
56 +**Anti-patterns**:
57 +- `document.addEventListener('alpine:init', ...)` in HTML - FORBIDDEN
58 +- Defining store inline in HTML with `<script>` + `Alpine.store(...)` - FORBIDDEN
59 +
60 +---
61 +
62 +## Notification System (required)
63 +
64 +Do NOT render inline error/success blocks. Use the A0 notification system:
65 +
66 +```javascript
67 +// Frontend (Alpine store or component)
68 +import {
69 + toastFrontendError,
70 + toastFrontendSuccess,
71 + toastFrontendWarning,
72 + toastFrontendInfo
73 +} from "/components/notifications/notification-store.js";
74 +
75 +// Usage
76 +toastFrontendError("Connection failed", "My Plugin");
77 +toastFrontendSuccess("Saved successfully", "My Plugin");
78 +```
79 +
80 +```python
81 +# Backend (Python)
82 +from helpers.notification import AgentNotification
83 +
84 +AgentNotification.error("Something went wrong", context_id=context.id)
85 +AgentNotification.success("Operation complete", context_id=context.id)
86 +```
87 +
88 +**FAIL pattern** (do not allow):
89 +```html
90 +<!-- WRONG: inline error box -->
91 +<div x-show="store.error" class="error-box" x-text="store.error"></div>
92 +```
93 +
94 +---
95 +
96 +## API Handler Pattern
97 +
98 +```python
99 +# api/my_handler.py
100 +from helpers.api import ApiHandler, Request, Response
101 +
102 +class MyHandler(ApiHandler):
103 + async def process(self, input: dict, request: Request) -> dict | Response:
104 + # input is the parsed request body
105 + # return a dict (auto-serialized to JSON) or a Response object
106 + return {"ok": True, "data": "result"}
107 +```
108 +
109 +Route is auto-registered as `POST /api/plugins/my_plugin/my_handler`.
110 +
111 +---
112 +
113 +## Tool Pattern
114 +
115 +```python
116 +# tools/my_tool.py
117 +from helpers.tool import Tool, ToolResult
118 +
119 +class MyTool(Tool):
120 + async def execute(self, arg1: str, arg2: str = "default"):
121 + # Tool logic
122 + return ToolResult("Result text")
123 +```
124 +
125 +---
126 +
127 +## AgentContext Access
128 +
129 +```python
130 +# Correct imports
131 +from agent import AgentContext, AgentContextType
132 +
133 +# Get context by ID
134 +context = AgentContext.use(context_id)
135 +
136 +# Send a message proactively
137 +from helpers.messages import UserMessage
138 +task = context.communicate(UserMessage("Message text"))
139 +response = await task.result()
140 +```
141 +
142 +**Wrong** (do not use):
143 +```python
144 +from helpers.context import AgentContext # WRONG - does not exist
145 +```
146 +
147 +---
148 +
149 +## Plugin Settings (backend)
150 +
151 +```python
152 +from helpers.plugins import get_plugin_config, save_plugin_config
153 +
154 +# Read settings (resolves project/profile scope from running agent)
155 +settings = get_plugin_config("my_plugin", agent=agent) or {}
156 +
157 +# Write settings to specific scope
158 +save_plugin_config(
159 + "my_plugin",
160 + project_name="my-project",
161 + agent_profile="default",
162 + settings={"key": "value"},
163 +)
164 +```
165 +
166 +---
167 +
168 +## Plugin Settings UI (`webui/config.html`)
169 +
170 +```html
171 +<html>
172 +<head>
173 + <title>My Plugin Settings</title>
174 + <script type="module">
175 + import { store } from "/components/plugins/plugin-settings-store.js";
176 + </script>
177 +</head>
178 +<body>
179 + <div x-data>
180 + <template x-if="$store.pluginSettingsPrototype">
181 + <div x-init="context = $store.pluginSettingsPrototype.init()">
182 + <input x-model="config.api_key" type="password" placeholder="API Key" />
183 + <input type="checkbox" x-model="config.feature_enabled" />
184 + </div>
185 + </template>
186 + </div>
187 +</body>
188 +</html>
189 +```
190 +
191 +Use `saveMode = 'core'` if exposing core settings instead of plugin-specific ones:
192 +```html
193 +<div x-data x-init="context.saveMode = 'core'">
194 + <!-- core settings component -->
195 +</div>
196 +```
197 +
198 +---
199 +
200 +## Sidebar Button (extension point)
201 +
202 +```html
203 +<!-- extensions/webui/sidebar-quick-actions-main-start/my-button.html -->
204 +<div x-data x-move-after=".config-button#dashboard">
205 + <button class="config-button" @click="openModal('/plugins/my_plugin/webui/my-modal.html')">
206 + My Plugin
207 + </button>
208 +</div>
209 +```
210 +
211 +---
212 +
213 +## hooks.py Environment Targeting
214 +
215 +```python
216 +# hooks.py - install hook example
217 +import subprocess
218 +import sys
219 +
220 +def install():
221 + """Called by framework after plugin is placed in usr/plugins/."""
222 + # This installs into the Agent Zero FRAMEWORK runtime (/opt/venv-a0)
223 + subprocess.run([sys.executable, "-m", "pip", "install", "some-package==1.0.0"], check=True)
224 +
225 +async def async_hook():
226 + """Async hooks are also supported."""
227 + pass
228 +```
229 +
230 +**To install into the AGENT execution runtime** (separate from framework):
231 +```python
232 +import subprocess
233 +
234 +def install():
235 + # Explicitly target the agent runtime interpreter
236 + agent_python = "/opt/venv/bin/python"
237 + subprocess.run([agent_python, "-m", "pip", "install", "some-package"], check=True)
238 +```
239 +
240 +Never use `sys.executable` when you need the agent runtime - it targets the framework runtime.
241 +
242 +---
243 +
244 +## execute.py Pattern
245 +
246 +```python
247 +# execute.py - user-triggered script
248 +import subprocess
249 +import sys
250 +
251 +def main():
252 + print("Running setup...")
253 + result = subprocess.run(
254 + [sys.executable, "-m", "pip", "install", "requests==2.31.0"],
255 + text=True,
256 + )
257 + if result.returncode != 0:
258 + print("ERROR: Installation failed")
259 + return 1
260 + print("Done.")
261 + return 0
262 +
263 +if __name__ == "__main__":
264 + sys.exit(main())
265 +```
266 +
267 +Must: return `0` on success, non-zero on failure. Print progress. Be safe to rerun.
268 +
269 +---
270 +
271 +## plugin.yaml Schema Reference
272 +
273 +```yaml
274 +name: my_plugin # required for CI (^[a-z0-9_]+$, must match dir name)
275 +title: My Plugin # required, UI display name
276 +description: What it does. # required
277 +version: 1.0.0 # required
278 +settings_sections: # optional, valid: agent | external | mcp | developer | backup
279 + - agent
280 +per_project_config: false # optional, enables project-scoped settings
281 +per_agent_config: false # optional, enables agent-profile-scoped settings
282 +always_enabled: false # optional, framework use only
283 +```
284 +
285 +---
286 +
287 +## Community Index: What CI Checks
288 +
289 +When submitting to https://github.com/agent0ai/a0-plugins, CI validates:
290 +
291 +**`index.yaml`** (in the index repo, NOT `plugin.yaml`):
292 +- Fields: `title` (max 50), `description` (max 500), `github` (required), `tags` (optional, max 5), `screenshots` (optional, max 5 URLs)
293 +- Max total file length: 2000 characters
294 +- No unknown fields allowed
295 +
296 +**Remote `plugin.yaml`** (your plugin's own repo):
297 +- Must exist at repo root
298 +- Must contain `name` field matching the index folder name exactly
299 +
300 +**Folder name**:
301 +- Pattern: `^[a-z0-9_]+$` (underscores, no hyphens)
302 +- Must not start with `_`
303 +- Must be unique in the index