From 52946b9159a1160699caa6a5845cd639d774e2e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Salavisa?= Date: Sun, 19 Jul 2026 17:44:15 +0100 Subject: [PATCH] docs: document the plugin marketplace (#1805) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: document the plugin marketplace Add a public Plugin Marketplace page covering both audiences: operators using the in-app catalog (Browse/Installed tabs, one-click install, Update to vX, the Sources dialog for team/corporate registries, plugin icons, feature-flag gating, sideloading still works) and plugin authors publishing into it (one-repo-per-plugin GitHub Release assets, the optional manifest icon field, submitting a plugins.yaml PR to the official catalog, GitHub-stars-only ranking, and hosting your own index.json source). Also cross-link the new page from plugins.md (replacing the now-false "No plugin marketplace" security note), plugins-authoring.md, and plugins-manifest.md (new icon field row); register it in meta.json and coverage.json. Co-Authored-By: Claude Opus 4.8 (1M context) * docs: correct plugin-registry CI validation claims The registry index-build workflow does not validate plugins.yaml against schema.json, nor does build-index.mjs compare the entry id to the manifest id — bad entries are skipped-and-logged, and CI only fails when a non-empty list resolves to zero built entries. Reword the submission steps (in both the marketplace docs page and plugin-registry/README.md) to describe what the workflow actually does: it resolves each entry against the GitHub API and requires a correctly named -.tar.gz release asset, with maintainer approval as the real gate. Also clarify that plugins.yaml `categories` are free-form curation tags, not the manifest's category enum. Co-Authored-By: Claude Opus 4.8 (1M context) * ci: split plugin-registry Pages deploy into its own job The single build-and-deploy job bound the `github-pages` environment at the job level, so GitHub evaluated that environment's protection rules on every PR run — failing the check even though the deploy steps were gated to non-PR events. This broke every PR touching plugin-registry/**, which is exactly what a plugin-submission PR does. Split into a PR-safe `build` job (no environment) and a non-PR-gated `deploy` job that owns the `github-pages` environment and runs only `deploy-pages`. Behavior on push/schedule/workflow_call is unchanged; PRs now run build+tests without tripping the environment gate. The star refresh calls this workflow by file path, so the job rename is safe. Co-Authored-By: Claude Opus 4.8 (1M context) * docs: describe index-build asset resolution accurately build-index.mjs picks the -.tar.gz release asset when present but falls back to the release's first .tar.gz otherwise, and only errors when a release has no .tar.gz at all — it does not hard-fail a misnamed asset. Reword the submission steps to present the exact asset name as an authoring instruction (so the index resolves the right asset) rather than a builder-enforced gate. Co-Authored-By: Claude Opus 4.8 (1M context) * Revert "ci: split plugin-registry Pages deploy into its own job" This reverts commit 2c5f6491465961fcf636d52dbdf5c376ffc5bb11. * Reapply "ci: split plugin-registry Pages deploy into its own job" This reverts commit 3795cdb98a7a9b87a6ac18490558180e3e00d7e4. --------- Co-authored-by: Kandev Agent Co-authored-by: Claude Opus 4.8 (1M context) --- .github/workflows/plugin-registry-index.yml | 28 ++- docs/public/coverage.json | 8 + docs/public/meta.json | 1 + docs/public/plugins-authoring.md | 12 ++ docs/public/plugins-manifest.md | 1 + docs/public/plugins-marketplace.md | 187 ++++++++++++++++++++ docs/public/plugins.md | 28 +-- plugin-registry/README.md | 16 +- 8 files changed, 257 insertions(+), 24 deletions(-) create mode 100644 docs/public/plugins-marketplace.md diff --git a/.github/workflows/plugin-registry-index.yml b/.github/workflows/plugin-registry-index.yml index 91015a976f..0b16433cdc 100644 --- a/.github/workflows/plugin-registry-index.yml +++ b/.github/workflows/plugin-registry-index.yml @@ -12,8 +12,8 @@ on: paths: - "plugin-registry/**" - ".github/workflows/plugin-registry-index.yml" - # Validate registry edits before merge (build + test run; the Pages deploy - # steps below are gated to non-PR events, so a PR never deploys). + # Validate registry edits before merge (the build job runs; the separate + # deploy job is gated to non-PR events, so a PR never deploys to Pages). pull_request: paths: - "plugin-registry/**" @@ -39,12 +39,13 @@ concurrency: cancel-in-progress: false jobs: - build-and-deploy: - name: Build index.json and deploy to Pages + # Build the catalog and, on non-PR events, stage it as a Pages artifact. + # Kept free of the `github-pages` environment so it runs on PRs — a job that + # binds that environment is checked against its protection rules even when its + # deploy steps are skipped, which fails every PR that touches plugin-registry/. + build: + name: Build index.json runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deploy.outputs.page_url }} steps: - name: Checkout code uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 @@ -85,7 +86,18 @@ jobs: with: path: _site + # Deploy the staged artifact to GitHub Pages. Separate job so the + # `github-pages` environment (and its protection rules) is only ever + # evaluated on non-PR events. + deploy: + name: Deploy to Pages + if: github.event_name != 'pull_request' + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deploy.outputs.page_url }} + steps: - name: Deploy to GitHub Pages id: deploy - if: github.event_name != 'pull_request' uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 diff --git a/docs/public/coverage.json b/docs/public/coverage.json index 44761fafaa..4958b5a279 100644 --- a/docs/public/coverage.json +++ b/docs/public/coverage.json @@ -273,6 +273,14 @@ "sources": ["apps/backend/internal/plugins/handlers.go", "apps/backend/pkg/pluginsdk/host.go", "apps/web/lib/plugins/host-api.ts"], "tests": ["apps/backend/internal/plugins/service_test.go", "apps/backend/internal/plugins/manifest/manifest_test.go", "apps/web/lib/plugins/host-api.test.ts"] }, + { + "id": "plugin-marketplace", + "audiences": ["operator", "contributor"], + "stability": "experimental", + "docs": ["plugins-marketplace"], + "sources": ["apps/backend/internal/plugins/marketplace/catalog.go", "apps/backend/internal/plugins/marketplace/sourcestore.go", "apps/web/lib/api/domains/marketplace-api.ts"], + "tests": ["apps/backend/internal/plugins/marketplace/catalog_test.go", "apps/backend/internal/plugins/marketplace/sourcestore_test.go", "apps/web/hooks/domains/plugins/use-marketplace.test.ts"] + }, { "id": "websocket-protocol", "audiences": ["contributor", "integrator"], diff --git a/docs/public/meta.json b/docs/public/meta.json index 85d8e34c85..d3b2856ef1 100644 --- a/docs/public/meta.json +++ b/docs/public/meta.json @@ -43,6 +43,7 @@ "release-process", "---Plugins---", "plugins", + "plugins-marketplace", "plugins-authoring", "plugins-manifest", "---Office---", diff --git a/docs/public/plugins-authoring.md b/docs/public/plugins-authoring.md index 2c988b96e2..c4e86fcd4b 100644 --- a/docs/public/plugins-authoring.md +++ b/docs/public/plugins-authoring.md @@ -480,6 +480,18 @@ Install the resulting tarball via **Settings > Plugins** (upload) or: curl -F "package=@my-plugin-1.0.0.tar.gz" http://localhost:38429/api/plugins/install ``` +To show a custom card icon in the marketplace, ship an image in the package +(e.g. `assets/icon.svg`) and point the manifest's [`icon` +field](plugins-manifest.md#field-reference) at its package-relative path. + +## Publishing to the marketplace + +To make your plugin discoverable and one-click installable from inside kandev, +publish the `-.tar.gz` + `checksums.txt` as a GitHub **Release** +asset and either open a PR listing your repo in the official catalog or host +your own source. See [Plugin marketplace → +Publishing](plugins-marketplace.md#publishing-a-plugin). + ## Iterate loop - **UI-only or backend-only change:** edit the source, repackage diff --git a/docs/public/plugins-manifest.md b/docs/public/plugins-manifest.md index f8204f38a6..736ae96bd0 100644 --- a/docs/public/plugins-manifest.md +++ b/docs/public/plugins-manifest.md @@ -71,6 +71,7 @@ ui: # optional native frontend plugin | `description` | no | string | Shown in Settings > Plugins. | | `author` | no | string | Free-form. | | `categories` | no | string[] | Each entry must be one of `connector`, `automation`, `tools`, `analytics`. Unknown values are rejected. | +| `icon` | no | string | Package-relative path (e.g. `icon.svg`) to an image the package ships, rendered on the [marketplace](plugins-marketplace.md) card and in plugin lists. The registry index-build resolves it to an absolute `icon_url`; for an installed plugin it is served from the extracted package. Omit it and the card falls back to a letter tile. | | `runtime.type` | conditionally | string | `"binary"` is the only supported value. Setting it (vs. leaving it empty) makes the manifest **runtime-managed** — see "Managed vs. legacy" below. | | `runtime.executables` | required when `runtime.type: binary` | map\ | Key is `-` (e.g. `linux-amd64`, `darwin-arm64`, `windows-amd64`); value is a clean, package-relative path under `server/` (no leading `/`, no `..` segments). At least one entry required; the running host's key must be present at install time. Windows values end in `.exe`. | | `min_kandev_version` | no | string | Optional advisory; not currently enforced by the installer. | diff --git a/docs/public/plugins-marketplace.md b/docs/public/plugins-marketplace.md new file mode 100644 index 0000000000..5f8c2912a5 --- /dev/null +++ b/docs/public/plugins-marketplace.md @@ -0,0 +1,187 @@ +--- +title: "Plugin Marketplace" +description: "Discover and install kandev plugins from the in-app catalog, keep them updated, add a team or corporate source, and publish your own plugin to the official catalog." +status: experimental +--- + +# Plugin Marketplace + +The marketplace is a discoverable, curated catalog of kandev plugins you can +browse and install from inside the app — no tarball URL required. It is +assembled from one or more **sources**: kandev ships with the official source +enabled by default, and you can add team or corporate sources alongside it. + +This page covers using the marketplace (browse, install, update, add sources) +and publishing a plugin into it. For what plugins are and how the install +pipeline, enable/disable, and security posture work, see +[Plugins](plugins.md). For building one, see [Authoring a +plugin](plugins-authoring.md). + +Like the rest of plugins, the marketplace is gated behind the `plugins` feature +flag (**Settings > System > Feature Toggles**) — off by default in production, +on by default in dev/e2e builds. Enabling it requires a backend restart and +surfaces **Settings > Plugins** in the sidebar. + +> **Sideloading still works.** The marketplace only adds *discovery*. Installing +> a plugin by URL or by uploading a `.tar.gz` is unchanged and always available, +> even with every source disabled or offline — see +> [Plugins → Installing a plugin](plugins.md#installing-a-plugin). + +## Using the marketplace + +**Settings > Plugins** has two tabs: + +- **Installed** — the plugins on this instance, with Enable / Disable / + Uninstall and an **Update** button when a newer version is available. +- **Browse** — the merged catalog across all enabled sources. + +### Browse and install + +Open **Settings > Plugins > Browse**. Each plugin shows as a card with its +name, description, author, categories, source repository link, latest version, +and GitHub star count. To narrow the list: + +- **Search** — type in the search box to match plugin name or description. +- **Category** — filter to a single category with the category dropdown. +- **Sort** — **Most stars** (default), **Recently updated** (by latest release + / repo activity), or **Name**. + +Stars are a **sort hint, not a quality score**. kandev collects no download or +usage telemetry, so there is no "most installed" metric — ranking is GitHub +stars only, and "Recently updated" surfaces new or actively maintained plugins +that high-star incumbents would otherwise bury. + +Click **Install** on a card to install it. This resolves the entry to its +latest release tarball and runs the same verified install pipeline as +install-by-URL (`POST /api/plugins/install`) — see [Plugins → Installing a +plugin](plugins.md#installing-a-plugin) for the exact steps and integrity +checks. A card for a plugin you already have at the latest version shows +**Installed** instead of an install button. + +Plugin-provided icons render on the cards. A plugin that ships an icon (via the +manifest's `icon` field) shows it; otherwise the card falls back to a neutral +letter tile. + +### Keep plugins updated + +When the marketplace advertises a newer version than the one you have +installed, the **Installed** tab shows an **Update to v``** button on +that plugin's row. Clicking it reinstalls the newer tarball through the normal +pipeline; Enable, Disable, and Uninstall are unchanged. + +Updates are never automatic — kandev surfaces the newer version and waits for +an explicit click. There are no update channels and no background auto-update. + +### Add a team or corporate source + +The **Sources** button on the Browse tab opens the **Marketplace sources** +dialog. The official kandev source is present by default, badged **Official**, +and cannot be removed. To add another: + +1. Click **Sources**. +2. Enter a **name** (e.g. `Acme Internal`) and the **URL** of an `index.json` + document (see [Host your own source](#host-your-own-source) below). +3. Click **Add**. + +Its plugins are then merged into the Browse tab alongside the official ones. +You can enable/disable a source without deleting it, and remove any non-official +source. Adding a source is an explicit act of trust in its maintainer, much +like `brew tap`-ing a third-party tap. + +Notes: + +- When the same plugin `id` appears in more than one source, the **first + configured source wins** (the official source is always first); later + duplicates are hidden. +- A source that is unreachable or serves malformed JSON is reported as + **degraded** — its entries are omitted, but the healthy sources still load. +- Configured sources persist across restarts; the fetched catalog itself is a + short-lived cache and is re-fetched on demand. + +## Publishing a plugin + +Getting a plugin into the catalog follows a **one-repo-per-plugin** model, +mirroring Obsidian's community-plugin registry: each plugin lives in its own +public GitHub repository and publishes its package as a GitHub **Release** +asset; the official catalog is a curated pointer list that names *which repos* +are included. The descriptive metadata (name, description, version, tarball +URL, stars) is always read from your latest release, so a listing can never +drift from what actually ships. + +### 1. Publish the plugin package as a release + +Package your plugin as usual (see [Authoring a plugin → +Packaging](plugins-authoring.md#packaging)) and cut a GitHub **Release** whose +assets include both: + +- `-.tar.gz` — the plugin package, and +- `checksums.txt` — the integrity manifest the install pipeline verifies. + +The release must pass the standard package integrity gate. The +`kdlbs/kandev-plugin-template` starter repo is the recommended way to bootstrap +a repo with the right layout and a release workflow. + +### 2. Add an icon (optional) + +Add an `icon:` field to your `manifest.yaml` — a **package-relative path** to an +image your package ships (for example `icon.svg`). The catalog resolves it to +your icon and renders it on the plugin card; without it, the card shows a letter +tile. See the [Plugin manifest reference](plugins-manifest.md#field-reference). + +### 3. Submit to the official catalog + +The official source is curated: a plugin appears only after a maintainer merges +a PR that lists it. + +1. Fork `kdlbs/kandev` and add one entry to + [`plugin-registry/plugins.yaml`](https://github.com/kdlbs/kandev/blob/main/plugin-registry/plugins.yaml) + pointing at your public repo: + + ```yaml + plugins: + - id: my-plugin # MUST equal the `id` in your plugin manifest + repo: your-org/your-plugin-repo # owner/name + categories: [productivity] # optional + ``` + + Keep `id` equal to your manifest `id` and unique in the file, and name your + release package `-.tar.gz` to match, so the index resolves the + right asset. `categories` here are free-form curation tags for catalog + filtering (not the manifest's category enum). `featured` is a maintainer-only + pin — leave it out of submissions. The pointer-list shape is defined by + [`plugin-registry/schema.json`](https://github.com/kdlbs/kandev/blob/main/plugin-registry/schema.json). +2. Open a pull request. The registry index-build workflow runs on your PR + (build + tests, no Pages deploy), resolving your entry against the GitHub API + — your repo must have a latest release that publishes a `.tar.gz` package and + its `checksums.txt`. An entry whose repo has no release or no package asset is + skipped. +3. A maintainer reviews and merges — maintainer approval is what gates the + official catalog. The index-build workflow then picks up your entry and your + plugin appears in the in-app catalog on the next build. + +Ranking in the catalog is **GitHub stars only** — there is no download or usage +telemetry to game. Full submission details are in +[`plugin-registry/README.md`](https://github.com/kdlbs/kandev/blob/main/plugin-registry/README.md). + +## Host your own source + +You do not have to PR into the official registry to share plugins internally. +Any URL that serves an `index.json` document of the catalog shape can be added +as a marketplace source (see [Add a team or corporate +source](#add-a-team-or-corporate-source)), and its plugins merge into the Browse +tab alongside the official ones. This is the recommended path for a team or +corporate registry — no PR to the main repo. + +The `index.json` document is the fetch contract between a source and kandev. The +simplest way to produce one is to copy the official registry's +`plugin-registry/` directory into your own repo — its `plugins.yaml` + +[`build-index.mjs`](https://github.com/kdlbs/kandev/blob/main/plugin-registry/build-index.mjs) +build script (zero-dependency Node) + GitHub Action resolve each listed repo's +latest release into a full catalog record and publish the generated +`index.json` to GitHub Pages. Point kandev at that Pages URL. The document +shape, the build pipeline, and the source data model are specified in the +[plugin marketplace spec](https://github.com/kdlbs/kandev/blob/main/docs/specs/plugins/marketplace.md). + +Related: [Plugins](plugins.md), [Authoring a +plugin](plugins-authoring.md), [Plugin manifest +reference](plugins-manifest.md). diff --git a/docs/public/plugins.md b/docs/public/plugins.md index c0e729f18c..17b8b378b3 100644 --- a/docs/public/plugins.md +++ b/docs/public/plugins.md @@ -10,9 +10,10 @@ Plugins extend kandev without forking core: a plugin ships a **Go backend** that kandev spawns and supervises as a subprocess over a strict typed gRPC protocol, and can optionally ship a **native frontend bundle** that kandev loads into the SPA. This page covers what plugins are, how to install and -manage them, and the current security posture. For building a plugin, see -[Authoring a plugin](plugins-authoring.md). For the manifest schema, see -[Plugin manifest reference](plugins-manifest.md). +manage them, and the current security posture. To discover and install plugins +from the in-app catalog, see the [Plugin marketplace](plugins-marketplace.md). +For building a plugin, see [Authoring a plugin](plugins-authoring.md). For the +manifest schema, see [Plugin manifest reference](plugins-manifest.md). Plugins are an operator-level, instance-wide capability — there is no per-user plugin access. They are gated behind the `plugins` feature flag @@ -41,10 +42,14 @@ its own action right where you message an agent. ## Installing a plugin -Open **Settings > Plugins** and click **Install plugin**. You can install -from a URL (kandev downloads the tarball) or by uploading a `.tar.gz` file -directly. No credentials are ever shown or copied — installing a plugin has -nothing to reveal, unlike a webhook-secret/API-key registration flow. +The easiest way to install is from the in-app catalog — **Settings > Plugins > +Browse**, then **Install** on a card (see the [Plugin +marketplace](plugins-marketplace.md)). To install a plugin that is not in a +configured catalog, open **Settings > Plugins** and click **Install plugin**. +You can install from a URL (kandev downloads the tarball) or by uploading a +`.tar.gz` file directly. No credentials are ever shown or copied — installing a +plugin has nothing to reveal, unlike a webhook-secret/API-key registration +flow. ![The Install plugin dialog with From URL and Upload file tabs and a drag-and-drop area for a .tar.gz package.](../screenshots/plugin-install-dialog.png) @@ -188,9 +193,12 @@ identically to an unsigned one; signing is not required in v1. realm boundary) is explicit future work — see below. - **Package integrity is always checked; signing is optional.** See "Signed vs. unsigned packages" above. -- **No plugin marketplace.** Install-by-URL/upload is a manual, single-plugin - action; there is no central catalog, one-click install, or automatic - discovery. +- **Curated marketplace, no auto-install.** The [Plugin + marketplace](plugins-marketplace.md) adds one-click install from a catalog, + but the official source is PR-curated (a plugin appears only after a + maintainer approves it), install is always an explicit operator action, and + updates require an explicit click — there is no automatic discovery or + background install. kandev collects no download or usage telemetry. Related: [Authoring a plugin](plugins-authoring.md), [Plugin manifest reference](plugins-manifest.md), and [Extending Kandev](extending-kandev.md). diff --git a/plugin-registry/README.md b/plugin-registry/README.md index 72dbe342cd..a083b8f3f9 100644 --- a/plugin-registry/README.md +++ b/plugin-registry/README.md @@ -58,12 +58,16 @@ usage telemetry** — there is no "most installed" metric, by design. The `id` **MUST match the `id` in your plugin manifest** and be unique across the file. `repo` is `owner/name`. `featured` is a maintainer-only pin and should be left out of submissions. -4. **Open a pull request.** CI validates `plugins.yaml` against `schema.json` - and checks that your `id` matches the target repo's latest-release manifest - `id`. -5. **A maintainer reviews and merges.** Once merged, the index-build workflow - picks up your entry and your plugin appears in the in-app catalog on the next - build. +4. **Open a pull request.** The index-build workflow runs on your PR (build + + tests, no Pages deploy) and resolves your entry against the GitHub API — your + repo must have a latest release that publishes a `.tar.gz` package and its + `checksums.txt`; an entry whose repo has no release or no package is skipped. + Name the package `-.tar.gz` and keep your entry `id` equal to + your manifest `id` so the index resolves the right asset. `plugins.yaml` must + also follow the `schema.json` pointer-list contract. +5. **A maintainer reviews and merges** — maintainer approval is what gates the + official catalog. Once merged, the index-build workflow picks up your entry + and your plugin appears in the in-app catalog on the next build. ## Teams and corporates: host your own source instead