-
-
Notifications
You must be signed in to change notification settings - Fork 119
Plugin Best Practices
- Single Responsibility — Each plugin should do one thing well
-
Graceful Failure — Always return a result from tool
execute, never throw - Input Validation — Validate all args before execution
- Respect Timeouts — Default tool timeout is 30 s; return early if possible
- Minimal Permissions — Declare only capabilities you actually use
-
Clean Up — Unregister tools, cancel timers, close connections in
onUnload
// GOOD
execute: async (args) => {
const start = Date.now();
try {
const result = await doWork(args);
return { toolName: 'my_tool', success: true, output: result, durationMs: Date.now() - start };
} catch (err) {
return { toolName: 'my_tool', success: false, output: '',
error: (err as Error).message, durationMs: Date.now() - start };
}
},
// BAD — throws break the tool loop
execute: async (args) => {
if (!args.input) throw new Error('input required'); // never do this
},execute: async (args) => {
const url = args.url;
if (typeof url !== 'string' || !url.startsWith('https://')) {
return { toolName: 'fetch', success: false, output: '',
error: 'url must be a valid https:// URL', durationMs: 0 };
}
// ...
},output must always be a string. Use JSON.stringify() for structured data:
return { toolName: 'search', success: true,
output: JSON.stringify(results, null, 2), durationMs: Date.now() - start };- Use
ctx.statefor persistence — in-memory module variables reset on reload - Use
ctx.configfor user-configurable values, never hardcode strings - Use
ctx.loggerinstead ofconsole.logfor proper log prefixes - Register tools in
onActivate, unregister inonDeactivate(notonLoad/onUnload) - For fire-and-forget background work:
doSlowThing().catch(() => {})— never let uncaught promises kill the host
- Return valid JSON-RPC 2.0 errors:
{ "error": { "code": -32000, "message": "..." } } - Set a server-side timeout — CortexPrism has its own 30 s tool timeout
- Use
config:schemacapability to inject API keys viactx.config— never hardcode - Declare all tools in the manifest so the agent's tool selection works correctly
- Write capabilities as UTF-8 JSON:
plugin_get_capabilitieswrites[{"name":"...","description":"...","params":[...]}] - Always export
memoryfrom your module (required by the runtime) - Use
set_state/get_statehost functions —ctx.stateis not directly accessible from WASM - Keep binary size under 10 MB for fast startup
- Test outside CortexPrism first:
wasmtime run plugin.wasm - Return
0from host-interfacing functions on success, non-zero on error
Pipeline hooks can intercept the agent at 12 named stages. Mistakes here affect all users.
middlewarePre is called for every pipeline stage, not just pre-tool:
export const middlewarePre = async (ctx: unknown) => {
const c = ctx as { stage: string; toolCall?: { name: string } };
// ALWAYS check stage first
if (c.stage !== 'pre-tool') return {};
if (!c.toolCall) return {};
// ... your logic
return {};
};// GOOD
return { abort: { reason: 'policy', message: 'File access is blocked outside ~/projects/.' } };
// BAD — user sees nothing or a cryptic error
return { abort: { reason: 'denied', message: '' } };Sync hooks time out after 5 seconds. Offload slow work with fire-and-forget:
export const middlewarePost = async (ctx: unknown) => {
const c = ctx as { stage: string; toolResult?: { toolName: string } };
if (c.stage !== 'post-tool' || !c.toolResult) return {};
// Fire-and-forget for slow external logging
sendAuditLog(c.toolResult).catch(() => {});
return {};
};return {
sideEffects: [
{ type: 'store', payload: { key: 'lastTool', value: c.toolCall?.name } },
{ type: 'log', payload: { tool: c.toolCall?.name } },
],
};Only return modifyInput, modifyLLMResponse, or modifyOutput when you have a real reason.
Unnecessary mutations make debugging hard for the user.
let handler: ((event: unknown) => void) | null = null;
export const onLoad = async (ctx: PluginContext) => {
handler = (event) => {
ctx.logger.info(`Session: ${(event as { sessionId: string }).sessionId}`);
};
globalEventBus.on('session:start', handler);
};
export const onUnload = async (_ctx: PluginContext) => {
if (handler) {
globalEventBus.off('session:start', handler);
handler = null;
}
};Failing to unsubscribe leaves dangling handlers that fire after the plugin is disabled.
{
"capabilities": ["events:listener"],
"events": ["session:start", "llm:post-call"]
}Events not listed in events may be blocked by future security hardening.
// GOOD
"capabilities": ["tools", "network:fetch"]
// BAD — requesting everything without need
"capabilities": ["tools", "fs:read", "fs:write", "fs:delete", "shell:run", "network:fetch"]const myTool: Tool = {
definition: {
name: 'read_file',
description: 'Read a file',
params: [],
capabilities: ['fs:read'], // must be a subset of manifest capabilities
},
// ...
};If a tool declares capabilities not present in the manifest, it is rejected at load time.
If your plugin may run sandboxed (untrusted or signed trust level):
-
Do not use
Deno.env— env access is restricted in the Worker -
Do not use
Deno.Command— requiresshell:runcapability -
Do not call
Deno.exit()— terminates the Worker, tools stop responding -
Do not import from
https://at runtime withoutnetwork:fetch - Test with minimal permissions — run with only the capabilities you declare
- Sanitize dynamic content — never render raw API data as HTML; prevents XSS
-
Use
window.Cortex.getConfig()instead of hardcoded values -
Use
window.Cortex.notify()for user feedback — avoidalert()or browser dialogs -
Handle fetch failures —
window.Cortex.fetch()can fail; always wrap in try/catch - Design for narrow widths — sidebar panels may be as narrow as 240 px
- Never trust panel input — validate data from panels server-side if it drives actions
| Hook | What to do | What NOT to do |
|---|---|---|
onInstall |
Write default state, log first install | Make network calls |
onLoad |
Subscribe to events, init in-memory state | Register tools |
onActivate |
Register tools, start polling | Slow blocking initialization |
onDeactivate |
Unregister tools, stop polling | Modify persisted state |
onUnload |
Unsubscribe events, close connections | Throw — breaks the unload sequence |
onUninstall |
Delete persisted state/config | Assume onUnload already ran |
onConfigChange |
Apply the new config value | Block — called per-key on every change |
# Install from local directory
cortex plugins install ./my-plugin
# Enable and verify active
cortex plugins enable my-plugin
cortex plugins list
# Test tools interactively
cortex agent chat
# Check effective permissions
cortex plugins permissions my-plugin
# Test config change
cortex config set plugins.my-plugin.apiKey test123
# Disable/re-enable (tests deactivate/activate cycle)
cortex plugins disable my-plugin
cortex plugins enable my-plugin
# Remove (tests onUninstall)
cortex plugins remove my-plugin-
Don't throw from
execute()— it breaks the tool loop -
Don't call
Deno.exit()— kills the host process (trusted) or Worker (sandboxed) -
Don't hardcode API keys — use
ctx.config.get('apiKey')with aui.settingsfield -
Don't block
onLoad— slow synchronous work freezes all plugin loading - Don't skip cleanup — failing to unregister tools or listeners causes leaks
-
Don't check
toolCallwithout checking stage — it's only populated atpre-tool -
Don't emit events in
onUnload— the event bus may be partially torn down -
Don't store large data in
ctx.state— values are strings; store paths, not file contents
-
Export
plugin_get_abi_version— the host rejects plugins without it -
Use
host_allocfor dynamic memory — don't rely on fixed offsets; the host scratch area may shift -
Return 0 from
plugin_get_capabilities— non-zero aborts tool registration - Write full param schemas — LLMs can't use your tools without knowing their argument types
-
Keep
plugin_execute_toolunder 120 s — the host kills execution at this timeout - Return 0 on success — non-zero is treated as tool failure with output as error message
-
Call
host_set_stateearly — state is written asynchronously; set it before tool execution ends -
Don't import WASI system calls —
proc_exit,args_get,environ_get,sock_*are blocked by supply-chain scanning and won't work at runtime -
Use
host_randomfor cryptographic needs —Math.random()equivalent is not available in WASM -
Test with the SDK —
src/plugins/sdk/client.tsprovides validation helpers for capabilities JSON
- Plugin System — Architecture, sandbox mechanics, trust levels
- Developing Plugins — Full development guide with examples
- WASM Plugins — WASM-specific ABI reference, host functions, memory model, SDK
- Manifest Reference — Complete schema with all field types
- Publishing Plugins — Marketplace submission requirements
CortexPrism — Open-source AI agent operating system · Discord · Apache 2.0 License · Built with Deno 2.x + TypeScript
- Agent Loop
- Built-in Agents
- Metacognition
- Memory System
- Skills System
- Sub-Agents
- Built-in Tools
- Code Intelligence
- Code Sandbox
- Cross-Agent Context Protocol
- Prompt Lab
- PKM Assistant
- Voice Pipeline
- Computer Use
- Browser Tool
- Git & GitHub
- Scheduler & Jobs
- Dashboard
- Observability
- A2A Protocol
- MCP Gateway
- Distributed Nodes
- Memori Checkpoints
- Eval System
- Workflow Engine
- Triggers
- Projects
- TUI
- Glossary
- Update System
- Chrome Bridge
- Swarm
- AgentLint
- Model Benchmarking
- Smart Context
- Cost Optimizer