Files
kandev/docs/public/plugins.md
T
João Salavisa 370cee8f7f docs: add plugin system guide, authoring tutorial, and manifest reference (#1774)
* docs: add plugin system guide, authoring tutorial, and manifest reference

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: update plugin docs for host data API, config, and nav additions

Cherry-picked plugin docs (d853b66b8) were written before four follow-up
PRs merged: typed capability-gated Host data API (#1753, ADR 0043),
richer frontend SDK (#1759), per-plugin settings pages (#1761), and
sidebar Integrations nav items (#1767). Re-verify against the merged
code and bring the docs current:

- plugins.md: per-plugin settings/config page, expanded Host RPC list,
  updated capability vocabulary (api_read/api_write), sidebar nav note.
- plugins-authoring.md: GetConfig/GetSecret/SetSecret/DeleteSecret and
  the six Host data readers, registerRoute options/PluginPageChrome,
  NavItem icon/integrations section, host.api.baseUrl/navigate, and the
  full host.ui component list.
- plugins-manifest.md: corrected api_read/api_write semantics (now
  enforced, not reserved), config_schema validation/secret-masking
  behavior, and new Host data API / config-schema sections.
- configuration.md, feature-status.md: document the KANDEV_FEATURES_PLUGINS
  flag alongside the existing Office flag.
- coverage.json: new plugin-system coverage area so the public-docs
  validator accounts for the three new pages.

* docs: fix plugin config route, signing claim, and data-dir layout

Address automated review findings on PR #1774:
- plugins-manifest.md: PATCH route for config is /api/plugins/{id}, not
  /api/plugins/{id}/config (only GET uses the /config suffix).
- plugins-authoring.md: host.store is not read-only — setState is
  genuinely exposed and writable; correct the misleading inline comment.
- plugins.md: signature verification is not currently wired up in
  production (pkgtar.VerifySignature is nil, never assigned outside
  tests), so every package installs and is reported as unsigned today —
  correct the 'kandev verifies it' claim in both the install pipeline
  step and the signed/unsigned section.
- plugins.md: KANDEV_PLUGIN_DATA_DIR is <id>/data, a sibling of the
  <version>/ directory (shared across versions), not nested inside it —
  fix the on-disk layout diagram.

* docs: mark plugin docs as experimental

The plugin system is feature-flagged and off by default in production
(KANDEV_FEATURES_PLUGINS). Mark all three plugin pages with
status: experimental frontmatter, matching the k8s.md convention for a
page that is wholly about an experimental feature.

* docs: add plugin UI screenshots and a capture spec

Embed four operator-facing screenshots in the plugin docs (the Plugins
list, the install dialog, a per-plugin settings page with its manifest
card, and a native plugin route). Captured from an isolated e2e backend
with the kandev-plugin-hello reference example installed.

Add e2e/tests/plugins/plugins-docs-screenshots.spec.ts to regenerate them:
it reuses the plugins e2e harness (isolated backend, plugins flag on) and
is skipped unless CAPTURE_DOCS_MEDIA=1, so it adds no CI cost. Defaults to
the repo's own e2e fixture package; override DOCS_PLUGIN_* to point at the
polished hello example (as the committed captures were taken).

* docs: fix plugin screenshot paths for the landing publisher

The Cloudflare preview build failed because markdown images under
docs/public/media/ compile to build-time module imports that the docs
site can't resolve — media/ is reserved for <DocsVideo> assets. The
landing publisher only rewrites and copies images referenced under
screenshots/ (README convention). Move the four plugin captures to
docs/screenshots/plugin-*.png and reference them as ../screenshots/*.png,
and update the capture spec to write there.

* docs: convert plugin how-it-works diagram to mermaid

The hand-drawn ASCII pipeline rendered with broken alignment and stray
syntax highlighting on the docs site. Replace it with a mermaid flowchart
(same convention as architecture.md), which renders as a clean SVG.

* docs: give plugins its own nav section

* docs: clarify plugin tools are declared/listed, not agent-invocable yet

Plugin tools[] are validated and exposed via GET /api/plugins/tools, and
the gRPC InvokeTool transport is implemented and tested, but nothing in
production calls it — no agent, MCP bridge, or HTTP route. The docs
overstated this as agent-invocable. Correct the Plugin interface comment,
the manifest tools[] rows, the how-it-works diagram, and the example
plugin description, and add a note in both the authoring and manifest
pages that InvokeTool stays unused until the agent-tool wiring ships.

* docs: clarify webhook relay in how-it-works diagram

The diagram collapsed two hops into one node, reading as if the plugin
receives the external HTTP POST directly. Split it: an external caller
POSTs to kandev's webhook route, which relays the request to the plugin
over the gRPC HandleWebhook method (the plugin serves no HTTP itself).
Also label the DeliverEvent/InvokeTool/HandleWebhook edges by their real
proto method names.

* docs: note the plugin backend is any go-plugin gRPC executable, Go SDK only

* docs: remove plugin tools from the docs

Plugin tools[] overlap with MCP (kandev's established agent-tool surface)
and the invocation path was never wired to agents. Rather than document a
half-feature that implies plugins are an alternate MCP, drop it entirely:
the Plugin interface (InvokeTool), the manifest tools[] block and field
rows, the how-it-works diagram branch, and the example-plugin mention.
Removal from the backend code is tracked separately.

* docs: replace how-it-works mermaid with a designed diagram image

The mermaid flowchart rendered poorly. Replace it with a hand-authored
HTML/CSS architecture figure (install pipeline + the runtime gRPC
interactions), rendered to docs/screenshots/plugin-architecture.png. The
diagram source lives in the docs-screenshot capture spec (skipped unless
CAPTURE_DOCS_MEDIA=1) so it regenerates deterministically at 2x.

* docs: make the how-it-works figure a real box-and-arrow diagram

The previous figure read as a list of rows. Replace it with a hand-drawn
SVG architecture diagram: the install pipeline feeds a spawned plugin
subprocess, then kandev and the plugin communicate over one gRPC
connection (DeliverEvent, HandleWebhook, and the plugin's Host API
callbacks), with the external webhook caller and the browser SPA as
edge nodes. Source lives in the capture spec's ARCH_HTML.

* docs: make the how-it-works diagram a clean monochrome flowchart

Drop the multi-color component diagram for a calm top-down flowchart
(mermaid's shape, drawn properly): install → verify → extract → spawn,
branching into event delivery and webhook relay, converging on the Host
API callbacks, with an optional native-UI step. Grayscale, no accent
colors. Source stays in the capture spec's ARCH_HTML.

* docs: dark plugin diagram + space out the two page images

- Make the how-it-works flowchart a dark rounded card (matches the docs
  page in dark mode; reads as an embedded card in light). Rendered with
  omitBackground so the corners are transparent.
- Move the Settings > Plugins list screenshot out of the intro (where it
  sat right before the diagram) down into the "Installing a plugin"
  section, so text separates the two images.

* docs: update install-dialog screenshot for aligned footer actions

Recaptured after fix(plugins): align install-dialog footer actions
(#1780) — Cancel and Install now sit together in one right-aligned
footer row instead of the Install button being stranded under the
dropzone.

* docs: account for /settings/system/storage in coverage.json

The storage settings route (added by #1699) was never recorded in the
public-docs coverage inventory — the docs validator is path-filtered to
docs/public/** so it didn't run for that PR. This docs PR is the first to
trip it. Add the route to system-health-and-maintenance.

* docs: review fixes — remove stale tools refs, tighten accuracy

Review pass against the now-merged main (tools removed in #1776):
- extending-kandev.md: drop "serves agent-invocable tools" from the plugin
  overview (the feature no longer exists).
- plugins-manifest.md: the legacy endpoints block is health/events/webhooks
  only (no tools path); and the `signed` field is always false today since
  signature verification isn't wired.
- plugins.md: install validation covers the categories/UI-surface enums (not
  a "capability vocabulary"); AutoMTLS secures the channel on all platforms,
  not just Windows; and status vs signing are separate badges.

* docs: explain what Host state is (GetState/SetState)

The authoring guide listed the state RPCs but never said what 'state'
means. Add a plain-language explanation: a per-plugin, kandev-persisted
key/value store keyed by (scope, scopeID, key) holding JSON objects,
namespaced per plugin, scoped to instance/workspace/task/agent, and how it
differs from the writable data dir. Make the example a real increment.

* docs: note NavItem section:'settings' is not rendered

A NavItem with section:'settings' is accepted by the type but no sidebar
section renders it (plugin-nav-items renders main/default, the Integrations
sections render 'integrations'); annotate that settings pages use
registerSettingsRoute. Addresses repeated review feedback.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 00:56:32 +01:00

10 KiB

title, description, status
title description status
Plugins Install and manage kandev plugins: Go backends kandev spawns and supervises, with an optional native frontend bundle. experimental

Plugins

Plugins extend kandev without forking core: a plugin ships a Go backend that kandev spawns and supervises as a subprocess over a strict typed gRPC protocol, and can optionally ship a native frontend bundle that kandev loads into the SPA. This page covers what plugins are, how to install and manage them, and the current security posture. For building a plugin, see Authoring a plugin. For the manifest schema, see Plugin manifest reference.

Plugins are an operator-level, instance-wide capability — there is no per-user plugin access. They are gated behind the plugins feature flag (Settings > System > Feature Toggles), off by default in production builds and on by default in dev/e2e builds. Enabling the flag requires a backend restart and surfaces Settings > Plugins in the sidebar.

How it works

Plugin lifecycle: install, verify, extract, and spawn a go-plugin gRPC subprocess; then, over one supervised gRPC connection, kandev delivers bus events and relays external webhooks to the plugin, the plugin calls back into the Host API, and the SPA optionally loads the native UI bundle.

Kandev owns the whole process lifecycle: it extracts the package, spawns the binary, completes the go-plugin handshake, health-checks it (Ping every 30s), and restarts it on crash or repeated health-check failure (backoff, max 5 attempts). There is no separate operator-managed plugin process to run or babysit — install a package and kandev does the rest.

A native UI bundle can register a nav item that renders as a top-level sidebar entry or, when it declares itself part of the Integrations section, alongside kandev's first-party integration links in the main sidebar's Integrations section — expect new entries to appear there once such a plugin is installed and active.

Installing a plugin

Open Settings > Plugins and click Install plugin. You can install from a URL (kandev downloads the tarball) or by uploading a .tar.gz file directly. No credentials are ever shown or copied — installing a plugin has nothing to reveal, unlike a webhook-secret/API-key registration flow.

The Install plugin dialog with From URL and Upload file tabs and a drag-and-drop area for a .tar.gz package.

The same operations are available over HTTP:

# Install from a URL
curl -X POST http://localhost:38429/api/plugins/install \
  -H 'Content-Type: application/json' \
  -d '{"url": "https://example.com/acme-tools-1.0.0.tar.gz"}'

# Install by uploading a local tarball
curl -X POST http://localhost:38429/api/plugins/install \
  -F "package=@acme-tools-1.0.0.tar.gz"

Either path runs the same pipeline:

  1. Verify checksums.txt covers every other file in the tarball and every hash matches (always enforced).
  2. Check for checksums.txt.sig. Signature verification is not currently wired up, so every package — signed or not — installs and is reported as unsigned today (see "Signed vs. unsigned packages" below).
  3. Parse and validate manifest.yaml before any code runs: schema, id pattern, the categories and UI-surface enums, and that runtime.executables contains an entry for the host's OS/arch.
  4. Extract to ~/.kandev/plugins/<id>/<version>/ and record the installation.
  5. Spawn the platform-matched binary and complete the go-plugin handshake. Status is registered while this is pending, active once the handshake succeeds, or error if spawn/handshake fails (the operator can retry via Enable).

A successful install that failed to spawn returns HTTP 201 with a warning field rather than failing outright — the package is installed, just not yet running.

Once installed, the plugin appears in the list with its category, a status badge (active), a signing badge (unsigned today), and Disable and Uninstall actions:

The Settings > Plugins page listing an installed, active plugin with its category, an unsigned badge, and Disable/Uninstall actions.

Filesystem sideload and Sync

Besides install-by-URL/upload, an operator with shell access to the host can place plugin content directly under ~/.kandev/plugins/ without going through the install endpoint. The Sync button in Settings > Plugins (and POST /api/plugins/sync) reconciles kandev's registry with what is actually on disk:

  • A dropped directory — ~/.kandev/plugins/<id>/<version>/manifest.yaml placed manually with no existing record — is validated and registered with status disabled, never auto-enabled. Directory sideloads skip the checksum/integrity gate the URL/upload pipeline runs, so an operator must explicitly inspect and enable one. If more than one version directory exists for the same unregistered id, the lexically greatest version is registered and the others are reported as skipped.
  • A dropped tarball — any *.tar.gz sitting directly in ~/.kandev/plugins/ — is run through the same verified install pipeline POST /api/plugins/install uses. On success the tarball file is deleted; on failure it is left in place and the failure is reported.
  • A missing install — a registered record whose install_path no longer exists on disk — is stopped (if running) and marked error.

At boot, kandev runs only the directory-sideload and missing-install steps (never the tarball-install step), as part of resuming plugins that were already active. This is conservative by design: starting up never spawns a binary an operator hasn't explicitly approved via install or Sync.

Enable, disable, uninstall

  • Disable stops the subprocess. Config and state are preserved; no events or webhooks are delivered while disabled.
  • Enable respawns the subprocess and re-completes the handshake.
  • Uninstall stops the subprocess and deletes the plugin's package, registration record, and all persisted state — there is no grace period.

Per-plugin settings

A plugin can declare config_schema in its manifest, generating a settings form at Settings > Plugins > <plugin> (also GET /api/plugins/{id}/config and PATCH /api/plugins/{id}). Fields marked secret: true or format: "password" (for example a GitHub PAT) are never returned in cleartext to the operator UI — reads show a masked placeholder, and resubmitting the form unchanged leaves the stored secret alone. The plugin process itself receives the real values via the GetConfig Host RPC. Saving config restarts the running plugin so it re-reads its config. <id>.config.yml on disk is written with mode 0600 and may hold vault references rather than cleartext for secret fields.

A plugin's settings page: a schema-driven form with a masked API-token field, a toggle, and a text field, above a Manifest card showing the plugin's id, version, signing status, and capabilities.

Signed vs. unsigned packages

Every package's checksums.txt is verified at install time — this integrity gate is always enforced. Signing (checksums.txt.sig, an ed25519 signature over checksums.txt) is a separate, optional layer, and its verification hook is not currently wired up in the shipped product: no signature is cryptographically checked today, so every install — signed tarball or not — is currently treated and reported as unsigned. A signed package installs identically to an unsigned one; signing is not required in v1.

On-disk layout

~/.kandev/plugins/
├── <id>.yml                    # registration record (status, install_path, signed, ...)
├── <id>.config.yml             # operator-editable config (PATCH /api/plugins/{id})
└── <id>/
    ├── <version>/              # extracted package (InstallPath)
    │   ├── manifest.yaml
    │   ├── server/plugin-<goos>-<goarch>[.exe]
    │   └── ui/bundle.js         # optional
    └── data/                    # KANDEV_PLUGIN_DATA_DIR — shared across versions

Security posture

  • Auth is the spawn relationship. Kandev spawns the plugin subprocess itself over a unix domain socket (macOS/Linux) or loopback TCP (Windows) — never a routable network address — and secures it in both cases with the go-plugin handshake plus AutoMTLS. There is no api_key, webhook_secret, or HMAC signing anywhere in the contract.
  • Capability-based access control. A plugin can only call the Host RPCs it declared in its manifest: state gates the state RPCs, secrets gates the plugin-owned secret RPCs, and each read-only data accessor (tasks, sessions, workspaces, workflows, agent profiles, repositories) is gated individually via api_read:<resource> (write access, api_write:<resource>, is reserved but not yet implemented). An undeclared capability returns gRPC PermissionDenied with a message naming the missing capability, checked by a server interceptor before the handler runs. GetConfig and EmitEvent are the only ungated RPCs — a plugin can always read its own config (secrets included) and emit events.
  • Native UI plugins run in-origin with full app-store access. This is an accepted tradeoff, not an oversight: a plugin bundle shares the kandev React instance and Zustand store so it can build UI indistinguishable from first-party pages. In v1 only active, operator-installed plugins load; a failing bundle or initialize is caught and never breaks boot; slot components render behind error boundaries. Hard sandboxing (a worker or realm boundary) is explicit future work — see below.
  • Package integrity is always checked; signing is optional. See "Signed vs. unsigned packages" above.
  • No plugin marketplace. Install-by-URL/upload is a manual, single-plugin action; there is no central catalog, one-click install, or automatic discovery.

Related: Authoring a plugin, Plugin manifest reference, and Extending Kandev.