Skip to content

Commit c863ee1

Browse files
authored
fix(config): migrate bundled private-network aliases (#60862)
* refactor(plugin-sdk): centralize private-network opt-in semantics * fix(config): migrate bundled private-network aliases * fix(config): add bundled private-network doctor adapters * fix(config): expose bundled channel migration hooks * fix(config): prefer canonical private-network key * test(config): refresh rebased private-network outputs
1 parent 87b8680 commit c863ee1

73 files changed

Lines changed: 1935 additions & 87 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
4c880eb1ce03486f47aa21f49317ad15fc8d92bb720d70205743b72e45cf5fa3 config-baseline.json
2-
03ff4a3e314f17dd8851aed3653269294bc62412bee05a6804dce840bd3d7551 config-baseline.core.json
3-
73b57f395a2ad983f1660112d0b2b998342f1ddbe3089b440d7f73d0665de739 config-baseline.channel.json
1+
20a882f9991e17310013471756ac7ec62c272e29490daeede9c0901bd51c0e69 config-baseline.json
2+
8ba6e5c959d5fc3eee9e6c5d1d8f764f164052f4207c0352bb39e2a7dbad64a8 config-baseline.core.json
3+
ca6d1fa8a3507566979ea2da2b88a6a7ae49d650f3ebd3eee14a22ed18e5be89 config-baseline.channel.json
44
17fd37605bf6cb087932ec2ebcfa9dd22e669fa6b8b93081ab2deac9d24821c5 config-baseline.plugin.json

extensions/bluebubbles/contract-surfaces.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export { normalizeCompatibilityConfig, legacyConfigRules } from "./src/doctor-contract.js";
12
export {
23
collectRuntimeConfigAssignments,
34
secretTargetRegistryEntries,

extensions/bluebubbles/src/account-resolve.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { isBlockedHostnameOrIp } from "openclaw/plugin-sdk/ssrf-runtime";
1+
import {
2+
isBlockedHostnameOrIp,
3+
isPrivateNetworkOptInEnabled,
4+
} from "openclaw/plugin-sdk/ssrf-runtime";
25
import { resolveBlueBubblesAccount } from "./accounts.js";
36
import type { OpenClawConfig } from "./runtime-api.js";
47
import { normalizeResolvedSecretInputString } from "./secret-input.js";
@@ -58,6 +61,6 @@ export function resolveBlueBubblesServerAccount(params: BlueBubblesAccountResolv
5861
baseUrl,
5962
password,
6063
accountId: account.accountId,
61-
allowPrivateNetwork: account.config.allowPrivateNetwork === true || autoAllowPrivateNetwork,
64+
allowPrivateNetwork: isPrivateNetworkOptInEnabled(account.config) || autoAllowPrivateNetwork,
6265
};
6366
}

extensions/bluebubbles/src/actions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
readStringParam,
88
} from "openclaw/plugin-sdk/channel-actions";
99
import { createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime";
10+
import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime";
1011
import { extractToolSend } from "openclaw/plugin-sdk/tool-send";
1112
import { resolveBlueBubblesAccount } from "./accounts.js";
1213
import {
@@ -173,7 +174,7 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = {
173174
baseUrl,
174175
password,
175176
target,
176-
allowPrivateNetwork: account.config.allowPrivateNetwork === true,
177+
allowPrivateNetwork: isPrivateNetworkOptInEnabled(account.config),
177178
});
178179
if (!resolved) {
179180
throw new Error(`BlueBubbles ${action} failed: chatGuid not found for target.`);

extensions/bluebubbles/src/attachments.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,9 @@ describe("downloadBlueBubblesAttachment", () => {
278278
bluebubbles: {
279279
serverUrl: "http://localhost:1234",
280280
password: "test",
281-
allowPrivateNetwork: true,
281+
network: {
282+
dangerouslyAllowPrivateNetwork: true,
283+
},
282284
},
283285
},
284286
},

extensions/bluebubbles/src/channel.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
createComputedAccountStatusAdapter,
1818
createDefaultChannelRuntimeState,
1919
} from "openclaw/plugin-sdk/status-helpers";
20+
import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime";
2021
import {
2122
listBlueBubblesAccountIds,
2223
type ResolvedBlueBubblesAccount,
@@ -34,6 +35,7 @@ import {
3435
} from "./channel-shared.js";
3536
import type { BlueBubblesProbe } from "./channel.runtime.js";
3637
import { createBlueBubblesConversationBindingManager } from "./conversation-bindings.js";
38+
import { bluebubblesDoctor } from "./doctor.js";
3739
import {
3840
matchBlueBubblesAcpConversation,
3941
normalizeBlueBubblesAcpConversationId,
@@ -100,6 +102,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount, BlueBu
100102
isConfigured: (account) => account.configured,
101103
describeAccount: (account): ChannelAccountSnapshot => describeBlueBubblesAccount(account),
102104
},
105+
doctor: bluebubblesDoctor,
103106
conversationBindings: {
104107
supportsCurrentConversationBinding: true,
105108
createManager: ({ cfg, accountId }) =>
@@ -226,7 +229,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount, BlueBu
226229
baseUrl: account.baseUrl,
227230
password: account.config.password ?? null,
228231
timeoutMs,
229-
allowPrivateNetwork: account.config.allowPrivateNetwork === true,
232+
allowPrivateNetwork: isPrivateNetworkOptInEnabled(account.config),
230233
}),
231234
resolveAccountSnapshot: ({ account, runtime, probe }) => {
232235
const running = runtime?.running ?? false;

extensions/bluebubbles/src/config-schema.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ const bluebubblesGroupConfigSchema = z.object({
3232
tools: ToolPolicySchema,
3333
});
3434

35+
const bluebubblesNetworkSchema = z
36+
.object({
37+
/** Dangerous opt-in for same-host or trusted private/internal BlueBubbles deployments. */
38+
dangerouslyAllowPrivateNetwork: z.boolean().optional(),
39+
})
40+
.strict()
41+
.optional();
42+
3543
const bluebubblesAccountSchema = z
3644
.object({
3745
name: z.string().optional(),
@@ -53,7 +61,7 @@ const bluebubblesAccountSchema = z
5361
mediaMaxMb: z.number().int().positive().optional(),
5462
mediaLocalRoots: z.array(z.string()).optional(),
5563
sendReadReceipts: z.boolean().optional(),
56-
allowPrivateNetwork: z.boolean().optional(),
64+
network: bluebubblesNetworkSchema,
5765
blockStreaming: z.boolean().optional(),
5866
groups: z.object({}).catchall(bluebubblesGroupConfigSchema).optional(),
5967
})
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import type {
2+
ChannelDoctorConfigMutation,
3+
ChannelDoctorLegacyConfigRule,
4+
} from "openclaw/plugin-sdk/channel-contract";
5+
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
6+
import {
7+
hasLegacyFlatAllowPrivateNetworkAlias,
8+
migrateLegacyFlatAllowPrivateNetworkAlias,
9+
} from "openclaw/plugin-sdk/ssrf-runtime";
10+
11+
function isRecord(value: unknown): value is Record<string, unknown> {
12+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
13+
}
14+
15+
function hasLegacyAllowPrivateNetworkInAccounts(value: unknown): boolean {
16+
const accounts = isRecord(value) ? value : null;
17+
return Boolean(
18+
accounts &&
19+
Object.values(accounts).some((account) =>
20+
hasLegacyFlatAllowPrivateNetworkAlias(isRecord(account) ? account : {}),
21+
),
22+
);
23+
}
24+
25+
export const legacyConfigRules: ChannelDoctorLegacyConfigRule[] = [
26+
{
27+
path: ["channels", "bluebubbles"],
28+
message:
29+
"channels.bluebubbles.allowPrivateNetwork is legacy; use channels.bluebubbles.network.dangerouslyAllowPrivateNetwork instead (auto-migrated on load).",
30+
match: (value) => hasLegacyFlatAllowPrivateNetworkAlias(isRecord(value) ? value : {}),
31+
},
32+
{
33+
path: ["channels", "bluebubbles", "accounts"],
34+
message:
35+
"channels.bluebubbles.accounts.<id>.allowPrivateNetwork is legacy; use channels.bluebubbles.accounts.<id>.network.dangerouslyAllowPrivateNetwork instead (auto-migrated on load).",
36+
match: hasLegacyAllowPrivateNetworkInAccounts,
37+
},
38+
];
39+
40+
export function normalizeCompatibilityConfig({
41+
cfg,
42+
}: {
43+
cfg: OpenClawConfig;
44+
}): ChannelDoctorConfigMutation {
45+
const channels = isRecord(cfg.channels) ? cfg.channels : null;
46+
const bluebubbles = isRecord(channels?.bluebubbles) ? channels.bluebubbles : null;
47+
if (!bluebubbles) {
48+
return { config: cfg, changes: [] };
49+
}
50+
51+
const changes: string[] = [];
52+
let updatedBluebubbles = bluebubbles;
53+
let changed = false;
54+
55+
const topLevel = migrateLegacyFlatAllowPrivateNetworkAlias({
56+
entry: updatedBluebubbles,
57+
pathPrefix: "channels.bluebubbles",
58+
changes,
59+
});
60+
updatedBluebubbles = topLevel.entry;
61+
changed = changed || topLevel.changed;
62+
63+
const accounts = isRecord(updatedBluebubbles.accounts) ? updatedBluebubbles.accounts : null;
64+
if (accounts) {
65+
let accountsChanged = false;
66+
const nextAccounts: Record<string, unknown> = { ...accounts };
67+
for (const [accountId, accountValue] of Object.entries(accounts)) {
68+
const account = isRecord(accountValue) ? accountValue : null;
69+
if (!account) {
70+
continue;
71+
}
72+
const migrated = migrateLegacyFlatAllowPrivateNetworkAlias({
73+
entry: account,
74+
pathPrefix: `channels.bluebubbles.accounts.${accountId}`,
75+
changes,
76+
});
77+
if (!migrated.changed) {
78+
continue;
79+
}
80+
nextAccounts[accountId] = migrated.entry;
81+
accountsChanged = true;
82+
}
83+
if (accountsChanged) {
84+
updatedBluebubbles = { ...updatedBluebubbles, accounts: nextAccounts };
85+
changed = true;
86+
}
87+
}
88+
89+
if (!changed) {
90+
return { config: cfg, changes: [] };
91+
}
92+
93+
return {
94+
config: {
95+
...cfg,
96+
channels: {
97+
...cfg.channels,
98+
bluebubbles: updatedBluebubbles as NonNullable<OpenClawConfig["channels"]>["bluebubbles"],
99+
},
100+
},
101+
changes,
102+
};
103+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { describe, expect, it } from "vitest";
2+
import { bluebubblesDoctor } from "./doctor.js";
3+
4+
describe("bluebubbles doctor", () => {
5+
it("normalizes legacy private-network aliases", () => {
6+
const normalize = bluebubblesDoctor.normalizeCompatibilityConfig;
7+
expect(normalize).toBeDefined();
8+
if (!normalize) {
9+
return;
10+
}
11+
12+
const result = normalize({
13+
cfg: {
14+
channels: {
15+
bluebubbles: {
16+
allowPrivateNetwork: true,
17+
accounts: {
18+
default: {
19+
allowPrivateNetwork: false,
20+
},
21+
},
22+
},
23+
},
24+
} as never,
25+
});
26+
27+
expect(result.config.channels?.bluebubbles?.network).toEqual({
28+
dangerouslyAllowPrivateNetwork: true,
29+
});
30+
expect(result.config.channels?.bluebubbles?.accounts?.default?.network).toEqual({
31+
dangerouslyAllowPrivateNetwork: false,
32+
});
33+
});
34+
});
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import type {
2+
ChannelDoctorAdapter,
3+
ChannelDoctorConfigMutation,
4+
ChannelDoctorLegacyConfigRule,
5+
} from "openclaw/plugin-sdk/channel-contract";
6+
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
7+
import {
8+
hasLegacyFlatAllowPrivateNetworkAlias,
9+
migrateLegacyFlatAllowPrivateNetworkAlias,
10+
} from "openclaw/plugin-sdk/ssrf-runtime";
11+
12+
function isRecord(value: unknown): value is Record<string, unknown> {
13+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
14+
}
15+
16+
function hasLegacyAllowPrivateNetworkInAccounts(value: unknown): boolean {
17+
const accounts = isRecord(value) ? value : null;
18+
return Boolean(
19+
accounts &&
20+
Object.values(accounts).some((account) =>
21+
hasLegacyFlatAllowPrivateNetworkAlias(isRecord(account) ? account : {}),
22+
),
23+
);
24+
}
25+
26+
function normalizeBlueBubblesCompatibilityConfig(cfg: OpenClawConfig): ChannelDoctorConfigMutation {
27+
const channels = isRecord(cfg.channels) ? cfg.channels : null;
28+
const bluebubbles = isRecord(channels?.bluebubbles) ? channels.bluebubbles : null;
29+
if (!bluebubbles) {
30+
return { config: cfg, changes: [] };
31+
}
32+
33+
const changes: string[] = [];
34+
let updatedBluebubbles = bluebubbles;
35+
let changed = false;
36+
37+
const topLevel = migrateLegacyFlatAllowPrivateNetworkAlias({
38+
entry: updatedBluebubbles,
39+
pathPrefix: "channels.bluebubbles",
40+
changes,
41+
});
42+
updatedBluebubbles = topLevel.entry;
43+
changed = changed || topLevel.changed;
44+
45+
const accounts = isRecord(updatedBluebubbles.accounts) ? updatedBluebubbles.accounts : null;
46+
if (accounts) {
47+
let accountsChanged = false;
48+
const nextAccounts: Record<string, unknown> = { ...accounts };
49+
for (const [accountId, accountValue] of Object.entries(accounts)) {
50+
const account = isRecord(accountValue) ? accountValue : null;
51+
if (!account) {
52+
continue;
53+
}
54+
const migrated = migrateLegacyFlatAllowPrivateNetworkAlias({
55+
entry: account,
56+
pathPrefix: `channels.bluebubbles.accounts.${accountId}`,
57+
changes,
58+
});
59+
if (!migrated.changed) {
60+
continue;
61+
}
62+
nextAccounts[accountId] = migrated.entry;
63+
accountsChanged = true;
64+
}
65+
if (accountsChanged) {
66+
updatedBluebubbles = { ...updatedBluebubbles, accounts: nextAccounts };
67+
changed = true;
68+
}
69+
}
70+
71+
if (!changed) {
72+
return { config: cfg, changes: [] };
73+
}
74+
75+
return {
76+
config: {
77+
...cfg,
78+
channels: {
79+
...cfg.channels,
80+
bluebubbles: updatedBluebubbles as NonNullable<OpenClawConfig["channels"]>["bluebubbles"],
81+
},
82+
},
83+
changes,
84+
};
85+
}
86+
87+
const BLUEBUBBLES_LEGACY_CONFIG_RULES: ChannelDoctorLegacyConfigRule[] = [
88+
{
89+
path: ["channels", "bluebubbles"],
90+
message:
91+
"channels.bluebubbles.allowPrivateNetwork is legacy; use channels.bluebubbles.network.dangerouslyAllowPrivateNetwork instead (auto-migrated on load).",
92+
match: (value) => hasLegacyFlatAllowPrivateNetworkAlias(isRecord(value) ? value : {}),
93+
},
94+
{
95+
path: ["channels", "bluebubbles", "accounts"],
96+
message:
97+
"channels.bluebubbles.accounts.<id>.allowPrivateNetwork is legacy; use channels.bluebubbles.accounts.<id>.network.dangerouslyAllowPrivateNetwork instead (auto-migrated on load).",
98+
match: hasLegacyAllowPrivateNetworkInAccounts,
99+
},
100+
];
101+
102+
export const bluebubblesDoctor: ChannelDoctorAdapter = {
103+
legacyConfigRules: BLUEBUBBLES_LEGACY_CONFIG_RULES,
104+
normalizeCompatibilityConfig: ({ cfg }) => normalizeBlueBubblesCompatibilityConfig(cfg),
105+
};

0 commit comments

Comments
 (0)