Skip to content

fix(coding-agent): keep vendored rules discovery inside the project root across Windows drives - #568

Merged
code-yeongyu merged 22 commits into
code-yeongyu:mainfrom
MoerAI:fix/rules-finder-cross-drive-project-scope
Oct 4, 2026
Merged

code-yeongyu merged 22 commits into
code-yeongyu:mainfrom
MoerAI:fix/rules-finder-cross-drive-project-scope

Conversation

@MoerAI

@MoerAI MoerAI commented Jul 31, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Keep vendored project-rule discovery inside its project root on Windows. A read/edit/write target on another drive could make the finder walk that drive and inject unrelated AGENTS.md, CLAUDE.md, or scoped rule files as project instructions. A target beneath the project with a differently cased drive letter could make the upward walk continue above the project root. Neither escape changes POSIX behavior.

Root cause and fix

In rules/finder.ts, relative("C:\\project", "D:\\other") is an absolute Windows path. The old startsWith("/") check accepted it as a child. The containment check now uses isAbsolute() like the sibling helper in rules/engine.ts. The walk also stops when relative(projectRoot, currentDirectory) === "", so Windows drive-letter case does not prevent recognition of the root.

The regression uses Windows path semantics on every CI platform to test both an unrelated drive and a differently cased drive letter. The vendored-rule rationale is recorded in its nearest changes.md, and the release note is under [Unreleased]. This is distinct from the already fixed project-root.ts hang in #432 and does not close #518.

Current-main update (2026-09-28)

Merged main again at 4c2e53aff; the only conflict was packages/coding-agent/CHANGELOG.md, resolved by keeping this entry under [Unreleased] as the earlier audit asked. With current main's finder.ts swapped in, both cross-drive regressions fail (2/2); with this PR they pass, along with 30 rules tests and the 6/6 CLI rules scenario. Check, build, and the PR changelog gate pass.

Reproduction and verification

Before the fix, the original physical Windows 11 reproduction placed a project on C: and an AGENTS.md on an unrelated D: drive. The exported findRuleCandidates returned the D: rule as a project rule. With the same real inputs after the fix, it returned no off-project candidates. The regression was red before the fix (D:\\other\\AGENTS.md was collected) and green after it.

Current-main integration: merge commit 311a286b8 includes main at fc8eaac59. The PR-only diff is the finder, its two-case regression, the nearest tracker, and the [Unreleased] changelog entry. Validation on macOS with Node 24:

  • CI=1 bun run --cwd packages/coding-agent test test/rules test/suite/regressions/0000-rules-finder-cross-drive-project-scope.test.ts — 4 files, 19 tests passed; the new regression passed 2/2.
  • CI=1 bun run --cwd packages/coding-agent test test/suite/rules-upstream-sync.test.ts — 10/10 passed.
  • bun run check and bun run build — both exited 0.
  • node scripts/check-pr-changelog.mjs --base origin/main — passed.
  • Real source CLI QA with an isolated fake model and read tool: the model received a synthetic rule inside the project and the read result, but no rule planted above the project root. The scenario passed 6/6, removed its temporary project/server, and verified the real auth file was unchanged. The public QA harness also passed node .agents/skills/senpi-qa/scripts/mock-loop.mjs --with-tool --api openai-responses (4/4) and node .agents/skills/senpi-qa/scripts/cli-smoke.mjs --self-test (8/8), without real provider calls.

A full local bun run test with Rust available passed the root script tests and progressed through workspace packages, but its coding-agent phase exceeded a 600-second bound; this is not a full-suite pass. The first attempt had failed because Cargo was missing from PATH; the six native-grep tests passed once the already installed Rust toolchain was on PATH. Hosted PR checks will determine the complete suite result. The physical cross-drive reproduction was performed on Windows for the original PR; this refresh's real-CLI scenario and platform-neutral Windows regressions ran on macOS.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 687b5d0adb

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@MoerAI

MoerAI commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

The only remaining gate is the stale Cubic neutral result from the July quota exhaustion. The quota reset date has passed, but the existing reviewed head was never re-evaluated. Please rerun or refresh Cubic for commit 687b5d0adb, or apply the repository quota-exhaustion skip policy if that is the intended path. No code failure is currently reported.

@code-yeongyu

Copy link
Copy Markdown
Owner

ULW audit verdict: BLOCKED. The production cross-drive containment fix integrates cleanly, but the authored test fails off Windows, the changelog and required contexts are missing, a P1 thread remains unresolved, and case-variant root termination is incomplete. Local evidence: local-ignore/qa-evidence/20260817-fix-pr-audit/pr-568/VERDICT.md.

…oot across Windows drives

`isSameOrChildPath` in the vendored rules finder rejected an escaping
`relative()` result with `startsWith("/")`. On Windows, `relative()` between
two different drive roots returns an absolute path — `relative("C:\\proj",
"D:\\other")` is `"D:\\other"` — which starts with neither ".." nor "/", so the
containment test accepted it.

`getWalkDirectories` therefore walked the other drive instead of falling back
to the project root, and `findProjectCandidates` collected `AGENTS.md`,
`CLAUDE.md`, `CONTEXT.md` and `.claude/rules` from that unrelated drive as
*project* rules, injecting them into the model context for a project that does
not own them.

The sibling helper in `rules/engine.ts` already uses `isAbsolute()` for the
same test; this aligns the finder with it. POSIX behavior is unchanged, since
`isAbsolute()` and `startsWith("/")` agree there.

Verified on Windows 11 with a real second drive: with a rule planted at
`D:\senpi-qa-518\AGENTS.md` and the project root on `C:`, discovery returned
that file as an `AGENTS.md` project rule at distance 1 before the change and
returns no candidates after it.
…gression

The regression mapped dirname/join/relative/resolve to their win32 variants but
left isAbsolute as the host implementation. posix.isAbsolute("D:\\other") is
false, so off Windows the containment guard never tripped, the cross-drive
AGENTS.md was still collected, and the test failed:

  AssertionError: expected [ 'D:\other\AGENTS.md' ] to deeply equal []

Mapping isAbsolute to win32.isAbsolute makes the regression exercise the guard
on any host. The mock factory also stops using an inline dynamic type import,
which the root AGENTS.md forbids, in favour of a top-level namespace import.
getWalkDirectories ended the upward walk with an exact string compare against
the project root. Windows drive letters are case-insensitive, so a target under
c:\workspace\proj\src never matched a C:\workspace\proj root and the walk
continued past it:

  ["c:\\workspace\\proj\\src", "c:\\workspace\\proj", "c:\\workspace", "c:\\"]

findProjectCandidates collects rules from every walked directory and does not
re-check containment per directory, so an AGENTS.md two levels above the project
root was injected as a project rule. This is the same escape the cross-drive fix
closes, reached through drive-letter case instead of a different drive.

relative() folds drive-letter case, so terminating on an empty relative path
stops at the root on Windows and is a no-op on posix, where the two comparisons
already agree.
Adds the [Unreleased] > Fixed changelog entry the changelog gate requires and
extends the vendored rules tracker with the getWalkDirectories termination
change alongside the existing isSameOrChildPath entry.
@MoerAI
MoerAI force-pushed the fix/rules-finder-cross-drive-project-scope branch from 687b5d0 to f74fde4 Compare September 7, 2026 15:17
@MoerAI

MoerAI commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (through #1465) and addressed all five items from the BLOCKED verdict. Mapping each one to its fix and evidence.

1. The authored test fails off Windows — 2f7236c

The mock mapped dirname/join/relative/resolve to their win32 variants but left isAbsolute as the host implementation. posix.isAbsolute("D:\\other") is false, so off Windows the containment guard never tripped and the cross-drive AGENTS.md was still collected:

AssertionError: expected [ 'D:\other\AGENTS.md' ] to deeply equal []
 ❯ test/suite/regressions/0000-rules-finder-cross-drive-project-scope.test.ts:45:57

Mapping isAbsolute to win32.isAbsolute makes the regression exercise the guard on any host. Green on macOS now.

2. The P1 thread remains unresolved — 2f7236c

Same defect: that is exactly what the Codex thread called out. Resolved by the same commit. While there I also replaced the factory's importOriginal<typeof import("node:path")>() with a top-level namespace import, since the root AGENTS.md forbids inline dynamic type imports.

3. Case-variant root termination is incomplete — 2838fab

getWalkDirectories ended the walk with an exact string compare against the project root. Windows drive letters are case-insensitive, so a target under c:\workspace\proj\src never matched a C:\workspace\proj root and the walk continued past it:

["c:\\workspace\\proj\\src", "c:\\workspace\\proj", "c:\\workspace", "c:\\"]

findProjectCandidates collects rules from every walked directory and does not re-check containment per directory, so this is user-visible, not just a wasted walk — an AGENTS.md above the project root is injected as a project rule. New regression, RED before the fix:

AssertionError: expected [ 'c:\workspace\AGENTS.md' ] to deeply equal []

Terminating on relative(projectRoot, currentDirectory) === "" folds drive-letter case and stops at the root. POSIX is unaffected: there the two comparisons already agree.

4. The changelog is missing — f74fde4

Added the [Unreleased] > Fixed entry for #568. node scripts/check-pr-changelog.mjs went from

changelog-gate: FAIL - runtime source changed without a CHANGELOG.md entry

to

changelog-gate: PASS - changes.md coverage complete (0 production path(s) covered); changelog entry updated (packages/coding-agent/CHANGELOG.md)

5. Required contexts are missing — f74fde4

I read this as the tracker requirement and checked it against the repo's own tooling rather than guessing. .github/upstream.json pins badlogic/pi-mono, and the rules builtin is vendored from code-yeongyu/pi-rules, so it is fork-only and the four-canonical-heading policy does not apply — the gate confirms this with 0 production path(s) covered. scripts/audit-changes-md.mjs also exits 0. I extended rules/changes.md with the getWalkDirectories change in the existing vendored-adaptation format alongside the isSameOrChildPath entry. If you would rather have the four headings here regardless of the pin, say so and I will restructure it.


Verification on the rebased branch:

bun run --cwd packages/coding-agent test test/rules test/suite/regressions/0000-rules-finder-cross-drive-project-scope.test.ts
  Test Files  4 passed (4)
       Tests  17 passed (17)

bun run check                      -> exit 0
scripts/check-pr-changelog.mjs     -> exit 0
scripts/audit-changes-md.mjs       -> exit 0

Real-CLI QA via .agents/skills/senpi-qa, both asserting ~/.senpi/agent/auth.json is unchanged:

mock-loop.mjs --with-tool : 4/4 passed   (zero-token full agent turn)
cli-smoke.mjs --self-test : 8/8 passed

Windows-specific semantics are covered by the win32-mocked unit tests, since the suite runs on POSIX here. The production diff is 8 added / 3 removed lines in finder.ts; the rest is the regression test and the two doc entries. Ready for re-review.

…-yeongyu#518)

Keep the existing PR changelog entry under Unreleased while preserving released history. Verified root check, 17 rules tests, real CLI tool turn, and 8 CLI smoke assertions.
…fixes code-yeongyu#518)

Preserve both Unreleased change records. Verified root check, 17 rules tests, mock CLI tool turn and CLI smoke.
code-yeongyu#518)

Preserve both changelog records while integrating main. Verified root check, 17 rules tests, mock CLI tool turn and 8 CLI smoke checks.
…-main

# Conflicts:
#	packages/coding-agent/src/core/extensions/builtin/rules/changes.md
…-main

# Conflicts:
#	packages/coding-agent/CHANGELOG.md
…-main

# Conflicts:
#	packages/coding-agent/CHANGELOG.md
…eleased]

v2026.9.28-6 stamped the previous [Unreleased] entries into a released section, which carried this entry with it on merge.
…s-drive-project-scope

# Conflicts:
#	packages/coding-agent/CHANGELOG.md
…s-drive-project-scope

# Conflicts:
#	packages/coding-agent/CHANGELOG.md
…-main

# Conflicts:
#	packages/coding-agent/CHANGELOG.md
@MoerAI

MoerAI commented Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

The one failing check, Eval kernel WebView (windows-latest), is packages/senpi-codemode/test/js-kernel-webview.test.ts > "drives a chrome-backed view: navigate, evaluate, click, screenshot", which hit its 60 s cell timeout (JS cell timed out after 60000ms). This PR only changes coding-agent rules discovery, and the same job passed on main and on the other PR runs started in the same minutes. The Check and test fan-in fails only because of it (every other required job passed). Could a maintainer re-run that job? I can't re-run upstream workflows from the fork.

@MoerAI

MoerAI commented Oct 3, 2026

Copy link
Copy Markdown
Contributor Author

Merged current main into this branch and moved the changelog bullet back under [Unreleased] (the merge had left it in the released 2026.10.5 section). The changelog gate passes again.

The one red job, Test (coding-agent 1/3) (Check and test fails only because of it), is test/rpc-host-zero-session-trim-real.test.ts > "collects once the last of three 100 MiB sessions closes, returning at least half of their growth" (expected 70 to be greater than or equal to 157.56). It measures memory reclaimed by a real RPC host after GC, and this PR only changes rules discovery. On this head locally, the same test failed once and then passed on a rerun. The shard is green on main's recent runs. The other 4,949 tests in the shard pass. Could a maintainer re-run that job? I can't re-run upstream workflows from the fork.

…s-drive-project-scope

# Conflicts:
#	packages/coding-agent/CHANGELOG.md

@code-yeongyu code-yeongyu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Approving at 977109276e, verified on that head:

  • The fix is right. isSameOrChildPath uses isAbsolute(), matching rules/engine.ts, so a cross-drive relative() result (D:\other) is no longer accepted as a child. The upward walk stops when relative(projectRoot, currentDirectory) === "", so c:\… vs C:\… no longer walks past the root. POSIX behavior is unchanged.
  • The regression test can fail. Its 2 Windows-semantics cases pass on this head and both fail with main's finder.ts. The rules suites stay green (4 files / 19 tests).
  • Change-log: the bullet is under [Unreleased], and the vendored-rule rationale is in its nearest changes.md.

Thanks for keeping this current since July, @MoerAI.

@code-yeongyu
code-yeongyu merged commit 28bebc5 into code-yeongyu:main Oct 4, 2026
37 checks passed
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.

Windows cross-drive project-root traversal fix

2 participants