mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 01:51:24 +00:00
refactor(config): compile toolsBySender policy and migrate legacy keys
This commit is contained in:
@@ -378,6 +378,49 @@ describe("doctor config flow", () => {
|
||||
expect(cfg.channels.discord.accounts.work.allowFrom).toEqual(["*"]);
|
||||
});
|
||||
|
||||
it("migrates legacy toolsBySender keys to typed id entries on repair", async () => {
|
||||
const result = await runDoctorConfigWithInput({
|
||||
repair: true,
|
||||
config: {
|
||||
channels: {
|
||||
whatsapp: {
|
||||
groups: {
|
||||
"123@g.us": {
|
||||
toolsBySender: {
|
||||
owner: { allow: ["exec"] },
|
||||
alice: { deny: ["exec"] },
|
||||
"id:owner": { deny: ["exec"] },
|
||||
"username:@ops-bot": { allow: ["fs.read"] },
|
||||
"*": { deny: ["exec"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
run: loadAndMaybeMigrateDoctorConfig,
|
||||
});
|
||||
|
||||
const cfg = result.cfg as unknown as {
|
||||
channels: {
|
||||
whatsapp: {
|
||||
groups: {
|
||||
"123@g.us": {
|
||||
toolsBySender: Record<string, { allow?: string[]; deny?: string[] }>;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
const toolsBySender = cfg.channels.whatsapp.groups["123@g.us"].toolsBySender;
|
||||
expect(toolsBySender.owner).toBeUndefined();
|
||||
expect(toolsBySender.alice).toBeUndefined();
|
||||
expect(toolsBySender["id:owner"]).toEqual({ deny: ["exec"] });
|
||||
expect(toolsBySender["id:alice"]).toEqual({ deny: ["exec"] });
|
||||
expect(toolsBySender["username:@ops-bot"]).toEqual({ allow: ["fs.read"] });
|
||||
expect(toolsBySender["*"]).toEqual({ deny: ["exec"] });
|
||||
});
|
||||
|
||||
it("repairs googlechat dm.policy open by setting dm.allowFrom on repair", async () => {
|
||||
const result = await runDoctorConfigWithInput({
|
||||
repair: true,
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
readConfigFileSnapshot,
|
||||
} from "../config/config.js";
|
||||
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
|
||||
import { parseToolsBySenderTypedKey } from "../config/types.tools.js";
|
||||
import {
|
||||
listInterpreterLikeSafeBins,
|
||||
resolveMergedSafeBinProfileFixtures,
|
||||
@@ -836,6 +837,120 @@ function maybeRepairExecSafeBinProfiles(cfg: OpenClawConfig): {
|
||||
return { config: next, changes, warnings };
|
||||
}
|
||||
|
||||
type LegacyToolsBySenderKeyHit = {
|
||||
toolsBySenderPath: Array<string | number>;
|
||||
pathLabel: string;
|
||||
key: string;
|
||||
targetKey: string;
|
||||
};
|
||||
|
||||
function collectLegacyToolsBySenderKeyHits(
|
||||
value: unknown,
|
||||
pathParts: Array<string | number>,
|
||||
hits: LegacyToolsBySenderKeyHit[],
|
||||
) {
|
||||
if (Array.isArray(value)) {
|
||||
for (const [index, entry] of value.entries()) {
|
||||
collectLegacyToolsBySenderKeyHits(entry, [...pathParts, index], hits);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const record = asObjectRecord(value);
|
||||
if (!record) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolsBySender = asObjectRecord(record.toolsBySender);
|
||||
if (toolsBySender) {
|
||||
const path = [...pathParts, "toolsBySender"];
|
||||
const pathLabel = formatPath(path);
|
||||
for (const rawKey of Object.keys(toolsBySender)) {
|
||||
const trimmed = rawKey.trim();
|
||||
if (!trimmed || trimmed === "*" || parseToolsBySenderTypedKey(trimmed)) {
|
||||
continue;
|
||||
}
|
||||
hits.push({
|
||||
toolsBySenderPath: path,
|
||||
pathLabel,
|
||||
key: rawKey,
|
||||
targetKey: `id:${trimmed}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, nested] of Object.entries(record)) {
|
||||
if (key === "toolsBySender") {
|
||||
continue;
|
||||
}
|
||||
collectLegacyToolsBySenderKeyHits(nested, [...pathParts, key], hits);
|
||||
}
|
||||
}
|
||||
|
||||
function scanLegacyToolsBySenderKeys(cfg: OpenClawConfig): LegacyToolsBySenderKeyHit[] {
|
||||
const hits: LegacyToolsBySenderKeyHit[] = [];
|
||||
collectLegacyToolsBySenderKeyHits(cfg, [], hits);
|
||||
return hits;
|
||||
}
|
||||
|
||||
function maybeRepairLegacyToolsBySenderKeys(cfg: OpenClawConfig): {
|
||||
config: OpenClawConfig;
|
||||
changes: string[];
|
||||
} {
|
||||
const next = structuredClone(cfg);
|
||||
const hits = scanLegacyToolsBySenderKeys(next);
|
||||
if (hits.length === 0) {
|
||||
return { config: cfg, changes: [] };
|
||||
}
|
||||
|
||||
const summary = new Map<string, { migrated: number; dropped: number; examples: string[] }>();
|
||||
let changed = false;
|
||||
|
||||
for (const hit of hits) {
|
||||
const toolsBySender = asObjectRecord(resolvePathTarget(next, hit.toolsBySenderPath));
|
||||
if (!toolsBySender || !(hit.key in toolsBySender)) {
|
||||
continue;
|
||||
}
|
||||
const row = summary.get(hit.pathLabel) ?? { migrated: 0, dropped: 0, examples: [] };
|
||||
|
||||
if (toolsBySender[hit.targetKey] === undefined) {
|
||||
toolsBySender[hit.targetKey] = toolsBySender[hit.key];
|
||||
row.migrated++;
|
||||
if (row.examples.length < 3) {
|
||||
row.examples.push(`${hit.key} -> ${hit.targetKey}`);
|
||||
}
|
||||
} else {
|
||||
row.dropped++;
|
||||
if (row.examples.length < 3) {
|
||||
row.examples.push(`${hit.key} (kept existing ${hit.targetKey})`);
|
||||
}
|
||||
}
|
||||
delete toolsBySender[hit.key];
|
||||
summary.set(hit.pathLabel, row);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return { config: cfg, changes: [] };
|
||||
}
|
||||
|
||||
const changes: string[] = [];
|
||||
for (const [pathLabel, row] of summary) {
|
||||
if (row.migrated > 0) {
|
||||
const suffix = row.examples.length > 0 ? ` (${row.examples.join(", ")})` : "";
|
||||
changes.push(
|
||||
`- ${pathLabel}: migrated ${row.migrated} legacy key${row.migrated === 1 ? "" : "s"} to typed id: entries${suffix}.`,
|
||||
);
|
||||
}
|
||||
if (row.dropped > 0) {
|
||||
changes.push(
|
||||
`- ${pathLabel}: removed ${row.dropped} legacy key${row.dropped === 1 ? "" : "s"} where typed id: entries already existed.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return { config: next, changes };
|
||||
}
|
||||
|
||||
async function maybeMigrateLegacyConfig(): Promise<string[]> {
|
||||
const changes: string[] = [];
|
||||
const home = resolveHomeDir();
|
||||
@@ -991,6 +1106,15 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
|
||||
pendingChanges = true;
|
||||
cfg = allowFromRepair.config;
|
||||
}
|
||||
|
||||
const toolsBySenderRepair = maybeRepairLegacyToolsBySenderKeys(candidate);
|
||||
if (toolsBySenderRepair.changes.length > 0) {
|
||||
note(toolsBySenderRepair.changes.join("\n"), "Doctor changes");
|
||||
candidate = toolsBySenderRepair.config;
|
||||
pendingChanges = true;
|
||||
cfg = toolsBySenderRepair.config;
|
||||
}
|
||||
|
||||
const safeBinProfileRepair = maybeRepairExecSafeBinProfiles(candidate);
|
||||
if (safeBinProfileRepair.changes.length > 0) {
|
||||
note(safeBinProfileRepair.changes.join("\n"), "Doctor changes");
|
||||
@@ -1035,6 +1159,20 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
|
||||
);
|
||||
}
|
||||
|
||||
const toolsBySenderHits = scanLegacyToolsBySenderKeys(candidate);
|
||||
if (toolsBySenderHits.length > 0) {
|
||||
const sample = toolsBySenderHits[0];
|
||||
const sampleLabel = sample ? `${sample.pathLabel}.${sample.key}` : "toolsBySender";
|
||||
note(
|
||||
[
|
||||
`- Found ${toolsBySenderHits.length} legacy untyped toolsBySender key${toolsBySenderHits.length === 1 ? "" : "s"} (for example ${sampleLabel}).`,
|
||||
"- Untyped sender keys are deprecated; use explicit prefixes (id:, e164:, username:, name:).",
|
||||
`- Run "${formatCliCommand("openclaw doctor --fix")}" to migrate legacy keys to typed id: entries.`,
|
||||
].join("\n"),
|
||||
"Doctor warnings",
|
||||
);
|
||||
}
|
||||
|
||||
const safeBinCoverage = scanExecSafeBinCoverage(candidate);
|
||||
if (safeBinCoverage.length > 0) {
|
||||
const interpreterHits = safeBinCoverage.filter((hit) => hit.isInterpreter);
|
||||
|
||||
Reference in New Issue
Block a user