Skip to main content

Plugin Lifecycle

You are reading the dev branch of the docs, which may describe unreleased changes. Read the main docs.

Startup

  1. plugin_loader.scan() reads core/plugins/ (system band) then user/plugins/ (user band)
  2. Each plugin.json is validated and signature-checked
  3. Enabled plugins get hooks, tools, voice commands, routes, and schedules registered
  4. 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 leave sys.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() reads core/plugins/ (system band) then user/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-level plugins/ 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 optional shutdown() (also at Sapphire's own shutdown) → hooks/tools/routes unregistered → the plugin's modules purged from sys.modules.
  • Hot reload: POST /api/plugins/{name}/reload; a failed reload leaves the plugin unloaded — no half-loaded state. SAPPHIRE_DEV=1 enables the file watcher (auto-reload on .py/.json changes, polled).
  • Rescan: POST /api/plugins/rescan → {"added": [...], "removed": [...]} without restart.
  • The plugins_ready hook 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 BEFORE chat_deleted fires (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