main
md 334 lines 8.45 KB
Rendered Raw
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 ## Python Extension Layout
128
129 Use one of these backend extension layouts:
130
131 ```text
132 extensions/python/<extension_point>/
133 ```
134
135 For named lifecycle hooks such as `agent_init`, `system_prompt`, `monologue_start`, or `tool_execute_before`.
136
137 ```text
138 extensions/python/_functions/<module>/<qualname>/<start|end>/
139 ```
140
141 For implicit `@extensible` hook targets. The path must keep the full module path and every nested `__qualname__` segment.
142
143 **FAIL pattern**:
144
145 ```text
146 extensions/python/<module>_<qualname>_<start|end>/
147 ```
148
149 That flattened form is stale and no longer matches the current extensible runtime lookup.
150
151 ---
152
153 ## AgentContext Access
154
155 ```python
156 # Correct imports
157 from agent import AgentContext, AgentContextType
158
159 # Get context by ID
160 context = AgentContext.use(context_id)
161
162 # Send a message proactively
163 from helpers.messages import UserMessage
164 task = context.communicate(UserMessage("Message text"))
165 response = await task.result()
166 ```
167
168 **Wrong** (do not use):
169 ```python
170 from helpers.context import AgentContext # WRONG - does not exist
171 ```
172
173 ---
174
175 ## Plugin Settings (backend)
176
177 ```python
178 from helpers.plugins import get_plugin_config, save_plugin_config
179
180 # Read settings (resolves project/profile scope from running agent)
181 settings = get_plugin_config("my_plugin", agent=agent) or {}
182
183 # Write settings to specific scope
184 save_plugin_config(
185 "my_plugin",
186 project_name="my-project",
187 agent_profile="default",
188 settings={"key": "value"},
189 )
190 ```
191
192 ---
193
194 ## Plugin Settings UI (`webui/config.html`)
195
196 ```html
197 <html>
198 <head>
199 <title>My Plugin Settings</title>
200 <script type="module">
201 import { store } from "/components/plugins/plugin-settings-store.js";
202 </script>
203 </head>
204 <body>
205 <div x-data>
206 <template x-if="$store.pluginSettingsPrototype">
207 <div x-init="context = $store.pluginSettingsPrototype.init()">
208 <input x-model="config.api_key" type="password" placeholder="API Key" />
209 <input type="checkbox" x-model="config.feature_enabled" />
210 </div>
211 </template>
212 </div>
213 </body>
214 </html>
215 ```
216
217 ---
218
219 ## Sidebar Button (extension point)
220
221 ```html
222 <!-- extensions/webui/sidebar-quick-actions-main-start/my-button.html -->
223 <div x-data x-move-after=".config-button#dashboard">
224 <button class="config-button" @click="openModal('/plugins/my_plugin/webui/my-modal.html')">
225 My Plugin
226 </button>
227 </div>
228 ```
229
230 ---
231
232 ## hooks.py Environment Targeting
233
234 ```python
235 # hooks.py - install/uninstall/pre_update hook example
236 import subprocess
237 import sys
238
239 def install():
240 """Called by framework after plugin is placed in usr/plugins/."""
241 # This installs into the Agent Zero FRAMEWORK runtime (/opt/venv-a0)
242 subprocess.run([sys.executable, "-m", "pip", "install", "some-package==1.0.0"], check=True)
243
244 def uninstall():
245 """Called by framework before deleting plugin directory. Clean up dependencies added by install()."""
246 subprocess.run([sys.executable, "-m", "pip", "uninstall", "-y", "some-package"], check=True)
247
248 def pre_update():
249 """Called by framework immediately before plugin update pulls new code into place."""
250 # This installs into the Agent Zero FRAMEWORK runtime (/opt/venv-a0)
251 subprocess.run([sys.executable, "-m", "pip", "install", "some-package==1.0.0"], check=True)
252
253 async def async_hook():
254 """Async hooks are also supported."""
255 pass
256 ```
257
258 **To install into the AGENT execution runtime** (separate from framework):
259 ```python
260 import subprocess
261
262 def install():
263 # Explicitly target the agent runtime interpreter
264 agent_python = "/opt/venv/bin/python"
265 subprocess.run([agent_python, "-m", "pip", "install", "some-package"], check=True)
266 ```
267
268 Never use `sys.executable` when you need the agent runtime - it targets the framework runtime.
269
270 ---
271
272 ## execute.py Pattern
273
274 ```python
275 # execute.py - user-triggered script
276 import subprocess
277 import sys
278
279 def main():
280 print("Running setup...")
281 result = subprocess.run(
282 [sys.executable, "-m", "pip", "install", "requests==2.31.0"],
283 text=True,
284 )
285 if result.returncode != 0:
286 print("ERROR: Installation failed")
287 return 1
288 print("Done.")
289 return 0
290
291 if __name__ == "__main__":
292 sys.exit(main())
293 ```
294
295 Must: return `0` on success, non-zero on failure. Print progress. Be safe to rerun.
296
297 ---
298
299 ## plugin.yaml Schema Reference
300
301 ```yaml
302 name: my_plugin # required for community index (^[a-z0-9_]+$, must match dir name)
303 title: My Plugin # required, UI display name
304 description: What it does. # required
305 version: 1.0.0 # required
306 settings_sections: # optional, valid: agent | external | mcp | developer | backup
307 - agent
308 per_project_config: false # optional, enables project-scoped settings
309 per_agent_config: false # optional, enables agent-profile-scoped settings
310 always_enabled: false # optional, framework use only
311 ```
312
313 ---
314
315 ## Community Index: What CI Checks
316
317 When submitting to https://github.com/agent0ai/a0-plugins, CI validates:
318
319 **`index.yaml`** (in the index repo, NOT `plugin.yaml`):
320 - Fields: `title` (max 50), `description` (max 500), `github` (required), `tags` (optional, max 5), `screenshots` (optional, max 5 URLs)
321 - Max total file length: 2000 characters
322 - No unknown fields allowed
323
324 **Remote `plugin.yaml`** (your plugin's own repo):
325 - Must exist at repo root
326 - Must contain `name` field matching the index folder name exactly
327
328 **`LICENSE`** (your plugin's own repo):
329 - Must exist at repo root for Plugin Index / community listings (policy; same terms users expect from any open repo)
330
331 **Folder name**:
332 - Pattern: `^[a-z0-9_]+$` (underscores, no hyphens)
333 - Must not start with `_`
334 - Must be unique in the index