Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Community Plugin Host Boundary

This page defines the approved host boundary for community plugin execution across browser/PWA and Electron desktop runtimes. It expands the host-model direction in Plugin Runtime without changing the current Obsidian-compatible plugin folder format.

Scope

The boundary applies to community plugins discovered under /.obsidian/plugins/<id>/. Bundled first-party plugins remain renderer-local and continue to use the normal PluginManager lifecycle.

The current renderer implementation remains the compatibility fallback. PluginManager now routes community plugin evaluation through a runtime-neutral execution-host seam, with the renderer host as the default implementation. The Electron sidecar path is the approved desktop target, but it must stay capability-advertised as unavailable until the IPC protocol, sidecar process manager, and API facades are implemented together.

That fallback remains the trusted DOM compatibility host for existing Obsidian-style plugins. New Lapis-native extension surfaces should prefer brokered capabilities plus isolated hosts or UI containers so the app can make trust and ownership boundaries explicit.

Ownership Model

PluginManager remains the lifecycle authority in every runtime. It owns discovery, manifest preflight, enable/disable state, boot ordering, rollback, and user-facing failure reporting.

A selected community-plugin execution host owns only code execution and host-local diagnostics:

HostRuntimeExecution locationIsolation guarantee
Renderer hostbrowser/PWA and fallbackactive renderertransactional lifecycle and structured unsupported-API errors, but no preemption
Electron sidecar hostElectron desktopElectron main-owned JS child process poolrequest timeouts, crash containment, sidecar restart, and capability-brokered APIs

The renderer host and Electron sidecar host must expose the same lifecycle surface to PluginManager so boot remains discover -> preflight -> activate plugins -> restore layout.

Electron Sidecar Shape

Electron main owns the sidecar lifecycle. The renderer never receives Node.js primitives, child-process handles, raw file paths outside the selected vault, or the real Electron IPC surface.

The sidecar manager follows the proven notebook DuckDB sidecar pattern:

  • fork a JS child process from Electron main
  • use request/response messages with unique request IDs
  • enforce a default 30-second request timeout
  • reject all pending requests when the sidecar exits
  • restart after crashes or timeouts only when there are still hosted plugins to service
  • shut down during app quit and when the owning vault/window closes

The preload bridge exposes typed desktop_plugin_host_* IPC commands. The Electron host supports prepare, evaluate, activate, deactivate, and shutdown against Electron main-owned JS child processes, includes the initial parent-brokered capability request path, and advertises plugin-sidecar with status: "available". The current desktop hardening slice runs one trusted child process per plugin ID and vault/window context, so crash recovery and restart budgets are isolated per hosted plugin. The shared renderer still preserves the renderer host as the compatibility fallback and selects the sidecar only for desktop/sidecar-aware plugins or plugins that request brokered capabilities.

The Electron sidecar CommonJS v1 path is bundled-only. It evaluates the selected entry file as a single CommonJS bundle; synchronous local require("./chunk"), parent-relative, or absolute require() calls fail with a clear bundle-required diagnostic instead of attempting async vault reads inside CommonJS resolution. The sidecar hosted dependency surface is generated from the shared host-module catalogue and is intentionally limited to lapis and @lapis-notes/api. Renderer compatibility modules such as obsidian, Svelte, DOM and CodeMirror view packages, and @lapis-notes/ui are unavailable in the sidecar. Dependency failures include the host, selected module format, selected entry, and rejected specifier so PluginManager can preserve actionable diagnostics in the settings UI.

Capability Broker

Hosted community plugins must not receive the real renderer App graph. They receive a capability facade assembled by the selected execution host and brokered through PluginManager policy.

The initial background-safe capability set is:

CapabilityPurpose
vault:readRead vault-scoped text/binary files and metadata needed by the plugin
vault:writeWrite, create, rename, and remove vault-scoped files through existing path-safe adapters
plugin:dataLoad and save plugin-scoped data.json
commandsRegister and unregister serializable command contributions
noticesRequest renderer-owned notices without direct DOM access
settingsRead app/plugin settings and register declarative plugin settings surfaces
metadata:queryQuery indexed metadata snapshots without receiving live renderer cache internals
eventsSubscribe to approved serializable app/vault/plugin lifecycle events
loggingEmit structured plugin logs and diagnostics tied to plugin ID and host mode

The first hosted phase does not include raw DOM access, direct Svelte component mounting, live CodeMirror instances, arbitrary renderer mutation, Node.js built-ins, network access, or direct filesystem paths outside vault-scoped broker calls.

Community manifest extensions may declare supportedRuntimes, requiredCapabilities, and executionHints, while baseline Obsidian manifests remain valid when those fields are absent. Runtime-aware preflight rejects a plugin before evaluation when declared requirements cannot be satisfied by the selected host. The renderer host currently grants no brokered hosted capabilities, so plugins that explicitly require brokered capabilities wait for a capable host instead of running against an unsafe partial facade.

IPC Protocol Direction

The Electron host protocol uses renderer-to-main IPC for lifecycle requests and main-to-sidecar child-process messages for execution. All payloads must be structured-clone safe.

Planned renderer-visible commands:

CommandPurpose
desktop_plugin_host_prepareCreate or reuse the sidecar context for a vault/window and return status
desktop_plugin_host_evaluateEvaluate a community plugin bundle after manifest preflight, including the selected runtime entry metadata
desktop_plugin_host_activateRun hosted plugin onload() against a capability facade
desktop_plugin_host_deactivateRun hosted plugin onunload() and dispose registered contributions
desktop_plugin_host_shutdownStop the sidecar context for a vault/window

Capability calls from the sidecar back into the renderer-visible app must go through main-owned broker requests, not ad hoc require("obsidian") access to renderer objects. Contributions that affect renderer UI, such as commands and future settings views, are returned as serializable descriptors and installed by PluginManager in the renderer.

Failure And Recovery

Sidecar failures must preserve the current plugin-runtime containment rules:

  • a sidecar evaluation or activation error marks only that plugin as failed
  • a sidecar crash marks hosted active plugins as failed, rolls back renderer-installed contributions, and keeps workspace boot moving
  • timeout errors include the request type and plugin ID in diagnostics
  • restart attempts are bounded and visible through plugin diagnostics
  • plugins-loaded still fires only after all activation attempts settle

The community plugin settings UI surfaces host mode, requested capabilities, granted capabilities, requested and granted app permissions for indexed Lapis extensions, a clear trusted-desktop warning when that runtime is declared, last failure, and restart/disable actions for both registered plugins and preflight/evaluation failures that never produced a plugin instance.

Workspace Trust is the user-facing policy layer on top of this boundary. App now owns a vault-scoped WorkspaceTrustService backed by the active adapter vault id through a ScopedVaultStore("workspace-trust") record. Trust defaults to trusted for existing vaults until the user explicitly revokes it, and the service exposes the persisted trust state, grant(), revoke(), request(), plus changed and requested events for future prompt flows.

When a workspace is untrusted, community plugins cannot activate trusted desktop runtime entries, cannot receive brokered hosted capabilities, and cannot register compatibility settings tabs. Plugin diagnostics preserve the selected host mode, requested capabilities, and the trust-gated failure reason so the settings UI can explain why the plugin stayed blocked before evaluation. App also mirrors the current state into the workspace.trusted context key so when clauses can react to trust state.

The workspace Community Plugins settings section now shows a top-level Workspace Trust row with current status plus trust and revoke actions. Trust does not make trusted desktop code safe by itself; it only gives the app a clear place to gate powerful plugin features and explain the decision to users.

Implementation Phases

  1. Done: PluginManager community-plugin evaluation is behind a runtime-neutral execution-host interface while preserving the renderer host as the default.
  2. Done: Electron main/preload sidecar IPC skeleton and child-process manager exist for prepare/shutdown smoke coverage.
  3. Done: hosted-plugin capability facade types and the initial Electron broker path exist for vault I/O, plugin data, commands, notices, settings reads, metadata queries, approved event subscriptions, and logging.
  4. Done: runtime-aware manifest preflight and diagnostics UI exist for host mode, capability decisions, last failure, restart, and disable actions.
  5. Done: Electron sidecar evaluate/activate/deactivate runs trusted Lapis desktop entries through brokered capability facades while PluginManager remains the renderer lifecycle authority.
  6. Expand renderer-safe declarative contribution models for richer community plugin UI.

Each phase must preserve the current plugin boot invariant: layout restoration begins only after community plugin activation attempts have completed.