Add area-level documentation for the five most significant cross-context feature areas under agents-docs/features/: - websocket-envelopes: full envelope catalogue, lifecycle, dispatcher - ipc-bridge: window.electronAPI surface, IPC channels, CQRS dispatch - plugin-system: manifest contract, runtime, capabilities, plugin-support API - server-directory: REST endpoints, CQRS, entities, business rules - voice-signaling: mesh signaling, RNNoise pipeline, domain split Update agents-docs/FEATURES.md index alphabetically and remove the "no cross-context feature docs" placeholder. Each doc records honest TODOs for verified gaps (stale signaling-contracts.ts, window.api vs window.electronAPI mismatch, IPC error envelope drift from CONTEXT.md, missing OpenAPI coverage for server-directory routes, no envelope round-trip test). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
181 lines
13 KiB
Markdown
181 lines
13 KiB
Markdown
# 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/<id>/` 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<void>;
|
|
deactivate?(context: TojuPluginActivationContext): void | Promise<void>;
|
|
ready?(context: TojuPluginActivationContext): void | Promise<void>;
|
|
onServerRequirementsChanged?(context, snapshot): void | Promise<void>;
|
|
onPluginDataChanged?(context, event): void | Promise<void>;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 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 |
|