Extensions
Aleph's plugin architecture supporting WASM and Node.js runtimes, manifest-driven configuration, hook systems, background services, and channel/provider plugins.
The extension system is how Aleph grows beyond its built-in capabilities. Through a manifest-driven plugin architecture, third-party developers (or you) can add new tools, intercept system events, define custom AI providers, expose HTTP endpoints, and even create entirely new messaging channels. Extensions run in sandboxed runtimes -- either fast WebAssembly modules or full-featured Node.js processes -- with declarative permission controls.
Architecture Overview
Extension Manager
├── Loader ── Discovery, manifest parsing, validation
├── Registry ── Plugin registration, tool lookup, status tracking
├── Watcher ── Hot reload on file changes
└── Plugin Runtimes
├── WASM Runtime (Extism) ── Sandboxed, fast startup, limited I/O
└── Node.js Runtime (IPC) ── Full Node.js API, stdio communicationThe Extension Manager coordinates loading, lifecycle, and communication across all plugins. Each plugin declares its capabilities in a manifest file, and the manager routes tool calls, hook events, and service commands to the appropriate runtime.
Plugin Structure
Every plugin lives in its own directory with a manifest file:
~/.aleph/plugins/my-plugin/
├── aleph_plugin.toml # Plugin manifest (required)
├── dist/
│ └── index.js # Entry point (Node.js)
├── src/
│ └── index.ts # Source code
├── SKILL.md # Optional prompt file
└── docs/
└── INSTRUCTIONS.md # Optional tool-specific instructionsManifest Format
The manifest file (aleph_plugin.toml) is the contract between a plugin and the Aleph runtime. It declares everything the plugin provides and everything it needs.
Complete Example
[plugin]
id = "my-plugin" # Unique identifier
name = "My Plugin" # Display name
version = "1.0.0" # SemVer version
description = "Does something useful"
author = "Your Name"
kind = "nodejs" # nodejs | wasm | static
entry = "dist/index.js" # Entry point for nodejs/wasm
[permissions]
network = ["connect:https://*"] # Network access rules
filesystem = ["read:./data", "write:./output"]
env = ["API_KEY", "DEBUG"] # Allowed environment variables
[prompt]
file = "SKILL.md" # Prompt file path
scope = "system" # system | tool | standalone | disabled
[[tools]]
name = "my_tool"
description = "Performs a specific task"
handler = "handleMyTool"
instruction_file = "docs/INSTRUCTIONS.md"
[[tools]]
name = "another_tool"
description = "Another useful tool"
handler = "handleAnotherTool"
[[hooks]]
event = "before_tool_call"
kind = "interceptor" # interceptor | observer | resolver
priority = "normal" # system | high | normal | low
handler = "onBeforeTool"
[[hooks]]
event = "after_tool_call"
kind = "observer"
priority = "low"
handler = "onAfterTool"Plugin Kinds
| Kind | Description | Entry Point | Use Case |
|---|---|---|---|
nodejs | Node.js process communicating via JSON-RPC over stdio | JavaScript/TypeScript file | Full-featured plugins with network access |
wasm | WebAssembly module loaded into Extism runtime | .wasm binary | Fast, sandboxed computation |
static | No executable code, only prompts and configuration | None | Coding standards, style guides, knowledge bases |
Manifest Priority
When multiple manifest formats exist, Aleph uses this resolution order:
aleph_plugin.toml(V2 TOML) -- preferredaleph_plugin.json(V2 JSON)package.jsonwithalephPluginsection- Legacy manifest formats
WASM Runtime
The WASM runtime uses Extism to execute WebAssembly plugins in a sandboxed environment. WASM plugins are ideal for compute-intensive operations that do not need filesystem or network access.
pub struct WasmRuntime {
plugins: HashMap<String, ExtismPlugin>,
}
impl WasmRuntime {
pub fn load(&mut self, path: &Path) -> Result<()>;
pub fn call(&self, plugin: &str, function: &str, input: &[u8])
-> Result<Vec<u8>>;
}Writing a WASM Plugin (Rust)
use extism_pdk::*;
#[plugin_fn]
pub fn my_tool(input: String) -> FnResult<String> {
let args: serde_json::Value = serde_json::from_str(&input)?;
let result = process(args);
Ok(serde_json::to_string(&result)?)
}WASM Sandbox Limitations
| Resource | Constraint |
|---|---|
| Filesystem | No access (fully sandboxed) |
| Network | No access (fully sandboxed) |
| Memory | Configurable limit (default: 256 MB) |
| CPU time | Configurable timeout (default: 30 seconds) |
These limits are configured in the extension settings:
[extensions.runtimes.wasm]
enabled = true
memory_limit = "256MB"
timeout_ms = 30000Node.js Runtime
Node.js plugins run as child processes and communicate with Aleph via JSON-RPC over stdio. They have access to the full Node.js API, making them suitable for plugins that need network access, file I/O, or complex dependencies.
pub struct NodejsRuntime {
processes: HashMap<String, Child>,
}
impl NodejsRuntime {
pub async fn start(&mut self, plugin: &PluginManifest) -> Result<()>;
pub async fn call(&self, plugin: &str, method: &str, args: Value)
-> Result<Value>;
}Writing a Node.js Plugin
import { createServer } from '@aleph/plugin-sdk';
const server = createServer({
name: 'my-plugin',
tools: {
my_tool: async (args: { input: string }) => {
return { result: `Processed: ${args.input}` };
},
another_tool: async (args: { query: string }) => {
const response = await fetch(`https://api.example.com?q=${args.query}`);
return response.json();
}
}
});
server.start();The @aleph/plugin-sdk handles all JSON-RPC communication automatically. You just define your tool handlers as async functions.
Plugin Discovery and Registration
Discovery
The PluginDiscovery component scans three directories for plugins:
~/.aleph/plugins/-- User plugins/usr/local/share/aleph/plugins/-- System plugins./plugins/-- Project-local plugins
Registration Flow
Plugin Directory Found
│
▼
1. Parse manifest (aleph_plugin.toml)
│
▼
2. Validate manifest
• Required fields present
• Version compatibility
• No tool name conflicts
│
▼
3. Select runtime
WASM → WasmRuntime
Node.js → NodejsRuntime
Static → No runtime needed
│
▼
4. Register tools in ToolServer
│
▼
Plugin ReadyPlugin Status
pub enum PluginStatus {
Loaded, // Manifest parsed, not yet started
Running, // Runtime active, tools available
Stopped, // Manually stopped
Error(String), // Failed to start or crashed
}Hook System
Hooks allow plugins to intercept and respond to system events at defined points in the execution flow.
Hook Types
| Type | Execution | Behavior |
|---|---|---|
| Interceptor | Sequential | Can modify context or block execution. Each hook receives the output of the previous one. |
| Observer | Parallel | Fire-and-forget. Errors logged but do not affect execution. Used for telemetry and logging. |
| Resolver | Sequential | First-win competition. Execution stops when a hook returns a non-null result. |
Available Events
| Event | Description |
|---|---|
before_tool_call | Before any tool is invoked |
after_tool_call | After tool execution completes |
on_message | When a user message is received |
on_response | Before response is sent to user |
on_error | When an error occurs |
Hook Priorities
| Priority | Value | Use Case |
|---|---|---|
system | -1000 | Core system hooks (runs first) |
high | -100 | Security checks, validation |
normal | 0 | Default priority |
low | 100 | Logging, telemetry, cleanup |
Lower values execute first. Within the same priority, hooks execute in registration order.
Hook Examples
// Interceptor: can modify or block
async function onBeforeTool(context: HookContext): Promise<HookContext> {
if (context.toolName === 'dangerous_tool') {
throw new Error('Tool blocked by security policy');
}
context.args.timestamp = Date.now();
return context;
}
// Observer: fire-and-forget
async function onAfterTool(context: HookContext): Promise<void> {
console.log(`Tool ${context.toolName} executed in ${context.duration}ms`);
}
// Resolver: first-win
async function resolveConfig(context: HookContext): Promise<Config | null> {
if (context.key in myConfigs) {
return myConfigs[context.key]; // Wins, stops chain
}
return null; // Continue to next resolver
}Prompt Scopes
Plugins can inject prompts into the agent's context with fine-grained control:
| Scope | Behavior |
|---|---|
system | Always injected when the plugin is active |
tool | Injected only when the bound tool is available in the current context |
standalone | User must explicitly invoke (e.g., /my-plugin) |
disabled | Never injected (useful for temporarily disabling prompts) |
Direct Commands
Direct commands bypass the LLM entirely, executing plugin functions immediately when invoked by the user:
[[commands]]
name = "ping"
description = "Check if the plugin is responsive"
handler = "handlePing"
[[commands]]
name = "status"
description = "Get current plugin status"
handler = "handleStatus"Direct commands are fast (no LLM round-trip), deterministic (same input always produces the same output), and explicit (user must explicitly invoke them). They are invoked via the Gateway RPC method plugins.executeCommand.
Background Services
Plugins can run long-lived background processes that operate independently of the request/response cycle:
[[services]]
name = "file-watcher"
description = "Watches filesystem for changes"
start_handler = "startFileWatcher"
stop_handler = "stopFileWatcher"
auto_start = true
[[services]]
name = "sync-daemon"
description = "Background sync service"
start_handler = "startSync"
stop_handler = "stopSync"
auto_start = falseServices follow a state machine: Stopped -> Starting -> Running -> Stopping -> Stopped. The ServiceManager coordinates all background services and exposes them through Gateway RPC methods (services.start, services.stop, services.list, services.status).
Channel Plugins
Channel plugins enable new messaging integrations. They are bidirectional bridges between external platforms and Aleph:
[[channels]]
name = "slack"
description = "Slack workspace integration"
connect_handler = "connectSlack"
disconnect_handler = "disconnectSlack"
send_handler = "sendSlack"Channel plugins implement three handlers:
- connect -- Establish the connection to the external platform and register a message callback.
- disconnect -- Clean up connections and resources.
- send -- Format and deliver a message to a specific chat on the platform.
Provider Plugins
Provider plugins add custom AI backends to Aleph:
[[providers]]
name = "local-llama"
description = "Local Llama.cpp server"
list_models_handler = "listModels"
chat_handler = "handleChat"
stream_handler = "handleStream"
embed_handler = "handleEmbed"The PluginProviderAdapter bridges plugin providers with Aleph's AiProvider trait, making plugin-provided models available alongside built-in providers.
HTTP Routes
Plugins can expose REST endpoints for webhooks, integrations, and custom APIs:
[[http_routes]]
method = "POST"
path = "/webhooks/github"
handler = "handleGithubWebhook"
[[http_routes]]
method = "GET"
path = "/api/status"
handler = "handleStatus"HTTP routes are served under /plugins/{plugin_name}/ on the Gateway's HTTP server.
Hot Reload
The PluginWatcher monitors plugin directories for changes and automatically reloads plugins:
pub struct PluginWatcher {
watcher: RecommendedWatcher,
registry: Arc<RwLock<PluginRegistry>>,
}When a file changes, the watcher:
- Create/Modify -- Reloads the affected plugin (re-parses manifest, restarts runtime).
- Remove -- Unloads the plugin and removes its tools from the registry.
Extension Configuration
Global extension settings in config.toml:
[extensions]
enabled = true
search_paths = ["~/.aleph/plugins", "./plugins"]
hot_reload = true
[extensions.runtimes.wasm]
enabled = true
memory_limit = "256MB"
timeout_ms = 30000
[extensions.runtimes.nodejs]
enabled = true
node_version = "20"Gateway RPC Methods
| Method | Description |
|---|---|
plugins.list | List all installed plugins with status |
plugins.install | Install a plugin from path or URL |
plugins.uninstall | Remove a plugin |
plugins.enable | Enable a disabled plugin |
plugins.disable | Disable a plugin without removing it |
plugins.reload | Force reload a specific plugin |
plugins.executeCommand | Execute a direct command |
Related Pages
- Skills -- How skills integrate with the extension system
- Model Providers -- Provider plugins for custom AI backends
- Workspaces -- Plugin directory structure and discovery paths
- Architecture Overview -- System-level view of the extension layer