import fs from "node:fs"; import path from "node:path"; import type { OpenClawConfig } from "../config/config.js"; import { resolveAgentModelFallbackValues } from "../config/model-input.js"; import { resolveStateDir } from "../config/paths.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { DEFAULT_AGENT_ID, normalizeAgentId, parseAgentSessionKey, resolveAgentIdFromSessionKey, } from "../routing/session-key.js"; import { resolveUserPath } from "../utils.js"; import { normalizeSkillFilter } from "./skills/filter.js"; import { resolveDefaultAgentWorkspaceDir } from "./workspace.js"; const log = createSubsystemLogger("agent-scope"); /** 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 }; type AgentEntry = NonNullable["list"]>[number]; type ResolvedAgentConfig = { name?: string; workspace?: string; agentDir?: string; model?: AgentEntry["model"]; skills?: AgentEntry["skills"]; memorySearch?: AgentEntry["memorySearch"]; humanDelay?: AgentEntry["humanDelay"]; heartbeat?: AgentEntry["heartbeat"]; identity?: AgentEntry["identity"]; groupChat?: AgentEntry["groupChat"]; subagents?: AgentEntry["subagents"]; sandbox?: AgentEntry["sandbox"]; tools?: AgentEntry["tools"]; }; let defaultAgentWarned = false; export function listAgentEntries(cfg: OpenClawConfig): AgentEntry[] { const list = cfg.agents?.list; if (!Array.isArray(list)) { return []; } return list.filter((entry): entry is AgentEntry => Boolean(entry && typeof entry === "object")); } export function listAgentIds(cfg: OpenClawConfig): string[] { const agents = listAgentEntries(cfg); if (agents.length === 0) { return [DEFAULT_AGENT_ID]; } const seen = new Set(); const ids: string[] = []; for (const entry of agents) { const id = normalizeAgentId(entry?.id); if (seen.has(id)) { continue; } seen.add(id); ids.push(id); } return ids.length > 0 ? ids : [DEFAULT_AGENT_ID]; } export function resolveDefaultAgentId(cfg: OpenClawConfig): string { const agents = listAgentEntries(cfg); if (agents.length === 0) { return DEFAULT_AGENT_ID; } const defaults = agents.filter((agent) => agent?.default); if (defaults.length > 1 && !defaultAgentWarned) { defaultAgentWarned = true; log.warn("Multiple agents marked default=true; using the first entry as default."); } const chosen = (defaults[0] ?? agents[0])?.id?.trim(); return normalizeAgentId(chosen || DEFAULT_AGENT_ID); } export function resolveSessionAgentIds(params: { sessionKey?: string; config?: OpenClawConfig; agentId?: string; }): { defaultAgentId: string; sessionAgentId: string; } { const defaultAgentId = resolveDefaultAgentId(params.config ?? {}); const explicitAgentIdRaw = typeof params.agentId === "string" ? params.agentId.trim().toLowerCase() : ""; const explicitAgentId = explicitAgentIdRaw ? normalizeAgentId(explicitAgentIdRaw) : null; const sessionKey = params.sessionKey?.trim(); const normalizedSessionKey = sessionKey ? sessionKey.toLowerCase() : 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; } function resolveAgentEntry(cfg: OpenClawConfig, agentId: string): AgentEntry | undefined { const id = normalizeAgentId(agentId); return listAgentEntries(cfg).find((entry) => normalizeAgentId(entry.id) === id); } export function resolveAgentConfig( cfg: OpenClawConfig, agentId: string, ): ResolvedAgentConfig | undefined { const id = normalizeAgentId(agentId); const entry = resolveAgentEntry(cfg, id); if (!entry) { return undefined; } return { name: typeof entry.name === "string" ? entry.name : undefined, workspace: typeof entry.workspace === "string" ? entry.workspace : undefined, agentDir: typeof entry.agentDir === "string" ? entry.agentDir : undefined, model: typeof entry.model === "string" || (entry.model && typeof entry.model === "object") ? entry.model : undefined, skills: Array.isArray(entry.skills) ? entry.skills : undefined, memorySearch: entry.memorySearch, humanDelay: entry.humanDelay, heartbeat: entry.heartbeat, identity: entry.identity, groupChat: entry.groupChat, subagents: typeof entry.subagents === "object" && entry.subagents ? entry.subagents : undefined, sandbox: entry.sandbox, tools: entry.tools, }; } export function resolveAgentSkillsFilter( cfg: OpenClawConfig, agentId: string, ): string[] | undefined { return normalizeSkillFilter(resolveAgentConfig(cfg, agentId)?.skills); } function resolveModelPrimary(raw: unknown): string | undefined { if (typeof raw === "string") { const trimmed = raw.trim(); return trimmed || undefined; } if (!raw || typeof raw !== "object") { return undefined; } const primary = (raw as { primary?: unknown }).primary; if (typeof primary !== "string") { return undefined; } const trimmed = primary.trim(); return trimmed || undefined; } export function resolveAgentExplicitModelPrimary( cfg: OpenClawConfig, agentId: string, ): string | undefined { const raw = resolveAgentConfig(cfg, agentId)?.model; return resolveModelPrimary(raw); } export function resolveAgentEffectiveModelPrimary( cfg: OpenClawConfig, agentId: string, ): string | undefined { return ( resolveAgentExplicitModelPrimary(cfg, agentId) ?? resolveModelPrimary(cfg.agents?.defaults?.model) ); } // Backward-compatible alias. 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 || typeof raw === "string") { return undefined; } // Important: treat an explicitly provided empty array as an override to disable global fallbacks. if (!Object.hasOwn(raw, "fallbacks")) { return undefined; } return Array.isArray(raw.fallbacks) ? raw.fallbacks : undefined; } export function resolveFallbackAgentId(params: { agentId?: string | null; sessionKey?: string | null; }): string { const explicitAgentId = typeof params.agentId === "string" ? params.agentId.trim() : ""; 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; }): string[] | undefined { const agentFallbacksOverride = resolveAgentModelFallbacksOverride(params.cfg, params.agentId); if (!params.hasSessionModelOverride) { return agentFallbacksOverride; } const defaultFallbacks = resolveAgentModelFallbackValues(params.cfg.agents?.defaults?.model); return agentFallbacksOverride ?? defaultFallbacks; } export function resolveAgentWorkspaceDir(cfg: OpenClawConfig, agentId: string) { const id = normalizeAgentId(agentId); const configured = resolveAgentConfig(cfg, id)?.workspace?.trim(); if (configured) { return stripNullBytes(resolveUserPath(configured)); } const defaultAgentId = resolveDefaultAgentId(cfg); if (id === defaultAgentId) { const fallback = cfg.agents?.defaults?.workspace?.trim(); if (fallback) { return stripNullBytes(resolveUserPath(fallback)); } return stripNullBytes(resolveDefaultAgentWorkspaceDir(process.env)); } const stateDir = resolveStateDir(process.env); return stripNullBytes(path.join(stateDir, `workspace-${id}`)); } 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 normalized.toLowerCase(); } return normalized; } function isPathWithinRoot(candidatePath: string, rootPath: string): boolean { const relative = path.relative(rootPath, candidatePath); return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); } 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 (!isPathWithinRoot(normalizedWorkspacePath, workspaceDir)) { 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]; } export function resolveAgentDir(cfg: OpenClawConfig, agentId: string) { const id = normalizeAgentId(agentId); const configured = resolveAgentConfig(cfg, id)?.agentDir?.trim(); if (configured) { return resolveUserPath(configured); } const root = resolveStateDir(process.env); return path.join(root, "agents", id, "agent"); }