mirror of https://github.com/openclaw/openclaw.git
fix(config): migrate legacy group allow aliases (#60597)
* fix(config): migrate legacy group allow aliases * fix(config): inline legacy streaming migration helpers * refactor(config): rename legacy account matcher helper * chore(agents): codify config contract boundaries * fix(config): keep legacy allow aliases writable * Update AGENTS.md
This commit is contained in:
parent
945b198c76
commit
9e389cff3d
|
|
@ -55,6 +55,11 @@
|
|||
- Public docs: `docs/gateway/protocol.md`, `docs/gateway/bridge-protocol.md`, `docs/concepts/architecture.md`
|
||||
- Definition files: `src/gateway/protocol/schema.ts`, `src/gateway/protocol/schema/*.ts`, `src/gateway/protocol/index.ts`
|
||||
- Rule: protocol changes are contract changes. Prefer additive evolution; incompatible changes require explicit versioning, docs, and client/codegen follow-through.
|
||||
- Config contract boundary:
|
||||
- Canonical public config lives in exported config types, zod/schema surfaces, schema help/labels, generated config metadata, config baselines, and any user-facing gateway/config payloads. Keep those surfaces aligned.
|
||||
- When a legacy config key is retired from the public contract, remove it from every public config surface above. Keep backward compatibility only through raw-config migration/doctor seams unless explicit product policy says otherwise.
|
||||
- Do not reintroduce removed legacy aliases into public types/schema/help/baselines “for convenience”. If old configs still need to load, handle that in `legacy.migrations.*`, config ingest, or `openclaw doctor --fix`.
|
||||
- `hooks.internal.entries` is the canonical public hook config model. `hooks.internal.handlers` is compatibility-only input and must not be re-exposed in public schema/help/baseline surfaces.
|
||||
- Bundled plugin contract boundary:
|
||||
- Public docs: `docs/plugins/architecture.md`, `docs/plugins/manifest.md`, `docs/plugins/sdk-overview.md`
|
||||
- Definition files: `src/plugins/contracts/registry.ts`, `src/plugins/types.ts`, `src/plugins/public-artifacts.ts`
|
||||
|
|
|
|||
|
|
@ -37,6 +37,137 @@
|
|||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "channels.bluebubbles.accounts.*.actions",
|
||||
"kind": "channel",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "channels.bluebubbles.accounts.*.actions.addParticipant",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"defaultValue": true,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.bluebubbles.accounts.*.actions.edit",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"defaultValue": true,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.bluebubbles.accounts.*.actions.leaveGroup",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"defaultValue": true,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.bluebubbles.accounts.*.actions.reactions",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"defaultValue": true,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.bluebubbles.accounts.*.actions.removeParticipant",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"defaultValue": true,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.bluebubbles.accounts.*.actions.renameGroup",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"defaultValue": true,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.bluebubbles.accounts.*.actions.reply",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"defaultValue": true,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.bluebubbles.accounts.*.actions.sendAttachment",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"defaultValue": true,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.bluebubbles.accounts.*.actions.sendWithEffect",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"defaultValue": true,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.bluebubbles.accounts.*.actions.setGroupIcon",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"defaultValue": true,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.bluebubbles.accounts.*.actions.unsend",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"defaultValue": true,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.bluebubbles.accounts.*.allowFrom",
|
||||
"kind": "channel",
|
||||
|
|
@ -1970,16 +2101,6 @@
|
|||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "channels.discord.accounts.*.guilds.*.channels.*.allow",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.discord.accounts.*.guilds.*.channels.*.autoArchiveDuration",
|
||||
"kind": "channel",
|
||||
|
|
@ -4473,16 +4594,6 @@
|
|||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "channels.discord.guilds.*.channels.*.allow",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.discord.guilds.*.channels.*.autoArchiveDuration",
|
||||
"kind": "channel",
|
||||
|
|
@ -8466,16 +8577,6 @@
|
|||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "channels.googlechat.accounts.*.groups.*.allow",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.googlechat.accounts.*.groups.*.enabled",
|
||||
"kind": "channel",
|
||||
|
|
@ -9147,16 +9248,6 @@
|
|||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "channels.googlechat.groups.*.allow",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.googlechat.groups.*.enabled",
|
||||
"kind": "channel",
|
||||
|
|
@ -20851,16 +20942,6 @@
|
|||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "channels.slack.accounts.*.channels.*.allow",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.slack.accounts.*.channels.*.allowBots",
|
||||
"kind": "channel",
|
||||
|
|
@ -22313,16 +22394,6 @@
|
|||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "channels.slack.channels.*.allow",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.slack.channels.*.allowBots",
|
||||
"kind": "channel",
|
||||
|
|
|
|||
|
|
@ -28067,6 +28067,137 @@
|
|||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "channels.bluebubbles.accounts.*.actions",
|
||||
"kind": "channel",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "channels.bluebubbles.accounts.*.actions.addParticipant",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"defaultValue": true,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.bluebubbles.accounts.*.actions.edit",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"defaultValue": true,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.bluebubbles.accounts.*.actions.leaveGroup",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"defaultValue": true,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.bluebubbles.accounts.*.actions.reactions",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"defaultValue": true,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.bluebubbles.accounts.*.actions.removeParticipant",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"defaultValue": true,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.bluebubbles.accounts.*.actions.renameGroup",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"defaultValue": true,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.bluebubbles.accounts.*.actions.reply",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"defaultValue": true,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.bluebubbles.accounts.*.actions.sendAttachment",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"defaultValue": true,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.bluebubbles.accounts.*.actions.sendWithEffect",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"defaultValue": true,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.bluebubbles.accounts.*.actions.setGroupIcon",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"defaultValue": true,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.bluebubbles.accounts.*.actions.unsend",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"defaultValue": true,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.bluebubbles.accounts.*.allowFrom",
|
||||
"kind": "channel",
|
||||
|
|
@ -30000,16 +30131,6 @@
|
|||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "channels.discord.accounts.*.guilds.*.channels.*.allow",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.discord.accounts.*.guilds.*.channels.*.autoArchiveDuration",
|
||||
"kind": "channel",
|
||||
|
|
@ -32503,16 +32624,6 @@
|
|||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "channels.discord.guilds.*.channels.*.allow",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.discord.guilds.*.channels.*.autoArchiveDuration",
|
||||
"kind": "channel",
|
||||
|
|
@ -36496,16 +36607,6 @@
|
|||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "channels.googlechat.accounts.*.groups.*.allow",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.googlechat.accounts.*.groups.*.enabled",
|
||||
"kind": "channel",
|
||||
|
|
@ -37177,16 +37278,6 @@
|
|||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "channels.googlechat.groups.*.allow",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.googlechat.groups.*.enabled",
|
||||
"kind": "channel",
|
||||
|
|
@ -48881,16 +48972,6 @@
|
|||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "channels.slack.accounts.*.channels.*.allow",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.slack.accounts.*.channels.*.allowBots",
|
||||
"kind": "channel",
|
||||
|
|
@ -50343,16 +50424,6 @@
|
|||
"tags": [],
|
||||
"hasChildren": true
|
||||
},
|
||||
{
|
||||
"path": "channels.slack.channels.*.allow",
|
||||
"kind": "channel",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"deprecated": false,
|
||||
"sensitive": false,
|
||||
"tags": [],
|
||||
"hasChildren": false
|
||||
},
|
||||
{
|
||||
"path": "channels.slack.channels.*.allowBots",
|
||||
"kind": "channel",
|
||||
|
|
|
|||
|
|
@ -30,9 +30,6 @@ function shouldAuditChannelConfig(config: DiscordGuildChannelConfig | undefined)
|
|||
if (!config) {
|
||||
return true;
|
||||
}
|
||||
if (config.allow === false) {
|
||||
return false;
|
||||
}
|
||||
if (config.enabled === false) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ function createAutoThreadMentionContext() {
|
|||
const guildInfo: DiscordGuildEntryResolved = {
|
||||
requireMention: true,
|
||||
channels: {
|
||||
general: { allow: true, autoThread: true },
|
||||
general: { enabled: true, autoThread: true },
|
||||
},
|
||||
};
|
||||
const channelConfig = resolveDiscordChannelConfig({
|
||||
|
|
@ -301,12 +301,11 @@ describe("discord guild/channel resolution", () => {
|
|||
it("resolves channel config by slug", () => {
|
||||
const guildInfo: DiscordGuildEntryResolved = {
|
||||
channels: {
|
||||
general: { allow: true },
|
||||
general: { enabled: true },
|
||||
help: {
|
||||
allow: true,
|
||||
enabled: true,
|
||||
requireMention: true,
|
||||
skills: ["search"],
|
||||
enabled: false,
|
||||
users: ["123"],
|
||||
systemPrompt: "Use short answers.",
|
||||
autoThread: true,
|
||||
|
|
@ -340,7 +339,7 @@ describe("discord guild/channel resolution", () => {
|
|||
it("denies channel when config present but no match", () => {
|
||||
const guildInfo: DiscordGuildEntryResolved = {
|
||||
channels: {
|
||||
general: { allow: true },
|
||||
general: { enabled: true },
|
||||
},
|
||||
};
|
||||
const channel = resolveDiscordChannelConfig({
|
||||
|
|
@ -368,8 +367,8 @@ describe("discord guild/channel resolution", () => {
|
|||
it("inherits parent config for thread channels", () => {
|
||||
const guildInfo: DiscordGuildEntryResolved = {
|
||||
channels: {
|
||||
general: { allow: true },
|
||||
random: { allow: false },
|
||||
general: { enabled: true },
|
||||
random: { enabled: false },
|
||||
},
|
||||
};
|
||||
const thread = resolveDiscordChannelConfigWithFallback({
|
||||
|
|
@ -388,8 +387,8 @@ describe("discord guild/channel resolution", () => {
|
|||
it("does not match thread name/slug when resolving allowlists", () => {
|
||||
const guildInfo: DiscordGuildEntryResolved = {
|
||||
channels: {
|
||||
general: { allow: true },
|
||||
random: { allow: false },
|
||||
general: { enabled: true },
|
||||
random: { enabled: false },
|
||||
},
|
||||
};
|
||||
const thread = resolveDiscordChannelConfigWithFallback({
|
||||
|
|
@ -408,8 +407,8 @@ describe("discord guild/channel resolution", () => {
|
|||
it("applies wildcard channel config when no specific match", () => {
|
||||
const guildInfo: DiscordGuildEntryResolved = {
|
||||
channels: {
|
||||
general: { allow: true, requireMention: false },
|
||||
"*": { allow: true, autoThread: true, requireMention: true },
|
||||
general: { enabled: true, requireMention: false },
|
||||
"*": { enabled: true, autoThread: true, requireMention: true },
|
||||
},
|
||||
};
|
||||
// Specific channel should NOT use wildcard
|
||||
|
|
@ -440,7 +439,7 @@ describe("discord guild/channel resolution", () => {
|
|||
it("falls back to wildcard when thread channel and parent are missing", () => {
|
||||
const guildInfo: DiscordGuildEntryResolved = {
|
||||
channels: {
|
||||
"*": { allow: true, requireMention: false },
|
||||
"*": { enabled: true, requireMention: false },
|
||||
},
|
||||
};
|
||||
const thread = resolveDiscordChannelConfigWithFallback({
|
||||
|
|
@ -481,7 +480,7 @@ describe("discord mention gating", () => {
|
|||
const guildInfo: DiscordGuildEntryResolved = {
|
||||
requireMention: true,
|
||||
channels: {
|
||||
general: { allow: true },
|
||||
general: { enabled: true },
|
||||
},
|
||||
};
|
||||
const channelConfig = resolveDiscordChannelConfig({
|
||||
|
|
@ -527,7 +526,7 @@ describe("discord mention gating", () => {
|
|||
const guildInfo: DiscordGuildEntryResolved = {
|
||||
requireMention: true,
|
||||
channels: {
|
||||
"parent-1": { allow: true, requireMention: false },
|
||||
"parent-1": { enabled: true, requireMention: false },
|
||||
},
|
||||
};
|
||||
const channelConfig = resolveDiscordChannelConfigWithFallback({
|
||||
|
|
@ -1174,7 +1173,7 @@ describe("discord DM reaction handling", () => {
|
|||
roles: ["role:blocked-role"],
|
||||
channels: {
|
||||
"channel-1": {
|
||||
allow: true,
|
||||
enabled: true,
|
||||
roles: ["role:trusted-role"],
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export const CATEGORY_GUILD_CFG = {
|
|||
guilds: {
|
||||
"*": {
|
||||
requireMention: false,
|
||||
channels: { c1: { allow: true } },
|
||||
channels: { c1: { enabled: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -122,7 +122,7 @@ export async function createCategoryGuildHandler(runtimeError?: (err: unknown) =
|
|||
return createGuildHandler({
|
||||
cfg: CATEGORY_GUILD_CFG,
|
||||
guildEntries: {
|
||||
"*": { requireMention: false, channels: { c1: { allow: true } } },
|
||||
"*": { requireMention: false, channels: { c1: { enabled: true } } },
|
||||
},
|
||||
runtimeError,
|
||||
});
|
||||
|
|
@ -298,7 +298,7 @@ export function createMentionRequiredGuildConfig(overrides?: Partial<Config>): C
|
|||
guilds: {
|
||||
"*": {
|
||||
requireMention: true,
|
||||
channels: { c1: { allow: true } },
|
||||
channels: { c1: { enabled: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export type DiscordGuildEntryResolved = {
|
|||
reactionNotifications?: "off" | "own" | "all" | "allowlist";
|
||||
users?: string[];
|
||||
roles?: string[];
|
||||
channels?: Record<string, { allow?: boolean } & DiscordChannelOverrideConfig>;
|
||||
channels?: Record<string, DiscordChannelOverrideConfig>;
|
||||
};
|
||||
|
||||
export type DiscordChannelConfigResolved = DiscordChannelOverrideConfig & {
|
||||
|
|
@ -394,7 +394,7 @@ function resolveDiscordChannelConfigEntry(
|
|||
entry: DiscordChannelEntry,
|
||||
): DiscordChannelConfigResolved {
|
||||
const resolved: DiscordChannelConfigResolved = {
|
||||
allowed: entry.allow !== false,
|
||||
allowed: entry.enabled !== false,
|
||||
requireMention: entry.requireMention,
|
||||
ignoreOtherMentions: entry.ignoreOtherMentions,
|
||||
skills: entry.skills,
|
||||
|
|
|
|||
|
|
@ -177,7 +177,6 @@ function createAllowedGuildEntries(requireMention = false) {
|
|||
id: GUILD_ID,
|
||||
channels: {
|
||||
[CHANNEL_ID]: {
|
||||
allow: true,
|
||||
enabled: true,
|
||||
requireMention,
|
||||
},
|
||||
|
|
@ -250,7 +249,6 @@ describe("preflightDiscordMessage configured ACP bindings", () => {
|
|||
id: GUILD_ID,
|
||||
channels: {
|
||||
[CHANNEL_ID]: {
|
||||
allow: true,
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
|
|
@ -272,7 +270,6 @@ describe("preflightDiscordMessage configured ACP bindings", () => {
|
|||
id: GUILD_ID,
|
||||
channels: {
|
||||
[CHANNEL_ID]: {
|
||||
allow: true,
|
||||
enabled: true,
|
||||
requireMention: false,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -621,7 +621,7 @@ describe("preflightDiscordMessage", () => {
|
|||
[guildId]: {
|
||||
channels: {
|
||||
[channelId]: {
|
||||
allow: true,
|
||||
enabled: true,
|
||||
requireMention: true,
|
||||
},
|
||||
},
|
||||
|
|
@ -655,7 +655,7 @@ describe("preflightDiscordMessage", () => {
|
|||
"guild-1": {
|
||||
channels: {
|
||||
"ch-1": {
|
||||
allow: true,
|
||||
enabled: true,
|
||||
requireMention: false,
|
||||
},
|
||||
},
|
||||
|
|
@ -704,7 +704,7 @@ describe("preflightDiscordMessage", () => {
|
|||
"guild-1": {
|
||||
channels: {
|
||||
[parentId]: {
|
||||
allow: true,
|
||||
enabled: true,
|
||||
requireMention: false,
|
||||
},
|
||||
},
|
||||
|
|
@ -890,7 +890,7 @@ describe("preflightDiscordMessage", () => {
|
|||
"guild-1": {
|
||||
channels: {
|
||||
[channelId]: {
|
||||
allow: true,
|
||||
enabled: true,
|
||||
requireMention: true,
|
||||
},
|
||||
},
|
||||
|
|
@ -958,7 +958,7 @@ describe("preflightDiscordMessage", () => {
|
|||
[guildId]: {
|
||||
channels: {
|
||||
[channelId]: {
|
||||
allow: true,
|
||||
enabled: true,
|
||||
requireMention: true,
|
||||
users: ["user-1"],
|
||||
},
|
||||
|
|
@ -1004,7 +1004,7 @@ describe("preflightDiscordMessage", () => {
|
|||
}),
|
||||
discordConfig: {} as DiscordConfig,
|
||||
guildEntries: {
|
||||
"guild-1": { channels: { [channelId]: { allow: true, requireMention: true } } },
|
||||
"guild-1": { channels: { [channelId]: { enabled: true, requireMention: true } } },
|
||||
},
|
||||
});
|
||||
expect(result).toBeNull();
|
||||
|
|
@ -1048,7 +1048,7 @@ describe("preflightDiscordMessage", () => {
|
|||
}),
|
||||
discordConfig: {} as DiscordConfig,
|
||||
guildEntries: {
|
||||
"guild-1": { channels: { [channelId]: { allow: true, requireMention: true } } },
|
||||
"guild-1": { channels: { [channelId]: { enabled: true, requireMention: true } } },
|
||||
},
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
|
|
|
|||
|
|
@ -460,7 +460,7 @@ describe("discord component interactions", () => {
|
|||
channels: { discord: { replyToMode: "first", groupPolicy: "allowlist" } },
|
||||
} as OpenClawConfig,
|
||||
discordConfig: createDiscordConfig({ groupPolicy: "allowlist" }),
|
||||
guildEntries: { g1: { channels: { "guild-channel": { allow: true, enabled: false } } } },
|
||||
guildEntries: { g1: { channels: { "guild-channel": { enabled: false } } } },
|
||||
}),
|
||||
);
|
||||
const { interaction, reply } = createComponentButtonInteraction({
|
||||
|
|
@ -494,7 +494,7 @@ describe("discord component interactions", () => {
|
|||
channels: { discord: { replyToMode: "first", groupPolicy: "allowlist" } },
|
||||
} as OpenClawConfig,
|
||||
discordConfig: createDiscordConfig({ groupPolicy: "allowlist" }),
|
||||
guildEntries: { g1: { channels: { "guild-channel": { allow: false } } } },
|
||||
guildEntries: { g1: { channels: { "guild-channel": { enabled: false } } } },
|
||||
}),
|
||||
);
|
||||
const { interaction, reply } = createComponentButtonInteraction({
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ function createConfig(): OpenClawConfig {
|
|||
"345678901234567890": {
|
||||
channels: {
|
||||
"234567890123456789": {
|
||||
allow: true,
|
||||
enabled: true,
|
||||
requireMention: false,
|
||||
},
|
||||
},
|
||||
|
|
@ -222,7 +222,7 @@ describe("Discord native slash commands with commands.allowFrom", () => {
|
|||
"345678901234567890": {
|
||||
channels: {
|
||||
"234567890123456789": {
|
||||
allow: true,
|
||||
enabled: true,
|
||||
requireMention: false,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ function createConfiguredAcpCase(params: {
|
|||
guilds: {
|
||||
[params.guildId!]: {
|
||||
channels: {
|
||||
[params.channelId]: { allow: true, requireMention: false },
|
||||
[params.channelId]: { enabled: true, requireMention: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -422,7 +422,7 @@ describe("Discord native plugin command dispatch", () => {
|
|||
"345678901234567890": {
|
||||
channels: {
|
||||
"234567890123456789": {
|
||||
allow: true,
|
||||
enabled: true,
|
||||
requireMention: false,
|
||||
},
|
||||
},
|
||||
|
|
@ -557,11 +557,11 @@ describe("Discord native plugin command dispatch", () => {
|
|||
"345678901234567890": {
|
||||
channels: {
|
||||
"thread-123": {
|
||||
allow: true,
|
||||
enabled: true,
|
||||
requireMention: false,
|
||||
},
|
||||
"parent-456": {
|
||||
allow: true,
|
||||
enabled: true,
|
||||
requireMention: false,
|
||||
},
|
||||
},
|
||||
|
|
@ -658,7 +658,7 @@ describe("Discord native plugin command dispatch", () => {
|
|||
guilds: {
|
||||
[guildId]: {
|
||||
channels: {
|
||||
[channelId]: { allow: true, requireMention: false },
|
||||
[channelId]: { enabled: true, requireMention: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export function setDiscordGuildChannelAllowlist(
|
|||
const existing = guilds[guildKey] ?? {};
|
||||
if (entry.channelKey) {
|
||||
const channels = { ...existing.channels };
|
||||
channels[entry.channelKey] = { allow: true };
|
||||
channels[entry.channelKey] = { enabled: true };
|
||||
guilds[guildKey] = { ...existing, channels };
|
||||
} else {
|
||||
guilds[guildKey] = existing;
|
||||
|
|
|
|||
|
|
@ -230,7 +230,7 @@ describe("googlechat inbound access policy", () => {
|
|||
config: {
|
||||
groups: {
|
||||
"spaces/AAA": {
|
||||
allow: true,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -337,7 +337,7 @@ describe("googlechat inbound access policy", () => {
|
|||
users: ["users/alice"],
|
||||
},
|
||||
"Finance Ops": {
|
||||
allow: false,
|
||||
enabled: false,
|
||||
users: ["users/bob"],
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -63,7 +63,6 @@ export function isSenderAllowed(
|
|||
|
||||
type GoogleChatGroupEntry = {
|
||||
requireMention?: boolean;
|
||||
allow?: boolean;
|
||||
enabled?: boolean;
|
||||
users?: Array<string | number>;
|
||||
systemPrompt?: string;
|
||||
|
|
@ -242,7 +241,7 @@ export async function applyGoogleChatInboundAccessPolicy(params: {
|
|||
groupPolicy,
|
||||
routeAllowlistConfigured: groupAllowlistConfigured,
|
||||
routeMatched: Boolean(groupEntry),
|
||||
routeEnabled: groupEntry?.enabled !== false && groupEntry?.allow !== false,
|
||||
routeEnabled: groupEntry?.enabled !== false,
|
||||
});
|
||||
if (!routeAccess.allowed) {
|
||||
if (routeAccess.reason === "disabled") {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ export type SlackChannelConfigResolved = {
|
|||
|
||||
export type SlackChannelConfigEntry = {
|
||||
enabled?: boolean;
|
||||
allow?: boolean;
|
||||
requireMention?: boolean;
|
||||
allowBots?: boolean;
|
||||
users?: Array<string | number>;
|
||||
|
|
@ -135,9 +134,7 @@ export function resolveSlackChannelConfig(params: {
|
|||
}
|
||||
|
||||
const resolved = matched ?? fallback ?? {};
|
||||
const allowed =
|
||||
firstDefined(resolved.enabled, resolved.allow, fallback?.enabled, fallback?.allow, true) ??
|
||||
true;
|
||||
const allowed = firstDefined(resolved.enabled, fallback?.enabled, true) ?? true;
|
||||
const requireMention =
|
||||
firstDefined(resolved.requireMention, fallback?.requireMention, requireMentionDefault) ??
|
||||
requireMentionDefault;
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export function createSlackSystemEventTestHarness(overrides?: SlackSystemEventTe
|
|||
? {
|
||||
C1: {
|
||||
users: overrides.channelUsers,
|
||||
allow: true,
|
||||
enabled: true,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ describe("resolveSlackChannelConfig", () => {
|
|||
it("uses wildcard entries when no direct channel config exists", () => {
|
||||
const res = resolveSlackChannelConfig({
|
||||
channelId: "C1",
|
||||
channels: { "*": { allow: true, requireMention: false } },
|
||||
channels: { "*": { enabled: true, requireMention: false } },
|
||||
defaultRequireMention: true,
|
||||
});
|
||||
expect(res).toMatchObject({
|
||||
|
|
@ -49,7 +49,7 @@ describe("resolveSlackChannelConfig", () => {
|
|||
it("uses direct match metadata when channel config exists", () => {
|
||||
const res = resolveSlackChannelConfig({
|
||||
channelId: "C1",
|
||||
channels: { C1: { allow: true, requireMention: false } },
|
||||
channels: { C1: { enabled: true, requireMention: false } },
|
||||
defaultRequireMention: true,
|
||||
});
|
||||
expect(res).toMatchObject({
|
||||
|
|
@ -63,7 +63,7 @@ describe("resolveSlackChannelConfig", () => {
|
|||
// Users commonly copy them in lowercase from docs or older CLI output.
|
||||
const res = resolveSlackChannelConfig({
|
||||
channelId: "C0ABC12345", // pragma: allowlist secret
|
||||
channels: { c0abc12345: { allow: true, requireMention: false } },
|
||||
channels: { c0abc12345: { enabled: true, requireMention: false } },
|
||||
defaultRequireMention: true,
|
||||
});
|
||||
expect(res).toMatchObject({ allowed: true, requireMention: false });
|
||||
|
|
@ -73,7 +73,7 @@ describe("resolveSlackChannelConfig", () => {
|
|||
// Defensive: also handle the inverse direction.
|
||||
const res = resolveSlackChannelConfig({
|
||||
channelId: "c0abc12345", // pragma: allowlist secret
|
||||
channels: { C0ABC12345: { allow: true, requireMention: false } },
|
||||
channels: { C0ABC12345: { enabled: true, requireMention: false } },
|
||||
defaultRequireMention: true,
|
||||
});
|
||||
expect(res).toMatchObject({ allowed: true, requireMention: false });
|
||||
|
|
@ -83,7 +83,7 @@ describe("resolveSlackChannelConfig", () => {
|
|||
const res = resolveSlackChannelConfig({
|
||||
channelId: "C1",
|
||||
channelName: "ops-room",
|
||||
channels: { "ops-room": { allow: true, requireMention: false } },
|
||||
channels: { "ops-room": { enabled: true, requireMention: false } },
|
||||
defaultRequireMention: true,
|
||||
});
|
||||
expect(res).toMatchObject({ allowed: false, requireMention: true });
|
||||
|
|
@ -93,7 +93,7 @@ describe("resolveSlackChannelConfig", () => {
|
|||
const res = resolveSlackChannelConfig({
|
||||
channelId: "C1",
|
||||
channelName: "ops-room",
|
||||
channels: { "ops-room": { allow: true, requireMention: false } },
|
||||
channels: { "ops-room": { enabled: true, requireMention: false } },
|
||||
defaultRequireMention: true,
|
||||
allowNameMatching: true,
|
||||
});
|
||||
|
|
@ -266,8 +266,8 @@ describe("isChannelAllowed with groupPolicy and channelsConfig", () => {
|
|||
...baseParams(),
|
||||
groupPolicy: "open",
|
||||
channelsConfig: {
|
||||
C_ALLOWED: { allow: true },
|
||||
C_DENIED: { allow: false },
|
||||
C_ALLOWED: { enabled: true },
|
||||
C_DENIED: { enabled: false },
|
||||
},
|
||||
});
|
||||
// Explicitly allowed channel
|
||||
|
|
|
|||
|
|
@ -695,7 +695,7 @@ describe("Slack native command argument menus", () => {
|
|||
|
||||
function createPolicyHarness(overrides?: {
|
||||
groupPolicy?: "open" | "allowlist";
|
||||
channelsConfig?: Record<string, { allow?: boolean; requireMention?: boolean }>;
|
||||
channelsConfig?: Record<string, { enabled?: boolean; requireMention?: boolean }>;
|
||||
channelId?: string;
|
||||
channelName?: string;
|
||||
allowFrom?: string[];
|
||||
|
|
@ -864,7 +864,7 @@ describe("slack slash commands channel policy", () => {
|
|||
it("blocks explicitly denied channels when groupPolicy is open", async () => {
|
||||
const harness = createPolicyHarness({
|
||||
groupPolicy: "open",
|
||||
channelsConfig: { C_DENIED: { allow: false } },
|
||||
channelsConfig: { C_DENIED: { enabled: false } },
|
||||
channelId: "C_DENIED",
|
||||
channelName: "denied",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ export function createSlackSetupWizardBase(handlers: {
|
|||
resolveSlackAccount({ cfg, accountId }).config.groupPolicy ?? "allowlist",
|
||||
currentEntries: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId: string }) =>
|
||||
Object.entries(resolveSlackAccount({ cfg, accountId }).config.channels ?? {})
|
||||
.filter(([, value]) => value?.allow !== false && value?.enabled !== false)
|
||||
.filter(([, value]) => value?.enabled !== false)
|
||||
.map(([key]) => key),
|
||||
updatePrompt: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId: string }) =>
|
||||
Boolean(resolveSlackAccount({ cfg, accountId }).config.channels),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { setSlackChannelAllowlist } from "./shared.js";
|
||||
|
||||
describe("setSlackChannelAllowlist", () => {
|
||||
it("writes canonical enabled entries for setup-generated channel allowlists", () => {
|
||||
const result = setSlackChannelAllowlist(
|
||||
{
|
||||
channels: {
|
||||
slack: {
|
||||
accounts: {
|
||||
work: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"work",
|
||||
["C123", "C456"],
|
||||
);
|
||||
|
||||
expect(result.channels?.slack?.accounts?.work?.channels).toEqual({
|
||||
C123: { enabled: true },
|
||||
C456: { enabled: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -119,7 +119,7 @@ export function setSlackChannelAllowlist(
|
|||
accountId: string,
|
||||
channelKeys: string[],
|
||||
): OpenClawConfig {
|
||||
const channels = Object.fromEntries(channelKeys.map((key) => [key, { allow: true }]));
|
||||
const channels = Object.fromEntries(channelKeys.map((key) => [key, { enabled: true }]));
|
||||
return patchChannelConfigForAccount({
|
||||
cfg,
|
||||
channel: SLACK_CHANNEL,
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ describe("configureChannelAccessWithAllowlist", () => {
|
|||
...params.cfg.channels,
|
||||
slack: {
|
||||
...params.cfg.channels?.slack,
|
||||
channels: Object.fromEntries(params.resolved.map((id) => [id, { allow: true }])),
|
||||
channels: Object.fromEntries(params.resolved.map((id) => [id, { enabled: true }])),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -170,8 +170,8 @@ describe("configureChannelAccessWithAllowlist", () => {
|
|||
|
||||
expect(calls).toEqual(["resolve", "setPolicy", "apply"]);
|
||||
expect(next.channels?.slack?.channels).toEqual({
|
||||
C1: { allow: true },
|
||||
C2: { allow: true },
|
||||
C1: { enabled: true },
|
||||
C2: { enabled: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -708,6 +708,124 @@ describe("doctor config flow", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("warns clearly about legacy nested channel allow aliases and points to doctor --fix", async () => {
|
||||
const noteSpy = vi.spyOn(noteModule, "note").mockImplementation(() => {});
|
||||
try {
|
||||
await runDoctorConfigWithInput({
|
||||
config: {
|
||||
channels: {
|
||||
slack: {
|
||||
channels: {
|
||||
ops: {
|
||||
allow: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
googlechat: {
|
||||
groups: {
|
||||
"spaces/aaa": {
|
||||
allow: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
discord: {
|
||||
guilds: {
|
||||
"100": {
|
||||
channels: {
|
||||
general: {
|
||||
allow: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
run: loadAndMaybeMigrateDoctorConfig,
|
||||
});
|
||||
|
||||
expect(
|
||||
noteSpy.mock.calls.some(
|
||||
([message, title]) =>
|
||||
title === "Legacy config keys detected" &&
|
||||
String(message).includes("channels.slack:") &&
|
||||
String(message).includes("channels.slack.channels.<id>.allow is legacy"),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
noteSpy.mock.calls.some(
|
||||
([message, title]) =>
|
||||
title === "Legacy config keys detected" &&
|
||||
String(message).includes("channels.googlechat:") &&
|
||||
String(message).includes("channels.googlechat.groups.<id>.allow is legacy"),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
noteSpy.mock.calls.some(
|
||||
([message, title]) =>
|
||||
title === "Legacy config keys detected" &&
|
||||
String(message).includes("channels.discord:") &&
|
||||
String(message).includes("channels.discord.guilds.<id>.channels.<id>.allow is legacy"),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
noteSpy.mock.calls.some(
|
||||
([message, title]) =>
|
||||
title === "Doctor" &&
|
||||
String(message).includes('Run "openclaw doctor --fix" to migrate legacy config keys.'),
|
||||
),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
noteSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("repairs legacy nested channel allow aliases on repair", async () => {
|
||||
const result = await runDoctorConfigWithInput({
|
||||
repair: true,
|
||||
config: {
|
||||
channels: {
|
||||
slack: {
|
||||
channels: {
|
||||
ops: {
|
||||
allow: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
googlechat: {
|
||||
groups: {
|
||||
"spaces/aaa": {
|
||||
allow: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
discord: {
|
||||
guilds: {
|
||||
"100": {
|
||||
channels: {
|
||||
general: {
|
||||
allow: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
run: loadAndMaybeMigrateDoctorConfig,
|
||||
});
|
||||
|
||||
expect(result.cfg.channels?.slack?.channels?.ops).toEqual({
|
||||
enabled: false,
|
||||
});
|
||||
expect(result.cfg.channels?.googlechat?.groups?.["spaces/aaa"]).toEqual({
|
||||
enabled: false,
|
||||
});
|
||||
expect(result.cfg.channels?.discord?.guilds?.["100"]?.channels?.general).toEqual({
|
||||
enabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("sanitizes config-derived doctor warnings and changes before logging", async () => {
|
||||
const noteSpy = vi.spyOn(noteModule, "note").mockImplementation(() => {});
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,69 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
|||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
actions: {
|
||||
type: "object",
|
||||
properties: {
|
||||
reactions: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
edit: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
unsend: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
reply: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
sendWithEffect: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
renameGroup: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
setGroupIcon: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
addParticipant: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
removeParticipant: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
leaveGroup: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
sendAttachment: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
required: [
|
||||
"reactions",
|
||||
"edit",
|
||||
"unsend",
|
||||
"reply",
|
||||
"sendWithEffect",
|
||||
"renameGroup",
|
||||
"setGroupIcon",
|
||||
"addParticipant",
|
||||
"removeParticipant",
|
||||
"leaveGroup",
|
||||
"sendAttachment",
|
||||
],
|
||||
additionalProperties: false,
|
||||
},
|
||||
serverUrl: {
|
||||
type: "string",
|
||||
},
|
||||
|
|
@ -234,6 +297,69 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
|||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
actions: {
|
||||
type: "object",
|
||||
properties: {
|
||||
reactions: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
edit: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
unsend: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
reply: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
sendWithEffect: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
renameGroup: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
setGroupIcon: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
addParticipant: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
removeParticipant: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
leaveGroup: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
sendAttachment: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
required: [
|
||||
"reactions",
|
||||
"edit",
|
||||
"unsend",
|
||||
"reply",
|
||||
"sendWithEffect",
|
||||
"renameGroup",
|
||||
"setGroupIcon",
|
||||
"addParticipant",
|
||||
"removeParticipant",
|
||||
"leaveGroup",
|
||||
"sendAttachment",
|
||||
],
|
||||
additionalProperties: false,
|
||||
},
|
||||
serverUrl: {
|
||||
type: "string",
|
||||
},
|
||||
|
|
@ -428,69 +554,6 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
|||
defaultAccount: {
|
||||
type: "string",
|
||||
},
|
||||
actions: {
|
||||
type: "object",
|
||||
properties: {
|
||||
reactions: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
edit: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
unsend: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
reply: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
sendWithEffect: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
renameGroup: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
setGroupIcon: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
addParticipant: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
removeParticipant: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
leaveGroup: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
sendAttachment: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
required: [
|
||||
"reactions",
|
||||
"edit",
|
||||
"unsend",
|
||||
"reply",
|
||||
"sendWithEffect",
|
||||
"renameGroup",
|
||||
"setGroupIcon",
|
||||
"addParticipant",
|
||||
"removeParticipant",
|
||||
"leaveGroup",
|
||||
"sendAttachment",
|
||||
],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
required: ["enrichGroupParticipantsFromContacts"],
|
||||
additionalProperties: false,
|
||||
|
|
@ -1006,9 +1069,6 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
|||
additionalProperties: {
|
||||
type: "object",
|
||||
properties: {
|
||||
allow: {
|
||||
type: "boolean",
|
||||
},
|
||||
requireMention: {
|
||||
type: "boolean",
|
||||
},
|
||||
|
|
@ -2151,9 +2211,6 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
|||
additionalProperties: {
|
||||
type: "object",
|
||||
properties: {
|
||||
allow: {
|
||||
type: "boolean",
|
||||
},
|
||||
requireMention: {
|
||||
type: "boolean",
|
||||
},
|
||||
|
|
@ -4180,9 +4237,6 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
|||
enabled: {
|
||||
type: "boolean",
|
||||
},
|
||||
allow: {
|
||||
type: "boolean",
|
||||
},
|
||||
requireMention: {
|
||||
type: "boolean",
|
||||
},
|
||||
|
|
@ -4562,9 +4616,6 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
|||
enabled: {
|
||||
type: "boolean",
|
||||
},
|
||||
allow: {
|
||||
type: "boolean",
|
||||
},
|
||||
requireMention: {
|
||||
type: "boolean",
|
||||
},
|
||||
|
|
@ -10598,9 +10649,6 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
|||
enabled: {
|
||||
type: "boolean",
|
||||
},
|
||||
allow: {
|
||||
type: "boolean",
|
||||
},
|
||||
requireMention: {
|
||||
type: "boolean",
|
||||
},
|
||||
|
|
@ -11437,9 +11485,6 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
|||
enabled: {
|
||||
type: "boolean",
|
||||
},
|
||||
allow: {
|
||||
type: "boolean",
|
||||
},
|
||||
requireMention: {
|
||||
type: "boolean",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -791,6 +791,116 @@ describe("config strict validation", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("accepts legacy nested channel allow aliases via auto-migration and reports legacyIssues", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
await writeOpenClawConfig(home, {
|
||||
channels: {
|
||||
slack: {
|
||||
channels: {
|
||||
ops: {
|
||||
allow: false,
|
||||
},
|
||||
},
|
||||
accounts: {
|
||||
work: {
|
||||
channels: {
|
||||
general: {
|
||||
allow: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
googlechat: {
|
||||
groups: {
|
||||
"spaces/aaa": {
|
||||
allow: false,
|
||||
},
|
||||
},
|
||||
accounts: {
|
||||
work: {
|
||||
groups: {
|
||||
"spaces/bbb": {
|
||||
allow: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
discord: {
|
||||
guilds: {
|
||||
"100": {
|
||||
channels: {
|
||||
general: {
|
||||
allow: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
accounts: {
|
||||
work: {
|
||||
guilds: {
|
||||
"200": {
|
||||
channels: {
|
||||
help: {
|
||||
allow: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const snap = await readConfigFileSnapshot();
|
||||
|
||||
expect(snap.valid).toBe(true);
|
||||
expect(snap.legacyIssues.some((issue) => issue.path === "channels.slack")).toBe(true);
|
||||
expect(snap.legacyIssues.some((issue) => issue.path === "channels.slack.accounts")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(snap.legacyIssues.some((issue) => issue.path === "channels.googlechat")).toBe(true);
|
||||
expect(snap.legacyIssues.some((issue) => issue.path === "channels.googlechat.accounts")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(snap.legacyIssues.some((issue) => issue.path === "channels.discord")).toBe(true);
|
||||
expect(snap.legacyIssues.some((issue) => issue.path === "channels.discord.accounts")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(snap.sourceConfig.channels?.slack?.channels?.ops).toMatchObject({
|
||||
enabled: false,
|
||||
});
|
||||
expect(snap.sourceConfig.channels?.googlechat?.groups?.["spaces/aaa"]).toMatchObject({
|
||||
enabled: false,
|
||||
});
|
||||
expect(snap.sourceConfig.channels?.discord?.guilds?.["100"]?.channels?.general).toMatchObject(
|
||||
{
|
||||
enabled: false,
|
||||
},
|
||||
);
|
||||
expect(
|
||||
(snap.sourceConfig.channels?.slack?.channels?.ops as Record<string, unknown> | undefined)
|
||||
?.allow,
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
(
|
||||
snap.sourceConfig.channels?.googlechat?.groups?.["spaces/aaa"] as
|
||||
| Record<string, unknown>
|
||||
| undefined
|
||||
)?.allow,
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
(
|
||||
snap.sourceConfig.channels?.discord?.guilds?.["100"]?.channels?.general as
|
||||
| Record<string, unknown>
|
||||
| undefined
|
||||
)?.allow,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts telegram groupMentionsOnly via auto-migration and reports legacyIssues", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
await writeOpenClawConfig(home, {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ describe("config discord", () => {
|
|||
requireMention: false,
|
||||
users: ["steipete"],
|
||||
channels: {
|
||||
general: { allow: true, autoThread: true },
|
||||
general: { enabled: true, autoThread: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -53,7 +53,7 @@ describe("config discord", () => {
|
|||
expect(cfg.channels?.discord?.actions?.stickerUploads).toBe(false);
|
||||
expect(cfg.channels?.discord?.actions?.channels).toBe(true);
|
||||
expect(cfg.channels?.discord?.guilds?.["123"]?.slug).toBe("friends-of-openclaw");
|
||||
expect(cfg.channels?.discord?.guilds?.["123"]?.channels?.general?.allow).toBe(true);
|
||||
expect(cfg.channels?.discord?.guilds?.["123"]?.channels?.general?.enabled).toBe(true);
|
||||
expect(cfg.channels?.discord?.guilds?.["123"]?.channels?.general?.autoThread).toBe(true);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,12 @@ import type { OpenClawConfig } from "./types.js";
|
|||
// AJV JSON Schema carries a `default` value. This lets the #56772 regression
|
||||
// test exercise the exact code path that caused the bug: AJV injecting
|
||||
// defaults during the write-back validation pass.
|
||||
const mockLoadPluginManifestRegistry = vi.hoisted(() => vi.fn());
|
||||
const mockLoadPluginManifestRegistry = vi.hoisted(() =>
|
||||
vi.fn(() => ({
|
||||
diagnostics: [],
|
||||
plugins: [],
|
||||
})),
|
||||
);
|
||||
|
||||
vi.mock("../plugins/manifest-registry.js", () => ({
|
||||
loadPluginManifestRegistry: (...args: unknown[]) => mockLoadPluginManifestRegistry(...args),
|
||||
|
|
@ -734,4 +739,83 @@ describe("config io write", () => {
|
|||
expect(last.watchCommand).toBe("gateway --force");
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts unrelated writes when the file still contains legacy nested allow aliases", async () => {
|
||||
await withSuiteHome(async (home) => {
|
||||
const { configPath, io, snapshot } = await writeConfigAndCreateIo({
|
||||
home,
|
||||
initialConfig: {
|
||||
channels: {
|
||||
slack: {
|
||||
channels: {
|
||||
ops: {
|
||||
allow: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
googlechat: {
|
||||
groups: {
|
||||
"spaces/aaa": {
|
||||
allow: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
discord: {
|
||||
guilds: {
|
||||
"100": {
|
||||
channels: {
|
||||
general: {
|
||||
allow: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const next = structuredClone(snapshot.config);
|
||||
next.gateway = {
|
||||
...next.gateway,
|
||||
auth: { mode: "token" },
|
||||
};
|
||||
|
||||
await io.writeConfigFile(next);
|
||||
|
||||
const persisted = JSON.parse(await fs.readFile(configPath, "utf-8")) as {
|
||||
channels?: Record<string, unknown>;
|
||||
gateway?: Record<string, unknown>;
|
||||
};
|
||||
expect(persisted.gateway).toEqual({
|
||||
auth: { mode: "token" },
|
||||
});
|
||||
expect(
|
||||
(
|
||||
(persisted.channels?.slack as { channels?: Record<string, unknown> } | undefined)
|
||||
?.channels?.ops as Record<string, unknown> | undefined
|
||||
)?.enabled,
|
||||
).toBe(false);
|
||||
expect(
|
||||
(
|
||||
(persisted.channels?.googlechat as { groups?: Record<string, unknown> } | undefined)
|
||||
?.groups?.["spaces/aaa"] as Record<string, unknown> | undefined
|
||||
)?.enabled,
|
||||
).toBe(true);
|
||||
expect(
|
||||
(
|
||||
(
|
||||
(persisted.channels?.discord as { guilds?: Record<string, unknown> } | undefined)
|
||||
?.guilds?.["100"] as { channels?: Record<string, unknown> } | undefined
|
||||
)?.channels?.general as Record<string, unknown> | undefined
|
||||
)?.enabled,
|
||||
).toBe(false);
|
||||
expect(
|
||||
(
|
||||
(persisted.channels?.slack as { channels?: Record<string, unknown> } | undefined)
|
||||
?.channels?.ops as Record<string, unknown> | undefined
|
||||
)?.allow,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { migrateLegacyConfig } from "./legacy-migrate.js";
|
||||
import { validateConfigObjectWithPlugins } from "./validation.js";
|
||||
import {
|
||||
validateConfigObjectRawWithPlugins,
|
||||
validateConfigObjectWithPlugins,
|
||||
} from "./validation.js";
|
||||
|
||||
describe("legacy migrate audio transcription", () => {
|
||||
it("does not rewrite removed routing.transcribeAudio migrations", () => {
|
||||
|
|
@ -508,6 +511,177 @@ describe("legacy migrate channel streaming aliases", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("legacy migrate nested channel enabled aliases", () => {
|
||||
it("accepts legacy allow aliases through with-plugins validation and normalizes them", () => {
|
||||
const raw = {
|
||||
channels: {
|
||||
slack: {
|
||||
channels: {
|
||||
ops: {
|
||||
allow: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
googlechat: {
|
||||
groups: {
|
||||
"spaces/aaa": {
|
||||
allow: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
discord: {
|
||||
guilds: {
|
||||
"100": {
|
||||
channels: {
|
||||
general: {
|
||||
allow: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const validated = validateConfigObjectWithPlugins(raw);
|
||||
expect(validated.ok).toBe(true);
|
||||
if (!validated.ok) {
|
||||
return;
|
||||
}
|
||||
expect(validated.config.channels?.slack?.channels?.ops).toEqual({
|
||||
enabled: false,
|
||||
});
|
||||
expect(validated.config.channels?.googlechat?.groups?.["spaces/aaa"]).toEqual({
|
||||
enabled: true,
|
||||
});
|
||||
expect(validated.config.channels?.discord?.guilds?.["100"]?.channels?.general).toEqual({
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const rawValidated = validateConfigObjectRawWithPlugins(raw);
|
||||
expect(rawValidated.ok).toBe(true);
|
||||
if (!rawValidated.ok) {
|
||||
return;
|
||||
}
|
||||
expect(rawValidated.config.channels?.slack?.channels?.ops).toEqual({
|
||||
enabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("moves legacy allow toggles into enabled for slack, googlechat, and discord", () => {
|
||||
const res = migrateLegacyConfig({
|
||||
channels: {
|
||||
slack: {
|
||||
channels: {
|
||||
ops: {
|
||||
allow: false,
|
||||
},
|
||||
},
|
||||
accounts: {
|
||||
work: {
|
||||
channels: {
|
||||
general: {
|
||||
allow: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
googlechat: {
|
||||
groups: {
|
||||
"spaces/aaa": {
|
||||
allow: false,
|
||||
},
|
||||
},
|
||||
accounts: {
|
||||
work: {
|
||||
groups: {
|
||||
"spaces/bbb": {
|
||||
allow: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
discord: {
|
||||
guilds: {
|
||||
"100": {
|
||||
channels: {
|
||||
general: {
|
||||
allow: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
accounts: {
|
||||
work: {
|
||||
guilds: {
|
||||
"200": {
|
||||
channels: {
|
||||
help: {
|
||||
allow: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.changes).toContain(
|
||||
"Moved channels.slack.channels.ops.allow → channels.slack.channels.ops.enabled.",
|
||||
);
|
||||
expect(res.changes).toContain(
|
||||
"Moved channels.slack.accounts.work.channels.general.allow → channels.slack.accounts.work.channels.general.enabled.",
|
||||
);
|
||||
expect(res.changes).toContain(
|
||||
"Moved channels.googlechat.groups.spaces/aaa.allow → channels.googlechat.groups.spaces/aaa.enabled.",
|
||||
);
|
||||
expect(res.changes).toContain(
|
||||
"Moved channels.googlechat.accounts.work.groups.spaces/bbb.allow → channels.googlechat.accounts.work.groups.spaces/bbb.enabled.",
|
||||
);
|
||||
expect(res.changes).toContain(
|
||||
"Moved channels.discord.guilds.100.channels.general.allow → channels.discord.guilds.100.channels.general.enabled.",
|
||||
);
|
||||
expect(res.changes).toContain(
|
||||
"Moved channels.discord.accounts.work.guilds.200.channels.help.allow → channels.discord.accounts.work.guilds.200.channels.help.enabled.",
|
||||
);
|
||||
expect(res.config?.channels?.slack?.channels?.ops).toEqual({
|
||||
enabled: false,
|
||||
});
|
||||
expect(res.config?.channels?.googlechat?.groups?.["spaces/aaa"]).toEqual({
|
||||
enabled: false,
|
||||
});
|
||||
expect(res.config?.channels?.discord?.guilds?.["100"]?.channels?.general).toEqual({
|
||||
enabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("drops legacy allow when enabled is already set", () => {
|
||||
const res = migrateLegacyConfig({
|
||||
channels: {
|
||||
slack: {
|
||||
channels: {
|
||||
ops: {
|
||||
allow: true,
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.changes).toContain(
|
||||
"Removed channels.slack.channels.ops.allow (channels.slack.channels.ops.enabled already set).",
|
||||
);
|
||||
expect(res.config?.channels?.slack?.channels?.ops).toEqual({
|
||||
enabled: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("legacy migrate x_search auth", () => {
|
||||
it("moves only legacy x_search auth into plugin-owned xai config", () => {
|
||||
const res = migrateLegacyConfig({
|
||||
|
|
|
|||
|
|
@ -5,10 +5,158 @@ import {
|
|||
type LegacyConfigRule,
|
||||
} from "./legacy.shared.js";
|
||||
|
||||
type StreamingMode = "off" | "partial" | "block" | "progress";
|
||||
type DiscordPreviewStreamMode = "off" | "partial" | "block";
|
||||
type TelegramPreviewStreamMode = "off" | "partial" | "block";
|
||||
type SlackLegacyDraftStreamMode = "replace" | "status_final" | "append";
|
||||
|
||||
function hasOwnKey(target: Record<string, unknown>, key: string): boolean {
|
||||
return Object.prototype.hasOwnProperty.call(target, key);
|
||||
}
|
||||
|
||||
function normalizeStreamingMode(value: unknown): string | null {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return normalized || null;
|
||||
}
|
||||
|
||||
function parseStreamingMode(value: unknown): StreamingMode | null {
|
||||
const normalized = normalizeStreamingMode(value);
|
||||
if (
|
||||
normalized === "off" ||
|
||||
normalized === "partial" ||
|
||||
normalized === "block" ||
|
||||
normalized === "progress"
|
||||
) {
|
||||
return normalized;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseDiscordPreviewStreamMode(value: unknown): DiscordPreviewStreamMode | null {
|
||||
const parsed = parseStreamingMode(value);
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
return parsed === "progress" ? "partial" : parsed;
|
||||
}
|
||||
|
||||
function parseTelegramPreviewStreamMode(value: unknown): TelegramPreviewStreamMode | null {
|
||||
const parsed = parseStreamingMode(value);
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
return parsed === "progress" ? "partial" : parsed;
|
||||
}
|
||||
|
||||
function parseSlackLegacyDraftStreamMode(value: unknown): SlackLegacyDraftStreamMode | null {
|
||||
const normalized = normalizeStreamingMode(value);
|
||||
if (normalized === "replace" || normalized === "status_final" || normalized === "append") {
|
||||
return normalized;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function mapSlackLegacyDraftStreamModeToStreaming(mode: SlackLegacyDraftStreamMode): StreamingMode {
|
||||
if (mode === "append") {
|
||||
return "block";
|
||||
}
|
||||
if (mode === "status_final") {
|
||||
return "progress";
|
||||
}
|
||||
return "partial";
|
||||
}
|
||||
|
||||
function resolveTelegramPreviewStreamMode(
|
||||
params: {
|
||||
streamMode?: unknown;
|
||||
streaming?: unknown;
|
||||
} = {},
|
||||
): TelegramPreviewStreamMode {
|
||||
const parsedStreaming = parseStreamingMode(params.streaming);
|
||||
if (parsedStreaming) {
|
||||
return parsedStreaming === "progress" ? "partial" : parsedStreaming;
|
||||
}
|
||||
|
||||
const legacy = parseTelegramPreviewStreamMode(params.streamMode);
|
||||
if (legacy) {
|
||||
return legacy;
|
||||
}
|
||||
if (typeof params.streaming === "boolean") {
|
||||
return params.streaming ? "partial" : "off";
|
||||
}
|
||||
return "partial";
|
||||
}
|
||||
|
||||
function resolveDiscordPreviewStreamMode(
|
||||
params: {
|
||||
streamMode?: unknown;
|
||||
streaming?: unknown;
|
||||
} = {},
|
||||
): DiscordPreviewStreamMode {
|
||||
const parsedStreaming = parseDiscordPreviewStreamMode(params.streaming);
|
||||
if (parsedStreaming) {
|
||||
return parsedStreaming;
|
||||
}
|
||||
|
||||
const legacy = parseDiscordPreviewStreamMode(params.streamMode);
|
||||
if (legacy) {
|
||||
return legacy;
|
||||
}
|
||||
if (typeof params.streaming === "boolean") {
|
||||
return params.streaming ? "partial" : "off";
|
||||
}
|
||||
return "off";
|
||||
}
|
||||
|
||||
function resolveSlackStreamingMode(
|
||||
params: {
|
||||
streamMode?: unknown;
|
||||
streaming?: unknown;
|
||||
} = {},
|
||||
): StreamingMode {
|
||||
const parsedStreaming = parseStreamingMode(params.streaming);
|
||||
if (parsedStreaming) {
|
||||
return parsedStreaming;
|
||||
}
|
||||
const legacyStreamMode = parseSlackLegacyDraftStreamMode(params.streamMode);
|
||||
if (legacyStreamMode) {
|
||||
return mapSlackLegacyDraftStreamModeToStreaming(legacyStreamMode);
|
||||
}
|
||||
if (typeof params.streaming === "boolean") {
|
||||
return params.streaming ? "partial" : "off";
|
||||
}
|
||||
return "partial";
|
||||
}
|
||||
|
||||
function resolveSlackNativeStreaming(
|
||||
params: {
|
||||
nativeStreaming?: unknown;
|
||||
streaming?: unknown;
|
||||
} = {},
|
||||
): boolean {
|
||||
if (typeof params.nativeStreaming === "boolean") {
|
||||
return params.nativeStreaming;
|
||||
}
|
||||
if (typeof params.streaming === "boolean") {
|
||||
return params.streaming;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function formatSlackStreamModeMigrationMessage(pathPrefix: string, resolvedStreaming: string) {
|
||||
return `Moved ${pathPrefix}.streamMode → ${pathPrefix}.streaming (${resolvedStreaming}).`;
|
||||
}
|
||||
|
||||
function formatSlackStreamingBooleanMigrationMessage(
|
||||
pathPrefix: string,
|
||||
resolvedNativeStreaming: boolean,
|
||||
) {
|
||||
return `Moved ${pathPrefix}.streaming (boolean) → ${pathPrefix}.nativeStreaming (${resolvedNativeStreaming}).`;
|
||||
}
|
||||
|
||||
function hasLegacyThreadBindingTtl(value: unknown): boolean {
|
||||
const threadBindings = getRecord(value);
|
||||
return Boolean(threadBindings && hasOwnKey(threadBindings, "ttlHours"));
|
||||
|
|
@ -70,6 +218,104 @@ function hasLegacyThreadBindingTtlInAnyChannel(value: unknown): boolean {
|
|||
});
|
||||
}
|
||||
|
||||
function hasLegacyTelegramStreamingKeys(value: unknown): boolean {
|
||||
const entry = getRecord(value);
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
return entry.streamMode !== undefined;
|
||||
}
|
||||
|
||||
function hasLegacyDiscordStreamingKeys(value: unknown): boolean {
|
||||
const entry = getRecord(value);
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
return entry.streamMode !== undefined || typeof entry.streaming === "boolean";
|
||||
}
|
||||
|
||||
function hasLegacySlackStreamingKeys(value: unknown): boolean {
|
||||
const entry = getRecord(value);
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
return entry.streamMode !== undefined || typeof entry.streaming === "boolean";
|
||||
}
|
||||
|
||||
function hasLegacyKeysInAccounts(
|
||||
value: unknown,
|
||||
matchEntry: (entry: Record<string, unknown>) => boolean,
|
||||
): boolean {
|
||||
const accounts = getRecord(value);
|
||||
if (!accounts) {
|
||||
return false;
|
||||
}
|
||||
return Object.values(accounts).some((entry) => matchEntry(getRecord(entry) ?? {}));
|
||||
}
|
||||
|
||||
function hasLegacyAllowAlias(entry: Record<string, unknown>): boolean {
|
||||
return hasOwnKey(entry, "allow");
|
||||
}
|
||||
|
||||
function migrateAllowAliasForPath(params: {
|
||||
entry: Record<string, unknown>;
|
||||
pathPrefix: string;
|
||||
changes: string[];
|
||||
}): boolean {
|
||||
if (!hasLegacyAllowAlias(params.entry)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const legacyAllow = params.entry.allow;
|
||||
const hadEnabled = params.entry.enabled !== undefined;
|
||||
if (!hadEnabled) {
|
||||
params.entry.enabled = legacyAllow;
|
||||
}
|
||||
delete params.entry.allow;
|
||||
|
||||
if (hadEnabled) {
|
||||
params.changes.push(
|
||||
`Removed ${params.pathPrefix}.allow (${params.pathPrefix}.enabled already set).`,
|
||||
);
|
||||
} else {
|
||||
params.changes.push(`Moved ${params.pathPrefix}.allow → ${params.pathPrefix}.enabled.`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function hasLegacySlackChannelAllowAlias(value: unknown): boolean {
|
||||
const entry = getRecord(value);
|
||||
const channels = getRecord(entry?.channels);
|
||||
if (!channels) {
|
||||
return false;
|
||||
}
|
||||
return Object.values(channels).some((channel) => hasLegacyAllowAlias(getRecord(channel) ?? {}));
|
||||
}
|
||||
|
||||
function hasLegacyGoogleChatGroupAllowAlias(value: unknown): boolean {
|
||||
const entry = getRecord(value);
|
||||
const groups = getRecord(entry?.groups);
|
||||
if (!groups) {
|
||||
return false;
|
||||
}
|
||||
return Object.values(groups).some((group) => hasLegacyAllowAlias(getRecord(group) ?? {}));
|
||||
}
|
||||
|
||||
function hasLegacyDiscordGuildChannelAllowAlias(value: unknown): boolean {
|
||||
const entry = getRecord(value);
|
||||
const guilds = getRecord(entry?.guilds);
|
||||
if (!guilds) {
|
||||
return false;
|
||||
}
|
||||
return Object.values(guilds).some((guildValue) => {
|
||||
const channels = getRecord(getRecord(guildValue)?.channels);
|
||||
if (!channels) {
|
||||
return false;
|
||||
}
|
||||
return Object.values(channels).some((channel) => hasLegacyAllowAlias(getRecord(channel) ?? {}));
|
||||
});
|
||||
}
|
||||
|
||||
const THREAD_BINDING_RULES: LegacyConfigRule[] = [
|
||||
{
|
||||
path: ["session", "threadBindings"],
|
||||
|
|
@ -85,6 +331,84 @@ const THREAD_BINDING_RULES: LegacyConfigRule[] = [
|
|||
},
|
||||
];
|
||||
|
||||
const CHANNEL_STREAMING_RULES: LegacyConfigRule[] = [
|
||||
{
|
||||
path: ["channels", "telegram"],
|
||||
message:
|
||||
"channels.telegram.streamMode is legacy; use channels.telegram.streaming instead (auto-migrated on load).",
|
||||
match: (value) => hasLegacyTelegramStreamingKeys(value),
|
||||
},
|
||||
{
|
||||
path: ["channels", "telegram", "accounts"],
|
||||
message:
|
||||
"channels.telegram.accounts.<id>.streamMode is legacy; use channels.telegram.accounts.<id>.streaming instead (auto-migrated on load).",
|
||||
match: (value) => hasLegacyKeysInAccounts(value, hasLegacyTelegramStreamingKeys),
|
||||
},
|
||||
{
|
||||
path: ["channels", "discord"],
|
||||
message:
|
||||
"channels.discord.streamMode and boolean channels.discord.streaming are legacy; use channels.discord.streaming with enum values instead (auto-migrated on load).",
|
||||
match: (value) => hasLegacyDiscordStreamingKeys(value),
|
||||
},
|
||||
{
|
||||
path: ["channels", "discord", "accounts"],
|
||||
message:
|
||||
"channels.discord.accounts.<id>.streamMode and boolean channels.discord.accounts.<id>.streaming are legacy; use channels.discord.accounts.<id>.streaming with enum values instead (auto-migrated on load).",
|
||||
match: (value) => hasLegacyKeysInAccounts(value, hasLegacyDiscordStreamingKeys),
|
||||
},
|
||||
{
|
||||
path: ["channels", "slack"],
|
||||
message:
|
||||
"channels.slack.streamMode and boolean channels.slack.streaming are legacy; use channels.slack.streaming with enum values instead (auto-migrated on load).",
|
||||
match: (value) => hasLegacySlackStreamingKeys(value),
|
||||
},
|
||||
{
|
||||
path: ["channels", "slack", "accounts"],
|
||||
message:
|
||||
"channels.slack.accounts.<id>.streamMode and boolean channels.slack.accounts.<id>.streaming are legacy; use channels.slack.accounts.<id>.streaming with enum values instead (auto-migrated on load).",
|
||||
match: (value) => hasLegacyKeysInAccounts(value, hasLegacySlackStreamingKeys),
|
||||
},
|
||||
];
|
||||
|
||||
const CHANNEL_ENABLED_ALIAS_RULES: LegacyConfigRule[] = [
|
||||
{
|
||||
path: ["channels", "slack"],
|
||||
message:
|
||||
"channels.slack.channels.<id>.allow is legacy; use channels.slack.channels.<id>.enabled instead (auto-migrated on load).",
|
||||
match: (value) => hasLegacySlackChannelAllowAlias(value),
|
||||
},
|
||||
{
|
||||
path: ["channels", "slack", "accounts"],
|
||||
message:
|
||||
"channels.slack.accounts.<id>.channels.<id>.allow is legacy; use channels.slack.accounts.<id>.channels.<id>.enabled instead (auto-migrated on load).",
|
||||
match: (value) => hasLegacyKeysInAccounts(value, hasLegacySlackChannelAllowAlias),
|
||||
},
|
||||
{
|
||||
path: ["channels", "googlechat"],
|
||||
message:
|
||||
"channels.googlechat.groups.<id>.allow is legacy; use channels.googlechat.groups.<id>.enabled instead (auto-migrated on load).",
|
||||
match: (value) => hasLegacyGoogleChatGroupAllowAlias(value),
|
||||
},
|
||||
{
|
||||
path: ["channels", "googlechat", "accounts"],
|
||||
message:
|
||||
"channels.googlechat.accounts.<id>.groups.<id>.allow is legacy; use channels.googlechat.accounts.<id>.groups.<id>.enabled instead (auto-migrated on load).",
|
||||
match: (value) => hasLegacyKeysInAccounts(value, hasLegacyGoogleChatGroupAllowAlias),
|
||||
},
|
||||
{
|
||||
path: ["channels", "discord"],
|
||||
message:
|
||||
"channels.discord.guilds.<id>.channels.<id>.allow is legacy; use channels.discord.guilds.<id>.channels.<id>.enabled instead (auto-migrated on load).",
|
||||
match: (value) => hasLegacyDiscordGuildChannelAllowAlias(value),
|
||||
},
|
||||
{
|
||||
path: ["channels", "discord", "accounts"],
|
||||
message:
|
||||
"channels.discord.accounts.<id>.guilds.<id>.channels.<id>.allow is legacy; use channels.discord.accounts.<id>.guilds.<id>.channels.<id>.enabled instead (auto-migrated on load).",
|
||||
match: (value) => hasLegacyKeysInAccounts(value, hasLegacyDiscordGuildChannelAllowAlias),
|
||||
},
|
||||
];
|
||||
|
||||
export const LEGACY_CONFIG_MIGRATIONS_CHANNELS: LegacyConfigMigrationSpec[] = [
|
||||
defineLegacyConfigMigration({
|
||||
id: "thread-bindings.ttlHours->idleHours",
|
||||
|
|
@ -139,4 +463,224 @@ export const LEGACY_CONFIG_MIGRATIONS_CHANNELS: LegacyConfigMigrationSpec[] = [
|
|||
raw.channels = channels;
|
||||
},
|
||||
}),
|
||||
defineLegacyConfigMigration({
|
||||
id: "channels.streaming-keys->channels.streaming",
|
||||
describe:
|
||||
"Normalize legacy streaming keys to channels.<provider>.streaming (Telegram/Discord/Slack)",
|
||||
legacyRules: CHANNEL_STREAMING_RULES,
|
||||
apply: (raw, changes) => {
|
||||
const channels = getRecord(raw.channels);
|
||||
if (!channels) {
|
||||
return;
|
||||
}
|
||||
|
||||
const migrateProviderEntry = (params: {
|
||||
provider: "telegram" | "discord" | "slack";
|
||||
entry: Record<string, unknown>;
|
||||
pathPrefix: string;
|
||||
}) => {
|
||||
const migrateCommonStreamingMode = (
|
||||
resolveMode: (entry: Record<string, unknown>) => string,
|
||||
) => {
|
||||
const hasLegacyStreamMode = params.entry.streamMode !== undefined;
|
||||
const legacyStreaming = params.entry.streaming;
|
||||
if (!hasLegacyStreamMode && typeof legacyStreaming !== "boolean") {
|
||||
return false;
|
||||
}
|
||||
const resolved = resolveMode(params.entry);
|
||||
params.entry.streaming = resolved;
|
||||
if (hasLegacyStreamMode) {
|
||||
delete params.entry.streamMode;
|
||||
changes.push(
|
||||
`Moved ${params.pathPrefix}.streamMode → ${params.pathPrefix}.streaming (${resolved}).`,
|
||||
);
|
||||
}
|
||||
if (typeof legacyStreaming === "boolean") {
|
||||
changes.push(`Normalized ${params.pathPrefix}.streaming boolean → enum (${resolved}).`);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const hasLegacyStreamMode = params.entry.streamMode !== undefined;
|
||||
const legacyStreaming = params.entry.streaming;
|
||||
const legacyNativeStreaming = params.entry.nativeStreaming;
|
||||
|
||||
if (params.provider === "telegram") {
|
||||
migrateCommonStreamingMode(resolveTelegramPreviewStreamMode);
|
||||
return;
|
||||
}
|
||||
|
||||
if (params.provider === "discord") {
|
||||
migrateCommonStreamingMode(resolveDiscordPreviewStreamMode);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasLegacyStreamMode && typeof legacyStreaming !== "boolean") {
|
||||
return;
|
||||
}
|
||||
const resolvedStreaming = resolveSlackStreamingMode(params.entry);
|
||||
const resolvedNativeStreaming = resolveSlackNativeStreaming(params.entry);
|
||||
params.entry.streaming = resolvedStreaming;
|
||||
params.entry.nativeStreaming = resolvedNativeStreaming;
|
||||
if (hasLegacyStreamMode) {
|
||||
delete params.entry.streamMode;
|
||||
changes.push(formatSlackStreamModeMigrationMessage(params.pathPrefix, resolvedStreaming));
|
||||
}
|
||||
if (typeof legacyStreaming === "boolean") {
|
||||
changes.push(
|
||||
formatSlackStreamingBooleanMigrationMessage(params.pathPrefix, resolvedNativeStreaming),
|
||||
);
|
||||
} else if (typeof legacyNativeStreaming !== "boolean" && hasLegacyStreamMode) {
|
||||
changes.push(`Set ${params.pathPrefix}.nativeStreaming → ${resolvedNativeStreaming}.`);
|
||||
}
|
||||
};
|
||||
|
||||
const migrateProvider = (provider: "telegram" | "discord" | "slack") => {
|
||||
const providerEntry = getRecord(channels[provider]);
|
||||
if (!providerEntry) {
|
||||
return;
|
||||
}
|
||||
migrateProviderEntry({
|
||||
provider,
|
||||
entry: providerEntry,
|
||||
pathPrefix: `channels.${provider}`,
|
||||
});
|
||||
const accounts = getRecord(providerEntry.accounts);
|
||||
if (!accounts) {
|
||||
return;
|
||||
}
|
||||
for (const [accountId, accountValue] of Object.entries(accounts)) {
|
||||
const account = getRecord(accountValue);
|
||||
if (!account) {
|
||||
continue;
|
||||
}
|
||||
migrateProviderEntry({
|
||||
provider,
|
||||
entry: account,
|
||||
pathPrefix: `channels.${provider}.accounts.${accountId}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
migrateProvider("telegram");
|
||||
migrateProvider("discord");
|
||||
migrateProvider("slack");
|
||||
},
|
||||
}),
|
||||
defineLegacyConfigMigration({
|
||||
id: "channels.allow->channels.enabled",
|
||||
describe:
|
||||
"Normalize legacy nested channel allow toggles to enabled (Slack/Google Chat/Discord)",
|
||||
legacyRules: CHANNEL_ENABLED_ALIAS_RULES,
|
||||
apply: (raw, changes) => {
|
||||
const channels = getRecord(raw.channels);
|
||||
if (!channels) {
|
||||
return;
|
||||
}
|
||||
|
||||
const migrateSlackEntry = (entry: Record<string, unknown>, pathPrefix: string) => {
|
||||
const channelEntries = getRecord(entry.channels);
|
||||
if (!channelEntries) {
|
||||
return;
|
||||
}
|
||||
for (const [channelId, channelRaw] of Object.entries(channelEntries)) {
|
||||
const channel = getRecord(channelRaw);
|
||||
if (!channel) {
|
||||
continue;
|
||||
}
|
||||
migrateAllowAliasForPath({
|
||||
entry: channel,
|
||||
pathPrefix: `${pathPrefix}.channels.${channelId}`,
|
||||
changes,
|
||||
});
|
||||
channelEntries[channelId] = channel;
|
||||
}
|
||||
entry.channels = channelEntries;
|
||||
};
|
||||
|
||||
const migrateGoogleChatEntry = (entry: Record<string, unknown>, pathPrefix: string) => {
|
||||
const groups = getRecord(entry.groups);
|
||||
if (!groups) {
|
||||
return;
|
||||
}
|
||||
for (const [groupId, groupRaw] of Object.entries(groups)) {
|
||||
const group = getRecord(groupRaw);
|
||||
if (!group) {
|
||||
continue;
|
||||
}
|
||||
migrateAllowAliasForPath({
|
||||
entry: group,
|
||||
pathPrefix: `${pathPrefix}.groups.${groupId}`,
|
||||
changes,
|
||||
});
|
||||
groups[groupId] = group;
|
||||
}
|
||||
entry.groups = groups;
|
||||
};
|
||||
|
||||
const migrateDiscordEntry = (entry: Record<string, unknown>, pathPrefix: string) => {
|
||||
const guilds = getRecord(entry.guilds);
|
||||
if (!guilds) {
|
||||
return;
|
||||
}
|
||||
for (const [guildId, guildRaw] of Object.entries(guilds)) {
|
||||
const guild = getRecord(guildRaw);
|
||||
if (!guild) {
|
||||
continue;
|
||||
}
|
||||
const channelEntries = getRecord(guild.channels);
|
||||
if (!channelEntries) {
|
||||
guilds[guildId] = guild;
|
||||
continue;
|
||||
}
|
||||
for (const [channelId, channelRaw] of Object.entries(channelEntries)) {
|
||||
const channel = getRecord(channelRaw);
|
||||
if (!channel) {
|
||||
continue;
|
||||
}
|
||||
migrateAllowAliasForPath({
|
||||
entry: channel,
|
||||
pathPrefix: `${pathPrefix}.guilds.${guildId}.channels.${channelId}`,
|
||||
changes,
|
||||
});
|
||||
channelEntries[channelId] = channel;
|
||||
}
|
||||
guild.channels = channelEntries;
|
||||
guilds[guildId] = guild;
|
||||
}
|
||||
entry.guilds = guilds;
|
||||
};
|
||||
|
||||
const migrateProviderAccounts = (
|
||||
provider: "slack" | "googlechat" | "discord",
|
||||
migrateEntry: (entry: Record<string, unknown>, pathPrefix: string) => void,
|
||||
) => {
|
||||
const providerEntry = getRecord(channels[provider]);
|
||||
if (!providerEntry) {
|
||||
return;
|
||||
}
|
||||
migrateEntry(providerEntry, `channels.${provider}`);
|
||||
const accounts = getRecord(providerEntry.accounts);
|
||||
if (!accounts) {
|
||||
channels[provider] = providerEntry;
|
||||
return;
|
||||
}
|
||||
for (const [accountId, accountRaw] of Object.entries(accounts)) {
|
||||
const account = getRecord(accountRaw);
|
||||
if (!account) {
|
||||
continue;
|
||||
}
|
||||
migrateEntry(account, `channels.${provider}.accounts.${accountId}`);
|
||||
accounts[accountId] = account;
|
||||
}
|
||||
providerEntry.accounts = accounts;
|
||||
channels[provider] = providerEntry;
|
||||
};
|
||||
|
||||
migrateProviderAccounts("slack", migrateSlackEntry);
|
||||
migrateProviderAccounts("googlechat", migrateGoogleChatEntry);
|
||||
migrateProviderAccounts("discord", migrateDiscordEntry);
|
||||
raw.channels = channels;
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ export type DiscordDmConfig = {
|
|||
};
|
||||
|
||||
export type DiscordGuildChannelConfig = {
|
||||
allow?: boolean;
|
||||
requireMention?: boolean;
|
||||
/**
|
||||
* If true, drop messages that mention another user/role but not this one (not @everyone/@here).
|
||||
|
|
|
|||
|
|
@ -18,10 +18,8 @@ export type GoogleChatDmConfig = {
|
|||
};
|
||||
|
||||
export type GoogleChatGroupConfig = {
|
||||
/** If false, disable the bot in this space. (Alias for allow: false.) */
|
||||
/** If false, disable the bot in this space. */
|
||||
enabled?: boolean;
|
||||
/** Legacy allow toggle; prefer enabled. */
|
||||
allow?: boolean;
|
||||
/** Require mentioning the bot to trigger replies. */
|
||||
requireMention?: boolean;
|
||||
/** Allowlist of users that can invoke the bot in this space. */
|
||||
|
|
|
|||
|
|
@ -29,10 +29,8 @@ export type SlackDmConfig = {
|
|||
};
|
||||
|
||||
export type SlackChannelConfig = {
|
||||
/** If false, disable the bot in this channel. (Alias for allow: false.) */
|
||||
/** If false, disable the bot in this channel. */
|
||||
enabled?: boolean;
|
||||
/** Legacy channel allow toggle; prefer enabled. */
|
||||
allow?: boolean;
|
||||
/** Require mentioning the bot to trigger replies. */
|
||||
requireMention?: boolean;
|
||||
/** Optional tool policy overrides for this channel. */
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import { findDuplicateAgentDirs, formatDuplicateAgentDirError } from "./agent-di
|
|||
import { appendAllowedValuesHint, summarizeAllowedValues } from "./allowed-values.js";
|
||||
import { GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA } from "./bundled-channel-config-metadata.generated.js";
|
||||
import { collectChannelSchemaMetadata } from "./channel-config-metadata.js";
|
||||
import { findLegacyConfigIssues } from "./legacy.js";
|
||||
import { applyLegacyMigrations, findLegacyConfigIssues } from "./legacy.js";
|
||||
import { materializeRuntimeConfig } from "./materialize.js";
|
||||
import type { OpenClawConfig, ConfigValidationIssue } from "./types.js";
|
||||
import { coerceSecretRef } from "./types.secrets.js";
|
||||
|
|
@ -543,7 +543,13 @@ function validateConfigObjectWithPluginsBase(
|
|||
raw: unknown,
|
||||
opts: { applyDefaults: boolean; env?: NodeJS.ProcessEnv },
|
||||
): ValidateConfigWithPluginsResult {
|
||||
const base = opts.applyDefaults ? validateConfigObject(raw) : validateConfigObjectRaw(raw);
|
||||
// Config edit flows often start from raw parsed files that may still contain legacy keys.
|
||||
// Accept known legacy inputs here by normalizing them before schema/plugin validation.
|
||||
const migrated = applyLegacyMigrations(raw);
|
||||
const normalizedRaw = migrated.next ?? raw;
|
||||
const base = opts.applyDefaults
|
||||
? validateConfigObject(normalizedRaw)
|
||||
: validateConfigObjectRaw(normalizedRaw);
|
||||
if (!base.ok) {
|
||||
return { ok: false, issues: base.issues, warnings: [] };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -405,7 +405,6 @@ export const DiscordDmSchema = z
|
|||
|
||||
export const DiscordGuildChannelSchema = z
|
||||
.object({
|
||||
allow: z.boolean().optional(),
|
||||
requireMention: z.boolean().optional(),
|
||||
ignoreOtherMentions: z.boolean().optional(),
|
||||
tools: ToolPolicySchema,
|
||||
|
|
@ -757,7 +756,6 @@ export const GoogleChatDmSchema = z
|
|||
export const GoogleChatGroupSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
allow: z.boolean().optional(),
|
||||
requireMention: z.boolean().optional(),
|
||||
users: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
systemPrompt: z.string().optional(),
|
||||
|
|
@ -831,7 +829,6 @@ export const SlackDmSchema = z
|
|||
export const SlackChannelSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
allow: z.boolean().optional(),
|
||||
requireMention: z.boolean().optional(),
|
||||
tools: ToolPolicySchema,
|
||||
toolsBySender: ToolPolicyBySenderSchema,
|
||||
|
|
|
|||
|
|
@ -2309,7 +2309,7 @@ describe("security audit", () => {
|
|||
guilds: {
|
||||
"123": {
|
||||
channels: {
|
||||
general: { allow: true },
|
||||
general: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -2330,7 +2330,7 @@ describe("security audit", () => {
|
|||
guilds: {
|
||||
"123": {
|
||||
channels: {
|
||||
general: { allow: true },
|
||||
general: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -2373,7 +2373,7 @@ describe("security audit", () => {
|
|||
guilds: {
|
||||
"123": {
|
||||
channels: {
|
||||
general: { allow: true },
|
||||
general: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -2388,7 +2388,7 @@ describe("security audit", () => {
|
|||
guilds: {
|
||||
"123": {
|
||||
channels: {
|
||||
general: { allow: true },
|
||||
general: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -2957,7 +2957,7 @@ describe("security audit", () => {
|
|||
guilds: {
|
||||
"123": {
|
||||
channels: {
|
||||
general: { allow: true },
|
||||
general: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -3759,7 +3759,7 @@ describe("security audit", () => {
|
|||
guilds: {
|
||||
"1234567890": {
|
||||
channels: {
|
||||
"7777777777": { allow: true },
|
||||
"7777777777": { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue