fix: enforce plugin config schemas (#1272) (thanks @thewilloftheshadow)
Co-authored-by: thewilloftheshadow <thewilloftheshadow@users.noreply.github.com>
This commit is contained in:
committed by
Peter Steinberger
parent
48f733e4b3
commit
2f6d5805de
@@ -12,7 +12,7 @@ describe("config backup rotation", () => {
|
||||
const configPath = resolveConfigPath();
|
||||
const buildConfig = (version: number): ClawdbotConfig =>
|
||||
({
|
||||
identity: { name: `v${version}` },
|
||||
agents: { list: [{ id: `v${version}` }] },
|
||||
}) as ClawdbotConfig;
|
||||
|
||||
for (let version = 0; version <= 6; version += 1) {
|
||||
@@ -21,7 +21,10 @@ describe("config backup rotation", () => {
|
||||
|
||||
const readName = async (suffix = "") => {
|
||||
const raw = await fs.readFile(`${configPath}${suffix}`, "utf-8");
|
||||
return (JSON.parse(raw) as { identity?: { name?: string } }).identity?.name ?? null;
|
||||
return (
|
||||
(JSON.parse(raw) as { agents?: { list?: Array<{ id?: string }> } }).agents?.list?.[0]
|
||||
?.id ?? null
|
||||
);
|
||||
};
|
||||
|
||||
await expect(readName()).resolves.toBe("v6");
|
||||
|
||||
@@ -96,6 +96,25 @@ describe("Nix integration (U3, U5, U9)", () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configDir = path.join(home, ".clawdbot");
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
const pluginDir = path.join(home, "plugins", "demo-plugin");
|
||||
await fs.mkdir(pluginDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(pluginDir, "index.js"),
|
||||
'export default { id: "demo-plugin", register() {} };',
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(pluginDir, "clawdbot.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: "demo-plugin",
|
||||
configSchema: { type: "object", additionalProperties: false, properties: {} },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(configDir, "clawdbot.json"),
|
||||
JSON.stringify(
|
||||
|
||||
152
src/config/config.plugin-validation.test.ts
Normal file
152
src/config/config.plugin-validation.test.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { withTempHome } from "./test-helpers.js";
|
||||
|
||||
async function writePluginFixture(params: {
|
||||
dir: string;
|
||||
id: string;
|
||||
schema: Record<string, unknown>;
|
||||
}) {
|
||||
await fs.mkdir(params.dir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(params.dir, "index.js"),
|
||||
`export default { id: "${params.id}", register() {} };`,
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(params.dir, "clawdbot.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: params.id,
|
||||
configSchema: params.schema,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
}
|
||||
|
||||
describe("config plugin validation", () => {
|
||||
it("rejects missing plugin load paths", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
process.env.CLAWDBOT_STATE_DIR = path.join(home, ".clawdbot");
|
||||
vi.resetModules();
|
||||
const { validateConfigObjectWithPlugins } = await import("./config.js");
|
||||
const missingPath = path.join(home, "missing-plugin");
|
||||
const res = validateConfigObjectWithPlugins({
|
||||
agents: { list: [{ id: "pi" }] },
|
||||
plugins: { enabled: false, load: { paths: [missingPath] } },
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
if (!res.ok) {
|
||||
const hasIssue = res.issues.some(
|
||||
(issue) =>
|
||||
issue.path === "plugins.load.paths" && issue.message.includes("plugin path not found"),
|
||||
);
|
||||
expect(hasIssue).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects missing plugin ids in entries", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
process.env.CLAWDBOT_STATE_DIR = path.join(home, ".clawdbot");
|
||||
vi.resetModules();
|
||||
const { validateConfigObjectWithPlugins } = await import("./config.js");
|
||||
const res = validateConfigObjectWithPlugins({
|
||||
agents: { list: [{ id: "pi" }] },
|
||||
plugins: { enabled: false, entries: { "missing-plugin": { enabled: true } } },
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
if (!res.ok) {
|
||||
expect(res.issues).toContainEqual({
|
||||
path: "plugins.entries.missing-plugin",
|
||||
message: "plugin not found: missing-plugin",
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects missing plugin ids in allow/deny/slots", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
process.env.CLAWDBOT_STATE_DIR = path.join(home, ".clawdbot");
|
||||
vi.resetModules();
|
||||
const { validateConfigObjectWithPlugins } = await import("./config.js");
|
||||
const res = validateConfigObjectWithPlugins({
|
||||
agents: { list: [{ id: "pi" }] },
|
||||
plugins: {
|
||||
enabled: false,
|
||||
allow: ["missing-allow"],
|
||||
deny: ["missing-deny"],
|
||||
slots: { memory: "missing-slot" },
|
||||
},
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
if (!res.ok) {
|
||||
expect(res.issues).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ path: "plugins.allow", message: "plugin not found: missing-allow" },
|
||||
{ path: "plugins.deny", message: "plugin not found: missing-deny" },
|
||||
{ path: "plugins.slots.memory", message: "plugin not found: missing-slot" },
|
||||
]),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("surfaces plugin config diagnostics", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
process.env.CLAWDBOT_STATE_DIR = path.join(home, ".clawdbot");
|
||||
const pluginDir = path.join(home, "bad-plugin");
|
||||
await writePluginFixture({
|
||||
dir: pluginDir,
|
||||
id: "bad-plugin",
|
||||
schema: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
value: { type: "boolean" },
|
||||
},
|
||||
required: ["value"],
|
||||
},
|
||||
});
|
||||
|
||||
vi.resetModules();
|
||||
const { validateConfigObjectWithPlugins } = await import("./config.js");
|
||||
const res = validateConfigObjectWithPlugins({
|
||||
agents: { list: [{ id: "pi" }] },
|
||||
plugins: {
|
||||
enabled: true,
|
||||
load: { paths: [pluginDir] },
|
||||
entries: { "bad-plugin": { config: { value: "nope" } } },
|
||||
},
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
if (!res.ok) {
|
||||
const hasIssue = res.issues.some(
|
||||
(issue) =>
|
||||
issue.path === "plugins.entries.bad-plugin.config" &&
|
||||
issue.message.includes("invalid config"),
|
||||
);
|
||||
expect(hasIssue).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts known plugin ids", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
process.env.CLAWDBOT_STATE_DIR = path.join(home, ".clawdbot");
|
||||
vi.resetModules();
|
||||
const { validateConfigObjectWithPlugins } = await import("./config.js");
|
||||
const res = validateConfigObjectWithPlugins({
|
||||
agents: { list: [{ id: "pi" }] },
|
||||
plugins: { enabled: false, entries: { discord: { enabled: true } } },
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -10,5 +10,5 @@ export { migrateLegacyConfig } from "./legacy-migrate.js";
|
||||
export * from "./paths.js";
|
||||
export * from "./runtime-overrides.js";
|
||||
export * from "./types.js";
|
||||
export { validateConfigObject } from "./validation.js";
|
||||
export { validateConfigObject, validateConfigObjectWithPlugins } from "./validation.js";
|
||||
export { ClawdbotSchema } from "./zod-schema.js";
|
||||
|
||||
@@ -30,8 +30,7 @@ import { normalizeConfigPaths } from "./normalize-paths.js";
|
||||
import { resolveConfigPath, resolveStateDir } from "./paths.js";
|
||||
import { applyConfigOverrides } from "./runtime-overrides.js";
|
||||
import type { ClawdbotConfig, ConfigFileSnapshot, LegacyConfigIssue } from "./types.js";
|
||||
import { validateConfigObject } from "./validation.js";
|
||||
import { ClawdbotSchema } from "./zod-schema.js";
|
||||
import { validateConfigObjectWithPlugins } from "./validation.js";
|
||||
import { compareClawdbotVersions } from "./version.js";
|
||||
|
||||
// Re-export for backwards compatibility
|
||||
@@ -233,21 +232,34 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
const resolvedConfig = substituted;
|
||||
warnOnConfigMiskeys(resolvedConfig, deps.logger);
|
||||
if (typeof resolvedConfig !== "object" || resolvedConfig === null) return {};
|
||||
const validated = ClawdbotSchema.safeParse(resolvedConfig);
|
||||
if (!validated.success) {
|
||||
deps.logger.error("Invalid config:");
|
||||
for (const iss of validated.error.issues) {
|
||||
deps.logger.error(`- ${iss.path.join(".")}: ${iss.message}`);
|
||||
}
|
||||
return {};
|
||||
const preValidationDuplicates = findDuplicateAgentDirs(resolvedConfig as ClawdbotConfig, {
|
||||
env: deps.env,
|
||||
homedir: deps.homedir,
|
||||
});
|
||||
if (preValidationDuplicates.length > 0) {
|
||||
throw new DuplicateAgentDirError(preValidationDuplicates);
|
||||
}
|
||||
warnIfConfigFromFuture(validated.data as ClawdbotConfig, deps.logger);
|
||||
const validated = validateConfigObjectWithPlugins(resolvedConfig);
|
||||
if (!validated.ok) {
|
||||
const details = validated.issues
|
||||
.map((iss) => `- ${iss.path || "<root>"}: ${iss.message}`)
|
||||
.join("\n");
|
||||
deps.logger.error(`Invalid config:\\n${details}`);
|
||||
throw new Error("Invalid config");
|
||||
}
|
||||
if (validated.warnings.length > 0) {
|
||||
const details = validated.warnings
|
||||
.map((iss) => `- ${iss.path || "<root>"}: ${iss.message}`)
|
||||
.join("\n");
|
||||
deps.logger.warn(`Config warnings:\\n${details}`);
|
||||
}
|
||||
warnIfConfigFromFuture(validated.config, deps.logger);
|
||||
const cfg = applyModelDefaults(
|
||||
applyCompactionDefaults(
|
||||
applyContextPruningDefaults(
|
||||
applyAgentDefaults(
|
||||
applySessionDefaults(
|
||||
applyLoggingDefaults(applyMessageDefaults(validated.data as ClawdbotConfig)),
|
||||
applyLoggingDefaults(applyMessageDefaults(validated.config)),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -310,6 +322,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
config,
|
||||
hash,
|
||||
issues: [],
|
||||
warnings: [],
|
||||
legacyIssues,
|
||||
};
|
||||
}
|
||||
@@ -328,6 +341,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
config: {},
|
||||
hash,
|
||||
issues: [{ path: "", message: `JSON5 parse failed: ${parsedRes.error}` }],
|
||||
warnings: [],
|
||||
legacyIssues: [],
|
||||
};
|
||||
}
|
||||
@@ -353,6 +367,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
config: coerceConfig(parsedRes.parsed),
|
||||
hash,
|
||||
issues: [{ path: "", message }],
|
||||
warnings: [],
|
||||
legacyIssues: [],
|
||||
};
|
||||
}
|
||||
@@ -375,6 +390,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
config: coerceConfig(resolved),
|
||||
hash,
|
||||
issues: [{ path: "", message }],
|
||||
warnings: [],
|
||||
legacyIssues: [],
|
||||
};
|
||||
}
|
||||
@@ -382,7 +398,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
const resolvedConfigRaw = substituted;
|
||||
const legacyIssues = findLegacyConfigIssues(resolvedConfigRaw);
|
||||
|
||||
const validated = validateConfigObject(resolvedConfigRaw);
|
||||
const validated = validateConfigObjectWithPlugins(resolvedConfigRaw);
|
||||
if (!validated.ok) {
|
||||
return {
|
||||
path: configPath,
|
||||
@@ -393,6 +409,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
config: coerceConfig(resolvedConfigRaw),
|
||||
hash,
|
||||
issues: validated.issues,
|
||||
warnings: validated.warnings,
|
||||
legacyIssues,
|
||||
};
|
||||
}
|
||||
@@ -415,6 +432,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
),
|
||||
hash,
|
||||
issues: [],
|
||||
warnings: validated.warnings,
|
||||
legacyIssues,
|
||||
};
|
||||
} catch (err) {
|
||||
@@ -427,6 +445,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
config: {},
|
||||
hash: hashConfigRaw(null),
|
||||
issues: [{ path: "", message: `read failed: ${String(err)}` }],
|
||||
warnings: [],
|
||||
legacyIssues: [],
|
||||
};
|
||||
}
|
||||
@@ -434,6 +453,18 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
|
||||
async function writeConfigFile(cfg: ClawdbotConfig) {
|
||||
clearConfigCache();
|
||||
const validated = validateConfigObjectWithPlugins(cfg);
|
||||
if (!validated.ok) {
|
||||
const issue = validated.issues[0];
|
||||
const pathLabel = issue?.path ? issue.path : "<root>";
|
||||
throw new Error(`Config validation failed: ${pathLabel}: ${issue?.message ?? "invalid"}`);
|
||||
}
|
||||
if (validated.warnings.length > 0) {
|
||||
const details = validated.warnings
|
||||
.map((warning) => `- ${warning.path}: ${warning.message}`)
|
||||
.join("\n");
|
||||
deps.logger.warn(`Config warnings:\n${details}`);
|
||||
}
|
||||
const dir = path.dirname(configPath);
|
||||
await deps.fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
|
||||
const json = JSON.stringify(applyModelDefaults(stampConfigVersion(cfg)), null, 2)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { applyLegacyMigrations } from "./legacy.js";
|
||||
import type { ClawdbotConfig } from "./types.js";
|
||||
import { validateConfigObject } from "./validation.js";
|
||||
import { validateConfigObjectWithPlugins } from "./validation.js";
|
||||
|
||||
export function migrateLegacyConfig(raw: unknown): {
|
||||
config: ClawdbotConfig | null;
|
||||
@@ -8,7 +8,7 @@ export function migrateLegacyConfig(raw: unknown): {
|
||||
} {
|
||||
const { next, changes } = applyLegacyMigrations(raw);
|
||||
if (!next) return { config: null, changes: [] };
|
||||
const validated = validateConfigObject(next);
|
||||
const validated = validateConfigObjectWithPlugins(next);
|
||||
if (!validated.ok) {
|
||||
changes.push("Migration applied, but config still invalid; fix remaining issues manually.");
|
||||
return { config: null, changes };
|
||||
|
||||
@@ -105,5 +105,6 @@ export type ConfigFileSnapshot = {
|
||||
config: ClawdbotConfig;
|
||||
hash?: string;
|
||||
issues: ConfigValidationIssue[];
|
||||
warnings: ConfigValidationIssue[];
|
||||
legacyIssues: LegacyConfigIssue[];
|
||||
};
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { CHANNEL_IDS } from "../channels/registry.js";
|
||||
import {
|
||||
normalizePluginsConfig,
|
||||
resolveEnableState,
|
||||
resolveMemorySlotDecision,
|
||||
} from "../plugins/config-state.js";
|
||||
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
import { validateJsonSchemaValue } from "../plugins/schema-validator.js";
|
||||
import { findDuplicateAgentDirs, formatDuplicateAgentDirError } from "./agent-dirs.js";
|
||||
import { applyAgentDefaults, applyModelDefaults, applySessionDefaults } from "./defaults.js";
|
||||
import { findLegacyConfigIssues } from "./legacy.js";
|
||||
@@ -46,3 +55,183 @@ export function validateConfigObject(
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
export function validateConfigObjectWithPlugins(raw: unknown):
|
||||
| {
|
||||
ok: true;
|
||||
config: ClawdbotConfig;
|
||||
warnings: ConfigValidationIssue[];
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
issues: ConfigValidationIssue[];
|
||||
warnings: ConfigValidationIssue[];
|
||||
} {
|
||||
const base = validateConfigObject(raw);
|
||||
if (!base.ok) {
|
||||
return { ok: false, issues: base.issues, warnings: [] };
|
||||
}
|
||||
|
||||
const config = base.config;
|
||||
const issues: ConfigValidationIssue[] = [];
|
||||
const warnings: ConfigValidationIssue[] = [];
|
||||
const pluginsConfig = config.plugins;
|
||||
const normalizedPlugins = normalizePluginsConfig(pluginsConfig);
|
||||
|
||||
const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
|
||||
const registry = loadPluginManifestRegistry({
|
||||
config,
|
||||
workspaceDir: workspaceDir ?? undefined,
|
||||
});
|
||||
|
||||
const knownIds = new Set(registry.plugins.map((record) => record.id));
|
||||
|
||||
for (const diag of registry.diagnostics) {
|
||||
let path = diag.pluginId ? `plugins.entries.${diag.pluginId}` : "plugins";
|
||||
if (!diag.pluginId && diag.message.includes("plugin path not found")) {
|
||||
path = "plugins.load.paths";
|
||||
}
|
||||
const pluginLabel = diag.pluginId ? `plugin ${diag.pluginId}` : "plugin";
|
||||
const message = `${pluginLabel}: ${diag.message}`;
|
||||
if (diag.level === "error") {
|
||||
issues.push({ path, message });
|
||||
} else {
|
||||
warnings.push({ path, message });
|
||||
}
|
||||
}
|
||||
|
||||
const entries = pluginsConfig?.entries;
|
||||
if (entries && isRecord(entries)) {
|
||||
for (const pluginId of Object.keys(entries)) {
|
||||
if (!knownIds.has(pluginId)) {
|
||||
issues.push({
|
||||
path: `plugins.entries.${pluginId}`,
|
||||
message: `plugin not found: ${pluginId}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const allow = pluginsConfig?.allow ?? [];
|
||||
for (const pluginId of allow) {
|
||||
if (typeof pluginId !== "string" || !pluginId.trim()) continue;
|
||||
if (!knownIds.has(pluginId)) {
|
||||
issues.push({
|
||||
path: "plugins.allow",
|
||||
message: `plugin not found: ${pluginId}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const deny = pluginsConfig?.deny ?? [];
|
||||
for (const pluginId of deny) {
|
||||
if (typeof pluginId !== "string" || !pluginId.trim()) continue;
|
||||
if (!knownIds.has(pluginId)) {
|
||||
issues.push({
|
||||
path: "plugins.deny",
|
||||
message: `plugin not found: ${pluginId}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const memorySlot = normalizedPlugins.slots.memory;
|
||||
if (typeof memorySlot === "string" && memorySlot.trim() && !knownIds.has(memorySlot)) {
|
||||
issues.push({
|
||||
path: "plugins.slots.memory",
|
||||
message: `plugin not found: ${memorySlot}`,
|
||||
});
|
||||
}
|
||||
|
||||
const allowedChannels = new Set<string>(["defaults", ...CHANNEL_IDS]);
|
||||
for (const record of registry.plugins) {
|
||||
for (const channelId of record.channels) {
|
||||
allowedChannels.add(channelId);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.channels && isRecord(config.channels)) {
|
||||
for (const key of Object.keys(config.channels)) {
|
||||
const trimmed = key.trim();
|
||||
if (!trimmed) continue;
|
||||
if (!allowedChannels.has(trimmed)) {
|
||||
issues.push({
|
||||
path: `channels.${trimmed}`,
|
||||
message: `unknown channel id: ${trimmed}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let selectedMemoryPluginId: string | null = null;
|
||||
const seenPlugins = new Set<string>();
|
||||
for (const record of registry.plugins) {
|
||||
const pluginId = record.id;
|
||||
if (seenPlugins.has(pluginId)) {
|
||||
continue;
|
||||
}
|
||||
seenPlugins.add(pluginId);
|
||||
const entry = normalizedPlugins.entries[pluginId];
|
||||
const entryHasConfig = Boolean(entry?.config);
|
||||
|
||||
const enableState = resolveEnableState(pluginId, record.origin, normalizedPlugins);
|
||||
let enabled = enableState.enabled;
|
||||
let reason = enableState.reason;
|
||||
|
||||
if (enabled) {
|
||||
const memoryDecision = resolveMemorySlotDecision({
|
||||
id: pluginId,
|
||||
kind: record.kind,
|
||||
slot: memorySlot,
|
||||
selectedId: selectedMemoryPluginId,
|
||||
});
|
||||
if (!memoryDecision.enabled) {
|
||||
enabled = false;
|
||||
reason = memoryDecision.reason;
|
||||
}
|
||||
if (memoryDecision.selected && record.kind === "memory") {
|
||||
selectedMemoryPluginId = pluginId;
|
||||
}
|
||||
}
|
||||
|
||||
const shouldValidate = enabled || entryHasConfig;
|
||||
if (shouldValidate) {
|
||||
if (record.configSchema) {
|
||||
const res = validateJsonSchemaValue({
|
||||
schema: record.configSchema,
|
||||
cacheKey: record.schemaCacheKey ?? record.manifestPath ?? pluginId,
|
||||
value: entry?.config ?? {},
|
||||
});
|
||||
if (!res.ok) {
|
||||
for (const error of res.errors) {
|
||||
issues.push({
|
||||
path: `plugins.entries.${pluginId}.config`,
|
||||
message: `invalid config: ${error}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
issues.push({
|
||||
path: `plugins.entries.${pluginId}`,
|
||||
message: `plugin schema missing for ${pluginId}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!enabled && entryHasConfig) {
|
||||
warnings.push({
|
||||
path: `plugins.entries.${pluginId}`,
|
||||
message: `plugin disabled (${reason ?? "disabled"}) but config is present`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (issues.length > 0) {
|
||||
return { ok: false, issues, warnings };
|
||||
}
|
||||
|
||||
return { ok: true, config, warnings };
|
||||
}
|
||||
|
||||
@@ -30,5 +30,5 @@ export const ChannelsSchema = z
|
||||
imessage: IMessageConfigSchema.optional(),
|
||||
msteams: MSTeamsConfigSchema.optional(),
|
||||
})
|
||||
.strict()
|
||||
.passthrough()
|
||||
.optional();
|
||||
|
||||
Reference in New Issue
Block a user