Plugin System¶
NornicDB features a unified, auto-detecting plugin system that supports two plugin types: Function Plugins (extend Cypher) and Heimdall Plugins (extend the SLM subsystem). Plugins are dynamically loaded at runtime from .so files with zero configuration required.
Quick Start¶
Using Plugins¶
# Run with both plugin types
NORNICDB_PLUGINS_DIR=apoc/built-plugins \
NORNICDB_HEIMDALL_ENABLED=true \
NORNICDB_HEIMDALL_PLUGINS_DIR=plugins/heimdall/built-plugins \
./bin/nornicdb serve
Output:
╔══════════════════════════════════════════════════════════════╗
║ Loading Plugins ║
╠══════════════════════════════════════════════════════════════╣
║ ✓ [FUNC] apoc v1.0.0 983 functions ║
║ ✓ [HEIM] watcher v1.0.0 11 actions ║
╠══════════════════════════════════════════════════════════════╣
║ Loaded: 2 plugins (1 function, 1 heimdall) ║
║ 983 Cypher functions available ║
║ 11 Heimdall actions available ║
╚══════════════════════════════════════════════════════════════╝
Building Plugins¶
# Build all plugins
make plugins
# Build individual plugins
make plugin-apoc # Function plugin (APOC)
make plugin-heimdall-watcher # Heimdall plugin
# Clean built plugins
make plugins-clean
Plugin Types¶
| Type | Purpose | Example | Configuration |
|---|---|---|---|
| Function | Extend Cypher with custom functions | APOC (983 functions) | NORNICDB_PLUGINS_DIR |
| Heimdall | Extend SLM with subsystem management | Watcher (11 actions) | NORNICDB_HEIMDALL_PLUGINS_DIR |
Function Plugins¶
Provide custom Cypher functions callable from queries:
// Collection functions
RETURN apoc.coll.sum([1, 2, 3, 4, 5]) AS total
// Text processing
RETURN apoc.text.join(['Hello', 'World'], ' ') AS greeting
// Custom plugin functions
RETURN myplugin.analyze(node) AS result
Heimdall Plugins¶
Provide SLM-invokable actions for subsystem management:
User: "check system metrics"
SLM → Invokes: heimdall_watcher_metrics
Result: CPU 45%, Memory 2.3GB, Queries 1.2K/s
Configuration¶
Environment Variables¶
# Function plugin directory (APOC-style)
NORNICDB_PLUGINS_DIR=/opt/nornicdb/plugins
# Heimdall plugin directory (subsystem management)
NORNICDB_HEIMDALL_PLUGINS_DIR=/opt/nornicdb/heimdall-plugins
# Enable Heimdall (required for Heimdall plugins; env overrides config file)
NORNICDB_HEIMDALL_ENABLED=true
NORNICDB_MODELS_DIR=/opt/nornicdb/models
Important: Heimdall plugins require an initialized Heimdall subsystem context (Bifrost, DB reader, invoker). If Heimdall is disabled, Heimdall plugins are skipped and not started — this prevents background goroutines from running when heimdall.enabled: false.
Docker Example¶
version: '3.8'
services:
nornicdb:
image: nornicdb/nornicdb:latest
environment:
- NORNICDB_PLUGINS_DIR=/plugins/functions
- NORNICDB_HEIMDALL_ENABLED=true
- NORNICDB_HEIMDALL_PLUGINS_DIR=/plugins/heimdall
- NORNICDB_MODELS_DIR=/models
volumes:
- ./custom-plugins:/plugins/functions
- ./heimdall-plugins:/plugins/heimdall
- ./models:/models
- ./data:/var/lib/nornicdb
How It Works¶
Auto-Detection¶
The plugin loader automatically detects plugin type by calling the Type() method:
plugin.Type() → "function" // Loads as function plugin
plugin.Type() → "heimdall" // Loads as Heimdall plugin
plugin.Type() → "apoc" // Alias for function plugin
plugin.Type() → "" // Defaults to function plugin
No manual configuration or type declaration needed - plugins self-identify.
Loading Flow¶
1. Scan directory for *.so files
↓
2. For each plugin:
- plugin.Open("plugin.so")
- Lookup "Plugin" symbol
- Call Plugin.Type()
↓
3. Auto-route based on type:
┌──────────────────┬─────────────────┐
│ type="function" │ type="heimdall" │
├──────────────────┼─────────────────┤
│ Extract functions│ Register with │
│ Register with │ SubsystemMgr │
│ Cypher executor │ Start plugin │
└──────────────────┴─────────────────┘
Creating Plugins¶
Function Plugin¶
Provides custom Cypher functions.
Minimal Example:
// my-plugin/plugin.go
package main
import "github.com/orneryd/nornicdb/pkg/cypher"
type MyPlugin struct{}
func (p *MyPlugin) Name() string { return "myplugin" }
func (p *MyPlugin) Version() string { return "1.0.0" }
func (p *MyPlugin) Type() string { return "function" }
func (p *MyPlugin) Functions() map[string]cypher.PluginFunction {
return map[string]cypher.PluginFunction{
"myplugin.greet": {
Handler: func(args ...interface{}) (interface{}, error) {
if len(args) == 0 {
return "Hello!", nil
}
return "Hello, " + args[0].(string) + "!", nil
},
Description: "Returns a greeting",
},
}
}
// Export as Plugin
var Plugin = &MyPlugin{}
Build:
Use:
Heimdall Plugin¶
Provides SLM-invokable subsystem management actions.
Minimal Example:
// my-subsystem/plugin.go
package main
import "github.com/orneryd/nornicdb/pkg/heimdall"
type MySubsystem struct {
// plugin state
}
func (p *MySubsystem) Name() string { return "mysubsystem" }
func (p *MySubsystem) Version() string { return "1.0.0" }
func (p *MySubsystem) Type() string { return "heimdall" }
func (p *MySubsystem) Description() string { return "Custom subsystem" }
// Lifecycle
func (p *MySubsystem) Initialize(ctx heimdall.SubsystemContext) error { return nil }
func (p *MySubsystem) Start() error { return nil }
func (p *MySubsystem) Stop() error { return nil }
func (p *MySubsystem) Shutdown() error { return nil }
// State
func (p *MySubsystem) Status() heimdall.SubsystemStatus {
return heimdall.StatusRunning
}
func (p *MySubsystem) Health() heimdall.SubsystemHealth {
return heimdall.SubsystemHealth{Status: heimdall.StatusRunning, Healthy: true}
}
func (p *MySubsystem) Metrics() map[string]interface{} { return nil }
func (p *MySubsystem) Config() map[string]interface{} { return nil }
func (p *MySubsystem) Configure(settings map[string]interface{}) error { return nil }
func (p *MySubsystem) ConfigSchema() map[string]interface{} { return nil }
func (p *MySubsystem) Summary() string { return "Running" }
func (p *MySubsystem) RecentEvents(limit int) []heimdall.SubsystemEvent { return nil }
// Actions (invoked by SLM)
func (p *MySubsystem) Actions() map[string]heimdall.ActionFunc {
return map[string]heimdall.ActionFunc{
"check": {
Description: "Check subsystem status",
Category: "monitoring",
Handler: p.check,
},
}
}
func (p *MySubsystem) check(ctx heimdall.ActionContext) (*heimdall.ActionResult, error) {
return &heimdall.ActionResult{
Success: true,
Message: "All systems operational",
}, nil
}
// Export as HeimdallPlugin
var Plugin heimdall.HeimdallPlugin = &MySubsystem{}
Build:
Use:
Built-In Plugins¶
APOC Plugin¶
Type: Function
Functions: 983
Location: apoc/built-plugins/apoc.so
Provides Neo4j-compatible APOC functions: - Collection operations (apoc.coll.*) - Text processing (apoc.text.*) - Math functions (apoc.math.*) - Date/time (apoc.date.*, apoc.temporal.*) - Graph algorithms (apoc.algo.*) - Complete list →
Watcher Plugin¶
Type: Heimdall
Actions: 11
Location: plugins/heimdall/built-plugins/watcher.so
Provides SLM monitoring and management: - System status (heimdall_watcher_status) - Health checks (heimdall_watcher_health) - Metrics collection (heimdall_watcher_metrics) - Configuration management - Event tracking
Platform Support¶
| Platform | Function Plugins | Heimdall Plugins | Notes |
|---|---|---|---|
| Linux (amd64) | ✅ | ✅ | Full support |
| Linux (arm64) | ✅ | ✅ | Full support |
| macOS (arm64) | ✅ | ✅ | Apple Silicon |
| macOS (amd64) | ✅ | ✅ | Intel Macs |
| Windows | ❌ | ❌ | Go plugin limitation - use static linking |
Windows Users: Compile plugins directly into the binary instead of using .so files.
Best Practices¶
1. Plugin Interface¶
Always implement the Type() method explicitly:
func (p *MyPlugin) Type() string { return "function" }
// or
func (p *MyPlugin) Type() string { return "heimdall" }
2. Error Handling¶
Return descriptive errors:
func myHandler(args ...interface{}) (interface{}, error) {
if len(args) == 0 {
return nil, fmt.Errorf("myplugin.func: missing required argument")
}
// ...
}
3. Naming Conventions¶
Function plugins: Use namespace.function format:
Heimdall plugins: Actions are auto-prefixed:
4. Documentation¶
Document each function/action:
return map[string]cypher.PluginFunction{
"myplugin.analyze": {
Handler: analyzeFunc,
Description: "Analyzes data and returns insights",
Category: "analysis",
},
}
5. Testing¶
Test plugins both standalone and via the loader:
func TestMyPlugin(t *testing.T) {
// Test plugin directly
p := &MyPlugin{}
funcs := p.Functions()
result, err := funcs["myplugin.greet"].Handler("World")
assert.NoError(t, err)
assert.Equal(t, "Hello, World!", result)
}
Troubleshooting¶
Plugin Not Loading¶
Symptom: Plugin file exists but doesn't appear in loading output
Causes: 1. Missing Type() method 2. Incorrect export name (must be Plugin) 3. Plugin not compiled with -buildmode=plugin 4. Wrong directory (check NORNICDB_PLUGINS_DIR or NORNICDB_HEIMDALL_PLUGINS_DIR)
Solution:
# Verify plugin exports "Plugin" symbol
nm -gU myplugin.so | grep Plugin
# Rebuild with correct flags
go build -buildmode=plugin -o myplugin.so .
Type Detection Fails¶
Symptom: Plugin loads as wrong type
Solution: Check Type() method returns correct string: - "function", "apoc", or "" → Function plugin - "heimdall" → Heimdall plugin
Functions Not Callable¶
Symptom: ERROR: Unknown function: myplugin.func
Causes: 1. Function plugin not loaded 2. Function name mismatch 3. Plugin directory not set
Solution:
# Check plugin loaded
# Look for: ✓ [FUNC] myplugin in startup output
# Verify environment variable
echo $NORNICDB_PLUGINS_DIR
# List available functions
MATCH (n) RETURN keys(n) LIMIT 1
// Check error message for available functions
Heimdall Actions Not Available¶
Symptom: SLM can't invoke action
Causes: 1. Heimdall not enabled (NORNICDB_HEIMDALL_ENABLED=false) 2. Heimdall plugin not loaded 3. Model not initialized
Solution:
# Enable Heimdall
NORNICDB_HEIMDALL_ENABLED=true \
NORNICDB_HEIMDALL_PLUGINS_DIR=plugins/heimdall/built-plugins \
NORNICDB_MODELS_DIR=models \
./bin/nornicdb serve
# Check Heimdall status
curl http://localhost:7474/api/bifrost/status
Security Considerations¶
Plugin Permissions¶
Plugins run with full NornicDB permissions: - Database read/write access - Filesystem access - Network access - System calls
Best Practices: 1. Only load plugins from trusted sources 2. Review plugin code before deployment 3. Use file permissions to restrict plugin directory:
Sandboxing (Future)¶
Plugin sandboxing is on the roadmap: - Resource limits (CPU, memory) - Permission restrictions - Network isolation - Audit logging
Performance¶
Function Plugin Overhead¶
Function plugins have minimal overhead: - Direct function pointer calls - No serialization - No IPC
Benchmark: ~50ns per plugin function call (vs ~30ns for built-in functions)
Heimdall Plugin Overhead¶
Heimdall plugins have low overhead: - Direct method calls - Asynchronous execution - Event-driven architecture
Typical latency: <10ms for most actions
Examples¶
Collection Plugin¶
type CollectionPlugin struct{}
func (p *CollectionPlugin) Type() string { return "function" }
func (p *CollectionPlugin) Name() string { return "collections" }
func (p *CollectionPlugin) Functions() map[string]cypher.PluginFunction {
return map[string]cypher.PluginFunction{
"collections.unique": {
Handler: func(args ...interface{}) (interface{}, error) {
list := args[0].([]interface{})
seen := make(map[interface{}]bool)
result := []interface{}{}
for _, item := range list {
if !seen[item] {
seen[item] = true
result = append(result, item)
}
}
return result, nil
},
},
}
}
Monitoring Plugin¶
type MonitoringPlugin struct {
metrics map[string]float64
}
func (p *MonitoringPlugin) Type() string { return "heimdall" }
func (p *MonitoringPlugin) Name() string { return "monitor" }
func (p *MonitoringPlugin) Actions() map[string]heimdall.ActionFunc {
return map[string]heimdall.ActionFunc{
"cpu": {
Handler: p.getCPU,
Description: "Get current CPU usage",
},
}
}
func (p *MonitoringPlugin) getCPU(ctx heimdall.ActionContext) (*heimdall.ActionResult, error) {
// Collect CPU metrics
usage := p.metrics["cpu"]
return &heimdall.ActionResult{
Success: true,
Message: fmt.Sprintf("CPU: %.1f%%", usage),
Data: map[string]interface{}{"usage": usage},
}, nil
}
Architecture¶
┌─────────────────────────────────────────────────────────────┐
│ NornicDB Core │
├─────────────────────────────────────────────────────────────┤
│ Unified Plugin Loader (pkg/nornicdb/plugins.go) │
│ ├─ Auto-detect type via Type() method │
│ ├─ Load function plugins → Cypher executor │
│ └─ Load Heimdall plugins → SubsystemManager │
├─────────────────────────────────────────────────────────────┤
│ Function Plugins │ Heimdall Plugins │
│ ├─ APOC (983 funcs) │ ├─ Watcher (11 actions) │
│ └─ Custom plugins │ └─ Custom subsystems │
└─────────────────────────────────────────────────────────────┘
Next Steps¶
- APOC Functions - Complete function reference
- Heimdall - SLM subsystem management
- Development Guide - Plugin development details
- API Reference - Function documentation
Create plugins → Development Guide