mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(channels): config validation rejects documented channels.<id>.configWrites on 13 channels (#117206)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"core": 2307,
|
||||
"channel": 3664,
|
||||
"channel": 3692,
|
||||
"plugin": 4055
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
820fe810979007010e2ade951ac178d204fcac10e48a6657cdd4b75de6ce0aee config-baseline.json
|
||||
00b1e5142ead3ba8522cc4815dfb6f8d3fb5ea4e2937df93da280d44506308d7 config-baseline.json
|
||||
b89715475e4b18a0d32765fda42bcce38537f6d49f949bb4ff0c7d1630101882 config-baseline.core.json
|
||||
26077716f773821c1ad07160632c3a5ed48f7bdcc95ea82cf77c99bb8bba5834 config-baseline.channel.json
|
||||
c02f1b49ac814cb27fd692a8107528b6ccfa7ea9e5aea854fc81830abca38eb4 config-baseline.plugin.json
|
||||
e9a81ee89ff032033012413e161316e4d07e8f6b206382a25fed2f8485151b5e config-baseline.channel.json
|
||||
d0352edda9ee2e6ff3c52b163bf2364ffef6a1d0d2ffe3b0b38de28756fd6c1b config-baseline.plugin.json
|
||||
|
||||
@@ -18,6 +18,7 @@ const RawBuzzConfigSchema = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
configWrites: z.boolean().optional(),
|
||||
markdown: MarkdownConfigSchema,
|
||||
relayUrl: z
|
||||
.string()
|
||||
|
||||
@@ -12,6 +12,7 @@ const ClickClackAccountConfigSchema = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
configWrites: z.boolean().optional(),
|
||||
baseUrl: z.string().url().optional(),
|
||||
apiBaseUrl: z.string().url().optional(),
|
||||
token: buildSecretInputSchema().optional(),
|
||||
|
||||
@@ -22,6 +22,7 @@ const ThreadBindingsSchema = z
|
||||
|
||||
const LineCommonConfigSchemaBase = z.object({
|
||||
enabled: z.boolean().optional(),
|
||||
configWrites: z.boolean().optional(),
|
||||
channelAccessToken: z.string().optional(),
|
||||
channelSecret: z.string().optional(),
|
||||
tokenFile: z.string().optional(),
|
||||
|
||||
@@ -130,6 +130,7 @@ function hasCanonicalMatrixAccountStreaming(account: unknown): boolean {
|
||||
const MatrixConfigSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
configWrites: z.boolean().optional(),
|
||||
defaultAccount: z.string().optional(),
|
||||
// Accounts stay schema-open, but retired scalar streaming must fail loudly
|
||||
// instead of silently resolving to "off"; doctor migrates the old spelling.
|
||||
|
||||
@@ -28,6 +28,7 @@ const NextcloudTalkAccountSchemaBase = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
configWrites: z.boolean().optional(),
|
||||
markdown: MarkdownConfigSchema,
|
||||
baseUrl: z.string().optional(),
|
||||
botSecret: buildSecretInputSchema().optional(),
|
||||
|
||||
@@ -78,6 +78,7 @@ export const NostrConfigSchema = z.object({
|
||||
|
||||
/** Whether this channel is enabled */
|
||||
enabled: z.boolean().optional(),
|
||||
configWrites: z.boolean().optional(),
|
||||
|
||||
/** Markdown formatting overrides (tables). */
|
||||
markdown: MarkdownConfigSchema,
|
||||
|
||||
@@ -26,6 +26,7 @@ const QaChannelAccountConfigSchema = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
configWrites: z.boolean().optional(),
|
||||
baseUrl: z.string().url().optional(),
|
||||
botUserId: z.string().optional(),
|
||||
botDisplayName: z.string().optional(),
|
||||
|
||||
@@ -9,6 +9,7 @@ const RaftAccountSchema = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
configWrites: z.boolean().optional(),
|
||||
profile: z.string().min(1).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
@@ -13,6 +13,7 @@ const RelayUrlSchema = z
|
||||
export const ReefChannelConfigSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().default(true),
|
||||
configWrites: z.boolean().optional(),
|
||||
relayUrl: RelayUrlSchema.default("https://reefwire.ai"),
|
||||
handle: HandleSchema.optional(),
|
||||
email: z.email().optional(),
|
||||
|
||||
@@ -16,6 +16,7 @@ const SmsAccountConfigSchema = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
configWrites: z.boolean().optional(),
|
||||
accountSid: z.string().optional(),
|
||||
authToken: SecretInputSchema.optional(),
|
||||
fromNumber: z.string().optional(),
|
||||
|
||||
@@ -28,6 +28,7 @@ const TlonNetworkSchema = z
|
||||
const tlonCommonConfigFields = {
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
configWrites: z.boolean().optional(),
|
||||
ship: ShipSchema.optional(),
|
||||
url: z.string().optional(),
|
||||
code: z.string().optional(),
|
||||
|
||||
@@ -18,6 +18,8 @@ const TwitchAccountShape = {
|
||||
channel: z.string().min(1),
|
||||
/** Enable this account */
|
||||
enabled: z.boolean().optional(),
|
||||
/** Allow channel-initiated configuration writes */
|
||||
configWrites: z.boolean().optional(),
|
||||
/** Allowlist of Twitch user IDs who can interact with the bot (use IDs for safety, not usernames) */
|
||||
allowFrom: z.array(z.string()).optional(),
|
||||
/** Roles allowed to interact with the bot (e.g., ["moderator", "vip", "subscriber"]) */
|
||||
@@ -47,6 +49,7 @@ const TwitchAccountSchema = z.object(TwitchAccountShape);
|
||||
const TwitchConfigBaseShape = {
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
configWrites: z.boolean().optional(),
|
||||
markdown: MarkdownConfigSchema.optional(),
|
||||
defaultAccount: z.string().optional(),
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import { buildSecretInputSchema } from "./secret-input.js";
|
||||
const zaloAccountSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
configWrites: z.boolean().optional(),
|
||||
markdown: MarkdownConfigSchema,
|
||||
botToken: buildSecretInputSchema().optional(),
|
||||
tokenFile: z.string().optional(),
|
||||
|
||||
@@ -16,6 +16,7 @@ const groupConfigSchema = buildGroupEntrySchema()
|
||||
const zalouserAccountSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
configWrites: z.boolean().optional(),
|
||||
markdown: MarkdownConfigSchema,
|
||||
profile: z.string().optional(),
|
||||
dangerouslyAllowNameMatching: z.boolean().optional(),
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,71 @@
|
||||
// Verifies every bundled channel schema accepts the documented configWrites policy key.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA } from "../../config/bundled-channel-config-metadata.generated.js";
|
||||
|
||||
type JsonSchemaLike = {
|
||||
properties?: Record<string, unknown>;
|
||||
additionalProperties?: unknown;
|
||||
allOf?: unknown[];
|
||||
anyOf?: unknown[];
|
||||
oneOf?: unknown[];
|
||||
};
|
||||
|
||||
function asSchema(value: unknown): JsonSchemaLike | undefined {
|
||||
return value && typeof value === "object" ? (value as JsonSchemaLike) : undefined;
|
||||
}
|
||||
|
||||
function schemasAtLevel(schema: JsonSchemaLike | undefined): JsonSchemaLike[] {
|
||||
if (!schema) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
schema,
|
||||
...[schema.allOf, schema.anyOf, schema.oneOf]
|
||||
.flatMap((variants) => variants ?? [])
|
||||
.flatMap((variant) => schemasAtLevel(asSchema(variant))),
|
||||
];
|
||||
}
|
||||
|
||||
/** Any closed alternative silently refuses the whole config when the documented key is missing. */
|
||||
function rejectsKey(schema: JsonSchemaLike | undefined, key: string): boolean {
|
||||
return schemasAtLevel(schema).some(
|
||||
(candidate) =>
|
||||
candidate.additionalProperties === false && !Object.hasOwn(candidate.properties ?? {}, key),
|
||||
);
|
||||
}
|
||||
|
||||
function accountSchemas(schema: JsonSchemaLike | undefined): JsonSchemaLike[] {
|
||||
return schemasAtLevel(schema).flatMap((root) => {
|
||||
const accounts = asSchema(root.properties?.accounts);
|
||||
return schemasAtLevel(accounts).flatMap((accountsVariant) =>
|
||||
schemasAtLevel(asSchema(accountsVariant.additionalProperties)),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
describe("bundled channel configWrites contract", () => {
|
||||
const channels = GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA.map((entry) => ({
|
||||
id: entry.channelId,
|
||||
schema: asSchema(entry.schema),
|
||||
}));
|
||||
|
||||
it("covers the bundled channels", () => {
|
||||
expect(channels.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it.each(channels.map((channel) => [channel.id, channel] as const))(
|
||||
"%s accepts channels.<id>.configWrites",
|
||||
(_id, channel) => {
|
||||
expect(rejectsKey(channel.schema, "configWrites")).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(channels.map((channel) => [channel.id, channel] as const))(
|
||||
"%s accepts channels.<id>.accounts.<account>.configWrites",
|
||||
(_id, channel) => {
|
||||
expect(
|
||||
accountSchemas(channel.schema).some((account) => rejectsKey(account, "configWrites")),
|
||||
).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user