mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
perf(channels): add lightweight doctor contract APIs
This commit is contained in:
5
extensions/imessage/doctor-contract-api.ts
Normal file
5
extensions/imessage/doctor-contract-api.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { ChannelDoctorLegacyConfigRule } from "openclaw/plugin-sdk/channel-contract";
|
||||
|
||||
// iMessage does not expose doctor legacy rules today. Keep that empty answer on
|
||||
// a lightweight contract surface so doctor scans stay off the full plugin path.
|
||||
export const legacyConfigRules: ChannelDoctorLegacyConfigRule[] = [];
|
||||
@@ -1,2 +1,2 @@
|
||||
export { collectZalouserSecurityAuditFindings } from "./src/security-audit.js";
|
||||
export { legacyConfigRules, normalizeCompatibilityConfig } from "./src/doctor.js";
|
||||
export { legacyConfigRules, normalizeCompatibilityConfig } from "./src/doctor-contract.js";
|
||||
|
||||
1
extensions/zalouser/doctor-contract-api.ts
Normal file
1
extensions/zalouser/doctor-contract-api.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { normalizeCompatibilityConfig, legacyConfigRules } from "./src/doctor-contract.js";
|
||||
156
extensions/zalouser/src/doctor-contract.ts
Normal file
156
extensions/zalouser/src/doctor-contract.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import type {
|
||||
ChannelDoctorConfigMutation,
|
||||
ChannelDoctorLegacyConfigRule,
|
||||
} from "openclaw/plugin-sdk/channel-contract";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
|
||||
type ZalouserChannelsConfig = NonNullable<OpenClawConfig["channels"]>;
|
||||
|
||||
function asObjectRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: null;
|
||||
}
|
||||
|
||||
function hasLegacyZalouserGroupAllowAlias(value: unknown): boolean {
|
||||
const group = asObjectRecord(value);
|
||||
return Boolean(group && typeof group.allow === "boolean");
|
||||
}
|
||||
|
||||
function hasLegacyZalouserGroupAllowAliases(value: unknown): boolean {
|
||||
const groups = asObjectRecord(value);
|
||||
return Boolean(
|
||||
groups && Object.values(groups).some((group) => hasLegacyZalouserGroupAllowAlias(group)),
|
||||
);
|
||||
}
|
||||
|
||||
function hasLegacyZalouserAccountGroupAllowAliases(value: unknown): boolean {
|
||||
const accounts = asObjectRecord(value);
|
||||
if (!accounts) {
|
||||
return false;
|
||||
}
|
||||
return Object.values(accounts).some((account) => {
|
||||
const accountRecord = asObjectRecord(account);
|
||||
return Boolean(accountRecord && hasLegacyZalouserGroupAllowAliases(accountRecord.groups));
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeZalouserGroupAllowAliases(params: {
|
||||
groups: Record<string, unknown>;
|
||||
pathPrefix: string;
|
||||
changes: string[];
|
||||
}): { groups: Record<string, unknown>; changed: boolean } {
|
||||
let changed = false;
|
||||
const nextGroups: Record<string, unknown> = { ...params.groups };
|
||||
for (const [groupId, groupValue] of Object.entries(params.groups)) {
|
||||
const group = asObjectRecord(groupValue);
|
||||
if (!group || typeof group.allow !== "boolean") {
|
||||
continue;
|
||||
}
|
||||
const nextGroup = { ...group };
|
||||
if (typeof nextGroup.enabled !== "boolean") {
|
||||
nextGroup.enabled = group.allow;
|
||||
}
|
||||
delete nextGroup.allow;
|
||||
nextGroups[groupId] = nextGroup;
|
||||
changed = true;
|
||||
params.changes.push(
|
||||
`Moved ${params.pathPrefix}.${groupId}.allow → ${params.pathPrefix}.${groupId}.enabled (${String(nextGroup.enabled)}).`,
|
||||
);
|
||||
}
|
||||
return { groups: nextGroups, changed };
|
||||
}
|
||||
|
||||
function normalizeZalouserCompatibilityConfig(cfg: OpenClawConfig): ChannelDoctorConfigMutation {
|
||||
const channels = asObjectRecord(cfg.channels);
|
||||
const zalouser = asObjectRecord(channels?.zalouser);
|
||||
if (!zalouser) {
|
||||
return { config: cfg, changes: [] };
|
||||
}
|
||||
|
||||
const changes: string[] = [];
|
||||
let updatedZalouser: Record<string, unknown> = zalouser;
|
||||
let changed = false;
|
||||
|
||||
const groups = asObjectRecord(updatedZalouser.groups);
|
||||
if (groups) {
|
||||
const normalized = normalizeZalouserGroupAllowAliases({
|
||||
groups,
|
||||
pathPrefix: "channels.zalouser.groups",
|
||||
changes,
|
||||
});
|
||||
if (normalized.changed) {
|
||||
updatedZalouser = { ...updatedZalouser, groups: normalized.groups };
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
const accounts = asObjectRecord(updatedZalouser.accounts);
|
||||
if (accounts) {
|
||||
let accountsChanged = false;
|
||||
const nextAccounts: Record<string, unknown> = { ...accounts };
|
||||
for (const [accountId, accountValue] of Object.entries(accounts)) {
|
||||
const account = asObjectRecord(accountValue);
|
||||
if (!account) {
|
||||
continue;
|
||||
}
|
||||
const accountGroups = asObjectRecord(account.groups);
|
||||
if (!accountGroups) {
|
||||
continue;
|
||||
}
|
||||
const normalized = normalizeZalouserGroupAllowAliases({
|
||||
groups: accountGroups,
|
||||
pathPrefix: `channels.zalouser.accounts.${accountId}.groups`,
|
||||
changes,
|
||||
});
|
||||
if (!normalized.changed) {
|
||||
continue;
|
||||
}
|
||||
nextAccounts[accountId] = {
|
||||
...account,
|
||||
groups: normalized.groups,
|
||||
};
|
||||
accountsChanged = true;
|
||||
}
|
||||
if (accountsChanged) {
|
||||
updatedZalouser = { ...updatedZalouser, accounts: nextAccounts };
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return { config: cfg, changes: [] };
|
||||
}
|
||||
|
||||
return {
|
||||
config: {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
zalouser: updatedZalouser as ZalouserChannelsConfig["zalouser"],
|
||||
},
|
||||
},
|
||||
changes,
|
||||
};
|
||||
}
|
||||
|
||||
export const legacyConfigRules: ChannelDoctorLegacyConfigRule[] = [
|
||||
{
|
||||
path: ["channels", "zalouser", "groups"],
|
||||
message:
|
||||
'channels.zalouser.groups.<id>.allow is legacy; use channels.zalouser.groups.<id>.enabled instead. Run "openclaw doctor --fix".',
|
||||
match: hasLegacyZalouserGroupAllowAliases,
|
||||
},
|
||||
{
|
||||
path: ["channels", "zalouser", "accounts"],
|
||||
message:
|
||||
'channels.zalouser.accounts.<id>.groups.<id>.allow is legacy; use channels.zalouser.accounts.<id>.groups.<id>.enabled instead. Run "openclaw doctor --fix".',
|
||||
match: hasLegacyZalouserAccountGroupAllowAliases,
|
||||
},
|
||||
];
|
||||
|
||||
export function normalizeCompatibilityConfig(params: {
|
||||
cfg: OpenClawConfig;
|
||||
}): ChannelDoctorConfigMutation {
|
||||
return normalizeZalouserCompatibilityConfig(params.cfg);
|
||||
}
|
||||
@@ -1,165 +1,14 @@
|
||||
import type {
|
||||
ChannelDoctorAdapter,
|
||||
ChannelDoctorConfigMutation,
|
||||
ChannelDoctorLegacyConfigRule,
|
||||
} from "openclaw/plugin-sdk/channel-contract";
|
||||
import type { ChannelDoctorAdapter } from "openclaw/plugin-sdk/channel-contract";
|
||||
import { createDangerousNameMatchingMutableAllowlistWarningCollector } from "openclaw/plugin-sdk/channel-policy";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { legacyConfigRules, normalizeCompatibilityConfig } from "./doctor-contract.js";
|
||||
import { isZalouserMutableGroupEntry } from "./security-audit.js";
|
||||
|
||||
type ZalouserChannelsConfig = NonNullable<OpenClawConfig["channels"]>;
|
||||
|
||||
function asObjectRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: null;
|
||||
}
|
||||
|
||||
function hasLegacyZalouserGroupAllowAlias(value: unknown): boolean {
|
||||
const group = asObjectRecord(value);
|
||||
return Boolean(group && typeof group.allow === "boolean");
|
||||
}
|
||||
|
||||
function hasLegacyZalouserGroupAllowAliases(value: unknown): boolean {
|
||||
const groups = asObjectRecord(value);
|
||||
return Boolean(
|
||||
groups && Object.values(groups).some((group) => hasLegacyZalouserGroupAllowAlias(group)),
|
||||
);
|
||||
}
|
||||
|
||||
function hasLegacyZalouserAccountGroupAllowAliases(value: unknown): boolean {
|
||||
const accounts = asObjectRecord(value);
|
||||
if (!accounts) {
|
||||
return false;
|
||||
}
|
||||
return Object.values(accounts).some((account) => {
|
||||
const accountRecord = asObjectRecord(account);
|
||||
return Boolean(accountRecord && hasLegacyZalouserGroupAllowAliases(accountRecord.groups));
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeZalouserGroupAllowAliases(params: {
|
||||
groups: Record<string, unknown>;
|
||||
pathPrefix: string;
|
||||
changes: string[];
|
||||
}): { groups: Record<string, unknown>; changed: boolean } {
|
||||
let changed = false;
|
||||
const nextGroups: Record<string, unknown> = { ...params.groups };
|
||||
for (const [groupId, groupValue] of Object.entries(params.groups)) {
|
||||
const group = asObjectRecord(groupValue);
|
||||
if (!group || typeof group.allow !== "boolean") {
|
||||
continue;
|
||||
}
|
||||
const nextGroup = { ...group };
|
||||
if (typeof nextGroup.enabled !== "boolean") {
|
||||
nextGroup.enabled = group.allow;
|
||||
}
|
||||
delete nextGroup.allow;
|
||||
nextGroups[groupId] = nextGroup;
|
||||
changed = true;
|
||||
params.changes.push(
|
||||
`Moved ${params.pathPrefix}.${groupId}.allow → ${params.pathPrefix}.${groupId}.enabled (${String(nextGroup.enabled)}).`,
|
||||
);
|
||||
}
|
||||
return { groups: nextGroups, changed };
|
||||
}
|
||||
|
||||
function normalizeZalouserCompatibilityConfig(cfg: OpenClawConfig): ChannelDoctorConfigMutation {
|
||||
const channels = asObjectRecord(cfg.channels);
|
||||
const zalouser = asObjectRecord(channels?.zalouser);
|
||||
if (!zalouser) {
|
||||
return { config: cfg, changes: [] };
|
||||
}
|
||||
|
||||
const changes: string[] = [];
|
||||
let updatedZalouser: Record<string, unknown> = zalouser;
|
||||
let changed = false;
|
||||
|
||||
const groups = asObjectRecord(updatedZalouser.groups);
|
||||
if (groups) {
|
||||
const normalized = normalizeZalouserGroupAllowAliases({
|
||||
groups,
|
||||
pathPrefix: "channels.zalouser.groups",
|
||||
changes,
|
||||
});
|
||||
if (normalized.changed) {
|
||||
updatedZalouser = { ...updatedZalouser, groups: normalized.groups };
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
const accounts = asObjectRecord(updatedZalouser.accounts);
|
||||
if (accounts) {
|
||||
let accountsChanged = false;
|
||||
const nextAccounts: Record<string, unknown> = { ...accounts };
|
||||
for (const [accountId, accountValue] of Object.entries(accounts)) {
|
||||
const account = asObjectRecord(accountValue);
|
||||
if (!account) {
|
||||
continue;
|
||||
}
|
||||
const accountGroups = asObjectRecord(account.groups);
|
||||
if (!accountGroups) {
|
||||
continue;
|
||||
}
|
||||
const normalized = normalizeZalouserGroupAllowAliases({
|
||||
groups: accountGroups,
|
||||
pathPrefix: `channels.zalouser.accounts.${accountId}.groups`,
|
||||
changes,
|
||||
});
|
||||
if (!normalized.changed) {
|
||||
continue;
|
||||
}
|
||||
nextAccounts[accountId] = {
|
||||
...account,
|
||||
groups: normalized.groups,
|
||||
};
|
||||
accountsChanged = true;
|
||||
}
|
||||
if (accountsChanged) {
|
||||
updatedZalouser = { ...updatedZalouser, accounts: nextAccounts };
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return { config: cfg, changes: [] };
|
||||
}
|
||||
|
||||
return {
|
||||
config: {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
zalouser: updatedZalouser as ZalouserChannelsConfig["zalouser"],
|
||||
},
|
||||
},
|
||||
changes,
|
||||
};
|
||||
}
|
||||
|
||||
const ZALOUSER_LEGACY_CONFIG_RULES: ChannelDoctorLegacyConfigRule[] = [
|
||||
{
|
||||
path: ["channels", "zalouser", "groups"],
|
||||
message:
|
||||
'channels.zalouser.groups.<id>.allow is legacy; use channels.zalouser.groups.<id>.enabled instead. Run "openclaw doctor --fix".',
|
||||
match: hasLegacyZalouserGroupAllowAliases,
|
||||
},
|
||||
{
|
||||
path: ["channels", "zalouser", "accounts"],
|
||||
message:
|
||||
'channels.zalouser.accounts.<id>.groups.<id>.allow is legacy; use channels.zalouser.accounts.<id>.groups.<id>.enabled instead. Run "openclaw doctor --fix".',
|
||||
match: hasLegacyZalouserAccountGroupAllowAliases,
|
||||
},
|
||||
];
|
||||
|
||||
export const legacyConfigRules = ZALOUSER_LEGACY_CONFIG_RULES;
|
||||
|
||||
export function normalizeCompatibilityConfig(params: {
|
||||
cfg: OpenClawConfig;
|
||||
}): ChannelDoctorConfigMutation {
|
||||
return normalizeZalouserCompatibilityConfig(params.cfg);
|
||||
}
|
||||
|
||||
export const collectZalouserMutableAllowlistWarnings =
|
||||
createDangerousNameMatchingMutableAllowlistWarningCollector({
|
||||
channel: "zalouser",
|
||||
|
||||
@@ -107,6 +107,55 @@ describe("collectChannelLegacyConfigRules", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not rescan registry when a bundled bootstrap plugin has no legacy rules", () => {
|
||||
getBootstrapChannelPluginMock.mockImplementation((channelId: string) =>
|
||||
channelId === "imessage"
|
||||
? {
|
||||
doctor: {},
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
|
||||
const rules = collectChannelLegacyConfigRules({
|
||||
channels: {
|
||||
imessage: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(rules).toEqual([]);
|
||||
expect(listPluginDoctorLegacyConfigRulesMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("treats empty doctor-contract legacy rules as authoritative", () => {
|
||||
loadBundledChannelDoctorContractApiMock.mockImplementation((channelId: string) =>
|
||||
channelId === "imessage" ? { legacyConfigRules: [] } : undefined,
|
||||
);
|
||||
getBootstrapChannelPluginMock.mockImplementation((channelId: string) =>
|
||||
channelId === "imessage"
|
||||
? {
|
||||
doctor: {
|
||||
legacyConfigRules: [
|
||||
{
|
||||
path: ["channels", "imessage", "legacy"],
|
||||
message: "should not load bootstrap rules",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
|
||||
const rules = collectChannelLegacyConfigRules({
|
||||
channels: {
|
||||
imessage: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(rules).toEqual([]);
|
||||
expect(getBootstrapChannelPluginMock).not.toHaveBeenCalled();
|
||||
expect(listPluginDoctorLegacyConfigRulesMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("scopes channel legacy scans to touched channels during dry-run validation", () => {
|
||||
loadBundledChannelDoctorContractApiMock.mockImplementation((channelId: string) => ({
|
||||
legacyConfigRules: [
|
||||
|
||||
@@ -82,8 +82,9 @@ export function collectChannelLegacyConfigRules(
|
||||
const rules: LegacyConfigRule[] = [];
|
||||
const unresolvedChannelIds: ChannelId[] = [];
|
||||
for (const channelId of channelIds) {
|
||||
const contractRules = loadBundledChannelDoctorContractApi(channelId)?.legacyConfigRules;
|
||||
if (Array.isArray(contractRules) && contractRules.length > 0) {
|
||||
const contractApi = loadBundledChannelDoctorContractApi(channelId);
|
||||
const contractRules = contractApi?.legacyConfigRules;
|
||||
if (Array.isArray(contractRules)) {
|
||||
rules.push(...contractRules);
|
||||
continue;
|
||||
}
|
||||
@@ -93,6 +94,9 @@ export function collectChannelLegacyConfigRules(
|
||||
rules.push(...plugin.doctor.legacyConfigRules);
|
||||
continue;
|
||||
}
|
||||
if (plugin) {
|
||||
continue;
|
||||
}
|
||||
|
||||
unresolvedChannelIds.push(channelId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user