import type { OpenClawConfig } from "../config/config.js"; import type { CliBackendConfig } from "../config/types.js"; import { CLI_FRESH_WATCHDOG_DEFAULTS, CLI_RESUME_WATCHDOG_DEFAULTS, } from "./cli-watchdog-defaults.js"; import { normalizeProviderId } from "./model-selection.js"; export type ResolvedCliBackend = { id: string; config: CliBackendConfig; }; const CLAUDE_MODEL_ALIASES: Record = { opus: "opus", "opus-4.6": "opus", "opus-4.5": "opus", "opus-4": "opus", "claude-opus-4-6": "opus", "claude-opus-4-5": "opus", "claude-opus-4": "opus", sonnet: "sonnet", "sonnet-4.6": "sonnet", "sonnet-4.5": "sonnet", "sonnet-4.1": "sonnet", "sonnet-4.0": "sonnet", "claude-sonnet-4-6": "sonnet", "claude-sonnet-4-5": "sonnet", "claude-sonnet-4-1": "sonnet", "claude-sonnet-4-0": "sonnet", haiku: "haiku", "haiku-3.5": "haiku", "claude-haiku-3-5": "haiku", }; const CLAUDE_LEGACY_SKIP_PERMISSIONS_ARG = "--dangerously-skip-permissions"; const CLAUDE_PERMISSION_MODE_ARG = "--permission-mode"; const CLAUDE_BYPASS_PERMISSIONS_MODE = "bypassPermissions"; const DEFAULT_CLAUDE_BACKEND: CliBackendConfig = { command: "claude", args: ["-p", "--output-format", "json", "--permission-mode", "bypassPermissions"], resumeArgs: [ "-p", "--output-format", "json", "--permission-mode", "bypassPermissions", "--resume", "{sessionId}", ], output: "json", input: "arg", modelArg: "--model", modelAliases: CLAUDE_MODEL_ALIASES, sessionArg: "--session-id", sessionMode: "always", sessionIdFields: ["session_id", "sessionId", "conversation_id", "conversationId"], systemPromptArg: "--append-system-prompt", systemPromptMode: "append", systemPromptWhen: "first", clearEnv: ["ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY_OLD"], reliability: { watchdog: { fresh: { ...CLI_FRESH_WATCHDOG_DEFAULTS }, resume: { ...CLI_RESUME_WATCHDOG_DEFAULTS }, }, }, serialize: true, }; const DEFAULT_CODEX_BACKEND: CliBackendConfig = { command: "codex", args: ["exec", "--json", "--color", "never", "--sandbox", "read-only", "--skip-git-repo-check"], resumeArgs: [ "exec", "resume", "{sessionId}", "--color", "never", "--sandbox", "read-only", "--skip-git-repo-check", ], output: "jsonl", resumeOutput: "text", input: "arg", modelArg: "--model", sessionIdFields: ["thread_id"], sessionMode: "existing", imageArg: "--image", imageMode: "repeat", reliability: { watchdog: { fresh: { ...CLI_FRESH_WATCHDOG_DEFAULTS }, resume: { ...CLI_RESUME_WATCHDOG_DEFAULTS }, }, }, serialize: true, }; function normalizeBackendKey(key: string): string { return normalizeProviderId(key); } function pickBackendConfig( config: Record, normalizedId: string, ): CliBackendConfig | undefined { for (const [key, entry] of Object.entries(config)) { if (normalizeBackendKey(key) === normalizedId) { return entry; } } return undefined; } function mergeBackendConfig(base: CliBackendConfig, override?: CliBackendConfig): CliBackendConfig { if (!override) { return { ...base }; } const baseFresh = base.reliability?.watchdog?.fresh ?? {}; const baseResume = base.reliability?.watchdog?.resume ?? {}; const overrideFresh = override.reliability?.watchdog?.fresh ?? {}; const overrideResume = override.reliability?.watchdog?.resume ?? {}; return { ...base, ...override, args: override.args ?? base.args, env: { ...base.env, ...override.env }, modelAliases: { ...base.modelAliases, ...override.modelAliases }, clearEnv: Array.from(new Set([...(base.clearEnv ?? []), ...(override.clearEnv ?? [])])), sessionIdFields: override.sessionIdFields ?? base.sessionIdFields, sessionArgs: override.sessionArgs ?? base.sessionArgs, resumeArgs: override.resumeArgs ?? base.resumeArgs, reliability: { ...base.reliability, ...override.reliability, watchdog: { ...base.reliability?.watchdog, ...override.reliability?.watchdog, fresh: { ...baseFresh, ...overrideFresh, }, resume: { ...baseResume, ...overrideResume, }, }, }, }; } function normalizeClaudePermissionArgs(args?: string[]): string[] | undefined { if (!args) { return args; } const normalized: string[] = []; let sawLegacySkip = false; let hasPermissionMode = false; for (let i = 0; i < args.length; i += 1) { const arg = args[i]; if (arg === CLAUDE_LEGACY_SKIP_PERMISSIONS_ARG) { sawLegacySkip = true; continue; } if (arg === CLAUDE_PERMISSION_MODE_ARG) { hasPermissionMode = true; normalized.push(arg); const maybeValue = args[i + 1]; if (typeof maybeValue === "string") { normalized.push(maybeValue); i += 1; } continue; } if (arg.startsWith(`${CLAUDE_PERMISSION_MODE_ARG}=`)) { hasPermissionMode = true; } normalized.push(arg); } if (sawLegacySkip && !hasPermissionMode) { normalized.push(CLAUDE_PERMISSION_MODE_ARG, CLAUDE_BYPASS_PERMISSIONS_MODE); } return normalized; } function normalizeClaudeBackendConfig(config: CliBackendConfig): CliBackendConfig { return { ...config, args: normalizeClaudePermissionArgs(config.args), resumeArgs: normalizeClaudePermissionArgs(config.resumeArgs), }; } export function resolveCliBackendIds(cfg?: OpenClawConfig): Set { const ids = new Set([ normalizeBackendKey("claude-cli"), normalizeBackendKey("codex-cli"), ]); const configured = cfg?.agents?.defaults?.cliBackends ?? {}; for (const key of Object.keys(configured)) { ids.add(normalizeBackendKey(key)); } return ids; } export function resolveCliBackendConfig( provider: string, cfg?: OpenClawConfig, ): ResolvedCliBackend | null { const normalized = normalizeBackendKey(provider); const configured = cfg?.agents?.defaults?.cliBackends ?? {}; const override = pickBackendConfig(configured, normalized); if (normalized === "claude-cli") { const merged = mergeBackendConfig(DEFAULT_CLAUDE_BACKEND, override); const config = normalizeClaudeBackendConfig(merged); const command = config.command?.trim(); if (!command) { return null; } return { id: normalized, config: { ...config, command } }; } if (normalized === "codex-cli") { const merged = mergeBackendConfig(DEFAULT_CODEX_BACKEND, override); const command = merged.command?.trim(); if (!command) { return null; } return { id: normalized, config: { ...merged, command } }; } if (!override) { return null; } const command = override.command?.trim(); if (!command) { return null; } return { id: normalized, config: { ...override, command } }; }