| 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"); |
|
|
| |
| function stripNullBytes(s: string): string { |
| |
| return s.replace(/\0/g, ""); |
| } |
|
|
| export { resolveAgentIdFromSessionKey }; |
|
|
| type AgentEntry = NonNullable<NonNullable<OpenClawConfig["agents"]>["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<string>(); |
| 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) |
| ); |
| } |
|
|
| |
| 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; |
| } |
| |
| 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; |
| |
| |
| try { |
| normalized = fs.realpathSync.native(resolved); |
| } catch { |
| |
| } |
| 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"); |
| } |
|
|