fix(plugins): distinguish missing entry file from path-escape security violation#52491
Conversation
resolvePackageEntrySource() treats all openBoundaryFileSync failures as path-escape security violations. When an extension entry file is simply missing (ENOENT, reason="path"), the gateway emits "extension entry escapes package directory" and aborts — crashing in a loop. Root cause: src/plugins/discovery.ts:478 checks !opened.ok but never inspects opened.reason. SafeOpenSyncResult already distinguishes "path" (ENOENT) from "validation" (actual path escape). Fix: only push the security diagnostic when opened.reason is "validation". For "path" or "io" failures, return null to skip the entry silently — a missing file is not a security violation. Closes openclaw#52445 Co-Authored-By: Claude Opus 4.6 <[email protected]> Signed-off-by: HCL <[email protected]>
|
@vincentkoc — |
Greptile SummaryThis PR fixes a crash-loop on gateway startup by correctly distinguishing a missing plugin entry file (ENOENT →
Confidence Score: 5/5
Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/plugins/discovery.ts
Line: 479-482
Comment:
**Consider warning on unexpected I/O errors**
`"io"` covers unexpected errors (e.g. permission denied, disk errors) that are distinct from a simple missing file (`"path"`). Silently skipping them makes disk-level problems invisible to the operator. A `"warn"` diagnostic here would preserve the non-fatal startup behaviour while still surfacing the anomaly in logs:
```suggestion
if (opened.reason === "path") {
// File missing (ENOENT/ENOTDIR/ELOOP) — skip silently, not a security violation.
return null;
}
if (opened.reason === "io") {
params.diagnostics.push({
level: "warn",
message: `extension entry could not be read (I/O error): ${params.entryPath}`,
source: params.sourceLabel,
});
return null;
}
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "fix(plugins): distinguish missing entry ..." | Re-trigger Greptile |
src/plugins/discovery.ts
Outdated
| if (opened.reason !== "validation") { | ||
| // File missing (ENOENT) or I/O error — skip silently, not a security violation. | ||
| return null; | ||
| } |
There was a problem hiding this comment.
Consider warning on unexpected I/O errors
"io" covers unexpected errors (e.g. permission denied, disk errors) that are distinct from a simple missing file ("path"). Silently skipping them makes disk-level problems invisible to the operator. A "warn" diagnostic here would preserve the non-fatal startup behaviour while still surfacing the anomaly in logs:
| if (opened.reason !== "validation") { | |
| // File missing (ENOENT) or I/O error — skip silently, not a security violation. | |
| return null; | |
| } | |
| if (opened.reason === "path") { | |
| // File missing (ENOENT/ENOTDIR/ELOOP) — skip silently, not a security violation. | |
| return null; | |
| } | |
| if (opened.reason === "io") { | |
| params.diagnostics.push({ | |
| level: "warn", | |
| message: `extension entry could not be read (I/O error): ${params.entryPath}`, | |
| source: params.sourceLabel, | |
| }); | |
| return null; | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugins/discovery.ts
Line: 479-482
Comment:
**Consider warning on unexpected I/O errors**
`"io"` covers unexpected errors (e.g. permission denied, disk errors) that are distinct from a simple missing file (`"path"`). Silently skipping them makes disk-level problems invisible to the operator. A `"warn"` diagnostic here would preserve the non-fatal startup behaviour while still surfacing the anomaly in logs:
```suggestion
if (opened.reason === "path") {
// File missing (ENOENT/ENOTDIR/ELOOP) — skip silently, not a security violation.
return null;
}
if (opened.reason === "io") {
params.diagnostics.push({
level: "warn",
message: `extension entry could not be read (I/O error): ${params.entryPath}`,
source: params.sourceLabel,
});
return null;
}
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4535ba1352
ℹ️ 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".
src/plugins/discovery.ts
Outdated
| if (opened.reason !== "validation") { | ||
| // File missing (ENOENT) or I/O error — skip silently, not a security violation. | ||
| return null; |
There was a problem hiding this comment.
Preserve diagnostics for I/O failures opening plugin entries
openVerifiedFileSync() intentionally distinguishes missing paths from real filesystem failures by returning reason: "io" for cases like EACCES/EMFILE (src/infra/safe-open-sync.ts:84-88), but this branch now collapses those into a silent null. Both discovery callers treat null as “skip this plugin” (src/plugins/discovery.ts:564-595 and 694-725), so a configured plugin package with an unreadable entry file now just disappears with no diagnostic at all. The ENOENT fix makes sense, but swallowing non-path errors regresses debuggability for genuine startup failures.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in a87ccfa. Now handles all 3 reasons explicitly: path → silent null (ENOENT), io → warn diagnostic (EACCES/EMFILE surfaced without abort), validation → error diagnostic (security). I/O failures are no longer invisible.
src/plugins/discovery.ts
Outdated
| if (opened.reason !== "validation") { | ||
| // File missing (ENOENT) or I/O error — skip silently, not a security violation. | ||
| return null; |
There was a problem hiding this comment.
Restrict missing-entry suppression to partially installed bundles
This return null also applies when the user explicitly points OpenClaw at a package directory via extraPaths/config. In that path, if package.json declares openclaw.extensions but the built entry file is missing, discoverFromPath() now returns with no candidates and no error (src/plugins/discovery.ts:694-725), which turns a broken third-party package or typo into a silent no-op. The original Arch/AUR case is about partially installed bundled plugins, but applying the same ENOENT suppression to all package origins hides actionable misconfiguration elsewhere.
Useful? React with 👍 / 👎.
Address Codex P1 + Greptile P2: the previous commit collapsed both "path" (ENOENT) and "io" (EACCES/EMFILE) into silent null returns. Now: - reason="path" (missing file): return null silently — not a security issue - reason="io" (permission/disk): push warn diagnostic — surface anomaly without aborting gateway - reason="validation" (path escape): push error diagnostic — security violation Co-Authored-By: Claude Opus 4.6 <[email protected]> Signed-off-by: HCL <[email protected]>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a87ccfaa3f
ℹ️ 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".
| if (opened.reason === "path") { | ||
| // File missing (ENOENT) — skip, not a security violation. | ||
| return null; |
There was a problem hiding this comment.
Restrict missing-entry suppression to ENOENT
opened.reason === "path" is broader than the missing-file case this fix is targeting: openVerifiedFileSync() also maps ENOTDIR and ELOOP into "path" (src/infra/safe-open-sync.ts:17-20,84-88). With this branch, a package entry like ./dist/index.js now disappears silently if dist is accidentally a file or a symlink loop, instead of producing any diagnostic. That regresses debuggability for malformed packages; only plain ENOENT should be skipped here.
Useful? React with 👍 / 👎.
|
Landed via temp rebase onto main.
Thanks @hclsys! |
lobster-biscuit
Closes #52445
Problem
Gateway enters a fatal crash loop during startup when any extension entry point file is missing. The error message says "extension entry escapes package directory" but the file is simply not found (ENOENT) — not a security violation.
This affects AUR/Arch Linux builds and any environment where built-in extensions may be partially installed.
Root cause
src/plugins/discovery.ts:478—resolvePackageEntrySource()checks!opened.okbut never inspectsopened.reason.openBoundaryFileSyncreturnsSafeOpenSyncResultwithreason: "path"for ENOENT andreason: "validation"for actual path-escape violations (safe-open-sync.ts:4).All failures were treated as security violations, aborting the gateway.
User impact
Gateway cannot start at all. Crash loop on every startup attempt. No workaround except manually creating missing entry files.
Fix
Only emit the security diagnostic when
opened.reason === "validation". For"path"(ENOENT) or"io"errors, returnnullto skip the entry silently.1 file, +4 lines.
How to verify
extensions/acpx/index.js)Tests
The
SafeOpenSyncResulttype andopenBoundaryFileSyncare covered by existing tests insafe-open-sync.test.ts. This change is a call-site guard — the function behavior is unchanged.🤖 Generated with Claude Code