Skip to content

feat(plugins): backend core + security/ESM fixes + tests + discovery stub [deferred to v4.0.0-rc1] - #2912

Merged
diegosouzapw merged 8 commits into
diegosouzapw:release/v3.8.7from
oyi77:feat/plugin-system
May 29, 2026
Merged

diegosouzapw merged 8 commits into
diegosouzapw:release/v3.8.7from
oyi77:feat/plugin-system

Conversation

@oyi77

@oyi77 oyi77 commented May 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Plugin system backend fixes and extensions for v4.0.0-rc1.

Security + ESM Fixes

  • IPC: process.send()/process.on("message") instead of worker_threads.parentPort
  • ESM: .mjs host script (forces ESM regardless of package.json)
  • Timeout: 10s default on callHook() with Promise.race
  • SIGKILL escalation: SIGTERM first, then SIGKILL after 3s grace
  • Env filtering: curated allowlist (safeKeys) instead of passing all env vars
  • Path traversal: fs.realpath() instead of startsWith()

Code Review Feedback

  • Replaced vm module with child_process.fork() for process-level isolation
  • Auth on all API routes
  • Config validation against configSchema

Discovery Tool

  • Stub service at src/lib/discovery/index.ts
  • DiscoveryConfig, DiscoveryResult types
  • probeEndpoint(), scanProvider(), getDiscoveryResults() stubs
  • Default disabled (opt-in)

Tests

  • 20 unit tests (scanner, loader, manager)
  • All passing

Migration

  • Renumbered from 059 to 076 (no collision with release branch)

Test plan

  • npm run typecheck:core — 0 errors
  • node --import tsx/esm --test tests/unit/plugins-scanner.test.ts — 9/9 pass
  • node --import tsx/esm --test tests/unit/plugins-loader.test.ts — 5/5 pass
  • node --import tsx/esm --test tests/unit/plugins-manager.test.ts — 6/6 pass

oyi77 and others added 7 commits May 29, 2026 23:48
- 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
@oyi77
oyi77 requested a review from diegosouzapw as a code owner May 29, 2026 17:59

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive plugin system to OmniRoute, adding database migrations, CRUD operations, lifecycle management (scanning, loading, activation, configuration), API endpoints, and integration into the chat core request pipeline and MCP server tools. The feedback highlights several critical issues: a lack of proper context chaining in blocking hooks, a non-cross-platform path traversal guard, potential crashes in the plugin host script on non-Error exceptions, and an ignored filter argument in the plugin executions tool. Additionally, there is a potential temporary file leak on synchronous fork failures, duplicate migration files, and violations of style guide Rule 2.8 due to locally defined Zod schemas in the API routes.

Comment thread src/lib/plugins/hooks.ts
Comment on lines +148 to +150
for (const reg of list) {
try {
const result = await reg.handler(payload);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In emitHookBlocking, the original payload is passed to each hook handler in the loop. This prevents proper chaining because subsequent plugins will not receive the modified body or metadata returned by previous plugins. To enable proper chaining, pass the updated context containing mergedBody and mergedMetadata to the handler.

Suggested change
for (const reg of list) {
try {
const result = await reg.handler(payload);
for (const reg of list) {
try {
const result = await reg.handler({ ...ctx, body: mergedBody, metadata: mergedMetadata });

Comment on lines +151 to +157
const resolvedEntry = await realpath(entryPoint).catch(() => null);
if (
!resolvedEntry ||
(!resolvedEntry.startsWith(resolvedPluginDir + "/") && resolvedEntry !== resolvedPluginDir)
) {
throw new Error(`Plugin '${name}' entry point escapes plugin directory`);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The path traversal guard uses string concatenation with a forward slash (resolvedPluginDir + "/") to verify if the entry point is within the plugin directory. This is not cross-platform and will fail on Windows where backslashes are used. Use relative and isAbsolute to securely and portably verify the directory containment.

Suggested change
const resolvedEntry = await realpath(entryPoint).catch(() => null);
if (
!resolvedEntry ||
(!resolvedEntry.startsWith(resolvedPluginDir + "/") && resolvedEntry !== resolvedPluginDir)
) {
throw new Error(`Plugin '${name}' entry point escapes plugin directory`);
}
const resolvedEntry = await realpath(entryPoint).catch(() => null);
if (!resolvedEntry) {
throw new Error(`Plugin '${name}' entry point escapes plugin directory`);
}
const rel = relative(resolvedPluginDir, resolvedEntry);
if (rel.startsWith("..") || isAbsolute(rel)) {
throw new Error(`Plugin '${name}' entry point escapes plugin directory`);
}

Comment thread src/lib/plugins/loader.ts
Comment on lines +58 to +60
} catch (err) {
process.send({ type: "result", id: msg.id, error: err.message });
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In the plugin host script, err.message is accessed directly in the catch block. If a plugin throws a non-Error value (such as a string or null), accessing err.message will throw a TypeError and crash the host process. Use a safe fallback like err instanceof Error ? err.message : String(err).

Suggested change
} catch (err) {
process.send({ type: "result", id: msg.id, error: err.message });
}
} catch (err) {
const errMsg = err instanceof Error ? err.message : String(err);
process.send({ type: "result", id: msg.id, error: errMsg });
}

handler: async (args: { name?: string; limit?: number }) => {
// Plugin executions are tracked via the skills system
const { skillExecutor } = await import("../../../src/lib/skills/executor");
const executions = skillExecutor.listExecutions(undefined, args.limit || 20);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The args.name filter is defined in the input schema but is completely ignored in the handler implementation. It should be passed to skillExecutor.listExecutions instead of undefined.

Suggested change
const executions = skillExecutor.listExecutions(undefined, args.limit || 20);
const executions = skillExecutor.listExecutions(args.name, args.limit || 20);

*/

import { mkdir, cp, rm, realpath, readFile } from "fs/promises";
import { join, dirname } from "path";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Import relative and isAbsolute from "path" to support a cross-platform path traversal check.

Suggested change
import { join, dirname } from "path";
import { join, dirname, relative, isAbsolute } from "path";

Comment thread src/lib/plugins/loader.ts
Comment on lines +86 to +90
const child = fork(hostScriptPath, [entryPoint], {
env,
stdio: ["pipe", "pipe", "pipe", "ipc"],
execArgv: ["--no-warnings"],
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If fork() throws an error synchronously (e.g., due to resource limits like EMFILE or invalid arguments), the temporary host script file hostScriptPath will leak in the temp directory because the cleanup handlers are registered on the child process which was never successfully created. Wrap the fork call in a try/catch block to clean up the temporary file on failure.

  let child;
  try {
    child = fork(hostScriptPath, [entryPoint], {
      env,
      stdio: ["pipe", "pipe", "pipe", "ipc"],
      execArgv: ["--no-warnings"],
    });
  } catch (err) {
    await rm(hostScriptPath, { force: true }).catch(() => {});
    throw err;
  }

Comment on lines +45 to +47
const schema = z.object({
config: z.record(z.string(), z.unknown()),
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

According to the repository style guide (Rule 2.8), all inputs must be validated using Zod schemas imported from src/shared/validation/schemas.ts. Defining the schema locally in the route violates this rule. Please move this schema to src/shared/validation/schemas.ts and import it here.

References
  1. Always validate inputs with Zod schemas from src/shared/validation/schemas.ts (Repository Style Guide, Rule 2.8) (link)

Comment on lines +36 to +38
const schema = z.object({
path: z.string().min(1),
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

According to the repository style guide (Rule 2.8), all inputs must be validated using Zod schemas imported from src/shared/validation/schemas.ts. Defining the schema locally in the route violates this rule. Please move this schema to src/shared/validation/schemas.ts and import it here.

References
  1. Always validate inputs with Zod schemas from src/shared/validation/schemas.ts (Repository Style Guide, Rule 2.8) (link)

Comment on lines +1 to +31
-- 059: Plugin system tables
-- WordPress-style plugin management with lifecycle tracking

CREATE TABLE IF NOT EXISTS plugins (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
version TEXT NOT NULL DEFAULT '1.0.0',
description TEXT,
author TEXT,
license TEXT DEFAULT 'MIT',
main TEXT NOT NULL DEFAULT 'index.js',
source TEXT NOT NULL DEFAULT 'local',
tags TEXT DEFAULT '[]',
status TEXT NOT NULL DEFAULT 'installed'
CHECK (status IN ('installed', 'active', 'inactive', 'error')),
enabled INTEGER NOT NULL DEFAULT 0,
manifest TEXT NOT NULL,
config TEXT DEFAULT '{}',
config_schema TEXT DEFAULT '{}',
hooks TEXT DEFAULT '[]',
permissions TEXT DEFAULT '[]',
plugin_dir TEXT NOT NULL,
error_message TEXT,
installed_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
activated_at TEXT
);

CREATE INDEX IF NOT EXISTS idx_plugins_status ON plugins(status);
CREATE INDEX IF NOT EXISTS idx_plugins_enabled ON plugins(enabled);
CREATE INDEX IF NOT EXISTS idx_plugins_name ON plugins(name);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This migration file is identical to 059_create_plugins.sql. Having duplicate migration files is redundant and can lead to confusion or maintenance issues. Please remove one of the duplicate migration files.

@kilo-code-bot

kilo-code-bot Bot commented May 29, 2026 •

Copy link
Copy Markdown

Code Review Summary

Status: 4 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 2
Issue Details (click to expand)

WARNING

File Line Issue
src/lib/plugins/manager.ts 75 Empty catch block silently swallows all errors when checking for direct plugin.json. Should log errors for debugging installation failures.
src/lib/plugins/loader.ts 131 code parameter can be null when child process exits due to uncaught exception or signal. Error message Plugin process exited with code null is confusing.

SUGGESTION

File Line Issue
src/lib/plugins/scanner.ts 77 Redundant type assertion - result is already correctly typed as { success: false; errors: string[] } from safeValidateManifest() return type.
src/lib/plugins/loader.ts 59 err.message in PLUGIN_HOST_SCRIPT template may throw if caught error lacks message property (non-Error throws). Use String(err) for safety.
Other Observations (not in diff)

Issues found in unchanged code or existing comments that cannot receive inline comments:

File Line Issue
src/lib/plugins/hooks.ts 69 Duplicate check uses both pluginName AND handler reference equality. If two plugins share a handler function (e.g., imported from shared code), both could register for the same event.
src/lib/plugins/loader.ts 230-241 getFilteredEnv() passes all PATH, HOME, etc. values without validation. If these contain secrets, plugins with env permission receive unfiltered values.
src/db/migrations/076_create_plugins.sql 1 Migration filename shows 076 but comment header still says -- 059:. Should update header to match filename.
Files Reviewed (12 files)
  • open-sse/handlers/chatCore.ts - Plugin hook integration in request pipeline
  • open-sse/mcp-server/tools/pluginTools.ts - MCP plugin management tools
  • src/app/api/plugins/route.ts - Plugin API routes
  • src/app/api/plugins/[name]/route.ts - Individual plugin operations
  • src/app/api/plugins/[name]/activate/route.ts - Plugin activation endpoint
  • src/app/api/plugins/[name]/deactivate/route.ts - Plugin deactivation endpoint
  • src/app/api/plugins/[name]/config/route.ts - Plugin config management
  • src/app/api/plugins/scan/route.ts - Plugin scan endpoint
  • src/lib/db/plugins.ts - Plugin DB CRUD operations
  • src/lib/plugins/hooks.ts - Hook registry system
  • src/lib/plugins/loader.ts - Child process plugin loader
  • src/lib/plugins/manager.ts - Plugin lifecycle manager
  • src/lib/plugins/manifest.ts - Zod manifest validation
  • src/lib/plugins/scanner.ts - Filesystem plugin discovery
  • src/lib/plugins/index.ts - Plugin registry
  • src/lib/discovery/index.ts - Discovery service stub
  • src/lib/localDb.ts - DB re-exports
  • tests/unit/plugins-*.test.ts - Test files

Positive notes:

  • Good security practices: path traversal protection via realpath(), environment variable allowlisting
  • ESM compatibility via .mjs host script
  • Timeout + SIGKILL escalation for plugin isolation
  • Management auth on all plugin API routes
  • Zod validation throughout
  • Clean separation of concerns across scanner/loader/manager

Reviewed by nemotron-3-super-120b-a12b-20230311:free · 855,599 tokens

@diegosouzapw diegosouzapw changed the title feat(plugins): backend core + security/ESM fixes + tests + discovery stub feat(plugins): backend core + security/ESM fixes + tests + discovery stub [deferred to v4.0.0-rc1] May 29, 2026
@diegosouzapw
diegosouzapw changed the base branch from release/v3.8.6 to release/v3.8.7 May 29, 2026 20:42
@diegosouzapw
diegosouzapw merged commit 2f975c0 into diegosouzapw:release/v3.8.7 May 29, 2026
3 checks passed
tkgo11 pushed a commit to tkgo11/OmniRoute that referenced this pull request Sep 23, 2026
feat(plugins): backend core + security/ESM fixes + tests + discovery stub [deferred to v4.0.0-rc1]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants