Plugin Development
ZERG plugins extend the Sol server with custom behavior using a sandboxed Lua runtime. This guide covers the plugin API, bridge modules, lifecycle hooks, and practical examples.
Architecture
Plugins run inside a Luerl Lua VM sandbox within the Sol Erlang process. The plugin manager (sol_plugin_manager) loads plugins at startup, manages their lifecycle, and dispatches events to them.
Sol Server
|
+-- sol_plugin_manager (gen_server)
|
+-- Plugin A (Luerl VM)
| +-- sol.log
| +-- sol.http
| +-- sol.config
| +-- sol.events
| +-- sol.channel
| +-- sol.json
| +-- sol.time
|
+-- Plugin B (Luerl VM)
+-- ...Plugin Sandbox
The Luerl VM is stripped of dangerous standard libraries:
| Library | Status |
|---|---|
os | Removed |
io | Removed |
debug | Removed |
package | Removed |
string | Available |
table | Available |
math | Available |
coroutine | Available |
Additional protections:
- Eval timeout: 15 seconds per lifecycle call
- Periodic garbage collection between calls
- No filesystem access
- No direct network access (must use
sol.httpbridge) - No process spawning
Bridge Modules
Plugins access Sol functionality through 15 bridge modules exposed in the sol table:
sol.log
Logging bridge. Outputs to the Erlang logger:
sol.log.info("processing started")
sol.log.warning("high memory usage detected")
sol.log.error("failed to process item")sol.http
HTTP client bridge. Makes outbound requests without exposing the network layer:
local response = sol.http.get("https://api.example.com/status")
local data = sol.http.post("https://api.example.com/webhook", payload)Requests are pooled and managed by sol_plugin_http_pool_owner.
sol.config
Configuration bridge. Read plugin-specific configuration:
local api_key = sol.config.get("api_key")
local timeout = sol.config.get("timeout") or 30Configuration is loaded from the plugin's entry in the plugins config file.
sol.events
Event subscription bridge. Listen to Sol system events:
sol.events.subscribe("task_completed", function(event)
sol.log.info("task " .. event.task_id .. " completed")
end)sol.channel
Channel messaging bridge. Send messages to conversation channels:
sol.channel.send(channel_id, "Processing complete")Used by format_and_send to deliver formatted event notifications.
sol.json
JSON bridge. Encode and decode JSON data:
local data = sol.json.decode('{"key": "value"}')
local encoded = sol.json.encode({result = "ok"})sol.time
Time bridge. Access current time without the os library:
local now = sol.time.now()
local formatted = sol.time.format(now)sol.tools
Tool registration bridge. Register custom tools that workers can invoke:
sol.tools.register("my_tool", {
description = "Does something useful",
handler = function(args)
return {result = "ok"}
end
})sol.commands
Command registration bridge. Register slash commands for the TUI:
sol.commands.register("/mycommand", function(args)
sol.log.info("command executed: " .. args)
end)sol.sessions
Session access bridge. Read and search session data:
local session = sol.sessions.get(session_id)
local results = sol.sessions.search("query")sol.providers
Provider configuration bridge. List and query available AI providers:
local providers = sol.providers.list()
local model_info = sol.providers.get_model("glm-5.1")sol.workflows
Workflow control bridge. Start, monitor, and cancel workflows:
local wf_id = sol.workflows.start("my_workflow", {input = "data"})
sol.workflows.cancel(wf_id)sol.agents
Agent management bridge. Spawn and manage agent instances:
local agent_id = sol.agents.spawn({model = "glm-5.1", mode = "build"})
sol.agents.send_message(agent_id, "Hello")sol.memory
Memory storage bridge. Persistent KV store with TTL:
sol.memory.store("key", "value", {ttl = 3600})
local val = sol.memory.get("key")
sol.memory.delete("key")sol.scheduler
Job scheduling bridge. Create and manage scheduled jobs:
local job_id = sol.scheduler.create("cron", "*/5 * * * *", {task = "check_health"})
sol.scheduler.cancel(job_id)Plugin Lifecycle
Plugins implement optional lifecycle functions that the plugin manager calls at specific points:
init
Called once when the plugin is loaded. Use for setup and configuration:
function init()
sol.log.info("my plugin initialized")
return true
endstart
Called after all plugins are loaded. Use for starting background operations:
function start()
sol.log.info("my plugin started")
sol.events.subscribe("task_completed", on_task_complete)
return true
endstop
Called when the plugin is unloaded or the server shuts down. Clean up resources:
function stop()
sol.log.info("my plugin stopping")
return true
endpoll
Called periodically for polling plugins. Register with start_polling:
function poll(interval_ms)
local status = sol.http.get("https://monitor.example.com/health")
if status ~= "ok" then
sol.channel.send("alerts", "Service degraded: " .. status)
end
return true
endformat_event
Called when the plugin is asked to format an event for a channel:
function format_event(event_type, event_data)
if event_type == "task_completed" then
return "Task " .. event_data.task_id .. " finished successfully"
end
return nil
endPlugin Configuration
Plugins are configured in the Sol configuration. Each plugin has a name, type, version, and config map:
{plugins, [
#{name => <<"monitor">>,
type => <<"poller">>,
version => <<"1.0.0">>,
config => #{endpoint => <<"https://monitor.example.com">>,
interval => 60000}}
]}Loading and Unloading
Via API
curl -X POST http://127.0.0.1:21434/api/v1/plugins/load \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "my-plugin", "config": {"key": "value"}}'List Loaded Plugins
curl -s http://127.0.0.1:21434/api/v1/plugins | jq .Unload a Plugin
curl -X POST http://127.0.0.1:21434/api/v1/plugins/my-plugin/unload \
-H "Authorization: Bearer $TOKEN"Reload All Plugins
curl -X POST http://127.0.0.1:21434/api/v1/plugins/reload \
-H "Authorization: Bearer $TOKEN"Complete Example
A notification plugin that sends alerts on task failures:
function init()
webhook_url = sol.config.get("webhook_url") or ""
sol.log.info("alert plugin initialized")
return true
end
function start()
sol.events.subscribe("task_failed", on_task_failed)
sol.log.info("alert plugin watching for failures")
return true
end
function on_task_failed(event)
local payload = sol.json.encode({
task_id = event.task_id,
error = event.error,
timestamp = sol.time.now()
})
sol.http.post(webhook_url, payload)
sol.log.warning("alert sent for task " .. event.task_id)
end
function stop()
sol.log.info("alert plugin stopped")
return true
endSave as priv/plugins/alert.lua and configure:
{plugins, [
#{name => <<"alert">>,
type => <<"responder">>,
version => <<"1.0.0">>,
config => #{webhook_url => <<"https://hooks.example.com/zerg">>}}
]}Plugin Directories
Plugins are loaded from the directory configured by SOL_PLUGIN_DIR (default: priv/plugins). Each plugin is a single .lua file. The plugin manager scans this directory at startup.
Error Handling
Plugin errors do not crash the Sol server. The plugin manager catches all exceptions and logs them. If a plugin's lifecycle function fails, the plugin is marked as errored but other plugins continue operating.
The eval timeout (15 seconds) prevents runaway plugins from blocking the system. Periodic GC runs between lifecycle calls to reclaim memory from the Luerl VM.