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 |
|---|---|---|
| agent-runtime | Service | Generic ephemeral CLI subprocess driver for agent-style tools (claude-cli, codex, gemini, etc.). Spawns a CLI per turn, drains stdout/stderr with line callbacks, supports periodic tick callbacks for live UI updates, kills on timeout. Lifted from dogfood-agent's run loop so any app needing 'spawn a coding-agent CLI for one turn' can share the same plumbing. No persistent daemon — each call is a one-shot subprocess |
| applio | Service | AI voice conversion via Applio (port 6969) |
| blender | Service | 3D modeling, rendering, and animation via Blender |
| cadquery | Service | Articulated 3D object compile + URDF export via CadQuery, run inside a user-home Python 3.12 venv |
| claude-design | Service | Connector for claude.ai/design — a named service (list/create/pull/push design projects, the EOS_UI-sweep round-trip) plus a cloud provider on the artifact capability. Transport is the Claude Code CLI bridge (DesignSync tool via agent-runtime); dark on headless deployments where the CLI isn't authed |
| comfyui | Enhancer (draw) | GPU image generation via ComfyUI |
| command-launcher | Service | Spawns a borderless Chrome/Edge app-mode window pointing at the command palette when the global hotkey fires |
| dogfood-demo | Service | Sidecar EmptyOS daemon on :9001 with a curated sample vault. Lets ppt embed slides, screenshots, and any other share-shaped surface render against demo content instead of the user's personal vault — without flipping the personal daemon's demo mode |
| edge-tts | Service | Free in-process TTS via Microsoft Edge voices (edge-tts pip package) |
| email-smtp | Service | Injects an SMTP provider into the send capability — outbound email via your own mail server |
| footage | Service | Injects royalty-free stock-video providers into the footage capability — keyword → downloaded clip |
| global-hotkey | Service | Listens for global OS keyboard shortcuts and emits them to the EventBus |
| gmail | Service | Read-only Gmail access (scope-pinned to gmail.readonly) for in-system email categorization. No send/modify/delete |
| health | Service | System health checks — capabilities, connectors, apps |
| markitdown | Service | Local document → markdown conversion (PPTX/XLSX/DOCX/EPub/HTML/CSV/JSON/XML/ZIP) via Microsoft MarkItDown, run in an isolated user-home venv. Injects a provider into the read capability; PDFs are excluded (KB keeps the page-marked extractor). 100% local — no Azure, no cloud client |
| notifications | Service | Proactive notifications — vault file by default, Telegram when configured |
| obsidian | Service | Declares Obsidian as EmptyOS's vault viewer — owns URI templates (obsidian://open, obsidian://new) used by every note link. Swap this plugin (or override uri_templates in config) to route note links through a different viewer |
| ocr | Service | Local GPU OCR for image-only / scanned PDFs (and images) that have no text layer — the case markitdown and the KB page-marked extractor both punt on. Wraps datalab marker-pdf in an isolated user-home venv, injects a provider into the read capability that fires ONLY when in-process text extraction finds no text layer (born-digital PDFs are never shadowed). Returns page-marked markdown. 100% local — no cloud, no consent gate |
| ollama | Enhancer (think) | Local LLM inference via Ollama |
| openai-image | Service | Cloud image-gen fallback via OpenAI gpt-image-1. Cost-bearing — opt-in per call by domain routing, not auto-selected for free local generations |
| playwright | Service | Headless browser automation — provides the browse capability via Chromium |
| pronounce | Service | Phoneme-level pronunciation scoring via a local wav2vec2 service |
| sandbox-pool | Service | Supervises a small pool of throwaway EmptyOS daemons on :9002+ that Claude (and other operators) can lease, restart, and release through a daemon-owned API. The plugin spawns each python.exe and tracks its PID itself, so the auto-classifier doesn't need to allow raw Bash taskkill — kills go through POST /sandbox/api/lease/ |
| system-tray | Service | Adds EmptyOS to the native Windows/macOS system tray |
| tailscale | Service | Tailscale-aware integration — reads tailscale status --json to expose this node's tailnet identity, online peers, and Funnel state. Read-only: does not run tailscale up/down. Apps use self.require("tailscale") for peer-aware audit logs, device pickers, and is-this-request-from-my-tailnet checks |
| tailscale-serve | Service | Write-side companion to the read-only tailscale plugin. Runs tailscale serve to give the daemon zero-config tailnet HTTPS — reach EmptyOS securely from your phone/laptop without changing the daemon bind. Opt-in: inert until [plugins.tailscale-serve] enabled=true. Only ever proxies to 127.0.0.1, never exposes EmptyOS to the public internet (no Funnel). State is read from tailscale serve status every call, never cached |
| telegram | Service | Push notifications + two-way bot via Telegram |
| translate | Service | Local deterministic translation via Meta NLLB-200 (CTranslate2). English fans out to ~200 languages with stable output. Dark until a converted model is installed; the LLM translate provider covers everything until then |
| voice-api | Enhancer (speak+listen) | Text-to-speech and speech-to-text via local Voice API |
| webcam | Service | Local camera capture via OpenCV — provides the see capability |
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.