mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Release v3.8.26 (#3875)
OmniRoute v3.8.26 — see CHANGELOG.md [3.8.26] for the full notes. Highlights: Vertex AI media generation (#3929), GLM-5.2 effort-tier routing (#3885), sticky round-robin combos (#3846), OpenRouter connection presets (#3878), compression prompt-cache fix (#3936/#3890), and a security pass (form-data/vite + workflow hardening, #3949). Co-authored-by: artickc <artickc@users.noreply.github.com> Co-authored-by: rdself <rdself@users.noreply.github.com> Co-authored-by: herjarsa <herjarsa@users.noreply.github.com> Co-authored-by: Jack Smith <16862258+YunyunZhai@users.noreply.github.com> Co-authored-by: dhaern <dhaern@users.noreply.github.com> Co-authored-by: adivekar-utexas <adivekar-utexas@users.noreply.github.com> Co-authored-by: megamen32 <megamen32@users.noreply.github.com> Co-authored-by: zhiru <zhiru@users.noreply.github.com> Co-authored-by: insoln <insoln@users.noreply.github.com> Co-authored-by: diego-anselmo <diego-anselmo@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
1f87a9589c
commit
81a37b67ed
@@ -14,14 +14,14 @@ ACP (Agent Client Protocol) is a **"CLI-as-backend" transport** for OmniRoute. I
|
||||
|
||||
### Why Use ACP?
|
||||
|
||||
| Benefit | Description |
|
||||
|---------|-------------|
|
||||
| **No API keys needed** | Uses your existing CLI authentication |
|
||||
| **Native protocol** | Uses each CLI's native input/output format |
|
||||
| **Auto-discovery** | Detects installed CLIs on your system |
|
||||
| **14 built-in agents** | Pre-configured for popular CLI tools |
|
||||
| **Custom agents** | Add your own CLI tools via settings |
|
||||
| **Process management** | Handles lifecycle (spawn, send, kill) |
|
||||
| Benefit | Description |
|
||||
| ---------------------- | ------------------------------------------ |
|
||||
| **No API keys needed** | Uses your existing CLI authentication |
|
||||
| **Native protocol** | Uses each CLI's native input/output format |
|
||||
| **Auto-discovery** | Detects installed CLIs on your system |
|
||||
| **14 built-in agents** | Pre-configured for popular CLI tools |
|
||||
| **Custom agents** | Add your own CLI tools via settings |
|
||||
| **Process management** | Handles lifecycle (spawn, send, kill) |
|
||||
|
||||
---
|
||||
|
||||
@@ -29,22 +29,22 @@ ACP (Agent Client Protocol) is a **"CLI-as-backend" transport** for OmniRoute. I
|
||||
|
||||
ACP supports **14 built-in CLI agents** out of the box:
|
||||
|
||||
| Agent ID | Display Name | Binary | Protocol |
|
||||
|----------|--------------|--------|----------|
|
||||
| `codex` | OpenAI Codex CLI | `codex` | stdio |
|
||||
| `claude` | Claude Code CLI | `claude` | stdio |
|
||||
| `goose` | Goose CLI | `goose` | stdio |
|
||||
| `gemini-cli` | Gemini CLI | `gemini` | stdio |
|
||||
| `openclaw` | OpenClaw | `openclaw` | stdio |
|
||||
| `aider` | Aider | `aider` | stdio |
|
||||
| `opencode` | OpenCode | `opencode` | stdio |
|
||||
| `cline` | Cline | `cline` | stdio |
|
||||
| `qwen-code` | Qwen Code | `qwen` | stdio |
|
||||
| `forge` | ForgeCode | `forge` | stdio |
|
||||
| `amazon-q` | Amazon Q Developer | `q` | stdio |
|
||||
| `interpreter` | Open Interpreter | `interpreter` | stdio |
|
||||
| `cursor-cli` | Cursor CLI | `cursor` | stdio |
|
||||
| `warp` | Warp AI | `warp` | stdio |
|
||||
| Agent ID | Display Name | Binary | Protocol |
|
||||
| ------------- | ------------------ | ------------- | -------- |
|
||||
| `codex` | OpenAI Codex CLI | `codex` | stdio |
|
||||
| `claude` | Claude Code CLI | `claude` | stdio |
|
||||
| `goose` | Goose CLI | `goose` | stdio |
|
||||
| `gemini-cli` | Gemini CLI | `gemini` | stdio |
|
||||
| `openclaw` | OpenClaw | `openclaw` | stdio |
|
||||
| `aider` | Aider | `aider` | stdio |
|
||||
| `opencode` | OpenCode | `opencode` | stdio |
|
||||
| `cline` | Cline | `cline` | stdio |
|
||||
| `qwen-code` | Qwen Code | `qwen` | stdio |
|
||||
| `forge` | ForgeCode | `forge` | stdio |
|
||||
| `amazon-q` | Amazon Q Developer | `q` | stdio |
|
||||
| `interpreter` | Open Interpreter | `interpreter` | stdio |
|
||||
| `cursor-cli` | Cursor CLI | `cursor` | stdio |
|
||||
| `warp` | Warp AI | `warp` | stdio |
|
||||
|
||||
### Custom Agents
|
||||
|
||||
@@ -129,16 +129,16 @@ const agents = detectInstalledAgents();
|
||||
// Returns: CliAgentInfo[]
|
||||
|
||||
interface CliAgentInfo {
|
||||
id: string; // e.g., "codex", "claude"
|
||||
name: string; // Display name
|
||||
binary: string; // Binary name to spawn
|
||||
versionCommand: string; // Version detection command
|
||||
version: string | null; // Detected version (null if not installed)
|
||||
installed: boolean; // Whether the agent is installed
|
||||
providerAlias: string; // Provider ID in OmniRoute
|
||||
spawnArgs: string[]; // Arguments to pass when spawning
|
||||
protocol: "stdio" | "http"; // Communication protocol
|
||||
isCustom?: boolean; // Whether this is a user-defined custom agent
|
||||
id: string; // e.g., "codex", "claude"
|
||||
name: string; // Display name
|
||||
binary: string; // Binary name to spawn
|
||||
versionCommand: string; // Version detection command
|
||||
version: string | null; // Detected version (null if not installed)
|
||||
installed: boolean; // Whether the agent is installed
|
||||
providerAlias: string; // Provider ID in OmniRoute
|
||||
spawnArgs: string[]; // Arguments to pass when spawning
|
||||
protocol: "stdio" | "http"; // Communication protocol
|
||||
isCustom?: boolean; // Whether this is a user-defined custom agent
|
||||
}
|
||||
```
|
||||
|
||||
@@ -193,12 +193,9 @@ Spawns a new CLI agent process.
|
||||
```typescript
|
||||
import { acpManager } from "@/lib/acp";
|
||||
|
||||
const session = acpManager.spawn(
|
||||
"claude",
|
||||
"claude",
|
||||
["--print", "--output-format", "json"],
|
||||
{ /* custom env vars */ }
|
||||
);
|
||||
const session = acpManager.spawn("claude", "claude", ["--print", "--output-format", "json"], {
|
||||
/* custom env vars */
|
||||
});
|
||||
// Returns: AcpSession
|
||||
```
|
||||
|
||||
@@ -214,7 +211,7 @@ import { acpManager } from "@/lib/acp";
|
||||
const response = await acpManager.sendPrompt(
|
||||
"acp-claude-1234567890-abc123",
|
||||
"What is 2+2?",
|
||||
120000 // 2 minutes timeout
|
||||
120000 // 2 minutes timeout
|
||||
);
|
||||
// Returns: Promise<string>
|
||||
```
|
||||
@@ -255,13 +252,13 @@ acpManager.killAll();
|
||||
|
||||
```typescript
|
||||
interface AcpSession {
|
||||
id: string; // Unique session ID
|
||||
agentId: string; // Agent ID (e.g., "claude")
|
||||
process: ChildProcess; // Child process handle
|
||||
alive: boolean; // Whether the process is alive
|
||||
stdoutBuffer: string; // Accumulated stdout buffer
|
||||
stderrBuffer: string; // Accumulated stderr buffer
|
||||
createdAt: Date; // Created timestamp
|
||||
id: string; // Unique session ID
|
||||
agentId: string; // Agent ID (e.g., "claude")
|
||||
process: ChildProcess; // Child process handle
|
||||
alive: boolean; // Whether the process is alive
|
||||
stdoutBuffer: string; // Accumulated stdout buffer
|
||||
stderrBuffer: string; // Accumulated stderr buffer
|
||||
createdAt: Date; // Created timestamp
|
||||
}
|
||||
```
|
||||
|
||||
@@ -412,6 +409,7 @@ Each ACP session runs in its own child process. The process is killed when the s
|
||||
**Problem**: `acpManager.spawn()` throws `Unknown agent: <id>`
|
||||
|
||||
**Solution**: Only 4 agents are allowed in `spawn()`:
|
||||
|
||||
- `claude`
|
||||
- `codex`
|
||||
- `gemini`
|
||||
@@ -448,6 +446,7 @@ await acpManager.sendPrompt(sessionId, prompt, 300000); // 5 minutes
|
||||
**Problem**: `detectInstalledAgents()` doesn't find your CLI
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Check PATH**: Ensure the CLI is in your system PATH
|
||||
2. **Check version command**: Run `claude --version` manually
|
||||
3. **Check permissions**: Ensure the CLI is executable
|
||||
@@ -458,6 +457,7 @@ await acpManager.sendPrompt(sessionId, prompt, 300000); // 5 minutes
|
||||
**Problem**: ACP can't execute the CLI
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Check file permissions**: `chmod +x /usr/local/bin/claude`
|
||||
2. **Check ownership**: Ensure OmniRoute has read/execute permissions
|
||||
3. **Check SELinux/AppArmor**: May block process spawning
|
||||
@@ -477,11 +477,7 @@ const claude = agents.find((a) => a.id === "claude");
|
||||
|
||||
if (claude?.installed) {
|
||||
// Spawn a new session
|
||||
const session = acpManager.spawn(
|
||||
"claude",
|
||||
claude.binary,
|
||||
["--print", "--output-format", "json"]
|
||||
);
|
||||
const session = acpManager.spawn("claude", claude.binary, ["--print", "--output-format", "json"]);
|
||||
|
||||
// Send a prompt
|
||||
const response = await acpManager.sendPrompt(
|
||||
@@ -548,7 +544,7 @@ const agents = detectInstalledAgents();
|
||||
## What's Next?
|
||||
|
||||
- **[API Reference](../reference/API_REFERENCE.md)** — REST API endpoints
|
||||
- **[Provider Reference](../reference/PROVIDER_REFERENCE.md)** — All 223 providers
|
||||
- **[Provider Reference](../reference/PROVIDER_REFERENCE.md)** — All 226 providers
|
||||
- **[MCP Server](./MCP-SERVER.md)** — Model Context Protocol integration
|
||||
- **[A2A Server](./A2A-SERVER.md)** — Agent-to-Agent protocol
|
||||
- **[Cloud Agent](./CLOUD_AGENT.md)** — Cloud-based agents
|
||||
|
||||
@@ -162,7 +162,7 @@ See [A2A-SERVER.md](./A2A-SERVER.md) for protocol details.
|
||||
| `omni-sync-cloud` | sync-cloud | Cloud sync |
|
||||
| `omni-db-backups` | db-backups | Database backups |
|
||||
| `omni-webhooks` | webhooks | Webhook event dispatcher |
|
||||
| `omni-mcp` | mcp | MCP server (37 tools, 3 transports) |
|
||||
| `omni-mcp` | mcp | MCP server (87 tools, 3 transports) |
|
||||
| `omni-agents-a2a` | agents-a2a | A2A agent protocol |
|
||||
| `omni-version-manager` | version-manager | Version and update management |
|
||||
| `omni-inference` | inference | Direct inference / completions |
|
||||
|
||||
@@ -22,7 +22,7 @@ When an IDE agent (e.g., GitHub Copilot, Cursor, Claude Code) makes an API call,
|
||||
|
||||
This means you can:
|
||||
|
||||
- **Reroute any agent to any provider**: Copilot talking to OpenAI? Redirect it to Anthropic Claude, Gemini, or any of OmniRoute's 160+ providers.
|
||||
- **Reroute any agent to any provider**: Copilot talking to OpenAI? Redirect it to Anthropic Claude, Gemini, or any of OmniRoute's 226+ providers.
|
||||
- **Apply model mappings**: `gemini-3-flash` → `claude-sonnet-4.7` transparently at the handler level.
|
||||
- **Observe all agent traffic**: every intercepted request is published to the [Traffic Inspector](./TRAFFIC_INSPECTOR.md).
|
||||
- **Apply OmniRoute resilience**: combo routing, circuit breakers, fallbacks, and cost tracking work for IDE agent traffic too.
|
||||
|
||||
@@ -6,13 +6,13 @@ lastUpdated: 2026-05-30
|
||||
|
||||
# OmniRoute MCP Server Documentation
|
||||
|
||||
> Model Context Protocol server with 43 tools across routing, cache, compression, memory, skills, proxy, and context source operations.
|
||||
> Model Context Protocol server with 87 tools across routing, cache, compression, memory, skills, proxy, and context source operations.
|
||||
>
|
||||
> Source of truth: `open-sse/mcp-server/schemas/tools.ts` (30 tools) + `open-sse/mcp-server/tools/memoryTools.ts` (3 tools) + `open-sse/mcp-server/tools/skillTools.ts` (4 tools) + `open-sse/mcp-server/tools/notionTools.ts` (6 tools). Tool registration and scope wiring lives in `open-sse/mcp-server/server.ts`.
|
||||
> Source of truth: `open-sse/mcp-server/schemas/tools.ts` (33 base) + `memoryTools.ts` (3) + `skillTools.ts` (4) + `agentSkillTools.ts` (3) + `gamificationTools.ts` (8) + `pluginTools.ts` (8) + `notionTools.ts` (6) + `obsidianTools.ts` (22) = **87** (`TOTAL_MCP_TOOL_COUNT`). Tool registration and scope wiring lives in `open-sse/mcp-server/server.ts`.
|
||||
|
||||

|
||||

|
||||
|
||||
> Source: [diagrams/mcp-tools-43.mmd](../diagrams/mcp-tools-43.mmd) (regenerate via `npm run docs:render-diagrams`).
|
||||
> Source: [diagrams/mcp-tools-87.mmd](../diagrams/mcp-tools-87.mmd) (regenerate via `npm run docs:render-diagrams`).
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -217,7 +217,7 @@ See [AGENT-SKILLS.md](./AGENT-SKILLS.md) for the full catalog and how external a
|
||||
|
||||
## Related Frameworks (v3.8.0)
|
||||
|
||||
The MCP tool inventory above (43 tools = 30 core + 3 memory + 4 skills + 6 notion) is intentionally
|
||||
The MCP tool inventory above (87 tools = 33 core + 3 memory + 4 skills + 3 agent-skills + 8 gamification + 8 plugins + 6 notion + 22 obsidian) is intentionally
|
||||
scoped to runtime routing/cache/compression/memory/skills/proxy/context-source operations. Two adjacent
|
||||
frameworks ship alongside the MCP server in v3.8.0 and are documented separately:
|
||||
|
||||
|
||||
131
docs/frameworks/NOTION_CONTEXT.md
Normal file
131
docs/frameworks/NOTION_CONTEXT.md
Normal file
@@ -0,0 +1,131 @@
|
||||
---
|
||||
title: "Notion Context Source"
|
||||
version: 3.8.24
|
||||
lastUpdated: 2026-06-13
|
||||
---
|
||||
|
||||
# Notion Context Source
|
||||
|
||||
> **Source of truth:** `src/lib/notion/api.ts` (REST client), `src/lib/db/notion.ts`
|
||||
> (token persistence), `open-sse/mcp-server/tools/notionTools.ts` (6 MCP tools),
|
||||
> `src/app/api/settings/notion/route.ts` (settings API). Tool registration and scope
|
||||
> wiring lives in `open-sse/mcp-server/server.ts`.
|
||||
|
||||
## What it is
|
||||
|
||||
OmniRoute can connect to a **Notion** workspace as a **context source** — a read/write
|
||||
knowledge base that agents reach through the built-in MCP server. Once a Notion
|
||||
integration token is configured, the MCP tools let an LLM search pages and databases,
|
||||
read page content and block trees, query databases with filters/sorts, and append new
|
||||
blocks — all proxied through OmniRoute (with retry, timeout, and error classification)
|
||||
so the model never touches the Notion API directly.
|
||||
|
||||
The integration is a thin, hardened wrapper over the official Notion REST API
|
||||
(`https://api.notion.com/v1`, `Notion-Version: 2026-03-11`). The client
|
||||
(`src/lib/notion/api.ts`) adds:
|
||||
|
||||
- **Retry with exponential backoff** (up to 3 attempts) for `429` and `5xx`.
|
||||
- **55-second request timeout** via `AbortController`.
|
||||
- **Typed error classification** — `NotionAuthError` (401/403),
|
||||
`NotionNotFoundError` (404), `NotionRateLimitError` (429, honors `retry after`
|
||||
hints), `NotionValidationError` (400/409), `NotionServerError` (5xx),
|
||||
`NotionTimeoutError`.
|
||||
- **Message sanitization** that strips stack-trace-like fragments before surfacing.
|
||||
|
||||
## Setup
|
||||
|
||||
There is **no environment variable** for the Notion token — it is stored in the
|
||||
SQLite `key_value` table (namespace `notion`, key `integration_token`) via
|
||||
`src/lib/db/notion.ts`. Configure it from the **Context Sources** tab of the Endpoint
|
||||
dashboard (`ObsidianSourceCard`'s sibling `NotionSourceCard`), or via the settings REST API.
|
||||
|
||||
> [!NOTE]
|
||||
> The token is a **Notion internal integration token**. Create an integration at
|
||||
> <https://www.notion.com/my-integrations>, then share the pages/databases you want
|
||||
> OmniRoute to access with that integration (Notion's permission model is share-based,
|
||||
> not workspace-wide).
|
||||
|
||||
### Configure via REST
|
||||
|
||||
```bash
|
||||
# Save + validate the integration token (POST validates by issuing a test search)
|
||||
curl -X POST http://localhost:20128/api/settings/notion \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"token":"ntn_xxx"}'
|
||||
|
||||
# Check connection status
|
||||
curl http://localhost:20128/api/settings/notion
|
||||
|
||||
# Disconnect (clears the stored token)
|
||||
curl -X DELETE http://localhost:20128/api/settings/notion
|
||||
```
|
||||
|
||||
All three methods require dashboard authentication (`isAuthenticated`). On `POST`,
|
||||
OmniRoute saves the token and immediately runs a 1-result test search; if Notion
|
||||
returns an error object the token is cleared and the call fails with `400`.
|
||||
|
||||
## MCP tools (6)
|
||||
|
||||
Defined in `open-sse/mcp-server/tools/notionTools.ts`. The token is resolved at call
|
||||
time via `getNotionToken()`; if none is configured the tool throws
|
||||
`"Notion integration token not configured. Set it in Settings > Context Sources."`
|
||||
|
||||
| Tool | Scope | Description |
|
||||
| ---------------------------- | -------------- | --------------------------------------------------------------------------------- |
|
||||
| `notion_search` | `read:notion` | Search pages and databases by text query (returns titles, IDs, URLs). Paginated. |
|
||||
| `notion_get_page` | `read:notion` | Get content and metadata of a page by its ID. |
|
||||
| `notion_list_block_children` | `read:notion` | List all block children of a block or page (the block tree). Paginated. |
|
||||
| `notion_query_database` | `read:notion` | Query a database with optional `filter` + `sorts` (Notion API format). Paginated. |
|
||||
| `notion_get_database` | `read:notion` | Get the schema/metadata of a database by ID. |
|
||||
| `notion_append_blocks` | `write:notion` | Append block children to an existing block or page (max 100 blocks per request). |
|
||||
|
||||
### Input parameters
|
||||
|
||||
- `notion_search` — `query` (1–500 chars), `pageSize` (1–100, default 20),
|
||||
`startCursor` (optional).
|
||||
- `notion_get_page` — `pageId` (32-char hex or UUID).
|
||||
- `notion_list_block_children` — `blockId`, `pageSize` (1–100, default 50),
|
||||
`startCursor` (optional).
|
||||
- `notion_query_database` — `databaseId`, `filter` (optional, Notion filter format),
|
||||
`sorts` (optional array), `pageSize` (1–100, default 50), `startCursor` (optional).
|
||||
- `notion_get_database` — `databaseId`.
|
||||
- `notion_append_blocks` — `blockId`, `children` (array of block objects),
|
||||
`after` (optional position).
|
||||
|
||||
### Scopes
|
||||
|
||||
The read tools require `read:notion` and the write tool requires `write:notion`.
|
||||
Scopes are enforced by `withScopeEnforcement()` in
|
||||
`open-sse/mcp-server/server.ts` only when `OMNIROUTE_MCP_ENFORCE_SCOPES=true`; the
|
||||
caller's allowed scopes come from `OMNIROUTE_MCP_SCOPES` (comma-separated) or the
|
||||
authenticated API key's scope context. See [MCP-SERVER.md](./MCP-SERVER.md) for the
|
||||
full scope model.
|
||||
|
||||
## Endpoints
|
||||
|
||||
| Method | Path | Purpose |
|
||||
| -------- | ---------------------- | -------------------------------------- |
|
||||
| `GET` | `/api/settings/notion` | Return `{ connected, hasToken }`. |
|
||||
| `POST` | `/api/settings/notion` | Save + validate the integration token. |
|
||||
| `DELETE` | `/api/settings/notion` | Disconnect (clear the stored token). |
|
||||
|
||||
> These are dashboard settings routes. There is **no public `/v1` Notion proxy
|
||||
> endpoint** — Notion is reached exclusively through the MCP tools above.
|
||||
|
||||
## Use cases
|
||||
|
||||
- **Knowledge-grounded answers** — let an agent `notion_search` the workspace and
|
||||
`notion_get_page` the top hit before answering, so responses cite real internal docs.
|
||||
- **Database-backed workflows** — `notion_query_database` a tasks/CRM database with
|
||||
filters + sorts, then summarize or triage the rows.
|
||||
- **Write-back / logging** — `notion_append_blocks` to append meeting notes, run
|
||||
summaries, or agent output into an existing page (append-only; no destructive edits).
|
||||
- **Structure exploration** — `notion_list_block_children` to walk a page's block tree,
|
||||
or `notion_get_database` to discover a database's property schema before querying it.
|
||||
|
||||
## Related
|
||||
|
||||
- [MCP Server](./MCP-SERVER.md) — transports, scope enforcement, full tool inventory.
|
||||
- [Obsidian Context Source](./OBSIDIAN_CONTEXT.md) — the other built-in context source.
|
||||
- [Memory System](./MEMORY.md) — persistent conversational memory (complementary
|
||||
context layer, injected automatically rather than tool-fetched).
|
||||
191
docs/frameworks/OBSIDIAN_CONTEXT.md
Normal file
191
docs/frameworks/OBSIDIAN_CONTEXT.md
Normal file
@@ -0,0 +1,191 @@
|
||||
---
|
||||
title: "Obsidian Context Source"
|
||||
version: 3.8.24
|
||||
lastUpdated: 2026-06-13
|
||||
---
|
||||
|
||||
# Obsidian Context Source
|
||||
|
||||
> **Source of truth:** `src/lib/obsidian/api.ts` (REST + sync client),
|
||||
> `src/lib/db/obsidian.ts` (token / base-URL / WebDAV persistence),
|
||||
> `src/lib/obsidianSync.ts` (WebDAV vault sync), `open-sse/mcp-server/tools/obsidianTools.ts`
|
||||
> (22 MCP tools), `src/app/api/settings/obsidian/route.ts` +
|
||||
> `src/app/api/settings/obsidian/webdav/route.ts` (settings APIs). Tool registration
|
||||
> and scope wiring lives in `open-sse/mcp-server/server.ts`.
|
||||
|
||||
## What it is
|
||||
|
||||
OmniRoute connects to an **Obsidian** vault as a **context source** — a local Markdown
|
||||
knowledge base that agents read and write through the built-in MCP server. The
|
||||
integration talks to the **Obsidian Local REST API** community plugin running inside the
|
||||
desktop app, so agents can search notes, read/write/patch files, list the vault, work
|
||||
with daily/weekly periodic notes, manage tags, run Obsidian commands, and (optionally)
|
||||
coordinate a bidirectional desktop↔mobile vault sync.
|
||||
|
||||
The client (`src/lib/obsidian/api.ts`) wraps the Local REST API with:
|
||||
|
||||
- **Retry with backoff** for transient `5xx`, **30-second timeout** via `AbortController`.
|
||||
- **Typed error classification** — `ObsidianAuthError` (401/403),
|
||||
`ObsidianNotFoundError` (404), `ObsidianServerError` (5xx), `ObsidianTimeoutError`.
|
||||
- A **friendly "cannot reach Obsidian" hint** that calls out the common port mistake
|
||||
(HTTP on `27123`, **not** the MCP endpoint on `27124`) and the Tailscale form.
|
||||
- Vault-relative **path encoding** so note paths with spaces/slashes are safe.
|
||||
|
||||
## Setup
|
||||
|
||||
There is **no environment variable** for the Obsidian token or base URL — both are
|
||||
stored in the SQLite `key_value` table (namespace `obsidian`) via
|
||||
`src/lib/db/obsidian.ts`. The token is **encrypted at rest** (AES-256-GCM, with
|
||||
plaintext backward-compat fallback). Configure from the **Context Sources** tab of the
|
||||
Endpoint dashboard (`ObsidianSourceCard`), or via the settings REST API.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The **Obsidian Local REST API** plugin must be installed and running. Its REST
|
||||
> interface listens on **HTTP `127.0.0.1:27123`** (the default base URL). Port `27124`
|
||||
> is a _separate_ MCP/HTTPS endpoint and is explicitly rejected by the settings route.
|
||||
> If connecting from another device, use `http://<tailscale-ip>:27123`.
|
||||
|
||||
### Configuration keys (SQLite `key_value`, namespace `obsidian`)
|
||||
|
||||
| Key | Purpose | Encrypted |
|
||||
| ----------------- | ------------------------------------------------ | --------- |
|
||||
| `api_key` | Local REST API bearer token | yes |
|
||||
| `base_url` | REST base URL (default `http://127.0.0.1:27123`) | no |
|
||||
| `vault_path` | Absolute path to the vault directory (for sync) | no |
|
||||
| `webdav_username` | Generated WebDAV username (vault sync) | no |
|
||||
| `webdav_password` | Generated WebDAV password (vault sync) | yes |
|
||||
| `webdav_enabled` | Whether WebDAV vault sync is enabled | no |
|
||||
|
||||
### Configure via REST
|
||||
|
||||
```bash
|
||||
# Save + validate the Local REST API token (POST validates via a status check)
|
||||
curl -X POST http://localhost:20128/api/settings/obsidian \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"token":"<obsidian-rest-api-key>","baseUrl":"http://127.0.0.1:27123"}'
|
||||
|
||||
# Check connection status (returns connected, hasToken, baseUrl, vaultPath)
|
||||
curl http://localhost:20128/api/settings/obsidian
|
||||
|
||||
# Disconnect (clears the stored token)
|
||||
curl -X DELETE http://localhost:20128/api/settings/obsidian
|
||||
```
|
||||
|
||||
All methods require dashboard authentication. `POST` rejects any URL on port `27124`
|
||||
and validates the token by calling the Local REST API status endpoint before persisting.
|
||||
|
||||
### WebDAV vault sync
|
||||
|
||||
`src/app/api/settings/obsidian/webdav/route.ts` manages an optional WebDAV-backed
|
||||
vault sync (driven by `src/lib/obsidianSync.ts`). Enabling it points OmniRoute at a
|
||||
local vault directory and mints a random WebDAV username/password pair:
|
||||
|
||||
```bash
|
||||
# Enable WebDAV sync for a vault directory (mints username/password)
|
||||
curl -X POST http://localhost:20128/api/settings/obsidian/webdav \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"vaultPath":"/home/me/MyVault"}'
|
||||
|
||||
# Get WebDAV sync status (credentials returned only while enabled)
|
||||
curl http://localhost:20128/api/settings/obsidian/webdav
|
||||
|
||||
# Disable WebDAV sync (clears credentials + managed .stignore)
|
||||
curl -X DELETE http://localhost:20128/api/settings/obsidian/webdav
|
||||
```
|
||||
|
||||
### Per-API-key context source (optional)
|
||||
|
||||
Obsidian config can be scoped **per API key** via the `api_key_context_sources` table
|
||||
(`src/lib/db/apiKeyContextSources.ts`). When an MCP call carries an authenticated API
|
||||
key id, `getObsidianConfigForApiKey()` prefers that key's own token/base-URL/vault-path
|
||||
(`source: "api_key"`) and otherwise falls back to the global config (`source: "global"`).
|
||||
|
||||
## MCP tools (22)
|
||||
|
||||
Defined in `open-sse/mcp-server/tools/obsidianTools.ts`. The token/base-URL are resolved
|
||||
per call (per-API-key first, then global). Tools that hit the OmniRoute **sync server**
|
||||
(the four `obsidian_sync_*` tools) additionally require the sync auth token configured
|
||||
in OmniRoute settings.
|
||||
|
||||
### Read tools (`read:obsidian`)
|
||||
|
||||
| Tool | Description |
|
||||
| ---------------------------- | ------------------------------------------------------------------------------------ |
|
||||
| `obsidian_check_status` | Check whether the Local REST API is reachable and authenticated. |
|
||||
| `obsidian_search_simple` | Full-text search of note content; returns snippets with file paths. |
|
||||
| `obsidian_search_structured` | Search using a JSON Logic expression (and/or/regex/path filters). |
|
||||
| `obsidian_read_note` | Read a note by vault-relative path; optionally a specific heading/block/frontmatter. |
|
||||
| `obsidian_list_vault` | List files and directories in the vault (tree of entries). |
|
||||
| `obsidian_get_document_map` | Get the note's heading structure as a map of headings → line numbers. |
|
||||
| `obsidian_get_note_metadata` | Get frontmatter, tags, links, char/word count without the full content. |
|
||||
| `obsidian_get_active_file` | Get the path + content of the currently active file in Obsidian. |
|
||||
| `obsidian_get_periodic_note` | Get the daily/weekly/monthly periodic note for a date (today if omitted). |
|
||||
| `obsidian_get_tags` | List all vault tags with their frequencies. |
|
||||
| `obsidian_list_commands` | List available Obsidian command IDs (use with `obsidian_execute_command`). |
|
||||
| `obsidian_sync_status` | OmniRoute sync server status: running, vault name, port, uptime, last sync. |
|
||||
| `obsidian_sync_conflicts` | List unresolved sync conflicts (path, conflict path, detected-at). |
|
||||
|
||||
### Write tools (`write:obsidian`)
|
||||
|
||||
| Tool | Description |
|
||||
| -------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| `obsidian_write_note` | Create or overwrite a note with given Markdown content. |
|
||||
| `obsidian_append_note` | Append content to a note; optionally to a specific heading/block. |
|
||||
| `obsidian_patch_note` | Surgically append/prepend/replace at a heading, block, or frontmatter field. |
|
||||
| `obsidian_delete_note` | Permanently delete a note from the vault. |
|
||||
| `obsidian_move_note` | Move or rename a note within the vault. |
|
||||
| `obsidian_execute_command` | Execute an Obsidian command by its command ID. |
|
||||
| `obsidian_open_file` | Open a file in Obsidian (creates it if it does not exist). |
|
||||
| `obsidian_sync_trigger` | Trigger an immediate bidirectional desktop↔mobile vault sync. |
|
||||
| `obsidian_sync_resolve_conflict` | Resolve a sync conflict: keep `local` (mobile), `remote` (desktop), or `keep-both`. |
|
||||
|
||||
> [!NOTE]
|
||||
> `obsidian_patch_note` targets accept `targetType` of `heading | block | frontmatter`
|
||||
> and `operation` of `append | prepend | replace`, with an optional
|
||||
> `createTargetIfMissing`. The four `obsidian_sync_*` tools talk to the local sync
|
||||
> server (`http://127.0.0.1:27781` by default) and require the sync token.
|
||||
|
||||
### Scopes
|
||||
|
||||
Read tools require `read:obsidian`; write tools require `write:obsidian`. Enforcement
|
||||
is identical to Notion — handled by `withScopeEnforcement()` in
|
||||
`open-sse/mcp-server/server.ts`, gated on `OMNIROUTE_MCP_ENFORCE_SCOPES=true`, with
|
||||
allowed scopes sourced from `OMNIROUTE_MCP_SCOPES` or the API key's scope context. See
|
||||
[MCP-SERVER.md](./MCP-SERVER.md).
|
||||
|
||||
## Endpoints
|
||||
|
||||
| Method | Path | Purpose |
|
||||
| -------- | ------------------------------- | ----------------------------------------------------- |
|
||||
| `GET` | `/api/settings/obsidian` | Return `{ connected, hasToken, baseUrl, vaultPath }`. |
|
||||
| `POST` | `/api/settings/obsidian` | Save + validate token (rejects port `27124`). |
|
||||
| `DELETE` | `/api/settings/obsidian` | Disconnect (clear stored token). |
|
||||
| `GET` | `/api/settings/obsidian/webdav` | WebDAV sync status + credentials (while enabled). |
|
||||
| `POST` | `/api/settings/obsidian/webdav` | Enable WebDAV sync for a vault directory. |
|
||||
| `DELETE` | `/api/settings/obsidian/webdav` | Disable WebDAV sync. |
|
||||
|
||||
> These are dashboard settings routes. The vault itself is reached through the Obsidian
|
||||
> Local REST API (the configured `base_url`) and through the MCP tools above — there is
|
||||
> no public `/v1` Obsidian proxy endpoint.
|
||||
|
||||
## Use cases
|
||||
|
||||
- **Vault-grounded answers** — `obsidian_search_simple` / `obsidian_search_structured`
|
||||
then `obsidian_read_note` so an agent answers from your real notes.
|
||||
- **Note authoring / journaling** — `obsidian_write_note`, `obsidian_append_note`, or
|
||||
the surgical `obsidian_patch_note` to log agent output, summaries, or daily notes
|
||||
(`obsidian_get_periodic_note`) into the vault.
|
||||
- **Vault navigation** — `obsidian_list_vault`, `obsidian_get_document_map`, and
|
||||
`obsidian_get_tags` to explore structure before reading/writing.
|
||||
- **Obsidian automation** — `obsidian_list_commands` + `obsidian_execute_command` to
|
||||
drive plugins/commands from an agent; `obsidian_open_file` to surface a note in the UI.
|
||||
- **Mobile sync** — enable WebDAV sync, then `obsidian_sync_trigger` /
|
||||
`obsidian_sync_status` / `obsidian_sync_conflicts` / `obsidian_sync_resolve_conflict`
|
||||
to coordinate desktop↔mobile and resolve conflicts.
|
||||
|
||||
## Related
|
||||
|
||||
- [MCP Server](./MCP-SERVER.md) — transports, scope enforcement, full tool inventory.
|
||||
- [Notion Context Source](./NOTION_CONTEXT.md) — the other built-in context source.
|
||||
- [Memory System](./MEMORY.md) — persistent conversational memory (complementary
|
||||
context layer, injected automatically rather than tool-fetched).
|
||||
348
docs/frameworks/PLUGIN_MARKETPLACE.md
Normal file
348
docs/frameworks/PLUGIN_MARKETPLACE.md
Normal file
@@ -0,0 +1,348 @@
|
||||
---
|
||||
title: "Plugin Marketplace"
|
||||
version: 3.8.24
|
||||
lastUpdated: 2026-06-13
|
||||
---
|
||||
|
||||
# Plugin Marketplace
|
||||
|
||||
> **Source of truth:** `src/lib/plugins/` (`marketplace.ts`, `manager.ts`, `manifest.ts`,
|
||||
> `scanner.ts`, `loader.ts`), `src/app/api/plugins/`, and
|
||||
> `src/app/(dashboard)/dashboard/plugins/`
|
||||
> **Last updated:** 2026-06-13 — v3.8.24
|
||||
|
||||
OmniRoute ships a WordPress-style plugin system. Plugins are self-contained
|
||||
directories — each with a `plugin.json` manifest and an entry file — that hook
|
||||
into the request pipeline (`onRequest` / `onResponse` / `onError`) and into
|
||||
lifecycle events (`onInstall` / `onActivate` / `onDeactivate` / `onUninstall`).
|
||||
|
||||
The **Plugin Marketplace** is the discovery layer on top of that system. It
|
||||
exposes a browsable catalog of installable plugins. By default the catalog is a
|
||||
small built-in seed registry; an operator can point it at a custom remote
|
||||
registry URL, in which case the fetch is hardened by a DNS-resolving SSRF guard
|
||||
(see [Security](#security)).
|
||||
|
||||
Every plugin route is **loopback-only** (Tier 1 — `LOCAL_ONLY`): plugins load
|
||||
and execute code in child processes, so the routes are unreachable from a
|
||||
non-loopback origin regardless of auth. See
|
||||
[`docs/security/ROUTE_GUARD_TIERS.md`](../security/ROUTE_GUARD_TIERS.md).
|
||||
|
||||
## How It Fits Together
|
||||
|
||||
```
|
||||
Dashboard (/dashboard/plugins)
|
||||
├─ "Installed" tab → GET /api/plugins (listPlugins)
|
||||
│ POST /api/plugins/scan (pluginManager.scan)
|
||||
│ POST /api/plugins/{name}/activate|deactivate
|
||||
│ DELETE /api/plugins/{name} (uninstall)
|
||||
└─ "Marketplace" tab → GET /api/plugins/marketplace
|
||||
→ listMarketplacePlugins()
|
||||
├─ no custom URL → built-in SEED_REGISTRY
|
||||
└─ custom URL → isSafeMarketplaceUrl() SSRF guard
|
||||
→ safeOutboundFetch(guard:"public-only")
|
||||
```
|
||||
|
||||
- **Registry layer** — `src/lib/plugins/marketplace.ts`: lists / searches the
|
||||
catalog, falling back to the seed registry on any failure.
|
||||
- **Lifecycle layer** — `src/lib/plugins/manager.ts` (`pluginManager` singleton):
|
||||
install, upgrade, activate, deactivate, uninstall, scan, startup load.
|
||||
- **Manifest layer** — `src/lib/plugins/manifest.ts`: Zod schema + defaults for
|
||||
`plugin.json`.
|
||||
- **Scanner** — `src/lib/plugins/scanner.ts`: discovers plugins on disk under
|
||||
the plugin directory.
|
||||
- **Loader** — `src/lib/plugins/loader.ts`: spawns each plugin in an isolated
|
||||
child process and brokers hook calls over IPC.
|
||||
|
||||
## Marketplace Catalog
|
||||
|
||||
`listMarketplacePlugins()` (`src/lib/plugins/marketplace.ts`) returns a list of
|
||||
`MarketplaceEntry` objects:
|
||||
|
||||
| Field | Type | Notes |
|
||||
| ------------- | -------- | ------------------------------------ |
|
||||
| `name` | string | kebab-case plugin name |
|
||||
| `version` | string | semver |
|
||||
| `description` | string | Short summary |
|
||||
| `author` | string | Author / org |
|
||||
| `license` | string | SPDX-style license id |
|
||||
| `downloadUrl` | string | Source download URL (may be empty) |
|
||||
| `repository` | string? | Optional repository URL |
|
||||
| `tags` | string[] | Search/filter tags |
|
||||
| `downloads` | number | Download count |
|
||||
| `rating` | number | 0–5 |
|
||||
| `verified` | boolean | Whether the entry is marked verified |
|
||||
| `lastUpdated` | string | ISO-ish date string |
|
||||
|
||||
When no custom registry URL is configured, the catalog is the built-in
|
||||
`SEED_REGISTRY` (currently `request-logger`, `rate-limiter`, `cost-tracker`, and
|
||||
`theme-manager`). The seed registry is always available — if a configured remote
|
||||
registry is unreachable, returns a non-`200` status, or returns an unrecognized
|
||||
body, `listMarketplacePlugins()` logs a warning and falls back to the seed list.
|
||||
|
||||
> Note: the marketplace **catalog** (browse/search) is wired end to end, but
|
||||
> one-click marketplace **install** from the catalog is not yet implemented — the
|
||||
> dashboard's "Install" button on a marketplace entry currently shows a
|
||||
> "coming soon" notice. Installation today goes through the local-path install
|
||||
> flow (`POST /api/plugins`) and on-disk discovery (`POST /api/plugins/scan`).
|
||||
|
||||
## REST API
|
||||
|
||||
All endpoints require management auth (`requireManagementAuth`) **and** are
|
||||
loopback-only — `/api/plugins` and `/api/plugins/` are listed in
|
||||
`LOCAL_ONLY_API_PREFIXES` (`src/server/authz/routeGuard.ts`).
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
| -------------------------------- | ------ | --------------------------------------------------- |
|
||||
| `/api/plugins` | GET | List installed plugins (optional `?status=` filter) |
|
||||
| `/api/plugins` | POST | Install a plugin from an absolute local path |
|
||||
| `/api/plugins/scan` | POST | Scan the plugin directory and register new plugins |
|
||||
| `/api/plugins/marketplace` | GET | List marketplace catalog entries |
|
||||
| `/api/plugins/[name]` | GET | Get installed plugin details |
|
||||
| `/api/plugins/[name]` | DELETE | Uninstall a plugin |
|
||||
| `/api/plugins/[name]/activate` | POST | Activate (load + register hooks) |
|
||||
| `/api/plugins/[name]/deactivate` | POST | Deactivate (fire `onDeactivate`, unregister hooks) |
|
||||
| `/api/plugins/[name]/config` | GET | Get plugin config + config schema |
|
||||
| `/api/plugins/[name]/config` | PUT | Update plugin config (validated against schema) |
|
||||
|
||||
The `GET /api/plugins` `status` filter accepts one of
|
||||
`installed` / `active` / `inactive` / `error`. An invalid value returns `400`.
|
||||
|
||||
### List installed plugins
|
||||
|
||||
```bash
|
||||
curl http://localhost:20128/api/plugins \
|
||||
-H "Cookie: auth_token=..."
|
||||
```
|
||||
|
||||
### Install from a local path
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:20128/api/plugins \
|
||||
-H "Cookie: auth_token=..." \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{ "path": "/absolute/path/to/my-plugin" }'
|
||||
```
|
||||
|
||||
The `path` must be **absolute** and may not contain `..` traversal segments or
|
||||
null bytes (enforced by Zod). The source directory must contain a valid
|
||||
`plugin.json` (or be a parent of one). On success the response is `201` with the
|
||||
installed plugin row.
|
||||
|
||||
### Browse the marketplace
|
||||
|
||||
```bash
|
||||
curl http://localhost:20128/api/plugins/marketplace \
|
||||
-H "Cookie: auth_token=..."
|
||||
```
|
||||
|
||||
### Update plugin config
|
||||
|
||||
```bash
|
||||
curl -X PUT http://localhost:20128/api/plugins/my-plugin/config \
|
||||
-H "Cookie: auth_token=..." \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{ "config": { "level": "debug", "maxItems": 100 } }'
|
||||
```
|
||||
|
||||
`PUT .../config` validates each provided value against the plugin's
|
||||
`configSchema` (declared in the manifest): `number` fields honor `min`/`max`,
|
||||
`select` fields must match the declared `enum`. Keys not present in the schema
|
||||
are allowed through.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Plugin directory
|
||||
|
||||
Plugins live under the OmniRoute data directory:
|
||||
|
||||
```
|
||||
~/.omniroute/plugins/<plugin-name>/
|
||||
├─ plugin.json
|
||||
└─ index.js # (or whatever manifest.main points to)
|
||||
```
|
||||
|
||||
`getDefaultPluginDir()` (`src/lib/plugins/scanner.ts`) resolves this to
|
||||
`<home>/.omniroute/plugins`, where `<home>` is taken from the `HOME` /
|
||||
`USERPROFILE` environment variables. `POST /api/plugins/scan` discovers any
|
||||
subdirectory there that holds a valid `plugin.json` and registers it.
|
||||
|
||||
### Custom marketplace registry URL
|
||||
|
||||
The marketplace catalog source is read from the `pluginMarketplaceUrl` setting
|
||||
(`src/lib/plugins/marketplace.ts` reads `settings.pluginMarketplaceUrl`). When
|
||||
set to an `http(s)` URL, `listMarketplacePlugins()` fetches that URL and accepts
|
||||
either a top-level JSON array of entries or an object with a `plugins` array;
|
||||
entries without a string `name` are filtered out. When unset (or when the fetch
|
||||
fails the SSRF guard / returns a bad response), the built-in seed registry is
|
||||
used.
|
||||
|
||||
The dashboard "Marketplace" tab exposes a field for this URL (read back from
|
||||
`GET /api/settings`).
|
||||
|
||||
> Implementation note: the dashboard "Save" action sends
|
||||
> `pluginMarketplaceUrl` to `PATCH /api/settings`. At the time of writing this
|
||||
> key is not declared in `updateSettingsSchema`
|
||||
> (`src/shared/validation/settingsSchemas.ts`), so verify persistence in your
|
||||
> release before relying on it — the **read** path (`getSettings()` →
|
||||
> `listMarketplacePlugins()`) honors the key once it is present in the settings
|
||||
> store.
|
||||
|
||||
## Security
|
||||
|
||||
### Route tier — loopback only
|
||||
|
||||
Plugins execute code in spawned child processes, so the entire `/api/plugins`
|
||||
surface is classified `LOCAL_ONLY` (Tier 1). Loopback enforcement runs
|
||||
unconditionally **before** any auth check, so a leaked management token reaching
|
||||
the box over a tunnel still cannot install, activate, or uninstall a plugin.
|
||||
See [`docs/security/ROUTE_GUARD_TIERS.md`](../security/ROUTE_GUARD_TIERS.md) and
|
||||
Hard Rules #15 / #17.
|
||||
|
||||
### Marketplace registry SSRF guard
|
||||
|
||||
A custom registry URL is attacker-influenceable configuration, so before
|
||||
fetching it `listMarketplacePlugins()` runs it through two layers:
|
||||
|
||||
1. **`isSafeMarketplaceUrl(url)`** (`src/lib/plugins/marketplace.ts`):
|
||||
- Rejects anything that is not `http:` / `https:`.
|
||||
- Rejects literal private/loopback/link-local/ULA hosts (IPv4 **and** IPv6,
|
||||
including IPv4-mapped) via the canonical `isPrivateHost`
|
||||
(`src/shared/network/outboundUrlGuard.ts`).
|
||||
- Resolves **both** `A` and `AAAA` records and rejects if **any** resolved
|
||||
address is private — closing the public-hostname → private-IP bypass.
|
||||
- **Fails closed**: a DNS resolution failure rejects the URL.
|
||||
2. **`safeOutboundFetch(url, { guard: "public-only", timeoutMs: 5000 })`**
|
||||
(`src/shared/network/safeOutboundFetch.ts`): re-applies the public-only URL
|
||||
guard at fetch time and **blocks redirects** (no public → private `30x`
|
||||
pivot).
|
||||
|
||||
A URL that fails either layer does not abort the request — the marketplace
|
||||
silently falls back to the built-in seed registry and logs a warning.
|
||||
|
||||
> This guard was hardened in PR #3774 specifically to resolve A + AAAA and use
|
||||
> the canonical `isPrivateHost` instead of an IPv4-only check.
|
||||
|
||||
### Plugin execution isolation
|
||||
|
||||
- **Process isolation** — `loadPlugin()` (`src/lib/plugins/loader.ts`) spawns
|
||||
each plugin in a separate Node.js child process and communicates over IPC.
|
||||
Hook calls have a timeout with `SIGTERM` → `SIGKILL` escalation.
|
||||
- **Env allowlist** — the child receives only an allowlisted set of environment
|
||||
variables; the broader set is only granted when the manifest requests the
|
||||
`env` permission.
|
||||
- **Path containment** — install/upgrade/uninstall assert that the plugin
|
||||
directory and `manifest.main` resolve **within** the managed plugin root
|
||||
before any copy or recursive delete (guards against tampered DB paths and
|
||||
`../` traversal in `manifest.main`). Activation resolves symlinks via
|
||||
`realpath` and refuses to load an entry point that escapes the plugin
|
||||
directory.
|
||||
- **Optional integrity pin** — a manifest may declare an `integrity`
|
||||
(`sha256-<base64>`, SRI format) field. When present, the loader verifies the
|
||||
entry file hash at load time and refuses to activate on mismatch. It is
|
||||
opt-in tamper-detection, **not** a security boundary — loopback-only routing
|
||||
and the permission model are the real boundaries.
|
||||
|
||||
## Manifest (`plugin.json`)
|
||||
|
||||
Validated by `PluginManifestSchema` (`src/lib/plugins/manifest.ts`):
|
||||
|
||||
| Field | Type | Notes |
|
||||
| ------------------ | --------- | ----------------------------------------------------------- |
|
||||
| `name` | string | Required; kebab-case (`^[a-z0-9-]+$`), 1–100 chars |
|
||||
| `version` | string | Required; semver (`MAJOR.MINOR.PATCH`) |
|
||||
| `description` | string? | ≤ 500 chars |
|
||||
| `author` | string? | ≤ 200 chars |
|
||||
| `license` | string? | Defaults to `MIT` |
|
||||
| `main` | string? | Entry file; defaults to `index.js` |
|
||||
| `source` | enum? | `local` \| `marketplace` (defaults to `local`) |
|
||||
| `tags` | string[]? | Search tags |
|
||||
| `requires` | object? | `{ omniroute?, permissions[] }` |
|
||||
| `hooks` | object? | Booleans declaring which hooks the plugin implements |
|
||||
| `skills` | object[]? | Optional skill definitions |
|
||||
| `enabledByDefault` | boolean? | Auto-activate on install |
|
||||
| `configSchema` | object? | Map of config fields (`string`/`number`/`boolean`/`select`) |
|
||||
| `integrity` | string? | Optional `sha256-<base64>` entry-file pin |
|
||||
|
||||
Permissions are drawn from the enum
|
||||
`network` / `file-read` / `file-write` / `env` / `exec`.
|
||||
|
||||
## Lifecycle Flow
|
||||
|
||||
```
|
||||
install (POST /api/plugins, path)
|
||||
→ scan/validate manifest → copy to staging → assert main within dir
|
||||
→ atomic rename into ~/.omniroute/plugins/<name> → insert DB row
|
||||
→ fire onInstall → if enabledByDefault: activate
|
||||
|
||||
activate (POST /api/plugins/{name}/activate)
|
||||
→ realpath containment check → loadPlugin() (spawn child process)
|
||||
→ register declared hooks → status = "active" → fire onActivate
|
||||
|
||||
deactivate (POST /api/plugins/{name}/deactivate)
|
||||
→ fire onDeactivate (BEFORE unregister) → unregister hooks
|
||||
→ kill child process → status = "inactive"
|
||||
|
||||
uninstall (DELETE /api/plugins/{name})
|
||||
→ deactivate if active → fire onUninstall
|
||||
→ containment-checked recursive delete of plugin dir → delete DB row
|
||||
```
|
||||
|
||||
Re-running `install` against a directory whose manifest version is **strictly
|
||||
newer** than the installed version auto-upgrades (clean reinstall; config resets
|
||||
to defaults). A same-or-older version is rejected.
|
||||
|
||||
## Database
|
||||
|
||||
Table `plugins` (migration `076_create_plugins.sql`):
|
||||
|
||||
| Column | Type | Notes |
|
||||
| --------------- | ------- | ------------------------------------------------ |
|
||||
| `id` | TEXT PK | UUID |
|
||||
| `name` | TEXT | Unique |
|
||||
| `version` | TEXT | semver; default `1.0.0` |
|
||||
| `description` | TEXT | Optional |
|
||||
| `author` | TEXT | Optional |
|
||||
| `license` | TEXT | Default `MIT` |
|
||||
| `main` | TEXT | Entry file; default `index.js` |
|
||||
| `source` | TEXT | Default `local` |
|
||||
| `tags` | TEXT | JSON array; default `[]` |
|
||||
| `status` | TEXT | `installed` \| `active` \| `inactive` \| `error` |
|
||||
| `enabled` | INT | 0/1; default 0 |
|
||||
| `manifest` | TEXT | Full manifest JSON |
|
||||
| `config` | TEXT | JSON; default `{}` |
|
||||
| `config_schema` | TEXT | JSON; default `{}` |
|
||||
| `hooks` | TEXT | JSON array of declared hook names; default `[]` |
|
||||
| `permissions` | TEXT | JSON array; default `[]` |
|
||||
| `plugin_dir` | TEXT | Absolute install directory |
|
||||
| `error_message` | TEXT | Set when `status = "error"` |
|
||||
| `installed_at` | TEXT | `datetime('now')` |
|
||||
| `updated_at` | TEXT | `datetime('now')` |
|
||||
| `activated_at` | TEXT | Set on activation |
|
||||
|
||||
Plugin metrics/analytics are tracked in additional tables
|
||||
(`090_plugin_metrics.sql`, `091_plugin_analytics.sql`).
|
||||
|
||||
## Dashboard
|
||||
|
||||
The dashboard page at `/dashboard/plugins`
|
||||
(`src/app/(dashboard)/dashboard/plugins/page.tsx`) provides two tabs:
|
||||
|
||||
- **Installed** — lists installed plugins with their declared hooks, an
|
||||
activate/deactivate toggle, an uninstall button, and a "Scan for plugins"
|
||||
action (`POST /api/plugins/scan`).
|
||||
- **Marketplace** — shows the catalog from `GET /api/plugins/marketplace` with a
|
||||
field to set the custom registry URL.
|
||||
|
||||
A per-plugin config page lives at `/dashboard/plugins/[name]/config`
|
||||
(`src/app/(dashboard)/dashboard/plugins/[name]/config/page.tsx`).
|
||||
|
||||
## See Also
|
||||
|
||||
- [`docs/security/ROUTE_GUARD_TIERS.md`](../security/ROUTE_GUARD_TIERS.md) —
|
||||
why `/api/plugins` is loopback-only (Tier 1)
|
||||
- [`docs/frameworks/SKILLS.md`](./SKILLS.md) — the related skills framework
|
||||
(`src/lib/skills/`); plugins may declare skills in their manifest
|
||||
- [`docs/frameworks/WEBHOOKS.md`](./WEBHOOKS.md) — event-driven outbound
|
||||
integrations
|
||||
- [`docs/security/ERROR_SANITIZATION.md`](../security/ERROR_SANITIZATION.md) —
|
||||
the `buildErrorBody()` pattern every plugin route uses for error responses
|
||||
@@ -9,7 +9,10 @@
|
||||
"EVALS",
|
||||
"GAMIFICATION",
|
||||
"MEMORY",
|
||||
"NOTION_CONTEXT",
|
||||
"OBSIDIAN_CONTEXT",
|
||||
"OPENCODE",
|
||||
"PLUGIN_MARKETPLACE",
|
||||
"SKILLS",
|
||||
"WEBHOOKS"
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user