diff --git a/CHANGELOG.md b/CHANGELOG.md index 829a5d85ec6..6b5c41be1d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai - Sessions/Agents: pass `agentId` through status and usage transcript-resolution paths (auto-reply, gateway usage APIs, and session cost/log loaders) so non-default agents can resolve absolute session files without path-validation failures. (#15103) Thanks @jalehman. - Signal/Install: auto-install `signal-cli` via Homebrew on non-x64 Linux architectures, avoiding x86_64 native binary `Exec format error` failures on arm64/arm hosts. (#15443) Thanks @jogvan-k. - Web tools/web_fetch: prefer `text/markdown` responses for Cloudflare Markdown for Agents, add `cf-markdown` extraction for markdown bodies, and redact fetched URLs in `x-markdown-tokens` debug logs to avoid leaking raw paths/query params. (#15376) Thanks @Yaxuan42. +- Config: keep legacy audio transcription migration strict by rejecting non-string/unsafe command tokens while still migrating valid custom script executables. (#5042) Thanks @shayan919293. ## 2026.2.12 diff --git a/apps/macos/Sources/OpenClaw/ExecApprovalsSocket.swift b/apps/macos/Sources/OpenClaw/ExecApprovalsSocket.swift index f6d88c48302..c87dd1e5884 100644 --- a/apps/macos/Sources/OpenClaw/ExecApprovalsSocket.swift +++ b/apps/macos/Sources/OpenClaw/ExecApprovalsSocket.swift @@ -242,6 +242,8 @@ enum ExecApprovalsPromptPresenter { stack.orientation = .vertical stack.spacing = 8 stack.alignment = .leading + stack.translatesAutoresizingMaskIntoConstraints = false + stack.widthAnchor.constraint(greaterThanOrEqualToConstant: 380).isActive = true let commandTitle = NSTextField(labelWithString: "Command") commandTitle.font = NSFont.boldSystemFont(ofSize: NSFont.systemFontSize) @@ -258,16 +260,19 @@ enum ExecApprovalsPromptPresenter { commandText.textContainer?.lineFragmentPadding = 0 commandText.textContainer?.widthTracksTextView = true commandText.isHorizontallyResizable = false - commandText.isVerticallyResizable = false + commandText.isVerticallyResizable = true let commandScroll = NSScrollView() commandScroll.borderType = .lineBorder - commandScroll.hasVerticalScroller = false + commandScroll.hasVerticalScroller = true commandScroll.hasHorizontalScroller = false + commandScroll.autohidesScrollers = true commandScroll.documentView = commandText commandScroll.translatesAutoresizingMaskIntoConstraints = false + commandScroll.widthAnchor.constraint(greaterThanOrEqualToConstant: 380).isActive = true commandScroll.widthAnchor.constraint(lessThanOrEqualToConstant: 440).isActive = true commandScroll.heightAnchor.constraint(greaterThanOrEqualToConstant: 56).isActive = true + commandScroll.heightAnchor.constraint(lessThanOrEqualToConstant: 120).isActive = true stack.addArrangedSubview(commandScroll) let contextTitle = NSTextField(labelWithString: "Context") diff --git a/src/config/config.legacy-config-detection.rejects-routing-allowfrom.e2e.test.ts b/src/config/config.legacy-config-detection.rejects-routing-allowfrom.e2e.test.ts index 1ef9bfb68f4..695239cf8e7 100644 --- a/src/config/config.legacy-config-detection.rejects-routing-allowfrom.e2e.test.ts +++ b/src/config/config.legacy-config-detection.rejects-routing-allowfrom.e2e.test.ts @@ -134,6 +134,45 @@ describe("legacy config detection", () => { }); expect(res.config?.routing).toBeUndefined(); }); + it("migrates audio.transcription with custom script names", async () => { + vi.resetModules(); + const { migrateLegacyConfig } = await import("./config.js"); + const res = migrateLegacyConfig({ + audio: { + transcription: { + command: ["/home/user/.scripts/whisperx-transcribe.sh"], + timeoutSeconds: 120, + }, + }, + }); + expect(res.changes).toContain("Moved audio.transcription → tools.media.audio.models."); + expect(res.config?.tools?.media?.audio).toEqual({ + enabled: true, + models: [ + { + command: "/home/user/.scripts/whisperx-transcribe.sh", + type: "cli", + timeoutSeconds: 120, + }, + ], + }); + expect(res.config?.audio).toBeUndefined(); + }); + it("rejects audio.transcription when command contains non-string parts", async () => { + vi.resetModules(); + const { migrateLegacyConfig } = await import("./config.js"); + const res = migrateLegacyConfig({ + audio: { + transcription: { + command: [{}], + timeoutSeconds: 120, + }, + }, + }); + expect(res.changes).toContain("Removed audio.transcription (invalid or empty command)."); + expect(res.config?.tools?.media?.audio).toBeUndefined(); + expect(res.config?.audio).toBeUndefined(); + }); it("migrates agent config into agents.defaults and tools", async () => { vi.resetModules(); const { migrateLegacyConfig } = await import("./config.js"); diff --git a/src/config/legacy.migrations.part-2.ts b/src/config/legacy.migrations.part-2.ts index c08020b146d..f9625856bb5 100644 --- a/src/config/legacy.migrations.part-2.ts +++ b/src/config/legacy.migrations.part-2.ts @@ -369,46 +369,53 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_2: LegacyConfigMigration[] = [ changes.push("Removed routing.transcribeAudio (tools.media.audio.models already set)."); } } else { - changes.push("Removed routing.transcribeAudio (unsupported transcription CLI)."); + changes.push("Removed routing.transcribeAudio (invalid or empty command)."); } delete routing.transcribeAudio; } - const audio = getRecord(raw.audio); - if (audio?.transcription !== undefined) { - const mapped = mapLegacyAudioTranscription(audio.transcription); - if (mapped) { - const tools = ensureRecord(raw, "tools"); - const media = ensureRecord(tools, "media"); - const mediaAudio = ensureRecord(media, "audio"); - const models = Array.isArray(mediaAudio.models) ? (mediaAudio.models as unknown[]) : []; - if (models.length === 0) { - mediaAudio.enabled = true; - mediaAudio.models = [mapped]; - changes.push("Moved audio.transcription → tools.media.audio.models."); - } else { - changes.push("Removed audio.transcription (tools.media.audio.models already set)."); - } - delete audio.transcription; - if (Object.keys(audio).length === 0) { - delete raw.audio; - } else { - raw.audio = audio; - } - } else { - delete audio.transcription; - changes.push("Removed audio.transcription (unsupported transcription CLI)."); - if (Object.keys(audio).length === 0) { - delete raw.audio; - } else { - raw.audio = audio; - } - } - } - if (Object.keys(routing).length === 0) { delete raw.routing; } }, }, + { + id: "audio.transcription-v2", + describe: "Move audio.transcription to tools.media.audio.models", + apply: (raw, changes) => { + const audio = getRecord(raw.audio); + if (audio?.transcription === undefined) { + return; + } + + const mapped = mapLegacyAudioTranscription(audio.transcription); + if (mapped) { + const tools = ensureRecord(raw, "tools"); + const media = ensureRecord(tools, "media"); + const mediaAudio = ensureRecord(media, "audio"); + const models = Array.isArray(mediaAudio.models) ? (mediaAudio.models as unknown[]) : []; + if (models.length === 0) { + mediaAudio.enabled = true; + mediaAudio.models = [mapped]; + changes.push("Moved audio.transcription → tools.media.audio.models."); + } else { + changes.push("Removed audio.transcription (tools.media.audio.models already set)."); + } + delete audio.transcription; + if (Object.keys(audio).length === 0) { + delete raw.audio; + } else { + raw.audio = audio; + } + } else { + delete audio.transcription; + changes.push("Removed audio.transcription (invalid or empty command)."); + if (Object.keys(audio).length === 0) { + delete raw.audio; + } else { + raw.audio = audio; + } + } + }, + }, ]; diff --git a/src/config/legacy.shared.ts b/src/config/legacy.shared.ts index 211e65459a0..3ffe911cff7 100644 --- a/src/config/legacy.shared.ts +++ b/src/config/legacy.shared.ts @@ -10,6 +10,7 @@ export type LegacyConfigMigration = { apply: (raw: Record, changes: string[]) => void; }; +import { isSafeExecutableValue } from "../infra/exec-safety.js"; import { isRecord } from "../utils.js"; export { isRecord }; @@ -45,24 +46,27 @@ export const mergeMissing = (target: Record, source: Record | null => { const transcriber = getRecord(value); const command = Array.isArray(transcriber?.command) ? transcriber?.command : null; if (!command || command.length === 0) { return null; } - const rawExecutable = String(command[0] ?? "").trim(); + 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; } - const executableName = rawExecutable.split(/[\\/]/).pop() ?? rawExecutable; - if (!AUDIO_TRANSCRIPTION_CLI_ALLOWLIST.has(executableName)) { + if (!isSafeExecutableValue(rawExecutable)) { return null; } - const args = command.slice(1).map((part) => String(part)); + const args = command.slice(1); const timeoutSeconds = typeof transcriber?.timeoutSeconds === "number" ? transcriber?.timeoutSeconds : undefined;