docs(plugins): improve plugin authoring guidance (#2164)

This commit is contained in:
Carlos Florêncio
2026-08-02 21:39:02 +01:00
committed by GitHub
parent 3c7ae1ab9a
commit e68d3f1d8f
19 changed files with 1017 additions and 821 deletions
+65 -8
View File
@@ -9,6 +9,12 @@ Build Kandev runtime plugins from the official template and current public
contracts. Keep plugin source outside the Kandev monorepo and prove the packaged contracts. Keep plugin source outside the Kandev monorepo and prove the packaged
artifact against a disposable development instance before publishing it. artifact against a disposable development instance before publishing it.
Start with the [canonical plugin authoring guide](../../../docs/public/plugins-authoring.md).
Use this skill for the repository workflow after choosing a recipe there:
choose recipe → edit manifest → implement → validate → package → smoke test.
The guide owns the complete hook/Host matrix; this skill keeps the repository
and verification procedure concise.
## Establish The Boundary ## Establish The Boundary
1. Invoke this skill only when the user intends to create, change, fix, test, 1. Invoke this skill only when the user intends to create, change, fix, test,
@@ -95,7 +101,7 @@ Choose the narrowest surface that satisfies the behavior:
| Store small structured data | Host state | Values are JSON objects keyed by scope and key; there is no transaction or compare-and-swap API. | | Store small structured data | Host state | Values are JSON objects keyed by scope and key; there is no transaction or compare-and-swap API. |
| Store files or use a plugin-managed database | `KANDEV_PLUGIN_DATA_DIR` | The plugin owns schema, locking, migrations, and recovery. | | Store files or use a plugin-managed database | `KANDEV_PLUGIN_DATA_DIR` | The plugin owns schema, locking, migrations, and recovery. |
| Read Kandev entities | Typed Host readers plus `api_read` | Use opaque pagination cursors and stable SDK DTOs; never query Kandev's database or internal HTTP API. | | Read Kandev entities | Typed Host readers plus `api_read` | Use opaque pagination cursors and stable SDK DTOs; never query Kandev's database or internal HTTP API. |
| Mutate Kandev entities | Native host UI commands where available | `api_write` is reserved and SDK write methods are not implemented. A missing mutation requires a separate Host API change. | | Mutate Kandev entities | Typed Host writers | `api_write:tasks` gates `Tasks().Create/Update`; `api_write:messages` gates `Messages().Send`. A missing mutation requires a separate Host API change. |
| Notify another plugin | `Host.EmitEvent` | Events are published as `plugin.<id>.<name>`; keep names and payloads versionable. | | Notify another plugin | `Host.EmitEvent` | Events are published as `plugin.<id>.<name>`; keep names and payloads versionable. |
| Add native interface | UI registry and `host.ui` | Use host-owned React and components so themes, contexts, portals, and mobile behavior remain compatible. | | Add native interface | UI registry and `host.ui` | Use host-owned React and components so themes, contexts, portals, and mobile behavior remain compatible. |
@@ -104,7 +110,7 @@ Choose the narrowest surface that satisfies the behavior:
Read these sources before designing the plugin: Read these sources before designing the plugin:
1. `docs/public/plugins-authoring.md` for the supported backend, Host API, 1. `docs/public/plugins-authoring.md` for the supported backend, Host API,
native UI, packaging, install, and iteration workflow. native UI, recipes, packaging, install, and iteration workflow.
2. `docs/public/plugins-manifest.md` for the authoritative manifest fields, 2. `docs/public/plugins-manifest.md` for the authoritative manifest fields,
capability gates, and event vocabulary. capability gates, and event vocabulary.
3. `docs/public/plugins-marketplace.md` when publishing or updating a catalog 3. `docs/public/plugins-marketplace.md` when publishing or updating a catalog
@@ -112,10 +118,19 @@ Read these sources before designing the plugin:
4. The current `kdlbs/kandev-plugin-template` repository, including its 4. The current `kdlbs/kandev-plugin-template` repository, including its
`README.md`, `Makefile`, tests, and release workflow. `README.md`, `Makefile`, tests, and release workflow.
Prefer the public authoring docs and current template over old examples or Prefer the public authoring docs and current template over old examples. The
implementation plans. Read `docs/plans/plugins/GRPC-CONTRACT.md` and frontend contract pair is `docs/plans/plugins/PLUGIN-API.md` plus
`docs/plans/plugins/PLUGIN-API.md` only when changing the host contract or when `apps/web/lib/plugins/types.ts`; concrete UI exports are in
the public docs do not answer a low-level compatibility question. `apps/web/lib/plugins/host-api.ts`. The backend contract is
`apps/backend/pkg/pluginsdk` plus `apps/backend/proto/kandev/plugin/v1/plugin.proto`.
Read `docs/plans/plugins/GRPC-CONTRACT.md` when changing the wire contract or
when the public docs do not answer a low-level compatibility question.
The current frontend branch does not expose `registerTaskPanel`,
`registerTaskMenuAction`, `host.storage`, `RichTextEditor`,
`RichTextReadOnly`, or a Kanban-card injection hook. Use the supported slots,
routes, Host state, and shared store documented in the guide; do not invent
future signatures.
When debugging a contract discrepancy, verify it at the implementation When debugging a contract discrepancy, verify it at the implementation
boundary: manifest and package rules live under boundary: manifest and package rules live under
@@ -123,6 +138,42 @@ boundary: manifest and package rules live under
and delivery behavior live under `apps/backend/internal/plugins`; native UI and delivery behavior live under `apps/backend/internal/plugins`; native UI
loading and registration live under `apps/web/lib/plugins`. loading and registration live under `apps/web/lib/plugins`.
## When Adding Or Changing A Hook
Treat a new hook, Host method, capability, manifest field, or mounted UI slot as
a contract change. Do not implement the runtime surface and leave author docs
for later. In the same change:
1. Update the implementation boundary and its focused tests.
2. Update the authoritative contract source:
- frontend: `docs/plans/plugins/PLUGIN-API.md` plus
`apps/web/lib/plugins/types.ts`; update `host-api.ts`, `registry.ts`, or
the mounted component when the concrete surface changes;
- backend: `apps/backend/pkg/pluginsdk`,
`apps/backend/proto/kandev/plugin/v1/plugin.proto`, and the manifest model
or validator when applicable; update `docs/plans/plugins/GRPC-CONTRACT.md`
for wire-level changes.
3. Update `docs/public/plugins-authoring.md` in the same change: add the hook to
the frontend or backend matrix, document inputs/props, capability and
lifecycle/cleanup behavior, and add a copy-pasteable recipe or maintained
fixture link. Remove it from the unavailable-API list when it becomes real.
4. Update `docs/public/plugins-manifest.md` for capability/manifest changes and
update `docs/public/plugins.md`, `docs/plugins-example.md`, or the relevant
ADR when their claims or links change. Keep the public guide as a summary;
never create a second schema or type definition in prose.
5. Recheck the root/backend/web `AGENTS.md` authority pointers and this skill if
the source-of-truth locations or author workflow changed.
6. Run the focused implementation tests plus
`node --test scripts/validate-public-docs.test.mjs`,
`node scripts/validate-public-docs.mjs`, and a stale-reference search. Report
the exact commands and results.
If a proposed hook is not implemented on the current branch, document it as
unavailable with the nearest supported recipe; do not publish a speculative
signature. If the hook is implemented but the matrix, recipe, fixture, or
authoritative contract is missing, the plugin change is not documentation
complete.
## Create A New Repository ## Create A New Repository
Skip this section for changes to an existing plugin after resolving its owning Skip this section for changes to an existing plugin after resolving its owning
@@ -188,8 +239,8 @@ request. Do not silently substitute a directory in the Kandev monorepo.
Run the plugin repository's own formatting, tests, and lint commands first, Run the plugin repository's own formatting, tests, and lint commands first,
then verify the artifact rather than only the source tree: then verify the artifact rather than only the source tree:
1. Build a host-only package with the template's `make package-host` target for 1. Run the plugin repository's tests, vet/lint, and build. Build a host-only
the local loop. package with the template's `make package-host` target for the local loop.
2. Confirm the archive contains `manifest.yaml`, the current host executable, 2. Confirm the archive contains `manifest.yaml`, the current host executable,
optional UI assets, and the generated internal `checksums.txt`. Never author optional UI assets, and the generated internal `checksums.txt`. Never author
the internal checksum file by hand. the internal checksum file by hand.
@@ -210,6 +261,12 @@ then verify the artifact rather than only the source tree:
7. Before release, run the repository's full cross-platform package workflow 7. Before release, run the repository's full cross-platform package workflow
and confirm its release asset name is `<id>-<version>.tar.gz`. and confirm its release asset name is `<id>-<version>.tar.gz`.
There is no standalone exhaustive package checker in this branch. `plugin-pack`
stages files and generates checksums; install-time `pkgtar.Install` validates the
manifest, archive safety, checksums, managed runtime, and host executable. These
checks do not execute plugin code or prove browser/module behavior, so the
disposable-instance smoke test remains required.
Do not test with a developer's primary instance, database, or credentials. Do not test with a developer's primary instance, database, or credentials.
Report commands run, artifact name, host platform tested, and any platform or Report commands run, artifact name, host platform tested, and any platform or
integration path not exercised. integration path not exercised.
+3 -1
View File
@@ -183,7 +183,9 @@ Jira and Linear are the model (per-workspace credentials, 90s auth-health poller
### Kandev plugins ### Kandev plugins
Production Kandev plugins live in dedicated repositories, not in this monorepo. Official plugins use public `kdlbs/kandev-plugin-<slug>` repositories and start from [`kdlbs/kandev-plugin-template`](https://github.com/kdlbs/kandev-plugin-template). Use `/create-kandev-plugin` for plugin creation, modification, bug fixes, packaging, release, and marketplace work. When a Kandev Worktree task needs the plugin repository, attach it with `add_branch_to_task_kandev` instead of cloning inside this worktree. Keep host API, SDK, loader, registry, and the in-tree test fixture in this repository. Production Kandev plugins live in dedicated repositories, not in this monorepo. Start at the [canonical plugin authoring guide](docs/public/plugins-authoring.md), then use `/create-kandev-plugin` for plugin creation, modification, bug fixes, packaging, release, and marketplace work. Official plugins use public `kdlbs/kandev-plugin-<slug>` repositories and start from [`kdlbs/kandev-plugin-template`](https://github.com/kdlbs/kandev-plugin-template). The recommended workflow is: choose recipe → edit manifest → implement → validate → package → smoke test. When the user explicitly requests a persistent Kandev task, attach the plugin repository through that task's workflow; otherwise do not use Kandev task/session APIs as repository-work mechanisms.
Contract authority is intentionally split by implementation boundary: frontend authors use [`docs/plans/plugins/PLUGIN-API.md`](docs/plans/plugins/PLUGIN-API.md) plus [`apps/web/lib/plugins/types.ts`](apps/web/lib/plugins/types.ts), with concrete Host UI exports in `apps/web/lib/plugins/host-api.ts`; backend authors use `apps/backend/pkg/pluginsdk`, the wire contract in `apps/backend/proto/kandev/plugin/v1/plugin.proto`, and manifest/package rules in `apps/backend/internal/plugins/manifest` and `apps/backend/internal/plugins/pkgtar`. The in-tree fixture under `apps/backend/cmd/plugin-fixture` is test support, not a production plugin starter.
### Runtime profiles (prod / dev / e2e) ### Runtime profiles (prod / dev / e2e)
+1 -1
View File
@@ -1,6 +1,6 @@
# Backend (Go) — architecture and conventions # Backend (Go) — architecture and conventions
Scoped guidance for `apps/backend/`. Repo-wide rules (commit format, code-quality limits, etc.) live in the root `AGENTS.md`. Scoped guidance for `apps/backend/`. Repo-wide rules (commit format, code-quality limits, etc.) live in the root `AGENTS.md`. For plugin work, start at the [canonical plugin authoring guide](../../docs/public/plugins-authoring.md), follow choose recipe → edit `manifest.yaml` → implement → validate → package → smoke test, and treat `pkg/pluginsdk`, `proto/kandev/plugin/v1/plugin.proto`, `internal/plugins/manifest`, and `internal/plugins/pkgtar` as authoritative; the fixture is test support, and plugins must not access databases or `internal/...` packages.
## Package Structure ## Package Structure
@@ -6,7 +6,7 @@
* registers a nav item, a top-level route, a `task-sidebar` slot component, * registers a nav item, a top-level route, a `task-sidebar` slot component,
* a `main-top-bar` slot component, a `task.created` WS handler, and the * a `main-top-bar` slot component, a `task.created` WS handler, and the
* `open-demo` keybinding (declared in manifest.yaml's `ui.keybindings`, * `open-demo` keybinding (declared in manifest.yaml's `ui.keybindings`,
* default `mod+shift+k`) which opens a `host.openModal(...)` demo modal. * default `mod+shift+j`) which opens a `host.openModal(...)` demo modal.
* Uses only host.React/host.jsx. * Uses only host.React/host.jsx.
* *
* The task-created counter lives in module scope (not component state) with * The task-created counter lives in module scope (not component state) with
+2 -2
View File
@@ -152,8 +152,8 @@ type writeProbeRecord struct {
// snapshotWriteProbeBestEffort exercises the write RPCs end to end: Host // snapshotWriteProbeBestEffort exercises the write RPCs end to end: Host
// CreateTask then, on success, Host SendMessage to the new task, writing the // CreateTask then, on success, Host SendMessage to the new task, writing the
// result (task id, dispatch status, or the error) to write-probe.json. // result (task id, dispatch status, or the error) to write-probe.json.
// Best-effort — the Host may deny the write when the fixture's manifest lacks // Best-effort — the fixture manifest exercises the current api_write contract;
// api_write, which is itself useful evidence. // any permission or service error is still recorded as useful evidence.
func (p *fixturePlugin) snapshotWriteProbeBestEffort(ctx context.Context) { func (p *fixturePlugin) snapshotWriteProbeBestEffort(ctx context.Context) {
host := p.Host() host := p.Host()
if host == nil { if host == nil {
+4
View File
@@ -2,6 +2,10 @@
Scoped guidance for `apps/web/`. Repo-wide rules (commit format, code-quality limits, etc.) live in the root `AGENTS.md`. Scoped guidance for `apps/web/`. Repo-wide rules (commit format, code-quality limits, etc.) live in the root `AGENTS.md`.
## Plugin authoring
For plugin UI work, begin with the [canonical plugin authoring guide](../../docs/public/plugins-authoring.md). Follow: choose recipe → edit `manifest.yaml` → implement → validate → package → smoke test. The frontend contract pair is `../../docs/plans/plugins/PLUGIN-API.md` plus `lib/plugins/types.ts`; concrete shared Host UI exports are in `lib/plugins/host-api.ts`, and registration/cleanup behavior is in `lib/plugins/registry.ts` and `lib/plugins/host.ts`. Keep the guide and that contract pair synchronized; do not invent hooks such as task panels, task-menu actions, per-user `host.storage`, rich-text components, or Kanban-card injection when they are absent from the current source.
## UI Components ## UI Components
**Shadcn Components:** Import from `@kandev/ui` package: **Shadcn Components:** Import from `@kandev/ui` package:
+1 -1
View File
@@ -71,7 +71,7 @@ export interface PluginRouteOptions {
/** /**
* Named slot the host renders via `<PluginSlot name .../>`. Initial slots: * Named slot the host renders via `<PluginSlot name .../>`. Initial slots:
* "task-sidebar", "settings-nav", "main-nav-footer", "chat-input-actions" * "task-sidebar", "settings-nav", "chat-input-actions"
* (icon buttons in the chat composer toolbar, beside the model picker / mic / * (icon buttons in the chat composer toolbar, beside the model picker / mic /
* send — receives `{ taskId, taskTitle, activeSessionId, sessionIds }` as * send — receives `{ taskId, taskTitle, activeSessionId, sessionIds }` as
* `slotProps`), "chat-top-bar" (status in the session top bar, beside the * `slotProps`), "chat-top-bar" (status in the session top bar, beside the
@@ -0,0 +1,123 @@
---
spec: docs/specs/plugins/authoring-experience.md
created: 2026-08-02
status: complete
---
# Implementation Plan: Plugin Authoring Experience
## Overview
Make the existing public authoring page the single discovery entry point,
reconcile nearby documentation against the current frontend and backend
contracts, and route agents there from scoped guidance and the existing plugin
skill. The change is documentation-first and adds no plugin runtime or
validation code.
## Canonical developer guide
Rewrite `docs/public/plugins-authoring.md` around the author workflow while
keeping its stable public slug:
1. lifecycle diagram and package/data layout;
2. plugin shapes plus security/capability boundaries;
3. storage decision table;
4. complete current frontend and backend matrices linked to authoritative
source;
5. six current-contract recipes;
6. exact build/test/package/install/smoke sequence and validation scope;
7. common mistakes and explicit unavailable-surface notes.
The guide will use small copy-pasteable snippets and link larger examples to the
in-tree fixture and official template. Broken references to absent example
repositories will be removed.
The workflow will document existing validation honestly:
- plugin repository formatting, unit tests, vet/lint, and builds;
- `plugin-pack` package creation and generated `checksums.txt`;
- package/archive/manifest/host-executable checks performed by installation;
- `make -C apps/backend e2e-plugin-package` and the focused `plugin-pack` test
as the maintained in-tree package smoke validation;
- install and capability smoke tests in a disposable development instance.
It will state that there is no standalone exhaustive package checker in this
branch and distinguish build-time, pack-time, install-time, and runtime checks.
## Contract and example reconciliation
Update the public plugin overview, manifest guide, marketplace cross-links,
`docs/plugins-example.md`, the frozen plugin contract notes, and plugin spec
where they contradict the current implementation. Key corrections are:
- implemented Host task/message writes are not described as reserved;
- `ui.bundle` examples include the required leading slash;
- only actually mounted component slots are listed as supported;
- missing example repositories are not presented as maintained code;
- unsupported task-panel, task-menu, per-user storage, rich-text, and Kanban
card APIs are identified as unavailable rather than assigned speculative
signatures.
Prefer documentation edits. Change source files only if a stale documentation
comment inside a fixture or type file would otherwise remain directly
misleading; do not change runtime types, behavior, schemas, or tests.
## Agent and contributor discoverability
Update `AGENTS.md`, `apps/backend/AGENTS.md`, and `apps/web/AGENTS.md` with a
direct canonical-guide link, the authority map appropriate to each scope, and
the workflow: choose recipe → edit manifest → implement → validate → package →
smoke test.
Update `.agents/skills/create-kandev-plugin/SKILL.md` so agents read the same
canonical guide and contract map, use implemented Host writes correctly, and
follow the documented validation boundaries. Keep the skill procedural and the
public page explanatory; do not duplicate full matrices in the skill.
## Tests and validation
- Public-doc structure and local links:
`node --test scripts/validate-public-docs.test.mjs` and
`node scripts/validate-public-docs.mjs`.
- Backend package/example evidence:
`make -C apps/backend e2e-plugin-package` and focused tests for
`cmd/plugin-pack`, `cmd/plugin-fixture`, `internal/plugins/manifest`,
`internal/plugins/pkgtar`, and `pkg/pluginsdk`.
- Frontend contract/lifecycle evidence: focused plugin host, API, registry, and
WebSocket bridge tests.
- Harness/doc hygiene: targeted stale-reference searches plus
`git diff --check`.
No new browser E2E is planned because no product UI behavior changes.
## Implementation waves
Wave 1:
- [x] [task-01-canonical-guide](task-01-canonical-guide.md)
Wave 2:
- [x] [task-02-contract-reconciliation](task-02-contract-reconciliation.md)
Wave 3:
- [x] [task-03-agent-discoverability](task-03-agent-discoverability.md)
## Risks
- A summary matrix can drift into a competing contract. Keep signatures
abbreviated, link every section to authoritative files, and avoid copied DTO
field inventories.
- The external template can drift independently. Treat it as scaffold/build
workflow, not as the API source of truth.
- Install-time checks must not be described as a standalone authoring validator;
explicitly state which command checks which layer.
- The system is still experimental according to public coverage evidence, so
wording must not imply a stability promotion.
## Verification results
- `node --test scripts/validate-public-docs.test.mjs` — 58 passed.
- `node scripts/validate-public-docs.mjs` — validated 41 published docs pages.
- `(cd apps/backend && go test ./cmd/plugin-pack ./cmd/plugin-fixture ./internal/plugins/manifest ./internal/plugins/pkgtar ./pkg/pluginsdk)` — 167 passed in 5 packages.
- `make -C apps/backend e2e-plugin-package` — produced `.build/kandev-plugin-e2e-1.0.0.tar.gz` for linux-amd64.
- `(cd apps && pnpm --filter @kandev/web test -- lib/plugins/host-api.test.ts lib/plugins/host.test.ts lib/plugins/registry.test.ts lib/ws/plugin-bridge.test.ts)` — 4 files and 50 tests passed. Happy-DOM emitted expected external-stylesheet warnings.
- `git diff --check` — passed.
@@ -0,0 +1,53 @@
---
id: "01-canonical-guide"
title: "Canonical plugin guide"
status: completed
wave: 1
depends_on: []
plan: "plan.md"
spec: "../../specs/plugins/authoring-experience.md"
---
# Task 01: Canonical plugin guide
## Acceptance
1. `docs/public/plugins-authoring.md` contains the lifecycle, layouts, plugin
shapes, security/capabilities, storage table, complete current matrices, six
recipes, exact validation/package/test workflow, and common mistakes.
2. The guide links to authoritative source rather than redefining it, identifies
unavailable APIs, and references only real maintained files/repositories.
3. The workflow clearly separates source checks, packaging/checksum generation,
install-time validation, and disposable-instance smoke tests.
## Verification
```sh
node --test scripts/validate-public-docs.test.mjs
node scripts/validate-public-docs.mjs
rg -n "registerTaskPanel|registerTaskMenuAction|host.storage|RichText(Editor|ReadOnly)|Kanban" docs/public/plugins-authoring.md
rg -n "kandev-plugin-(hello|github)" docs/public/plugins*.md docs/plugins-example.md
```
The searches are audits: future-surface matches must be explicit
unavailability notes, and missing-repository matches should be absent.
## Files likely touched
- `docs/public/plugins-authoring.md`
- `docs/public/plugins.md`
- `docs/public/plugins-manifest.md`
- `docs/public/plugins-marketplace.md`
- `docs/public/extending-kandev.md`
- `docs/public/coverage.json` only if required for navigation/evidence, without
changing experimental status
## Results
- Rewrote the canonical public guide with lifecycle, package/data layout,
current frontend/backend matrices, six recipes, layered validation workflow,
and common mistakes.
- Removed references to absent external example repositories and kept the
plugin status experimental.
- `node --test scripts/validate-public-docs.test.mjs` — 58 passed.
- `node scripts/validate-public-docs.mjs` — validated 41 published docs pages.
@@ -0,0 +1,45 @@
---
id: "02-contract-reconciliation"
title: "Plugin contract reconciliation"
status: completed
wave: 2
depends_on: ["01-canonical-guide"]
plan: "plan.md"
spec: "../../specs/plugins/authoring-experience.md"
---
# Task 02: Plugin contract reconciliation
## Acceptance
1. Frozen contract notes, the plugin spec, and example docs agree with source on
Host writes, UI path rules, mounted slots, and authoritative ownership.
2. `docs/plugins-example.md` and the in-tree fixture references describe only
real current examples and commands.
3. Runtime source is unchanged except for optional comment-only corrections
that directly prevent a maintained example from lying.
## Verification
```sh
(cd apps/backend && go test ./cmd/plugin-pack ./cmd/plugin-fixture ./internal/plugins/manifest ./internal/plugins/pkgtar ./pkg/pluginsdk)
make -C apps/backend e2e-plugin-package
(cd apps && pnpm --filter @kandev/web test -- lib/plugins/host-api.test.ts lib/plugins/host.test.ts lib/plugins/registry.test.ts lib/ws/plugin-bridge.test.ts)
node scripts/validate-public-docs.mjs
```
## Files likely touched
- `docs/plans/plugins/PLUGIN-API.md`
- `docs/plans/plugins/GRPC-CONTRACT.md`
- `docs/specs/plugins/spec.md`
- `docs/plugins-example.md`
- maintained fixture/type comments only if necessary
## Results
- Reconciled `api_write` task/message writes, root-relative UI paths, mounted
slots, fixture comments, and example documentation against current source.
- `(cd apps/backend && go test ./cmd/plugin-pack ./cmd/plugin-fixture ./internal/plugins/manifest ./internal/plugins/pkgtar ./pkg/pluginsdk)` — 167 passed in 5 packages.
- `make -C apps/backend e2e-plugin-package` — produced the linux-amd64 fixture archive.
- Frontend-focused plugin tests passed with 50 tests across 4 files.
@@ -0,0 +1,50 @@
---
id: "03-agent-discoverability"
title: "Agent plugin discoverability"
status: completed
wave: 3
depends_on: ["02-contract-reconciliation"]
plan: "plan.md"
spec: "../../specs/plugins/authoring-experience.md"
---
# Task 03: Agent plugin discoverability
## Acceptance
1. Root, backend, and web `AGENTS.md` each link directly to the canonical guide
and identify the contract authorities relevant to their scope.
2. The existing `create-kandev-plugin` skill follows the canonical guide,
reflects implemented Host writes, and sends agents through choose recipe →
edit manifest → implement → validate → package → smoke test.
3. All focused documentation, harness, backend package, and frontend plugin-host
checks pass, with exact results recorded in the plan.
## Verification
```sh
rg -n "docs/public/plugins-authoring.md|PLUGIN-API.md|apps/web/lib/plugins/types.ts|pkg/pluginsdk|plugin.proto|internal/plugins/manifest" AGENTS.md apps/backend/AGENTS.md apps/web/AGENTS.md .agents/skills/create-kandev-plugin/SKILL.md
node --test scripts/validate-public-docs.test.mjs
node scripts/validate-public-docs.mjs
make -C apps/backend e2e-plugin-package
(cd apps/backend && go test ./cmd/plugin-pack ./cmd/plugin-fixture ./internal/plugins/manifest ./internal/plugins/pkgtar ./pkg/pluginsdk)
(cd apps && pnpm --filter @kandev/web test -- lib/plugins/host-api.test.ts lib/plugins/host.test.ts lib/plugins/registry.test.ts lib/ws/plugin-bridge.test.ts)
git diff --check
```
## Files likely touched
- `AGENTS.md`
- `apps/backend/AGENTS.md`
- `apps/web/AGENTS.md`
- `.agents/skills/create-kandev-plugin/SKILL.md`
- this plan and its task files
## Results
- Root, backend, and web AGENTS now link directly to the canonical guide and
identify the frontend pair, backend SDK, proto, manifest, and package
authorities.
- `create-kandev-plugin` now routes through the same guide, current Host write
behavior, unavailable-API notes, and layered validation scope.
- Stale-reference audit and `git diff --check` passed.
+1 -1
View File
@@ -140,7 +140,7 @@ interface PluginRegistry {
// Named slot injection. Host renders all components registered for a slot via // Named slot injection. Host renders all components registered for a slot via
// <PluginSlot name="..." slotProps={...}/>. Initial slots: "task-sidebar", // <PluginSlot name="..." slotProps={...}/>. Initial slots: "task-sidebar",
// "settings-nav", "main-nav-footer", "chat-input-actions", "chat-top-bar", // "settings-nav", "chat-input-actions", "chat-top-bar",
// "main-top-bar", "app-status-bar-left", "app-status-bar-right", and // "main-top-bar", "app-status-bar-left", "app-status-bar-right", and
// "plugin-settings". // "plugin-settings".
// "chat-input-actions" renders icon buttons in the chat composer toolbar // "chat-input-actions" renders icon buttons in the chat composer toolbar
+13 -12
View File
@@ -1,13 +1,14 @@
# Example plugin # Plugin examples
`kandev-plugin-hello` is a standalone, importable reference implementation of The canonical authoring guide is [Authoring a Plugin](public/plugins-authoring.md).
the kandev native-UI plugin contract (manifest + Go SDK backend, spawned by It documents the current frontend hooks, backend Host surfaces, six recipes,
kandev as a gRPC subprocess via `pkg/pluginsdk` + hand-written, no-build ES package layout, and the reproducible build/package/install/test workflow.
module UI bundle) — it registers a "Hello" nav item and route, a
task-sidebar slot widget, a live WebSocket-driven counter, and a backend For a maintained scaffold, start from
tool/event/webhook, and is meant to be copied as the starting point for a [`kdlbs/kandev-plugin-template`](https://github.com/kdlbs/kandev-plugin-template).
new plugin. It lives in its own git repository, sibling to this one, at For an in-tree package smoke test, use the fixture and commands in
`../kandev-plugin-hello/` (see that repo's `README.md` for how to build, `apps/backend/cmd/plugin-fixture` and `make -C apps/backend e2e-plugin-package`.
package, and install it against a running kandev instance); see
`docs/plans/plugins/GRPC-CONTRACT.md` for the backend/transport contract and The fixture is test support, not a production plugin repository. Do not rely on
`docs/plans/plugins/PLUGIN-API.md` for the frontend contract it implements. old external example links that are not present in the maintained source
catalog; use the template and fixture paths above.
File diff suppressed because it is too large Load Diff
+12 -9
View File
@@ -46,7 +46,7 @@ min_kandev_version: "0.78.0" # optional
capabilities: capabilities:
events: ["task.created", "task.state_changed", "agent.completed"] events: ["task.created", "task.state_changed", "agent.completed"]
api_read: ["tasks", "agent_profiles"] # gates the Host data-reader RPCs api_read: ["tasks", "agent_profiles"] # gates the Host data-reader RPCs
api_write: ["tasks"] # reserved, no Host RPC enforces this yet api_write: ["tasks"] # gates Tasks().Create/Update
state: true state: true
secrets: true secrets: true
agent_invoke: true # gates Host.InvokeUtilityAgent agent_invoke: true # gates Host.InvokeUtilityAgent
@@ -103,7 +103,7 @@ ui: # optional native frontend plugin
| `min_kandev_version` | no | string | Optional advisory; not currently enforced by the installer. | | `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.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`, `messages`, `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_read` | no | string[] | Gates the Host data API's read-only accessors. Each entry is a resource name: `tasks`, `sessions`, `messages`, `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.api_write` | no | string[] | Gates Host data writes. Current resources are `tasks` (`Tasks().Create/Update`) and `messages` (`Messages().Send`); reads and writes are gated independently. |
| `capabilities.state` | no | bool | Gates `Host.GetState`/`SetState`/`DeleteState`/`ListState`. Calling any of them without this set to `true` returns gRPC `PermissionDenied`. | | `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`. | | `capabilities.secrets` | no | bool | Gates `Host.RevealSecret`/`GetSecret`/`SetSecret`/`DeleteSecret`. Calling any of them without this set to `true` returns gRPC `PermissionDenied`. |
| `capabilities.agent_invoke` | no | bool | Gates `Host.InvokeUtilityAgent` — a one-shot completion run by the utility agent selected for this plugin. Declare a `utility_agent` config property with `type: string` and `format: utility-agent`; Settings > Plugins renders the picker. Calling without this capability returns gRPC `PermissionDenied`; calling without a valid enabled selection returns gRPC `FailedPrecondition`. See ADR 0048. | | `capabilities.agent_invoke` | no | bool | Gates `Host.InvokeUtilityAgent` — a one-shot completion run by the utility agent selected for this plugin. Declare a `utility_agent` config property with `type: string` and `format: utility-agent`; Settings > Plugins renders the picker. Calling without this capability returns gRPC `PermissionDenied`; calling without a valid enabled selection returns gRPC `FailedPrecondition`. See ADR 0048. |
@@ -115,19 +115,20 @@ ui: # optional native frontend plugin
| `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. | | `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.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.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` | no | object[] | Optional declarative metadata accepted by the manifest model. The current frontend does not render these entries; a native bundle registers its supported routes/nav/slots 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[].key` | yes* | string | Stable identifier for the page (*required when a page entry is present). |
| `ui.pages[].title` | yes* | string | Display title. | | `ui.pages[].title` | yes* | string | Display title. |
| `ui.pages[].path` | yes* | string | Route path for the page. | | `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[].surface` | yes* | string | Metadata enum (`settings` · `task-panel` · `main-nav`) validated by the manifest parser. It is not a current frontend mount; use the registry hooks in the authoring guide. |
| `ui.keybindings` | no | object[] | Declares plugin keybindings bound at runtime via `registerKeybinding`. Requires `ui.bundle`. | | `ui.keybindings` | no | object[] | Declares plugin keybindings bound at runtime via `registerKeybinding`. Requires `ui.bundle`. |
| `ui.keybindings[].id` | yes* | string | Stable, plugin-local slug (*required when a keybinding entry is present). Must match `^[a-z0-9][a-z0-9-]*$` and be unique within this plugin's own `ui.keybindings` list — not globally; the effective shortcut is namespaced `plugin:{pluginId}:{id}`. | | `ui.keybindings[].id` | yes* | string | Stable, plugin-local slug (*required when a keybinding entry is present). Must match `^[a-z0-9][a-z0-9-]*$` and be unique within this plugin's own `ui.keybindings` list — not globally; the effective shortcut is namespaced `plugin:{pluginId}:{id}`. |
| `ui.keybindings[].default` | yes* | string | Default combo string. `+`-separated tokens: zero or more modifiers from `mod`, `ctrl`, `cmd`, `meta`, `alt`, `option`, `shift` (`mod` = ⌘ on macOS, Ctrl elsewhere) plus exactly one non-modifier key. `shift` may not combine with a digit or symbol key — the browser reports the shifted glyph for those keys, so the combo could never match. | | `ui.keybindings[].default` | yes* | string | Default combo string. `+`-separated tokens: zero or more modifiers from `mod`, `ctrl`, `cmd`, `meta`, `alt`, `option`, `shift` (`mod` = ⌘ on macOS, Ctrl elsewhere) plus exactly one non-modifier key. `shift` may not combine with a digit or symbol key — the browser reports the shifted glyph for those keys, so the combo could never match. |
| `ui.keybindings[].description` | yes* | string | Non-empty, human-readable label shown in **Settings > Keyboard Shortcuts**. | | `ui.keybindings[].description` | yes* | string | Non-empty, human-readable label shown in **Settings > Keyboard Shortcuts**. |
`ui.pages` is declarative manifest metadata only. A native bundle's runtime `ui.pages` is declarative manifest metadata only and is not currently rendered
nav items, icons, and per-route title-bar chrome (`registerNavItem`, by the frontend. A native bundle's runtime nav items, icons, routes, named
`registerRoute`'s `options.topbar`) are a separate JS SDK surface with no slots, and per-route title-bar chrome (`registerNavItem`, `registerRoute`, and
`registerComponent`) are the supported JS SDK surface with no corresponding
`manifest.yaml` field — see [Authoring a plugin](plugins-authoring.md). `manifest.yaml` field — see [Authoring a plugin](plugins-authoring.md).
## Managed vs. legacy manifests ## Managed vs. legacy manifests
@@ -157,8 +158,10 @@ resource grants the matching `Host` accessor (`Tasks()`, `Sessions()`,
`Messages()`, `Workspaces()`, `Workflows()`, `AgentProfiles()`, `Messages()`, `Workspaces()`, `Workflows()`, `AgentProfiles()`,
`Repositories()` — see [Authoring a plugin](plugins-authoring.md)); calling an `Repositories()` — see [Authoring a plugin](plugins-authoring.md)); calling an
accessor for an undeclared resource returns gRPC `PermissionDenied`. accessor for an undeclared resource returns gRPC `PermissionDenied`.
`capabilities.api_write` reserves the same resource names for a future write `capabilities.api_write` gates the current Host data writes: `tasks` gates
path — no write RPC exists yet, so declaring it currently has no effect. `Tasks().Create`/`Update`, and `messages` gates `Messages().Send`. A plugin may
declare a read resource without its write capability, or vice versa. Calling a
write without the matching declaration returns gRPC `PermissionDenied`.
`messages` reads historical **conversation content** (`Messages().List`): `messages` reads historical **conversation content** (`Messages().List`):
one user/agent message per row (`id`, `session_id`, `task_id`, `turn_id`, one user/agent message per row (`id`, `session_id`, `task_id`, `turn_id`,
+4 -4
View File
@@ -192,8 +192,8 @@ A plugin can also render its own UI inline on this page — at the top, above th
settings form — via the `plugin-settings` slot, for example a live settings form — via the `plugin-settings` slot, for example a live
integration-health card ("CLI installed ✅ v0.45.2", "API token ✅ integration-health card ("CLI installed ✅ v0.45.2", "API token ✅
authenticated"). This is owner-scoped, so a plugin's card only ever appears on authenticated"). This is owner-scoped, so a plugin's card only ever appears on
its own settings page. See [Named its own settings page. See [supported named
slots](plugins-authoring.md#named-slots) in the authoring guide. slots](plugins-authoring.md#supported-named-slots) in the authoring guide.
<details> <details>
<summary>Package signing, storage, and security details</summary> <summary>Package signing, storage, and security details</summary>
@@ -235,8 +235,8 @@ identically to an unsigned one; signing is not required in v1.
it declared in its manifest: `state` gates the state RPCs, `secrets` gates it declared in its manifest: `state` gates the state RPCs, `secrets` gates
the plugin-owned secret RPCs, and each read-only data accessor (tasks, the plugin-owned secret RPCs, and each read-only data accessor (tasks,
sessions, workspaces, workflows, agent profiles, repositories) is gated sessions, workspaces, workflows, agent profiles, repositories) is gated
individually via `api_read:<resource>` (write access, `api_write:<resource>`, individually via `api_read:<resource>` and `api_write:<resource>` for the
is reserved but not yet implemented). An undeclared capability returns gRPC implemented task/message Host writes. An undeclared capability returns gRPC
`PermissionDenied` with a message naming the missing capability, checked by `PermissionDenied` with a message naming the missing capability, checked by
a server interceptor before the handler runs. `GetConfig` and `EmitEvent` a server interceptor before the handler runs. `GetConfig` and `EmitEvent`
are the only ungated RPCs — a plugin can always read its own config are the only ungated RPCs — a plugin can always read its own config
+1
View File
@@ -40,6 +40,7 @@ Product-wide capabilities that are not tied to a single feature area.
| [background-work-liveness](platform/background-work-liveness.md) | shipped | | [background-work-liveness](platform/background-work-liveness.md) | shipped |
| [i18n](platform/i18n.md) | building | | [i18n](platform/i18n.md) | building |
| [plugins](plugins/spec.md) | draft | | [plugins](plugins/spec.md) | draft |
| [plugins — authoring experience](plugins/authoring-experience.md) | draft |
| [plugins — marketplace](plugins/marketplace.md) | building | | [plugins — marketplace](plugins/marketplace.md) | building |
| [semantic-notifications](platform/notifications.md) | shipped | | [semantic-notifications](platform/notifications.md) | shipped |
| [workspace-git-status](platform/workspace-git-status.md) | shipped | | [workspace-git-status](platform/workspace-git-status.md) | shipped |
+126
View File
@@ -0,0 +1,126 @@
---
status: draft
created: 2026-08-02
owner: kandev
---
# Plugin Authoring Experience
## Why
Plugin authors and coding agents cannot currently move from discovery to a
tested package without reconciling public tutorials, frozen contract notes,
frontend types, the Go SDK, the manifest implementation, packaging code, and
ADRs. Some of those references have drifted behind the shipped contract, which
makes otherwise copy-pasteable guidance unsafe.
## What
- `docs/public/plugins-authoring.md` is the canonical developer entry point and
is linked directly from the root, backend, and web `AGENTS.md` files.
- The entry point teaches one workflow: choose a supported recipe, edit
`manifest.yaml`, implement against the authoritative contract, run the
available validation, package, install, and smoke test.
- It explains the install-to-load lifecycle, package and per-plugin data layout,
backend/UI-focused/combined plugin shapes, capability boundary, and the rule
that plugins use Host APIs rather than Kandev databases or internal packages.
- A storage decision table distinguishes plugin-scoped Host state, the absence
of per-user `host.storage` in this branch, operator config and vault-backed
secrets, and files rooted under `KANDEV_PLUGIN_DATA_DIR`.
- A frontend matrix covers every registration and Host value currently exposed
by `apps/web/lib/plugins/types.ts` and `apps/web/lib/plugins/host-api.ts`. It
names every mounted component slot and records input, manifest requirement,
lifecycle/cleanup, and an example or authoritative source link.
- A backend matrix covers `Plugin.OnEvent`, `Plugin.HandleWebhook`, every
`pluginsdk.Host` method/resource accessor, the required capability, and the
relevant SDK/proto source.
- The guide explicitly says that `registerTaskPanel`, `registerTaskMenuAction`,
per-user `host.storage`, `RichTextEditor`, `RichTextReadOnly`, and direct
Kanban-card injection are not part of the current branch. It does not invent
future signatures.
- At least six current-contract recipes are runnable or link to maintained
source: a UI-focused plugin with the required no-op managed binary; a Go
backend using Host state; a `task-sidebar` contribution backed by task-scoped
Host state; a webhook receiver; an event subscriber; and a Kanban-aware route
that reads `host.store` without injecting into first-party cards.
- The reproducible workflow documents the exact checks already available in
the plugin template and monorepo, including their limits. Package creation
generates checksums; the install path performs manifest, checksum, archive,
extraction, and host-binary checks; the in-tree fixture package tests provide
the closest no-production-instance validation. This change does not add a
second validator or new runtime behavior.
- Existing plugin pages, contract notes, relevant README-style example docs,
and the `create-kandev-plugin` skill link back to the canonical entry point
instead of duplicating or contradicting the API.
- The plugin system remains marked experimental unless a separate product
stability decision changes the public status evidence.
## Authoritative contracts
This feature does not add plugin runtime hooks or Host RPCs. It documents the
current contract and points readers to:
- Frontend pair: `docs/plans/plugins/PLUGIN-API.md` and
`apps/web/lib/plugins/types.ts`.
- Concrete frontend Host exports: `apps/web/lib/plugins/host-api.ts`.
- Backend author API: `apps/backend/pkg/pluginsdk`.
- Wire API: `apps/backend/proto/kandev/plugin/v1/plugin.proto`.
- Manifest model and semantics: `apps/backend/internal/plugins/manifest`.
- Package integrity and install semantics:
`apps/backend/internal/plugins/pkgtar`.
The canonical guide summarizes those sources for discovery, but does not become
another type, proto, or schema definition.
## Permissions and storage
- Frontend bundles run in the Kandev origin and receive the current shared
React instance and curated host/store surface. Frontend registrations are not
individually capability-gated; `ui.bundle` and `ui.keybindings` declare the
relevant UI entry points.
- Host state requires `state`; secrets require `secrets`; reads require the
matching `api_read` resource; task/message writes require the matching
`api_write` resource; utility-agent invocation requires `agent_invoke`; and
event delivery requires a matching event subscription.
- `Host.GetConfig` and `Host.EmitEvent` are ungated. Webhook keys must be
declared. The external-auth response directive additionally requires `auth`.
- Plugins do not receive sanctioned direct database access. Domain reads and
writes go through typed Host RPCs and Kandev's service layer.
- Host state is plugin-scoped structured data and is not per-user browser
storage. Arbitrary plugin files belong below `KANDEV_PLUGIN_DATA_DIR`.
## Scenarios
- **GIVEN** an agent starts at a relevant `AGENTS.md`, **WHEN** it follows the
plugin-authoring link, **THEN** it reaches the canonical guide in one step and
sees the contract map and supported workflow.
- **GIVEN** a UI-focused plugin author, **WHEN** they follow the recipe, **THEN**
they include the no-op managed executable required by the current installer
and a native UI bundle without inventing unsupported backend behavior.
- **GIVEN** an author needs durable task-related state, **WHEN** they consult the
storage table and sidebar recipe, **THEN** they use task-scoped Host state and
do not mistake it for unavailable per-user `host.storage`.
- **GIVEN** a manifest declares `api_write: ["tasks"]`, **WHEN** an author reads
the capability matrix, **THEN** it documents `Tasks().Create/Update` as
implemented and links to the SDK/proto.
- **GIVEN** a package whose `ui.bundle` omits the leading slash, **WHEN** an
author follows the workflow, **THEN** the guide identifies that error before
install and explains which remaining checks happen during packaging and
install.
- **GIVEN** a disabled or uninstalled UI plugin, **WHEN** an author consults the
lifecycle matrix, **THEN** it explains `destroy`, bulk unregistration, modal
closure, stylesheet removal, and plugin-owned side-effect cleanup.
- **GIVEN** a request for a future task-panel, task-menu, per-user storage,
rich-text, or Kanban-card hook, **WHEN** an author checks the canonical page,
**THEN** it says the API is unavailable in this branch and points to the
closest supported recipe.
## Out of scope
- Adding a package checker or changing manifest, packaging, installation,
runtime supervision, capability enforcement, signing, or database behavior.
- Adding frontend hooks, per-user plugin storage, rich-text components, or
Kanban-card injection.
- Publishing or modifying external plugin repositories.
- Creating a second manifest, proto, TypeScript, or Go SDK definition in docs.
- Promoting the plugin system from experimental to stable.
+4 -4
View File
@@ -126,8 +126,8 @@ config_schema:
required: ["bot_token_secret", "default_channel"] required: ["bot_token_secret", "default_channel"]
ui: # Native frontend plugin (see "Frontend plugin runtime") ui: # Native frontend plugin (see "Frontend plugin runtime")
bundle: "ui/bundle.js" # ES module extracted alongside the package bundle: "/ui/bundle.js" # root-relative ES module path
styles: ["ui/plugin.css"] # optional stylesheets styles: ["/ui/plugin.css"] # optional root-relative stylesheets
# Runtime fields managed by kandev (not authored): # Runtime fields managed by kandev (not authored):
status: "active" status: "active"
@@ -506,8 +506,8 @@ components, and WebSocket handlers that run inside the kandev frontend (the
Mattermost-webapp model), not iframes. The full contract lives in Mattermost-webapp model), not iframes. The full contract lives in
`docs/plans/plugins/PLUGIN-API.md`; summary: `docs/plans/plugins/PLUGIN-API.md`; summary:
- **Manifest:** a plugin declares `ui.bundle` (a path inside the extracted package, - **Manifest:** a plugin declares `ui.bundle` (a root-relative path inside the
e.g. `ui/bundle.js`) and optional `ui.styles`. extracted package, e.g. `/ui/bundle.js`) and optional root-relative `ui.styles`.
- **Bundle delivery:** kandev serves the bundle at `GET /api/plugins/{id}/bundle` - **Bundle delivery:** kandev serves the bundle at `GET /api/plugins/{id}/bundle`
(and any assets under `GET /api/plugins/{id}/ui/*`) directly from the extracted (and any assets under `GET /api/plugins/{id}/ui/*`) directly from the extracted
package directory on local disk, forcing `Content-Type: text/javascript` and package directory on local disk, forcing `Content-Type: text/javascript` and