feat: add plugin-backed canvases (#3111)

This commit is contained in:
Carlos Florêncio
2026-09-05 18:44:43 +00:00
committed by GitHub
parent 97140168d8
commit d74e85883d
274 changed files with 34222 additions and 369 deletions
+75 -9
View File
@@ -7,8 +7,9 @@ status: experimental
# Authoring a Plugin
This is the canonical developer entry point for Kandev runtime plugins. Use it
to choose a supported surface, edit manifest.yaml, implement the backend or
native UI, package it, and test it against a disposable development instance.
to choose a supported surface, edit manifest.yaml, implement the backend,
native UI, or isolated web app, package it, and test it against a disposable
development instance.
The [plugin manifest reference](plugins-manifest.md) owns field-by-field schema
details; [Plugins](plugins.md) covers operator installation and lifecycle.
@@ -20,9 +21,10 @@ support, not a starter repository.
## Quick workflow
1. Choose the closest recipe in this page.
2. Copy the template and edit manifest.yaml: identity, runtime executable, UI
paths, capabilities, events, webhooks, and config.
3. Implement only through the Go pluginsdk and the frontend registry/Host API.
2. Copy the template and edit manifest.yaml: identity, runtime executable or
`ui.web_apps`, UI paths, capabilities, events, webhooks, and config.
3. Implement through the Go pluginsdk, the native frontend registry/Host API,
or the relative canvas protocol for an isolated web app.
4. Run the plugin repository tests, vet/lint, and build.
5. Stage the package, run plugin-pack, inspect the archive and generated
checksums, then install it in a disposable Kandev instance.
@@ -77,10 +79,14 @@ exactly once; late callbacks from an expired generation are fenced from host sta
runtime.executables maps a platform key such as linux-amd64 to a clean
package-relative path such as server/plugin-linux-amd64. Windows values must
end in .exe. The host platform's key must be present at install time. A
UI-focused plugin still ships a managed backend executable because the current
installer requires runtime.type: binary; that executable can be a no-op
pluginsdk.UnimplementedPlugin server.
end in .exe. The host platform's key must be present at install time.
A UI-focused native plugin still ships a managed backend executable because the
current installer requires `runtime.type: binary`. That executable can be a
no-op pluginsdk.UnimplementedPlugin server.
An isolated web-app package can omit `runtime`. It contains a static entry
document and the files that document imports. Use the `ui.web_apps` fields in
the [manifest reference](plugins-manifest.md#isolated-web-applications).
Kandev injects KANDEV_PLUGIN_DATA_DIR into the subprocess. It is the durable,
per-plugin directory for arbitrary files or a plugin-owned database:
@@ -101,6 +107,55 @@ directory.
| Backend plugin | Go binary and manifest | events, webhooks, Host data, state, secrets, or utility-agent calls | only the capabilities used by the backend |
| UI-focused plugin | no-op managed binary, ui/bundle.js, optional styles/assets | routes, nav, named slots, WebSocket handlers, keybindings, shared store | ui.bundle; add ui.keybindings when declaring shortcuts |
| Combined plugin | Go binary plus UI bundle | UI calls a declared webhook or backend API; backend uses Host | union of the two surfaces, kept least-privilege |
| Isolated web app | manifest plus static HTML, CSS, JS, and assets | task or workspace canvas inside a sandboxed iframe | only the Kandev data, event, state, and network access it needs |
## Build an isolated web application
Use this shape for a canvas app that runs packaged browser code. The package
does not need a Go backend or an injected Kandev JavaScript API.
1. Add one or more `ui.web_apps` entries with a package-relative HTML entry.
2. Set `placements` to `task-canvas`, `workspace-canvas`, or both.
3. Build the app with relative `./_kandev/v1` requests.
4. Declare only the Kandev capabilities and exact HTTPS `network_origins` that
the app needs.
5. Package the manifest and static files as a gzip-compressed tar archive.
For example, a page can read task data with the browser Fetch API:
```js
const response = await fetch("./_kandev/v1/data/tasks");
if (!response.ok) {
throw new Error("The task data request failed");
}
const tasks = await response.json();
```
Use `./_kandev/v1/events` for the event stream. Keep all protocol paths
relative so the same package works in task and workspace scope. Do not copy a
capability URL from the host into the app.
The frame has an opaque browser origin. Do not use `localStorage`,
`sessionStorage`, IndexedDB, or service workers. Use the state protocol for
small app-specific shared values and JavaScript memory for temporary values.
Network access uses exact HTTPS origins that a user approves. Wildcards,
origin paths, query strings, credentials, and remote scripts are not allowed.
Keep scripts, styles, fonts, and images in the package.
Forms cannot submit to external origins. The runtime sets `form-action 'none'`.
External requests to approved origins are sent directly by the browser. Kandev
cannot inspect them after they leave the browser, so a release, grant, scope,
archive, disable, or removal notification immediately tears down the matching
iframe. A replacement iframe gets a fresh runtime binding.
The validator rejects unsafe paths and unsupported files. It also limits a
package to 10 MiB compressed, 25 MiB expanded, 512 files, and 5 MiB per file.
The manifest limit is 64 KiB and the normalized path limit is 240 bytes.
Build and test the archive outside Kandev. Kandev validates the archive before
it stores or runs a release. Use [Agent-authored Canvases](canvases.md) for
creation, permission review, promotion, Quick Chat editing, and recovery.
There is no separate HTTP server to launch. pluginsdk.Serve owns the
go-plugin/gRPC handshake and Host injection. The backend implements
@@ -181,12 +236,20 @@ curated React, UI, and app-store surface.
assert a verified external identity with X-Kandev-Auth-Login; only assert an
email the IdP verified as owned by the subject. See [ADR 0050](../decisions/0050-plugin-external-auth-capability.md).
An isolated web app has a separate browser boundary. Kandev loads it in a
sandboxed iframe with an opaque origin. It cannot use the host DOM, cookies,
host authentication headers, popups, top-level navigation, or a global Kandev
JavaScript API. See [Security and trust](security.md#isolated-web-applications)
for the runtime boundary.
## Storage decision table
| Need | Use | Scope/lifecycle | Capability or rule |
| ----------------------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Small JSON object owned by this plugin | Host state: GetState, SetState, DeleteState, ListState | instance, workspace, task, or agent; survives restart/upgrade and is included in Kandev state backups | capabilities.state: true; values are JSON objects, not bare scalars |
| Canvas app shared state | Relative `./_kandev/v1/state` protocol | Canvas instance; survives restart while the instance remains | `state` grant, store app-specific shared values, not duplicate task data |
| Per-user browser/plugin storage | host.storage: get/set/delete/list/subscribe | instance, workspace, task, session, or repository, scoped per user | capabilities.user_state: true; set/delete accept ifUnmodifiedSince and writerId |
| Temporary canvas app value | JavaScript memory inside the iframe | Current document only | Opaque origin blocks browser storage and service workers |
| Operator configuration | Host.GetConfig and manifest config_schema | Plugin-owned settings; config changes restart an active subprocess | Ungated GetConfig; secret fields arrive cleartext in the subprocess |
| Plugin-owned credentials | Host.GetSecret/SetSecret/DeleteSecret, or secret: true config fields | Encrypted Kandev vault, namespaced to this plugin | capabilities.secrets: true; never log values |
| Files, caches, or plugin-managed database | KANDEV_PLUGIN_DATA_DIR | Shared across versions, removed on uninstall | Write only below the injected directory; own schema, locking, and migrations |
@@ -196,6 +259,9 @@ no transaction primitive, so design updates to be idempotent; host.storage
(below) is per-user, per-scope frontend storage with an optional
ifUnmodifiedSince compare-and-swap.
`host.storage` belongs to the native plugin frontend contract. An isolated web
app does not receive that API. Use the canvas state protocol instead.
## Authoritative contracts
The guide summarizes the contract for discovery. These files remain the source