Files
kandev/docs/public/plugins-manifest.md
T
João Salavisa 52946b9159 docs: document the plugin marketplace (#1805)
* docs: document the plugin marketplace

Add a public Plugin Marketplace page covering both audiences: operators
using the in-app catalog (Browse/Installed tabs, one-click install,
Update to vX, the Sources dialog for team/corporate registries, plugin
icons, feature-flag gating, sideloading still works) and plugin authors
publishing into it (one-repo-per-plugin GitHub Release assets, the
optional manifest icon field, submitting a plugins.yaml PR to the
official catalog, GitHub-stars-only ranking, and hosting your own
index.json source).

Also cross-link the new page from plugins.md (replacing the now-false
"No plugin marketplace" security note), plugins-authoring.md, and
plugins-manifest.md (new icon field row); register it in meta.json and
coverage.json.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: correct plugin-registry CI validation claims

The registry index-build workflow does not validate plugins.yaml against
schema.json, nor does build-index.mjs compare the entry id to the
manifest id — bad entries are skipped-and-logged, and CI only fails when
a non-empty list resolves to zero built entries. Reword the submission
steps (in both the marketplace docs page and plugin-registry/README.md)
to describe what the workflow actually does: it resolves each entry
against the GitHub API and requires a correctly named
<id>-<version>.tar.gz release asset, with maintainer approval as the real
gate. Also clarify that plugins.yaml `categories` are free-form curation
tags, not the manifest's category enum.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: split plugin-registry Pages deploy into its own job

The single build-and-deploy job bound the `github-pages` environment at
the job level, so GitHub evaluated that environment's protection rules on
every PR run — failing the check even though the deploy steps were gated
to non-PR events. This broke every PR touching plugin-registry/**, which
is exactly what a plugin-submission PR does.

Split into a PR-safe `build` job (no environment) and a non-PR-gated
`deploy` job that owns the `github-pages` environment and runs only
`deploy-pages`. Behavior on push/schedule/workflow_call is unchanged; PRs
now run build+tests without tripping the environment gate. The star
refresh calls this workflow by file path, so the job rename is safe.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: describe index-build asset resolution accurately

build-index.mjs picks the <id>-<version>.tar.gz release asset when
present but falls back to the release's first .tar.gz otherwise, and only
errors when a release has no .tar.gz at all — it does not hard-fail a
misnamed asset. Reword the submission steps to present the exact asset
name as an authoring instruction (so the index resolves the right asset)
rather than a builder-enforced gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Revert "ci: split plugin-registry Pages deploy into its own job"

This reverts commit 2c5f649146.

* Reapply "ci: split plugin-registry Pages deploy into its own job"

This reverts commit 3795cdb98a.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 17:44:15 +01:00

16 KiB

title, description, status
title description status
Plugin Manifest Reference Complete field-by-field reference for a kandev plugin's manifest.yaml, including the full event-subscription vocabulary. experimental

Plugin Manifest Reference

manifest.yaml is the authoritative description of a plugin — identity, runtime executables, capabilities, webhooks, config schema, and optional UI bundle. Kandev parses and validates it before any plugin code runs. See Authoring a plugin for the build workflow and Plugins for install/operate.

Annotated example

id: "kandev-plugin-slack"                    # ^[a-z0-9][a-z0-9._-]*$
api_version: 1                               # must be 1 (the only supported value)
version: "1.0.0"
display_name: "Slack Notifications"
description: "Post to Slack on task events, relay messages to agents"
author: "kandev"
categories: ["connector"]                    # connector | automation | tools | analytics

runtime:
  type: binary                               # only supported value today
  executables:
    linux-amd64: server/plugin-linux-amd64
    linux-arm64: server/plugin-linux-arm64
    darwin-amd64: server/plugin-darwin-amd64
    darwin-arm64: server/plugin-darwin-arm64
    windows-amd64: server/plugin-windows-amd64.exe
                                              # any subset; the host's <goos>-<goarch> key
                                              # is required at install time
min_kandev_version: "0.78.0"                 # optional

capabilities:
  events: ["task.created", "task.state_changed", "agent.completed"]
  api_read: ["tasks", "agent_profiles"]      # gates the Host data-reader RPCs
  api_write: ["tasks"]                       # reserved, no Host RPC enforces this yet
  state: true
  secrets: true

webhooks:
  - key: "slack-events"
    description: "Slack Events API webhook"
    method: "POST"                           # informational only, not enforced

config_schema:
  type: object
  properties:
    bot_token:  { type: string, secret: true, title: "Bot Token", description: "Slack bot OAuth token" }
    default_channel:  { type: string, description: "Default channel for notifications" }
    notify_on_task_created: { type: boolean, default: true }
  required: ["bot_token", "default_channel"]

ui:                                           # optional native frontend plugin
  bundle: "/ui/bundle.js"                    # root-relative
  styles: ["/ui/plugin.css"]                 # optional, root-relative

Field reference

Field Required Type Notes
id yes string Must match ^[a-z0-9][a-z0-9._-]*$ (lowercase alphanumeric, dots, underscores, hyphens; must start with a lowercase alphanumeric). Directory name under ~/.kandev/plugins/.
api_version yes int Must be exactly 1. Any other value is rejected.
version yes string Free-form; used as the version directory name (~/.kandev/plugins/<id>/<version>/).
display_name no string Shown in Settings > Plugins.
description no string Shown in Settings > Plugins.
author no string Free-form.
categories no string[] Each entry must be one of connector, automation, tools, analytics. Unknown values are rejected.
icon no string Package-relative path (e.g. icon.svg) to an image the package ships, rendered on the marketplace card and in plugin lists. The registry index-build resolves it to an absolute icon_url; for an installed plugin it is served from the extracted package. Omit it and the card falls back to a letter tile.
runtime.type conditionally string "binary" is the only supported value. Setting it (vs. leaving it empty) makes the manifest runtime-managed — see "Managed vs. legacy" below.
runtime.executables required when runtime.type: binary map<string,string> Key is <goos>-<goarch> (e.g. linux-amd64, darwin-arm64, windows-amd64); value is a clean, package-relative path under server/ (no leading /, no .. segments). At least one entry required; the running host's key must be present at install time. Windows values end in .exe.
min_kandev_version no string Optional advisory; not currently enforced by the installer.
capabilities.events no string[] Bus subjects (or wildcard patterns) this plugin subscribes to. See "Event subscription vocabulary" below.
capabilities.api_read no string[] Gates the Host data API's read-only accessors. Each entry is a resource name: tasks, sessions, workspaces, workflows, agent_profiles, repositories. Calling the matching Host accessor (e.g. Tasks()) without its resource declared returns gRPC PermissionDenied. See "Host data API resource vocabulary" below.
capabilities.api_write no string[] Reserved for future Host RPCs. Declared but not enforced by anything today — no Host RPC currently writes kandev's own data.
capabilities.state no bool Gates Host.GetState/SetState/DeleteState/ListState. Calling any of them without this set to true returns gRPC PermissionDenied.
capabilities.secrets no bool Gates Host.RevealSecret/GetSecret/SetSecret/DeleteSecret. Calling any of them without this set to true returns gRPC PermissionDenied.
webhooks[].key yes string Must be unique within the manifest. Used in the relay path POST /api/plugins/{id}/webhooks/{key}.
webhooks[].description no string Free-form.
webhooks[].method no string Informational only — kandev does not validate or enforce the inbound HTTP method against this value.
config_schema no object JSON-Schema-like object driving the settings form at Settings > Plugins > <plugin> (GET /api/plugins/{id}/config and PATCH /api/plugins/{id}). See "Config schema validation and secret fields" below.
ui.bundle no string Root-relative path (must start with /, e.g. /ui/bundle.js) to the plugin's native UI ES module, served at GET /api/plugins/{id}/bundle.
ui.styles no string[] Root-relative CSS paths (each must start with /), served at GET /api/plugins/{id}/ui/* and injected as <link> tags on load.
ui.pages no object[] Optional declarative page metadata. Secondary to ui.bundle — a native bundle registers its own routes/nav at runtime, so most plugins omit ui.pages.
ui.pages[].key yes* string Stable identifier for the page (*required when a page entry is present).
ui.pages[].title yes* string Display title.
ui.pages[].path yes* string Route path for the page.
ui.pages[].surface yes* string Where the page mounts. Enum, one of: settings · task-panel · main-nav. Any other value is a validation error.

ui.pages is declarative manifest metadata only. A native bundle's runtime nav items, icons, and per-route title-bar chrome (registerNavItem, registerRoute's options.topbar) are a separate JS SDK surface with no manifest.yaml field — see Authoring a plugin.

Managed vs. legacy manifests

Setting runtime.type: binary makes a manifest runtime-managed: kandev spawns and supervises the declared executable itself. A managed manifest must not set base_url or an endpoints block (health/events/ webhooks paths on a remote service) — those describe the old remote/operator-hosted tier, and validation rejects a managed manifest that sets them.

A manifest with an empty runtime.type still parses as a legacy remote manifest (with base_url/endpoints instead) and passes manifest.Validate() on its own — but the installer rejects it: pkgtar .Install requires manifest.IsManaged() to be true (runtime.type: binary), so a legacy manifest can never actually be installed via POST /api/plugins/install or a filesystem sideload. The remote tier is effectively removed in practice, even though the manifest schema still recognizes its shape.

Host data API resource vocabulary

capabilities.api_read gates the read-only Host data accessors (ADR 0043): each entry must be one of tasks, sessions, workspaces, workflows, agent_profiles, repositories. Declaring a resource grants the matching Host accessor (Tasks(), Sessions(), Workspaces(), Workflows(), AgentProfiles(), Repositories() — see Authoring a plugin); calling an accessor for an undeclared resource returns gRPC PermissionDenied. capabilities.api_write reserves the same resource names for a future write path — no write RPC exists yet, so declaring it currently has no effect.

Config schema validation and secret fields

config_schema is not an arbitrary, purely descriptive JSON Schema — kandev validates submitted config against a specific subset of it before persisting:

  • required (an array of property names) is enforced — a PATCH missing a required property is rejected.
  • type (string, boolean, number, or integer) is checked against the submitted value.
  • enum membership is checked when present.
  • A property with secret: true, or format: "password", is treated as a secret field and must be type: string (or untyped) — a non-string secret is rejected. Secret values are moved into kandev's encrypted vault; GET /api/plugins/{id}/config returns the literal mask "********" in their place, and resubmitting that mask unchanged is treated as "keep the stored value" rather than overwriting it with the literal string.
  • title is read by the settings-page renderer as a display label override for the property (falling back to the property name); it has no backend validation effect.

The plugin process itself always sees real, unmasked values (secrets included) via the GetConfig Host RPC — masking only applies to the operator-facing API/UI.

Event subscription vocabulary

capabilities.events entries are bus subjects (e.g. task.created) or wildcard patterns using * as a single dot-segment wildcard (e.g. task.*, agent.*, github.*). A pattern segment of * matches exactly one subject segment; every other segment must match literally; and the pattern and subject must have the same number of dot-separated segments to match at all. task.* matches task.created (2 segments each) but does not match a three-segment subject such as shell.output.<sessionId> (shell.* would not match it either — 2 vs. 3 segments).

Any subject kandev publishes on its internal event bus is a valid subscription target — this is not a closed list scoped to any one feature area. The table below groups every subject defined in internal/events/types.go by domain (some are further suffixed per-session, e.g. shell.output.<sessionId>, git.event.<sessionId> — subscribe to the literal wildcard segment count that matches, e.g. shell.output.*).

Domain Events
Tasks task.created, task.updated, task.state_changed, task.deleted, task.moved, task.tree_hold_created, task.tree_hold_released
Workspaces workspace.created, workspace.updated, workspace.deleted
Workflows workflow.created, workflow.updated, workflow.deleted
Workflow steps workflow_step.created, workflow_step.updated, workflow_step.deleted, workflow.step_completion_signaled
Comments / messages message.added, message.updated, message.deleted, message.queue.status_changed
Task sessions task_session.state_changed
Task plans task_plan.created, task_plan.updated, task_plan.deleted, task_plan.revision.created, task_plan.reverted
Task walkthroughs task_walkthrough.created, task_walkthrough.updated, task_walkthrough.deleted
Session turns turn.started, turn.completed
Repositories repository.created, repository.updated, repository.deleted, repository.script.created, repository.script.updated, repository.script.deleted
Executors executor.created, executor.updated, executor.deleted, executor.profile.created, executor.profile.updated, executor.profile.deleted, executor.prepare.progress, executor.prepare.completed
Users user.settings.updated
System system.job.update
Environments environment.created, environment.updated, environment.deleted
Agent profiles agent_profile.created, agent_profile.updated, agent_profile.deleted
Agents agent.started, agent.running, agent.boot_ready, agent.ready, agent.completed, agent.failed, agent.stopped, agent.context_reset, agent.acp_session_created, agentctl.starting, agentctl.ready, agentctl.error
Agent stream agent.stream (per-session: agent.stream.<sessionId>), agent.turn.message_saved
Agent prompts permission_request.received (per-session: permission_request.received.<sessionId>)
Clarification clarification.answered, clarification.primary_answered, clarification.cancelled, clarification.stale_dismissed
Git / workspace status git.event (per-session), git.ws (per-session), file.change.notified (per-session)
Shell I/O shell.output (per-session), shell.exit (per-session)
Dev server I/O process.output (per-session), process.status (per-session)
Session context context_window.updated (per-session), available_commands.updated (per-session), session_mode.changed (per-session), agent_capabilities.updated (per-session), session_models.updated (per-session), session_info.updated (per-session), session_todos.updated (per-session), session_prompt_usage.updated (per-session)
Automations automation.triggered, automation.run.created
GitHub github.pr_feedback, github.pr_state_changed, github.new_pr_to_review, github.new_issue, github.task_pr.updated, github.task_ci_options.updated, github.watch.event, github.rate_limit.updated
GitLab gitlab.mr_feedback, gitlab.mr_state_changed, gitlab.new_mr_to_review, gitlab.new_issue, gitlab.task_mr.updated, gitlab.watch.event
Jira jira.new_issue
Linear linear.new_issue
Sentry sentry.new_issue
Office (autonomous agents) office.agent.created, office.agent.updated, office.agent.status_changed, office.skill.created, office.skill.updated, office.project.created, office.project.updated, office.approval.created, office.approval.resolved, office.comment.created, office.cost.recorded, office.run.queued, office.run.processed, office.run.event_appended (per-run), office.routine.triggered, office.inbox.item, office.task.status_changed, office.task.updated, office.task.decision_recorded, office.task.review_requested, office.provider.health_changed, office.route_attempt.appended, office.routing.settings_updated
Cross-plugin plugin.<plugin_id>.<name> — published by Host.EmitEvent; subscribe with plugin.<other-plugin-id>.* to react to another plugin's events.

Event list current as of 2026-07-16; regenerate from apps/backend/internal/events/types.go if this page drifts from the code.

Runtime-managed fields (do not author these)

Once installed, kandev writes several fields onto the stored record alongside the parsed manifest. These are kandev-owned runtime state, not author-supplied manifest fields — do not include them in manifest.yaml; they have no effect there and are overwritten on install:

Field Meaning
status Current lifecycle state: registered, active, error, disabled, or uninstalled.
install_path Absolute path the package was extracted to (~/.kandev/plugins/<id>/<version>/).
signed Whether the package's checksums.txt.sig was cryptographically verified. Signature verification is not currently wired, so this is always false today (every package is reported unsigned).
installed_at Install timestamp.
restart_count Best-effort restart bookkeeping used by the supervision loop.

Related: Plugins, Authoring a plugin.