Plugin Lifecycle
Written by ddxfish on .
- Docs
- Plugin Author Guide
- Plugin Lifecycle
- All docs ↓
You are reading the dev branch of the docs, which may describe unreleased changes. Read the main docs.
Startup
plugin_loader.scan()readscore/plugins/(system band) thenuser/plugins/(user band)- Each
plugin.jsonis validated and signature-checked - Enabled plugins get hooks, tools, voice commands, routes, and schedules registered
- Scheduler tasks are deferred if the scheduler hasn't initialized yet
Same name in both bands: the user-band copy wins — it's scanned second and replaces the system entry, with a warning in the log ('<name>' (user band) shadows the system copy at … — only the user copy will load). The store installer refuses a name that collides with a shipped plugin, so this only happens when a folder is placed in user/plugins/ by hand (a local fork of a bundled plugin). Delete or rename the user copy to get the shipped one back.
Old root plugins/ folder: the shipped band used to live in plugins/ at the repo root. That folder is no longer scanned — if it still holds folders with a plugin.json, boot logs a warning naming them (plugins/ at the repo root is no longer scanned … your own plugins go in user/plugins/). Move them to user/plugins/. Boot also logs an error if a core/user/ folder exists, which means some code computed the project root one level short (see Finding the project root).
Live Toggle
Settings > Plugins calls PUT /api/webui/plugins/toggle/{name}:
- Enable: Loads immediately — all capabilities register
- Disable: Unloads immediately — device drivers let go (presence), each tool module's optional
shutdown()runs, then hooks, tools, routes, schedules are removed and the plugin's modules leavesys.modules
Unsigned/tampered plugins return 403 and the toggle reverts.
Hot Reload (Dev)
POST /api/plugins/{name}/reload unloads and reloads a single plugin.
Set SAPPHIRE_DEV=1 to enable file watching — plugins auto-reload when .py or .json files change (2s polling).
If reload fails, the plugin stays unloaded. No half-loaded state.
Rescan
POST /api/plugins/rescan discovers new or removed plugin folders without restart. Returns {"added": [...], "removed": [...]}.
Error Isolation
A buggy plugin never crashes the system. If a hook handler throws an exception, it's logged and skipped — the next handler fires normally. Tool execution errors are caught and returned as error messages to the AI.
Chat Lifecycle
Plugin data can also be scoped to a chat instead of the plugin. Rows written
through plugin_loader.get_chat_state(name) live in the chat database, and core
owns their whole life: carried across a rename, encrypted when the chat goes
private, deleted with the chat (public and private alike, inside the delete
transaction). Plugins no longer register chat_renamed / chat_deleted to
shepherd that data — those hooks are for chat-keyed things kept elsewhere.
See Chat-Scoped State.
API Reference
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/webui/plugins |
List all plugins with metadata |
| PUT | /api/webui/plugins/toggle/{name} |
Enable/disable (live) |
| POST | /api/plugins/rescan |
Discover new/removed plugins |
| POST | /api/plugins/{name}/reload |
Hot-reload (dev) |
| GET | /api/plugins/{name}/check-deps |
Which pip_dependencies are missing, the environment, and the install command |
| POST | /api/plugins/{name}/install-deps |
Install the plugin's packages — pip_dependencies and its optional extra — in the background (core/extras.py; conda/venv only; poll GET /api/system/extras/plugin:{name}), then auto-reload the plugin |
| GET | /api/webui/plugins/{name}/settings |
Read plugin settings |
| PUT | /api/webui/plugins/{name}/settings |
Save plugin settings |
| DELETE | /api/webui/plugins/{name}/settings |
Reset plugin settings |
| GET | /plugin-web/{name}/{path} |
Serve plugin web assets |
| * | /api/plugin/{name}/{path} |
Plugin custom routes (auth enforced) |
Plugin List Response
{
"plugins": [
{
"name": "ssh",
"enabled": true,
"locked": false,
"title": "SSH",
"settingsUI": "plugin",
"verified": true,
"verify_msg": "verified",
"version": "1.0.0",
"author": "sapphire",
"url": "https://sapphireblue.dev"
}
],
"locked": ["setup-wizard", "backup", "continuity"]
}
Reference for AI
- Boot:
plugin_loader.scan()readscore/plugins/(system band) thenuser/plugins/(user band) — a same-named user-band copy SHADOWS the system one (warned in the log; the installer refuses such a name, so it means a hand-placed fork); a root-levelplugins/is NOT scanned (warning if it holds plugin folders);core/user/existing logs an error (root computed one level short); each manifest is validated and signature-checked; enabled plugins register hooks, tools, voice commands, routes, schedules; scheduler tasks defer until the scheduler initializes. - Live toggle:
PUT /api/webui/plugins/toggle/{name}— enable loads immediately, disable unloads immediately; unsigned/tampered plugins return 403 and the toggle reverts. Unload order: device presence release → each tool module's optionalshutdown()(also at Sapphire's own shutdown) → hooks/tools/routes unregistered → the plugin's modules purged fromsys.modules. - Hot reload:
POST /api/plugins/{name}/reload; a failed reload leaves the plugin unloaded — no half-loaded state.SAPPHIRE_DEV=1enables the file watcher (auto-reload on.py/.jsonchanges, polled). - Rescan:
POST /api/plugins/rescan→{"added": [...], "removed": [...]}without restart. - The
plugins_readyhook fires after the boot scan AND after reload, rescan, and toggle-on — handlers must be idempotent (see hooks.md). - Error isolation: a hook handler exception is logged and skipped (next handler fires); tool execution errors are returned to the AI as error messages.
- Chat-scoped rows (
plugin_loader.get_chat_state(name)) are core-managed: carried through rename, encrypted on vault, deleted inside the chat-delete transaction BEFOREchat_deletedfires (no archive-on-delete possible from that hook). They survive a chat clear. - Purge data (plugin card ⋮ menu, any band; also the first step of uninstall) deletes everything a plugin stores about the user in one call —
user/plugin_state/<name>.json, its<name>-*/<name>_*siblings, and every chat-scoped row it holds — while the plugin stays installed and configured. Rows in sealed (locked-vault) chats are skipped and counted, never deleted blind. Hand-deleting the state file never reached the chat rows; this lever does. - Key endpoints:
GET /api/webui/plugins(list),GET/PUT/DELETE /api/webui/plugins/{name}/settings,GET /plugin-web/{name}/{path}(web assets),/api/plugin/{name}/{path}(custom routes, auth enforced).
Edit this page on GitHub · docs/plugin-author/lifecycle.md @ a5fa790 · synced
