openclaw - ✅(Solved) Fix [Bug]: boundary-file-read conflates ENOENT with path-escape violation — misleading error message [2 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#61708Fetched 2026-04-08 02:55:35
View on GitHub
Comments
0
Participants
1
Timeline
3
Reactions
0
Timeline (top)
cross-referenced ×3

openBoundaryFileSync() returns { ok: false } for both "file not found" (ENOENT) and "path escapes boundary" cases. The caller in channel-entry-contract.ts reports all failures with the message "plugin entry path escapes plugin root: <path>", even when the actual cause is that the file simply doesn't exist in the build output.

Error Message

This significantly increases debugging time. When hitting the crash introduced in #61681/#61682 (Mattermost ./src/channel.js specifier missing from dist), the error message strongly implies a security boundary violation or a traversal attack — not a missing build artifact. Users (and AI agents helping debug) spend time investigating path security rather than checking whether the file exists in dist/extensions/<name>/. dist/extensions/mattermost/src/channel.js did not exist in the build output. The actual error cause: ENOENT. The reported error: plugin entry path escapes plugin root: ./src/channel.js. These suggest very different fixes.

Root Cause

openBoundaryFileSync() returns { ok: false } for both "file not found" (ENOENT) and "path escapes boundary" cases. The caller in channel-entry-contract.ts reports all failures with the message "plugin entry path escapes plugin root: <path>", even when the actual cause is that the file simply doesn't exist in the build output.

Fix Action

Fixed

PR fix notes

PR #61715: fix(infra): distinguish ENOENT from path-escape in boundary-file-read

Description (problem / solution / changelog)

Summary

  • Problem: channel-entry-contract.ts throws "plugin entry path escapes plugin root: <specifier>" for all openBoundaryFileSync failures, including plain ENOENT (file not found). This is deeply misleading — when the Mattermost build regression caused ./src/channel.js to be missing from the build output, the error suggested a security/boundary violation rather than a missing artifact.
  • Why it matters: Debugging time increases significantly when the error message points at the wrong root cause. A "file not found" and a "path escape" are fundamentally different problems requiring different fixes.
  • What changed: resolveBundledEntryModulePath() in channel-entry-contract.ts now uses matchBoundaryFileOpenFailure() to emit reason-specific error messages: reason: "path" → "plugin entry file not found in build output" ; all other reasons → "plugin entry path escapes plugin root" (unchanged).
  • What did NOT change (scope boundary): No changes to boundary-file-read.ts itself, no changes to BoundaryFileOpenResult types, no changes to safe-open-sync.ts. The existing reason discriminant ("path" | "validation" | "io") already distinguishes ENOENT from boundary escapes — the caller just was not using it.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #61708
  • This PR fixes a bug or regression

Root Cause (if applicable)

  • Root cause: resolveBundledEntryModulePath() treated all openBoundaryFileSync failures uniformly, throwing the same "path escapes plugin root" error regardless of the actual failure reason.
  • Missing detection / guardrail: The existing matchBoundaryFileOpenFailure() helper and reason discriminant were available but unused in this caller.
  • Contributing context (if known): Other callers (e.g., manifest.ts, bundle-manifest.ts, discovery.ts) already use matchBoundaryFileOpenFailure to produce reason-specific messages. This caller was an outlier.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: src/plugin-sdk/channel-entry-contract.ts (or a dedicated test for resolveBundledEntryModulePath)
  • Scenario the test should lock in: When openBoundaryFileSync returns { ok: false, reason: "path" }, the thrown error message should contain "not found in build output" rather than "escapes plugin root".
  • Why this is the smallest reliable guardrail: A unit test mocking openBoundaryFileSync to return different failure reasons and asserting on the error message text.
  • If no new test is added, why not: The function is an internal module-private helper. The existing boundary-file-read.test.ts suite (7 tests) already validates the reason discriminant surface. Adding a test here would require mocking several layers deep. The fix is a 5-line change using an already-tested helper.

User-visible / Behavior Changes

Error message when a bundled channel entry file is missing changes from:

plugin entry path escapes plugin root: ./src/channel.js

to:

plugin entry file not found in build output: ./src/channel.js

All other failure modes (actual boundary escapes, IO errors, validation failures) retain the existing error message.

Diagram (if applicable)

Before:
[openBoundaryFileSync fails] -> "plugin entry path escapes plugin root: <path>" (always)

After:
[openBoundaryFileSync fails, reason="path"] -> "plugin entry file not found in build output: <path>"
[openBoundaryFileSync fails, reason=other]  -> "plugin entry path escapes plugin root: <path>"

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: Linux Mint 21.3 (x64)
  • Runtime/container: Node.js v22.17.1
  • Model/provider: N/A
  • Integration/channel (if any): Mattermost (where the original incident occurred)
  • Relevant config (redacted): N/A

Steps

  1. Have a bundled channel extension where the entry file is missing from build output (e.g., ./src/channel.js not in dist/)
  2. Start OpenClaw — extension loading triggers resolveBundledEntryModulePath()
  3. Observe the error message

Expected

Error: plugin entry file not found in build output: ./src/channel.js

Actual (before fix)

Error: plugin entry path escapes plugin root: ./src/channel.js

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets

Before (real incident log):

Error: plugin entry path escapes plugin root: ./src/channel.js
    at resolveBundledEntryModulePath (channel-entry-contract.ts:98)

The actual cause was ENOENT — the Mattermost extension build output was missing src/channel.js.

After (with fix applied): The same scenario now produces:

Error: plugin entry file not found in build output: ./src/channel.js
  • pnpm exec vitest run src/infra/boundary-file-read.test.ts — 7/7 tests pass
  • npx tsc --noEmit — no new type errors (pre-existing errors on main in unrelated files)

Human Verification (required)

  • Verified scenarios: Confirmed matchBoundaryFileOpenFailure correctly routes reason: "path" to the new message and all other reasons to the existing message by code inspection and type checking.
  • Edge cases checked: reason: "io" and reason: "validation" both fall through to the fallback handler, preserving the original "escapes plugin root" message.
  • What you did not verify: End-to-end runtime verification with a real missing extension entry file (would require intentionally breaking a build).

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No

Risks and Mitigations

  • Risk: None significant. The change is a 5-line error message improvement using an existing, well-tested helper function.

Note: This PR was AI-assisted (authored by an AI agent under human direction). Pre-existing main CI failures (listControlledSubagentRuns, ChatMessage type errors) are unrelated to this change.

Changed files

  • src/plugin-sdk/channel-entry-contract.ts (modified, +7/-2)

PR #61752: fix: distinguish ENOENT from boundary-escape in plugin entry errors

Description (problem / solution / changelog)

When a plugin entry file is missing from the build output, openBoundaryFileSync() returns { ok: false, reason: "path" } — but every caller threw "plugin entry path escapes plugin root," strongly implying a security boundary violation. Users investigating #61682 (Mattermost ./src/channel.js missing from dist) spent time auditing path traversal instead of checking whether the file existed.

What changed

Added describeBoundaryFileOpenFailure() in src/infra/boundary-file-read.ts that inspects both the failure reason and the underlying error code:

FailureError codeMessage
pathENOENTplugin entry file not found: <path>
pathENOTDIR / ELOOPplugin entry path error (<code>): <path>
validationplugin entry path escapes plugin root or fails alias checks: <path>
ioplugin entry I/O error (<error>): <path>

Updated all 4 call sites that previously used a hardcoded escape message:

  • src/plugin-sdk/channel-entry-contract.ts
  • src/plugins/bundled-capability-runtime.ts
  • src/plugins/loader.ts (2 sites)

Before

plugin entry path escapes plugin root or fails alias checks

(for every failure, including a simple missing file)

After

plugin entry file not found: ./src/channel.js

(when the file doesn't exist — the common case from #61682)

Tests

4 new test cases in src/infra/boundary-file-read.test.ts covering ENOENT, ENOTDIR, validation, and I/O branches.

Known limitation

resolveBoundaryFilePathGeneric() collapses resolver-side exceptions (e.g. ELOOP during symlink canonicalization) into reason: "validation" before the formatter runs. A deeper fix for that would need changes to the boundary resolution pipeline itself — left for a follow-up.

Closes #61708

Changed files

  • .agents/maintainers.md (removed, +0/-1)
  • .agents/skills/openclaw-ghsa-maintainer/SKILL.md (removed, +0/-87)
  • .agents/skills/openclaw-parallels-smoke/SKILL.md (removed, +0/-116)
  • .agents/skills/openclaw-pr-maintainer/SKILL.md (removed, +0/-75)
  • .agents/skills/openclaw-qa-testing/SKILL.md (removed, +0/-86)
  • .agents/skills/openclaw-qa-testing/agents/openai.yaml (removed, +0/-4)
  • .agents/skills/openclaw-release-maintainer/SKILL.md (removed, +0/-267)
  • .agents/skills/openclaw-test-heap-leaks/SKILL.md (removed, +0/-75)
  • .agents/skills/openclaw-test-heap-leaks/agents/openai.yaml (removed, +0/-4)
  • .agents/skills/openclaw-test-heap-leaks/scripts/heapsnapshot-delta.mjs (removed, +0/-553)
  • .agents/skills/parallels-discord-roundtrip/SKILL.md (removed, +0/-62)
  • .agents/skills/security-triage/SKILL.md (removed, +0/-111)
  • .codex (removed, +0/-0)
  • .detect-secrets.cfg (removed, +0/-45)
  • .dockerignore (removed, +0/-72)
  • .env.example (removed, +0/-80)
  • .gitattributes (removed, +0/-3)
  • .github/CODEOWNERS (removed, +0/-54)
  • .github/ISSUE_TEMPLATE/bug_report.yml (removed, +0/-148)
  • .github/ISSUE_TEMPLATE/config.yml (removed, +0/-8)
  • .github/ISSUE_TEMPLATE/feature_request.yml (removed, +0/-70)
  • .github/actionlint.yaml (removed, +0/-23)
  • .github/actions/detect-docs-changes/action.yml (removed, +0/-53)
  • .github/actions/ensure-base-commit/action.yml (removed, +0/-61)
  • .github/actions/setup-node-env/action.yml (removed, +0/-99)
  • .github/actions/setup-pnpm-store-cache/action.yml (removed, +0/-76)
  • .github/codeql/codeql-javascript-typescript.yml (removed, +0/-18)
  • .github/dependabot.yml (removed, +0/-127)
  • .github/instructions/copilot.instructions.md (removed, +0/-64)
  • .github/labeler.yml (removed, +0/-355)
  • .github/pull_request_template.md (removed, +0/-147)
  • .github/workflows/auto-response.yml (removed, +0/-534)
  • .github/workflows/ci.yml (removed, +0/-1220)
  • .github/workflows/codeql.yml (removed, +0/-137)
  • .github/workflows/control-ui-locale-refresh.yml (removed, +0/-172)
  • .github/workflows/docker-release.yml (removed, +0/-389)
  • .github/workflows/docs-sync-publish.yml (removed, +0/-70)
  • .github/workflows/docs-translate-trigger-release.yml (removed, +0/-42)
  • .github/workflows/install-smoke.yml (removed, +0/-207)
  • .github/workflows/labeler.yml (removed, +0/-877)
  • .github/workflows/macos-release.yml (removed, +0/-93)
  • .github/workflows/openclaw-npm-release.yml (removed, +0/-449)
  • .github/workflows/plugin-clawhub-release.yml (removed, +0/-276)
  • .github/workflows/plugin-npm-release.yml (removed, +0/-217)
  • .github/workflows/sandbox-common-smoke.yml (removed, +0/-64)
  • .github/workflows/stale.yml (removed, +0/-217)
  • .github/workflows/workflow-sanity.yml (removed, +0/-98)
  • .gitignore (removed, +0/-152)
  • .jscpd.json (removed, +0/-16)
  • .mailmap (removed, +0/-13)
  • .markdownlint-cli2.jsonc (removed, +0/-55)
  • .npmignore (removed, +0/-3)
  • .npmrc (removed, +0/-4)
  • .oxfmtrc.jsonc (removed, +0/-26)
  • .oxlintrc.json (removed, +0/-39)
  • .pi/extensions/diff.ts (removed, +0/-117)
  • .pi/extensions/files.ts (removed, +0/-134)
  • .pi/extensions/prompt-url-widget.ts (removed, +0/-190)
  • .pi/extensions/redraws.ts (removed, +0/-26)
  • .pi/extensions/ui/paged-select.ts (removed, +0/-82)
  • .pi/git/.gitignore (removed, +0/-2)
  • .pi/prompts/cl.md (removed, +0/-58)
  • .pi/prompts/is.md (removed, +0/-22)
  • .pi/prompts/landpr.md (removed, +0/-73)
  • .pi/prompts/reviewpr.md (removed, +0/-134)
  • .pre-commit-config.yaml (removed, +0/-157)
  • .prettierignore (removed, +0/-1)
  • .secrets.baseline (removed, +0/-13017)
  • .shellcheckrc (removed, +0/-25)
  • .swiftformat (removed, +0/-51)
  • .swiftlint.yml (removed, +0/-150)
  • .vscode/extensions.json (removed, +0/-3)
  • .vscode/settings.json (removed, +0/-22)
  • AGENTS.md (removed, +0/-321)
  • CHANGELOG.md (removed, +0/-5560)
  • CLAUDE.md (removed, +0/-1)
  • CONTRIBUTING.md (removed, +0/-217)
  • Dockerfile (removed, +0/-266)
  • Dockerfile.sandbox (removed, +0/-24)
  • Dockerfile.sandbox-browser (removed, +0/-36)
  • Dockerfile.sandbox-common (removed, +0/-48)
  • LICENSE (removed, +0/-21)
  • Makefile (removed, +0/-4)
  • README.md (removed, +0/-614)
  • SECURITY.md (removed, +0/-323)
  • Swabble/.github/workflows/ci.yml (removed, +0/-54)
  • Swabble/.gitignore (removed, +0/-33)
  • Swabble/.swiftformat (removed, +0/-8)
  • Swabble/.swiftlint.yml (removed, +0/-43)
  • Swabble/CHANGELOG.md (removed, +0/-11)
  • Swabble/LICENSE (removed, +0/-21)
  • Swabble/Package.resolved (removed, +0/-69)
  • Swabble/Package.swift (removed, +0/-55)
  • Swabble/README.md (removed, +0/-111)
  • Swabble/Sources/SwabbleCore/Config/Config.swift (removed, +0/-77)
  • Swabble/Sources/SwabbleCore/Hooks/HookExecutor.swift (removed, +0/-75)
  • Swabble/Sources/SwabbleCore/Speech/BufferConverter.swift (removed, +0/-50)
  • Swabble/Sources/SwabbleCore/Speech/SpeechPipeline.swift (removed, +0/-114)
  • Swabble/Sources/SwabbleCore/Support/AttributedString+Sentences.swift (removed, +0/-62)
  • Swabble/Sources/SwabbleCore/Support/Logging.swift (removed, +0/-41)

Code Example

type BoundaryFileResult =
  | { ok: true; content: string }
  | { ok: false; reason: 'not-found' | 'escape' }
RAW_BUFFERClick to expand / collapse

Summary

openBoundaryFileSync() returns { ok: false } for both "file not found" (ENOENT) and "path escapes boundary" cases. The caller in channel-entry-contract.ts reports all failures with the message "plugin entry path escapes plugin root: <path>", even when the actual cause is that the file simply doesn't exist in the build output.

Impact

This significantly increases debugging time. When hitting the crash introduced in #61681/#61682 (Mattermost ./src/channel.js specifier missing from dist), the error message strongly implies a security boundary violation or a traversal attack — not a missing build artifact. Users (and AI agents helping debug) spend time investigating path security rather than checking whether the file exists in dist/extensions/<name>/.

Concrete example

dist/extensions/mattermost/src/channel.js did not exist in the build output. The actual error cause: ENOENT. The reported error: plugin entry path escapes plugin root: ./src/channel.js. These suggest very different fixes.

Proposed fix

Distinguish the two failure modes in openBoundaryFileSync() and propagate the distinction to callers:

type BoundaryFileResult =
  | { ok: true; content: string }
  | { ok: false; reason: 'not-found' | 'escape' }

Callers can then emit appropriate messages:

  • 'escape'plugin entry path escapes plugin root: <path> (current message, appropriate here)
  • 'not-found'plugin entry file not found in build output: <path> (new, more actionable)

Environment

  • OpenClaw v2026.4.5/2026.4.6 (source install, Linux)
  • Related: #61681, #61682

extent analysis

TL;DR

Update the openBoundaryFileSync() function to distinguish between "file not found" and "path escapes boundary" cases and propagate this distinction to callers.

Guidance

  • Modify the openBoundaryFileSync() function to return a BoundaryFileResult object with a reason property that indicates whether the failure was due to the file not being found or the path escaping the boundary.
  • Update callers of openBoundaryFileSync() to handle the new reason property and emit appropriate error messages.
  • Test the updated function with both "file not found" and "path escapes boundary" scenarios to ensure correct error messages are reported.
  • Consider adding additional logging or debugging information to help with troubleshooting in the future.

Example

type BoundaryFileResult =
  | { ok: true; content: string }
  | { ok: false; reason: 'not-found' | 'escape' }

function openBoundaryFileSync(path: string): BoundaryFileResult {
  // implementation details omitted
  if (/* file not found */) {
    return { ok: false, reason: 'not-found' };
  } else if (/* path escapes boundary */) {
    return { ok: false, reason: 'escape' };
  } else {
    // file found and within boundary
    return { ok: true, content: /* file content */ };
  }
}

Notes

This solution assumes that the openBoundaryFileSync() function is the correct place to make this change, and that the BoundaryFileResult type is a suitable way to propagate the distinction between the two failure modes.

Recommendation

Apply the proposed fix to update the openBoundaryFileSync() function and its callers to distinguish between the two failure modes and report more accurate error messages. This will help reduce debugging time and improve the overall user experience.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix [Bug]: boundary-file-read conflates ENOENT with path-escape violation — misleading error message [2 pull requests, 1 participants]