-
-
Notifications
You must be signed in to change notification settings - Fork 119
Developing Plugins
Complete guide to building CortexPrism plugins — tools, lifecycle hooks, pipeline hooks, UI panels,
CLI commands, LLM providers, events, sandbox mode, and the window.Cortex panel API.
my-plugin/
├── manifest.json # Plugin identity, capabilities, entry point
├── mod.ts # Entry point — exports tools, hooks, providers
├── tools/
│ └── my_tool.ts # Tool implementations (optional, can inline)
├── ui/
│ ├── panel.html # Web UI panel HTML
│ └── panel.js # Web UI panel JS (optional)
└── README.md
Every plugin requires a manifest.json at its root.
{
"name": "my-plugin",
"version": "1.0.0",
"description": "An example plugin",
"kind": "esm",
"entryPoint": "./mod.ts",
"runtime": "deno",
"capabilities": ["tools", "network:fetch"],
"author": "acme",
"homepage": "https://example.com",
"license": "MIT",
"repository": "https://github.com/acme/my-plugin",
"dependencies": { "other-plugin": "^1.0.0" },
"events": ["session:start", "llm:post-call"],
"tools": [
{
"name": "my_tool",
"description": "Does something useful",
"params": [
{ "name": "input", "type": "string", "description": "Input value", "required": true },
{ "name": "mode", "type": "string", "description": "Mode", "required": false,
"enum": ["fast", "thorough"] }
]
}
],
"cliCommands": [
{
"name": "my-cmd",
"description": "A CLI subcommand",
"options": [
{ "name": "verbose", "type": "boolean", "description": "Verbose output", "flag": "-v" }
]
}
],
"ui": {
"panels": [
{ "id": "main", "title": "My Panel", "icon": "star", "htmlPath": "./ui/panel.html" }
],
"widgets": [
{ "id": "stats", "title": "Stats", "type": "html", "config": {} }
],
"settings": [
{
"section": "API",
"fields": [
{ "key": "apiKey", "label": "API Key", "type": "secret", "defaultValue": "",
"description": "Your service API key" },
{ "key": "maxResults", "label": "Max Results", "type": "number", "defaultValue": 10 },
{ "key": "mode", "label": "Mode", "type": "select", "defaultValue": "fast",
"options": [{ "label": "Fast", "value": "fast" }, { "label": "Thorough", "value": "thorough" }] }
]
}
]
},
"config": {
"providers": [
{ "kind": "my-provider", "label": "My LLM Provider", "defaultModel": "my-model-v1" }
]
}
}See Manifest Reference for every field in detail.
The loader reads these named exports from your entry point. All are optional.
import type { Tool, PluginContext, CliCommandDeclaration } from 'cortex/plugins';
// --- Tools ---
export const tools: Tool[] = [];
// --- CLI subcommands (requires cli:commands capability) ---
export const cliCommands: CliCommandDeclaration[] = [];
// --- LLM provider factories (requires config:provider capability) ---
export const providers: Record<string, (config: Record<string, unknown>) => unknown> = {};
// --- Lifecycle hooks ---
export const onInstall = async (ctx: PluginContext) => {};
export const onLoad = async (ctx: PluginContext) => {};
export const onActivate = async (ctx: PluginContext) => {};
export const onDeactivate = async (ctx: PluginContext) => {};
export const onUnload = async (ctx: PluginContext) => {};
export const onUninstall = async (ctx: PluginContext) => {};
export const onConfigChange = async (key: string, value: unknown, ctx: PluginContext) => {};
// --- Pipeline hooks (requires middleware:pre / middleware:post capability) ---
export const middlewarePre = async (ctx: unknown) => ({});
export const middlewarePost = async (ctx: unknown) => ({});Each hook receives a PluginContext. All are optional exports.
// Called once when the plugin is first installed
export const onInstall = async (ctx: PluginContext) => {
await ctx.state.set('installedAt', new Date().toISOString());
};
// Called each time the module is imported into memory
export const onLoad = async (ctx: PluginContext) => {
ctx.logger.info('Plugin loaded');
};
// Called after tools are registered and events subscribed (plugin is now "live")
export const onActivate = async (ctx: PluginContext) => {
ctx.logger.info('Plugin active');
};
// Called before tools are unregistered (plugin about to go offline)
export const onDeactivate = async (ctx: PluginContext) => {};
// Called when the module is unloaded from memory — clean up intervals, connections
export const onUnload = async (ctx: PluginContext) => {};
// Called when the plugin is permanently removed — clean up persisted data
export const onUninstall = async (ctx: PluginContext) => {
await ctx.state.delete('installedAt');
};
// Called each time a single config key changes at runtime
export const onConfigChange = async (key: string, value: unknown, ctx: PluginContext) => {
ctx.logger.info(`Config "${key}" → ${value}`);
};Install: onInstall → onLoad → onActivate
Enable: onLoad → onActivate
Disable: onDeactivate → onUnload
Remove: onDeactivate → onUnload → onUninstall
Reconfigure: onConfigChange (per key, non-blocking)
Every lifecycle hook and tool execute receives a PluginContext.
Survives plugin reloads and host restarts. Values are always strings.
await ctx.state.set('counter', '0');
const counter = await ctx.state.get('counter'); // string | null
const all = await ctx.state.list(); // Record<string, string>
await ctx.state.delete('counter');Reads from ~/.cortex/config.json under plugins.<name>.<key>.
const apiKey = await ctx.config.get<string>('apiKey'); // T | null
const maxRetries = await ctx.config.get<number>('maxRetries');
const all = await ctx.config.getAll(); // Record<string, unknown>
await ctx.config.set('lastInit', Date.now());Prefixes every message with [plugin:<name>].
ctx.logger.info('Operation completed');
ctx.logger.warn('Rate limit approaching');
ctx.logger.error('Failed to connect');
ctx.logger.debug('Payload:', { key: 'value' });Register or unregister tools at runtime (e.g. based on config).
export const onActivate = async (ctx: PluginContext) => {
ctx.host.registerTool({
definition: {
name: 'dynamic_tool',
description: 'Created at runtime',
params: [],
capabilities: [],
},
execute: async () => ({
toolName: 'dynamic_tool',
success: true,
output: 'Hello from dynamic tool',
durationMs: 0,
}),
});
};
export const onDeactivate = async (ctx: PluginContext) => {
ctx.host.unregisterTool('dynamic_tool');
};ctx.pluginId // Plugin name string, e.g. "my-plugin"
ctx.pluginDir // Absolute path to ~/.cortex/data/plugins/my-plugin/interface Tool {
definition: {
name: string;
description: string;
params: {
name: string;
type: string; // 'string' | 'number' | 'boolean' | 'object' | 'array'
description: string;
required?: boolean;
enum?: string[]; // restrict to allowed values
}[];
capabilities: string[]; // per-tool permissions, subset of manifest capabilities
};
execute(args: Record<string, unknown>, ctx: ToolContext): Promise<ToolCallResult>;
}
interface ToolCallResult {
toolName: string;
success: boolean;
output: string; // always a string — JSON.stringify objects
error?: string; // set when success is false
durationMs: number;
}const myTool: Tool = {
definition: {
name: 'fetch_data',
description: 'Fetch data from a URL',
params: [
{ name: 'url', type: 'string', description: 'URL to fetch', required: true },
],
capabilities: ['network:fetch'],
},
execute: async (args, ctx) => {
const start = Date.now();
// 1. Validate inputs first
if (!args.url || typeof args.url !== 'string') {
return { toolName: 'fetch_data', success: false, output: '',
error: 'url is required', durationMs: 0 };
}
// 2. Never throw — always return a result
try {
const res = await fetch(args.url);
const text = await res.text();
return { toolName: 'fetch_data', success: true, output: text,
durationMs: Date.now() - start };
} catch (err) {
return { toolName: 'fetch_data', success: false, output: '',
error: (err as Error).message, durationMs: Date.now() - start };
}
},
};Plugins can intercept the agent execution pipeline at 12 named stages. Export middlewarePre and/or
middlewarePost from your module and declare the matching capability.
| Stage | When | Available context fields |
|---|---|---|
pre-assess |
Before metacognitive assessment |
input, messages
|
post-assess |
After assessment |
assessment, messages
|
pre-reason |
Before reasoning loop |
input, messages
|
post-reason |
After reasoning loop | output |
pre-tool |
Before each tool call | toolCall |
post-tool |
After each tool call |
toolCall, toolResult
|
pre-llm |
Before LLM API call | messages |
post-llm |
After LLM API call | currentLLMResponse |
pre-reflect |
Before reflection | output |
post-reflect |
After reflection | reflection |
pre-output |
Before final response | output |
post-output |
After final response | output |
middlewarePre hooks run at pre-tool. middlewarePost hooks run at post-tool.
interface HookResult {
abort?: { reason: string; message: string }; // abort the turn — message shown to user
modifyInput?: string; // replace user input
modifyLLMResponse?: string; // replace raw LLM response text
modifyOutput?: string; // replace final output
injectMessages?: Message[]; // append to conversation history
sideEffects?: Array<
| { type: 'log'; payload: unknown }
| { type: 'metric'; payload: unknown }
| { type: 'store'; payload: { key: string; value: unknown } }
| { type: 'notify'; payload: unknown }
>;
}export const middlewarePre = async (ctx: unknown) => {
const c = ctx as { stage: string; toolCall?: { name: string } };
if (c.stage !== 'pre-tool' || !c.toolCall) return {};
const BLOCKED = ['file_delete', 'shell_run'];
if (BLOCKED.includes(c.toolCall.name)) {
return {
abort: {
reason: 'policy',
message: `Tool "${c.toolCall.name}" is blocked by the security plugin.`,
},
};
}
return {};
};export const middlewarePost = async (ctx: unknown) => {
const c = ctx as {
stage: string;
toolResult?: { toolName: string; durationMs: number; success: boolean };
};
if (c.stage !== 'post-tool' || !c.toolResult) return {};
console.log(
`[audit] ${c.toolResult.toolName} → ${c.toolResult.success ? 'ok' : 'fail'} ` +
`(${c.toolResult.durationMs}ms)`
);
return {};
};Sync hooks time out after 5 s. Set
async: truein yourHookResultfor up to 15 s. Hooks frommiddlewarePre/middlewarePostrun atpriority: 50.
Declare events:listener in capabilities, list event types in the manifest events field, then
subscribe in onLoad.
{
"capabilities": ["events:listener"],
"events": ["session:start", "session:end", "tool:post-execute", "llm:post-call"]
}import { globalEventBus } from 'cortex/plugins';
export const onLoad = async (ctx: PluginContext) => {
globalEventBus.on('session:start', (event) => {
ctx.logger.info(`Session started: ${(event as { sessionId: string }).sessionId}`);
});
globalEventBus.on('tool:post-execute', (event) => {
const e = event as { toolName: string; result: unknown };
ctx.logger.debug(`Tool ${e.toolName} completed`);
});
globalEventBus.on('llm:post-call', (event) => {
const e = event as { provider: string; model: string; tokensIn: number; tokensOut: number };
ctx.logger.debug(`LLM ${e.provider}/${e.model}: ${e.tokensIn}→${e.tokensOut} tokens`);
});
};| Event | Payload fields |
|---|---|
session:start |
{ sessionId: string } |
session:end |
{ sessionId: string } |
tool:pre-execute |
{ toolName: string, args: Record<string, unknown> } |
tool:post-execute |
{ toolName: string, result: unknown } |
llm:pre-call |
{ provider: string, model: string } |
llm:post-call |
{ provider: string, model: string, tokensIn: number, tokensOut: number } |
agent:turn-start |
{ sessionId: string, turnId: string } |
agent:turn-end |
{ sessionId: string, turnId: string, response: string } |
config:change |
{ key: string, value: unknown } |
daemon:status |
{ daemon: string, status: 'up' | 'down' } |
Unsubscribe by calling globalEventBus.off(type, handler) in onUnload.
Export a handler function and declare the command in the manifest.
// mod.ts
export async function myCommand(args: Record<string, unknown>) {
const verbose = args.verbose as boolean;
console.log('Running:', args, verbose ? '(verbose)' : '');
}{
"capabilities": ["cli:commands"],
"cliCommands": [
{
"name": "my-cmd",
"description": "My plugin CLI command",
"args": [
{ "name": "target", "type": "string", "description": "Target name", "required": true }
],
"options": [
{ "name": "verbose", "type": "boolean", "description": "Verbose output", "flag": "-v" }
]
}
]
}Available as cortex my-cmd <target> [-v] after enabling.
// mod.ts
export const providers = {
'my-llm': (config: Record<string, unknown>) => ({
name: 'my-llm',
defaultModel: 'my-model-v1',
async complete(opts: unknown) {
// Return { content: string, tokensIn: number, tokensOut: number }
},
async *stream(opts: unknown) {
// Yield { delta: string } chunks
},
}),
};{
"capabilities": ["config:provider"],
"config": {
"providers": [
{ "kind": "my-llm", "label": "My LLM", "defaultModel": "my-model-v1" }
]
}
}Declare panels in the manifest. Panels are served as iframes with the window.Cortex API injected.
manifest.json:
{
"capabilities": ["ui:panel"],
"ui": {
"panels": [
{ "id": "dashboard", "title": "My Dashboard", "icon": "bar-chart", "htmlPath": "./ui/panel.html" }
]
}
}ui/panel.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My Dashboard</title>
<style>
body { font-family: system-ui; padding: 1rem; background: #0a0a0f; color: #e2e2ea; }
</style>
</head>
<body>
<h2>My Plugin</h2>
<div id="output"></div>
<script>
// window.Cortex is injected by the host — no import needed
window.Cortex.getConfig('apiKey').then(key => {
document.getElementById('output').textContent = key ? 'Key configured' : 'No key set';
});
</script>
</body>
</html>Declare settings fields in the manifest ui.settings array. They appear in the plugin settings UI.
{
"ui": {
"settings": [
{
"section": "Connection",
"fields": [
{ "key": "apiKey", "label": "API Key", "type": "secret", "defaultValue": "" },
{ "key": "endpoint", "label": "Endpoint URL", "type": "text", "defaultValue": "https://api.example.com" },
{ "key": "timeout", "label": "Timeout (ms)", "type": "number", "defaultValue": 5000 },
{ "key": "verify", "label": "Verify SSL", "type": "boolean", "defaultValue": true },
{ "key": "region", "label": "Region", "type": "select", "defaultValue": "us-east",
"options": [{ "label": "US East", "value": "us-east" }, { "label": "EU West", "value": "eu-west" }] }
]
}
]
}
}Setting types: text | number | boolean | select | secret
See Pipeline Hooks above.
Injected into every plugin panel iframe by the CortexPrism host:
// Authenticated fetch — path relative to /api/plugins/<name>/ or absolute
const res = await window.Cortex.fetch('config');
const res2 = await window.Cortex.fetch('/api/sessions');
// Config access
const key = await window.Cortex.getConfig('apiKey');
await window.Cortex.setConfig('theme', 'dark');
// Events
window.Cortex.onEvent('my-event', (data) => console.log(data));
window.Cortex.emit('panel:ready', { ts: Date.now() });
// Notifications (shown in the host UI)
window.Cortex.notify('Saved!', 'info'); // 'info' | 'warn' | 'error'Direct postMessage to the parent frame:
// Emit a named event
window.parent.postMessage({
type: 'cortex-event',
pluginName: 'my-plugin',
event: 'my-event',
data: { key: 'value' }
}, '*');
// Show notification
window.parent.postMessage({
type: 'cortex-notification',
pluginName: 'my-plugin',
notification: { msg: 'Done!', type: 'info' }
}, '*');Structured commands panels can send to the host:
type |
Required fields | Effect |
|---|---|---|
navigate |
to: string |
Navigate host UI to route |
open-modal |
title: string, content: string |
Open host modal |
notification |
text: string, level: string |
Show notification |
config-get |
key: string |
Read a config value |
config-set |
key: string, value: unknown |
Write a config value |
query |
query: string |
Execute a query |
Plugins with trust_level of untrusted or signed run in a Deno Worker sandbox with
restricted permissions. Only trusted plugins (verified supply-chain) run in-process.
| Capability | Deno Worker permission |
|---|---|
fs:read, fs:list
|
read: true |
fs:write, fs:edit, fs:delete
|
write: true |
shell:run |
run: true |
network:fetch, net:outbound, net:inbound
|
net: true |
The sandbox expects your worker to post { type: 'ready' } on startup and respond to two RPC methods:
// sandbox-mod.ts — loaded as Deno Worker
import { tools } from './tools.ts';
// Signal readiness
self.postMessage({ type: 'ready' });
self.onmessage = async (ev) => {
const { id, method, params } = ev.data;
if (method === 'getTools') {
self.postMessage({ id, result: tools.map(t => t.definition) });
return;
}
if (method === 'executeTool') {
const tool = tools.find(t => t.definition.name === params.toolName);
if (!tool) {
self.postMessage({ id, error: { message: `Unknown tool: ${params.toolName}` } });
return;
}
try {
const result = await tool.execute(params.args, {});
self.postMessage({ id, result });
} catch (e) {
self.postMessage({ id, error: { message: (e as Error).message } });
}
return;
}
self.postMessage({ id, error: { message: `Unknown method: ${method}` } });
};Timeout: The sandbox must post
{ type: 'ready' }within 30 seconds or the worker is terminated and the plugin fails to load with an error.
Marketplace plugins use @author/name scoping:
@acme/weather → author=acme, name=weather
my-plugin → author=unknown, name=my-plugin (auto-resolved)
Tool names from namespaced plugins follow @author/plugin-name/tool_name.
MCP plugins wrap an external JSON-RPC 2.0 server. CortexPrism creates a synthetic tool
mcp_<name> that proxies calls to the server.
{
"name": "my-mcp",
"kind": "mcp",
"entryPoint": "https://my-server.example.com/rpc",
"runtime": "deno",
"capabilities": ["tools", "network:fetch"],
"tools": [
{ "name": "search", "description": "Search", "params": [
{ "name": "query", "type": "string", "description": "Query", "required": true }
]}
]
}The host sends:
{ "jsonrpc": "2.0", "id": 1, "method": "search", "params": { "query": "..." } }The server must respond:
{ "jsonrpc": "2.0", "id": 1, "result": { ... } }WASM plugins compile to .wasm (WebAssembly) and run in a dedicated worker sandbox with synchronous
host functions. Any language that compiles to WASM is supported (C, Rust, Zig, Go, AssemblyScript).
WASM plugins must export plugin_get_abi_version() returning the ABI version they support. The host
checks this on load and rejects incompatible versions. Current host ABI version: 1.
int32_t plugin_get_abi_version(void) { return 1; }| Export | Signature | Required | Purpose |
|---|---|---|---|
plugin_get_abi_version |
() → i32 |
Yes | Return supported ABI version |
plugin_init |
() → void |
No | Called once on load |
plugin_destroy |
() → void |
No | Called on unload — free resources |
plugin_get_capabilities |
(outPtr, outLenPtr) → i32 |
Yes | Return JSON capabilities |
plugin_execute_tool |
(namePtr, nameLen, argsPtr, argsLen, outPtr, outLenPtr) → i32 |
Yes | Execute a tool |
memory |
WebAssembly.Memory |
Yes | Shared linear memory (must be exported) |
plugin_get_capabilities must return a JSON object with full parameter schemas:
{
"abi_version": 1,
"tools": [
{
"name": "my_tool",
"description": "Does something useful",
"params": [
{ "name": "input", "type": "string", "description": "The input", "required": true },
{ "name": "limit", "type": "number", "description": "Max results", "required": false }
]
}
]
}Parameter types: string | number | boolean | object | array. The host uses this schema to
describe tool parameters to LLMs, so WASM tools are fully understood by agents.
[0x000000 - 0x00FFFF] Host scratch area (64 KB) — do not use
[0x010000 - 0x01FFFF] Host allocator metadata (64 KB) — do not use
[0x020000 - 0x0FFFFF] Host-managed heap (896 KB) — use via host_alloc/host_free
[0x100000 - ...] Your memory — start data here (__heap_base = 0x100000)
Default memory: 256 initial pages (16 MiB), 512 max pages (32 MiB).
All strings are UTF-8 in linear memory. Return 0 for success, non-zero for error. Parameters are
(pointer, length) pairs. Output is written to outPtr/outLenPtr.
| Function | Signature | Description |
|---|---|---|
host_alloc |
(size: i32) → i32 |
Bump-allocate from host-managed heap. Returns aligned pointer or 0. |
host_free |
(ptr: i32) → void |
Free allocation (current no-op, call for forward compatibility). |
host_log |
(ptr: i32, len: i32) → void |
Log a message to the CortexPrism log system. |
host_get_config |
(keyPtr, keyLen, outPtr, outLenPtr) → i32 |
Read config from CORTEX_PLUGIN_{NAME}_{KEY} or CORTEX_WASM_{KEY} env vars. |
host_set_state |
(keyPtr, keyLen, valPtr, valLen) → void |
Persist key-value state (in-memory cache + async SQLite flush). |
host_get_state |
(keyPtr, keyLen, outPtr, outLenPtr) → i32 |
Read persisted state. Returns -1 if not found. |
host_http_request |
(methodPtr, methodLen, urlPtr, urlLen, bodyPtr, bodyLen, headersPtr, headersLen, outStatusPtr, outBodyPtr, outBodyLenPtr) → i32 |
Synchronous HTTP request (via Worker + SharedArrayBuffer + Atomics.wait). 30 s timeout. Requires network:fetch or net:outbound capability. |
host_get_abi_version |
() → i32 |
Returns host ABI version (currently 1). |
host_get_time_ms |
() → i64 |
Current time in milliseconds since Unix epoch. |
host_random |
(outPtr: i32, len: i32) → void |
Fill len bytes with cryptographically random data. |
host_http_request is gated on the network:fetch or net:outbound capability declared in the
plugin manifest. If the capability is not present, the request returns HTTP 403. WASM plugins
execute in a dedicated Worker and have no direct filesystem, shell, or database access — only the
host functions above.
Each tool execution is limited to 120 seconds. HTTP requests within tool execution have a 30-second individual timeout. The timeout guards against infinite loops and runaway memory allocation.
host_set_state/host_get_state use an in-memory cache backed by SQLite. State survives plugin
reloads and host restarts. The cache is flushed to the plugin_state table asynchronously.
A C-compatible header is available at src/plugins/sdk/wasm-plugin.h declaring all host functions,
memory layout constants (CORTEX_HEAP_BASE, CORTEX_SCRATCH_SIZE), and required exports.
A TypeScript helper library at src/plugins/sdk/client.ts provides defineTool(), definePlugin(),
generateCapabilitiesJson(), and generateWasmPluginModule() for building WASM plugin code.
#include "wasm-plugin.h"
static char result_buffer[65536];
int32_t plugin_get_abi_version(void) { return 1; }
void plugin_init(void) {
host_log("Hello from WASM plugin!", 23);
}
int32_t plugin_get_capabilities(char* out, uint32_t* outLen) {
const char* json = "{\"abi_version\":1,\"tools\":[{\"name\":\"echo\","
"\"description\":\"Echoes input\",\"params\":[{\"name\":\"msg\","
"\"type\":\"string\",\"description\":\"Message\",\"required\":true}]}]}";
uint32_t len = strlen(json);
memcpy(out, json, len);
*outLen = len;
return 0;
}
int32_t plugin_execute_tool(
const char* namePtr, uint32_t nameLen,
const char* argsPtr, uint32_t argsLen,
char* outPtr, uint32_t* outLen) {
// Parse JSON args, execute tool, write result
const char* result = "{\"echo\":\"hello\"}";
uint32_t len = strlen(result);
memcpy(outPtr, result, len);
*outLen = len;
return 0;
}
void plugin_destroy(void) {
// Clean up resources
}Load: fetch WASM → instantiate → plugin_get_abi_version → plugin_init → plugin_get_capabilities
Execute: plugin_execute_tool (per agent tool call)
Unload: plugin_destroy → free WebAssembly.Memory → clear state cache
WASM lifecycle is fully managed — plugin_destroy is called automatically when the plugin is
disabled or removed. State cache is cleaned up on unload.
Every plugin installation triggers an integrity check:
| Check | Description |
|---|---|
| SHA-256 hash | Compared against known-good hash list |
| Blocked hash | Known-malicious hashes are rejected outright |
| Digital signature | Optional GPG signature validation |
| Author reputation | Score 0–100; blocked/allowed author lists |
| Malware scan | 6 default patterns: eval, child_process, rm -rf /, curl|sh, wget|sh
|
Trust level is derived from the result:
- Verification passes →
trusted(runs in-process) - No hash match →
signed(sandboxed) - Suspicious / blocked →
untrusted(sandboxed with minimal permissions)
Re-scan via: POST /api/plugins/:name/verification
Admins can grant or deny specific capabilities beyond what the manifest declares:
POST /api/plugins/:name/permissions
{ "permission_path": "shell:run", "action": "deny", "value": "" }
{ "permission_path": "fs:read", "action": "grant", "value": "" }
Effective permissions = declared − denied + granted. These govern sandbox Deno Worker permissions.
# Install locally (entry point must be absolute or file:// URL after resolution)
cortex plugins install ./my-plugin/manifest.json
# Enable
cortex plugins enable my-plugin
# Verify it loaded
cortex plugins list
# Test in a chat session
cortex agent chat
# View permission details
cortex plugins permissions my-plugin
# Push an update after changes
cortex plugins update my-plugin| Symptom | Cause / Fix |
|---|---|
Status: error
|
Run cortex plugins list — check error_message. Common: bad entry point, missing export, import error |
| Tool not visible | Plugin must be active; tool must be in tools export |
| Permission denied | Tool capabilities must be a subset of manifest capabilities
|
| UI panel missing | Declare ui:panel; verify htmlPath is relative to manifest and file exists |
| Events not firing | Declare events:listener AND list event types in manifest events field |
| Sandbox not ready | Worker must post { type: 'ready' } within 30 s |
| Config not reading | Key must match what was set via ctx.config.set() or ~/.cortex/config.json under plugins.<name>
|
Plugin data: ~/.cortex/data/plugins/<name>/
- Plugin System — Architecture overview, lifecycle, security model
- Manifest Reference — Every manifest field with types
- Plugin Best Practices — Design principles and what to avoid
- Publishing Plugins — Marketplace submission
- Pipeline Hooks — Full agent pipeline hook reference
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