mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-20 21:23:23 +00:00
refactor(doctor): continue provider and shared extractions (#51905)
* refactor(doctor): extract empty allowlist scanning * refactor(doctor): extract matrix provider helpers * refactor(doctor): extract matrix repair orchestration
This commit is contained in:
@@ -3,23 +3,8 @@ import type { OpenClawConfig } from "../config/config.js";
|
||||
import { CONFIG_PATH, migrateLegacyConfig } from "../config/config.js";
|
||||
import { formatConfigIssueLines } from "../config/issue-format.js";
|
||||
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
|
||||
import {
|
||||
autoPrepareLegacyMatrixCrypto,
|
||||
detectLegacyMatrixCrypto,
|
||||
} from "../infra/matrix-legacy-crypto.js";
|
||||
import {
|
||||
autoMigrateLegacyMatrixState,
|
||||
detectLegacyMatrixState,
|
||||
} from "../infra/matrix-legacy-state.js";
|
||||
import {
|
||||
hasActionableMatrixMigration,
|
||||
hasPendingMatrixMigration,
|
||||
maybeCreateMatrixMigrationSnapshot,
|
||||
} from "../infra/matrix-migration-snapshot.js";
|
||||
import {
|
||||
detectPluginInstallPathIssue,
|
||||
formatPluginInstallPathIssue,
|
||||
} from "../infra/plugin-install-path-warnings.js";
|
||||
import { detectLegacyMatrixCrypto } from "../infra/matrix-legacy-crypto.js";
|
||||
import { detectLegacyMatrixState } from "../infra/matrix-legacy-state.js";
|
||||
import { sanitizeForLog } from "../terminal/ansi.js";
|
||||
import { note } from "../terminal/note.js";
|
||||
import { noteOpencodeProviderOverrides, stripUnknownConfigKeys } from "./doctor-config-analysis.js";
|
||||
@@ -31,7 +16,13 @@ import {
|
||||
scanDiscordNumericIdEntries,
|
||||
} from "./doctor/providers/discord.js";
|
||||
import {
|
||||
collectTelegramGroupPolicyWarnings,
|
||||
applyMatrixDoctorRepair,
|
||||
collectMatrixInstallPathWarnings,
|
||||
formatMatrixLegacyCryptoPreview,
|
||||
formatMatrixLegacyStatePreview,
|
||||
} from "./doctor/providers/matrix.js";
|
||||
import {
|
||||
collectTelegramEmptyAllowlistExtraWarnings,
|
||||
maybeRepairTelegramAllowFromUsernames,
|
||||
scanTelegramAllowFromUsernameEntries,
|
||||
} from "./doctor/providers/telegram.js";
|
||||
@@ -40,7 +31,7 @@ import {
|
||||
collectMissingDefaultAccountBindingWarnings,
|
||||
collectMissingExplicitDefaultAccountWarnings,
|
||||
} from "./doctor/shared/default-account-warnings.js";
|
||||
import { collectEmptyAllowlistPolicyWarningsForAccount } from "./doctor/shared/empty-allowlist-policy.js";
|
||||
import { scanEmptyAllowlistPolicyWarnings } from "./doctor/shared/empty-allowlist-scan.js";
|
||||
import {
|
||||
maybeRepairExecSafeBinProfiles,
|
||||
scanExecSafeBinCoverage,
|
||||
@@ -51,150 +42,8 @@ import {
|
||||
scanLegacyToolsBySenderKeys,
|
||||
} from "./doctor/shared/legacy-tools-by-sender.js";
|
||||
import { scanMutableAllowlistEntries } from "./doctor/shared/mutable-allowlist.js";
|
||||
import { asObjectRecord } from "./doctor/shared/object.js";
|
||||
import { maybeRepairOpenPolicyAllowFrom } from "./doctor/shared/open-policy-allowfrom.js";
|
||||
|
||||
function formatMatrixLegacyStatePreview(
|
||||
detection: Exclude<ReturnType<typeof detectLegacyMatrixState>, null | { warning: string }>,
|
||||
): string {
|
||||
return [
|
||||
"- Matrix plugin upgraded in place.",
|
||||
`- Legacy sync store: ${detection.legacyStoragePath} -> ${detection.targetStoragePath}`,
|
||||
`- Legacy crypto store: ${detection.legacyCryptoPath} -> ${detection.targetCryptoPath}`,
|
||||
...(detection.selectionNote ? [`- ${detection.selectionNote}`] : []),
|
||||
'- Run "openclaw doctor --fix" to migrate this Matrix state now.',
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function formatMatrixLegacyCryptoPreview(
|
||||
detection: ReturnType<typeof detectLegacyMatrixCrypto>,
|
||||
): string[] {
|
||||
const notes: string[] = [];
|
||||
for (const warning of detection.warnings) {
|
||||
notes.push(`- ${warning}`);
|
||||
}
|
||||
for (const plan of detection.plans) {
|
||||
notes.push(
|
||||
[
|
||||
`- Matrix encrypted-state migration is pending for account "${plan.accountId}".`,
|
||||
`- Legacy crypto store: ${plan.legacyCryptoPath}`,
|
||||
`- New recovery key file: ${plan.recoveryKeyPath}`,
|
||||
`- Migration state file: ${plan.statePath}`,
|
||||
'- Run "openclaw doctor --fix" to extract any saved backup key now. Backed-up room keys will restore automatically on next gateway start.',
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
return notes;
|
||||
}
|
||||
|
||||
async function collectMatrixInstallPathWarnings(cfg: OpenClawConfig): Promise<string[]> {
|
||||
const issue = await detectPluginInstallPathIssue({
|
||||
pluginId: "matrix",
|
||||
install: cfg.plugins?.installs?.matrix,
|
||||
});
|
||||
if (!issue) {
|
||||
return [];
|
||||
}
|
||||
return formatPluginInstallPathIssue({
|
||||
issue,
|
||||
pluginLabel: "Matrix",
|
||||
defaultInstallCommand: "openclaw plugins install @openclaw/matrix",
|
||||
repoInstallCommand: "openclaw plugins install ./extensions/matrix",
|
||||
formatCommand: formatCliCommand,
|
||||
}).map((entry) => `- ${entry}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan all channel configs for dmPolicy="allowlist" without any allowFrom entries.
|
||||
* This configuration blocks all DMs because no sender can match the empty
|
||||
* allowlist. Common after upgrades that remove external allowlist
|
||||
* file support.
|
||||
*/
|
||||
function detectEmptyAllowlistPolicy(cfg: OpenClawConfig): string[] {
|
||||
const channels = cfg.channels;
|
||||
if (!channels || typeof channels !== "object") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const warnings: string[] = [];
|
||||
|
||||
const checkAccount = (
|
||||
account: Record<string, unknown>,
|
||||
prefix: string,
|
||||
parent?: Record<string, unknown>,
|
||||
channelName?: string,
|
||||
) => {
|
||||
const accountDm = asObjectRecord(account.dm);
|
||||
const parentDm = asObjectRecord(parent?.dm);
|
||||
const dmPolicy =
|
||||
(account.dmPolicy as string | undefined) ??
|
||||
(accountDm?.policy as string | undefined) ??
|
||||
(parent?.dmPolicy as string | undefined) ??
|
||||
(parentDm?.policy as string | undefined) ??
|
||||
undefined;
|
||||
const effectiveAllowFrom =
|
||||
(account.allowFrom as Array<string | number> | undefined) ??
|
||||
(parent?.allowFrom as Array<string | number> | undefined) ??
|
||||
(accountDm?.allowFrom as Array<string | number> | undefined) ??
|
||||
(parentDm?.allowFrom as Array<string | number> | undefined) ??
|
||||
undefined;
|
||||
|
||||
warnings.push(
|
||||
...collectEmptyAllowlistPolicyWarningsForAccount({
|
||||
account,
|
||||
channelName,
|
||||
doctorFixCommand: formatCliCommand("openclaw doctor --fix"),
|
||||
parent,
|
||||
prefix,
|
||||
}),
|
||||
);
|
||||
if (
|
||||
channelName === "telegram" &&
|
||||
((account.groupPolicy as string | undefined) ??
|
||||
(parent?.groupPolicy as string | undefined) ??
|
||||
undefined) === "allowlist"
|
||||
) {
|
||||
warnings.push(
|
||||
...collectTelegramGroupPolicyWarnings({
|
||||
account,
|
||||
dmPolicy,
|
||||
effectiveAllowFrom,
|
||||
parent,
|
||||
prefix,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
for (const [channelName, channelConfig] of Object.entries(
|
||||
channels as Record<string, Record<string, unknown>>,
|
||||
)) {
|
||||
if (!channelConfig || typeof channelConfig !== "object") {
|
||||
continue;
|
||||
}
|
||||
checkAccount(channelConfig, `channels.${channelName}`, undefined, channelName);
|
||||
|
||||
const accounts = channelConfig.accounts;
|
||||
if (accounts && typeof accounts === "object") {
|
||||
for (const [accountId, account] of Object.entries(
|
||||
accounts as Record<string, Record<string, unknown>>,
|
||||
)) {
|
||||
if (!account || typeof account !== "object") {
|
||||
continue;
|
||||
}
|
||||
checkAccount(
|
||||
account,
|
||||
`channels.${channelName}.accounts.${accountId}`,
|
||||
channelConfig,
|
||||
channelName,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
export async function loadAndMaybeMigrateDoctorConfig(params: {
|
||||
options: DoctorOptions;
|
||||
confirm: (p: { message: string; initialValue: boolean }) => Promise<boolean>;
|
||||
@@ -266,80 +115,16 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
|
||||
cfg: candidate,
|
||||
env: process.env,
|
||||
});
|
||||
const pendingMatrixMigration = hasPendingMatrixMigration({
|
||||
cfg: candidate,
|
||||
env: process.env,
|
||||
});
|
||||
const actionableMatrixMigration = hasActionableMatrixMigration({
|
||||
cfg: candidate,
|
||||
env: process.env,
|
||||
});
|
||||
if (shouldRepair) {
|
||||
let matrixSnapshotReady = true;
|
||||
if (actionableMatrixMigration) {
|
||||
try {
|
||||
const snapshot = await maybeCreateMatrixMigrationSnapshot({
|
||||
trigger: "doctor-fix",
|
||||
env: process.env,
|
||||
});
|
||||
note(
|
||||
`Matrix migration snapshot ${snapshot.created ? "created" : "reused"} before applying Matrix upgrades.\n- ${snapshot.archivePath}`,
|
||||
"Doctor changes",
|
||||
);
|
||||
} catch (err) {
|
||||
matrixSnapshotReady = false;
|
||||
note(
|
||||
`- Failed creating a Matrix migration snapshot before repair: ${String(err)}`,
|
||||
"Doctor warnings",
|
||||
);
|
||||
note(
|
||||
'- Skipping Matrix migration changes for now. Resolve the snapshot failure, then rerun "openclaw doctor --fix".',
|
||||
"Doctor warnings",
|
||||
);
|
||||
}
|
||||
} else if (pendingMatrixMigration) {
|
||||
note(
|
||||
"- Matrix migration warnings are present, but no on-disk Matrix mutation is actionable yet. No pre-migration snapshot was needed.",
|
||||
"Doctor warnings",
|
||||
);
|
||||
const matrixRepair = await applyMatrixDoctorRepair({
|
||||
cfg: candidate,
|
||||
env: process.env,
|
||||
});
|
||||
for (const change of matrixRepair.changes) {
|
||||
note(change, "Doctor changes");
|
||||
}
|
||||
if (matrixSnapshotReady) {
|
||||
const matrixStateRepair = await autoMigrateLegacyMatrixState({
|
||||
cfg: candidate,
|
||||
env: process.env,
|
||||
});
|
||||
if (matrixStateRepair.changes.length > 0) {
|
||||
note(
|
||||
[
|
||||
"Matrix plugin upgraded in place.",
|
||||
...matrixStateRepair.changes.map((entry) => `- ${entry}`),
|
||||
"- No user action required.",
|
||||
].join("\n"),
|
||||
"Doctor changes",
|
||||
);
|
||||
}
|
||||
if (matrixStateRepair.warnings.length > 0) {
|
||||
note(matrixStateRepair.warnings.map((entry) => `- ${entry}`).join("\n"), "Doctor warnings");
|
||||
}
|
||||
const matrixCryptoRepair = await autoPrepareLegacyMatrixCrypto({
|
||||
cfg: candidate,
|
||||
env: process.env,
|
||||
});
|
||||
if (matrixCryptoRepair.changes.length > 0) {
|
||||
note(
|
||||
[
|
||||
"Matrix encrypted-state migration prepared.",
|
||||
...matrixCryptoRepair.changes.map((entry) => `- ${entry}`),
|
||||
].join("\n"),
|
||||
"Doctor changes",
|
||||
);
|
||||
}
|
||||
if (matrixCryptoRepair.warnings.length > 0) {
|
||||
note(
|
||||
matrixCryptoRepair.warnings.map((entry) => `- ${entry}`).join("\n"),
|
||||
"Doctor warnings",
|
||||
);
|
||||
}
|
||||
for (const warning of matrixRepair.warnings) {
|
||||
note(warning, "Doctor warnings");
|
||||
}
|
||||
} else if (matrixLegacyState) {
|
||||
if ("warning" in matrixLegacyState) {
|
||||
@@ -408,7 +193,10 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
|
||||
cfg = allowlistRepair.config;
|
||||
}
|
||||
|
||||
const emptyAllowlistWarnings = detectEmptyAllowlistPolicy(candidate);
|
||||
const emptyAllowlistWarnings = scanEmptyAllowlistPolicyWarnings(candidate, {
|
||||
doctorFixCommand: formatCliCommand("openclaw doctor --fix"),
|
||||
extraWarningsForAccount: collectTelegramEmptyAllowlistExtraWarnings,
|
||||
});
|
||||
if (emptyAllowlistWarnings.length > 0) {
|
||||
note(
|
||||
emptyAllowlistWarnings.map((line) => sanitizeForLog(line)).join("\n"),
|
||||
@@ -471,7 +259,10 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
|
||||
);
|
||||
}
|
||||
|
||||
const emptyAllowlistWarnings = detectEmptyAllowlistPolicy(candidate);
|
||||
const emptyAllowlistWarnings = scanEmptyAllowlistPolicyWarnings(candidate, {
|
||||
doctorFixCommand: formatCliCommand("openclaw doctor --fix"),
|
||||
extraWarningsForAccount: collectTelegramEmptyAllowlistExtraWarnings,
|
||||
});
|
||||
if (emptyAllowlistWarnings.length > 0) {
|
||||
note(
|
||||
emptyAllowlistWarnings.map((line) => sanitizeForLog(line)).join("\n"),
|
||||
|
||||
140
src/commands/doctor/providers/matrix.test.ts
Normal file
140
src/commands/doctor/providers/matrix.test.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
applyMatrixDoctorRepair,
|
||||
collectMatrixInstallPathWarnings,
|
||||
formatMatrixLegacyCryptoPreview,
|
||||
formatMatrixLegacyStatePreview,
|
||||
} from "./matrix.js";
|
||||
|
||||
vi.mock("../../../infra/matrix-migration-snapshot.js", () => ({
|
||||
hasActionableMatrixMigration: vi.fn(() => false),
|
||||
hasPendingMatrixMigration: vi.fn(() => false),
|
||||
maybeCreateMatrixMigrationSnapshot: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../../infra/matrix-legacy-state.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../../../infra/matrix-legacy-state.js")>(
|
||||
"../../../infra/matrix-legacy-state.js",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
autoMigrateLegacyMatrixState: vi.fn(async () => ({ changes: [], warnings: [] })),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../../infra/matrix-legacy-crypto.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../../../infra/matrix-legacy-crypto.js")>(
|
||||
"../../../infra/matrix-legacy-crypto.js",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
autoPrepareLegacyMatrixCrypto: vi.fn(async () => ({ changes: [], warnings: [] })),
|
||||
};
|
||||
});
|
||||
|
||||
describe("doctor matrix provider helpers", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("formats the legacy state preview", () => {
|
||||
const preview = formatMatrixLegacyStatePreview({
|
||||
accountId: "default",
|
||||
legacyStoragePath: "/tmp/legacy-sync.json",
|
||||
targetStoragePath: "/tmp/new-sync.json",
|
||||
legacyCryptoPath: "/tmp/legacy-crypto.json",
|
||||
targetCryptoPath: "/tmp/new-crypto.json",
|
||||
selectionNote: "Picked the newest account.",
|
||||
targetRootDir: "/tmp/account-root",
|
||||
});
|
||||
|
||||
expect(preview).toContain("Matrix plugin upgraded in place.");
|
||||
expect(preview).toContain("/tmp/legacy-sync.json -> /tmp/new-sync.json");
|
||||
expect(preview).toContain("Picked the newest account.");
|
||||
});
|
||||
|
||||
it("formats encrypted-state migration previews", () => {
|
||||
const previews = formatMatrixLegacyCryptoPreview({
|
||||
warnings: ["matrix warning"],
|
||||
plans: [
|
||||
{
|
||||
accountId: "default",
|
||||
rootDir: "/tmp/account-root",
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "tok-123",
|
||||
deviceId: "DEVICE123",
|
||||
legacyCryptoPath: "/tmp/legacy-crypto.json",
|
||||
recoveryKeyPath: "/tmp/recovery-key.txt",
|
||||
statePath: "/tmp/state.json",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(previews[0]).toBe("- matrix warning");
|
||||
expect(previews[1]).toContain(
|
||||
'Matrix encrypted-state migration is pending for account "default".',
|
||||
);
|
||||
expect(previews[1]).toContain("/tmp/recovery-key.txt");
|
||||
});
|
||||
|
||||
it("warns on stale custom Matrix plugin paths", async () => {
|
||||
const missingPath = path.join(tmpdir(), "openclaw-matrix-missing-provider-test");
|
||||
await fs.rm(missingPath, { recursive: true, force: true });
|
||||
|
||||
const warnings = await collectMatrixInstallPathWarnings({
|
||||
plugins: {
|
||||
installs: {
|
||||
matrix: {
|
||||
source: "path",
|
||||
sourcePath: missingPath,
|
||||
installPath: missingPath,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(warnings[0]).toContain("custom path that no longer exists");
|
||||
expect(warnings[0]).toContain(missingPath);
|
||||
expect(warnings[1]).toContain("openclaw plugins install @openclaw/matrix");
|
||||
expect(warnings[2]).toContain("openclaw plugins install ./extensions/matrix");
|
||||
});
|
||||
|
||||
it("summarizes matrix repair messaging", async () => {
|
||||
const matrixSnapshotModule = await import("../../../infra/matrix-migration-snapshot.js");
|
||||
const matrixStateModule = await import("../../../infra/matrix-legacy-state.js");
|
||||
const matrixCryptoModule = await import("../../../infra/matrix-legacy-crypto.js");
|
||||
|
||||
vi.mocked(matrixSnapshotModule.hasActionableMatrixMigration).mockReturnValue(true);
|
||||
vi.mocked(matrixSnapshotModule.maybeCreateMatrixMigrationSnapshot).mockResolvedValue({
|
||||
archivePath: "/tmp/matrix-backup.tgz",
|
||||
created: true,
|
||||
markerPath: "/tmp/marker.json",
|
||||
});
|
||||
vi.mocked(matrixStateModule.autoMigrateLegacyMatrixState).mockResolvedValue({
|
||||
migrated: true,
|
||||
changes: ["Migrated legacy sync state"],
|
||||
warnings: [],
|
||||
});
|
||||
vi.mocked(matrixCryptoModule.autoPrepareLegacyMatrixCrypto).mockResolvedValue({
|
||||
migrated: true,
|
||||
changes: ["Prepared recovery key export"],
|
||||
warnings: [],
|
||||
});
|
||||
|
||||
const result = await applyMatrixDoctorRepair({
|
||||
cfg: {},
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
expect(result.changes).toEqual([
|
||||
expect.stringContaining("Matrix migration snapshot created"),
|
||||
expect.stringContaining("Matrix plugin upgraded in place."),
|
||||
expect.stringContaining("Matrix encrypted-state migration prepared."),
|
||||
]);
|
||||
expect(result.warnings).toEqual([]);
|
||||
});
|
||||
});
|
||||
147
src/commands/doctor/providers/matrix.ts
Normal file
147
src/commands/doctor/providers/matrix.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { formatCliCommand } from "../../../cli/command-format.js";
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import {
|
||||
autoPrepareLegacyMatrixCrypto,
|
||||
detectLegacyMatrixCrypto,
|
||||
} from "../../../infra/matrix-legacy-crypto.js";
|
||||
import {
|
||||
autoMigrateLegacyMatrixState,
|
||||
detectLegacyMatrixState,
|
||||
} from "../../../infra/matrix-legacy-state.js";
|
||||
import {
|
||||
hasActionableMatrixMigration,
|
||||
hasPendingMatrixMigration,
|
||||
maybeCreateMatrixMigrationSnapshot,
|
||||
} from "../../../infra/matrix-migration-snapshot.js";
|
||||
import {
|
||||
detectPluginInstallPathIssue,
|
||||
formatPluginInstallPathIssue,
|
||||
} from "../../../infra/plugin-install-path-warnings.js";
|
||||
|
||||
export function formatMatrixLegacyStatePreview(
|
||||
detection: Exclude<ReturnType<typeof detectLegacyMatrixState>, null | { warning: string }>,
|
||||
): string {
|
||||
return [
|
||||
"- Matrix plugin upgraded in place.",
|
||||
`- Legacy sync store: ${detection.legacyStoragePath} -> ${detection.targetStoragePath}`,
|
||||
`- Legacy crypto store: ${detection.legacyCryptoPath} -> ${detection.targetCryptoPath}`,
|
||||
...(detection.selectionNote ? [`- ${detection.selectionNote}`] : []),
|
||||
'- Run "openclaw doctor --fix" to migrate this Matrix state now.',
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function formatMatrixLegacyCryptoPreview(
|
||||
detection: ReturnType<typeof detectLegacyMatrixCrypto>,
|
||||
): string[] {
|
||||
const notes: string[] = [];
|
||||
for (const warning of detection.warnings) {
|
||||
notes.push(`- ${warning}`);
|
||||
}
|
||||
for (const plan of detection.plans) {
|
||||
notes.push(
|
||||
[
|
||||
`- Matrix encrypted-state migration is pending for account "${plan.accountId}".`,
|
||||
`- Legacy crypto store: ${plan.legacyCryptoPath}`,
|
||||
`- New recovery key file: ${plan.recoveryKeyPath}`,
|
||||
`- Migration state file: ${plan.statePath}`,
|
||||
'- Run "openclaw doctor --fix" to extract any saved backup key now. Backed-up room keys will restore automatically on next gateway start.',
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
return notes;
|
||||
}
|
||||
|
||||
export async function collectMatrixInstallPathWarnings(cfg: OpenClawConfig): Promise<string[]> {
|
||||
const issue = await detectPluginInstallPathIssue({
|
||||
pluginId: "matrix",
|
||||
install: cfg.plugins?.installs?.matrix,
|
||||
});
|
||||
if (!issue) {
|
||||
return [];
|
||||
}
|
||||
return formatPluginInstallPathIssue({
|
||||
issue,
|
||||
pluginLabel: "Matrix",
|
||||
defaultInstallCommand: "openclaw plugins install @openclaw/matrix",
|
||||
repoInstallCommand: "openclaw plugins install ./extensions/matrix",
|
||||
formatCommand: formatCliCommand,
|
||||
}).map((entry) => `- ${entry}`);
|
||||
}
|
||||
|
||||
export async function applyMatrixDoctorRepair(params: {
|
||||
cfg: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): Promise<{ changes: string[]; warnings: string[] }> {
|
||||
const changes: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
const pendingMatrixMigration = hasPendingMatrixMigration({
|
||||
cfg: params.cfg,
|
||||
env: params.env,
|
||||
});
|
||||
const actionableMatrixMigration = hasActionableMatrixMigration({
|
||||
cfg: params.cfg,
|
||||
env: params.env,
|
||||
});
|
||||
|
||||
let matrixSnapshotReady = true;
|
||||
if (actionableMatrixMigration) {
|
||||
try {
|
||||
const snapshot = await maybeCreateMatrixMigrationSnapshot({
|
||||
trigger: "doctor-fix",
|
||||
env: params.env,
|
||||
});
|
||||
changes.push(
|
||||
`Matrix migration snapshot ${snapshot.created ? "created" : "reused"} before applying Matrix upgrades.\n- ${snapshot.archivePath}`,
|
||||
);
|
||||
} catch (err) {
|
||||
matrixSnapshotReady = false;
|
||||
warnings.push(`- Failed creating a Matrix migration snapshot before repair: ${String(err)}`);
|
||||
warnings.push(
|
||||
'- Skipping Matrix migration changes for now. Resolve the snapshot failure, then rerun "openclaw doctor --fix".',
|
||||
);
|
||||
}
|
||||
} else if (pendingMatrixMigration) {
|
||||
warnings.push(
|
||||
"- Matrix migration warnings are present, but no on-disk Matrix mutation is actionable yet. No pre-migration snapshot was needed.",
|
||||
);
|
||||
}
|
||||
|
||||
if (!matrixSnapshotReady) {
|
||||
return { changes, warnings };
|
||||
}
|
||||
|
||||
const matrixStateRepair = await autoMigrateLegacyMatrixState({
|
||||
cfg: params.cfg,
|
||||
env: params.env,
|
||||
});
|
||||
if (matrixStateRepair.changes.length > 0) {
|
||||
changes.push(
|
||||
[
|
||||
"Matrix plugin upgraded in place.",
|
||||
...matrixStateRepair.changes.map((entry) => `- ${entry}`),
|
||||
"- No user action required.",
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
if (matrixStateRepair.warnings.length > 0) {
|
||||
warnings.push(matrixStateRepair.warnings.map((entry) => `- ${entry}`).join("\n"));
|
||||
}
|
||||
|
||||
const matrixCryptoRepair = await autoPrepareLegacyMatrixCrypto({
|
||||
cfg: params.cfg,
|
||||
env: params.env,
|
||||
});
|
||||
if (matrixCryptoRepair.changes.length > 0) {
|
||||
changes.push(
|
||||
[
|
||||
"Matrix encrypted-state migration prepared.",
|
||||
...matrixCryptoRepair.changes.map((entry) => `- ${entry}`),
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
if (matrixCryptoRepair.warnings.length > 0) {
|
||||
warnings.push(matrixCryptoRepair.warnings.map((entry) => `- ${entry}`).join("\n"));
|
||||
}
|
||||
|
||||
return { changes, warnings };
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
collectTelegramEmptyAllowlistExtraWarnings,
|
||||
collectTelegramGroupPolicyWarnings,
|
||||
scanTelegramAllowFromUsernameEntries,
|
||||
} from "./telegram.js";
|
||||
@@ -57,6 +58,33 @@ describe("doctor telegram provider warnings", () => {
|
||||
expect(warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns extra empty-allowlist warnings only for telegram allowlist groups", () => {
|
||||
const warnings = collectTelegramEmptyAllowlistExtraWarnings({
|
||||
account: {
|
||||
botToken: "123:abc",
|
||||
groupPolicy: "allowlist",
|
||||
groups: {
|
||||
ops: { allow: true },
|
||||
},
|
||||
},
|
||||
channelName: "telegram",
|
||||
prefix: "channels.telegram",
|
||||
});
|
||||
|
||||
expect(warnings).toEqual([
|
||||
expect.stringContaining(
|
||||
'channels.telegram.groupPolicy is "allowlist" but groupAllowFrom (and allowFrom) is empty',
|
||||
),
|
||||
]);
|
||||
expect(
|
||||
collectTelegramEmptyAllowlistExtraWarnings({
|
||||
account: { groupPolicy: "allowlist" },
|
||||
channelName: "signal",
|
||||
prefix: "channels.signal",
|
||||
}),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("finds non-numeric telegram allowFrom username entries across account scopes", () => {
|
||||
const hits = scanTelegramAllowFromUsernameEntries({
|
||||
channels: {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { resolveTelegramAccount } from "../../../plugin-sdk/account-resolution.j
|
||||
import { describeUnknownError } from "../../../secrets/shared.js";
|
||||
import { sanitizeForLog } from "../../../terminal/ansi.js";
|
||||
import { hasAllowFromEntries } from "../shared/allowlist.js";
|
||||
import type { EmptyAllowlistAccountScanParams } from "../shared/empty-allowlist-scan.js";
|
||||
import { asObjectRecord } from "../shared/object.js";
|
||||
import type { DoctorAccountRecord, DoctorAllowFromList } from "../types.js";
|
||||
|
||||
@@ -338,3 +339,20 @@ export function collectTelegramGroupPolicyWarnings(
|
||||
`- ${params.prefix}.groupPolicy is "allowlist" but groupAllowFrom (and allowFrom) is empty — all group messages will be silently dropped. Add sender IDs to ${params.prefix}.groupAllowFrom or ${params.prefix}.allowFrom, or set ${params.prefix}.groupPolicy to "open".`,
|
||||
];
|
||||
}
|
||||
|
||||
export function collectTelegramEmptyAllowlistExtraWarnings(
|
||||
params: EmptyAllowlistAccountScanParams,
|
||||
): string[] {
|
||||
return params.channelName === "telegram" &&
|
||||
((params.account.groupPolicy as string | undefined) ??
|
||||
(params.parent?.groupPolicy as string | undefined) ??
|
||||
undefined) === "allowlist"
|
||||
? collectTelegramGroupPolicyWarnings({
|
||||
account: params.account,
|
||||
dmPolicy: params.dmPolicy,
|
||||
effectiveAllowFrom: params.effectiveAllowFrom,
|
||||
parent: params.parent,
|
||||
prefix: params.prefix,
|
||||
})
|
||||
: [];
|
||||
}
|
||||
|
||||
46
src/commands/doctor/shared/empty-allowlist-scan.test.ts
Normal file
46
src/commands/doctor/shared/empty-allowlist-scan.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { scanEmptyAllowlistPolicyWarnings } from "./empty-allowlist-scan.js";
|
||||
|
||||
describe("doctor empty allowlist policy scan", () => {
|
||||
it("scans top-level and account-scoped channel warnings", () => {
|
||||
const warnings = scanEmptyAllowlistPolicyWarnings(
|
||||
{
|
||||
channels: {
|
||||
signal: {
|
||||
dmPolicy: "allowlist",
|
||||
accounts: {
|
||||
work: { dmPolicy: "allowlist" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ doctorFixCommand: "openclaw doctor --fix" },
|
||||
);
|
||||
|
||||
expect(warnings).toEqual([
|
||||
expect.stringContaining('channels.signal.dmPolicy is "allowlist" but allowFrom is empty'),
|
||||
expect.stringContaining(
|
||||
'channels.signal.accounts.work.dmPolicy is "allowlist" but allowFrom is empty',
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
it("allows provider-specific extra warnings without importing providers", () => {
|
||||
const warnings = scanEmptyAllowlistPolicyWarnings(
|
||||
{
|
||||
channels: {
|
||||
telegram: {
|
||||
groupPolicy: "allowlist",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
doctorFixCommand: "openclaw doctor --fix",
|
||||
extraWarningsForAccount: ({ channelName, prefix }) =>
|
||||
channelName === "telegram" ? [`extra:${prefix}`] : [],
|
||||
},
|
||||
);
|
||||
|
||||
expect(warnings).toContain("extra:channels.telegram");
|
||||
});
|
||||
});
|
||||
101
src/commands/doctor/shared/empty-allowlist-scan.ts
Normal file
101
src/commands/doctor/shared/empty-allowlist-scan.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import type { DoctorAccountRecord, DoctorAllowFromList } from "../types.js";
|
||||
import { collectEmptyAllowlistPolicyWarningsForAccount } from "./empty-allowlist-policy.js";
|
||||
import { asObjectRecord } from "./object.js";
|
||||
|
||||
export type EmptyAllowlistAccountScanParams = {
|
||||
account: DoctorAccountRecord;
|
||||
channelName: string;
|
||||
dmPolicy?: string;
|
||||
effectiveAllowFrom?: DoctorAllowFromList;
|
||||
parent?: DoctorAccountRecord;
|
||||
prefix: string;
|
||||
};
|
||||
|
||||
type ScanEmptyAllowlistPolicyWarningsParams = {
|
||||
doctorFixCommand: string;
|
||||
extraWarningsForAccount?: (params: EmptyAllowlistAccountScanParams) => string[];
|
||||
};
|
||||
|
||||
export function scanEmptyAllowlistPolicyWarnings(
|
||||
cfg: OpenClawConfig,
|
||||
params: ScanEmptyAllowlistPolicyWarningsParams,
|
||||
): string[] {
|
||||
const channels = cfg.channels;
|
||||
if (!channels || typeof channels !== "object") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const warnings: string[] = [];
|
||||
|
||||
const checkAccount = (
|
||||
account: DoctorAccountRecord,
|
||||
prefix: string,
|
||||
channelName: string,
|
||||
parent?: DoctorAccountRecord,
|
||||
) => {
|
||||
const accountDm = asObjectRecord(account.dm);
|
||||
const parentDm = asObjectRecord(parent?.dm);
|
||||
const dmPolicy =
|
||||
(account.dmPolicy as string | undefined) ??
|
||||
(accountDm?.policy as string | undefined) ??
|
||||
(parent?.dmPolicy as string | undefined) ??
|
||||
(parentDm?.policy as string | undefined) ??
|
||||
undefined;
|
||||
const effectiveAllowFrom =
|
||||
(account.allowFrom as DoctorAllowFromList | undefined) ??
|
||||
(parent?.allowFrom as DoctorAllowFromList | undefined) ??
|
||||
(accountDm?.allowFrom as DoctorAllowFromList | undefined) ??
|
||||
(parentDm?.allowFrom as DoctorAllowFromList | undefined) ??
|
||||
undefined;
|
||||
|
||||
warnings.push(
|
||||
...collectEmptyAllowlistPolicyWarningsForAccount({
|
||||
account,
|
||||
channelName,
|
||||
doctorFixCommand: params.doctorFixCommand,
|
||||
parent,
|
||||
prefix,
|
||||
}),
|
||||
);
|
||||
if (params.extraWarningsForAccount) {
|
||||
warnings.push(
|
||||
...params.extraWarningsForAccount({
|
||||
account,
|
||||
channelName,
|
||||
dmPolicy,
|
||||
effectiveAllowFrom,
|
||||
parent,
|
||||
prefix,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
for (const [channelName, channelConfig] of Object.entries(
|
||||
channels as Record<string, DoctorAccountRecord>,
|
||||
)) {
|
||||
if (!channelConfig || typeof channelConfig !== "object") {
|
||||
continue;
|
||||
}
|
||||
checkAccount(channelConfig, `channels.${channelName}`, channelName);
|
||||
|
||||
const accounts = asObjectRecord(channelConfig.accounts);
|
||||
if (!accounts) {
|
||||
continue;
|
||||
}
|
||||
for (const [accountId, account] of Object.entries(accounts)) {
|
||||
if (!account || typeof account !== "object") {
|
||||
continue;
|
||||
}
|
||||
checkAccount(
|
||||
account as DoctorAccountRecord,
|
||||
`channels.${channelName}.accounts.${accountId}`,
|
||||
channelName,
|
||||
channelConfig,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
Reference in New Issue
Block a user