diff --git a/CHANGELOG.md b/CHANGELOG.md index 3798bc2f372..bee3256740a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai ### Changes +- Infra/path-guards: add a fast path for canonical absolute POSIX containment checks, avoiding repeated `path.resolve` and `path.relative` work in hot filesystem walkers. Refs #75895, #75575, and #68782. Thanks @Enderfga. - Tools: add a platform-level tool descriptor planner for descriptor-first visibility, generic availability checks, and executor references. Thanks @shakkernerd. - Docs/Codex: clarify that ChatGPT/Codex subscription setups should use `openai/gpt-*` with `agentRuntime.id: "codex"` for native Codex runtime, while `openai-codex/*` remains the PI OAuth route. Thanks @pashpashpash. - Plugins/source checkout: load bundled plugins from the `extensions/*` pnpm workspace tree in source checkouts, so plugin-local dependencies and edits are used directly while packaged installs keep using the built runtime tree. Thanks @vincentkoc. diff --git a/src/infra/path-guards.test.ts b/src/infra/path-guards.test.ts index a9090e56c3f..ce2941e62a4 100644 --- a/src/infra/path-guards.test.ts +++ b/src/infra/path-guards.test.ts @@ -74,6 +74,15 @@ describe("isPathInside", () => { ["/workspace/root", "/workspace/root/nested/file.txt", true], ["/workspace/root", "/workspace/root/..file.txt", true], ["/workspace/root", "/workspace/root/../escape.txt", false], + ["/workspace/root", "/workspace/rootless/file.txt", false], + ["/workspace/root", "/workspace/root/a/b/c/d/e/file.txt", true], + ["/workspace/root", "/workspace/root/a/..", true], + ["/workspace/root", "/workspace/root/a/../..", false], + ["/workspace/root", "/workspace/root/a/b/../../../escape", false], + ["/", "/anything/at/all", true], + ["/", "/", true], + ["foo", "foo/bar", true], + ["foo", "../escape", false], ])("checks posix containment %s -> %s", (basePath, targetPath, expected) => { expect(isPathInside(basePath, targetPath)).toBe(expected); }); diff --git a/src/infra/path-guards.ts b/src/infra/path-guards.ts index e40c4649e1a..52139375e4b 100644 --- a/src/infra/path-guards.ts +++ b/src/infra/path-guards.ts @@ -4,6 +4,7 @@ import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; const NOT_FOUND_CODES = new Set(["ENOENT", "ENOTDIR"]); const SYMLINK_OPEN_CODES = new Set(["ELOOP", "EINVAL", "ENOTSUP"]); const PARENT_SEGMENT_PREFIX = /^\.\.(?:[\\/]|$)/u; +const POSIX_SEPARATOR_CHAR_CODE = 0x2f; export function normalizeWindowsPathForComparison(input: string): string { let normalized = path.win32.normalize(input); @@ -44,6 +45,18 @@ export function isPathInside(root: string, target: string): boolean { ); } + if ( + root.length > 0 && + root.charCodeAt(0) === POSIX_SEPARATOR_CHAR_CODE && + target.length >= root.length && + target.charCodeAt(0) === POSIX_SEPARATOR_CHAR_CODE && + !target.includes("/..") && + (target === root || + (target.startsWith(root) && target.charCodeAt(root.length) === POSIX_SEPARATOR_CHAR_CODE)) + ) { + return true; + } + const resolvedRoot = path.resolve(root); const resolvedTarget = path.resolve(target); const relative = path.relative(resolvedRoot, resolvedTarget);