diff --git a/config/assertion-safety-baseline.txt b/config/assertion-safety-baseline.txt index 8fd04350dae6..08d8a11c8b5f 100644 --- a/config/assertion-safety-baseline.txt +++ b/config/assertion-safety-baseline.txt @@ -2028,7 +2028,7 @@ src/agents/sessions/extensions/types.ts 1 src/agents/sessions/keybindings.ts 2 src/agents/sessions/model-registry.ts 13 src/agents/sessions/model-resolver.ts 1 -src/agents/sessions/package-manager.ts 8 +src/agents/sessions/package-manager.ts 3 src/agents/sessions/resolve-config-value.ts 1 src/agents/sessions/sdk.ts 3 src/agents/sessions/session-manager-branching.ts 1 diff --git a/src/agents/sessions/package-manager.test.ts b/src/agents/sessions/package-manager.test.ts index a31034c5399f..2ab1e7fb3449 100644 --- a/src/agents/sessions/package-manager.test.ts +++ b/src/agents/sessions/package-manager.test.ts @@ -2,7 +2,7 @@ // project, and npm-declared agent resources. import { mkdir, stat, symlink, writeFile } from "node:fs/promises"; import { join, relative } from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { useAutoCleanupTempDirTracker } from "../../../test/helpers/temp-dir.js"; import { withEnvAsync } from "../../test-utils/env.js"; import { DefaultPackageManager } from "./package-manager.js"; @@ -284,6 +284,64 @@ describe("DefaultPackageManager", () => { expect(resolved.themes).toEqual([]); }); + it("honors filters on direct local extension files", async () => { + const root = tempDirs.make("openclaw-package-manager-filter-"); + const extensionPath = join(root, "extension.ts"); + await writeFile(extensionPath, "export default {};\n", "utf-8"); + const manager = new DefaultPackageManager({ + cwd: root, + agentDir: join(root, "agent"), + settingsManager: SettingsManager.inMemory({ + packages: [{ source: extensionPath, extensions: [] }], + }), + }); + + expect((await manager.resolve()).extensions).toEqual([ + expect.objectContaining({ path: extensionPath, enabled: false }), + ]); + }); + + it("treats object local directories without filters like string sources", async () => { + const root = tempDirs.make("openclaw-package-manager-object-"); + const extensionDir = join(root, "extension"); + const extensionPath = join(extensionDir, "index.ts"); + await mkdir(extensionDir); + await writeFile(extensionPath, "export default {};\n", "utf-8"); + const resolveSource = async (source: string | { source: string; extensions?: string[] }) => + await new DefaultPackageManager({ + cwd: root, + agentDir: join(root, "agent"), + settingsManager: SettingsManager.inMemory({ packages: [source] }), + }).resolve(); + + expect(await resolveSource({ source: extensionDir })).toEqual( + await resolveSource(extensionDir), + ); + expect((await resolveSource({ source: extensionDir, extensions: [] })).extensions).toEqual([ + expect.objectContaining({ path: extensionDir, enabled: false }), + ]); + }); + + it.each([ + ["local", "./missing-extension.ts"], + ["npm", "npm:@openclaw/missing-test"], + ["git", "https://github.com/openclaw/missing-test.git"], + ])("reports missing %s package sources through the owner callback", async (_kind, source) => { + const root = tempDirs.make("openclaw-package-manager-missing-"); + const onMissing = vi.fn(async () => "skip" as const); + const manager = new DefaultPackageManager({ + cwd: root, + agentDir: join(root, "agent"), + settingsManager: SettingsManager.inMemory({ packages: [source] }), + }); + + const resolved = await manager.resolve(onMissing); + + expect(onMissing).toHaveBeenCalledOnce(); + expect(onMissing).toHaveBeenCalledWith(source); + expect(resolved).toEqual({ extensions: [], skills: [], prompts: [], themes: [] }); + }); + it("keeps temporary package paths in a private per-agent directory", async () => { const root = tempDirs.make("openclaw-package-manager-temp-"); const agentDir = join(root, "agent"); diff --git a/src/agents/sessions/package-manager.ts b/src/agents/sessions/package-manager.ts index 73636a78c89e..134e2e3ddc44 100644 --- a/src/agents/sessions/package-manager.ts +++ b/src/agents/sessions/package-manager.ts @@ -88,13 +88,6 @@ interface ResourceManifest { themes?: string[]; } -interface ResourceAccumulator { - extensions: Map; - skills: Map; - prompts: Map; - themes: Map; -} - /** * Compute a numeric precedence rank for a resource based on its metadata. * Lower rank = higher precedence. Used to sort resolved resources so that @@ -124,8 +117,14 @@ interface PackageFilter { type ResourceType = "extensions" | "skills" | "prompts" | "themes"; type TopLevelAutoResourceType = Extract; +type ResourceState = { metadata: PathMetadata; enabled: boolean }; +type ResourceAccumulator = Record>; -const RESOURCE_TYPES: ResourceType[] = ["extensions", "skills", "prompts", "themes"]; +const RESOURCE_TYPES = ["extensions", "skills", "prompts", "themes"] as const; + +type ResolvedPackageTarget = + | { kind: "missing" } + | { kind: "file" | "directory"; path: string; baseDir: string }; const FILE_PATTERNS: Record = { extensions: /\.(ts|js)$/, @@ -543,6 +542,13 @@ function collectResourceFiles(dir: string, resourceType: ResourceType): string[] return collectFiles(dir, FILE_PATTERNS[resourceType]); } +const AUTO_RESOURCE_COLLECTORS = { + extensions: collectAutoExtensionEntries, + skills: (dir: string) => collectAutoSkillEntries(dir, "openclaw"), + prompts: (dir: string) => collectTopLevelAutoResourceEntries(dir, "prompts"), + themes: (dir: string) => collectTopLevelAutoResourceEntries(dir, "themes"), +} satisfies Record string[]>; + function resolveRealPathIfPossible(path: string): string { try { return realpathSync.native(path); @@ -658,6 +664,13 @@ function applyPatterns(allPaths: string[], patterns: string[], baseDir: string): return new Set(result); } +function getPackageFilter(pkg: PackageSource): PackageFilter | undefined { + if (typeof pkg === "string") { + return undefined; + } + return RESOURCE_TYPES.some((resourceType) => pkg[resourceType] !== undefined) ? pkg : undefined; +} + export class DefaultPackageManager implements PackageManager { private cwd: string; private agentDir: string; @@ -692,32 +705,20 @@ export class DefaultPackageManager implements PackageManager { const globalBaseDir = this.agentDir; const projectBaseDir = join(this.cwd, CONFIG_DIR_NAME); + const localScopes = [ + { scope: "project", settings: projectSettings, baseDir: projectBaseDir }, + { scope: "user", settings: globalSettings, baseDir: globalBaseDir }, + ] as const; for (const resourceType of RESOURCE_TYPES) { - const target = this.getTargetMap(accumulator, resourceType); - const globalEntries = globalSettings[resourceType] ?? []; - const projectEntries = projectSettings[resourceType] ?? []; - this.resolveLocalEntries( - projectEntries, - resourceType, - target, - { - source: "local", - scope: "project", - origin: "top-level", - }, - projectBaseDir, - ); - this.resolveLocalEntries( - globalEntries, - resourceType, - target, - { - source: "local", - scope: "user", - origin: "top-level", - }, - globalBaseDir, - ); + for (const { scope, settings, baseDir } of localScopes) { + this.resolveLocalEntries( + settings[resourceType] ?? [], + resourceType, + accumulator[resourceType], + { source: "local", scope, origin: "top-level" }, + baseDir, + ); + } } this.addAutoDiscoveredResources( @@ -753,79 +754,89 @@ export class DefaultPackageManager implements PackageManager { ): Promise { for (const { pkg, scope } of sources) { const sourceStr = typeof pkg === "string" ? pkg : pkg.source; - const filter = typeof pkg === "object" ? pkg : undefined; + const filter = getPackageFilter(pkg); const parsed = this.parseSource(sourceStr); const metadata: PathMetadata = { source: sourceStr, scope, origin: "package" }; - - if (parsed.type === "local") { - const baseDir = this.getBaseDirForScope(scope); - this.resolveLocalExtensionSource(parsed, accumulator, filter, metadata, baseDir); + const target = this.resolvePackageTarget(parsed, scope); + if (!target) { continue; } - - const handleMissing = async (): Promise => { - if (!onMissing) { - return; - } - const action = await onMissing(sourceStr); - if (action === "error") { + if (target.kind === "missing") { + if (onMissing && (await onMissing(sourceStr)) === "error") { throw new Error(`Missing source: ${sourceStr}`); } - }; - - if (parsed.type === "npm") { - const installedPath = this.getNpmInstallPath(parsed, scope); - const missingOrWrongVersion = - !existsSync(installedPath) || - (parsed.pinned && !this.installedNpmMatchesPinnedVersion(parsed, installedPath)); - if (missingOrWrongVersion) { - await handleMissing(); - continue; - } - metadata.baseDir = installedPath; - this.collectPackageResources(installedPath, accumulator, filter, metadata); continue; } - if (parsed.type === "git") { - const installedPath = this.getGitInstallPath(parsed, scope); - if (!existsSync(installedPath)) { - await handleMissing(); - continue; - } - metadata.baseDir = installedPath; - this.collectPackageResources(installedPath, accumulator, filter, metadata); + metadata.baseDir = target.baseDir; + if (target.kind === "file") { + this.addResource( + accumulator.extensions, + target.path, + metadata, + this.isExtensionEnabled(target.path, filter?.extensions, target.baseDir), + ); + continue; + } + + const hasPackageLayout = this.collectPackageResources( + target.path, + accumulator, + filter, + metadata, + ); + if (parsed.type === "local" && !hasPackageLayout) { + this.addResource( + accumulator.extensions, + target.path, + metadata, + this.isExtensionEnabled(target.path, filter?.extensions, target.baseDir), + ); } } } - private resolveLocalExtensionSource( - source: LocalSource, - accumulator: ResourceAccumulator, - filter: PackageFilter | undefined, - metadata: PathMetadata, - baseDir: string, - ): void { - const resolved = this.resolvePathFromBase(source.path, baseDir); - if (!existsSync(resolved)) { - return; + private resolvePackageTarget( + source: ParsedSource, + scope: SourceScope, + ): ResolvedPackageTarget | undefined { + if (source.type === "npm") { + const path = this.getNpmInstallPath(source, scope); + return !existsSync(path) || + (source.pinned && !this.installedNpmMatchesPinnedVersion(source, path)) + ? { kind: "missing" } + : { kind: "directory", path, baseDir: path }; + } + if (source.type === "git") { + const path = this.getGitInstallPath(source, scope); + return existsSync(path) ? { kind: "directory", path, baseDir: path } : { kind: "missing" }; } + const path = this.resolvePathFromBase(source.path, this.getBaseDirForScope(scope)); + if (!existsSync(path)) { + return { kind: "missing" }; + } try { - const stats = statSync(resolved); + const stats = statSync(path); if (stats.isFile()) { - metadata.baseDir = dirname(resolved); - this.addResource(accumulator.extensions, resolved, metadata, true); - return; + return { kind: "file", path, baseDir: dirname(path) }; } if (stats.isDirectory()) { - metadata.baseDir = resolved; - const resources = this.collectPackageResources(resolved, accumulator, filter, metadata); - if (!resources) { - this.addResource(accumulator.extensions, resolved, metadata, true); - } + return { kind: "directory", path, baseDir: path }; } } catch {} + return undefined; + } + + private isExtensionEnabled( + path: string, + patterns: string[] | undefined, + baseDir: string, + ): boolean { + if (patterns === undefined) { + return true; + } + return patterns.length > 0 && applyPatterns([path], patterns, baseDir).has(path); } private parseSource(source: string): ParsedSource { @@ -1012,69 +1023,29 @@ export class DefaultPackageManager implements PackageManager { filter: PackageFilter | undefined, metadata: PathMetadata, ): boolean { - if (filter) { - for (const resourceType of RESOURCE_TYPES) { - const patterns = filter[resourceType as keyof PackageFilter]; - const target = this.getTargetMap(accumulator, resourceType); - if (patterns !== undefined) { - this.applyPackageFilter(packageRoot, patterns, resourceType, target, metadata); - } else { - this.collectDefaultResources(packageRoot, resourceType, target, metadata); - } - } - return true; - } - - const manifest = this.readResourceManifest(packageRoot); - if (manifest) { - for (const resourceType of RESOURCE_TYPES) { - const entries = manifest[resourceType as keyof ResourceManifest]; - this.addManifestEntries( - entries, - packageRoot, - resourceType, - this.getTargetMap(accumulator, resourceType), - metadata, - ); - } - return true; - } - - let hasAnyDir = false; + const manifest = readResourceManifestFile(join(packageRoot, "package.json")); + const hasPackageLayout = + manifest !== null || RESOURCE_TYPES.some((type) => existsSync(join(packageRoot, type))); for (const resourceType of RESOURCE_TYPES) { + const patterns = filter?.[resourceType]; + const target = accumulator[resourceType]; + if (patterns !== undefined) { + this.applyPackageFilter(packageRoot, patterns, resourceType, target, metadata); + continue; + } + const entries = manifest?.[resourceType]; + if (manifest !== null && (!filter || entries !== undefined)) { + this.addManifestEntries(entries, packageRoot, resourceType, target, metadata); + continue; + } const dir = join(packageRoot, resourceType); if (existsSync(dir)) { - // Collect all files from the directory (all enabled by default) - const files = this.collectConventionResourceFiles(packageRoot, resourceType); - for (const f of files) { - this.addResource(this.getTargetMap(accumulator, resourceType), f, metadata, true); + for (const path of this.collectConventionResourceFiles(packageRoot, resourceType)) { + this.addResource(target, path, metadata, true); } - hasAnyDir = true; - } - } - return hasAnyDir; - } - - private collectDefaultResources( - packageRoot: string, - resourceType: ResourceType, - target: Map, - metadata: PathMetadata, - ): void { - const manifest = this.readResourceManifest(packageRoot); - const entries = manifest?.[resourceType as keyof ResourceManifest]; - if (entries) { - this.addManifestEntries(entries, packageRoot, resourceType, target, metadata); - return; - } - const dir = join(packageRoot, resourceType); - if (existsSync(dir)) { - // Collect all files from the directory (all enabled by default) - const files = this.collectConventionResourceFiles(packageRoot, resourceType); - for (const f of files) { - this.addResource(target, f, metadata, true); } } + return hasPackageLayout; } private applyPackageFilter( @@ -1084,7 +1055,7 @@ export class DefaultPackageManager implements PackageManager { target: Map, metadata: PathMetadata, ): void { - const { allFiles } = this.collectManifestFiles(packageRoot, resourceType); + const allFiles = this.collectManifestFiles(packageRoot, resourceType); if (userPatterns.length === 0) { // Empty array explicitly disables all resources of this type @@ -1103,29 +1074,20 @@ export class DefaultPackageManager implements PackageManager { } } - /** - * Collect all files from a package for a resource type, applying manifest patterns. - * Returns { allFiles, enabledByManifest } where enabledByManifest is the set of files - * that pass the manifest's own patterns. - */ - private collectManifestFiles( - packageRoot: string, - resourceType: ResourceType, - ): { allFiles: string[]; enabledByManifest: Set } { - const manifest = this.readResourceManifest(packageRoot); - const entries = manifest?.[resourceType as keyof ResourceManifest]; + private collectManifestFiles(packageRoot: string, resourceType: ResourceType): string[] { + const manifest = readResourceManifestFile(join(packageRoot, "package.json")); + const entries = manifest?.[resourceType]; if (entries && entries.length > 0) { const allFiles = this.collectFilesFromManifestEntries(entries, packageRoot, resourceType); const manifestPatterns = entries.filter(isOverridePattern); - const enabledByManifest = + return Array.from( manifestPatterns.length > 0 ? applyPatterns(allFiles, manifestPatterns, packageRoot) - : new Set(allFiles); - return { allFiles: Array.from(enabledByManifest), enabledByManifest }; + : new Set(allFiles), + ); } - const allFiles = this.collectConventionResourceFiles(packageRoot, resourceType); - return { allFiles, enabledByManifest: new Set(allFiles) }; + return this.collectConventionResourceFiles(packageRoot, resourceType); } private collectConventionResourceFiles( @@ -1142,21 +1104,6 @@ export class DefaultPackageManager implements PackageManager { ); } - private readResourceManifest(packageRoot: string): ResourceManifest | null { - const packageJsonPath = join(packageRoot, "package.json"); - if (!existsSync(packageJsonPath)) { - return null; - } - - try { - const content = readFileSync(packageJsonPath, "utf-8"); - const pkg = JSON.parse(content) as { openclaw?: ResourceManifest }; - return pkg.openclaw ?? null; - } catch { - return null; - } - } - private addManifestEntries( entries: string[] | undefined, root: string, @@ -1244,160 +1191,73 @@ export class DefaultPackageManager implements PackageManager { globalBaseDir: string, projectBaseDir: string, ): void { - const userMetadata: PathMetadata = { - source: "auto", - scope: "user", - origin: "top-level", - baseDir: globalBaseDir, - }; - const projectMetadata: PathMetadata = { - source: "auto", - scope: "project", - origin: "top-level", - baseDir: projectBaseDir, - }; - - const userOverrides = { - extensions: globalSettings.extensions ?? [], - skills: globalSettings.skills ?? [], - prompts: globalSettings.prompts ?? [], - themes: globalSettings.themes ?? [], - }; - const projectOverrides = { - extensions: projectSettings.extensions ?? [], - skills: projectSettings.skills ?? [], - prompts: projectSettings.prompts ?? [], - themes: projectSettings.themes ?? [], - }; - - const userDirs = { - extensions: join(globalBaseDir, "extensions"), - skills: join(globalBaseDir, "skills"), - prompts: join(globalBaseDir, "prompts"), - themes: join(globalBaseDir, "themes"), - }; - const projectDirs = { - extensions: join(projectBaseDir, "extensions"), - skills: join(projectBaseDir, "skills"), - prompts: join(projectBaseDir, "prompts"), - themes: join(projectBaseDir, "themes"), - }; const userAgentsSkillsDir = join(getHomeDir(), ".agents", "skills"); const projectAgentsSkillDirs = collectAncestorAgentsSkillDirs(this.cwd).filter( (dir) => resolve(dir) !== resolve(userAgentsSkillsDir), ); + const addScopeResources = (options: { + scope: Extract; + settings: ReturnType; + baseDir: string; + agentsSkillDirs: string[]; + }): void => { + const metadata: PathMetadata = { + source: "auto", + scope: options.scope, + origin: "top-level", + baseDir: options.baseDir, + }; + const addResources = ( + resourceType: ResourceType, + paths: string[], + resourceMetadata = metadata, + patternBaseDir = options.baseDir, + ): void => { + for (const path of paths) { + this.addResource( + accumulator[resourceType], + path, + resourceMetadata, + isEnabledByOverrides(path, options.settings[resourceType] ?? [], patternBaseDir), + ); + } + }; - const addResources = ( - resourceType: ResourceType, - paths: string[], - metadata: PathMetadata, - overrides: string[], - baseDir: string, - ) => { - const target = this.getTargetMap(accumulator, resourceType); - for (const path of paths) { - const enabled = isEnabledByOverrides(path, overrides, baseDir); - this.addResource(target, path, metadata, enabled); + for (const resourceType of RESOURCE_TYPES) { + addResources( + resourceType, + AUTO_RESOURCE_COLLECTORS[resourceType](join(options.baseDir, resourceType)), + ); + if (resourceType !== "skills") { + continue; + } + // .agents roots use different traversal and pattern bases; flattening them + // into the ordinary skill directory would weaken containment and overrides. + for (const skillsDir of options.agentsSkillDirs) { + const agentsBaseDir = dirname(skillsDir); + addResources( + "skills", + collectAutoSkillEntries(skillsDir, "agents"), + { ...metadata, baseDir: agentsBaseDir }, + agentsBaseDir, + ); + } } }; - // Project extensions from the embedded agent project directory. - addResources( - "extensions", - collectAutoExtensionEntries(projectDirs.extensions), - projectMetadata, - projectOverrides.extensions, - projectBaseDir, - ); - - // Project skills from the embedded agent project directory. - addResources( - "skills", - collectAutoSkillEntries(projectDirs.skills, "openclaw"), - projectMetadata, - projectOverrides.skills, - projectBaseDir, - ); - - // Project skills from .agents/ (each with its own baseDir) - for (const agentsSkillsDir of projectAgentsSkillDirs) { - const agentsBaseDir = dirname(agentsSkillsDir); // the .agents directory - const agentsMetadata: PathMetadata = { - ...projectMetadata, - baseDir: agentsBaseDir, - }; - addResources( - "skills", - collectAutoSkillEntries(agentsSkillsDir, "agents"), - agentsMetadata, - projectOverrides.skills, - agentsBaseDir, - ); - } - - addResources( - "prompts", - collectTopLevelAutoResourceEntries(projectDirs.prompts, "prompts"), - projectMetadata, - projectOverrides.prompts, - projectBaseDir, - ); - addResources( - "themes", - collectTopLevelAutoResourceEntries(projectDirs.themes, "themes"), - projectMetadata, - projectOverrides.themes, - projectBaseDir, - ); - - // User extensions from ~/.openclaw/agent/ - addResources( - "extensions", - collectAutoExtensionEntries(userDirs.extensions), - userMetadata, - userOverrides.extensions, - globalBaseDir, - ); - - // User skills from ~/.openclaw/agent/ - addResources( - "skills", - collectAutoSkillEntries(userDirs.skills, "openclaw"), - userMetadata, - userOverrides.skills, - globalBaseDir, - ); - - if (isDefaultStateDir()) { - // Home-scoped personal skills belong to the default install, not isolated state roots. - const userAgentsBaseDir = dirname(userAgentsSkillsDir); - const userAgentsMetadata: PathMetadata = { - ...userMetadata, - baseDir: userAgentsBaseDir, - }; - addResources( - "skills", - collectAutoSkillEntries(userAgentsSkillsDir, "agents"), - userAgentsMetadata, - userOverrides.skills, - userAgentsBaseDir, - ); - } - - addResources( - "prompts", - collectTopLevelAutoResourceEntries(userDirs.prompts, "prompts"), - userMetadata, - userOverrides.prompts, - globalBaseDir, - ); - addResources( - "themes", - collectTopLevelAutoResourceEntries(userDirs.themes, "themes"), - userMetadata, - userOverrides.themes, - globalBaseDir, - ); + addScopeResources({ + scope: "project", + settings: projectSettings, + baseDir: projectBaseDir, + agentsSkillDirs: projectAgentsSkillDirs, + }); + addScopeResources({ + scope: "user", + settings: globalSettings, + baseDir: globalBaseDir, + // Home-scoped personal skills belong only to the default install. + agentsSkillDirs: isDefaultStateDir() ? [userAgentsSkillsDir] : [], + }); } private collectFilesFromPaths(paths: string[], resourceType: ResourceType): string[] { @@ -1421,24 +1281,6 @@ export class DefaultPackageManager implements PackageManager { return files; } - private getTargetMap( - accumulator: ResourceAccumulator, - resourceType: ResourceType, - ): Map { - switch (resourceType) { - case "extensions": - return accumulator.extensions; - case "skills": - return accumulator.skills; - case "prompts": - return accumulator.prompts; - case "themes": - return accumulator.themes; - default: - throw new Error(`Unknown resource type: ${String(resourceType)}`); - } - } - private addResource( map: Map, path: string, diff --git a/src/agents/sessions/resource-loader.test.ts b/src/agents/sessions/resource-loader.test.ts index 7dc2586b74e1..9b4f94c0599e 100644 --- a/src/agents/sessions/resource-loader.test.ts +++ b/src/agents/sessions/resource-loader.test.ts @@ -6,6 +6,7 @@ import { useAutoCleanupTempDirTracker } from "../../../test/helpers/temp-dir.js" import { clearExtensionCache } from "./extensions/loader.js"; import { DefaultPackageManager } from "./package-manager.js"; import { DefaultResourceLoader } from "./resource-loader.js"; +import { SettingsManager } from "./settings-manager.js"; const tempDirs = useAutoCleanupTempDirTracker(afterEach); @@ -41,6 +42,27 @@ afterEach(() => { }); describe("DefaultResourceLoader", () => { + it("does not load a direct local extension disabled by its package filter", async () => { + const root = tempDirs.make("openclaw-resource-loader-filter-"); + const extensionPath = join(root, "extension.ts"); + await writeFile(extensionPath, "export default function extension() {}\n"); + const loader = new DefaultResourceLoader({ + cwd: root, + agentDir: join(root, "agent"), + settingsManager: SettingsManager.inMemory({ + packages: [{ source: extensionPath, extensions: [] }], + }), + noSkills: true, + noPromptTemplates: true, + noThemes: true, + noContextFiles: true, + }); + + await loader.reload(); + + expect(loader.getExtensions().extensions).toEqual([]); + }); + it("skips ambient package resolution while preserving explicit resource paths", async () => { const root = tempDirs.make("openclaw-resource-loader-explicit-"); const promptDir = join(root, "explicit-prompts");