Skip to content

Canonicalize PACKAGE_ROOT so it matches realpath'd allowedPath checks#1905

Open
kriszyp wants to merge 1 commit into
mainfrom
fix/canonicalize-package-root-symlinks
Open

Canonicalize PACKAGE_ROOT so it matches realpath'd allowedPath checks#1905
kriszyp wants to merge 1 commit into
mainfrom
fix/canonicalize-package-root-symlinks

Conversation

@kriszyp

@kriszyp kriszyp commented Jul 23, 2026

Copy link
Copy Markdown
Member

What / why

unitTests/components/globalIsolation.test.js was reported to intermittently reject node_modules/mqtt as "outside of allowed path" specifically when the checkout root is reached through a symlink (observed with dev-agent worktrees under .claude/worktrees/...).

Root cause: PACKAGE_ROOT (utility/packageUtils.js) is derived from a raw directory walk (__dirnamedirname() up to the nearest package.json) with no realpathSync. Meanwhile security/jsLoader.ts's checkAllowedModulePath always realpathSync()s the module path it's validating before doing path.startsWith(allowedPath). Whenever the process resolves symlinks inconsistently for these two paths (confirmed reproducible under --preserve-symlinks, which some environments set via NODE_OPTIONS), PACKAGE_ROOT keeps a symlinked prefix while the module path gets canonicalized — so startsWith never matches, even though the module is legitimately in scope.

components/componentLoader.ts already calls realpathSync(componentDirectory) for its allowedPath sources — this closes the same gap for PACKAGE_ROOT, which several test fixtures (and any other consumer treating it as an allow-list prefix) rely on being canonical.

Fix

utility/packageUtils.js: wrap PACKAGE_ROOT's computed value in realpathSync().

Test plan

  • Added unitTests/utility/packageUtils.test.js: spawns a child node --preserve-symlinks process that requires utility/packageUtils.js through a freshly created symlink to the repo root, and asserts PACKAGE_ROOT still equals the canonical (non-symlinked) path. Verified this test fails without the fix and passes with it.
  • Hardened the pre-existing PACKAGE_ROOT equality assertion to compare against realpathSync(...) rather than a raw join(...), so it stays correct if the suite itself is ever invoked with --preserve-symlinks (Gemini review leg caught this).
  • npm run build + npm run test:unit:main (3784 passing, 0 failing) and unitTests/components/globalIsolation.test.js + unitTests/security/** all green.
  • npm run format:write / oxlint clean on both changed files.

Review

Cross-model review (quick mode — diff <200 lines): the reviewer/codex-reviewer subagents aren't installed in this dev-agent profile, so I ran the Gemini leg (agy) directly on the diff. It flagged one real issue (the pre-existing equality assertion would break under --preserve-symlinks), which is fixed above; its other two suggestions (Windows symlink type, return vs this.skip()) match the established pattern already used in unitTests/agent/fsTools.test.js, so left as-is for consistency.

Refs finding from dispatch finding-fix-harper-i7a1 (queued from fix-harper-1880).

🤖 Generated with Claude Code

PACKAGE_ROOT was derived from a raw directory walk (no realpathSync),
while security/jsLoader.ts's checkAllowedModulePath always realpathSync()s
the module path it's validating before the startsWith(allowedPath)
comparison. When the checkout root is reached through a symlink (e.g.
--preserve-symlinks, a symlinked worktree root), that asymmetry makes a
legitimately in-scope module (like node_modules/mqtt) look like it's
outside the allowed path — this is what was tripping
unitTests/components/globalIsolation.test.js. componentLoader.ts already
realpathSync()s its allowedPath sources; PACKAGE_ROOT is now canonical
too, closing the gap for any consumer (tests included) that uses it as
an allowedPath prefix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@kriszyp
kriszyp requested a review from Ethan-Arrowood July 23, 2026 01:59

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

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.

Code Review

This pull request canonicalizes PACKAGE_ROOT in utility/packageUtils.js using realpathSync to resolve symlinks, ensuring correct path comparisons when the checkout is accessed via a symlink (e.g., under --preserve-symlinks). It also updates the unit tests to verify this behavior. The reviewer suggested using assert.strictEqual instead of assert.equal in the tests to comply with the repository's style guide.

// realpathSync'd: PACKAGE_ROOT is canonicalized (see the symlink test below), so compare
// against the canonical form rather than the raw join, which would only coincidentally
// match unless this suite itself is invoked with --preserve-symlinks.
assert.equal(packageUtils.PACKAGE_ROOT, realpathSync(join(__dirname, '../..')));

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

According to the repository style guide, strict assertion methods like assert.strictEqual should be used explicitly where strict semantics are needed (such as comparing string paths). Please use assert.strictEqual instead of assert.equal here and at line 58.

Suggested change
assert.equal(packageUtils.PACKAGE_ROOT, realpathSync(join(__dirname, '../..')));
assert.strictEqual(packageUtils.PACKAGE_ROOT, realpathSync(join(__dirname, '../..')));
References
  1. Use assert.strictEqual/assert.deepStrictEqual explicitly where strict semantics are needed. (link)
  2. Use the bare node:assert module instead of node:assert/strict for test assertions to comply with linting rules, while still utilizing strict assertion methods like assert.strictEqual from the bare module.

@claude

claude Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

@kriszyp
kriszyp marked this pull request as ready for review July 23, 2026 14:14

@Ethan-Arrowood Ethan-Arrowood left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Canonicalizing PACKAGE_ROOT so it matches the realpath'd allowedPath prefix checks is the correct fix — checking both sides in canonical form is the sound containment form and doesn't open the inverse (a symlink escaping the boundary still resolves outside it). It's a single realpathSync at module load (cold path, PACKAGE_ROOT is a const), and the child-process test reproducing the mismatch under --preserve-symlinks is a good regression guard.

sent with Claude Opus 4.8

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