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>
13 KiB
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_eventenvelopes themselves — those live with websocket-envelopes; this area defines the validation rules applied to them. - IPC plumbing for plugin manifest discovery — that's the ipc-bridge 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(fixed1),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 incapabilities[]; the host enforces grants per plugin. - Plugin event — a declared
{ eventName, direction, scope, schema?, maxPayloadBytes? }tuple.directionisclientToServer | serverRelay | p2pHint;scopeisserver | 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 itsactivate(context)hook:pluginId,manifest,api(the capability-gatedTojuClientPluginApi), and asubscriptions[]cleanup list. - Local plugin — a folder under
${app.getPath('userData')}/plugins/<id>/containingtoju-plugin.json(preferred) orplugin.jsonand 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+ optionalbundle.entrypointUrl— for remote / store-installed plugins.entrypoint— relative path within the local plugin folder; rejected if it escapespluginRoot.relationships.{requires,optional,conflicts,before,after}—pluginId+ optionalversionRange. Resolved at activation; missing required dependencies block activation.events[]— registered with the server viaplugin-supportwhen the plugin is required by a server. The server uses these definitions to validate inboundplugin_eventenvelopes.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) orplugin.jsonas 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 viaisPathInside()realpath check. - Returns
LocalPluginDiscoveryResult { plugins, errors, pluginsPath }where eachplugins[i]is aLocalPluginManifestDescriptorcarrying the raw manifest, manifest path, plugin root, plugin-rootfile://URL, optional entrypoint and readme paths, and adiscoveredAttimestamp.
Exposed to the renderer through the ipc-bridge 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 facadeTojuClientPluginApiper 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 inmetoyou_plugin_capability_grants(localStorage + desktop file).PluginMessageBusService— plugin-scoped pub/sub with topic, optional channel/peer targeting, optional message replay.PluginStorageService— split storage paths forlocalandserverDatascopes.PluginUiRegistryService— central registry of UI contributions consumed byplugin-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 thebundle.urlfor bundled. - Bytes are fetched, wrapped in a
Blob-backed object URL, then imported via dynamicimport()so devtools and stack traces resolve. GuardedPluginMutationObserverwraps 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
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 — 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:
eventNameis registered forpluginIdonserverId.directionpermits the source (clientToServer vs p2pHint vs serverRelay).payloadsize ≤maxPayloadBytes(default 64 KB).- If
schemawas 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 issuefetchrequests subject to the renderer's CSP. - Capability model is the primary security boundary. Every method on
TojuClientPluginApicallsPluginCapabilityService.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.urlfetches go through a CSP / allow-list or are unbounded.
Configuration
- Local plugins path:
${app.getPath('userData')}/plugins/(Electron). Exposed asgetLocalPluginsPath. - Capability grants:
metoyou_plugin_capability_grantsin localStorage; mirrored to a desktop file viaPluginDesktopStateService. - 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 fixtureTEST_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_eventvalidation 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 Goneon*/data/:keyroutes). Plugins must currently treatserverDatastorage 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 — surfaces
listLocalPluginManifests,getLocalPluginsPath, and the plugin CQRS commands (SavePluginData,DeletePluginData,GetPluginData). - websocket-envelopes — defines the
plugin_eventenvelope this area validates. - server-directory —
join_server/view_serverresponses includePluginRequirementsSnapshotfor 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.mddocs-site/docs/user-guide/plugins.mddocs-site/docs/developer/llm-plugin-builder-guide.md
Changelog
| Date | Change |
|---|---|
| 2026-05-25 | Initial documentation |