Skip to content

test(plugins): unit tests for manifest, hooks, and DB - [deferred to v4.0.0-rc1] #2388

Open
oyi77 wants to merge 2 commits into
diegosouzapw:release/v3.8.0from
oyi77:feat/plugin-tests
Open

test(plugins): unit tests for manifest, hooks, and DB - [deferred to v4.0.0-rc1] #2388
oyi77 wants to merge 2 commits into
diegosouzapw:release/v3.8.0from
oyi77:feat/plugin-tests

Conversation

@oyi77
Copy link
Copy Markdown
Contributor

@oyi77 oyi77 commented May 18, 2026

Summary

Unit tests for the plugin system (Wave 6 of #2060).

Depends on: #2385 (plugin backend core)

What's included:

  • tests/unit/plugins/manifest.test.ts — 12 tests: valid/invalid manifests, defaults, rejection cases
  • tests/unit/plugins/hooks.test.ts — 15 tests: register, unregister, emit, priority, error handling, async
  • tests/unit/plugins/db.test.ts — CRUD tests: insert, get, list, update, delete, exists

Test results: 27/27 pass

Test plan

  • node --import tsx/esm --test tests/unit/plugins/manifest.test.ts — all pass
  • node --import tsx/esm --test tests/unit/plugins/hooks.test.ts — all pass
  • node --import tsx/esm --test tests/unit/plugins/db.test.ts — all pass

@oyi77 oyi77 requested a review from diegosouzapw as a code owner May 18, 2026 22:57
Copy link
Copy Markdown
Contributor

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

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 implements a comprehensive plugin system featuring lifecycle management, sandboxed execution via the Node.js vm module, and an event-driven hook registry integrated into the request pipeline. It adds new database migrations, management API routes, and MCP tools for plugin administration. Feedback identifies critical security vulnerabilities, specifically potential path traversal during plugin installation and a lack of authentication on management endpoints. Additionally, the review suggests technical improvements for handling asynchronous initialization in the loader, implementing defensive type checks for plugin results, and enhancing filesystem error handling in the scanner.

* Install a plugin from a source directory.
* Copies to plugin dir, validates manifest, registers in DB.
*/
async install(sourceDir: string): Promise<PluginRow> {
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.

security-critical critical

The install method accepts an arbitrary filesystem path from the user (via the API or MCP tool) and copies its contents into the plugin directory. This presents a significant security risk, including path traversal and arbitrary file read, as an attacker could point this to sensitive system directories if they can place a plugin.json there. Access to this functionality should be strictly restricted or the input path should be validated against a safe allowlist.

/**
* POST /api/plugins — Install a plugin from a local path
*/
export async function POST(request: NextRequest) {
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.

security-critical critical

This API route (and other plugin management routes added in this PR) lacks authentication and authorization checks. Since plugins can execute arbitrary code on the host via the vm module, exposing these endpoints without protection allows for Remote Code Execution (RCE) by any user with network access to the gateway.

Comment thread src/lib/plugins/loader.ts
Comment on lines +115 to +118
filename: entryPoint,
timeout: 10000, // 10s init timeout
});
} catch (err: any) {
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 vm.runInContext call executes an async function wrapper, but the resulting promise is not awaited. This creates a race condition where moduleObj.exports might be accessed before the plugin has finished its synchronous initialization or before any top-level code has run if it contains asynchronous operations. Additionally, the timeout option in vm only applies to synchronous execution; it will not protect against infinite loops or long-running tasks that occur after the first await in the plugin code.

    await vm.runInContext(wrapped, context, {
      filename: entryPoint,
      timeout: 10000, // 10s init timeout
    });

),
};
}
if (pluginResult?.ctx && "body" in pluginResult.ctx) {
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 in operator will throw a TypeError if pluginResult.ctx is not an object (e.g., if a plugin incorrectly returns a string or number as the context). A more defensive check should be used to ensure ctx is an object before using the in operator.

Suggested change
if (pluginResult?.ctx && "body" in pluginResult.ctx) {
if (pluginResult?.ctx && typeof pluginResult.ctx === "object" && "body" in pluginResult.ctx) {

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 plugin_executions tool accepts a name argument for filtering, but it is currently ignored in the implementation. The name should be passed to the skillExecutor.listExecutions method.

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

entries = dirEntries
.filter((e) => e.isDirectory() && !e.name.startsWith("."))
.map((e) => e.name);
} catch (err: any) {
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 readdir call will throw an ENOTDIR error if the provided dir path points to a file instead of a directory. The current catch block only handles ENOENT. It should be updated to handle ENOTDIR gracefully to avoid crashing when an invalid path is provided.

Suggested change
} catch (err: any) {
} catch (err: any) {
if (err.code === "ENOENT" || err.code === "ENOTDIR") {
log.info("scanner.dir_not_found", { dir });
return { plugins: [], errors: [] };
}

@oyi77
Copy link
Copy Markdown
Contributor Author

oyi77 commented May 18, 2026

Review feedback addressed in #2385 commit 9fb6d52. This PR inherits those fixes.

@diegosouzapw diegosouzapw changed the title test(plugins): unit tests for manifest, hooks, and DB test(plugins): unit tests for manifest, hooks, and DB - [deferred to v4.0.0-rc1] May 19, 2026
@diegosouzapw
Copy link
Copy Markdown
Owner

Tests for the plugin system — keeping open for v4.0.0-rc1 with #2385#2387. Thanks @oyi77!

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