import fs from "node:fs"; import path from "node:path"; import { resolveAgentModelFallbackValues } from "../config/model-input.js"; import type { AgentDefaultsConfig } from "../config/types.agent-defaults.js"; import type { AgentModelConfig } from "../config/types.agents-shared.js"; import type { AgentConfig } from "../config/types.agents.js"; import type { OpenClawConfig } from "../config/types.js"; import { isPathInside } from "../infra/path-guards.js"; import { normalizeAgentId, parseAgentSessionKey, resolveAgentIdFromSessionKey, } from "../routing/session-key.js"; import { lowercasePreservingWhitespace, normalizeLowercaseStringOrEmpty, normalizeOptionalString, resolvePrimaryStringValue, } from "../shared/string-coerce.js"; import { resolveUserPath } from "../utils.js"; import { listAgentEntries, listAgentIds, resolveAgentConfig, resolveAgentContextLimits, resolveAgentDir, resolveDefaultAgentDir, resolveAgentWorkspaceDir, resolveDefaultAgentId, type ResolvedAgentConfig, } from "./agent-scope-config.js"; import { resolveEffectiveAgentSkillFilter } from "./skills/agent-filter.js"; export { listAgentEntries, listAgentIds, resolveAgentConfig, resolveAgentContextLimits, resolveAgentDir, resolveDefaultAgentDir, resolveAgentWorkspaceDir, resolveDefaultAgentId, type ResolvedAgentConfig, } from "./agent-scope-config.js"; /** Strip null bytes from paths to prevent ENOTDIR errors. */ function stripNullBytes(s: string): string { // eslint-disable-next-line no-control-regex return s.replace(/\0/g, ""); } export { resolveAgentIdFromSessionKey }; export function resolveSessionAgentIds(params: { sessionKey?: string; config?: OpenClawConfig; agentId?: string; }): { defaultAgentId: string; sessionAgentId: string; } { const defaultAgentId = resolveDefaultAgentId(params.config ?? {}); const explicitAgentIdRaw = normalizeLowercaseStringOrEmpty(params.agentId); const explicitAgentId = explicitAgentIdRaw ? normalizeAgentId(explicitAgentIdRaw) : null; const sessionKey = params.sessionKey?.trim(); const normalizedSessionKey = sessionKey ? normalizeLowercaseStringOrEmpty(sessionKey) : undefined; const parsed = normalizedSessionKey ? parseAgentSessionKey(normalizedSessionKey) : null; const sessionAgentId = explicitAgentId ?? (parsed?.agentId ? normalizeAgentId(parsed.agentId) : defaultAgentId); return { defaultAgentId, sessionAgentId }; } export function resolveSessionAgentId(params: { sessionKey?: string; config?: OpenClawConfig; }): string { return resolveSessionAgentIds(params).sessionAgentId; } export function resolveAgentExecutionContract( cfg: OpenClawConfig | undefined, agentId?: string | null, ): NonNullable["executionContract"]> | undefined { const defaultContract = cfg?.agents?.defaults?.embeddedPi?.executionContract; if (!cfg || !agentId) { return defaultContract; } const agentContract = resolveAgentConfig(cfg, agentId)?.embeddedPi?.executionContract; return agentContract ?? defaultContract; } export function resolveAgentSkillsFilter( cfg: OpenClawConfig, agentId: string, ): string[] | undefined { return resolveEffectiveAgentSkillFilter(cfg, agentId); } export function resolveAgentExplicitModelPrimary( cfg: OpenClawConfig, agentId: string, ): string | undefined { const raw = resolveAgentConfig(cfg, agentId)?.model; return resolvePrimaryStringValue(raw); } export function resolveAgentEffectiveModelPrimary( cfg: OpenClawConfig, agentId: string, ): string | undefined { return ( resolveAgentExplicitModelPrimary(cfg, agentId) ?? resolvePrimaryStringValue(cfg.agents?.defaults?.model) ); } function findMutableAgentEntry(cfg: OpenClawConfig, agentId: string): AgentConfig | undefined { const id = normalizeAgentId(agentId); return cfg.agents?.list?.find((entry) => normalizeAgentId(entry?.id) === id); } function updateAgentModelPrimary( existing: AgentModelConfig | undefined, primary: string, ): AgentModelConfig { if (existing && typeof existing === "object" && !Array.isArray(existing)) { return { ...existing, primary }; } return primary; } export type AgentModelPrimaryWriteTarget = "agent" | "defaults"; export function setAgentEffectiveModelPrimary( cfg: OpenClawConfig, agentId: string, primary: string, ): AgentModelPrimaryWriteTarget { const id = normalizeAgentId(agentId); if (resolveAgentExplicitModelPrimary(cfg, id)) { const entry = findMutableAgentEntry(cfg, id); if (entry) { entry.model = updateAgentModelPrimary(entry.model, primary); return "agent"; } } cfg.agents ??= {}; cfg.agents.defaults ??= {}; cfg.agents.defaults.model = updateAgentModelPrimary(cfg.agents.defaults.model, primary); return "defaults"; } /** @deprecated Prefer explicit/effective helpers at new call sites. */ export function resolveAgentModelPrimary(cfg: OpenClawConfig, agentId: string): string | undefined { return resolveAgentExplicitModelPrimary(cfg, agentId); } export function resolveAgentModelFallbacksOverride( cfg: OpenClawConfig, agentId: string, ): string[] | undefined { const raw = resolveAgentConfig(cfg, agentId)?.model; if (!raw) { return undefined; } if (typeof raw === "string") { return resolvePrimaryStringValue(raw) ? [] : undefined; } // Important: treat an explicitly provided empty array as an override to disable global fallbacks. if (!Object.hasOwn(raw, "fallbacks")) { return Object.hasOwn(raw, "primary") && resolvePrimaryStringValue(raw) ? [] : undefined; } return Array.isArray(raw.fallbacks) ? raw.fallbacks : undefined; } export function resolveFallbackAgentId(params: { agentId?: string | null; sessionKey?: string | null; }): string { const explicitAgentId = normalizeOptionalString(params.agentId) ?? ""; if (explicitAgentId) { return normalizeAgentId(explicitAgentId); } return resolveAgentIdFromSessionKey(params.sessionKey); } export function resolveRunModelFallbacksOverride(params: { cfg: OpenClawConfig | undefined; agentId?: string | null; sessionKey?: string | null; }): string[] | undefined { if (!params.cfg) { return undefined; } return resolveAgentModelFallbacksOverride( params.cfg, resolveFallbackAgentId({ agentId: params.agentId, sessionKey: params.sessionKey }), ); } export function hasConfiguredModelFallbacks(params: { cfg: OpenClawConfig | undefined; agentId?: string | null; sessionKey?: string | null; }): boolean { const fallbacksOverride = resolveRunModelFallbacksOverride(params); const defaultFallbacks = resolveAgentModelFallbackValues(params.cfg?.agents?.defaults?.model); return (fallbacksOverride ?? defaultFallbacks).length > 0; } export function resolveEffectiveModelFallbacks(params: { cfg: OpenClawConfig; agentId: string; hasSessionModelOverride: boolean; modelOverrideSource?: "auto" | "user"; }): string[] | undefined { const agentFallbacksOverride = resolveAgentModelFallbacksOverride(params.cfg, params.agentId); if (!params.hasSessionModelOverride) { return agentFallbacksOverride; } if (params.modelOverrideSource !== "auto") { return []; } const defaultFallbacks = resolveAgentModelFallbackValues(params.cfg.agents?.defaults?.model); return agentFallbacksOverride ?? defaultFallbacks; } function normalizePathForComparison(input: string): string { const resolved = path.resolve(stripNullBytes(resolveUserPath(input))); let normalized = resolved; // Prefer realpath when available to normalize aliases/symlinks (for example /tmp -> /private/tmp) // and canonical path case without forcing case-folding on case-sensitive macOS volumes. try { normalized = fs.realpathSync.native(resolved); } catch { // Keep lexical path for non-existent directories. } if (process.platform === "win32") { return lowercasePreservingWhitespace(normalized); } return normalized; } export function resolveAgentIdsByWorkspacePath( cfg: OpenClawConfig, workspacePath: string, ): string[] { const normalizedWorkspacePath = normalizePathForComparison(workspacePath); const ids = listAgentIds(cfg); const matches: Array<{ id: string; workspaceDir: string; order: number }> = []; for (let index = 0; index < ids.length; index += 1) { const id = ids[index]; const workspaceDir = normalizePathForComparison(resolveAgentWorkspaceDir(cfg, id)); if (!isPathInside(workspaceDir, normalizedWorkspacePath)) { continue; } matches.push({ id, workspaceDir, order: index }); } matches.sort((left, right) => { const workspaceLengthDelta = right.workspaceDir.length - left.workspaceDir.length; if (workspaceLengthDelta !== 0) { return workspaceLengthDelta; } return left.order - right.order; }); return matches.map((entry) => entry.id); } export function resolveAgentIdByWorkspacePath( cfg: OpenClawConfig, workspacePath: string, ): string | undefined { return resolveAgentIdsByWorkspacePath(cfg, workspacePath)[0]; }