diff --git a/packages/rules-core/src/finder.ts b/packages/rules-core/src/finder.ts index 71bd12205..b7cb4aea9 100644 --- a/packages/rules-core/src/finder.ts +++ b/packages/rules-core/src/finder.ts @@ -5,12 +5,22 @@ import { GLOBAL_DISTANCE, OPENCODE_USER_RULE_DIRS, PROJECT_RULE_FILES, PROJECT_R import { sortCandidates } from "./ordering"; import { findRuleFilesRecursive, safeRealpathSync } from "./scanner"; import type { DirectoryScanEntry, FindRuleFilesOptions, RuleFileCandidate, RuleScanCache, RuleSource } from "./types"; -import { log } from "../../../src/shared/logger"; + +export type SisyphusRuleDeprecationLogger = ( + message: string, + meta: { event: string; path: string }, +) => void; + +const noopSisyphusRuleDeprecationLogger: SisyphusRuleDeprecationLogger = () => {}; const SISYPHUS_DEPRECATION_MESSAGE = "[rules] .sisyphus/rules is deprecated and will be removed in v4.3.0; migrate to .omo/rules"; const SISYPHUS_LEGACY_RULE_SOURCES: ReadonlySet = new Set([".sisyphus/rules", "~/.sisyphus/rules"]); const warnedSisyphusRuleDirectories = new Set(); -let logSisyphusRuleDeprecation: typeof log = log; +let logSisyphusRuleDeprecation: SisyphusRuleDeprecationLogger = noopSisyphusRuleDeprecationLogger; + +export function setSisyphusRuleDeprecationLogger(logger: SisyphusRuleDeprecationLogger): void { + logSisyphusRuleDeprecation = logger; +} export function findRuleFiles( projectRoot: string | null, @@ -92,9 +102,10 @@ function addProjectSingleFileCandidates( candidates: RuleFileCandidate[], seenRealPaths: Set, ): void { + const projectRootRealPath = safeRealpathSync(projectRoot); for (const ruleFile of PROJECT_RULE_FILES) { const filePath = join(projectRoot, ruleFile); - const realPath = validFileRealPath(filePath); + const realPath = validFileRealPath(filePath, projectRootRealPath); if (realPath === null || seenRealPaths.has(realPath)) continue; seenRealPaths.add(realPath); candidates.push({ @@ -135,11 +146,11 @@ function addUserRuleCandidates( } } -function scanDirectoryWithCache(dir: string, cache: RuleScanCache | undefined): readonly DirectoryScanEntry[] { +function scanDirectoryWithCache(dir: string, cache: RuleScanCache | undefined, boundaryRealPath?: string): readonly DirectoryScanEntry[] { const cached = cache?.getDirScan(dir); if (cached) return cached; const entries: DirectoryScanEntry[] = []; - findRuleFilesRecursive(dir, entries); + findRuleFilesRecursive(dir, entries, new Set(), boundaryRealPath); cache?.setDirScan(dir, entries); return entries; } @@ -155,20 +166,22 @@ function warnSisyphusRuleDeprecation(source: RuleSource, path: string): void { }); } -export function _setSisyphusRuleDeprecationLoggerForTesting(logger: typeof log): void { +export function _setSisyphusRuleDeprecationLoggerForTesting(logger: SisyphusRuleDeprecationLogger): void { logSisyphusRuleDeprecation = logger; } export function _resetSisyphusRuleDeprecationWarningStateForTesting(): void { warnedSisyphusRuleDirectories.clear(); - logSisyphusRuleDeprecation = log; + logSisyphusRuleDeprecation = noopSisyphusRuleDeprecationLogger; } -function validFileRealPath(filePath: string): string | null { +function validFileRealPath(filePath: string, boundaryRealPath?: string): string | null { if (!existsSync(filePath)) return null; try { if (!statSync(filePath).isFile()) return null; - return safeRealpathSync(filePath); + const realPath = safeRealpathSync(filePath); + if (boundaryRealPath !== undefined && !isSameOrChildPath(realPath, boundaryRealPath)) return null; + return realPath; } catch { return null; } diff --git a/packages/rules-core/src/index.ts b/packages/rules-core/src/index.ts index e4f3c247e..b9f11f9f9 100644 --- a/packages/rules-core/src/index.ts +++ b/packages/rules-core/src/index.ts @@ -1,6 +1,6 @@ export { createAgentsMdCache, createRuleScanCache } from "./cache"; export { findAgentsMdUp, type FindAgentsMdUpInput } from "./agents-md"; -export { findRuleFiles } from "./finder"; +export { findRuleFiles, setSisyphusRuleDeprecationLogger, type SisyphusRuleDeprecationLogger } from "./finder"; export { parseRuleFrontmatter } from "./parser"; export { shouldApplyRule, createContentHash, isDuplicateByContentHash, isDuplicateByRealPath, resetMatcherCache, getMatcherCacheStats } from "./matcher"; export { findProjectRoot, clearProjectRootCache } from "./project-root"; diff --git a/packages/rules-core/src/scanner.ts b/packages/rules-core/src/scanner.ts index ff1fbce6c..dfa8e83f4 100644 --- a/packages/rules-core/src/scanner.ts +++ b/packages/rules-core/src/scanner.ts @@ -1,5 +1,5 @@ -import { existsSync, readdirSync, realpathSync } from "node:fs"; -import { join } from "node:path"; +import { existsSync, readdirSync, realpathSync, type Dirent } from "node:fs"; +import { isAbsolute, join, relative } from "node:path"; import { EXCLUDED_DIRS, GITHUB_INSTRUCTIONS_PATTERN, RULE_EXTENSIONS } from "./constants"; import type { DirectoryScanEntry } from "./types"; @@ -20,12 +20,24 @@ export function safeRealpathSync(filePath: string): string { } } -export function findRuleFilesRecursive(dir: string, results: DirectoryScanEntry[], visited = new Set()): void { +function isPathWithinRoot(candidate: string, root: string): boolean { + const rel = relative(root, candidate); + return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel)); +} + +export function findRuleFilesRecursive( + dir: string, + results: DirectoryScanEntry[], + visited = new Set(), + boundaryRoot?: string, +): void { if (!existsSync(dir)) return; const realDir = safeRealpathSync(dir); + const effectiveBoundary = boundaryRoot ?? realDir; + if (!isPathWithinRoot(realDir, effectiveBoundary)) return; if (visited.has(realDir)) return; visited.add(realDir); - let entries; + let entries: Dirent[] = []; try { entries = readdirSync(dir, { withFileTypes: true, encoding: "utf8" }).sort((left, right) => left.name.localeCompare(right.name)); } catch { @@ -34,11 +46,13 @@ export function findRuleFilesRecursive(dir: string, results: DirectoryScanEntry[ for (const entry of entries) { const fullPath = join(dir, entry.name); if (entry.isDirectory()) { - if (!EXCLUDED_DIRS.has(entry.name)) findRuleFilesRecursive(fullPath, results, visited); + if (!EXCLUDED_DIRS.has(entry.name)) findRuleFilesRecursive(fullPath, results, visited, effectiveBoundary); continue; } if (entry.isFile() && isRuleFile(entry.name, dir)) { - results.push({ path: fullPath, realPath: safeRealpathSync(fullPath), relativePath: entry.name }); + const realPath = safeRealpathSync(fullPath); + if (!isPathWithinRoot(realPath, effectiveBoundary)) continue; + results.push({ path: fullPath, realPath, relativePath: entry.name }); } } } diff --git a/src/hooks/rules-injector/rule-file-finder.ts b/src/hooks/rules-injector/rule-file-finder.ts index 64c50d937..3062e7df6 100644 --- a/src/hooks/rules-injector/rule-file-finder.ts +++ b/src/hooks/rules-injector/rule-file-finder.ts @@ -1,2 +1,7 @@ +import { setSisyphusRuleDeprecationLogger } from "@oh-my-opencode/rules-core"; +import { log } from "../../shared/logger"; + +setSisyphusRuleDeprecationLogger(log); + export { findRuleFiles } from "@oh-my-opencode/rules-core"; export type { FindRuleFilesOptions } from "@oh-my-opencode/rules-core";