fix(proxy): scope repo intelligence to project source and require direct evidence (#901)
* fix(proxy): skip vendor trees in repo intel BM25 was indexing .pixi/envs and other dependency dirs, then injecting those file:line guesses every turn. Exclude known vendor trees and drop the path block when no local scout is configured. Closes #899 Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com> * fix(proxy): detect dep trees by content, not name Name denylist missed conda envs, undotted pixi, and virtualenvs under any name, while dropping real first-party build/ and vendor/ dirs. - listFiles prefers `git ls-files --exclude-standard`, so the project's own .gitignore defines vendored - walk fallback identifies environments by contents (conda-meta, pyvenv.cfg, site-packages child), catching the interpreter stdlib they carry - ambiguous build/dist/target/vendor need a sibling manifest before exclusion - symlink and junction dirs are never followed Refs #899 Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com> * fix(proxy): treat the indexed repo as untrusted input Second review found the git listing both unsafe and incomplete. - gitCommand pins core.fsmonitor and other exec vectors off and strips GIT_*; fsmonitor runs during an index read, so `ls-files` in a hostile clone ran attacker code at session start - git-listed paths now run the same content filter as the walk, so an un-ignored conda prefix or committed vendor tree no longer rides in on git's say-so - an empty-but-successful listing means the cwd is ignored by an enclosing repo; fall back to the walk instead of reporting a complete map - Direct matches the file name, not the whole path: a term like "src" marked every file beneath it - ListingBasis discloses which path ran, hashed in Refs #899 Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com> * fix(proxy): log declined evidence, document invariants A declined turn stays silent to the model but now records repository_evidence_declined_no_direct_match, so weak evidence is distinguishable from a broken or disabled mechanism. Adds a positive control to the suppression test and records the gating, filtering, and git-hardening invariants in proxy/CLAUDE.md. Refs #899 Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com> * fix(installer): make prune path flavour explicit, not host-derived pruneOrphanedManagedHooks guards against pruning a hook whose path is absolute on the other platform. That guard is POSIX-only by construction: win32's absolute set is a superset of posix's, so a Windows host reads C:\... natively and prunes it. The test asserting the guard hard-coded a Windows path and so failed on the windows CI runner. Select the path flavour from an explicit platform arg (default process.platform, same as rewriteLegacyManagedHookCommands) so the POSIX side stays reachable from a Windows runner. Host behaviour unchanged. Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com> * test(cli): escape stub preload path in NODE_OPTIONS Node parses NODE_OPTIONS itself and treats a backslash inside a quoted value as an escape, so the raw Windows path reached --require with every separator eaten: Cannot find module 'C:UsersRUNNER~1AppDataLocalTempcave-pi-enableXbinstub-dispatch.cjs' Every stub-backed process died in the preload, failing all six pi enable and wrap round trips. The quotes must stay for profile names with spaces, so double the separators instead. POSIX is unaffected — stubEnv returns before this on non-Windows. Never caught before because npm test failed earlier in the same job, so test:windows has not run since these tests joined the gate. Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com> * fix(proxy): harden every untrusted git call, not just repo intelligence `repointel` hardened its own `git ls-files` and left three other call sites raw. `nativehook.currentRepositoryState` runs `git status` on EVERY hook event, and `status` refreshes the index, so it triggers `core.fsmonitor` harder than `ls-files` does — verified: the sentinel hook fires. Same for `store/detect_outcomes` and the two `execFileSync` sites in the CLI. - `proxy/internal/gitsafe` and `packages/cli/src/git-safe.ts` now own the `-c` override list; all five call sites route through them. - `verify_repo.py` fails the build on a NEW raw `git -C <dir>` call site, which is the regression class this bug actually belongs to. - `Bundle.DirectOnly()` strips metadata-only items BEFORE the evidence object is written. Filtering only at render time left the BM25-proximity guesses stored behind the `ccr://` handle, one retrieval from the model, which is what the block refuses to render. - `gitListBudget` caps git at half the caller's remaining warm budget. 2s of a 3s window left the fallback walk — the MORE expensive path — unable to finish, so the map failed closed in exactly the case the fallback exists for. - `NormalizeTerms` floors terms at 3 characters, matching the hook that produces them: direct evidence is a substring test on a file's own name, so `go`/`py`/`js` marked every file of that language a direct hit. Regression tests verified to fail without their fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(build): stop wedge generation racing itself on Windows mcp, shrink, and browse each run gen-wedge-installer from their own prepack/pretest, and every run writes all four targets, not just its own. `pnpm -r test` runs those packages in parallel, so several processes copy onto the same file at once and Windows fails the loser: EBUSY: resource busy or locked, copyfile packages/shared/binary-installer/installer.mjs -> mcp/bin/binary-installer.generated.mjs The bytes are identical every time and the outputs are committed, so the race was over writes that change nothing. Skip when the destination already matches, and land a real change through a temp file + rename so no concurrent reader sees a partial file. Verified with a 12-way concurrent storm, and against corrupted outputs to prove the write path still restores them. Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com> * fix(proxy): keep system git config readable while hardening GIT_CONFIG_NOSYSTEM defends against nothing here — /etc/gitconfig is root-owned, the threat is the repository's own .git/config, and the `-c` overrides beat every config level anyway. What it does cost is org-wide `safe.directory` allowances: without them `git status` fails "dubious ownership" on shared and CI machines, repository state comes back empty, and reuse silently stops. Dropped from both wrappers, with a test so it does not come back. Also names the fsmonitor tradeoff at the hot call site: disabling it taxes the people who configured it because their repo is huge, and status has a 100ms budget. That degrades safely; moving the budget is a latency decision for every user and needs a measurement this fix does not have. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(build): pin wedge generation against the concurrency it now survives The EBUSY fix has no committed regression proof, and the failure only appears when several packages run the script at once — which is exactly what `pnpm -r test` does and what a single-process check never reproduces. Eight concurrent runs must all succeed and leave the shared outputs byte-identical; a drifted output must still be repaired, so skip-if- identical cannot regress into skip-always. Runs in root `npm test`, which is the gate the Windows job failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com> Co-authored-by: Julius Brussee <juliusbrussee@getlockedin.nl> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
J
Julius Brussee committed
1b7ff8fae926c6fe3b72ad3786b14ebc11889d52
Parent: 7bb7130
Committed by GitHub <noreply@github.com>
on 8/24/2026, 11:12:21 PM