Skip to content

Commit e1fec3c

Browse files
fix(config): remove core BlueBubbles schema (#78612)
* fix(config): remove core BlueBubbles schema * fix(config): preserve BlueBubbles dmPolicy validation * fix(config): type BlueBubbles account refinement * chore(plugin-sdk): refresh API baseline * chore(plugin-sdk): refresh API baseline --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
1 parent bf3b994 commit e1fec3c

10 files changed

Lines changed: 149 additions & 130 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ Docs: https://docs.openclaw.ai
147147

148148
- fix(active-memory): require admin scope for global toggles [AI]. (#78863) Thanks @pgondhi987.
149149
- Honor owner enforcement for native commands [AI]. (#78864) Thanks @pgondhi987.
150+
- Config/BlueBubbles: remove the duplicate core-owned BlueBubbles config schema while preserving plugin-owned `dmPolicy` allowFrom validation for channel and account configs. Fixes #69238. Thanks @omarshahine.
150151
- Tavily: resolve dedicated `tavily_search` and `tavily_extract` tool credentials from the active runtime config snapshot, so `exec` SecretRef-backed API keys do not reach the tools unresolved. (#78610) Thanks @VACInc.
151152
- Gateway/sessions: clear cached skills snapshots during `/new` and `sessions.reset` so long-lived channel sessions rebuild the visible skill list after skills change. (#78873) Thanks @Evizero.
152153
- fix(auto-reply): gate inline skill tool dispatch [AI]. (#78517) Thanks @pgondhi987.
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
a2a671522a9855594b011c86425911f2297e756c666f4ceb1cc453f613983725 plugin-sdk-api-baseline.json
2-
b939b18ab3cbd21f338fe2a7cd8783b612c0956e79831d66dae4a49f9ba85014 plugin-sdk-api-baseline.jsonl
1+
25d5e7d7532d99d06187813e1cf75d3192fb2bf330862bfd1d4d4a50c91c77c9 plugin-sdk-api-baseline.json
2+
9f9758f36694c4004ab8c2fb772815c91cc2035de872a0e53f053537b2156910 plugin-sdk-api-baseline.jsonl
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export { BlueBubblesChannelConfigSchema } from "./src/config-schema.js";
1+
export { BlueBubblesChannelConfigSchema, BlueBubblesConfigSchema } from "./src/config-schema.js";

extensions/bluebubbles/src/config-schema.ts

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
GroupPolicySchema,
77
MarkdownConfigSchema,
88
ToolPolicySchema,
9+
requireAllowlistAllowFrom,
10+
requireOpenAllowFrom,
911
} from "openclaw/plugin-sdk/channel-config-schema";
1012
import { z } from "openclaw/plugin-sdk/zod";
1113
import { bluebubblesChannelConfigUiHints } from "./config-ui-hints.js";
@@ -123,11 +125,57 @@ const bluebubblesAccountSchema = z
123125
}
124126
});
125127

128+
type BlueBubblesAccountConfig = z.infer<typeof bluebubblesAccountSchema>;
129+
type BlueBubblesConfigWithAccounts = BlueBubblesAccountConfig & {
130+
accounts?: Record<string, BlueBubblesAccountConfig>;
131+
};
132+
126133
export const BlueBubblesConfigSchema = buildCatchallMultiAccountChannelSchema(
127134
bluebubblesAccountSchema,
128-
).safeExtend({
129-
actions: bluebubblesActionSchema,
130-
});
135+
)
136+
.safeExtend({
137+
actions: bluebubblesActionSchema,
138+
})
139+
.superRefine((value, ctx) => {
140+
const config = value as BlueBubblesConfigWithAccounts;
141+
requireOpenAllowFrom({
142+
policy: config.dmPolicy,
143+
allowFrom: config.allowFrom,
144+
ctx,
145+
path: ["allowFrom"],
146+
message:
147+
'channels.bluebubbles.dmPolicy="open" requires channels.bluebubbles.allowFrom to include "*"',
148+
});
149+
requireAllowlistAllowFrom({
150+
policy: config.dmPolicy,
151+
allowFrom: config.allowFrom,
152+
ctx,
153+
path: ["allowFrom"],
154+
message:
155+
'channels.bluebubbles.dmPolicy="allowlist" requires channels.bluebubbles.allowFrom to contain at least one sender ID',
156+
});
157+
158+
for (const [accountId, account] of Object.entries(config.accounts ?? {})) {
159+
const effectivePolicy = account.dmPolicy ?? config.dmPolicy;
160+
const effectiveAllowFrom = account.allowFrom ?? config.allowFrom;
161+
requireOpenAllowFrom({
162+
policy: effectivePolicy,
163+
allowFrom: effectiveAllowFrom,
164+
ctx,
165+
path: ["accounts", accountId, "allowFrom"],
166+
message:
167+
'channels.bluebubbles.accounts.*.dmPolicy="open" requires channels.bluebubbles.accounts.*.allowFrom (or channels.bluebubbles.allowFrom) to include "*"',
168+
});
169+
requireAllowlistAllowFrom({
170+
policy: effectivePolicy,
171+
allowFrom: effectiveAllowFrom,
172+
ctx,
173+
path: ["accounts", accountId, "allowFrom"],
174+
message:
175+
'channels.bluebubbles.accounts.*.dmPolicy="allowlist" requires channels.bluebubbles.accounts.*.allowFrom (or channels.bluebubbles.allowFrom) to contain at least one sender ID',
176+
});
177+
}
178+
});
131179

132180
export const BlueBubblesChannelConfigSchema = buildChannelConfigSchema(BlueBubblesConfigSchema, {
133181
uiHints: bluebubblesChannelConfigUiHints,

extensions/bluebubbles/src/setup-surface.test.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,96 @@ describe("BlueBubblesConfigSchema", () => {
557557
?.work?.replyContextApiFallback,
558558
).toBe(false);
559559
});
560+
561+
it('rejects dmPolicy="allowlist" without channel allowFrom', () => {
562+
const parsed = BlueBubblesConfigSchema.safeParse({
563+
dmPolicy: "allowlist",
564+
});
565+
expect(parsed.success).toBe(false);
566+
if (parsed.success) {
567+
return;
568+
}
569+
expect(parsed.error.issues[0]?.path).toEqual(["allowFrom"]);
570+
expect(parsed.error.issues[0]?.message).toBe(
571+
'channels.bluebubbles.dmPolicy="allowlist" requires channels.bluebubbles.allowFrom to contain at least one sender ID',
572+
);
573+
});
574+
575+
it('rejects dmPolicy="open" without channel allowFrom wildcard', () => {
576+
const parsed = BlueBubblesConfigSchema.safeParse({
577+
dmPolicy: "open",
578+
allowFrom: ["user@example.com"],
579+
});
580+
expect(parsed.success).toBe(false);
581+
if (parsed.success) {
582+
return;
583+
}
584+
expect(parsed.error.issues[0]?.path).toEqual(["allowFrom"]);
585+
expect(parsed.error.issues[0]?.message).toBe(
586+
'channels.bluebubbles.dmPolicy="open" requires channels.bluebubbles.allowFrom to include "*"',
587+
);
588+
});
589+
590+
it("rejects account allowlist when neither account nor channel has allowFrom", () => {
591+
const parsed = BlueBubblesConfigSchema.safeParse({
592+
accounts: {
593+
work: {
594+
dmPolicy: "allowlist",
595+
},
596+
},
597+
});
598+
expect(parsed.success).toBe(false);
599+
if (parsed.success) {
600+
return;
601+
}
602+
expect(parsed.error.issues[0]?.path).toEqual(["accounts", "work", "allowFrom"]);
603+
expect(parsed.error.issues[0]?.message).toBe(
604+
'channels.bluebubbles.accounts.*.dmPolicy="allowlist" requires channels.bluebubbles.accounts.*.allowFrom (or channels.bluebubbles.allowFrom) to contain at least one sender ID',
605+
);
606+
});
607+
608+
it("accepts account allowlist when channel allowFrom is inherited", () => {
609+
const parsed = BlueBubblesConfigSchema.safeParse({
610+
allowFrom: ["user@example.com"],
611+
accounts: {
612+
work: {
613+
dmPolicy: "allowlist",
614+
},
615+
},
616+
});
617+
expect(parsed.success).toBe(true);
618+
});
619+
620+
it("rejects account open policy when effective allowFrom has no wildcard", () => {
621+
const parsed = BlueBubblesConfigSchema.safeParse({
622+
allowFrom: ["user@example.com"],
623+
accounts: {
624+
work: {
625+
dmPolicy: "open",
626+
},
627+
},
628+
});
629+
expect(parsed.success).toBe(false);
630+
if (parsed.success) {
631+
return;
632+
}
633+
expect(parsed.error.issues[0]?.path).toEqual(["accounts", "work", "allowFrom"]);
634+
expect(parsed.error.issues[0]?.message).toBe(
635+
'channels.bluebubbles.accounts.*.dmPolicy="open" requires channels.bluebubbles.accounts.*.allowFrom (or channels.bluebubbles.allowFrom) to include "*"',
636+
);
637+
});
638+
639+
it("accepts account open policy when channel allowFrom wildcard is inherited", () => {
640+
const parsed = BlueBubblesConfigSchema.safeParse({
641+
allowFrom: ["*"],
642+
accounts: {
643+
work: {
644+
dmPolicy: "open",
645+
},
646+
},
647+
});
648+
expect(parsed.success).toBe(true);
649+
});
560650
});
561651

562652
describe("bluebubbles group policy", () => {

src/config/config.allowlist-requires-allowfrom.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it } from "vitest";
2+
import { BlueBubblesConfigSchema } from "../../extensions/bluebubbles/channel-config-api.js";
23
import {
3-
BlueBubblesConfigSchema,
44
DiscordConfigSchema,
55
IMessageConfigSchema,
66
IrcConfigSchema,

src/config/zod-schema.providers-core.ts

Lines changed: 0 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -1459,129 +1459,6 @@ export const IMessageConfigSchema = IMessageAccountSchemaBase.extend({
14591459
}
14601460
});
14611461

1462-
const BlueBubblesAllowFromEntry = z.union([z.string(), z.number()]);
1463-
1464-
const BlueBubblesActionSchema = z
1465-
.object({
1466-
reactions: z.boolean().optional(),
1467-
edit: z.boolean().optional(),
1468-
unsend: z.boolean().optional(),
1469-
reply: z.boolean().optional(),
1470-
sendWithEffect: z.boolean().optional(),
1471-
renameGroup: z.boolean().optional(),
1472-
setGroupIcon: z.boolean().optional(),
1473-
addParticipant: z.boolean().optional(),
1474-
removeParticipant: z.boolean().optional(),
1475-
leaveGroup: z.boolean().optional(),
1476-
sendAttachment: z.boolean().optional(),
1477-
})
1478-
.strict()
1479-
.optional();
1480-
1481-
const BlueBubblesGroupConfigSchema = z
1482-
.object({
1483-
requireMention: z.boolean().optional(),
1484-
tools: ToolPolicySchema,
1485-
toolsBySender: ToolPolicyBySenderSchema,
1486-
})
1487-
.strict();
1488-
1489-
export const BlueBubblesAccountSchemaBase = z
1490-
.object({
1491-
name: z.string().optional(),
1492-
capabilities: z.array(z.string()).optional(),
1493-
markdown: MarkdownConfigSchema,
1494-
configWrites: z.boolean().optional(),
1495-
enabled: z.boolean().optional(),
1496-
serverUrl: z.string().optional(),
1497-
password: SecretInputSchema.optional().register(sensitive),
1498-
webhookPath: z.string().optional(),
1499-
dmPolicy: DmPolicySchema.optional().default("pairing"),
1500-
allowFrom: z.array(BlueBubblesAllowFromEntry).optional(),
1501-
groupAllowFrom: z.array(BlueBubblesAllowFromEntry).optional(),
1502-
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
1503-
contextVisibility: ContextVisibilityModeSchema.optional(),
1504-
historyLimit: z.number().int().min(0).optional(),
1505-
dmHistoryLimit: z.number().int().min(0).optional(),
1506-
dms: z.record(z.string(), DmConfigSchema.optional()).optional(),
1507-
textChunkLimit: z.number().int().positive().optional(),
1508-
sendTimeoutMs: z.number().int().positive().optional(),
1509-
chunkMode: z.enum(["length", "newline"]).optional(),
1510-
mediaMaxMb: z.number().int().positive().optional(),
1511-
mediaLocalRoots: z.array(z.string()).optional(),
1512-
sendReadReceipts: z.boolean().optional(),
1513-
network: z
1514-
.object({
1515-
dangerouslyAllowPrivateNetwork: z.boolean().optional(),
1516-
})
1517-
.strict()
1518-
.optional(),
1519-
blockStreaming: z.boolean().optional(),
1520-
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
1521-
groups: z.record(z.string(), BlueBubblesGroupConfigSchema.optional()).optional(),
1522-
enrichGroupParticipantsFromContacts: z.boolean().optional(),
1523-
heartbeat: ChannelHeartbeatVisibilitySchema,
1524-
healthMonitor: ChannelHealthMonitorSchema,
1525-
responsePrefix: z.string().optional(),
1526-
coalesceSameSenderDms: z.boolean().optional(),
1527-
})
1528-
.strict();
1529-
1530-
// Account-level schemas skip allowFrom validation because accounts inherit
1531-
// allowFrom from the parent channel config at runtime.
1532-
// Validation is enforced at the top-level BlueBubblesConfigSchema instead.
1533-
export const BlueBubblesAccountSchema = BlueBubblesAccountSchemaBase;
1534-
1535-
export const BlueBubblesConfigSchema = BlueBubblesAccountSchemaBase.extend({
1536-
accounts: z.record(z.string(), BlueBubblesAccountSchema.optional()).optional(),
1537-
defaultAccount: z.string().optional(),
1538-
actions: BlueBubblesActionSchema,
1539-
}).superRefine((value, ctx) => {
1540-
requireOpenAllowFrom({
1541-
policy: value.dmPolicy,
1542-
allowFrom: value.allowFrom,
1543-
ctx,
1544-
path: ["allowFrom"],
1545-
message:
1546-
'channels.bluebubbles.dmPolicy="open" requires channels.bluebubbles.allowFrom to include "*"',
1547-
});
1548-
requireAllowlistAllowFrom({
1549-
policy: value.dmPolicy,
1550-
allowFrom: value.allowFrom,
1551-
ctx,
1552-
path: ["allowFrom"],
1553-
message:
1554-
'channels.bluebubbles.dmPolicy="allowlist" requires channels.bluebubbles.allowFrom to contain at least one sender ID',
1555-
});
1556-
1557-
if (!value.accounts) {
1558-
return;
1559-
}
1560-
for (const [accountId, account] of Object.entries(value.accounts)) {
1561-
if (!account) {
1562-
continue;
1563-
}
1564-
const effectivePolicy = account.dmPolicy ?? value.dmPolicy;
1565-
const effectiveAllowFrom = account.allowFrom ?? value.allowFrom;
1566-
requireOpenAllowFrom({
1567-
policy: effectivePolicy,
1568-
allowFrom: effectiveAllowFrom,
1569-
ctx,
1570-
path: ["accounts", accountId, "allowFrom"],
1571-
message:
1572-
'channels.bluebubbles.accounts.*.dmPolicy="open" requires channels.bluebubbles.accounts.*.allowFrom (or channels.bluebubbles.allowFrom) to include "*"',
1573-
});
1574-
requireAllowlistAllowFrom({
1575-
policy: effectivePolicy,
1576-
allowFrom: effectiveAllowFrom,
1577-
ctx,
1578-
path: ["accounts", accountId, "allowFrom"],
1579-
message:
1580-
'channels.bluebubbles.accounts.*.dmPolicy="allowlist" requires channels.bluebubbles.accounts.*.allowFrom (or channels.bluebubbles.allowFrom) to contain at least one sender ID',
1581-
});
1582-
}
1583-
});
1584-
15851462
export const MSTeamsChannelSchema = z
15861463
.object({
15871464
requireMention: z.boolean().optional(),

src/plugin-sdk/bundled-channel-config-schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export {
1919
GroupPolicySchema,
2020
MarkdownConfigSchema,
2121
ReplyRuntimeConfigSchemaShape,
22+
requireAllowlistAllowFrom,
2223
requireOpenAllowFrom,
2324
} from "../config/zod-schema.core.js";
2425
export { ToolPolicySchema } from "../config/zod-schema.agent-runtime.js";

src/plugin-sdk/channel-config-primitives.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ export {
1212
GroupPolicySchema,
1313
MarkdownConfigSchema,
1414
ReplyRuntimeConfigSchemaShape,
15+
requireAllowlistAllowFrom,
1516
requireOpenAllowFrom,
1617
} from "../config/zod-schema.core.js";

src/plugin-sdk/channel-config-schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export {
1414
GroupPolicySchema,
1515
MarkdownConfigSchema,
1616
ReplyRuntimeConfigSchemaShape,
17+
requireAllowlistAllowFrom,
1718
requireOpenAllowFrom,
1819
} from "../config/zod-schema.core.js";
1920
export { ToolPolicySchema } from "../config/zod-schema.agent-runtime.js";

0 commit comments

Comments
 (0)