fix(plugins): skip node_modules and other non-plugin directories during discovery#43859
fix(plugins): skip node_modules and other non-plugin directories during discovery#43859Bartok9 wants to merge 3 commits intoopenclaw:mainfrom
Conversation
Greptile SummaryThis PR adds an However, the new test case in Confidence Score: 3/5
Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/plugins/discovery.test.ts
Line: 121-140
Comment:
**Test doesn't actually exercise the fix**
The `fake-plugin` directories are placed under `memory-lancedb-pro/<ignoredDir>/deeply/nested/fake-plugin`, but `discoverInDirectory` only scans one level deep — it does **not** recurse into subdirectories of a discovered skill directory. After finding `memory-lancedb-pro/index.ts`, the scanner moves on and never descends into `memory-lancedb-pro/node_modules/...`. This means the `expect(ids).not.toContain("fake-plugin")` assertions would pass trivially **even without the fix**.
The real bug scenario is when an ignored directory (e.g. `node_modules`) appears as a **direct child** of the scanned extensions folder. The test should be structured like:
```ts
// node_modules, .git, etc. as DIRECT siblings of real skills inside globalExt
const ignoredDirs = ["node_modules", ".git", "dist", ".venv", "browser_data", ".cache"];
for (const dirName of ignoredDirs) {
const nestedDir = path.join(globalExt, dirName, "fake-plugin");
fs.mkdirSync(nestedDir, { recursive: true });
fs.writeFileSync(path.join(nestedDir, "index.ts"), "export default function () {}", "utf-8");
}
```
With this structure, `discoverInDirectory(globalExt)` would encounter `node_modules` directly and — without the fix — attempt to process it. The fix causes `shouldIgnoreScannedDirectory("node_modules")` to return `true` and skip it, making the test meaningfully validate the change.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/plugins/discovery.test.ts
Line: 139-140
Comment:
**Redundant assertion**
`expect(ids.filter((id) => id === "fake-plugin")).toHaveLength(0)` is semantically identical to the `not.toContain` assertion on line 139 and can be removed.
```suggestion
expect(ids).not.toContain("fake-plugin");
```
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: 535052f |
src/plugins/discovery.test.ts
Outdated
| const skillDir = path.join(globalExt, "memory-lancedb-pro"); | ||
| fs.mkdirSync(skillDir, { recursive: true }); | ||
| fs.writeFileSync(path.join(skillDir, "index.ts"), "export default function () {}", "utf-8"); | ||
|
|
||
| // These directories should NOT be scanned (would cause FD exhaustion) | ||
| const ignoredDirs = ["node_modules", ".git", "dist", ".venv", "browser_data", ".cache"]; | ||
| for (const dirName of ignoredDirs) { | ||
| const nestedDir = path.join(skillDir, dirName, "deeply", "nested", "fake-plugin"); | ||
| fs.mkdirSync(nestedDir, { recursive: true }); | ||
| fs.writeFileSync(path.join(nestedDir, "index.ts"), "export default function () {}", "utf-8"); | ||
| } | ||
|
|
||
| const { candidates } = await discoverWithStateDir(stateDir, {}); | ||
|
|
||
| const ids = candidates.map((candidate) => candidate.idHint); | ||
| // Should find the real skill | ||
| expect(ids).toContain("memory-lancedb-pro"); | ||
| // Should NOT find any fake plugins inside ignored directories | ||
| expect(ids).not.toContain("fake-plugin"); | ||
| expect(ids.filter((id) => id === "fake-plugin")).toHaveLength(0); |
There was a problem hiding this comment.
Test doesn't actually exercise the fix
The fake-plugin directories are placed under memory-lancedb-pro/<ignoredDir>/deeply/nested/fake-plugin, but discoverInDirectory only scans one level deep — it does not recurse into subdirectories of a discovered skill directory. After finding memory-lancedb-pro/index.ts, the scanner moves on and never descends into memory-lancedb-pro/node_modules/.... This means the expect(ids).not.toContain("fake-plugin") assertions would pass trivially even without the fix.
The real bug scenario is when an ignored directory (e.g. node_modules) appears as a direct child of the scanned extensions folder. The test should be structured like:
// node_modules, .git, etc. as DIRECT siblings of real skills inside globalExt
const ignoredDirs = ["node_modules", ".git", "dist", ".venv", "browser_data", ".cache"];
for (const dirName of ignoredDirs) {
const nestedDir = path.join(globalExt, dirName, "fake-plugin");
fs.mkdirSync(nestedDir, { recursive: true });
fs.writeFileSync(path.join(nestedDir, "index.ts"), "export default function () {}", "utf-8");
}With this structure, discoverInDirectory(globalExt) would encounter node_modules directly and — without the fix — attempt to process it. The fix causes shouldIgnoreScannedDirectory("node_modules") to return true and skip it, making the test meaningfully validate the change.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugins/discovery.test.ts
Line: 121-140
Comment:
**Test doesn't actually exercise the fix**
The `fake-plugin` directories are placed under `memory-lancedb-pro/<ignoredDir>/deeply/nested/fake-plugin`, but `discoverInDirectory` only scans one level deep — it does **not** recurse into subdirectories of a discovered skill directory. After finding `memory-lancedb-pro/index.ts`, the scanner moves on and never descends into `memory-lancedb-pro/node_modules/...`. This means the `expect(ids).not.toContain("fake-plugin")` assertions would pass trivially **even without the fix**.
The real bug scenario is when an ignored directory (e.g. `node_modules`) appears as a **direct child** of the scanned extensions folder. The test should be structured like:
```ts
// node_modules, .git, etc. as DIRECT siblings of real skills inside globalExt
const ignoredDirs = ["node_modules", ".git", "dist", ".venv", "browser_data", ".cache"];
for (const dirName of ignoredDirs) {
const nestedDir = path.join(globalExt, dirName, "fake-plugin");
fs.mkdirSync(nestedDir, { recursive: true });
fs.writeFileSync(path.join(nestedDir, "index.ts"), "export default function () {}", "utf-8");
}
```
With this structure, `discoverInDirectory(globalExt)` would encounter `node_modules` directly and — without the fix — attempt to process it. The fix causes `shouldIgnoreScannedDirectory("node_modules")` to return `true` and skip it, making the test meaningfully validate the change.
How can I resolve this? If you propose a fix, please make it concise.
src/plugins/discovery.test.ts
Outdated
| expect(ids).not.toContain("fake-plugin"); | ||
| expect(ids.filter((id) => id === "fake-plugin")).toHaveLength(0); |
There was a problem hiding this comment.
Redundant assertion
expect(ids.filter((id) => id === "fake-plugin")).toHaveLength(0) is semantically identical to the not.toContain assertion on line 139 and can be removed.
| expect(ids).not.toContain("fake-plugin"); | |
| expect(ids.filter((id) => id === "fake-plugin")).toHaveLength(0); | |
| expect(ids).not.toContain("fake-plugin"); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugins/discovery.test.ts
Line: 139-140
Comment:
**Redundant assertion**
`expect(ids.filter((id) => id === "fake-plugin")).toHaveLength(0)` is semantically identical to the `not.toContain` assertion on line 139 and can be removed.
```suggestion
expect(ids).not.toContain("fake-plugin");
```
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 535052f2d0
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| "dist", | ||
| "build", |
There was a problem hiding this comment.
Do not skip valid plugin roots named dist/build
Adding "dist" and "build" to the hard ignore set causes discoverInDirectory to skip those directories before checking for package.json or index.*, so any plugin placed directly under extensions/dist or extensions/build is now silently undiscoverable. This is a behavior regression from previous discovery logic and can break users who load compiled local plugins from those folder names.
Useful? React with 👍 / 👎.
…ng discovery Fixes openclaw#43813 The plugin discovery scanner was recursively scanning into node_modules, .git, .venv, and browser_data directories when workspace skills contained them. This caused file descriptor exhaustion (12,000+ FDs) on macOS, leading to 'spawn EBADF' errors on all subsequent exec tool calls. This change adds a set of known non-plugin directories to skip during recursive scans, aligning with the ignore patterns already used by: - DEFAULT_SKILLS_WATCH_IGNORED in refresh.ts - IGNORED_MEMORY_WATCH_DIR_NAMES in manager-sync-ops.ts Directories now skipped: - node_modules, .pnpm-store (package managers) - .git (version control) - dist, build (build outputs) - .venv, venv, __pycache__, .mypy_cache, .pytest_cache (Python) - .cache (general caches) - browser_data, .browser_data (Playwright/Puppeteer) The legacy .bak/.backup-/.disabled patterns are preserved.
Per Greptile's review: the original test placed fake-plugin deeply nested inside a skill's subdirectory, but discoverInDirectory only scans one level deep. The test passed trivially even without the fix. Now places ignored directories (node_modules, .git, etc.) as direct children of the extensions folder, which is the actual bug scenario. This ensures the test actually validates shouldIgnoreScannedDirectory.
Address Greptile review feedback: the previous test nested fake-plugin too deeply (two levels inside ignored dirs) when discoverInDirectory only scans one level. The test now puts index.ts directly inside ignored directories (node_modules/, .git/, etc.) to properly validate that these directories are skipped when shouldIgnoreScannedDirectory returns true.
aa5af6c to
ce285c7
Compare
Summary
Fixes #43813
The plugin discovery scanner was recursively scanning into
node_modules,.git,.venv, andbrowser_datadirectories when workspace skills contained them. This caused file descriptor exhaustion (12,000+ FDs) on macOS, leading tospawn EBADFerrors on all subsequent exec tool calls.Changes
Added
IGNORED_SCAN_DIRECTORIESset toshouldIgnoreScannedDirectory()function, aligning with patterns already used by:DEFAULT_SKILLS_WATCH_IGNOREDinrefresh.tsIGNORED_MEMORY_WATCH_DIR_NAMESinmanager-sync-ops.tsDirectories now skipped:
node_modules,.pnpm-store(package managers).git(version control)dist,build(build outputs).venv,venv,__pycache__,.mypy_cache,.pytest_cache(Python).cache(general caches)browser_data,.browser_data(Playwright/Puppeteer)The legacy
.bak/.backup-/.disabledpatterns are preserved.Testing
Added test case
ignores node_modules and other non-plugin directories to prevent FD exhaustionthat verifies:node_modules,.git,dist,.venv,browser_data, and.cacheare NOT discoveredBefore/After
Before: ~12,232 open FDs when a workspace skill has
node_modulesAfter: ~1,231 open FDs (normal range)