mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-08 07:41:08 +00:00
refactor: split lightweight plugin config policy
This commit is contained in:
committed by
Peter Steinberger
parent
4499d572fa
commit
c593ed0055
@@ -55,6 +55,7 @@ function createSinglePluginRegistry(params: {
|
||||
pluginRoot: string;
|
||||
skills: string[];
|
||||
format?: "openclaw" | "bundle";
|
||||
legacyPluginIds?: string[];
|
||||
}): PluginManifestRegistry {
|
||||
return {
|
||||
diagnostics: [],
|
||||
@@ -65,6 +66,7 @@ function createSinglePluginRegistry(params: {
|
||||
format: params.format,
|
||||
channels: [],
|
||||
providers: [],
|
||||
legacyPluginIds: params.legacyPluginIds,
|
||||
cliBackends: [],
|
||||
skills: params.skills,
|
||||
hooks: [],
|
||||
@@ -232,4 +234,31 @@ describe("resolvePluginSkillDirs", () => {
|
||||
path.resolve(pluginRoot, "commands"),
|
||||
]);
|
||||
});
|
||||
|
||||
it("resolves enabled plugin skills through legacy manifest aliases", async () => {
|
||||
const workspaceDir = await tempDirs.make("openclaw-");
|
||||
const pluginRoot = await tempDirs.make("openclaw-legacy-plugin-");
|
||||
await fs.mkdir(path.join(pluginRoot, "skills"), { recursive: true });
|
||||
|
||||
hoisted.loadPluginManifestRegistry.mockReturnValue(
|
||||
createSinglePluginRegistry({
|
||||
pluginRoot,
|
||||
skills: ["./skills"],
|
||||
legacyPluginIds: ["helper-legacy"],
|
||||
}),
|
||||
);
|
||||
|
||||
const dirs = resolvePluginSkillDirs({
|
||||
workspaceDir,
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
"helper-legacy": { enabled: true },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
});
|
||||
|
||||
expect(dirs).toEqual([path.resolve(pluginRoot, "skills")]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,16 +3,46 @@ import path from "node:path";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import {
|
||||
normalizePluginsConfig,
|
||||
normalizePluginsConfigWithResolver,
|
||||
resolveEffectivePluginActivationState,
|
||||
resolveMemorySlotDecision,
|
||||
} from "../../plugins/config-state.js";
|
||||
import { loadPluginManifestRegistry } from "../../plugins/manifest-registry.js";
|
||||
} from "../../plugins/config-policy.js";
|
||||
import {
|
||||
loadPluginManifestRegistry,
|
||||
type PluginManifestRegistry,
|
||||
} from "../../plugins/manifest-registry.js";
|
||||
import { hasKind } from "../../plugins/slots.js";
|
||||
import { isPathInsideWithRealpath } from "../../security/scan-paths.js";
|
||||
|
||||
const log = createSubsystemLogger("skills");
|
||||
|
||||
function buildRegistryPluginIdAliases(
|
||||
registry: PluginManifestRegistry,
|
||||
): Readonly<Record<string, string>> {
|
||||
return Object.fromEntries(
|
||||
registry.plugins
|
||||
.flatMap((record) => [
|
||||
...record.providers
|
||||
.filter((providerId) => providerId !== record.id)
|
||||
.map((providerId) => [providerId, record.id] as const),
|
||||
...(record.legacyPluginIds ?? []).map(
|
||||
(legacyPluginId) => [legacyPluginId, record.id] as const,
|
||||
),
|
||||
])
|
||||
.toSorted(([left], [right]) => left.localeCompare(right)),
|
||||
);
|
||||
}
|
||||
|
||||
function createRegistryPluginIdNormalizer(
|
||||
registry: PluginManifestRegistry,
|
||||
): (id: string) => string {
|
||||
const aliases = buildRegistryPluginIdAliases(registry);
|
||||
return (id: string) => {
|
||||
const trimmed = id.trim();
|
||||
return aliases[trimmed] ?? trimmed;
|
||||
};
|
||||
}
|
||||
|
||||
export function resolvePluginSkillDirs(params: {
|
||||
workspaceDir: string | undefined;
|
||||
config?: OpenClawConfig;
|
||||
@@ -28,7 +58,10 @@ export function resolvePluginSkillDirs(params: {
|
||||
if (registry.plugins.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const normalizedPlugins = normalizePluginsConfig(params.config?.plugins);
|
||||
const normalizedPlugins = normalizePluginsConfigWithResolver(
|
||||
params.config?.plugins,
|
||||
createRegistryPluginIdNormalizer(registry),
|
||||
);
|
||||
const acpEnabled = params.config?.acp?.enabled !== false;
|
||||
const memorySlot = normalizedPlugins.slots.memory;
|
||||
let selectedMemoryPluginId: string | null = null;
|
||||
|
||||
@@ -13,6 +13,10 @@ vi.mock("chokidar", () => ({
|
||||
default: { watch: watchMock },
|
||||
}));
|
||||
|
||||
vi.mock("./plugin-skills.js", () => ({
|
||||
resolvePluginSkillDirs: vi.fn(() => []),
|
||||
}));
|
||||
|
||||
describe("ensureSkillsWatcher", () => {
|
||||
beforeAll(async () => {
|
||||
refreshModule = await import("./refresh.js");
|
||||
|
||||
409
src/plugins/config-policy.ts
Normal file
409
src/plugins/config-policy.ts
Normal file
@@ -0,0 +1,409 @@
|
||||
import { normalizeChatChannelId } from "../channels/registry.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { defaultSlotIdForKey, hasKind } from "./slots.js";
|
||||
import type { PluginKind, PluginOrigin } from "./types.js";
|
||||
|
||||
export type PluginActivationSource = "disabled" | "explicit" | "auto" | "default";
|
||||
|
||||
export type PluginActivationState = {
|
||||
enabled: boolean;
|
||||
activated: boolean;
|
||||
explicitlyEnabled: boolean;
|
||||
source: PluginActivationSource;
|
||||
reason?: string;
|
||||
};
|
||||
|
||||
export type NormalizedPluginsConfig = {
|
||||
enabled: boolean;
|
||||
allow: string[];
|
||||
deny: string[];
|
||||
loadPaths: string[];
|
||||
slots: {
|
||||
memory?: string | null;
|
||||
};
|
||||
entries: Record<
|
||||
string,
|
||||
{
|
||||
enabled?: boolean;
|
||||
hooks?: {
|
||||
allowPromptInjection?: boolean;
|
||||
};
|
||||
subagent?: {
|
||||
allowModelOverride?: boolean;
|
||||
allowedModels?: string[];
|
||||
hasAllowedModelsConfig?: boolean;
|
||||
};
|
||||
config?: unknown;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
type NormalizePluginId = (id: string) => string;
|
||||
|
||||
const identityNormalizePluginId: NormalizePluginId = (id) => id.trim();
|
||||
|
||||
const normalizeList = (value: unknown, normalizePluginId: NormalizePluginId): string[] => {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
return value
|
||||
.map((entry) => (typeof entry === "string" ? normalizePluginId(entry) : ""))
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
const normalizeSlotValue = (value: unknown): string | null | undefined => {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
if (trimmed.toLowerCase() === "none") {
|
||||
return null;
|
||||
}
|
||||
return trimmed;
|
||||
};
|
||||
|
||||
const normalizePluginEntries = (
|
||||
entries: unknown,
|
||||
normalizePluginId: NormalizePluginId,
|
||||
): NormalizedPluginsConfig["entries"] => {
|
||||
if (!entries || typeof entries !== "object" || Array.isArray(entries)) {
|
||||
return {};
|
||||
}
|
||||
const normalized: NormalizedPluginsConfig["entries"] = {};
|
||||
for (const [key, value] of Object.entries(entries)) {
|
||||
const normalizedKey = normalizePluginId(key);
|
||||
if (!normalizedKey) {
|
||||
continue;
|
||||
}
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
normalized[normalizedKey] = {};
|
||||
continue;
|
||||
}
|
||||
const entry = value as Record<string, unknown>;
|
||||
const hooksRaw = entry.hooks;
|
||||
const hooks =
|
||||
hooksRaw && typeof hooksRaw === "object" && !Array.isArray(hooksRaw)
|
||||
? {
|
||||
allowPromptInjection: (hooksRaw as { allowPromptInjection?: unknown })
|
||||
.allowPromptInjection,
|
||||
}
|
||||
: undefined;
|
||||
const normalizedHooks =
|
||||
hooks && typeof hooks.allowPromptInjection === "boolean"
|
||||
? {
|
||||
allowPromptInjection: hooks.allowPromptInjection,
|
||||
}
|
||||
: undefined;
|
||||
const subagentRaw = entry.subagent;
|
||||
const subagent =
|
||||
subagentRaw && typeof subagentRaw === "object" && !Array.isArray(subagentRaw)
|
||||
? {
|
||||
allowModelOverride: (subagentRaw as { allowModelOverride?: unknown })
|
||||
.allowModelOverride,
|
||||
hasAllowedModelsConfig: Array.isArray(
|
||||
(subagentRaw as { allowedModels?: unknown }).allowedModels,
|
||||
),
|
||||
allowedModels: Array.isArray((subagentRaw as { allowedModels?: unknown }).allowedModels)
|
||||
? ((subagentRaw as { allowedModels?: unknown }).allowedModels as unknown[])
|
||||
.map((model) => (typeof model === "string" ? model.trim() : ""))
|
||||
.filter(Boolean)
|
||||
: undefined,
|
||||
}
|
||||
: undefined;
|
||||
const normalizedSubagent =
|
||||
subagent &&
|
||||
(typeof subagent.allowModelOverride === "boolean" ||
|
||||
subagent.hasAllowedModelsConfig ||
|
||||
(Array.isArray(subagent.allowedModels) && subagent.allowedModels.length > 0))
|
||||
? {
|
||||
...(typeof subagent.allowModelOverride === "boolean"
|
||||
? { allowModelOverride: subagent.allowModelOverride }
|
||||
: {}),
|
||||
...(subagent.hasAllowedModelsConfig ? { hasAllowedModelsConfig: true } : {}),
|
||||
...(Array.isArray(subagent.allowedModels) && subagent.allowedModels.length > 0
|
||||
? { allowedModels: subagent.allowedModels }
|
||||
: {}),
|
||||
}
|
||||
: undefined;
|
||||
normalized[normalizedKey] = {
|
||||
...normalized[normalizedKey],
|
||||
enabled:
|
||||
typeof entry.enabled === "boolean" ? entry.enabled : normalized[normalizedKey]?.enabled,
|
||||
hooks: normalizedHooks ?? normalized[normalizedKey]?.hooks,
|
||||
subagent: normalizedSubagent ?? normalized[normalizedKey]?.subagent,
|
||||
config: "config" in entry ? entry.config : normalized[normalizedKey]?.config,
|
||||
};
|
||||
}
|
||||
return normalized;
|
||||
};
|
||||
|
||||
export function normalizePluginsConfigWithResolver(
|
||||
config?: OpenClawConfig["plugins"],
|
||||
normalizePluginId: NormalizePluginId = identityNormalizePluginId,
|
||||
): NormalizedPluginsConfig {
|
||||
const memorySlot = normalizeSlotValue(config?.slots?.memory);
|
||||
return {
|
||||
enabled: config?.enabled !== false,
|
||||
allow: normalizeList(config?.allow, normalizePluginId),
|
||||
deny: normalizeList(config?.deny, normalizePluginId),
|
||||
loadPaths: normalizeList(config?.load?.paths, identityNormalizePluginId),
|
||||
slots: {
|
||||
memory: memorySlot === undefined ? defaultSlotIdForKey("memory") : memorySlot,
|
||||
},
|
||||
entries: normalizePluginEntries(config?.entries, normalizePluginId),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveExplicitPluginSelection(params: {
|
||||
id: string;
|
||||
origin: PluginOrigin;
|
||||
config: NormalizedPluginsConfig;
|
||||
rootConfig?: OpenClawConfig;
|
||||
}): { explicitlyEnabled: boolean; reason?: string } {
|
||||
if (params.config.entries[params.id]?.enabled === true) {
|
||||
return { explicitlyEnabled: true, reason: "enabled in config" };
|
||||
}
|
||||
if (
|
||||
params.origin === "bundled" &&
|
||||
isBundledChannelEnabledByChannelConfig(params.rootConfig, params.id)
|
||||
) {
|
||||
return { explicitlyEnabled: true, reason: "channel enabled in config" };
|
||||
}
|
||||
if (params.config.slots.memory === params.id) {
|
||||
return { explicitlyEnabled: true, reason: "selected memory slot" };
|
||||
}
|
||||
if (params.origin !== "bundled" && params.config.allow.includes(params.id)) {
|
||||
return { explicitlyEnabled: true, reason: "selected in allowlist" };
|
||||
}
|
||||
return { explicitlyEnabled: false };
|
||||
}
|
||||
|
||||
export function resolvePluginActivationState(params: {
|
||||
id: string;
|
||||
origin: PluginOrigin;
|
||||
config: NormalizedPluginsConfig;
|
||||
rootConfig?: OpenClawConfig;
|
||||
enabledByDefault?: boolean;
|
||||
sourceConfig?: NormalizedPluginsConfig;
|
||||
sourceRootConfig?: OpenClawConfig;
|
||||
autoEnabledReason?: string;
|
||||
}): PluginActivationState {
|
||||
const explicitSelection = resolveExplicitPluginSelection({
|
||||
id: params.id,
|
||||
origin: params.origin,
|
||||
config: params.sourceConfig ?? params.config,
|
||||
rootConfig: params.sourceRootConfig ?? params.rootConfig,
|
||||
});
|
||||
|
||||
if (!params.config.enabled) {
|
||||
return {
|
||||
enabled: false,
|
||||
activated: false,
|
||||
explicitlyEnabled: explicitSelection.explicitlyEnabled,
|
||||
source: "disabled",
|
||||
reason: "plugins disabled",
|
||||
};
|
||||
}
|
||||
if (params.config.deny.includes(params.id)) {
|
||||
return {
|
||||
enabled: false,
|
||||
activated: false,
|
||||
explicitlyEnabled: explicitSelection.explicitlyEnabled,
|
||||
source: "disabled",
|
||||
reason: "blocked by denylist",
|
||||
};
|
||||
}
|
||||
const entry = params.config.entries[params.id];
|
||||
if (entry?.enabled === false) {
|
||||
return {
|
||||
enabled: false,
|
||||
activated: false,
|
||||
explicitlyEnabled: explicitSelection.explicitlyEnabled,
|
||||
source: "disabled",
|
||||
reason: "disabled in config",
|
||||
};
|
||||
}
|
||||
const explicitlyAllowed = params.config.allow.includes(params.id);
|
||||
if (params.origin === "workspace" && !explicitlyAllowed && entry?.enabled !== true) {
|
||||
return {
|
||||
enabled: false,
|
||||
activated: false,
|
||||
explicitlyEnabled: explicitSelection.explicitlyEnabled,
|
||||
source: "disabled",
|
||||
reason: "workspace plugin (disabled by default)",
|
||||
};
|
||||
}
|
||||
if (params.config.slots.memory === params.id) {
|
||||
return {
|
||||
enabled: true,
|
||||
activated: true,
|
||||
explicitlyEnabled: true,
|
||||
source: "explicit",
|
||||
reason: "selected memory slot",
|
||||
};
|
||||
}
|
||||
if (params.config.allow.length > 0 && !explicitlyAllowed) {
|
||||
return {
|
||||
enabled: false,
|
||||
activated: false,
|
||||
explicitlyEnabled: explicitSelection.explicitlyEnabled,
|
||||
source: "disabled",
|
||||
reason: "not in allowlist",
|
||||
};
|
||||
}
|
||||
if (explicitSelection.explicitlyEnabled) {
|
||||
return {
|
||||
enabled: true,
|
||||
activated: true,
|
||||
explicitlyEnabled: true,
|
||||
source: "explicit",
|
||||
reason: explicitSelection.reason,
|
||||
};
|
||||
}
|
||||
if (params.autoEnabledReason) {
|
||||
return {
|
||||
enabled: true,
|
||||
activated: true,
|
||||
explicitlyEnabled: false,
|
||||
source: "auto",
|
||||
reason: params.autoEnabledReason,
|
||||
};
|
||||
}
|
||||
if (entry?.enabled === true) {
|
||||
return {
|
||||
enabled: true,
|
||||
activated: true,
|
||||
explicitlyEnabled: false,
|
||||
source: "auto",
|
||||
reason: "enabled by effective config",
|
||||
};
|
||||
}
|
||||
if (
|
||||
params.origin === "bundled" &&
|
||||
isBundledChannelEnabledByChannelConfig(params.rootConfig, params.id)
|
||||
) {
|
||||
return {
|
||||
enabled: true,
|
||||
activated: true,
|
||||
explicitlyEnabled: false,
|
||||
source: "auto",
|
||||
reason: "channel configured",
|
||||
};
|
||||
}
|
||||
if (params.origin === "bundled" && params.enabledByDefault === true) {
|
||||
return {
|
||||
enabled: true,
|
||||
activated: true,
|
||||
explicitlyEnabled: false,
|
||||
source: "default",
|
||||
reason: "bundled default enablement",
|
||||
};
|
||||
}
|
||||
if (params.origin === "bundled") {
|
||||
return {
|
||||
enabled: false,
|
||||
activated: false,
|
||||
explicitlyEnabled: false,
|
||||
source: "disabled",
|
||||
reason: "bundled (disabled by default)",
|
||||
};
|
||||
}
|
||||
return {
|
||||
enabled: true,
|
||||
activated: true,
|
||||
explicitlyEnabled: explicitSelection.explicitlyEnabled,
|
||||
source: "default",
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveEnableState(
|
||||
id: string,
|
||||
origin: PluginOrigin,
|
||||
config: NormalizedPluginsConfig,
|
||||
enabledByDefault?: boolean,
|
||||
): { enabled: boolean; reason?: string } {
|
||||
const state = resolvePluginActivationState({
|
||||
id,
|
||||
origin,
|
||||
config,
|
||||
enabledByDefault,
|
||||
});
|
||||
return state.enabled ? { enabled: true } : { enabled: false, reason: state.reason };
|
||||
}
|
||||
|
||||
export function isBundledChannelEnabledByChannelConfig(
|
||||
cfg: OpenClawConfig | undefined,
|
||||
pluginId: string,
|
||||
): boolean {
|
||||
if (!cfg) {
|
||||
return false;
|
||||
}
|
||||
const channelId = normalizeChatChannelId(pluginId);
|
||||
if (!channelId) {
|
||||
return false;
|
||||
}
|
||||
const channels = cfg.channels as Record<string, unknown> | undefined;
|
||||
const entry = channels?.[channelId];
|
||||
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
||||
return false;
|
||||
}
|
||||
return (entry as Record<string, unknown>).enabled === true;
|
||||
}
|
||||
|
||||
export function resolveEffectiveEnableState(params: {
|
||||
id: string;
|
||||
origin: PluginOrigin;
|
||||
config: NormalizedPluginsConfig;
|
||||
rootConfig?: OpenClawConfig;
|
||||
enabledByDefault?: boolean;
|
||||
}): { enabled: boolean; reason?: string } {
|
||||
const state = resolveEffectivePluginActivationState(params);
|
||||
return state.enabled ? { enabled: true } : { enabled: false, reason: state.reason };
|
||||
}
|
||||
|
||||
export function resolveEffectivePluginActivationState(params: {
|
||||
id: string;
|
||||
origin: PluginOrigin;
|
||||
config: NormalizedPluginsConfig;
|
||||
rootConfig?: OpenClawConfig;
|
||||
enabledByDefault?: boolean;
|
||||
sourceConfig?: NormalizedPluginsConfig;
|
||||
sourceRootConfig?: OpenClawConfig;
|
||||
autoEnabledReason?: string;
|
||||
}): PluginActivationState {
|
||||
return resolvePluginActivationState(params);
|
||||
}
|
||||
|
||||
export function resolveMemorySlotDecision(params: {
|
||||
id: string;
|
||||
kind?: PluginKind | PluginKind[];
|
||||
slot: string | null | undefined;
|
||||
selectedId: string | null;
|
||||
}): { enabled: boolean; reason?: string; selected?: boolean } {
|
||||
if (!hasKind(params.kind, "memory")) {
|
||||
return { enabled: true };
|
||||
}
|
||||
// A dual-kind plugin (e.g. ["memory", "context-engine"]) that lost the
|
||||
// memory slot must stay enabled so its other slot role can still load.
|
||||
const isMultiKind = Array.isArray(params.kind) && params.kind.length > 1;
|
||||
if (params.slot === null) {
|
||||
return isMultiKind ? { enabled: true } : { enabled: false, reason: "memory slot disabled" };
|
||||
}
|
||||
if (typeof params.slot === "string") {
|
||||
if (params.slot === params.id) {
|
||||
return { enabled: true, selected: true };
|
||||
}
|
||||
return isMultiKind
|
||||
? { enabled: true }
|
||||
: { enabled: false, reason: `memory slot set to "${params.slot}"` };
|
||||
}
|
||||
if (params.selectedId && params.selectedId !== params.id) {
|
||||
return isMultiKind
|
||||
? { enabled: true }
|
||||
: { enabled: false, reason: `memory slot already filled by "${params.selectedId}"` };
|
||||
}
|
||||
return { enabled: true, selected: true };
|
||||
}
|
||||
@@ -4,7 +4,10 @@ import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { resolveCompatibilityHostVersion } from "../version.js";
|
||||
import { loadBundleManifest } from "./bundle-manifest.js";
|
||||
import { normalizePluginsConfig, type NormalizedPluginsConfig } from "./config-state.js";
|
||||
import {
|
||||
normalizePluginsConfigWithResolver,
|
||||
type NormalizedPluginsConfig,
|
||||
} from "./config-policy.js";
|
||||
import { discoverOpenClawPlugins, type PluginCandidate } from "./discovery.js";
|
||||
import {
|
||||
loadPluginManifest,
|
||||
@@ -46,6 +49,7 @@ export type PluginManifestRecord = {
|
||||
version?: string;
|
||||
enabledByDefault?: boolean;
|
||||
autoEnableWhenConfiguredProviders?: string[];
|
||||
legacyPluginIds?: string[];
|
||||
format?: PluginFormat;
|
||||
bundleFormat?: PluginBundleFormat;
|
||||
bundleCapabilities?: string[];
|
||||
@@ -206,6 +210,7 @@ function buildRecord(params: {
|
||||
version: normalizeManifestLabel(params.manifest.version) ?? params.candidate.packageVersion,
|
||||
enabledByDefault: params.manifest.enabledByDefault === true ? true : undefined,
|
||||
autoEnableWhenConfiguredProviders: params.manifest.autoEnableWhenConfiguredProviders,
|
||||
legacyPluginIds: params.manifest.legacyPluginIds,
|
||||
format: params.candidate.format ?? "openclaw",
|
||||
bundleFormat: params.candidate.bundleFormat,
|
||||
kind: params.manifest.kind,
|
||||
@@ -356,7 +361,7 @@ export function loadPluginManifestRegistry(
|
||||
} = {},
|
||||
): PluginManifestRegistry {
|
||||
const config = params.config ?? {};
|
||||
const normalized = normalizePluginsConfig(config.plugins);
|
||||
const normalized = normalizePluginsConfigWithResolver(config.plugins);
|
||||
const env = params.env ?? process.env;
|
||||
const cacheKey = buildCacheKey({ workspaceDir: params.workspaceDir, plugins: normalized, env });
|
||||
const cacheEnabled = params.cache !== false && shouldUseManifestCache(env);
|
||||
|
||||
Reference in New Issue
Block a user