fix: bound auto-discovered resources to roots

This commit is contained in:
Peter Steinberger
2026-05-27 04:10:17 +01:00
parent 842bf24765
commit 35c8e061aa
2 changed files with 62 additions and 6 deletions

View File

@@ -120,6 +120,50 @@ describe("DefaultPackageManager", () => {
);
});
it("keeps auto-discovered project resources inside their resource roots", async () => {
const root = await makeTempDir("openclaw-package-manager-");
const configRoot = join(root, ".openclaw");
const outsideRoot = join(root, "outside");
const insidePrompt = join(configRoot, "prompts", "inside.md");
const insideTheme = join(configRoot, "themes", "inside.json");
const insideExtension = join(configRoot, "extensions", "inside.ts");
await mkdir(join(root, ".git"));
await mkdir(join(configRoot, "prompts"), { recursive: true });
await mkdir(join(configRoot, "themes"), { recursive: true });
await mkdir(join(configRoot, "extensions"), { recursive: true });
await mkdir(outsideRoot, { recursive: true });
await writeFile(insidePrompt, "# Inside\n", "utf-8");
await writeFile(insideTheme, "{}\n", "utf-8");
await writeFile(insideExtension, "export default {};\n", "utf-8");
await writeFile(join(outsideRoot, "outside.md"), "# Outside\n", "utf-8");
await writeFile(join(outsideRoot, "outside.json"), "{}\n", "utf-8");
await writeFile(join(outsideRoot, "outside.ts"), "export default {};\n", "utf-8");
try {
await symlink(join(outsideRoot, "outside.md"), join(configRoot, "prompts", "linked.md"));
await symlink(join(outsideRoot, "outside.json"), join(configRoot, "themes", "linked.json"));
await symlink(join(outsideRoot, "outside.ts"), join(configRoot, "extensions", "linked.ts"));
await symlink(outsideRoot, join(configRoot, "extensions", "linked-dir"), "dir");
} catch {
// Some filesystems disallow symlinks; the inside assertions still prove discovery.
}
const manager = new DefaultPackageManager({
cwd: root,
agentDir: join(root, "agent"),
settingsManager: SettingsManager.inMemory({}),
});
const resolved = await manager.resolve();
expect(resolved.prompts.map((prompt) => prompt.path)).toContain(insidePrompt);
expect(resolved.themes.map((theme) => theme.path)).toContain(insideTheme);
expect(resolved.extensions.map((extension) => extension.path)).toContain(insideExtension);
expect(resolved.prompts.some((prompt) => prompt.path.includes("linked"))).toBe(false);
expect(resolved.themes.some((theme) => theme.path.includes("linked"))).toBe(false);
expect(resolved.extensions.some((extension) => extension.path.includes("linked"))).toBe(false);
});
it("does not auto-install missing npm package resources", async () => {
const root = await makeTempDir("openclaw-package-manager-");
const manager = new DefaultPackageManager({

View File

@@ -422,6 +422,9 @@ function collectAutoPromptEntries(dir: string): string[] {
}
const fullPath = join(dir, entry.name);
if (!isRealPathWithinRoot(dir, fullPath)) {
continue;
}
let isFile = entry.isFile();
if (entry.isSymbolicLink()) {
try {
@@ -467,6 +470,9 @@ function collectAutoThemeEntries(dir: string): string[] {
}
const fullPath = join(dir, entry.name);
if (!isRealPathWithinRoot(dir, fullPath)) {
continue;
}
let isFile = entry.isFile();
if (entry.isSymbolicLink()) {
try {
@@ -502,7 +508,7 @@ function readResourceManifestFile(packageJsonPath: string): ResourceManifest | n
}
}
function resolveExtensionEntries(dir: string): string[] | null {
function resolveExtensionEntries(dir: string, rootDir = dir): string[] | null {
const packageJsonPath = join(dir, "package.json");
if (existsSync(packageJsonPath)) {
const manifest = readResourceManifestFile(packageJsonPath);
@@ -510,7 +516,7 @@ function resolveExtensionEntries(dir: string): string[] | null {
const entries: string[] = [];
for (const extPath of manifest.extensions) {
const resolvedExtPath = resolve(dir, extPath);
if (existsSync(resolvedExtPath)) {
if (existsSync(resolvedExtPath) && isRealPathWithinRoot(rootDir, resolvedExtPath)) {
entries.push(resolvedExtPath);
}
}
@@ -522,10 +528,10 @@ function resolveExtensionEntries(dir: string): string[] | null {
const indexTs = join(dir, "index.ts");
const indexJs = join(dir, "index.js");
if (existsSync(indexTs)) {
if (existsSync(indexTs) && isRealPathWithinRoot(rootDir, indexTs)) {
return [indexTs];
}
if (existsSync(indexJs)) {
if (existsSync(indexJs) && isRealPathWithinRoot(rootDir, indexJs)) {
return [indexJs];
}
@@ -559,6 +565,9 @@ function collectAutoExtensionEntries(dir: string): string[] {
}
const fullPath = join(dir, entry.name);
if (!isRealPathWithinRoot(dir, fullPath)) {
continue;
}
let isDir = entry.isDirectory();
let isFile = entry.isFile();
@@ -581,7 +590,7 @@ function collectAutoExtensionEntries(dir: string): string[] {
if (isFile && (entry.name.endsWith(".ts") || entry.name.endsWith(".js"))) {
entries.push(fullPath);
} else if (isDir) {
const resolvedEntries = resolveExtensionEntries(fullPath);
const resolvedEntries = resolveExtensionEntries(fullPath, dir);
if (resolvedEntries) {
entries.push(...resolvedEntries);
}
@@ -622,7 +631,10 @@ function isPathWithinRoot(root: string, candidate: string): boolean {
}
function isRealPathWithinRoot(root: string, candidate: string): boolean {
return isPathWithinRoot(resolveRealPathIfPossible(resolve(root)), resolveRealPathIfPossible(candidate));
return isPathWithinRoot(
resolveRealPathIfPossible(resolve(root)),
resolveRealPathIfPossible(candidate),
);
}
function matchesAnyPattern(filePath: string, patterns: string[], baseDir: string): boolean {