Manifest reference
manifest.json is the declaration half of a plugin (the other half is the main.ts
entry — see Sandboxed packages). It is validated on
discovery; an invalid manifest is rejected with a typed error and never partially
registered. The published JSON Schema
(https://schemas.lowkeyagents.com/plugins/manifest.schema.json,
in-repo packages/shared/src/plugins/manifest.schema.json) and Lowkey’s
TypeScript validator are parallel sources of truth.
The manifest has two families of fields: capabilities (host-run grants for the
Deno jail — the delivery model) and the declarative-seam blocks
(surface/mcp/webhook/agent) selected by scopes. A web-installable package
ships a main.ts and typically uses capabilities; a manifest that uses only
declarative seams is valid but operator-sideload only.
Top-level fields
Section titled “Top-level fields”| Field | Type | Required | Notes |
|---|---|---|---|
id |
string | yes | Unique, kebab-case (^[a-z0-9]+(?:-[a-z0-9]+)*$). Must equal the plugin’s directory name. |
version |
string | yes | Semver (MAJOR.MINOR.PATCH, optional pre-release/build). |
name |
string | yes | Display name. Non-empty. |
scopes |
string[] | yes | Closed set, unique items. See below. |
surface |
object | no | Mount-point placement. Requires the matching slot scope. |
launch |
object | no | Header launch-entry metadata for panel plugins. |
mcp |
object | no | MCP server to inject. Requires the mcp scope. |
webhook |
object | no | Inbound trigger source. Requires the webhook scope. |
agent |
object | no | Agent contribution. Requires the agent scope. |
capabilities |
object[] | no | Host-run grants for the Deno jail (the delivery model). Present on any package with a main.ts. See Sandboxed packages. |
signing |
object | no | Signature fields. Present but unenforced in v1. |
Unknown top-level keys and unknown keys inside any block are rejected.
Scopes — the closed set
Section titled “Scopes — the closed set”scopes is an enumerated, closed array. An undeclared capability is denied by the
scoped token at request time; declaring a seam block
without its scope fails validation.
| Scope | Grants |
|---|---|
cards |
Emit closed cards into the session feed. |
mcp |
Register an MCP server, injected per project at agent launch. |
webhook |
Register an inbound webhook source; subscribe to turn-completion events. |
focus |
Mount a surface in the focus slot. |
side-panel |
Mount a surface in the side-panel slot. |
agent |
Contribute an AgentDef (system prompt, prompt mode, translator, runtime). |
session-read |
Read a session’s transcript + lineage. |
media and storage are not in v1 (deferred — see Compatibility).
Section ↔ scope coupling
Section titled “Section ↔ scope coupling”A seam block requires its scope. Validation enforces:
| Block present | Scope required |
|---|---|
mcp |
mcp |
webhook |
webhook |
agent |
agent |
surface with slot: "focus" |
focus |
surface with slot: "side-panel" |
side-panel |
A validated manifest guarantees this coupling. Downstream seams nonetheless
re-check defensively — MCP registration re-tests the mcp scope before
injecting a server, and every scoped HTTP route re-runs the scoped-token guard
(token → project → registration/enablement → scope) on each request. Validation is the first
gate, not the only one.
surface
Section titled “surface”Declares which host-owned mount point the plugin’s iframe fills. The host draws the chrome; the plugin fills the rectangle. See surfaces.
| Field | Type | Required | Notes |
|---|---|---|---|
slot |
"focus" | "side-panel" |
yes | Closed set. |
url |
string | yes | Absolute http/https URL of the served surface. |
"surface": { "slot": "side-panel", "url": "https://plugin.example.com/panel" }launch
Section titled “launch”Header launch-entry metadata. Panel plugins only (those with a surface) get a
header icon next to the focus-pane button; headless plugins are managed on the
Plugins settings page and have no icon.
| Field | Type | Required | Notes |
|---|---|---|---|
icon |
string | yes | v1 renders a generic icon for every panel plugin and ignores this value; per-plugin launch.icon → glyph mapping is reserved for a future version. Still required when launch is present. |
label |
string | yes | Used as the icon’s tooltip + accessible label. Falls back to name if launch is absent. |
An MCP server config injected per project at agent launch, the same path as built-in MCP servers, keyed by plugin id. See MCP registration.
| Field | Type | Required | Notes |
|---|---|---|---|
command |
string | yes | Executable to spawn (stdio transport). |
args |
string[] | no | Arguments. |
env |
object<string,string> | no | Environment variables. |
webhook
Section titled “webhook”Registers an inbound trigger source. A POST /api/webhook/<source> carrying the
plugin’s scoped token spawns an agent turn. See webhook source.
| Field | Type | Required | Notes |
|---|---|---|---|
source |
string | yes | The <source> segment in the webhook path. Must be unique enough to route. |
Contributes an AgentDef. See agent contribution
and the external protocol.
| Field | Type | Required | Notes |
|---|---|---|---|
agentId |
string | yes | Roster id of the contributed agent. |
systemPrompt |
string | yes | The agent’s own system prompt. |
platformPromptMode |
"full" | "minimal" |
yes | minimal keeps the safety floor, drops productivity instructions. No none in v1. |
translator |
"none" | "default" |
yes | none = raw agent output, no post-turn rewrite. |
runtime |
string | yes | A built-in runtime id, or external:<pluginId>. |
endpoint |
string | conditional | Required when runtime starts with external:. Absolute http/https URL Lowkey POSTs turns to. |
"agent": { "agentId": "muse", "systemPrompt": "You are Muse, a warm conversational companion…", "platformPromptMode": "minimal", "translator": "none", "runtime": "external:muse", "endpoint": "https://muse.example.com/turn"}capabilities
Section titled “capabilities”A closed, enumerated array of host-run grants for the Deno jail — the capabilities
your main.ts may use. This is the plugin delivery model; an author-hosted service is
a main.ts shim that declares net.egress to its own domain. Omit capabilities
only for a manifest-only plugin that uses solely declarative seams (operator-sideload
only). The full vocabulary (net.egress, fs.project,
fs.project:write, events.turn, mount.focus/mount.side-panel, cards.emit,
session.read, session.fork, token, run.agent), each entry’s shape, the
consent-install flow, and the sandbox enforcement are documented in
Sandboxed packages. Every capability id and its per-entry
fields are in the published schema.
signing
Section titled “signing”Carries publicKey and signature. Designed-for but unenforced in v1 —
integrity verification lands with the catalog. Include the fields; do not expect
enforcement.
Discovery, sourcing, and enablement
Section titled “Discovery, sourcing, and enablement”Lowkey discovers plugins from two roots:
- Global —
~/.lowkey/plugins/<id>/ - Per-project —
<project>/.lowkey/plugins/<id>/
Rules:
- The directory name must equal the manifest
id; the loader enforces it. - Duplicate ids are rejected, not shadowed. If the same
idappears twice — in both roots, or twice in one — the first discovered (the per-project root is scanned before the global one) is registered and the duplicate is rejected with a clear error (plugin id "<id>" is already registered for this project). The rejection is not silent: it is returned in therejected[]array of theGET /api/pluginsresponse (alongsideplugins[]) so the UI surfaces it, and enabling an id that resolves only to a rejected manifest returns400with the error. Lowkey never silently picks one of two same-id plugins. - A plugin is discovered but disabled by default. Enablement is separate
per-project state in
<project>/.lowkey/plugin-state.json, toggled viaPOST /api/plugins. Only enabled plugins contribute MCP servers, mount points, and agents — and only an enabled plugin is issued a scoped token.
Manage all of this from /api/plugins (list/enable/disable/configure), surfaced
in the Plugins settings page and the per-project enable section.