Files
Moltbot/src/config/legacy.shared.ts
shayan919293 ab4adf7170 fix(macos): ensure exec approval prompt displays the command (#5042)
* fix(config): migrate audio.transcription with any CLI command

Two bugs fixed:
1. Removed CLI allowlist from mapLegacyAudioTranscription - the modern
   config format has no such restriction, so the allowlist only blocked
   legacy migration of valid configs like whisperx-transcribe.sh
2. Moved audio.transcription migration to a separate migration entry -
   it was nested inside routing.config-v2 which early-exited when no
   routing section existed

Closes #5017

* fix(macos): ensure exec approval prompt displays the command

The NSStackView and NSScrollView for the command text lacked proper
width constraints, causing the accessory view to collapse to zero
width in some cases. This fix:

1. Adds minimum width constraint (380px) to the root stack view
2. Adds minimum width constraint to the command scroll view
3. Enables vertical resizing and scrolling for long commands
4. Adds max height constraint to prevent excessively tall prompts

Closes #5038

* fix: validate legacy audio transcription migration input (openclaw#5042) thanks @shayan919293

* docs: add changelog note for legacy audio migration guard (openclaw#5042) thanks @shayan919293

* fix: satisfy lint on audio transcription migration braces (openclaw#5042) thanks @shayan919293

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-13 15:49:06 +01:00

130 lines
3.6 KiB
TypeScript

export type LegacyConfigRule = {
path: string[];
message: string;
match?: (value: unknown, root: Record<string, unknown>) => boolean;
};
export type LegacyConfigMigration = {
id: string;
describe: string;
apply: (raw: Record<string, unknown>, changes: string[]) => void;
};
import { isSafeExecutableValue } from "../infra/exec-safety.js";
import { isRecord } from "../utils.js";
export { isRecord };
export const getRecord = (value: unknown): Record<string, unknown> | null =>
isRecord(value) ? value : null;
export const ensureRecord = (
root: Record<string, unknown>,
key: string,
): Record<string, unknown> => {
const existing = root[key];
if (isRecord(existing)) {
return existing;
}
const next: Record<string, unknown> = {};
root[key] = next;
return next;
};
export const mergeMissing = (target: Record<string, unknown>, source: Record<string, unknown>) => {
for (const [key, value] of Object.entries(source)) {
if (value === undefined) {
continue;
}
const existing = target[key];
if (existing === undefined) {
target[key] = value;
continue;
}
if (isRecord(existing) && isRecord(value)) {
mergeMissing(existing, value);
}
}
};
export const mapLegacyAudioTranscription = (value: unknown): Record<string, unknown> | null => {
const transcriber = getRecord(value);
const command = Array.isArray(transcriber?.command) ? transcriber?.command : null;
if (!command || command.length === 0) {
return null;
}
if (typeof command[0] !== "string") {
return null;
}
if (!command.every((part) => typeof part === "string")) {
return null;
}
const rawExecutable = command[0].trim();
if (!rawExecutable) {
return null;
}
if (!isSafeExecutableValue(rawExecutable)) {
return null;
}
const args = command.slice(1);
const timeoutSeconds =
typeof transcriber?.timeoutSeconds === "number" ? transcriber?.timeoutSeconds : undefined;
const result: Record<string, unknown> = { command: rawExecutable, type: "cli" };
if (args.length > 0) {
result.args = args;
}
if (timeoutSeconds !== undefined) {
result.timeoutSeconds = timeoutSeconds;
}
return result;
};
export const getAgentsList = (agents: Record<string, unknown> | null) => {
const list = agents?.list;
return Array.isArray(list) ? list : [];
};
export const resolveDefaultAgentIdFromRaw = (raw: Record<string, unknown>) => {
const agents = getRecord(raw.agents);
const list = getAgentsList(agents);
const defaultEntry = list.find(
(entry): entry is { id: string } =>
isRecord(entry) &&
entry.default === true &&
typeof entry.id === "string" &&
entry.id.trim() !== "",
);
if (defaultEntry) {
return defaultEntry.id.trim();
}
const routing = getRecord(raw.routing);
const routingDefault =
typeof routing?.defaultAgentId === "string" ? routing.defaultAgentId.trim() : "";
if (routingDefault) {
return routingDefault;
}
const firstEntry = list.find(
(entry): entry is { id: string } =>
isRecord(entry) && typeof entry.id === "string" && entry.id.trim() !== "",
);
if (firstEntry) {
return firstEntry.id.trim();
}
return "main";
};
export const ensureAgentEntry = (list: unknown[], id: string): Record<string, unknown> => {
const normalized = id.trim();
const existing = list.find(
(entry): entry is Record<string, unknown> =>
isRecord(entry) && typeof entry.id === "string" && entry.id.trim() === normalized,
);
if (existing) {
return existing;
}
const created: Record<string, unknown> = { id: normalized };
list.push(created);
return created;
};