Plugins

Plugin Types

Plugins come in two flavors:

Service plugins expose named services that apps access explicitly via self.require("name") or self.service("name").

Enhancer plugins dynamically inject providers into capabilities at startup. If the plugin is absent or offline, the capability falls back to the next provider. No app code changes needed.

This is the Graceful Enhancement pattern: capabilities degrade silently when a plugin goes offline. Your search still works without the search enhancer — it just loses graph-aware features.

Active Plugins

Plugin Type What It Does
health Service Heartbeat monitoring, capability probes, GPU VRAM tracking
ollama Enhancer (think) Local LLM via Ollama — models, generate, embeddings
comfyui Enhancer (draw) GPU image/video/music generation with 8 style presets
voice-api Enhancer (speak+listen) TTS (F5-TTS) and STT (Whisper)
applio Service AI voice conversion
notifications Service Vault + Telegram push notifications
telegram Service Two-way Telegram bot — commands in, notifications out
blender Service 3D cable routing via Blender automation

How Enhancers Work

When the Ollama plugin starts, it registers itself as a provider for the think capability at priority 0 (highest). The capability's provider chain becomes:

think: ollama → openai → claude-cli → human

If Ollama crashes mid-request, the capability automatically falls back to OpenAI. Apps never see the failure — they just get a response.

# This code never changes regardless of which providers are available
result = await self.think("Analyze this code", domain="code")

Plugin Configuration

Plugins are configured in emptyos.toml:

[plugins.ollama]
url = "http://localhost:11434"

[plugins.comfyui]
url = "http://127.0.0.1:8188"
launcher = "D:/ComfyUI/run.bat"

[plugins.telegram]
token = "bot-token-here"
allowed_users = [123456789]

Plugins with a launcher config can auto-start their external service when EmptyOS boots.

Auto-Start Pattern

External services use embedded runtimes with headless launch:

# Plugin auto_start() — no visible windows
subprocess.Popen(
    [python_exe, "-s", main_py, "--flags"],
    cwd=launcher_dir,
    creationflags=subprocess.CREATE_NO_WINDOW,
    stdout=subprocess.DEVNULL,
    stderr=subprocess.DEVNULL,
)

The health plugin polls each service endpoint until ready, with a 60-second timeout.

Writing a Plugin

class MyPlugin(BasePlugin):
    async def on_start(self):
        # Register as a service
        self.register_service("my_service", self)

        # Or inject into a capability (enhancer pattern)
        self.inject_provider("think", MyProvider(), priority=0)

    async def on_stop(self):
        pass

Place in plugins/my-plugin/ with a plugin.py and manifest. The plugin loader discovers it automatically.