docs(plugins): improve plugin authoring guidance (#2164)
This commit is contained in:
@@ -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
|
||||
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
|
||||
|
||||
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 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. |
|
||||
| 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. |
|
||||
| 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:
|
||||
|
||||
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,
|
||||
capability gates, and event vocabulary.
|
||||
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
|
||||
`README.md`, `Makefile`, tests, and release workflow.
|
||||
|
||||
Prefer the public authoring docs and current template over old examples or
|
||||
implementation plans. Read `docs/plans/plugins/GRPC-CONTRACT.md` and
|
||||
`docs/plans/plugins/PLUGIN-API.md` only when changing the host contract or when
|
||||
the public docs do not answer a low-level compatibility question.
|
||||
Prefer the public authoring docs and current template over old examples. The
|
||||
frontend contract pair is `docs/plans/plugins/PLUGIN-API.md` plus
|
||||
`apps/web/lib/plugins/types.ts`; concrete UI exports are in
|
||||
`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
|
||||
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
|
||||
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
|
||||
|
||||
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,
|
||||
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
|
||||
the local loop.
|
||||
1. Run the plugin repository's tests, vet/lint, and build. Build a host-only
|
||||
package with the template's `make package-host` target for the local loop.
|
||||
2. Confirm the archive contains `manifest.yaml`, the current host executable,
|
||||
optional UI assets, and the generated internal `checksums.txt`. Never author
|
||||
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
|
||||
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.
|
||||
Report commands run, artifact name, host platform tested, and any platform or
|
||||
integration path not exercised.
|
||||
|
||||
@@ -183,7 +183,9 @@ Jira and Linear are the model (per-workspace credentials, 90s auth-health poller
|
||||
|
||||
### 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)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* 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
|
||||
* `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.
|
||||
*
|
||||
* The task-created counter lives in module scope (not component state) with
|
||||
|
||||
@@ -152,8 +152,8 @@ type writeProbeRecord struct {
|
||||
// snapshotWriteProbeBestEffort exercises the write RPCs end to end: Host
|
||||
// CreateTask then, on success, Host SendMessage to the new task, writing the
|
||||
// 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
|
||||
// api_write, which is itself useful evidence.
|
||||
// Best-effort — the fixture manifest exercises the current api_write contract;
|
||||
// any permission or service error is still recorded as useful evidence.
|
||||
func (p *fixturePlugin) snapshotWriteProbeBestEffort(ctx context.Context) {
|
||||
host := p.Host()
|
||||
if host == nil {
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
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
|
||||
|
||||
**Shadcn Components:** Import from `@kandev/ui` package:
|
||||
|
||||
@@ -71,7 +71,7 @@ export interface PluginRouteOptions {
|
||||
|
||||
/**
|
||||
* 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 /
|
||||
* send — receives `{ taskId, taskTitle, activeSessionId, sessionIds }` as
|
||||
* `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.
|
||||
@@ -140,7 +140,7 @@ interface PluginRegistry {
|
||||
|
||||
// Named slot injection. Host renders all components registered for a slot via
|
||||
// <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
|
||||
// "plugin-settings".
|
||||
// "chat-input-actions" renders icon buttons in the chat composer toolbar
|
||||
|
||||
+13
-12
@@ -1,13 +1,14 @@
|
||||
# Example plugin
|
||||
# Plugin examples
|
||||
|
||||
`kandev-plugin-hello` is a standalone, importable reference implementation of
|
||||
the kandev native-UI plugin contract (manifest + Go SDK backend, spawned by
|
||||
kandev as a gRPC subprocess via `pkg/pluginsdk` + hand-written, no-build ES
|
||||
module UI bundle) — it registers a "Hello" nav item and route, a
|
||||
task-sidebar slot widget, a live WebSocket-driven counter, and a backend
|
||||
tool/event/webhook, and is meant to be copied as the starting point for a
|
||||
new plugin. It lives in its own git repository, sibling to this one, at
|
||||
`../kandev-plugin-hello/` (see that repo's `README.md` for how to build,
|
||||
package, and install it against a running kandev instance); see
|
||||
`docs/plans/plugins/GRPC-CONTRACT.md` for the backend/transport contract and
|
||||
`docs/plans/plugins/PLUGIN-API.md` for the frontend contract it implements.
|
||||
The canonical authoring guide is [Authoring a Plugin](public/plugins-authoring.md).
|
||||
It documents the current frontend hooks, backend Host surfaces, six recipes,
|
||||
package layout, and the reproducible build/package/install/test workflow.
|
||||
|
||||
For a maintained scaffold, start from
|
||||
[`kdlbs/kandev-plugin-template`](https://github.com/kdlbs/kandev-plugin-template).
|
||||
For an in-tree package smoke test, use the fixture and commands in
|
||||
`apps/backend/cmd/plugin-fixture` and `make -C apps/backend e2e-plugin-package`.
|
||||
|
||||
The fixture is test support, not a production plugin repository. Do not rely on
|
||||
old external example links that are not present in the maintained source
|
||||
catalog; use the template and fixture paths above.
|
||||
|
||||
+508
-777
File diff suppressed because it is too large
Load Diff
@@ -46,7 +46,7 @@ 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
|
||||
api_write: ["tasks"] # gates Tasks().Create/Update
|
||||
state: true
|
||||
secrets: true
|
||||
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. |
|
||||
| `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_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.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. |
|
||||
@@ -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. |
|
||||
| `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` | 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[].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[].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[].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[].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
|
||||
nav items, icons, and per-route title-bar chrome (`registerNavItem`,
|
||||
`registerRoute`'s `options.topbar`) are a separate JS SDK surface with no
|
||||
`ui.pages` is declarative manifest metadata only and is not currently rendered
|
||||
by the frontend. A native bundle's runtime nav items, icons, routes, named
|
||||
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).
|
||||
|
||||
## Managed vs. legacy manifests
|
||||
@@ -157,8 +158,10 @@ resource grants the matching `Host` accessor (`Tasks()`, `Sessions()`,
|
||||
`Messages()`, `Workspaces()`, `Workflows()`, `AgentProfiles()`,
|
||||
`Repositories()` — see [Authoring a plugin](plugins-authoring.md)); 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.
|
||||
`capabilities.api_write` gates the current Host data writes: `tasks` gates
|
||||
`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`):
|
||||
one user/agent message per row (`id`, `session_id`, `task_id`, `turn_id`,
|
||||
|
||||
@@ -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
|
||||
integration-health card ("CLI installed ✅ v0.45.2", "API token ✅
|
||||
authenticated"). This is owner-scoped, so a plugin's card only ever appears on
|
||||
its own settings page. See [Named
|
||||
slots](plugins-authoring.md#named-slots) in the authoring guide.
|
||||
its own settings page. See [supported named
|
||||
slots](plugins-authoring.md#supported-named-slots) in the authoring guide.
|
||||
|
||||
<details>
|
||||
<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
|
||||
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
|
||||
individually via `api_read:<resource>` and `api_write:<resource>` for the
|
||||
implemented task/message Host writes. 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
|
||||
|
||||
@@ -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 |
|
||||
| [i18n](platform/i18n.md) | building |
|
||||
| [plugins](plugins/spec.md) | draft |
|
||||
| [plugins — authoring experience](plugins/authoring-experience.md) | draft |
|
||||
| [plugins — marketplace](plugins/marketplace.md) | building |
|
||||
| [semantic-notifications](platform/notifications.md) | shipped |
|
||||
| [workspace-git-status](platform/workspace-git-status.md) | shipped |
|
||||
|
||||
@@ -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.
|
||||
@@ -126,8 +126,8 @@ config_schema:
|
||||
required: ["bot_token_secret", "default_channel"]
|
||||
|
||||
ui: # Native frontend plugin (see "Frontend plugin runtime")
|
||||
bundle: "ui/bundle.js" # ES module extracted alongside the package
|
||||
styles: ["ui/plugin.css"] # optional stylesheets
|
||||
bundle: "/ui/bundle.js" # root-relative ES module path
|
||||
styles: ["/ui/plugin.css"] # optional root-relative stylesheets
|
||||
|
||||
# Runtime fields managed by kandev (not authored):
|
||||
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
|
||||
`docs/plans/plugins/PLUGIN-API.md`; summary:
|
||||
|
||||
- **Manifest:** a plugin declares `ui.bundle` (a path inside the extracted package,
|
||||
e.g. `ui/bundle.js`) and optional `ui.styles`.
|
||||
- **Manifest:** a plugin declares `ui.bundle` (a root-relative path inside the
|
||||
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`
|
||||
(and any assets under `GET /api/plugins/{id}/ui/*`) directly from the extracted
|
||||
package directory on local disk, forcing `Content-Type: text/javascript` and
|
||||
|
||||
Reference in New Issue
Block a user