Skip to content

Commit eb5b6eb

Browse files
committed
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.
1 parent e1f99dd commit eb5b6eb

14 files changed

Lines changed: 1479 additions & 32 deletions

File tree

docs/agents/AGENTS.plugins.md

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,10 @@ usr/plugins/<plugin_name>/
4646

4747
### plugin.yaml (runtime manifest)
4848

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).
5050

5151
```yaml
52+
name: my_plugin # required for community plugins (^[a-z0-9_]+$, must match dir name)
5253
title: My Plugin
5354
description: What this plugin does.
5455
version: 1.0.0
@@ -61,6 +62,7 @@ always_enabled: false
6162
```
6263
6364
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.
6466
- `title`: UI display name
6567
- `description`: Short plugin summary
6668
- `version`: Plugin version string
@@ -200,12 +202,13 @@ embedding:
200202

201203
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.
202204

203-
### Two Distinct plugin.yaml Files
205+
### Two Distinct Manifest Files
204206

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:
206208

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):
208210
```yaml
211+
name: my_plugin # REQUIRED for index submission; must match index folder name
209212
title: My Plugin
210213
description: What this plugin does.
211214
version: 1.0.0
@@ -216,17 +219,19 @@ per_agent_config: false
216219
always_enabled: false
217220
```
218221

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):
220223
```yaml
221224
title: My Plugin
222225
description: What this plugin does.
223226
github: https://github.com/yourname/your-plugin-repo
224227
tags:
225228
- tools
226229
- example
230+
screenshots: # optional, up to 5 full image URLs
231+
- https://raw.githubusercontent.com/yourname/your-plugin-repo/main/docs/screen.png
227232
```
228233

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.
230235

231236
### Repository Structure for Community Plugins
232237

@@ -248,19 +253,22 @@ Users install it locally by cloning (or downloading) the repo contents into `/a0
248253

249254
### Submitting to the Plugin Index
250255

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.
252257
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).
254259
4. Open a Pull Request with exactly one new plugin folder.
255260
5. CI validates the submission automatically. A maintainer reviews and merges.
256261

257262
Index submission rules:
258263
- 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`
260266
- 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
262268
- `title` max 50 characters, `description` max 500 characters
269+
- `index.yaml` total max 2000 characters
263270
- `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)
264272

265273
### Plugin Marketplace
266274

docs/developer/plugins.md

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ On name collisions, user plugins take precedence.
2424
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).
2525

2626
```yaml
27+
name: my_plugin # required for community plugins (^[a-z0-9_]+$, must match dir name)
2728
title: My Plugin
2829
description: What this plugin does.
2930
version: 1.0.0
@@ -36,6 +37,7 @@ always_enabled: false
3637
3738
Field reference:
3839
40+
- `name`: plugin identifier; required by CI for index submission; must be `^[a-z0-9_]+$` and match the index folder name exactly
3941
- `title`: UI display name
4042
- `description`: short plugin summary
4143
- `version`: plugin version string
@@ -189,12 +191,13 @@ Supported actions:
189191

190192
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.
191193

192-
### Two Distinct plugin.yaml Files
194+
### Two Distinct Manifest Files
193195

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:
195197

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):
197199
```yaml
200+
name: my_plugin # REQUIRED for index submission; must match index folder name
198201
title: My Plugin
199202
description: What this plugin does.
200203
version: 1.0.0
@@ -205,25 +208,27 @@ per_agent_config: false
205208
always_enabled: false
206209
```
207210

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):
209212
```yaml
210213
title: My Plugin
211214
description: What this plugin does.
212215
github: https://github.com/yourname/your-plugin-repo
213216
tags:
214217
- tools
215218
- example
219+
screenshots: # optional, up to 5 full image URLs
220+
- https://raw.githubusercontent.com/yourname/your-plugin-repo/main/docs/screen.png
216221
```
217222

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.
219224

220225
### Repository Structure for Community Plugins
221226

222227
Plugin repos should expose the plugin contents at the repo root, so they can be cloned directly into `usr/plugins/<name>/`:
223228

224229
```text
225230
your-plugin-repo/ ← GitHub repository root
226-
├── plugin.yaml ← runtime manifest
231+
├── plugin.yaml ← runtime manifest (must include name field)
227232
├── default_config.yaml
228233
├── README.md
229234
├── LICENSE
@@ -235,18 +240,21 @@ your-plugin-repo/ ← GitHub repository root
235240

236241
### Submission Process
237242

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.
239244
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`).
241246
4. Open a Pull Request. One PR must add exactly one new plugin folder.
242247
5. CI validates automatically. A maintainer reviews and merges.
243248

244249
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`
246252
- Folders starting with `_` are reserved for internal use
247253
- `title`: max 50 characters
248254
- `description`: max 500 characters
255+
- `index.yaml` total: max 2000 characters
249256
- `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)
250258

251259
### Plugin Marketplace
252260

plugins/README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ The **Plugin Index** at https://github.com/agent0ai/a0-plugins is the community-
6868

6969
To share a plugin with the community:
7070

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`):
7373

7474
```yaml
7575
title: My Plugin
@@ -79,9 +79,11 @@ tags:
7979
- tools
8080
```
8181

82+
Optional additional fields: `screenshots` (up to 5 image URLs).
83+
8284
3. Open a Pull Request. CI validates the submission; a maintainer reviews and merges.
8385

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.
8587

8688
## Plugin Marketplace
8789

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Whitespace-only changes.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Verify all of the following. If any is false, go back and fix it:
4141

4242
## Output Format
4343

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.
4545

4646
**Section 1** — Title line: `# 🛡️ Security Scan Report: {plugin title}`
4747

@@ -65,7 +65,7 @@ Status icons: {{STATUS_LEGEND}}
6565

6666
## Constraints
6767

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
6969
- Do NOT include your internal analysis process in the report
7070
- Do NOT add checks beyond the list above
7171
- Do NOT summarize multiple files into one finding

0 commit comments

Comments
 (0)