# Plugin System > **Area:** plugin-system > **Status:** Active > **Last updated:** 2026-05-25 ## Overview Plugins extend Toju's renderer with bundled or local ES modules that can publish events into a server, register UI contributions (pages, panels, actions, channel sections, embed renderers), store per-user or per-server data, and exchange messages over P2P. They are described by a typed manifest, gated by an explicit capability grant model, and coordinated across three subdomains: the Electron plugin loader (`electron/plugin-library.ts`), the renderer plugin runtime (`toju-app/src/app/domains/plugins/`), and the server's plugin-support surface (`server/src/routes/plugin-support.ts`). This area documents the contract those three sides share. ## Responsibilities - Define the canonical plugin manifest shape (`TojuPluginManifest`) and its semantic versioning, dependency, and capability requirements. - Discover local plugin manifests from disk in Electron and surface them to the renderer. - Load plugin modules — local file://, http(s)://, or bundled — into the renderer and run their lifecycle hooks. - Gate every host API call by an explicit capability grant. - Persist server-scoped plugin requirements and event definitions on the signaling server, broadcasting changes to connected clients. This area does **not** own: - The wire format of `plugin_event` envelopes themselves — those live with [websocket-envelopes](./websocket-envelopes.md); this area defines the **validation rules** applied to them. - IPC plumbing for plugin manifest discovery — that's the [ipc-bridge](./ipc-bridge.md) surface. - Per-plugin business logic — that lives in the plugin's own code. ## Key concepts - **Manifest** — `TojuPluginManifest` (`toju-app/src/app/shared-kernel/plugin-system.contracts.ts`). Required: `id`, `title`, `description`, `version`, `apiVersion`, `kind`, `schemaVersion` (fixed `1`), `compatibility.minimumTojuVersion`. Optional: `entrypoint`, `bundle`, `capabilities[]`, `events[]`, `data[]`, `ui`, `settings`, `relationships`, `authors`, `pluginUser`, `scope`, `homepage`, `changelog`, `license`, `readme`, `load.priority`. - **Capability** — a string ID (e.g. `messages.send`, `events.server.publish`, `ui.pages`, `storage.local`). Plugins declare what they need in `capabilities[]`; the host enforces grants per plugin. - **Plugin event** — a declared `{ eventName, direction, scope, schema?, maxPayloadBytes? }` tuple. `direction` is `clientToServer | serverRelay | p2pHint`; `scope` is `server | channel | user | plugin`. - **Capability grant** — an entry in `metoyou_plugin_capability_grants` (localStorage + desktop file) recording user consent for a `(pluginId, capability)` pair. - **Activation context** — `TojuPluginActivationContext` — what a plugin module receives in its `activate(context)` hook: `pluginId`, `manifest`, `api` (the capability-gated `TojuClientPluginApi`), and a `subscriptions[]` cleanup list. - **Local plugin** — a folder under `${app.getPath('userData')}/plugins//` containing `toju-plugin.json` (preferred) or `plugin.json` and resolved relative assets. --- ## Manifest contract Declared in `toju-app/src/app/shared-kernel/plugin-system.contracts.ts` (the source of truth on the renderer side; the Electron loader treats the manifest as `unknown` until the renderer validates it). Highlights: - **`schemaVersion: 1`** — fixed; bump only with a coordinated migration. - **`apiVersion`** — declares which host API surface the plugin expects. - **`kind`** — distinguishes plugin types (currently a string union; see file for exact members). - **`bundle.url`** + optional `bundle.entrypointUrl` — for remote / store-installed plugins. - **`entrypoint`** — relative path within the local plugin folder; rejected if it escapes `pluginRoot`. - **`relationships.{requires,optional,conflicts,before,after}`** — `pluginId` + optional `versionRange`. Resolved at activation; missing required dependencies block activation. - **`events[]`** — registered with the server via `plugin-support` when the plugin is required by a server. The server uses these definitions to validate inbound `plugin_event` envelopes. - **`data[]`** — `{ key, scope: server|channel|user|plugin, storage: local|serverData, schema? }`. - **`load.priority`** — `bootstrap | high | default | low`; controls ordering when several plugins are activated together. - **`pluginUser`** — synthetic user identity for messages a plugin posts on its own behalf. --- ## Discovery & loading ### Local discovery (Electron main) `electron/plugin-library.ts`: - Scans `${app.getPath('userData')}/plugins/` **one level deep** for plugin folders. - For each folder, reads `toju-plugin.json` (preferred) or `plugin.json` as raw JSON. No schema validation at this layer. - Resolves relative paths (`entrypoint`, `readme`) against the plugin root, rejecting `..`, absolute paths, or anything that escapes the root via `isPathInside()` realpath check. - Returns `LocalPluginDiscoveryResult { plugins, errors, pluginsPath }` where each `plugins[i]` is a `LocalPluginManifestDescriptor` carrying the raw manifest, manifest path, plugin root, plugin-root `file://` URL, optional entrypoint and readme paths, and a `discoveredAt` timestamp. Exposed to the renderer through the [ipc-bridge](./ipc-bridge.md) as `listLocalPluginManifests` and `getLocalPluginsPath`. ### Renderer runtime `toju-app/src/app/domains/plugins/`: - **`PluginHostService`** — orchestrates lifecycle: `discoverLocalPlugins`, `registerLocalManifest`, `activate`, `deactivate`, `reload`, `loadPluginModule`. - **`PluginClientApiService`** — constructs the capability-gated facade `TojuClientPluginApi` per plugin. Subsystems: `channels`, `events`, `messages`, `messageBus`, `p2p`, `profile`, `users`, `roles`, `server`, `attachments`, `media`, `storage`, `serverData`, `clientData`, `ui`, `logger`, `context`. - **`PluginCapabilityService`** — `grant(pluginId, capability)`, `revoke()`, `grantAll(manifest)`, `assert(pluginId, capability)`. Storage in `metoyou_plugin_capability_grants` (localStorage + desktop file). - **`PluginMessageBusService`** — plugin-scoped pub/sub with topic, optional channel/peer targeting, optional message replay. - **`PluginStorageService`** — split storage paths for `local` and `serverData` scopes. - **`PluginUiRegistryService`** — central registry of UI contributions consumed by `plugin-render-host`, `plugin-page-host`, `plugin-action-menu`. - **`PluginRequirementStateService`**, **`PluginDesktopStateService`** — state slices. ### Module loading - Entrypoint URL is `file://` for local plugins, `http(s)://` for remote, or the `bundle.url` for bundled. - Bytes are fetched, wrapped in a `Blob`-backed object URL, then imported via dynamic `import()` so devtools and stack traces resolve. - `GuardedPluginMutationObserver` wraps observer callbacks to catch plugin errors and break infinite redispatch loops. ### Lifecycle states From `plugin-runtime.models.ts`: `discovered → validated → blocked | loading → ready → loaded → failed | unloading → unloaded → disabled`. ### Module contract ```ts export interface TojuClientPluginModule { activate?(context: TojuPluginActivationContext): void | Promise; deactivate?(context: TojuPluginActivationContext): void | Promise; ready?(context: TojuPluginActivationContext): void | Promise; onServerRequirementsChanged?(context, snapshot): void | Promise; onPluginDataChanged?(context, event): void | Promise; } ``` --- ## Server surface (`server/src/routes/plugin-support.ts`) Each endpoint scoped to `:serverId`. Permission `manageServer` enforced for mutations via `server-permissions.service.ts`. - `GET /:serverId/plugins` → `PluginRequirementsSnapshot` — full set of required/optional plugins + event definitions for the server. - `PUT /:serverId/plugins/:pluginId/requirement` — upsert a requirement: `status: required | optional | recommended | blocked | incompatible`, `installUrl?`, `sourceUrl?`, `manifest?`, `versionRange?`. - `DELETE /:serverId/plugins/:pluginId/requirement` — remove. - `PUT /:serverId/plugins/:pluginId/events/:eventName` — register or update an event definition (`direction`, `scope`, `schema?`, `maxPayloadBytes?`). - `DELETE /:serverId/plugins/:pluginId/events/:eventName` — delete. - `GET|PUT|DELETE /:serverId/plugins/:pluginId/data/:key` — **disabled** on the signaling server (returns HTTP 410). Plugin data on the server is intentionally out of scope. Identifier patterns: - `pluginId`: `/^[a-z0-9][a-z0-9.-]{1,126}[a-z0-9]$/` - `eventName`: `/^[a-z][a-z0-9.:-]{1,126}[a-z0-9]$/` Changes broadcast to connected clients via the [websocket-envelopes](./websocket-envelopes.md) — the matching `PluginRequirementsSnapshot` is also delivered as part of the `join_server` and `view_server` responses. --- ## Plugin event validation `server/src/services/plugin-support.service.ts` exposes `validatePluginEventEnvelope()`. The `plugin_event` envelope handler (`server/src/websocket/handler.ts`) calls it before broadcasting. Validation checks: - `eventName` is registered for `pluginId` on `serverId`. - `direction` permits the source (clientToServer vs p2pHint vs serverRelay). - `payload` size ≤ `maxPayloadBytes` (default 64 KB). - If `schema` was declared in the manifest, the payload conforms — TODO: confirm the schema dialect (looks like JSON Schema subset). ## Security considerations - **Plugins run in the renderer's JS context.** There is no true sandbox: plugins have DOM access, can read/write `localStorage`, and can issue `fetch` requests subject to the renderer's CSP. - **Capability model is the primary security boundary.** Every method on `TojuClientPluginApi` calls `PluginCapabilityService.assert(pluginId, capability)`. Missing grant → `PluginCapabilityError`. - **Path traversal** in the local plugin loader is blocked by `isPathInside()` realpath checks (`electron/plugin-library.ts`). - **Payload bounds** on plugin events: default 64 KB, configurable per event definition. - **No code signing or integrity verification.** Plugins are trusted to the extent the user granted capabilities. The plugin store flow is documented in `docs-site/docs/plugin-development/`. - **TODO**: review whether `bundle.url` fetches go through a CSP / allow-list or are unbounded. ## Configuration - **Local plugins path**: `${app.getPath('userData')}/plugins/` (Electron). Exposed as `getLocalPluginsPath`. - **Capability grants**: `metoyou_plugin_capability_grants` in localStorage; mirrored to a desktop file via `PluginDesktopStateService`. - **Server-side persistence**: plugin requirements and event definitions are stored in the server's database; entities and migrations live alongside other server entities (TODO: enumerate the specific entities). ## Testing - **Electron**: `electron/plugin-library.spec.ts` — covers valid/invalid JSON, path traversal rejection, escaping entrypoints. Uses fixture `TEST_PLUGIN_FIXTURE_DIR`. - **Renderer**: `plugin-host.service.spec.ts`, `plugin-store.service.spec.ts`, `local-plugin-discovery.service.spec.ts`. - **Server**: `server/src/websocket/handler-plugin.spec.ts` — `plugin_event` validation flow. - **E2E**: `e2e/tests/.../plugin-support-api.spec.ts`, `plugin-manager-ui.spec.ts`, `plugin-api-two-users.spec.ts`. ## Known issues and limitations - **Server-side plugin data is intentionally disabled** (`410 Gone` on `*/data/:key` routes). Plugins must currently treat `serverData` storage as not yet implemented on the signaling server. TODO: clarify whether this is a permanent boundary or scheduled work. - **No true sandbox** for plugin execution. The capability model is the only restraint between a plugin and the renderer's globals. - **Manifest validation is renderer-side.** The Electron loader treats manifests as raw JSON; malformed or hostile manifests are caught when the renderer registers them. - **No host-side plugin allow-list** beyond per-server requirements — a user can install any local plugin. ## Related features - **[ipc-bridge](./ipc-bridge.md)** — surfaces `listLocalPluginManifests`, `getLocalPluginsPath`, and the plugin CQRS commands (`SavePluginData`, `DeletePluginData`, `GetPluginData`). - **[websocket-envelopes](./websocket-envelopes.md)** — defines the `plugin_event` envelope this area validates. - **[server-directory](./server-directory.md)** — `join_server` / `view_server` responses include `PluginRequirementsSnapshot` for the joined server. ## Documentation for plugin authors Author-facing docs ship with the desktop app via Docusaurus: - `docs-site/docs/plugin-development/create-a-plugin.md` - `docs-site/docs/user-guide/plugins.md` - `docs-site/docs/developer/llm-plugin-builder-guide.md` ## Changelog | Date | Change | |------|--------| | 2026-05-25 | Initial documentation |