Plugin System
neokapi plugins are manifest-driven, signed, out-of-process executables. The
default kapi binary is Apache-2.0 and links zero vendor-plugin code; everything
beyond the open-source core — cloud sync, the Okapi filter bridge, third-party
formats — ships as a separate binary that kapi discovers on disk and dispatches
to at runtime.
This page is the developer-facing overview. AD-007: Plugin System
holds the full design rationale; the Plugin model note
covers the complementary in-process side — how the Go code inside a plugin
binary wires its features into the shared cli.App.
The manifest
Every plugin's directory contains a manifest.json declaring its identity
(plugin, version, binary, license, min_kapi_version, group) and the
capabilities it provides:
{
"manifest_version": "1",
"plugin": "myplugin",
"version": "1.4.0",
"binary": "kapi-myplugin",
"license": "Apache-2.0",
"min_kapi_version": "1.0.0",
"capabilities": {
"commands": [...],
"mcp_tools": [...],
"formats": [...],
"tools": [...],
"source_connectors": [...],
"schema_extensions": [...]
},
"daemon": {
"idle_timeout_seconds": 300,
"handshake": { "type": "stdio-handshake", "fields": ["socket", "version"] }
}
}
The daemon block appears only when a plugin declares formats, tools, or source
connectors (the Mode-C transport, below). The canonical Go types live in
core/plugin/manifest/manifest.go,
and the embedded JSON Schema at core/plugin/manifest/schema.json.
kapi reads every manifest at startup and builds dispatch tables from them. There
is no name fall-through and no $PATH lookup — a capability dispatches only if a
manifest declares it.
Discovery
Plugins are discovered structurally by location, in precedence order:
| Order | Location | Purpose |
|---|---|---|
| 1 (highest) | $KAPI_PLUGINS_DIR (:-separated; ; on Windows) | Dev / CI / sandbox |
| 2 | $XDG_DATA_HOME/kapi/plugins/ (~/.local/share/kapi/plugins/) | kapi plugin install target |
| 3 | system roots (/opt/homebrew/share/kapi/plugins/, /usr/local/share/kapi/plugins/, /usr/share/kapi/plugins/) | OS package managers |
Within each location, every direct subdirectory containing a manifest.json is a
plugin. First-match-wins on plugin name. Two different plugins declaring the same
capability is an error — kapi prints both manifests and refuses to dispatch the
conflicting capability. A consolidated dispatch cache at
$XDG_CACHE_HOME/kapi/plugins-cache.json skips manifest parsing when no
discovery root has changed.
Three transport modes
A plugin declares one or more capability sections; kapi picks the transport per capability type.
-
Mode A — one-shot subprocess (
commands). kapi forks<binary> command <name> [args]once per invocation, inheriting stdio and propagating the exit code. No state survives across calls. -
Mode B — session subprocess (
mcp_tools). kapi spawns<binary> mcp-serveronce perkapi mcpsession and proxies tool calls over MCP-over-stdio. -
Mode C — daemon over Unix socket (
formats,tools,source_connectors). kapi spawns<binary> daemon; the plugin binds a Unix-domain socket and prints one JSON handshake line on stdout, then serves gRPC on the socket:{"socket":"/tmp/kapi-daemon-myplugin-12345.sock","version":"1.4.0"}kapi opens a gRPC client to that socket and dispatches concurrent requests. The daemon stays alive until kapi exits or hits its idle timeout (per-manifest, default 5 min). Concurrent daemons are capped via
KAPI_MAX_DAEMONS(default 8) with LRU eviction. Format and tool capabilities register into the standardFormatRegistry/ToolRegistryand are indistinguishable from native ones at the API level. The Okapi bridge is the canonical Mode-C plugin — see Okapi Bridge.
The host-side runtime — discovery, dispatch, the daemon pool, the registry
client, and signature verification — lives in
cli/pluginhost/.
Declaring a plugin dependency
A .kapi recipe declares the plugins it needs as a map of name → semver
constraint:
version: v1
name: my-app
requires:
myplugin: "^1.0"
okapi-bridge: ">=1.47.0"
Loading the recipe fails if a named plugin is not registered. On a TTY, kapi
offers to install it and retries; in CI it prints an actionable error pointing at
kapi plugin install.
Lifecycle commands
kapi plugin list # show installed plugins
kapi plugin install <name> # download + verify signature + register
kapi plugin install <name>@<version> # pin a specific version
kapi plugin update <name> # upgrade to latest matching constraint
kapi plugin remove <name> # uninstall
kapi plugin info <name> # show manifest details
kapi plugin search <query> # list registry candidates
kapi plugin verify <name> # re-check sha256 + signature
kapi plugin update-index # refresh the cached registry index
kapi plugin rebuild-cache # force a rebuild of the plugin dispatch cache
kapi plugin install resolves the plugin from a registry — a JSON index served
over HTTPS that maps plugin → versions → per-platform tarball URL, SHA-256, and a
cosign certificate identity. Tarballs are cosign-signed via Sigstore keyless
OIDC; install verifies the SHA-256 and the signing certificate against the
registry-pinned identity before unpacking. Unsigned plugins refuse to install
without --unsafe.
Standard plugins
- A platform plugin — cloud-server sync (
push/pull/auth), distributed separately on its own license terms. It demonstrates how a separately-licensed plugin attaches over the manifest model without re-licensingkapi: installed via its own Homebrew formula, which drops its binary intoshare/kapi/plugins/<plugin>/. - okapi-bridge — a JVM-backed Mode-C daemon exposing the Okapi Framework's filter library to neokapi. See Okapi Bridge.
A minimal Go reference plugin in
examples/plugins/hello/
covers Mode A + B with no third-party dependencies.
See also
- AD-007: Plugin System — full design rationale and the registry/signing model
- Plugin model note — the in-process registry contract a plugin binary uses to wire features into
cli.App - Okapi Bridge — the canonical Mode-C bridge plugin