Repository navigation
Conversation
- Path traversal guard: validate entryPoint stays within plugin dir - install() now handles direct plugin directories (not just parent dirs) - Non-null assertion replaced with explicit null check - require efficiency: allowedModules map moved outside function - Source wrapper: add newlines to prevent trailing comment issues - Config validation: validate values against configSchema on save - Dynamic import comment: clarify Node.js caching behavior Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
Addresses all remaining code review feedback: 1. **Loader rewrite**: Replaced Node.js vm module with child_process.fork() for proper process-level isolation. Complies with Rule 3 (no eval). Each plugin runs in a separate Node.js process with IPC communication. 2. **Auth on all routes**: Added requireManagementAuth to all 6 plugin API route files (list, install, scan, details, activate, deactivate, config). 3. **Env filtering**: Only safe env vars passed to plugin processes unless "env" permission is granted. Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
loader.ts:
- Fix IPC: use process.send()/process.on("message") instead of worker_threads.parentPort
- Fix ESM: write host script as .mjs (not .js) to force ESM execution
- Add timeout: 10s default on callHook() with Promise.race
- Add SIGKILL escalation: SIGTERM first, then SIGKILL after 3s grace
- Fix env filtering: use allowlist (safeKeys) instead of passing all env vars
- Clear timeout on successful IPC response (no timer leak)
manager.ts:
- Fix path traversal: use fs.realpath() instead of startsWith()
- Fix imports: use registerHook/unregisterHooks from hooks.ts
- Register hooks individually via registerHook(event, name, handler)
hooks.ts:
- Copied from feat/plugin-custom-hooks (canonical registry)
Phase 1 scaffold for automated provider discovery: - DiscoveryConfig, DiscoveryResult types - probeEndpoint() for URL availability checking - scanProvider() stub (Phase 2 will implement real scanning) - getDiscoveryResults() stub - Default config: disabled (opt-in)
- index.ts: replace console.log/error with pino structured logging - hooks.ts: remove redundant .sort() in emitHookBlocking/runOnResponse (already sorted on registration) - manager.ts: add readFile import
- scanner: 9 tests (discovery, hidden dirs, validation, entry point, multiple) - loader: 5 tests (type contracts, Plugin/PluginContext/PluginResult interfaces) - manager: 6 tests (singleton, lifecycle methods, error on unknown) - Total: 20 tests, all passing
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive plugin management system, including lifecycle tracking, a custom hook registry, sandboxed loading, and dashboard UI pages, while removing legacy gamification and token limit features. The review feedback highlights critical security vulnerabilities, specifically sandbox escapes through the Node.js vm module and path traversal risks in the plugin scanner. Additionally, the feedback addresses functional issues such as broken hook chaining in emitHookBlocking, performance overhead from dynamic imports in chatCore.ts, and silent UI failures during plugin activation.
| const wrapped = `(async function(module, exports, require) { ${source} })(module, exports, require);`; | ||
| vm.runInContext(wrapped, context, { | ||
| filename: entryPoint, | ||
| timeout: 10000, // 10s init timeout | ||
| }); |
There was a problem hiding this comment.
The Node.js vm module is not a secure sandbox. Untrusted plugins running in vm can easily escape the sandbox and execute arbitrary code on the host system (e.g., by accessing the constructor of standard objects to run arbitrary shell commands). If this plugin system is intended to support third-party plugins or a marketplace, using vm poses a critical security risk. Consider using a more secure isolation library like isolated-vm or running plugins in separate worker threads/processes with restricted permissions.
| for (const reg of list.sort((a, b) => a.priority - b.priority)) { | ||
| try { | ||
| const result = await reg.handler(payload); |
There was a problem hiding this comment.
In emitHookBlocking, subsequent hook handlers are called with the original payload object. If a previous handler modified the request body or metadata, those modifications are accumulated in mergedBody and mergedMetadata but are not passed to the next handler in the chain. This breaks hook chaining. We should pass an updated copy of the payload to each handler.
for (const reg of list.sort((a, b) => a.priority - b.priority)) {
try {
const currentPayload = { ...ctx, body: mergedBody, metadata: mergedMetadata };
const result = await reg.handler(currentPayload);| const manifest = result.data; | ||
| const entryPoint = join(pluginDir, manifest.main); | ||
|
|
There was a problem hiding this comment.
There is a potential path traversal vulnerability when resolving the plugin's entry point. If a malicious or malformed plugin.json specifies a main path containing directory traversal sequences (e.g., ../../malicious.js), join(pluginDir, manifest.main) will resolve to a path outside the plugin's directory. We should validate that the resolved entryPoint is strictly within pluginDir.
const manifest = result.data;
const entryPoint = join(pluginDir, manifest.main);
if (!entryPoint.startsWith(pluginDir)) {
errors.push({
name: entry,
error: `entry point must be within the plugin directory`,
});
continue;
}| try { | ||
| const { runOnRequest } = await import("@/lib/plugins/hooks"); | ||
| const pluginCtx = { |
There was a problem hiding this comment.
Importing @/lib/plugins/hooks dynamically on every request (and doing so multiple times for different hooks like runOnRequest, runOnResponse, runOnError, and emitHook) introduces unnecessary overhead. Since there are no circular dependencies, these should be statically imported at the top of the file for better performance.
| if (res.ok) { | ||
| addNotification({ type: "success", message: `${name} ${enable ? "activated" : "deactivated"}` }); | ||
| await fetchPlugins(); | ||
| } |
There was a problem hiding this comment.
In handleToggle, if the fetch request returns a non-OK status (e.g., 400 Bad Request because activation failed), the promise still resolves successfully, so the catch block is not triggered. As a result, no error notification is shown, and the failure is silently ignored in the UI. We should handle res.ok explicitly to show an error notification.
if (res.ok) {
addNotification({ type: "success", message: `${name} ${enable ? "activated" : "deactivated"}` });
await fetchPlugins();
} else {
addNotification({ type: "error", message: `Failed to ${enable ? "activate" : "deactivate"} ${name}` });
}
…l_logs only The 3.8.6 variant of diegosouzapw#2904 added SELECTs of combo_name/requested_model against usage_history, but those columns only exist in call_logs (no migration adds them to usage_history). This returned HTTP 500 on /api/usage/analytics. Restore the working query shape from the 3.8.7 variant. Fixes 18 failing usage-analytics-route tests.
…maphore test to diegosouzapw#2903 gate pruning - progressiveAging: type compression results so messages[0].content is indexable (was TS7053 against {}); restores typecheck:noimplicit:core gate. - services-branch-hardening: diegosouzapw#2903 (perf-ram) prunes idle rate-limit gates on zero; assert no-running/empty-queue without assuming the entry persists.
- llm.txt → 3.8.7 (Current version + Key Features header) - CHANGELOG: add Dmitry Kuznetsov & Nikolay Alafuzov to 3.8.6 Hall of Contributors - version already 3.8.7 across package.json/open-sse/electron/openapi (from diegosouzapw#2909)
| endpoint TEXT, | ||
| models_json TEXT DEFAULT '[]', | ||
| rate_limit TEXT, | ||
| feasibility INTEGER DEFAULT 0 CHECK (feasibility BETWEEN 1 AND 5), |
There was a problem hiding this comment.
CRITICAL: DEFAULT value 0 for feasibility conflicts with CHECK constraint requiring values BETWEEN 1 AND 5. Rows using the default will violate the constraint and fail to insert.
Either change DEFAULT to 1, or update the CHECK to allow 0: CHECK (feasibility >= 0 AND feasibility <= 5).
…rsing (diegosouzapw#2463) (diegosouzapw#2923) Integrated into release/v3.8.7
…apw#2832) (diegosouzapw#2846) Integrated into release/v3.8.7
|
|
||
| if (permissions.includes("file-read")) { | ||
| sandbox.fs = { | ||
| readFile: (p: string, enc?: string) => readFile(resolve(pluginDir, p), enc as BufferEncoding), |
There was a problem hiding this comment.
WARNING: Potential directory traversal via absolute path in file operations. The plugin is granted file-read permission but can use absolute paths to read/write outside the plugin directory. The sandbox should restrict paths to be within pluginDir.
Explanation: The resolve(pluginDir, p) function will return p if it is an absolute path, allowing plugins to bypass the intended directory restriction.
4b9cbb0 to
b1b78d9
Compare
Backend: - WordPress-style plugin system with scanner, loader, manager - Custom hooks with event-driven registry and priority - VM sandbox with 5 permission gates (path-restricted) - Plugin upgrade with semver comparison - Discovery implementation with real provider probing - Execution metrics tracking (calls, errors, latency) - Hot-reload with fs.watch and debounce - Plugin signing with SHA-256 integrity verification - Worker thread scaffold for v4.0 isolation Frontend: - Dashboard pages (list + config) with useTranslations - 42 locale i18n support (34 plugin keys) - 9 MCP tools for plugin management Tests: 138 total (hooks, scanner, loader, manager, tools, edge-cases, config-route, upgrade, metrics, signing, lifecycle) Docs: SDK documentation at docs/plugins/PLUGIN_SDK.md
b1b78d9 to
55dd7dd
Compare
|
Hi @oyi77 — thank you for the ambitious plugin system work here! 🙏 We're keeping this deferred to v4.0.0-rc1 as you flagged. While reviewing it for integration I want to flag one important security item and a concrete path forward. 🔒 Loader sandbox regression. On this branch Path forward for v4: could you rebase this onto the merged #2912 backend so the loader stays
One small robustness note: Happy to help coordinate the v4 integration — thanks again for pushing this forward! 🚀 |
…nalytics, sandbox, doctor, logger, dev mode Phase 1 (Security): config validation, rate limiting (100/sec/plugin), memory limit, install hooks Phase 3 (DX): error codes (14 PluginErrorCodes), per-plugin logger, dev mode hot-reload, doctor diagnostics, test runner Phase 4 (Ecosystem): signing (SHA-256 + Ed25519), sandbox levels (4), analytics (migration 079) 8 new source files, 9 new test files, 4 modified files. 158/158 tests pass. 0 typecheck errors, 0 lint errors.
# Conflicts: # open-sse/handlers/chatCore.ts # open-sse/mcp-server/server.ts # open-sse/mcp-server/tools/pluginTools.ts # src/app/api/plugins/[name]/activate/route.ts # src/app/api/plugins/[name]/config/route.ts # src/app/api/plugins/[name]/deactivate/route.ts # src/app/api/plugins/[name]/route.ts # src/app/api/plugins/route.ts # src/app/api/plugins/scan/route.ts # src/i18n/messages/ar.json # src/i18n/messages/az.json # src/i18n/messages/bg.json # src/i18n/messages/bn.json # src/i18n/messages/cs.json # src/i18n/messages/da.json # src/i18n/messages/de.json # src/i18n/messages/es.json # src/i18n/messages/fa.json # src/i18n/messages/fi.json # src/i18n/messages/fr.json # src/i18n/messages/gu.json # src/i18n/messages/he.json # src/i18n/messages/hi.json # src/i18n/messages/hu.json # src/i18n/messages/id.json # src/i18n/messages/in.json # src/i18n/messages/it.json # src/i18n/messages/ja.json # src/i18n/messages/ko.json # src/i18n/messages/mr.json # src/i18n/messages/ms.json # src/i18n/messages/nl.json # src/i18n/messages/no.json # src/i18n/messages/phi.json # src/i18n/messages/pl.json # src/i18n/messages/pt-BR.json # src/i18n/messages/pt.json # src/i18n/messages/ro.json # src/i18n/messages/ru.json # src/i18n/messages/sk.json # src/i18n/messages/sv.json # src/i18n/messages/sw.json # src/i18n/messages/ta.json # src/i18n/messages/te.json # src/i18n/messages/th.json # src/i18n/messages/tr.json # src/i18n/messages/uk-UA.json # src/i18n/messages/ur.json # src/i18n/messages/vi.json # src/i18n/messages/zh-CN.json # src/lib/db/plugins.ts # src/lib/discovery/index.ts # src/lib/localDb.ts # src/lib/plugins/hooks.ts # src/lib/plugins/index.ts # src/lib/plugins/loader.ts # src/lib/plugins/manager.ts # src/lib/plugins/manifest.ts # tests/unit/plugins-loader.test.ts
- Fix DEFAULT value conflict in 077_discovery_results.sql (0 -> 1) - Add path validation in pluginTools.ts to prevent directory traversal - Add proper Zod validation for status parameter in route.ts - Add regex validation for path to prevent traversal patterns - Wrap recordPluginMetric in try/catch for best-effort metrics
…els (#3041) Integrates two community contributions into release/v3.8.8 with security hardening and conflict resolution. - **Plugins framework** (#2913 — thanks @oyi77): hooks + registry unification, plugin SDK (`definePlugin`), worker-thread sandbox, per-plugin hook rate limiting, SHA-256 integrity verification, semver-gated upgrade, and execution analytics. Plugin routes are loopback-only (`isLocalOnlyPath`); `child_process` exec is opt-in via `OMNIROUTE_PLUGINS_ALLOW_EXEC` (default off). - **API key option: disable non-published models** (#3017 — thanks @androw): a per-key flag restricting the key to discovered public models (combos / `auto/*` / `qtSd/*` routing still allowed). Hardening applied during integration: migration renumber (089/090/091), `/api/plugins` LOCAL_ONLY route-guard classification (closes the plugin-RCE vector), atomic install/upgrade with path containment, `O_EXCL` tmp-file creation (TOCTOU), rate-limit-map eviction, `validatePluginConfig` on configure, `buildErrorBody` on all plugin error paths. 246/246 tests; typecheck / cycles / docs-sync clean. Co-authored-by: oyi77 <14921983+oyi77@users.noreply.github.com> Co-authored-by: Nicolas Lorin <androw95220@gmail.com>
|
Hi @oyi77 — thank you for this substantial contribution! 🙏 Your plugins framework has been integrated into
Because the work shipped through the combined integration branch rather than a direct merge of this PR, I'm closing this one — but you're credited in the CHANGELOG ( |
- hooks.ts: fix emitHookBlocking hook chaining — pass accumulated body/metadata via currentPayload to each handler so downstream plugins observe upstream modifications - scanner.ts: add path traversal guard — reject entryPoints that resolve outside the plugin directory - chatCore.ts: convert dynamic plugin imports to static imports (no circular dependency risk; avoids per-request overhead) - dashboard page.tsx: add error notification when activate/deactivate returns non-OK response (was silently swallowed)
…els (diegosouzapw#3041) Integrates two community contributions into release/v3.8.8 with security hardening and conflict resolution. - **Plugins framework** (diegosouzapw#2913 — thanks @oyi77): hooks + registry unification, plugin SDK (`definePlugin`), worker-thread sandbox, per-plugin hook rate limiting, SHA-256 integrity verification, semver-gated upgrade, and execution analytics. Plugin routes are loopback-only (`isLocalOnlyPath`); `child_process` exec is opt-in via `OMNIROUTE_PLUGINS_ALLOW_EXEC` (default off). - **API key option: disable non-published models** (diegosouzapw#3017 — thanks @androw): a per-key flag restricting the key to discovered public models (combos / `auto/*` / `qtSd/*` routing still allowed). Hardening applied during integration: migration renumber (089/090/091), `/api/plugins` LOCAL_ONLY route-guard classification (closes the plugin-RCE vector), atomic install/upgrade with path containment, `O_EXCL` tmp-file creation (TOCTOU), rate-limit-map eviction, `validatePluginConfig` on configure, `buildErrorBody` on all plugin error paths. 246/246 tests; typecheck / cycles / docs-sync clean. Co-authored-by: oyi77 <14921983+oyi77@users.noreply.github.com> Co-authored-by: Nicolas Lorin <androw95220@gmail.com>
…els (diegosouzapw#3041) Integrates two community contributions into release/v3.8.8 with security hardening and conflict resolution. - **Plugins framework** (diegosouzapw#2913 — thanks @oyi77): hooks + registry unification, plugin SDK (`definePlugin`), worker-thread sandbox, per-plugin hook rate limiting, SHA-256 integrity verification, semver-gated upgrade, and execution analytics. Plugin routes are loopback-only (`isLocalOnlyPath`); `child_process` exec is opt-in via `OMNIROUTE_PLUGINS_ALLOW_EXEC` (default off). - **API key option: disable non-published models** (diegosouzapw#3017 — thanks @androw): a per-key flag restricting the key to discovered public models (combos / `auto/*` / `qtSd/*` routing still allowed). Hardening applied during integration: migration renumber (089/090/091), `/api/plugins` LOCAL_ONLY route-guard classification (closes the plugin-RCE vector), atomic install/upgrade with path containment, `O_EXCL` tmp-file creation (TOCTOU), rate-limit-map eviction, `validatePluginConfig` on configure, `buildErrorBody` on all plugin error paths. 246/246 tests; typecheck / cycles / docs-sync clean. Co-authored-by: oyi77 <14921983+oyi77@users.noreply.github.com> Co-authored-by: Nicolas Lorin <androw95220@gmail.com>
…els (diegosouzapw#3041) Integrates two community contributions into release/v3.8.8 with security hardening and conflict resolution. - **Plugins framework** (diegosouzapw#2913 — thanks @oyi77): hooks + registry unification, plugin SDK (`definePlugin`), worker-thread sandbox, per-plugin hook rate limiting, SHA-256 integrity verification, semver-gated upgrade, and execution analytics. Plugin routes are loopback-only (`isLocalOnlyPath`); `child_process` exec is opt-in via `OMNIROUTE_PLUGINS_ALLOW_EXEC` (default off). - **API key option: disable non-published models** (diegosouzapw#3017 — thanks @androw): a per-key flag restricting the key to discovered public models (combos / `auto/*` / `qtSd/*` routing still allowed). Hardening applied during integration: migration renumber (089/090/091), `/api/plugins` LOCAL_ONLY route-guard classification (closes the plugin-RCE vector), atomic install/upgrade with path containment, `O_EXCL` tmp-file creation (TOCTOU), rate-limit-map eviction, `validatePluginConfig` on configure, `buildErrorBody` on all plugin error paths. 246/246 tests; typecheck / cycles / docs-sync clean. Co-authored-by: oyi77 <14921983+oyi77@users.noreply.github.com> Co-authored-by: Nicolas Lorin <androw95220@gmail.com>
Summary
Plugin system improvements for v4.0.0-rc1. Extends PR #2385 with:
Custom Hooks (Priority 1+2)
emitHookBlocking()with body/metadata chaining for blocking eventsrunOnRequest,runOnResponse,runOnErrorlifecycle wrappershooks.tsis canonical,index.tsis re-export shimrunOnResponsewired inchatCore.ts(was missing)SDK
sdk.ts:definePlugin()factory +blockRequest,modifyBody,addMetadatahelpersconfigSchemaMarketplace
marketplace.ts: local plugin registry with 3 seed pluginsTests
Test plan
npm run typecheck:core— 0 errorsnode --import tsx/esm --test tests/unit/plugins-hooks.test.ts— 19/19 passnode --import tsx/esm --test tests/integration/plugins-lifecycle.test.ts— 17/17 pass