Skip to main content

Plugin System

Lua-based plugins extend Mahresources with custom actions, hooks, pages, JSON API endpoints, and menu items. Plugins run in sandboxed VMs, are discovered automatically from a configurable directory, and can be enabled or disabled at runtime.

Configuration

FlagEnv VariableDefaultDescription
-plugin-pathPLUGIN_PATH./pluginsDirectory to scan for plugin subdirectories
-plugins-disabledPLUGINS_DISABLED=1falseDisable the plugin system entirely

Plugin Discovery

At startup, the plugin manager scans the plugin directory for subdirectories containing a plugin.lua file. Discovery is sorted alphabetically for deterministic load order.

plugins/
+-- my-plugin/
| +-- plugin.lua
+-- another-plugin/
+-- plugin.lua

During discovery, a temporary Lua VM executes only the top-level code of plugin.lua (not init()) to read the plugin global table for metadata and settings. The temporary VM is then closed.

Plugin Metadata

Every plugin declares a global plugin table:

plugin = {
name = "image-processor",
version = "1.0.0",
description = "Processes images using external APIs"
}
FieldRequiredDescription
nameYesPlugin identifier (displayed in management UI)
versionNoVersion string
descriptionNoShort description
settingsNoArray of setting definitions

Plugin Lifecycle

  1. Discovery -- Plugin directory is scanned at startup. Metadata and settings are read from each plugin.lua.
  2. State check -- The database is queried for previously enabled plugins. Those plugins are enabled automatically.
  3. Enable -- A full Lua VM is created with safe libraries. plugin.lua is executed, then init() is called (if defined). Hooks, actions, injections, pages, menus, and API endpoints registered during init() become active.
  4. Run -- The plugin responds to hooks, serves pages, and executes actions.
  5. Disable -- All hooks, injections, pages, menus, actions, and API endpoints are removed. In-flight async actions are awaited. The Lua VM is closed.

Plugin Settings

Settings are defined in the plugin.settings table and appear in the management UI when the plugin is selected.

plugin = {
name = "my-plugin",
settings = {
{ name = "api_key", type = "password", label = "API Key", required = true },
{ name = "model", type = "select", label = "Model", options = {"fast", "quality"}, default = "fast" },
{ name = "max_size", type = "number", label = "Max Size", default = 1024 },
{ name = "enabled", type = "boolean", label = "Feature Enabled", default = true },
{ name = "prefix", type = "string", label = "Output Prefix", default = "processed_" }
}
}

Setting Types

TypeValidationUI Element
stringRequired check onlyText input
passwordRequired check onlyPassword input
booleanMust be booleanCheckbox
numberMust be numericNumber input
selectMust match one of optionsDropdown

Required settings must be configured before the plugin can be enabled.

Reading Settings at Runtime

local api_key = mah.get_setting("api_key")
local max_size = mah.get_setting("max_size")

Returns the setting value with the correct Lua type (string, number, boolean), or nil if not set.

State Persistence

Plugin enabled/disabled state and settings are stored in the database (PluginState table). This means:

  • Plugins that were enabled before a restart are re-enabled automatically
  • Settings survive server restarts
  • The plugin directory itself only needs the Lua source files

Management UI

Plugin management page

Navigate to the plugin management page to see all discovered plugins with their name, version, description, and current state (enabled/disabled). From this page:

  • Enable or disable individual plugins
  • Configure plugin settings
  • View registered actions, hooks, and pages

Management API

MethodPathDescription
GET/v1/plugins/manageList all discovered plugins with state
POST/v1/plugin/enableEnable a plugin (form: name)
POST/v1/plugin/disableDisable a plugin (form: name)
POST/v1/plugin/settingsSave settings (query: name, JSON body: key-value pairs)
POST/v1/plugin/purge-dataPurge all KV store data for a disabled plugin (form: name)

Enable a Plugin

curl -X POST http://localhost:8181/v1/plugin/enable \
-d "name=image-processor"

Required settings must be saved before enabling. If required settings are missing, the enable request fails with a validation error.

Save Settings

curl -X POST "http://localhost:8181/v1/plugin/settings?name=image-processor" \
-H "Content-Type: application/json" \
-d '{
"api_key": "sk-abc123",
"model": "quality"
}'

Only keys declared in plugin.settings are persisted; unknown keys are ignored.

Key-Value Storage

Plugins have access to a persistent key-value store via the mah.kv module. Each plugin's data is scoped by plugin name -- plugins cannot read or write another plugin's keys.

mah.kv.set("last_run", "completed")
local last = mah.kv.get("last_run")
mah.kv.delete("last_run")
local keys = mah.kv.list("prefix_")

Values are JSON-serialized before storage and deserialized on read.

Purging Plugin Data

To purge all KV data for a plugin, disable the plugin first, then call the purge endpoint:

curl -X POST http://localhost:8181/v1/plugin/purge-data \
-d "name=image-processor"

The plugin must be disabled before purging. The management UI also has a Purge Data button on the plugin detail view for disabled plugins.

Lua VM Sandboxing

Each enabled plugin runs in an isolated Lua VM with restricted libraries.

Allowed: base, table, string, math, coroutine

Blocked: os, io, debug, package

Removed base functions: dofile, loadfile, load

Each VM has a mutex ensuring single-threaded access. All calls into the VM (hooks, actions, page handlers) acquire this lock.