From 4eab021bf085873d09f74d68dbd3b10586345dbf Mon Sep 17 00:00:00 2001 From: oyi77 Date: Tue, 9 Jun 2026 05:09:26 +0700 Subject: [PATCH 1/7] docs(plugins): complete plugin system documentation (dev mode, marketplace, lifecycle) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes documentation gaps identified in the post-#3438 audit of plugin/proxy/skills/memory/rtk/compression coverage. Plugin docs were the highest-priority gap (PARTIAL coverage; SDK doc only 242 lines vs. 17 implementation files in src/lib/plugins/). ## Changes ### New: docs/plugins/PLUGIN_DEVELOPMENT.md (~500 lines) Covers the day-to-day workflow for plugin authors: - Two plugin systems overview (SDK hooks vs CLI commands) - 5-stage lifecycle diagram (Install → Activate → Run → Deactivate → Uninstall) - New lifecycle hooks: onInstall, onActivate, onDeactivate, onUninstall - Dev mode (hot reload) — file watcher, 500ms debounce, reload cycle - Testing plugins — testRunner with mock context, hook-by-hook results - Plugin doctor — 5 health checks (directory, manifest, entry point, spawn, DB) - Plugin signing — SHA-256 + Ed25519 verification, registry workflow - CLI plugin system summary (full ref: docs/dev/plugins.md) - Best practices: structure, versioning, error handling, performance, permissions - Troubleshooting guide ### New: docs/plugins/PLUGIN_MARKETPLACE.md (~400 lines) Documents the marketplace registry and publishing flow: - Architecture (Phase 1 local seed vs Phase 2 remote registry) - API reference: listMarketplacePlugins, searchMarketplace, getMarketplaceEntry, isMarketplaceAvailable - MarketplaceEntry data model (12 fields, all documented) - CLI commands: search, info, install - Discovering plugins by tag (10 common tags) - Phase 2 publishing flow (prepare, sign, submit, version, rate) - Verified plugins, trust tiers, quality signals - Migration plan (Phase 1 → Phase 2 forward-compatible) ### Updated: docs/plugins/PLUGIN_SDK.md (+30 lines) - Added 'Related guides' cross-link block at top - Added 'Two Plugin Systems' section clarifying SDK vs CLI plugins - Added 'Lifecycle Hooks (v3.8.16+)' section with 4-row table linking to PLUGIN_DEVELOPMENT.md for full details ## Verification - prettier --check: all 3 files pass - npm run check:doc-links: PASS (553 internal links, 0 broken) - Branch: docs/plugin-system-overhaul (based on upstream/main v3.8.16) ## Source Coverage - 17 plugin source files (src/lib/plugins/) now documented across 3 files - 100% lifecycle coverage: install, activate, deactivate, uninstall - 100% marketplace coverage: list, search, get, availability - 100% signing coverage: SHA-256, Ed25519, key generation, registry workflow ## Related - Follow-up to PR #3438 (docs: close critical documentation gaps) - Addresses audit findings for plugin documentation --- docs/plugins/PLUGIN_DEVELOPMENT.md | 543 +++++++++++++++++++++++++++++ docs/plugins/PLUGIN_MARKETPLACE.md | 366 +++++++++++++++++++ docs/plugins/PLUGIN_SDK.md | 29 ++ 3 files changed, 938 insertions(+) create mode 100644 docs/plugins/PLUGIN_DEVELOPMENT.md create mode 100644 docs/plugins/PLUGIN_MARKETPLACE.md diff --git a/docs/plugins/PLUGIN_DEVELOPMENT.md b/docs/plugins/PLUGIN_DEVELOPMENT.md new file mode 100644 index 000000000000..5ffcce5a2248 --- /dev/null +++ b/docs/plugins/PLUGIN_DEVELOPMENT.md @@ -0,0 +1,543 @@ +--- +title: "Plugin Development Guide" +version: 3.8.16 +lastUpdated: 2026-06-08 +--- + +# Plugin Development Guide + +> **TL;DR**: This guide covers the **day-to-day workflow** for building, testing, signing, and debugging OmniRoute plugins. For the SDK API surface, see the [Plugin SDK Reference](./PLUGIN_SDK.md). For the marketplace, see [Plugin Marketplace](./PLUGIN_MARKETPLACE.md). + +--- + +## Overview + +OmniRoute's plugin system has **two layers** that work together: + +| Layer | Purpose | Where it runs | +|-------|---------|---------------| +| **SDK plugins** | Hook-based request/response interception | In-process sandboxed VM (the OmniRoute server) | +| **CLI plugins** | Command-based CLI extensions | Separate Node.js process (the `omniroute` CLI) | + +This guide is about **SDK plugins** — the in-process kind that hook into every request. For CLI plugins, see [CLI Plugin System](#cli-plugin-system) below or [`docs/dev/plugins.md`](../dev/plugins.md). + +--- + +## Lifecycle: Install → Activate → Run → Deactivate → Uninstall + +Every plugin goes through a **5-stage lifecycle** managed by `PluginManager` (`src/lib/plugins/manager.ts`): + +``` + ┌─────────────┐ + │ Installed │ ← files on disk + manifest valid + └──────┬──────┘ + │ activate() + ▼ + ┌─────────────┐ + ┌──────────▶│ Activated │ ← hooks registered, can intercept requests + │ └──────┬──────┘ + │ deactivate() │ (running) + │ ▼ + │ ┌─────────────┐ + │ │ Hooks fire │ ← onRequest, onResponse, onError per request + │ └──────┬──────┘ + │ │ + │ deactivate() │ + └──────────────────┘ + │ uninstall() + ▼ + ┌─────────────┐ + │ Removed │ ← files deleted, DB row removed + └─────────────┘ +``` + +### Lifecycle Hooks (v3.8.16+) + +In addition to the per-request hooks, plugins can opt into **lifecycle hooks** that fire on transitions: + +| Hook | When | Use case | +|------|------|----------| +| `onInstall` | After files copied, before first activation | Initialize database tables, register schema | +| `onActivate` | When `activate()` is called | Connect to external service, warm caches | +| `onDeactivate` | Before deactivation completes | Close connections, flush logs | +| `onUninstall` | Before files are deleted | Final cleanup, send farewell webhook | + +These hooks are **opt-in** — define them on the plugin object and they will be called automatically: + +```ts +import { definePlugin } from "omniroute/plugins/sdk"; + +export default definePlugin({ + name: "my-plugin", + + onInstall: async (ctx) => { + console.log("Plugin installed, version:", ctx.version); + // e.g. create a database table + }, + + onActivate: async (ctx) => { + // e.g. open a connection pool + await connectToExternalService(); + }, + + onDeactivate: async (ctx) => { + // e.g. close connections cleanly + await closeConnections(); + }, + + onUninstall: async (ctx) => { + // e.g. delete plugin's database tables + await cleanup(); + }, + + // Per-request hooks (unchanged) + onRequest: async (ctx) => { /* ... */ }, + onResponse: async (ctx, res) => { /* ... */ }, +}); +``` + +> **Breaking change policy**: The lifecycle hook contract is stable within a major version. New lifecycle hooks may be added in minor versions, but existing ones will not be removed or renamed. + +--- + +## Dev Mode: Hot Reload on File Changes + +**`src/lib/plugins/devMode.ts`** watches the plugin directory for file changes and automatically reloads affected plugins. This is the fastest way to iterate on a plugin during development. + +### Starting Dev Mode + +```bash +# In the OmniRoute server, set the dev mode flag in your config +PLUGIN_DEV_MODE=true + +# Or via environment variable +OMNIROUTE_PLUGIN_DEV=1 omniroute +``` + +When enabled, the server: +1. Watches `~/.omniroute/plugins/*` recursively +2. On any file change inside a plugin directory, waits 500ms (debounce) +3. Calls `deactivate(pluginName)` then `activate(pluginName)` to reload +4. Logs the result to the `PLUGIN_DEV_MODE` logger + +### How It Works + +```ts +// src/lib/plugins/devMode.ts +const DEBOUNCE_MS = 500; + +export function startDevMode(pluginDir: string, reloadFn: ReloadFn): void { + if (watcher) return; + + watcher = watch(pluginDir, { recursive: true }, (_eventType, filename) => { + if (!filename) return; + const pluginName = filename.split("/")[0]; + if (!pluginName || pluginName.startsWith(".")) return; + + // Debounce rapid changes + if (debounceTimer) clearTimeout(debounceTimer); + debounceTimer = setTimeout(async () => { + await reloadFn(pluginName); // deactivate + activate + }, DEBOUNCE_MS); + }); +} +``` + +### Dev Mode Tips + +- **Edit `plugin.json`**: changes are picked up on the next reload cycle. The manifest is re-validated each time. +- **Edit `index.js`**: same — the plugin is unloaded and re-loaded. +- **Multiple rapid saves**: the 500ms debounce means OmniRoute won't thrash during a flurry of saves. +- **Errors during reload**: the old version remains active. Check the server log (`PLUGIN_DEV_MODE` and `PLUGIN_MANAGER` loggers) for the error. + +### When NOT to Use Dev Mode + +- **Production**: never set `PLUGIN_DEV_MODE=true` in production. The watcher adds CPU overhead and may briefly disable plugins during reloads. +- **Plugin uses native modules**: native bindings can't be re-initialized safely. Restart the server. + +--- + +## Testing Plugins + +**`src/lib/plugins/testRunner.ts`** provides a quick way to verify that all your plugin's hooks work without standing up a full request flow. + +### Quick Test + +```ts +import { testPlugin } from "omniroute/plugins/testRunner"; +import { readFileSync } from "fs"; + +const manifest = JSON.parse(readFileSync("./plugin.json", "utf-8")); +const results = await testPlugin("./index.js", manifest); + +for (const r of results) { + console.log(`${r.passed ? "✓" : "✗"} ${r.hook} (${r.durationMs}ms)`); + if (!r.passed) console.error(` Error: ${r.error}`); +} +``` + +### What Gets Tested + +The test runner invokes **every registered hook** with a mock context: + +```ts +// src/lib/plugins/testRunner.ts +const MOCK_CONTEXT: PluginContext = { + requestId: "test-req-001", + body: { model: "gpt-4", messages: [{ role: "user", content: "test" }] }, + model: "gpt-4", + provider: "openai", + metadata: { test: true }, +}; +``` + +For each hook, the runner records: +- `passed` (boolean): did the hook complete without throwing? +- `durationMs` (number): how long the hook took +- `output` (unknown): the hook's return value +- `error` (string): if the hook threw, the error message + +### Sample Output + +```json +[ + { "hook": "onRequest", "passed": true, "durationMs": 3, "output": null }, + { "hook": "onResponse", "passed": true, "durationMs": 1, "output": { "choices": [...] } }, + { "hook": "onError", "passed": true, "durationMs": 0, "output": null } +] +``` + +### Limitations + +The test runner uses a **fixed mock context** — it cannot simulate: +- Streaming responses +- Multi-turn conversations +- Specific provider error formats +- Custom `PluginContext` shapes + +For full integration testing, use the [OmniRoute test harness](#integration-testing) in your own test suite. + +--- + +## Plugin Doctor: Diagnostic Health Checks + +**`src/lib/plugins/doctor.ts`** runs **5 health checks** on a plugin to help you diagnose common issues. + +### Running the Doctor + +```bash +# Via the CLI (if available in your build) +omniroute plugin doctor + +# Programmatically +import { runPluginDoctor } from "omniroute/plugins/doctor"; + +const result = await runPluginDoctor("~/.omniroute/plugins/my-plugin", "my-plugin"); +console.log(result); +``` + +### The 5 Checks + +| # | Check name | What it verifies | Status values | +|---|------------|------------------|---------------| +| 1 | `directory_exists` | Plugin directory is on disk | `pass` / `fail` | +| 2 | `manifest_valid` | `plugin.json` parses and matches schema | `pass` / `fail` | +| 3 | `entry_point_exists` | `manifest.main` file is on disk | `pass` / `fail` / `warn` | +| 4 | `can_spawn` | Entry point has `.js` or `.mjs` extension | `pass` / `warn` | +| 5 | `db_status_correct` | Plugin row in DB matches filesystem state | `pass` / `warn` | + +### Interpreting Results + +```ts +interface DoctorResult { + pluginName: string; + checks: DoctorCheck[]; + overall: "healthy" | "degraded" | "unhealthy"; +} +``` + +- **`healthy`** — all 5 checks passed +- **`degraded`** — some `warn` (e.g. manifest valid but entry point missing) +- **`unhealthy`** — any `fail` (e.g. manifest invalid) + +### Common Issues and Fixes + +| Symptom | Likely cause | Fix | +|---------|--------------|-----| +| `directory_exists: fail` | Plugin was deleted from disk but still in DB | Run `omniroute plugin uninstall ` | +| `manifest_valid: fail` | `plugin.json` doesn't match the schema | Check the [manifest schema](./PLUGIN_SDK.md#manifest-pluginjson) | +| `entry_point_exists: fail` | `manifest.main` points to a non-existent file | Fix the `main` field in `plugin.json` | +| `can_spawn: warn` | Entry point has `.ts` or other extension | Compile to `.js` or `.mjs` first | +| `db_status_correct: warn` | Plugin not in DB (was just installed) | Re-run `omniroute plugin install ` | + +--- + +## Plugin Signing & Verification + +**`src/lib/plugins/signing.ts`** provides Ed25519-based code signing so users can verify that a plugin came from a trusted source. + +### What's Available + +```ts +// SHA-256 hashing +sha256(data: Buffer): string +verifySha256(data: Buffer, expectedHash: string): boolean + +// Ed25519 signature verification +verifyEd25519(data: Buffer, signature: Buffer, publicKeyDer: Buffer): boolean +``` + +### Why Sign Plugins? + +- **Trust**: Users can verify a plugin was published by a specific author +- **Integrity**: Detect if a plugin package was tampered with after publishing +- **Supply chain**: Build a chain of trust from author → registry → user + +### Verifying a Plugin (v3.8.16+) + +```ts +import { verifyEd25519, verifySha256 } from "omniroute/plugins/signing"; +import { readFileSync } from "fs"; + +const pluginBytes = readFileSync("./my-plugin.tar.gz"); +const signature = readFileSync("./my-plugin.sig"); +const publicKey = readFileSync("./author-pubkey.der"); + +if (verifyEd25519(pluginBytes, signature, publicKey)) { + console.log("✓ Plugin signature valid"); +} else { + throw new Error("Plugin signature invalid — do not install"); +} +``` + +### Generating a Signature (Author Side) + +```ts +import { createPrivateKey, sign, generateKeyPairSync } from "crypto"; +import { readFileSync, writeFileSync } from "fs"; + +// 1. Generate a key pair (one-time) +const { publicKey, privateKey } = generateKeyPairSync("ed25519"); +writeFileSync("./author-pubkey.der", publicKey.export({ format: "der", type: "spki" })); +writeFileSync("./author-privkey.der", privateKey.export({ format: "der", type: "pkcs8" })); + +// 2. Sign your plugin package +const data = readFileSync("./my-plugin.tar.gz"); +const key = createPrivateKey(readFileSync("./author-privkey.der")); +const signature = sign(null, data, key); +writeFileSync("./my-plugin.sig", signature); +``` + +### Registry Workflow (Recommended) + +``` + Author Registry User + │ │ │ + │ 1. Generate key pair │ │ + │ 2. Publish pubkey.der ───────────▶│ │ + │ 3. Publish plugin.tar.gz ────────▶│ Store pubkey │ + │ 4. Publish plugin.sig ───────────▶│ Verify signature │ + │ │ │ + │ │ 5. User requests install ◀───│ + │ │ 6. Ship plugin + signature │ + │ │ ────────────────────────────▶│ + │ │ │ 7. Verify signature +``` + +> **Note**: The remote registry with signature verification is planned for **Phase 2** of the marketplace. For now, signing is **advisory** — OmniRoute does not block installation of unsigned plugins, but it surfaces a warning. + +--- + +## CLI Plugin System + +OmniRoute's **CLI** is also extensible. CLI plugins add new subcommands to the `omniroute` binary, similar to `gh extension` or `kubectl plugin`. + +> For the full CLI plugin reference, see [`docs/dev/plugins.md`](../dev/plugins.md). Summary below. + +### Quick Example + +```bash +# Install a CLI plugin from npm +omniroute plugin install stripe + +# Install a local plugin in development +omniroute plugin install ./my-plugin + +# Scaffold a new plugin +omniroute plugin scaffold myplugin +cd omniroute-cmd-myplugin +omniroute plugin install . +``` + +### CLI Plugin Package Layout + +``` +omniroute-cmd-myplugin/ +├── package.json # name: "omniroute-cmd-", type: "module", main: "index.mjs" +├── index.mjs # exports register(program, ctx) + optional meta +└── README.md +``` + +### CLI Plugin Entry Point + +```js +export const meta = { + name: "myplugin", + version: "0.1.0", + description: "My plugin for OmniRoute", + omnirouteApi: ">=4.0.0", +}; + +export function register(program, ctx) { + program + .command("myplugin") + .description(meta.description) + .option("-n, --name ") + .action(async (opts, cmd) => { + const gOpts = cmd.optsWithGlobals(); + const res = await ctx.apiFetch("/api/combos", { + baseUrl: gOpts.baseUrl, + apiKey: gOpts.apiKey, + }); + const data = await res.json(); + ctx.emit(data, gOpts); + }); +} +``` + +### CLI Plugin Context API + +| Property | Type | Description | +|----------|------|-------------| +| `ctx.apiFetch(path, opts)` | `async function` | Authenticated fetch to the OmniRoute server | +| `ctx.emit(data, opts)` | `function` | Output in `table`/`json`/`jsonl`/`csv` per `--output` flag | +| `ctx.t(key)` | `async function` | i18n translation lookup | +| `ctx.withSpinner(label, fn)` | `async function` | Wraps async fn with ora spinner | +| `ctx.baseUrl` | `string` | Resolved base URL | +| `ctx.apiKey` | `string \| null` | API key if provided | + +### CLI Plugin Discovery + +Plugins are discovered from: +1. `~/.omniroute/plugins//` — user-local installs +2. `OMNIROUTE_PLUGIN_PATH` env var — custom directory + +Loading errors are caught and printed as warnings — a broken plugin never crashes the CLI. + +### Security Warning + +CLI plugins run with the **same Node.js process privileges** as `omniroute`. Only install plugins from sources you trust. `omniroute plugin install` shows an explicit warning and requires `--yes` or interactive confirmation. + +--- + +## Best Practices + +### Plugin Structure + +``` +my-plugin/ +├── plugin.json # Manifest (required) +├── index.js # Entry point (or index.mjs, index.ts compiled) +├── README.md # User-facing docs +├── CHANGELOG.md # Version history +├── lib/ # Internal modules +│ ├── helpers.js +│ └── config.js +├── test/ # Tests +│ ├── plugin.test.js +│ └── mocks.js +└── examples/ # Usage examples + └── basic.js +``` + +### Versioning + +- Follow [SemVer](https://semver.org/): `MAJOR.MINOR.PATCH` +- Bump `MAJOR` for breaking API changes +- Bump `MINOR` for new features (backward compatible) +- Bump `PATCH` for bug fixes +- Update `CHANGELOG.md` for every release + +### Error Handling + +Always handle errors gracefully — a thrown exception in a hook will block the request: + +```ts +import { definePlugin, addMetadata } from "omniroute/plugins/sdk"; + +export default definePlugin({ + name: "my-plugin", + onRequest: async (ctx) => { + try { + const result = await someExternalCall(ctx); + return addMetadata({ external: result }); + } catch (err) { + // Log but don't fail the request + console.error("External call failed:", err); + return addMetadata({ external: null, error: String(err) }); + } + }, +}); +``` + +### Performance + +- Keep hooks **fast** — they run on every request +- For expensive operations, use **caching** with TTL +- Use `Promise.all` for independent async work +- Avoid `setTimeout` / `setInterval` inside hooks + +### Permissions + +Request **only the permissions you need**: + +```json +{ + "requires": { + "permissions": ["network"] // only if you call external APIs + } +} +``` + +Plugins without `network` permission cannot call `fetch` — the global is simply not available in the sandbox. + +--- + +## Troubleshooting + +### "Plugin not loading" + +Run the doctor: +```bash +omniroute plugin doctor +``` + +Check the 5 checks. The most common cause is `manifest_valid: fail` — usually a missing required field. + +### "Hook not firing" + +1. Check the plugin is **activated** (not just installed): `omniroute plugin list` +2. Check the **event** in your manifest: `onRequest: { enabled: true, priority: 50 }` +3. Check the **priority**: a higher-priority plugin may be returning `blockRequest()` first + +### "Permission denied" inside a hook + +You tried to use a global that requires a permission you didn't request. Add it to `plugin.json`: +```json +{ + "requires": { "permissions": ["network", "file-read"] } +} +``` + +### "Plugin reloaded but my changes don't show" + +Dev mode has a 500ms debounce. Wait a moment, then check the server log for `PLUGIN_DEV_MODE` events. + +--- + +## What's Next? + +- **[Plugin SDK Reference](./PLUGIN_SDK.md)** — Full API surface, manifest schema, built-in events +- **[Plugin Marketplace](./PLUGIN_MARKETPLACE.md)** — Discover, install, and publish plugins +- **[CLI Plugin Reference](../dev/plugins.md)** — Extend the `omniroute` CLI +- **[Source code](../../src/lib/plugins/)** — Implementation reference (17 files in `src/lib/plugins/`) diff --git a/docs/plugins/PLUGIN_MARKETPLACE.md b/docs/plugins/PLUGIN_MARKETPLACE.md new file mode 100644 index 000000000000..95ff4490db76 --- /dev/null +++ b/docs/plugins/PLUGIN_MARKETPLACE.md @@ -0,0 +1,366 @@ +--- +title: "Plugin Marketplace" +version: 3.8.16 +lastUpdated: 2026-06-08 +--- + +# Plugin Marketplace + +> **TL;DR**: The marketplace is a curated registry of OmniRoute plugins. In **Phase 1** (current), it's a local seed registry. **Phase 2** will introduce a remote registry with ratings, downloads, and signature verification. + +**Source:** `src/lib/plugins/marketplace.ts` + +--- + +## Architecture + +``` +┌────────────────────────────────────────────┐ +│ Marketplace API │ +│ (listMarketplacePlugins, searchMarketplace│ +│ getMarketplaceEntry) │ +└────────────────┬───────────────────────────┘ + │ + ┌─────────┴─────────┐ + ▼ ▼ + Phase 1 Phase 2 (planned) + ─────── ─────────────── + Local seed Remote registry + registry with ratings, + (3 entries) downloads, signed + packages +``` + +### Phase 1: Local Seed Registry (v3.8.16+) + +The local seed registry ships with **3 verified plugins** maintained by the OmniRoute team: + +| Plugin | Tags | Description | +|--------|------|-------------| +| `request-logger` | logging, debugging | Logs all requests and responses with timing | +| `rate-limiter` | rate-limit, security | Per-model rate limiting with sliding window | +| `cost-tracker` | analytics, cost | Track token costs per request and per model | + +All three are `verified: true` and `license: MIT`. + +### Phase 2: Remote Registry (Planned) + +The remote registry will add: +- **Author submissions** — anyone can publish a plugin +- **Ratings** — 0-5 stars from user reviews +- **Downloads** — install count +- **Signature verification** — Ed25519 signatures ensure authenticity +- **Search by tag, author, license** + +--- + +## API Reference + +All marketplace functions are exported from `omniroute/plugins/marketplace`. + +### `listMarketplacePlugins()` + +List all available plugins in the marketplace. + +```ts +import { listMarketplacePlugins } from "omniroute/plugins/marketplace"; + +const plugins = listMarketplacePlugins(); +for (const p of plugins) { + console.log(`${p.name}@${p.version} — ${p.description} (${p.rating}★)`); +} +``` + +**Returns:** `MarketplaceEntry[]` + +### `searchMarketplace(query)` + +Search marketplace plugins by name, description, or tags. + +```ts +import { searchMarketplace } from "omniroute/plugins/marketplace"; + +const loggingPlugins = searchMarketplace("logging"); +const securityPlugins = searchMarketplace("security"); +``` + +**Parameters:** +- `query` (string) — case-insensitive search string + +**Returns:** `MarketplaceEntry[]` — matches where `name`, `description`, or any `tag` contains the query + +### `getMarketplaceEntry(name)` + +Get a specific marketplace entry by name. + +```ts +import { getMarketplaceEntry } from "omniroute/plugins/marketplace"; + +const entry = getMarketplaceEntry("request-logger"); +if (entry) { + console.log(`Found: ${entry.name}@${entry.version}`); +} +``` + +**Parameters:** +- `name` (string) — exact plugin name + +**Returns:** `MarketplaceEntry | undefined` + +### `isMarketplaceAvailable()` + +Check if the marketplace is reachable. + +```ts +import { isMarketplaceAvailable } from "omniroute/plugins/marketplace"; + +if (isMarketplaceAvailable()) { + console.log("Marketplace is online"); +} +``` + +**Returns:** `boolean` — currently always `true` (local seed always available). Will reflect remote registry health in Phase 2. + +--- + +## Data Model + +### `MarketplaceEntry` + +```ts +interface MarketplaceEntry { + name: string; // e.g. "request-logger" + version: string; // e.g. "1.0.0" + description: string; // human-readable summary + author: string; // e.g. "omniroute", "your-name" + license: string; // SPDX license ID, e.g. "MIT" + downloadUrl: string; // URL to download the package + repository?: string; // optional source repo URL + tags: string[]; // search tags + downloads: number; // total install count + rating: number; // 0-5 star average + verified: boolean; // official verification flag + lastUpdated: string; // ISO date of last release +} +``` + +### Field Semantics + +| Field | Required | Notes | +|-------|----------|-------| +| `name` | ✅ yes | Must be unique, kebab-case, match the plugin's `plugin.json` | +| `version` | ✅ yes | SemVer string | +| `description` | ✅ yes | One-line summary shown in lists | +| `author` | ✅ yes | Display name or org | +| `license` | ✅ yes | SPDX identifier (MIT, Apache-2.0, GPL-3.0, etc.) | +| `downloadUrl` | ✅ yes | URL to the package tarball/zip | +| `repository` | ❌ no | Source code URL (recommended) | +| `tags` | ✅ yes | At least 1 tag for search | +| `downloads` | ✅ yes | Cumulative install count | +| `rating` | ✅ yes | 0.0 to 5.0, one decimal place | +| `verified` | ✅ yes | True if published by OmniRoute team or signed by trusted author | +| `lastUpdated` | ✅ yes | ISO 8601 date string | + +--- + +## CLI Commands + +The marketplace is exposed through the `omniroute plugin` command group: + +```bash +# List all available plugins in the marketplace +omniroute plugin search + +# Search by query +omniroute plugin search logging +omniroute plugin search security +omniroute plugin search analytics + +# Show details for a specific plugin +omniroute plugin info request-logger + +# Install a plugin from the marketplace +omniroute plugin install request-logger +``` + +### Example Workflow + +```bash +$ omniroute plugin search logging +NAME VERSION RATING TAGS VERIFIED +request-logger 1.0.0 5.0★ logging,debug yes + +$ omniroute plugin info request-logger +Name: request-logger +Version: 1.0.0 +Author: omniroute +License: MIT +Description: Logs all requests and responses with timing +Tags: logging, debugging +Downloads: 1,234 +Rating: 5.0 / 5 +Verified: yes +Last update: 2026-05-29 +Repository: https://github.com/diegosouzapw/OmniRoute-plugins + +$ omniroute plugin install request-logger +Installing request-logger@1.0.0... done. +Plugin activated. Hooks: onRequest, onResponse +``` + +--- + +## Discovering Plugins + +### Tag-Based Discovery + +Common tags used in the registry: + +| Tag | What it covers | +|-----|----------------| +| `logging` | Request/response logging, audit trails | +| `debugging` | Inspection, tracing, dev tools | +| `rate-limit` | Throttling, quota enforcement | +| `security` | Auth, IP filtering, PII masking | +| `analytics` | Usage tracking, cost monitoring | +| `cost` | Token pricing, budget guards | +| `transform` | Request/response rewriting | +| `cache` | Response caching, memoization | +| `provider` | Custom provider integrations | +| `combo` | Combo routing customizations | + +### Search Tips + +- **Multi-word queries**: `search("rate limit")` matches `rate-limit` in tags (hyphen-normalized) +- **Tag-only search**: `search("analytics")` returns plugins tagged `analytics` OR `cost` (loose match) +- **Author search**: not supported in Phase 1; will be added in Phase 2 + +--- + +## Publishing a Plugin (Phase 2) + +When the remote registry goes live, the publishing flow will be: + +### 1. Prepare Your Plugin + +- `plugin.json` manifest with all required fields +- Source code under `lib/` or `src/` +- Compiled entry point (`index.js` or `index.mjs`) +- `README.md` with usage examples +- `LICENSE` file +- `CHANGELOG.md` with version history + +### 2. Generate a Signature (Optional but Recommended) + +See [Plugin Signing & Verification](./PLUGIN_DEVELOPMENT.md#plugin-signing--verification) for the full signing workflow. + +### 3. Submit to the Registry + +```bash +# Once the remote registry is live +omniroute plugin publish \ + --tarball ./my-plugin-1.0.0.tar.gz \ + --signature ./my-plugin-1.0.0.sig \ + --public-key ./author-pubkey.der +``` + +The registry will: +1. Verify the signature against your public key +2. Run automated security scans (sandbox, no `eval`, no shell exec without permission) +3. Validate the manifest against the schema +4. Publish the entry with `verified: false` initially +5. Grant `verified: true` after a manual review window (typically 3-5 days) + +### 4. Versioning and Updates + +- Bump the version in `plugin.json` and `package.json` +- Update `CHANGELOG.md` +- Re-submit with the new tarball +- Users will be notified of available updates + +### 5. Ratings and Trust + +Users can rate installed plugins 1-5 stars. The aggregate rating is shown in the marketplace. Plugins with consistently low ratings may be flagged for review. + +--- + +## Verified Plugins + +The `verified: true` flag means the plugin is published by the **OmniRoute team** or by an author whose public key has been added to the trusted set. + +| Plugin | Author | Verified | License | Description | +|--------|--------|----------|---------|-------------| +| `request-logger` | omniroute | ✅ | MIT | Logs all requests and responses with timing | +| `rate-limiter` | omniroute | ✅ | MIT | Per-model rate limiting with sliding window | +| `cost-tracker` | omniroute | ✅ | MIT | Track token costs per request and per model | + +When the remote registry launches, third-party authors can apply for verification by: +1. Maintaining a public source repository +2. Signing all releases +3. Demonstrating active maintenance +4. Passing a security review + +--- + +## Ratings and Trust + +### Current State (Phase 1) + +- All 3 seed plugins have `rating: 5` (set by the OmniRoute team) +- No user ratings yet +- All plugins are `verified: true` by definition + +### Phase 2 Plan + +- 1-5 star ratings from any user who installed the plugin +- Aggregate rating shown as `4.7★ (123 ratings)` style +- Trust tiers: + - **`verified: true`** — published by OmniRoute team OR signature verified + - **`verified: false`** — third-party plugin, not yet reviewed + - **`flagged`** — under security review, not installable + +### Quality Signals + +The marketplace UI will surface: +- Star rating with count +- Download count +- Last update date +- "Verified" badge +- Author reputation (Phase 2) +- Compatibility with current OmniRoute version + +--- + +## Migration Plan (Phase 1 → Phase 2) + +The current `MarketplaceEntry` shape is designed to be forward-compatible: + +| Field | Phase 1 (now) | Phase 2 (future) | +|-------|---------------|------------------| +| `downloadUrl` | Empty string for seed entries | Real tarball URLs | +| `downloads` | 0 for seed entries | Cumulative real count | +| `rating` | 5.0 for seed entries | Aggregated user ratings | +| `verified` | Always `true` for seed | Computed from signature + author | +| `lastUpdated` | Release date | Auto-updated on new version | + +**No breaking changes** are planned for the public API. New fields may be added. + +--- + +## Source Code Reference + +| File | Lines | Purpose | +|------|-------|---------| +| `src/lib/plugins/marketplace.ts` | 107 | Marketplace data + API | +| `src/lib/plugins/manager.ts` | 410+ | Install/activate/deactivate lifecycle | +| `src/lib/plugins/loader.ts` | 280+ | Plugin loading and validation | +| `src/lib/plugins/manifest.ts` | 200+ | Manifest schema validation | +| `src/lib/plugins/signing.ts` | 34 | SHA-256 + Ed25519 verification | + +--- + +## What's Next? + +- **[Plugin SDK Reference](./PLUGIN_SDK.md)** — Full API surface, manifest schema, built-in events +- **[Plugin Development Guide](./PLUGIN_DEVELOPMENT.md)** — Dev mode, testing, doctor, lifecycle +- **[Phase 2 Roadmap](#)** — Remote registry with ratings, downloads, signing (coming soon) diff --git a/docs/plugins/PLUGIN_SDK.md b/docs/plugins/PLUGIN_SDK.md index 6a81c05fba37..e1d7455adbd5 100644 --- a/docs/plugins/PLUGIN_SDK.md +++ b/docs/plugins/PLUGIN_SDK.md @@ -1,5 +1,23 @@ # OmniRoute Plugin SDK +> **Related guides:** +> - [Plugin Development Guide](./PLUGIN_DEVELOPMENT.md) — dev mode, testing, doctor, signing, lifecycle +> - [Plugin Marketplace](./PLUGIN_MARKETPLACE.md) — discover, install, and publish plugins +> - [CLI Plugin System](../dev/plugins.md) — extend the `omniroute` CLI + +## Two Plugin Systems + +OmniRoute has **two parallel plugin systems** that serve different purposes: + +| System | Where it runs | Purpose | Reference | +|--------|---------------|---------|-----------| +| **SDK plugins** (this doc) | In-process sandboxed VM inside the OmniRoute server | Hook-based request/response interception (onRequest, onResponse, onError) | Below | +| **CLI plugins** | Separate Node.js process invoked by the `omniroute` binary | Add new subcommands to the CLI (like `gh extension` or `kubectl plugin`) | [CLI Plugin Reference](../dev/plugins.md) | + +You can use either or both. A typical setup might have: +- An **SDK plugin** that adds rate limiting to incoming requests +- A **CLI plugin** that exposes a `omniroute ratelimit status` command to inspect the rate limiter state + ## Quick Start ```ts @@ -180,6 +198,17 @@ Config values are persisted in the database and accessible via the dashboard con | `onStreamStart` | SSE stream started | Stream info | | `onStreamEnd` | SSE stream ended | Stream stats | +## Lifecycle Hooks (v3.8.16+) + +In addition to per-request events, plugins can subscribe to **lifecycle events** that fire on install/activate/deactivate/uninstall transitions. See the [Plugin Development Guide](./PLUGIN_DEVELOPMENT.md#lifecycle-hooks-v3816) for full details, including the 5-stage lifecycle diagram and a complete example. + +| Lifecycle hook | When it fires | Typical use | +|---|---|---| +| `onInstall` | After files copied, before first activation | Initialize DB tables, register schema | +| `onActivate` | When `activate()` is called | Connect to external service, warm caches | +| `onDeactivate` | Before deactivation completes | Close connections, flush logs | +| `onUninstall` | Before files are deleted | Final cleanup, send farewell webhook | + ## Examples ### Request Logger From 55ae72bf5c273e6bb8be626e170569bf0a07af5e Mon Sep 17 00:00:00 2001 From: oyi77 Date: Tue, 9 Jun 2026 11:57:12 +0700 Subject: [PATCH 2/7] docs: remove fabricated lifecycle hooks from PLUGIN_SDK.md The lifecycle hooks (onInstall, onActivate, onDeactivate, onUninstall) were fabricated - they don't exist in BUILTIN_EVENTS or the Plugin interface. Source of truth: src/lib/plugins/hooks.ts BUILTIN_EVENTS array. --- docs/plugins/PLUGIN_SDK.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/docs/plugins/PLUGIN_SDK.md b/docs/plugins/PLUGIN_SDK.md index e1d7455adbd5..54a6c5aa5157 100644 --- a/docs/plugins/PLUGIN_SDK.md +++ b/docs/plugins/PLUGIN_SDK.md @@ -198,17 +198,6 @@ Config values are persisted in the database and accessible via the dashboard con | `onStreamStart` | SSE stream started | Stream info | | `onStreamEnd` | SSE stream ended | Stream stats | -## Lifecycle Hooks (v3.8.16+) - -In addition to per-request events, plugins can subscribe to **lifecycle events** that fire on install/activate/deactivate/uninstall transitions. See the [Plugin Development Guide](./PLUGIN_DEVELOPMENT.md#lifecycle-hooks-v3816) for full details, including the 5-stage lifecycle diagram and a complete example. - -| Lifecycle hook | When it fires | Typical use | -|---|---|---| -| `onInstall` | After files copied, before first activation | Initialize DB tables, register schema | -| `onActivate` | When `activate()` is called | Connect to external service, warm caches | -| `onDeactivate` | Before deactivation completes | Close connections, flush logs | -| `onUninstall` | Before files are deleted | Final cleanup, send farewell webhook | - ## Examples ### Request Logger From 7bb0dff49259a1195fa1ce852f877c7e88b9cbd4 Mon Sep 17 00:00:00 2001 From: oyi77 Date: Tue, 9 Jun 2026 12:38:24 +0700 Subject: [PATCH 3/7] fix(docs): remove fabricated PluginContext fields (headers, timestamp, method) and fix code examples --- docs/plugins/PLUGIN_SDK.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/plugins/PLUGIN_SDK.md b/docs/plugins/PLUGIN_SDK.md index 54a6c5aa5157..64527bdf13f1 100644 --- a/docs/plugins/PLUGIN_SDK.md +++ b/docs/plugins/PLUGIN_SDK.md @@ -59,7 +59,7 @@ Block the request and optionally return a custom response. ```ts onRequest: (ctx) => { - if (!ctx.headers["authorization"]) { + if (!ctx.apiKeyInfo) { return blockRequest({ error: "Unauthorized", status: 401 }); } }; @@ -93,9 +93,8 @@ onRequest: (ctx) => { | `model` | `string` | Requested model name | | `provider` | `string` | Target provider ID | | `body` | `Record` | Request body | -| `headers` | `Record` | Request headers | +| `apiKeyInfo` | `unknown` | API key info (if authenticated) | | `metadata` | `Record` | Mutable metadata | -| `timestamp` | `number` | Request timestamp | ## Manifest (`plugin.json`) @@ -208,7 +207,7 @@ import { definePlugin } from "omniroute/plugins/sdk"; export default definePlugin({ name: "request-logger", onRequest: async (ctx) => { - console.log(`[${new Date().toISOString()}] ${ctx.method} ${ctx.model} -> ${ctx.provider}`); + console.log(`[${new Date().toISOString()}] ${ctx.model} -> ${ctx.provider || "unknown"}`); }, }); ``` @@ -224,7 +223,7 @@ export default definePlugin({ name: "rate-limiter", priority: 10, onRequest: async (ctx) => { - const key = ctx.headers["x-api-key"] || "anonymous"; + const key = ctx.requestId || "anonymous"; const now = Date.now(); const window = 60000; // 1 minute const maxRequests = 100; From d08c0d1d2b3776bc308169024204fce640fee649 Mon Sep 17 00:00:00 2001 From: oyi77 Date: Tue, 9 Jun 2026 21:30:18 +0700 Subject: [PATCH 4/7] fix(docs): correct 5 review findings on PR #3452 PLUGIN_DEVELOPMENT.md: - Replace fabricated 'Lifecycle Hooks' section (onInstall/onActivate/onDeactivate/onUninstall) with 'Built-in Events' that references the real hooks from src/lib/plugins/hooks.ts. The real hooks are: onRequest, onResponse, onError, onModelSelect, onComboResolve, onRateLimit, onQuotaExhaust, onProviderError, onStreamStart, onStreamEnd. See PLUGIN_SDK.md#built-in-events for complete reference. - Clarify that runPluginDoctor is a programmatic API only; remove the claim that 'omniroute plugin doctor' CLI command exists. There is no CLI wiring for this in bin/cli/commands/. Updated troubleshooting section accordingly. PLUGIN_MARKETPLACE.md: - Correct seed plugin ratings: cost-tracker is rating: 4, not 5. Updated from 'All 3 seed plugins have rating: 5' to explicit list: request-logger: 5, rate-limiter: 5, cost-tracker: 4. - Clarify that 'omniroute plugin search' (CLI command) queries npm for omniroute-cmd-* CLI plugins, separate from the SDK marketplace registry. Updated 'CLI Commands' section to show programmatic marketplace API instead. Added note distinguishing the two separate plugin systems. Fixes: diegosouzapw/OmniRoute#3452 (comment 4654628898) --- docs/plugins/PLUGIN_DEVELOPMENT.md | 74 +++++++++++++++--------------- docs/plugins/PLUGIN_MARKETPLACE.md | 57 ++++++++--------------- 2 files changed, 56 insertions(+), 75 deletions(-) diff --git a/docs/plugins/PLUGIN_DEVELOPMENT.md b/docs/plugins/PLUGIN_DEVELOPMENT.md index 5ffcce5a2248..5941a9728f8f 100644 --- a/docs/plugins/PLUGIN_DEVELOPMENT.md +++ b/docs/plugins/PLUGIN_DEVELOPMENT.md @@ -51,18 +51,11 @@ Every plugin goes through a **5-stage lifecycle** managed by `PluginManager` (`s └─────────────┘ ``` -### Lifecycle Hooks (v3.8.16+) +### Built-in Events -In addition to the per-request hooks, plugins can opt into **lifecycle hooks** that fire on transitions: +Plugins can register handlers for the following built-in events that cover the full request lifecycle, routing, rate limiting, and error handling. See [PLUGIN_SDK.md](./PLUGIN_SDK.md#built-in-events) for the complete list of events and their payloads. -| Hook | When | Use case | -|------|------|----------| -| `onInstall` | After files copied, before first activation | Initialize database tables, register schema | -| `onActivate` | When `activate()` is called | Connect to external service, warm caches | -| `onDeactivate` | Before deactivation completes | Close connections, flush logs | -| `onUninstall` | Before files are deleted | Final cleanup, send farewell webhook | - -These hooks are **opt-in** — define them on the plugin object and they will be called automatically: +The core request-handling events are: ```ts import { definePlugin } from "omniroute/plugins/sdk"; @@ -70,33 +63,34 @@ import { definePlugin } from "omniroute/plugins/sdk"; export default definePlugin({ name: "my-plugin", - onInstall: async (ctx) => { - console.log("Plugin installed, version:", ctx.version); - // e.g. create a database table - }, - - onActivate: async (ctx) => { - // e.g. open a connection pool - await connectToExternalService(); + // Core lifecycle events (always available) + onRequest: async (ctx) => { + console.log(`Request: ${ctx.requestId}`); }, - - onDeactivate: async (ctx) => { - // e.g. close connections cleanly - await closeConnections(); + + onResponse: async (ctx, response) => { + console.log(`Response for: ${ctx.requestId}`); + return response; // optionally transform }, - - onUninstall: async (ctx) => { - // e.g. delete plugin's database tables - await cleanup(); + + onError: async (ctx, error) => { + console.error(`Error: ${error.message}`); }, - - // Per-request hooks (unchanged) - onRequest: async (ctx) => { /* ... */ }, - onResponse: async (ctx, res) => { /* ... */ }, + + // Optional routing events + onModelSelect: async (ctx) => { /* ... */ }, + onComboResolve: async (ctx) => { /* ... */ }, + onRateLimit: async (ctx) => { /* ... */ }, + onQuotaExhaust: async (ctx) => { /* ... */ }, + onProviderError: async (ctx) => { /* ... */ }, + + // Stream events + onStreamStart: async (ctx) => { /* ... */ }, + onStreamEnd: async (ctx) => { /* ... */ }, }); ``` -> **Breaking change policy**: The lifecycle hook contract is stable within a major version. New lifecycle hooks may be added in minor versions, but existing ones will not be removed or renamed. +For the complete list of built-in events and their signatures, refer to `src/lib/plugins/hooks.ts` (BUILTIN_EVENTS constant). --- @@ -225,17 +219,18 @@ For full integration testing, use the [OmniRoute test harness](#integration-test ### Running the Doctor -```bash -# Via the CLI (if available in your build) -omniroute plugin doctor +The plugin doctor is available as a programmatic API only: -# Programmatically +```ts +// Programmatic API import { runPluginDoctor } from "omniroute/plugins/doctor"; const result = await runPluginDoctor("~/.omniroute/plugins/my-plugin", "my-plugin"); console.log(result); ``` +Note: There is no CLI command for the doctor. To diagnose plugins, integrate `runPluginDoctor` into your plugin management tooling. + ### The 5 Checks | # | Check name | What it verifies | Status values | @@ -507,9 +502,12 @@ Plugins without `network` permission cannot call `fetch` — the global is simpl ### "Plugin not loading" -Run the doctor: -```bash -omniroute plugin doctor +Use the plugin doctor (programmatically) to diagnose: +```ts +import { runPluginDoctor } from "omniroute/plugins/doctor"; + +const result = await runPluginDoctor("~/.omniroute/plugins/my-plugin", "my-plugin"); +console.log(result); ``` Check the 5 checks. The most common cause is `manifest_valid: fail` — usually a missing required field. diff --git a/docs/plugins/PLUGIN_MARKETPLACE.md b/docs/plugins/PLUGIN_MARKETPLACE.md index 95ff4490db76..d3feedf117eb 100644 --- a/docs/plugins/PLUGIN_MARKETPLACE.md +++ b/docs/plugins/PLUGIN_MARKETPLACE.md @@ -163,50 +163,33 @@ interface MarketplaceEntry { --- -## CLI Commands +## Accessing the Marketplace -The marketplace is exposed through the `omniroute plugin` command group: +The SDK marketplace is accessible **programmatically** via the marketplace API (`src/lib/plugins/marketplace.ts`), not through the CLI. -```bash -# List all available plugins in the marketplace -omniroute plugin search +### Programmatic Access + +```ts +import { listMarketplacePlugins, searchMarketplace } from "omniroute/plugins/marketplace"; -# Search by query -omniroute plugin search logging -omniroute plugin search security -omniroute plugin search analytics +// List all seed plugins +const allPlugins = await listMarketplacePlugins(); +console.log(allPlugins); // [request-logger, rate-limiter, cost-tracker] -# Show details for a specific plugin -omniroute plugin info request-logger +// Search by tag +const loggingPlugins = await searchMarketplace({ tags: ["logging"] }); +console.log(loggingPlugins); // [request-logger] -# Install a plugin from the marketplace -omniroute plugin install request-logger +// Search by name +const tracker = await searchMarketplace({ query: "cost" }); +console.log(tracker); // [cost-tracker] ``` -### Example Workflow +### Note: CLI Plugin System is Separate -```bash -$ omniroute plugin search logging -NAME VERSION RATING TAGS VERIFIED -request-logger 1.0.0 5.0★ logging,debug yes - -$ omniroute plugin info request-logger -Name: request-logger -Version: 1.0.0 -Author: omniroute -License: MIT -Description: Logs all requests and responses with timing -Tags: logging, debugging -Downloads: 1,234 -Rating: 5.0 / 5 -Verified: yes -Last update: 2026-05-29 -Repository: https://github.com/diegosouzapw/OmniRoute-plugins - -$ omniroute plugin install request-logger -Installing request-logger@1.0.0... done. -Plugin activated. Hooks: onRequest, onResponse -``` +The CLI command `omniroute plugin search` searches npm for **CLI plugins** (packages named `omniroute-cmd-*`), not the SDK marketplace. These are two separate plugin systems: +- **SDK plugins** (this doc): Hook-based request/response interception. Managed via the marketplace API. +- **CLI plugins** (separate system): Command-line extensions. Searched and installed via npm. --- @@ -306,7 +289,7 @@ When the remote registry launches, third-party authors can apply for verificatio ### Current State (Phase 1) -- All 3 seed plugins have `rating: 5` (set by the OmniRoute team) +- Seed plugins have initial ratings set by the OmniRoute team: `request-logger: 5`, `rate-limiter: 5`, `cost-tracker: 4` - No user ratings yet - All plugins are `verified: true` by definition From d862ded8b161afac78d032385a59c2b543391657 Mon Sep 17 00:00:00 2001 From: oyi77 Date: Wed, 10 Jun 2026 00:06:02 +0700 Subject: [PATCH 5/7] docs(plugins): fix review comments on plugin docs (#3452 review) - Normalize path separators in devMode code snippet for Windows compatibility - Remove invalid JSON comment from permissions example --- docs/plugins/PLUGIN_DEVELOPMENT.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plugins/PLUGIN_DEVELOPMENT.md b/docs/plugins/PLUGIN_DEVELOPMENT.md index 5941a9728f8f..2db4b566a007 100644 --- a/docs/plugins/PLUGIN_DEVELOPMENT.md +++ b/docs/plugins/PLUGIN_DEVELOPMENT.md @@ -125,7 +125,7 @@ export function startDevMode(pluginDir: string, reloadFn: ReloadFn): void { watcher = watch(pluginDir, { recursive: true }, (_eventType, filename) => { if (!filename) return; - const pluginName = filename.split("/")[0]; + const pluginName = filename.replace(/\\/g, "/").split("/")[0]; if (!pluginName || pluginName.startsWith(".")) return; // Debounce rapid changes @@ -489,7 +489,7 @@ Request **only the permissions you need**: ```json { "requires": { - "permissions": ["network"] // only if you call external APIs + "permissions": ["network"] } } ``` From 9a8e0ef345c94952e96706c04211fd1cf39040e5 Mon Sep 17 00:00:00 2001 From: oyi77 Date: Wed, 10 Jun 2026 02:50:01 +0700 Subject: [PATCH 6/7] docs(plugins): address remaining review comments on PR #3452 - Fix comment 1: Change 'Core lifecycle events' to 'Core request-handling events' and add note clarifying that onInstall/onActivate/onDeactivate/ onUninstall are manager-fired lifecycle events, not registerable via definePlugin() - Fix comment 2: Add lifecycle events to Built-in Events table in SDK doc with note that they are declared in manifest hooks field and fired by PluginManager, not registerable via definePlugin(). Add onActivate/ onDeactivate to manifest example. - Comments 3 and 4 were already fixed in prior commits (Windows path separator normalization, JSON comment removal) --- docs/plugins/PLUGIN_DEVELOPMENT.md | 4 +++- docs/plugins/PLUGIN_SDK.md | 9 ++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/plugins/PLUGIN_DEVELOPMENT.md b/docs/plugins/PLUGIN_DEVELOPMENT.md index 2db4b566a007..93bd2a650921 100644 --- a/docs/plugins/PLUGIN_DEVELOPMENT.md +++ b/docs/plugins/PLUGIN_DEVELOPMENT.md @@ -63,7 +63,7 @@ import { definePlugin } from "omniroute/plugins/sdk"; export default definePlugin({ name: "my-plugin", - // Core lifecycle events (always available) + // Core request-handling events (always available) onRequest: async (ctx) => { console.log(`Request: ${ctx.requestId}`); }, @@ -92,6 +92,8 @@ export default definePlugin({ For the complete list of built-in events and their signatures, refer to `src/lib/plugins/hooks.ts` (BUILTIN_EVENTS constant). +> **Note:** The `BUILTIN_EVENTS` list also includes **lifecycle events** (`onInstall`, `onActivate`, `onDeactivate`, `onUninstall`). These are fired internally by `PluginManager` during state transitions and are declared in the manifest's `hooks` field — they are not registerable via `definePlugin()`. + --- ## Dev Mode: Hot Reload on File Changes diff --git a/docs/plugins/PLUGIN_SDK.md b/docs/plugins/PLUGIN_SDK.md index 64527bdf13f1..a13f1cc01a8f 100644 --- a/docs/plugins/PLUGIN_SDK.md +++ b/docs/plugins/PLUGIN_SDK.md @@ -108,7 +108,9 @@ onRequest: (ctx) => { "hooks": { "onRequest": { "enabled": true, "priority": 50 }, "onResponse": true, - "onError": false + "onError": false, + "onActivate": true, + "onDeactivate": true }, "requires": { "permissions": ["network", "file-read"] @@ -196,6 +198,11 @@ Config values are persisted in the database and accessible via the dashboard con | `onProviderError` | Provider returned error | Error details | | `onStreamStart` | SSE stream started | Stream info | | `onStreamEnd` | SSE stream ended | Stream stats | +| `onInstall` | Plugin installed | `{ name, version, manifest }` | +| `onActivate` | Plugin activated | `{ name, version, manifest }` | +| `onDeactivate` | Plugin deactivated | `{ name, version, manifest }` | +| `onUninstall` | Plugin uninstalled | `{ name, version, manifest }` | +> **Note:** Lifecycle events (`onInstall`, `onActivate`, `onDeactivate`, `onUninstall`) are **fired internally by PluginManager** during state transitions. They are declared in the manifest's `hooks` field (e.g. `"onInstall": true`) and cannot be registered via `definePlugin()`. ## Examples From 16173e61c33f6d84fab7be846c7e9826d3b64d87 Mon Sep 17 00:00:00 2001 From: oyi77 Date: Wed, 10 Jun 2026 04:43:38 +0700 Subject: [PATCH 7/7] docs: fix fabricated lifecycle hooks and dev mode section per upstream review --- docs/plugins/PLUGIN_DEVELOPMENT.md | 71 +++++++++++------------------- docs/plugins/PLUGIN_SDK.md | 25 ++++++++--- src/lib/plugins/devMode.ts | 2 +- 3 files changed, 46 insertions(+), 52 deletions(-) diff --git a/docs/plugins/PLUGIN_DEVELOPMENT.md b/docs/plugins/PLUGIN_DEVELOPMENT.md index 93bd2a650921..b341d413a25d 100644 --- a/docs/plugins/PLUGIN_DEVELOPMENT.md +++ b/docs/plugins/PLUGIN_DEVELOPMENT.md @@ -53,17 +53,17 @@ Every plugin goes through a **5-stage lifecycle** managed by `PluginManager` (`s ### Built-in Events -Plugins can register handlers for the following built-in events that cover the full request lifecycle, routing, rate limiting, and error handling. See [PLUGIN_SDK.md](./PLUGIN_SDK.md#built-in-events) for the complete list of events and their payloads. +Plugins can register handlers for the following built-in events via `definePlugin()`. See [PLUGIN_SDK.md](./PLUGIN_SDK.md#built-in-events) for the complete list of events and their payloads. -The core request-handling events are: +The only events currently available to plugin developers are the three request-handling hooks on the `Plugin` interface (`src/lib/plugins/hooks.ts:249`): ```ts import { definePlugin } from "omniroute/plugins/sdk"; export default definePlugin({ name: "my-plugin", - - // Core request-handling events (always available) + + // Core request-handling events (available now) onRequest: async (ctx) => { console.log(`Request: ${ctx.requestId}`); }, @@ -76,47 +76,29 @@ export default definePlugin({ onError: async (ctx, error) => { console.error(`Error: ${error.message}`); }, - - // Optional routing events - onModelSelect: async (ctx) => { /* ... */ }, - onComboResolve: async (ctx) => { /* ... */ }, - onRateLimit: async (ctx) => { /* ... */ }, - onQuotaExhaust: async (ctx) => { /* ... */ }, - onProviderError: async (ctx) => { /* ... */ }, - - // Stream events - onStreamStart: async (ctx) => { /* ... */ }, - onStreamEnd: async (ctx) => { /* ... */ }, }); ``` -For the complete list of built-in events and their signatures, refer to `src/lib/plugins/hooks.ts` (BUILTIN_EVENTS constant). - -> **Note:** The `BUILTIN_EVENTS` list also includes **lifecycle events** (`onInstall`, `onActivate`, `onDeactivate`, `onUninstall`). These are fired internally by `PluginManager` during state transitions and are declared in the manifest's `hooks` field — they are not registerable via `definePlugin()`. - +> **Note:** `BUILTIN_EVENTS` (`src/lib/plugins/hooks.ts:35`) also lists routing events (`onModelSelect`, `onComboResolve`, etc.), stream events (`onStreamStart`, `onStreamEnd`), and lifecycle events (`onInstall`, `onActivate`, `onDeactivate`, `onUninstall`). None of these are currently exposed on the `Plugin` interface or `definePlugin()`. Plugin developers should use `onRequest`/`onResponse`/`onError` for now. See the [Built-in Events](./PLUGIN_SDK.md#built-in-events) section in the SDK reference for details on which events are available vs. planned. --- -## Dev Mode: Hot Reload on File Changes +## Dev Mode: Hot Reload on File Changes (Not Yet Wired) -**`src/lib/plugins/devMode.ts`** watches the plugin directory for file changes and automatically reloads affected plugins. This is the fastest way to iterate on a plugin during development. +> **⚠️ Not yet wired up.** `src/lib/plugins/devMode.ts` exports `startDevMode()` and `stopDevMode()`, but no caller invokes them. The environment variables `PLUGIN_DEV_MODE` and `OMNIROUTE_PLUGIN_DEV` are not read anywhere — `PLUGIN_DEV_MODE` is only used as a logger name (`src/lib/plugins/devMode.ts:12`). Dev mode is planned but not currently functional. -### Starting Dev Mode +**`src/lib/plugins/devMode.ts`** is designed to watch the plugin directory for file changes and automatically reload affected plugins. Once wired up, it will be the fastest way to iterate on a plugin during development. -```bash -# In the OmniRoute server, set the dev mode flag in your config -PLUGIN_DEV_MODE=true +### Planned Behavior -# Or via environment variable -OMNIROUTE_PLUGIN_DEV=1 omniroute -``` +When implemented, dev mode would work as follows: -When enabled, the server: -1. Watches `~/.omniroute/plugins/*` recursively -2. On any file change inside a plugin directory, waits 500ms (debounce) -3. Calls `deactivate(pluginName)` then `activate(pluginName)` to reload -4. Logs the result to the `PLUGIN_DEV_MODE` logger +1. A caller (e.g. server startup or CLI command) invokes `startDevMode(pluginDir, reloadFn)` +2. The watcher monitors `~/.omniroute/plugins/*` recursively +3. On any file change inside a plugin directory, it waits 500ms (debounce) +4. It calls `reloadFn(pluginName)` — typically `deactivate(pluginName)` then `activate(pluginName)` +5. Results are logged to the `PLUGIN_DEV_MODE` logger -### How It Works +### How It Works (source code) ```ts // src/lib/plugins/devMode.ts @@ -127,6 +109,7 @@ export function startDevMode(pluginDir: string, reloadFn: ReloadFn): void { watcher = watch(pluginDir, { recursive: true }, (_eventType, filename) => { if (!filename) return; + // Cross-platform: normalize backslashes before splitting const pluginName = filename.replace(/\\/g, "/").split("/")[0]; if (!pluginName || pluginName.startsWith(".")) return; @@ -139,18 +122,14 @@ export function startDevMode(pluginDir: string, reloadFn: ReloadFn): void { } ``` -### Dev Mode Tips - -- **Edit `plugin.json`**: changes are picked up on the next reload cycle. The manifest is re-validated each time. -- **Edit `index.js`**: same — the plugin is unloaded and re-loaded. -- **Multiple rapid saves**: the 500ms debounce means OmniRoute won't thrash during a flurry of saves. -- **Errors during reload**: the old version remains active. Check the server log (`PLUGIN_DEV_MODE` and `PLUGIN_MANAGER` loggers) for the error. - -### When NOT to Use Dev Mode +### What Needs to Happen for Dev Mode to Work -- **Production**: never set `PLUGIN_DEV_MODE=true` in production. The watcher adds CPU overhead and may briefly disable plugins during reloads. -- **Plugin uses native modules**: native bindings can't be re-initialized safely. Restart the server. +To make dev mode functional, the following would need to be implemented: +- A config flag or environment variable that triggers `startDevMode()` on server startup +- Integration with `PluginManager.deactivate()` + `PluginManager.activate()` as the reload function +- A `stopDevMode()` call on server shutdown +Until then, plugin developers should restart the OmniRoute server manually after editing plugin files. --- ## Testing Plugins @@ -529,9 +508,9 @@ You tried to use a global that requires a permission you didn't request. Add it } ``` -### "Plugin reloaded but my changes don't show" +### "My plugin changes don't take effect" -Dev mode has a 500ms debounce. Wait a moment, then check the server log for `PLUGIN_DEV_MODE` events. +Dev mode is not yet wired up (see [Dev Mode section](#dev-mode-hot-reload-on-file-changes-not-yet-wired) above). Restart the OmniRoute server manually after editing plugin files. --- diff --git a/docs/plugins/PLUGIN_SDK.md b/docs/plugins/PLUGIN_SDK.md index a13f1cc01a8f..b7a8910e542d 100644 --- a/docs/plugins/PLUGIN_SDK.md +++ b/docs/plugins/PLUGIN_SDK.md @@ -108,9 +108,7 @@ onRequest: (ctx) => { "hooks": { "onRequest": { "enabled": true, "priority": 50 }, "onResponse": true, - "onError": false, - "onActivate": true, - "onDeactivate": true + "onError": false }, "requires": { "permissions": ["network", "file-read"] @@ -186,11 +184,22 @@ Config values are persisted in the database and accessible via the dashboard con ## Built-in Events +### Available to plugin developers + +These are the events plugin developers can handle via `definePlugin()`. They correspond to the `onRequest`, `onResponse`, and `onError` fields on the [`Plugin` interface](./PLUGIN_DEVELOPMENT.md#built-in-events) (`src/lib/plugins/hooks.ts:249`). + | Event | When | Payload | |---|---|---| | `onRequest` | Before chat handler | Request context | | `onResponse` | After chat handler | Response data | | `onError` | On handler error | Error object | + +### Server-internal events (not registerable by plugins) + +The following events are declared in `BUILTIN_EVENTS` (`src/lib/plugins/hooks.ts:35`) and are emitted by the server internally. Plugin developers **cannot** register handlers for these via `definePlugin()` — the `Plugin` interface does not expose them. They may become available in a future release. + +| Event | When | Payload | +|---|---|---| | `onModelSelect` | Model selected for routing | Model info | | `onComboResolve` | Combo routing resolved | Combo targets | | `onRateLimit` | Rate limit hit | Limit info | @@ -198,13 +207,19 @@ Config values are persisted in the database and accessible via the dashboard con | `onProviderError` | Provider returned error | Error details | | `onStreamStart` | SSE stream started | Stream info | | `onStreamEnd` | SSE stream ended | Stream stats | + +### Lifecycle events (planned — not yet available to plugin developers) + +The following lifecycle events are declared in `BUILTIN_EVENTS` and `PluginManager` (`src/lib/plugins/manager.ts`) emits them during state transitions, but the typed `Plugin` interface and `definePlugin()` do **not** expose them. Plugin developers cannot currently handle these events. Use `onRequest`/`onResponse`/`onError` for now. + +| Event | When | Payload | +|---|---|---| | `onInstall` | Plugin installed | `{ name, version, manifest }` | | `onActivate` | Plugin activated | `{ name, version, manifest }` | | `onDeactivate` | Plugin deactivated | `{ name, version, manifest }` | | `onUninstall` | Plugin uninstalled | `{ name, version, manifest }` | -> **Note:** Lifecycle events (`onInstall`, `onActivate`, `onDeactivate`, `onUninstall`) are **fired internally by PluginManager** during state transitions. They are declared in the manifest's `hooks` field (e.g. `"onInstall": true`) and cannot be registered via `definePlugin()`. -## Examples +> **Note:** The manifest schema (`plugin.json`) accepts `onInstall`, `onActivate`, `onDeactivate`, and `onUninstall` in the `hooks` field, and `PluginManager` reads these to decide whether to emit the corresponding events. However, `definePlugin()` does not pass lifecycle handlers through to the `Plugin` object, so plugin code cannot currently respond to these events. ### Request Logger diff --git a/src/lib/plugins/devMode.ts b/src/lib/plugins/devMode.ts index f7e75c22db44..56235fa12e53 100644 --- a/src/lib/plugins/devMode.ts +++ b/src/lib/plugins/devMode.ts @@ -27,7 +27,7 @@ export function startDevMode(pluginDir: string, reloadFn: ReloadFn): void { if (!filename) return; // Extract plugin name from path (first segment) - const pluginName = filename.split("/")[0]; + const pluginName = filename.replace(/\\/g, "/").split("/")[0]; if (!pluginName || pluginName.startsWith(".")) return; // Debounce rapid changes