fix(plugins): move acpx config contracts into manifests

This commit is contained in:
Vincent Koc
2026-04-06 12:33:11 +01:00
parent 045d956111
commit e611761809
9 changed files with 641 additions and 47 deletions

View File

@@ -0,0 +1,149 @@
import type { OpenClawConfig } from "../config/config.js";
import { isRecord } from "../utils.js";
import { findBundledPluginMetadataById } from "./bundled-plugin-metadata.js";
import { loadPluginManifestRegistry } from "./manifest-registry.js";
import type { PluginManifestConfigContracts } from "./manifest.js";
import type { PluginOrigin } from "./types.js";
export type PluginConfigContractMatch = {
path: string;
value: unknown;
};
export type PluginConfigContractMetadata = {
origin: PluginOrigin;
configContracts: PluginManifestConfigContracts;
};
type TraversalState = {
segments: string[];
value: unknown;
};
function normalizePathPattern(pathPattern: string): string[] {
return pathPattern
.split(".")
.map((segment) => segment.trim())
.filter(Boolean);
}
function appendPathSegment(path: string, segment: string): string {
if (!path) {
return segment;
}
return /^\d+$/.test(segment) ? `${path}[${segment}]` : `${path}.${segment}`;
}
export function collectPluginConfigContractMatches(params: {
root: unknown;
pathPattern: string;
}): PluginConfigContractMatch[] {
const pattern = normalizePathPattern(params.pathPattern);
if (pattern.length === 0) {
return [];
}
let states: TraversalState[] = [{ segments: [], value: params.root }];
for (const segment of pattern) {
const nextStates: TraversalState[] = [];
for (const state of states) {
if (segment === "*") {
if (Array.isArray(state.value)) {
for (const [index, value] of state.value.entries()) {
nextStates.push({
segments: [...state.segments, String(index)],
value,
});
}
continue;
}
if (isRecord(state.value)) {
for (const [key, value] of Object.entries(state.value)) {
nextStates.push({
segments: [...state.segments, key],
value,
});
}
}
continue;
}
if (Array.isArray(state.value)) {
const index = Number.parseInt(segment, 10);
if (Number.isInteger(index) && index >= 0 && index < state.value.length) {
nextStates.push({
segments: [...state.segments, segment],
value: state.value[index],
});
}
continue;
}
if (!isRecord(state.value) || !Object.prototype.hasOwnProperty.call(state.value, segment)) {
continue;
}
nextStates.push({
segments: [...state.segments, segment],
value: state.value[segment],
});
}
states = nextStates;
if (states.length === 0) {
break;
}
}
return states.map((state) => ({
path: state.segments.reduce(appendPathSegment, ""),
value: state.value,
}));
}
export function resolvePluginConfigContractsById(params: {
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
cache?: boolean;
pluginIds: readonly string[];
}): ReadonlyMap<string, PluginConfigContractMetadata> {
const matches = new Map<string, PluginConfigContractMetadata>();
const pluginIds = [
...new Set(params.pluginIds.map((pluginId) => pluginId.trim()).filter(Boolean)),
];
if (pluginIds.length === 0) {
return matches;
}
const registry = loadPluginManifestRegistry({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
cache: params.cache,
});
for (const plugin of registry.plugins) {
if (!pluginIds.includes(plugin.id)) {
continue;
}
if (!plugin.configContracts) {
continue;
}
matches.set(plugin.id, {
origin: plugin.origin,
configContracts: plugin.configContracts,
});
}
for (const pluginId of pluginIds) {
if (matches.has(pluginId)) {
continue;
}
const bundled = findBundledPluginMetadataById(pluginId);
if (!bundled?.manifest.configContracts) {
continue;
}
matches.set(pluginId, {
origin: "bundled",
configContracts: bundled.manifest.configContracts,
});
}
return matches;
}

View File

@@ -500,6 +500,35 @@ describe("loadPluginManifestRegistry", () => {
}),
);
});
it("preserves manifest-owned config contracts from plugin manifests", () => {
const dir = makeTempDir();
writeManifest(dir, {
id: "acpx",
configSchema: { type: "object" },
configContracts: {
dangerousFlags: [{ path: "permissionMode", equals: "approve-all" }],
secretInputs: {
bundledDefaultEnabled: false,
paths: [{ path: "mcpServers.*.env.*", expected: "string" }],
},
},
});
const registry = loadSingleCandidateRegistry({
idHint: "acpx",
rootDir: dir,
origin: "bundled",
});
expect(registry.plugins[0]?.configContracts).toEqual({
dangerousFlags: [{ path: "permissionMode", equals: "approve-all" }],
secretInputs: {
bundledDefaultEnabled: false,
paths: [{ path: "mcpServers.*.env.*", expected: "string" }],
},
});
});
it("does not promote legacy top-level capability fields into contracts", () => {
const dir = makeTempDir();
writeManifest(dir, {

View File

@@ -12,6 +12,7 @@ import { discoverOpenClawPlugins, type PluginCandidate } from "./discovery.js";
import {
loadPluginManifest,
type OpenClawPackageManifest,
type PluginManifestConfigContracts,
type PluginManifest,
type PluginManifestChannelConfig,
type PluginManifestContracts,
@@ -86,6 +87,7 @@ export type PluginManifestRecord = {
configSchema?: Record<string, unknown>;
configUiHints?: Record<string, PluginConfigUiHint>;
contracts?: PluginManifestContracts;
configContracts?: PluginManifestConfigContracts;
channelConfigs?: Record<string, PluginManifestChannelConfig>;
channelCatalogMeta?: {
id: string;
@@ -305,6 +307,7 @@ function buildRecord(params: {
configSchema: params.configSchema,
configUiHints: params.manifest.uiHints,
contracts: params.manifest.contracts,
configContracts: params.manifest.configContracts,
channelConfigs,
...(params.candidate.packageManifest?.channel?.id
? {
@@ -360,6 +363,7 @@ function buildBundleRecord(params: {
schemaCacheKey: undefined,
configSchema: undefined,
configUiHints: undefined,
configContracts: undefined,
channelConfigs: undefined,
};
}

View File

@@ -32,6 +32,43 @@ export type PluginManifestModelSupport = {
modelPatterns?: string[];
};
export type PluginManifestConfigLiteral = string | number | boolean | null;
export type PluginManifestDangerousConfigFlag = {
/**
* Dot-separated config path relative to `plugins.entries.<id>.config`.
* Supports `*` wildcards for map/array segments.
*/
path: string;
/** Exact literal that marks this config value as dangerous. */
equals: PluginManifestConfigLiteral;
};
export type PluginManifestSecretInputPath = {
/**
* Dot-separated config path relative to `plugins.entries.<id>.config`.
* Supports `*` wildcards for map/array segments.
*/
path: string;
/** Expected resolved type for SecretRef materialization. */
expected?: "string";
};
export type PluginManifestSecretInputContracts = {
/**
* Override bundled-plugin default enablement when deciding whether this
* SecretRef surface is active. Use this when the plugin is bundled but the
* surface should stay inactive until explicitly enabled in config.
*/
bundledDefaultEnabled?: boolean;
paths: PluginManifestSecretInputPath[];
};
export type PluginManifestConfigContracts = {
dangerousFlags?: PluginManifestDangerousConfigFlag[];
secretInputs?: PluginManifestSecretInputContracts;
};
export type PluginManifest = {
id: string;
configSchema: Record<string, unknown>;
@@ -65,6 +102,8 @@ export type PluginManifest = {
* compat wiring, and contract coverage without importing plugin runtime.
*/
contracts?: PluginManifestContracts;
/** Manifest-owned config behavior consumed by generic core helpers. */
configContracts?: PluginManifestConfigContracts;
channelConfigs?: Record<string, PluginManifestChannelConfig>;
};
@@ -179,6 +218,88 @@ function normalizeManifestContracts(value: unknown): PluginManifestContracts | u
return Object.keys(contracts).length > 0 ? contracts : undefined;
}
function isManifestConfigLiteral(value: unknown): value is PluginManifestConfigLiteral {
return (
value === null ||
typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean"
);
}
function normalizeManifestDangerousConfigFlags(
value: unknown,
): PluginManifestDangerousConfigFlag[] | undefined {
if (!Array.isArray(value)) {
return undefined;
}
const normalized: PluginManifestDangerousConfigFlag[] = [];
for (const entry of value) {
if (!isRecord(entry)) {
continue;
}
const path = typeof entry.path === "string" ? entry.path.trim() : "";
if (!path || !isManifestConfigLiteral(entry.equals)) {
continue;
}
normalized.push({ path, equals: entry.equals });
}
return normalized.length > 0 ? normalized : undefined;
}
function normalizeManifestSecretInputPaths(
value: unknown,
): PluginManifestSecretInputPath[] | undefined {
if (!Array.isArray(value)) {
return undefined;
}
const normalized: PluginManifestSecretInputPath[] = [];
for (const entry of value) {
if (!isRecord(entry)) {
continue;
}
const path = typeof entry.path === "string" ? entry.path.trim() : "";
if (!path) {
continue;
}
const expected = entry.expected === "string" ? entry.expected : undefined;
normalized.push({
path,
...(expected ? { expected } : {}),
});
}
return normalized.length > 0 ? normalized : undefined;
}
function normalizeManifestConfigContracts(
value: unknown,
): PluginManifestConfigContracts | undefined {
if (!isRecord(value)) {
return undefined;
}
const rawSecretInputs = isRecord(value.secretInputs) ? value.secretInputs : undefined;
const dangerousFlags = normalizeManifestDangerousConfigFlags(value.dangerousFlags);
const secretInputPaths = rawSecretInputs
? normalizeManifestSecretInputPaths(rawSecretInputs.paths)
: undefined;
const secretInputs =
secretInputPaths && secretInputPaths.length > 0
? ({
...(rawSecretInputs?.bundledDefaultEnabled === true
? { bundledDefaultEnabled: true }
: rawSecretInputs?.bundledDefaultEnabled === false
? { bundledDefaultEnabled: false }
: {}),
paths: secretInputPaths,
} satisfies PluginManifestSecretInputContracts)
: undefined;
const configContracts = {
...(dangerousFlags ? { dangerousFlags } : {}),
...(secretInputs ? { secretInputs } : {}),
} satisfies PluginManifestConfigContracts;
return Object.keys(configContracts).length > 0 ? configContracts : undefined;
}
function normalizeManifestModelSupport(value: unknown): PluginManifestModelSupport | undefined {
if (!isRecord(value)) {
return undefined;
@@ -379,6 +500,7 @@ export function loadPluginManifest(
const providerAuthChoices = normalizeProviderAuthChoices(raw.providerAuthChoices);
const skills = normalizeStringList(raw.skills);
const contracts = normalizeManifestContracts(raw.contracts);
const configContracts = normalizeManifestConfigContracts(raw.configContracts);
const channelConfigs = normalizeChannelConfigs(raw.channelConfigs);
let uiHints: Record<string, PluginConfigUiHint> | undefined;
@@ -408,6 +530,7 @@ export function loadPluginManifest(
version,
uiHints,
contracts,
configContracts,
channelConfigs,
},
manifestPath,

View File

@@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import type { PluginOrigin } from "../plugins/types.js";
import { collectPluginConfigAssignments } from "./runtime-config-collectors-plugins.js";
@@ -8,6 +8,14 @@ import {
type SecretDefaults,
} from "./runtime-shared.js";
const { loadPluginManifestRegistryMock } = vi.hoisted(() => ({
loadPluginManifestRegistryMock: vi.fn(),
}));
vi.mock("../plugins/manifest-registry.js", () => ({
loadPluginManifestRegistry: loadPluginManifestRegistryMock,
}));
function asConfig(value: unknown): OpenClawConfig {
return value as OpenClawConfig;
}
@@ -28,6 +36,27 @@ function loadablePluginOrigins(entries: Array<[string, PluginOrigin]>) {
}
describe("collectPluginConfigAssignments", () => {
beforeEach(() => {
loadPluginManifestRegistryMock.mockReset();
loadPluginManifestRegistryMock.mockReturnValue({
plugins: [
{
id: "acpx",
origin: "bundled",
providers: [],
legacyPluginIds: [],
configContracts: {
secretInputs: {
bundledDefaultEnabled: false,
paths: [{ path: "mcpServers.*.env.*", expected: "string" }],
},
},
},
],
diagnostics: [],
});
});
it("collects SecretRef assignments from active acpx MCP server env vars", () => {
const config = asConfig({
plugins: {
@@ -250,7 +279,7 @@ describe("collectPluginConfigAssignments", () => {
);
});
it("treats bundled acpx as active by default", () => {
it("treats bundled acpx SecretRef surfaces as inactive until enabled", () => {
const config = asConfig({
plugins: {
enabled: true,
@@ -274,11 +303,9 @@ describe("collectPluginConfigAssignments", () => {
loadablePluginOrigins: loadablePluginOrigins([["acpx", "bundled"]]),
});
expect(context.assignments).toHaveLength(1);
expect(context.assignments[0]?.path).toBe("plugins.entries.acpx.config.mcpServers.s1.env.K");
expect(context.assignments[0]?.ref.id).toBe("K");
expect(context.assignments).toHaveLength(0);
expect(context.warnings.some((w) => w.code === "SECRETS_REF_IGNORED_INACTIVE_SURFACE")).toBe(
false,
true,
);
});
@@ -501,4 +528,53 @@ describe("collectPluginConfigAssignments", () => {
expect(context.assignments).toHaveLength(0);
});
it("collects manifest-declared SecretRef surfaces for non-acpx plugins", () => {
loadPluginManifestRegistryMock.mockReturnValue({
plugins: [
{
id: "other",
origin: "config",
providers: [],
legacyPluginIds: [],
configContracts: {
secretInputs: {
paths: [{ path: "service.tokens.*", expected: "string" }],
},
},
},
],
diagnostics: [],
});
const config = asConfig({
plugins: {
entries: {
other: {
enabled: true,
config: {
service: {
tokens: {
primary: envRef("PRIMARY_TOKEN"),
secondary: "${SECONDARY_TOKEN}",
},
},
},
},
},
},
});
const context = makeContext(config);
collectPluginConfigAssignments({
config,
defaults: undefined,
context,
loadablePluginOrigins: loadablePluginOrigins([["other", "config"]]),
});
expect(context.assignments.map((assignment) => assignment.path).toSorted()).toEqual([
"plugins.entries.other.config.service.tokens.primary",
"plugins.entries.other.config.service.tokens.secondary",
]);
});
});

View File

@@ -1,4 +1,9 @@
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
import type { OpenClawConfig } from "../config/config.js";
import {
collectPluginConfigContractMatches,
resolvePluginConfigContractsById,
} from "../plugins/config-contracts.js";
import { normalizePluginsConfig, resolveEnableState } from "../plugins/config-state.js";
import type { PluginOrigin } from "../plugins/types.js";
import {
@@ -8,18 +13,11 @@ import {
} from "./runtime-shared.js";
import { isRecord } from "./shared.js";
const ACPX_PLUGIN_ID = "acpx";
const ACPX_ENABLED_BY_DEFAULT = false;
/**
* Walk plugin config entries and collect SecretRef assignments for MCP server
* env vars. Without this, SecretRefs in paths like
* `plugins.entries.acpx.config.mcpServers.*.env.*` are never resolved and
* remain as raw objects at runtime.
*
* This surface is intentionally scoped to ACPX. Third-party plugins may define
* their own `mcpServers`-shaped config, but that is not a documented SecretRef
* surface and should not be rewritten here.
* Walk manifest-declared plugin config SecretRef surfaces and collect
* assignments for runtime materialization. Plugin-owned metadata controls which
* config paths support SecretRefs and whether bundled plugins stay inactive on
* that surface until explicitly enabled.
*
* When `loadablePluginOrigins` is provided, entries whose ID is not in the map
* are treated as inactive (stale config entries for plugins that are no longer
@@ -38,9 +36,40 @@ export function collectPluginConfigAssignments(params: {
}
const normalizedConfig = normalizePluginsConfig(params.config.plugins);
const workspaceDir = resolveAgentWorkspaceDir(
params.config,
resolveDefaultAgentId(params.config),
);
const pluginSecretInputs = new Map(
[
...resolvePluginConfigContractsById({
config: params.config,
workspaceDir,
env: params.context.env,
cache: true,
pluginIds: Object.keys(entries),
}).entries(),
].flatMap(([pluginId, metadata]) => {
const secretInputs = metadata.configContracts.secretInputs;
if (!secretInputs?.paths.length) {
return [];
}
return [
[
pluginId,
{
origin: metadata.origin,
bundledDefaultEnabled: secretInputs.bundledDefaultEnabled,
paths: secretInputs.paths,
},
] as const,
];
}),
);
for (const [pluginId, entry] of Object.entries(entries)) {
if (pluginId !== ACPX_PLUGIN_ID) {
const secretInputs = pluginSecretInputs.get(pluginId);
if (!secretInputs) {
continue;
}
if (!isRecord(entry)) {
@@ -53,9 +82,10 @@ export function collectPluginConfigAssignments(params: {
const pluginOrigin = params.loadablePluginOrigins?.get(pluginId);
if (params.loadablePluginOrigins && !pluginOrigin) {
collectMcpServerEnvAssignments({
collectConfiguredPluginSecretAssignments({
pluginId,
pluginConfig,
secretPaths: secretInputs.paths,
active: false,
inactiveReason: "plugin is not loadable (stale config entry).",
defaults: params.defaults,
@@ -64,17 +94,17 @@ export function collectPluginConfigAssignments(params: {
continue;
}
const resolvedOrigin = pluginOrigin ?? secretInputs.origin;
const enableState = resolveEnableState(
pluginId,
pluginOrigin ?? "config",
resolvedOrigin,
normalizedConfig,
pluginId === ACPX_PLUGIN_ID && pluginOrigin === "bundled"
? ACPX_ENABLED_BY_DEFAULT
: undefined,
resolvedOrigin === "bundled" ? secretInputs.bundledDefaultEnabled : undefined,
);
collectMcpServerEnvAssignments({
collectConfiguredPluginSecretAssignments({
pluginId,
pluginConfig,
secretPaths: secretInputs.paths,
active: enableState.enabled,
inactiveReason: enableState.reason ?? "plugin is disabled.",
defaults: params.defaults,
@@ -83,44 +113,79 @@ export function collectPluginConfigAssignments(params: {
}
}
function collectMcpServerEnvAssignments(params: {
function collectConfiguredPluginSecretAssignments(params: {
pluginId: string;
pluginConfig: Record<string, unknown>;
secretPaths: ReadonlyArray<{ path: string; expected?: "string" }>;
active: boolean;
inactiveReason: string;
defaults: SecretDefaults | undefined;
context: ResolverContext;
}): void {
const mcpServers = params.pluginConfig.mcpServers;
if (!isRecord(mcpServers)) {
return;
}
const seenPaths = new Set<string>();
for (const secretPath of params.secretPaths) {
for (const match of collectPluginConfigContractMatches({
root: params.pluginConfig,
pathPattern: secretPath.path,
})) {
const fullPath = `plugins.entries.${params.pluginId}.config.${match.path}`;
if (seenPaths.has(fullPath)) {
continue;
}
seenPaths.add(fullPath);
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
if (!isRecord(serverConfig)) {
continue;
}
const env = serverConfig.env;
if (!isRecord(env)) {
continue;
}
for (const [envKey, envValue] of Object.entries(env)) {
// SecretInput allows both explicit objects and inline env-template refs
// like `${MCP_API_KEY}`. Non-ref strings remain untouched because
// collectSecretInputAssignment ignores them.
collectSecretInputAssignment({
value: envValue,
path: `plugins.entries.${params.pluginId}.config.mcpServers.${serverName}.env.${envKey}`,
expected: "string",
value: match.value,
path: fullPath,
expected: secretPath.expected ?? "string",
defaults: params.defaults,
context: params.context,
active: params.active,
inactiveReason: `plugin "${params.pluginId}": ${params.inactiveReason}`,
apply: (value) => {
env[envKey] = value;
},
apply: createPluginConfigAssignmentApply(params.pluginConfig, match.path),
});
}
}
}
function createPluginConfigAssignmentApply(
pluginConfig: Record<string, unknown>,
relativePath: string,
): (value: unknown) => void {
return (value) => {
const segments = relativePath
.replace(/\[(\d+)\]/g, ".$1")
.split(".")
.map((segment) => segment.trim())
.filter(Boolean);
if (segments.length === 0) {
return;
}
let current: unknown = pluginConfig;
for (const segment of segments.slice(0, -1)) {
if (Array.isArray(current)) {
const index = Number.parseInt(segment, 10);
current = Number.isInteger(index) ? current[index] : undefined;
continue;
}
current = isRecord(current) ? current[segment] : undefined;
}
const finalSegment = segments.at(-1);
if (!finalSegment) {
return;
}
if (Array.isArray(current)) {
const index = Number.parseInt(finalSegment, 10);
if (Number.isInteger(index) && index >= 0 && index < current.length) {
current[index] = value;
}
return;
}
if (isRecord(current)) {
current[finalSegment] = value;
}
};
}

View File

@@ -0,0 +1,81 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { collectEnabledInsecureOrDangerousFlags } from "./dangerous-config-flags.js";
const { loadPluginManifestRegistryMock } = vi.hoisted(() => ({
loadPluginManifestRegistryMock: vi.fn(),
}));
vi.mock("../plugins/manifest-registry.js", () => ({
loadPluginManifestRegistry: loadPluginManifestRegistryMock,
}));
function asConfig(value: unknown): OpenClawConfig {
return value as OpenClawConfig;
}
describe("collectEnabledInsecureOrDangerousFlags", () => {
beforeEach(() => {
loadPluginManifestRegistryMock.mockReset();
});
it("collects manifest-declared dangerous plugin config values", () => {
loadPluginManifestRegistryMock.mockReturnValue({
plugins: [
{
id: "acpx",
configContracts: {
dangerousFlags: [{ path: "permissionMode", equals: "approve-all" }],
},
},
],
diagnostics: [],
});
expect(
collectEnabledInsecureOrDangerousFlags(
asConfig({
plugins: {
entries: {
acpx: {
config: {
permissionMode: "approve-all",
},
},
},
},
}),
),
).toContain("plugins.entries.acpx.config.permissionMode=approve-all");
});
it("ignores plugin config values that are not declared as dangerous", () => {
loadPluginManifestRegistryMock.mockReturnValue({
plugins: [
{
id: "other",
configContracts: {
dangerousFlags: [{ path: "mode", equals: "danger" }],
},
},
],
diagnostics: [],
});
expect(
collectEnabledInsecureOrDangerousFlags(
asConfig({
plugins: {
entries: {
other: {
config: {
mode: "safe",
},
},
},
},
}),
),
).toEqual([]);
});
});

View File

@@ -1,4 +1,14 @@
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
import type { OpenClawConfig } from "../config/config.js";
import {
collectPluginConfigContractMatches,
resolvePluginConfigContractsById,
} from "../plugins/config-contracts.js";
import { isRecord } from "../utils.js";
function formatDangerousConfigFlagValue(value: string | number | boolean | null): string {
return value === null ? "null" : String(value);
}
export function collectEnabledInsecureOrDangerousFlags(cfg: OpenClawConfig): string[] {
const enabledFlags: string[] = [];
@@ -24,8 +34,48 @@ export function collectEnabledInsecureOrDangerousFlags(cfg: OpenClawConfig): str
if (cfg.tools?.exec?.applyPatch?.workspaceOnly === false) {
enabledFlags.push("tools.exec.applyPatch.workspaceOnly=false");
}
if (cfg.plugins?.entries?.acpx?.config?.permissionMode === "approve-all") {
enabledFlags.push("plugins.entries.acpx.config.permissionMode=approve-all");
const pluginEntries = cfg.plugins?.entries;
if (!isRecord(pluginEntries)) {
return enabledFlags;
}
const configContracts = resolvePluginConfigContractsById({
config: cfg,
workspaceDir: resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg)),
env: process.env,
cache: true,
pluginIds: Object.keys(pluginEntries),
});
const seenFlags = new Set<string>();
for (const [pluginId, metadata] of configContracts.entries()) {
const dangerousFlags = metadata.configContracts.dangerousFlags;
if (!dangerousFlags?.length) {
continue;
}
const pluginEntry = pluginEntries[pluginId];
if (!isRecord(pluginEntry) || !isRecord(pluginEntry.config)) {
continue;
}
for (const flag of dangerousFlags) {
for (const match of collectPluginConfigContractMatches({
root: pluginEntry.config,
pathPattern: flag.path,
})) {
if (!Object.is(match.value, flag.equals)) {
continue;
}
const rendered =
`plugins.entries.${pluginId}.config.${match.path}` +
`=${formatDangerousConfigFlagValue(flag.equals)}`;
if (seenFlags.has(rendered)) {
continue;
}
seenFlags.add(rendered);
enabledFlags.push(rendered);
}
}
}
return enabledFlags;
}