Aleph
Concepts

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 communication

The 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 instructions

Manifest 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

KindDescriptionEntry PointUse Case
nodejsNode.js process communicating via JSON-RPC over stdioJavaScript/TypeScript fileFull-featured plugins with network access
wasmWebAssembly module loaded into Extism runtime.wasm binaryFast, sandboxed computation
staticNo executable code, only prompts and configurationNoneCoding standards, style guides, knowledge bases

Manifest Priority

When multiple manifest formats exist, Aleph uses this resolution order:

  1. aleph_plugin.toml (V2 TOML) -- preferred
  2. aleph_plugin.json (V2 JSON)
  3. package.json with alephPlugin section
  4. 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

ResourceConstraint
FilesystemNo access (fully sandboxed)
NetworkNo access (fully sandboxed)
MemoryConfigurable limit (default: 256 MB)
CPU timeConfigurable timeout (default: 30 seconds)

These limits are configured in the extension settings:

[extensions.runtimes.wasm]
enabled = true
memory_limit = "256MB"
timeout_ms = 30000

Node.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:

  1. ~/.aleph/plugins/ -- User plugins
  2. /usr/local/share/aleph/plugins/ -- System plugins
  3. ./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 Ready

Plugin 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

TypeExecutionBehavior
InterceptorSequentialCan modify context or block execution. Each hook receives the output of the previous one.
ObserverParallelFire-and-forget. Errors logged but do not affect execution. Used for telemetry and logging.
ResolverSequentialFirst-win competition. Execution stops when a hook returns a non-null result.

Available Events

EventDescription
before_tool_callBefore any tool is invoked
after_tool_callAfter tool execution completes
on_messageWhen a user message is received
on_responseBefore response is sent to user
on_errorWhen an error occurs

Hook Priorities

PriorityValueUse Case
system-1000Core system hooks (runs first)
high-100Security checks, validation
normal0Default priority
low100Logging, 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:

ScopeBehavior
systemAlways injected when the plugin is active
toolInjected only when the bound tool is available in the current context
standaloneUser must explicitly invoke (e.g., /my-plugin)
disabledNever 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 = false

Services 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

MethodDescription
plugins.listList all installed plugins with status
plugins.installInstall a plugin from path or URL
plugins.uninstallRemove a plugin
plugins.enableEnable a disabled plugin
plugins.disableDisable a plugin without removing it
plugins.reloadForce reload a specific plugin
plugins.executeCommandExecute a direct command

On this page