| title | Plugin API |
|---|---|
| description | Manifest, grants, bridge APIs, lifecycle, and security model for meith web-app plugins. |
| section | Developers |
| sectionOrder | 2 |
| order | 60 |
| slug | developers/plugin-api |
A meith plugin is a web app that runs inside a controlled plugin browser tab. It does not load code into the main process, does not get Node access, and does not register tools into the host.
Instead, the host may expose window.meithPlugin in that plugin tab. The bridge
contains only the API namespaces the user approved for that plugin, and every
privileged action routes back through the main-process tool registry.
See templates/plugin-basic/main.js for a starter web plugin.
- A plugin is installed from a local directory, packaged archive, or development URL.
- The host reads the plugin manifest and stores its requested grants.
- The plugin starts disabled with empty approved grants.
- The user reviews and approves a subset of the requested API namespaces and tool capabilities.
- The plugin can be enabled only after its requested API namespaces are approved.
- Opening the plugin creates a plugin-mode browser tab.
- The main process maps that tab's
webContents.idto the plugin id. - The plugin preload asks the main process for approved identity. If approved,
it exposes
window.meithPlugin; otherwise it exposes nothing.
Identity is always resolved from the sender webContents. The plugin page cannot forge another plugin id or grant itself extra permissions.
The manifest can be in plugin.json at the plugin root or in the meith field
of package.json.
{
"kind": "plugin",
"id": "com.example.hello",
"name": "Hello Plugin",
"version": "0.1.0",
"description": "Shown in the permissions review UI.",
"entry": "index.html",
"permissions": ["read-only"],
"requestedApis": ["tools", "storage"]
}| Field | Meaning |
|---|---|
kind |
Must be "plugin". |
id |
Reverse-DNS-style dotted identifier, such as com.example.hello. |
name |
Display name. |
version |
Version string. Defaults to 0.0.0 when omitted. |
description |
Optional review text shown in the UI. |
entry |
Web entry file for local/package plugins. Defaults to index.html. |
permissions |
Tool capabilities requested by the plugin. |
requestedApis |
Bridge namespaces requested by the plugin. |
permissions and requestedApis are requests, not grants. Runtime enforcement
uses only approvedGrants, never the requested values.
Plugins can be installed from three source kinds:
type PluginSource =
| { kind: "local-dir"; path: string }
| { kind: "package"; path: string; archivePath?: string }
| { kind: "dev-url"; url: string };The host resolves the directory with realpath, reads the manifest, and checks
that the entry file stays inside the plugin root.
Supported archives:
.tgz.tar.gz.tar
The host extracts archives into the managed plugin store under user data. The safe tar reader enforces these hard limits before writing any file to disk:
| Limit | Value |
|---|---|
| Maximum archive size (compressed) | 50 MB |
| Maximum number of files in the archive | 2 000 |
| Maximum size of any single extracted file | 10 MB |
In addition to the size limits, the reader rejects absolute paths, ..
traversal in entry names, hard links (tar type 1), and symbolic links (tar
type 2). Any violation aborts the extraction with INVALID before any file is
written.
The host fetches the manifest from:
<devUrl>/plugin.json
The plugin tab loads the dev URL itself.
Installed plugins store both requested and approved grants:
interface PluginGrants {
capabilities: ToolCapability[];
apis: PluginApiName[];
}requestedGrantsmirrors the manifest and is informational.approvedGrantsis the sole basis for runtime enforcement.
Approving grants always intersects the supplied grants with the requested grants, so approval cannot exceed what the manifest requested.
Reinstalling a plugin preserves existing approvals only when they are still a subset of the new requested grants. Otherwise they are dropped for review.
The bridge shape is exported from @meith/protocol as MeithPluginApi.
Namespaces are optional and should always be feature-detected.
interface MeithPluginApi {
readonly identity: MeithPluginIdentity;
tools?: MeithPluginToolsApi;
storage?: MeithPluginStorageApi;
cdp?: MeithPluginCdpApi;
ai?: MeithPluginAiApi;
}Always present when window.meithPlugin exists.
interface MeithPluginIdentity {
pluginId: string;
name: string;
version: string;
apis: PluginApiName[];
capabilities: ToolCapability[];
}The values are the approved identity and grants, not the raw manifest requests.
Requires the tools API namespace.
interface MeithPluginToolsApi {
list(): Promise<ToolDescriptor[]>;
call(name: string, args?: Record<string, unknown>): Promise<ToolResult>;
}Tool calls are still gated by approved capabilities. For example, a plugin with
the tools API but without controls-browser cannot call a browser-control
tool. The result is a normal ToolResult failure:
{
"ok": false,
"error": {
"code": "PERMISSION_DENIED",
"message": "Plugin com.example.hello lacks capabilities ..."
}
}Requires the storage API namespace.
interface MeithPluginStorageApi {
getBrowserTabs(): Promise<BrowserTab[]>;
getWorkspaceTabs(): Promise<WorkspaceTab[]>;
}Plugin-mode browser tabs are excluded from browser tab listings by default.
Requires the cdp API namespace and the relevant browser-control capability.
interface MeithPluginCdpApi {
send(
tabId: string,
method: string,
params?: Record<string, unknown>
): Promise<ToolResult>;
}The host stamps the plugin's authoritative owner id before calling the registry tool, so CDP commands follow the same tab-ownership model as ordinary plugin tool calls.
Requires the ai API namespace.
interface MeithPluginAiStreamOptions {
prompt: string;
onText?: (delta: string) => void;
onStart?: (controls: { cancel: () => void }) => void;
}
interface MeithPluginAiStreamResult {
text: string;
}
interface MeithPluginAiApi {
streamText(
options: MeithPluginAiStreamOptions
): Promise<MeithPluginAiStreamResult>;
}AbortSignal is not used across the context bridge because it is not cloneable.
The plugin receives cancellation through onStart.
const result = await window.meithPlugin.ai.streamText({
prompt: "Summarize my open tabs.",
onStart: (controls) => {
cancel = controls.cancel;
},
onText: (delta) => {
output.textContent += delta;
},
});The host creates an ephemeral agent session and deletes it after completion. Plugin AI calls cannot bypass agent/tool permissions.
Important invariants:
- Plugin tabs run with context isolation and no Node integration.
window.meithPluginis injected only by the plugin preload.- The main process resolves plugin identity from
webContents.id. - The plugin page cannot pass a trusted plugin id.
- API namespaces are present only when approved.
- Tool calls are checked against approved capabilities on every call.
- A disabled or uninstalled plugin loses live tab authority.
- Navigating away from a plugin entry revokes the webContents-to-plugin mapping.
- Plugin tabs are hidden from normal plugin storage listings.
- Local/package entries are realpath-contained inside the plugin root.
- Packaged archives are extracted with path traversal and link checks, and are subject to size and file-count limits (see Package Archive above).
- Every plugin browser tab has
setPermissionRequestHandlerset to deny all OS-level permission requests (camera, microphone, geolocation, notifications, MIDI, HID, serial, Bluetooth, clipboard-read, fullscreen). - Every plugin browser tab has
setWindowOpenHandlerset to deny allwindow.open()andtarget=_blanknavigations that would create a new renderer process.
Plugin management is itself exposed through normal tools:
| Tool | Purpose |
|---|---|
list_plugins |
List installed plugins, requested grants, approved grants, and enabled state. |
install_plugin |
Install from directory, archive, or devUrl. Exactly one source must be provided. |
approve_plugin_grants |
Approve a subset of requested capabilities and APIs. |
set_plugin_enabled |
Enable or disable an installed plugin. |
uninstall_plugin |
Remove the plugin record and revoke open plugin tabs. |
open_plugin_tab |
Open an enabled plugin in a plugin-mode browser tab. |
These tools are surfaced by the Settings Plugins panel and are also callable from the CLI.
Install from a local folder:
meith install-plugin --directory /absolute/path/to/pluginInstall from a dev server:
meith install-plugin --devUrl http://localhost:5173/Approve grants:
meith approve-plugin com.example.hello \
--capabilities read-only \
--apis tools \
--apis storageEnable:
meith enable-plugin com.example.hello trueOpen:
meith open-plugin com.example.helloPackage a plugin folder:
tar -czf hello-plugin.tgz -C dist .
meith install-plugin --archive /absolute/path/hello-plugin.tgz- Build a normal web app.
- Include
plugin.jsonor ameithfield inpackage.json. - Request the smallest API/capability set you need.
- Feature-detect every namespace before using it.
- Treat every
ToolResultas fallible. - Use
@meith/protocoltypes forMeithPluginApiwhen TypeScript is available.